An on-call ops connector is a small MCP server that lets whoever is on the rota ask their assistant "what is the error rate on checkout, and restart the stuck worker" and get a real answer and a real, bounded action. You build it with defineMcp: read-only tools that query status and metrics, plus a few write tools that run specific, safe runbook steps. The point is not autonomy. The point is that the dangerous parts, who can call it, what it can reach, and what it did, are constrained by the platform, not by hope. This is a pattern you assemble yourself, not a paging or incident product.
What goes in an ops connector?
Two kinds of tools, kept firmly separate. Read tools that answer questions, and a short list of write tools that each do one explicit, reversible thing. No tool named run_command. No free-form shell. Every action is a named runbook step you already trust a human to take.
import { defineMcp } from "@denkopsai/sdk/mcp";
import { z } from "zod";
import { fetchStatus, fetchErrorRate, restartWorker } from "./ops";
export default defineMcp({
name: "on-call-ops",
version: "1.0.0",
tools: {
service_status: {
description: "Current up/down status for a named service.",
input: z.object({ service: z.string() }),
handler: ({ service }) => fetchStatus(service),
},
error_rate: {
description: "5xx error rate for a service over the last N minutes.",
input: z.object({ service: z.string(), minutes: z.number().max(1440) }),
handler: ({ service, minutes }) => fetchErrorRate(service, minutes),
},
restart_worker: {
description: "Restart one named background worker. Safe, idempotent.",
input: z.object({ worker: z.enum(["email", "billing", "search"]) }),
handler: ({ worker }) => restartWorker(worker),
},
},
});Notice restart_worker takes an enum, not a free string. The assistant can restart one of three known workers and nothing else. That constraint lives in the schema, so an invalid or injected argument is rejected before your handler runs.
How do you keep the wrong person out?
Auth first, and by default. DenkOps apps require the app's key unless a path is made public, so there is no accidentally-open ops endpoint. For a connector you go further with OAuth: set "connector": true and restrict the project's access to an allowed-email list, so only the on-call rota gets past consent.
{ "name": "on-call-ops", "slug": "on-call-ops", "runtime": "bun", "connector": true }The hosted OAuth flow (discovery, registration, login, consent) comes with the flag, described in "connector: true" is a full OAuth stack. Add the six people on rota to the allowed-email list in the Access tab. Everyone else is refused at login, before a single tool runs. When the rotation changes, edit the list. If you run this as a team, workspace roles and the audit log record who changed access and when.
How do you bound what the connector can touch?
Two boundaries, both structural.
- Egress. Your ops backend calls a metrics API and an internal control endpoint, and nothing else. Zero-trust egress means outbound requests only reach hosts on an allowlist you set. A restart tool cannot be talked into calling an attacker's URL, because that host is not on the list. The request fails and shows up in your logs.
- The audit trail. Every OAuth grant is auditable, and you log each write action yourself: who called
restart_worker, which worker, and when. That log is the difference between "someone restarted billing at 2am" and a shrug.
Add observability on top and the connector also answers questions with its own numbers. The code versus I/O trace split tells the on-call engineer whether a slow service is your code or a dependency, and per-project usage metering shows load over the incident window. The assistant becomes a faster way to run the runbook you already have, inside a box the platform holds shut.