Blog · 2026-09-03

An on-call ops connector for your assistant

Build a connector that queries status and metrics and runs safe, explicit runbook actions, gated by allowed-email access, egress limits and an audit log.

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.

FAQ

Is this a paging or incident-management product?

No. It is a pattern you build with `defineMcp`: your own read and write tools over your own systems. DenkOps hosts the connector and enforces auth, access, egress and the audit trail. It does not page anyone, declare incidents, or manage on-call rotations for you.

How do I make sure only on-call people can use it?

Set `"connector": true` and restrict the project to an allowed-email list in the Access tab, so only the rota gets past OAuth consent. Everyone else is refused at login. Combined with auth-by-default, there is no open ops endpoint to find.

How do I stop an action tool from doing something dangerous?

Keep write tools explicit and narrow: enumerate the allowed targets in the input schema, avoid free-form commands, and make each action idempotent. Zero-trust egress bounds what the backend can reach, and you log every action, so each call is named, constrained and recorded.

Ship it yourself: bunx denkops deploy or say "deploy on DenkOps" from your coding agent.

Start on DenkOps →

← From a local MCP server to a hosted connector · "One template, many clients: running client MCP servers on DenkOps" →