---
title: "Ops server"
description: "Let a coding agent ask a running app how it is, what it has seen and what a playbook would do, through nine MCP tools on a user-only socket, with every change waiting for a person."
section: "interfaces"
status: "Building for 0.4 · ships as a beta API"
spec: "ops-mcp"
canonical: "https://mainsdk.dev/docs/interfaces/ops"
updated: "2026-10-28"
---

# Ops server

> Let a coding agent ask a running app how it is, what it has seen and what a playbook would do, through nine MCP tools on a user-only socket, with every change waiting for a person.

Status: Building for 0.4 · ships as a beta API · Canonical: https://mainsdk.dev/docs/interfaces/ops · Updated: 2026-10-28

A running app can answer an MCP client such as Claude Code about its health, signals and playbooks. Anything that changes the app waits for a person to say yes.

The agent can also ask what a playbook would do now and why a reaction ran. The server listens only on this machine, can't call arbitrary actions, change config or read app data, and every call acts as the app agent `ops`, so its budget, kill switch and audit apply.

## At a glance

- Status: Building for 0.4 · ships as a beta API
- Phase: 1 (Core, public beta)
- Milestone: 0.4 Hardening
- Plan: free in your app (Apache-2.0)
- Spec: https://github.com/hubxero/mainsdk/blob/develop/docs/specs/ops-mcp.md
- Category: AI and agents

Calls:

- `new OpsServer(opts)`
- `parseOps(section, env)`
- `opsIssues(section, ctx)`
- `OPS_TOOL_NAMES(const: the nine tool names)`

In main.yaml:

```yaml
ops:
  mcp: { write: approve }
  agent: ops
```

Guarantees, whatever the driver:

- Nothing leaves the machine
- No tool calls an app action, changes config or reads app data
- A run or revert always waits for a person; an agent never approves


## Drivers

Pick one in main.yaml with `ops.driver`.

| Driver | Id in main.yaml | Default | Note |
| --- | --- | --- | --- |
| Built in (a user-only socket and a token file) | `builtin` | yes |  |

## Codes

| Code | Where | Meaning |
| --- | --- | --- |
| `MAIN-OPS-0001` | check | An `ops` value is invalid: `write: auto`, a port outside 1024 to 65535, or `http: on` in production without `ops.agent` declared under `agents` |
| `MAIN-OPS-0002` | runtime |  |
| `MAIN-OPS-0004` | runtime |  |
| `MAIN-OPS-0005` | runtime | `mainsdk ops mcp` found no running app with an ops socket for the given folder or project |
| `MAIN-OPS-0006` | runtime |  |

## Features

| Feature | Phase | Milestone | Spec ticket |
| --- | --- | --- | --- |
| Read tools | 1 | 0.4 | `ops-mcp#tools-read` |
| Approval-only run and revert | 1 | 0.4 | `ops-mcp#tools-write` |
| mainsdk ops mcp | 1 | 0.4 | `ops-mcp#stdio` |

## MainSDK Cloud

Everything on this page is free in your app.


## Quick start

```yaml title="main.yaml"
ops:
  mcp: { write: approve }
agents:
  ops: { budget: { calls: 500/day } }
```

```bash
npx mainsdk dev          # the ops socket is on by default in dev
npx mainsdk ops mcp      # the stdio MCP server an agent starts; mainsdk init registers it
```

## Configuration

| Key | What it does |
| --- | --- |
| `ops.mcp.socket` | `on` or `off`. Default on in dev, off elsewhere. |
| `ops.mcp.write` | `approve` (default) or `off`, which makes the write tools unavailable (`MAIN-OPS-0004`). There is no `auto`. |
| `ops.mcp.socket-ports` | The loopback range used only where a Unix socket is refused. Default `47600-47699`. |
| `ops.agent` | The app agent clients act as. Default `ops`. |

| `ops.mcp.http` | `on` serves MCP over HTTP at `http://127.0.0.1:<port>/mcp` and, on a host with IPv6 loopback, `[::1]`. Default off. |
| `ops.mcp.port` | The loopback HTTP port, 1024 to 65535. Default `4950`. |
| `ops.card` | `on` (default) serves the agent card at `/.well-known/agent-card.json` on the HTTP endpoint and as the resource `mainsdk://ops/agent-card`. |

The HTTP endpoint takes `Authorization: Bearer <token>` with the token in `<run>/ops/<project>.token`, and refuses a request with no token (401, `MAIN-OPS-0002`) or with a `Host` or `Origin` that is not loopback (403, `MAIN-OPS-0003`). It never goes through the app's public adapter, so the public app does not serve the agent card.

## What you get

The app opens `<run>/ops/<project-id>.sock` (mode 0600 in a 0700 folder) and writes a fresh token file beside it at each start. A client presents the token first; a missing or wrong one is refused with `MAIN-OPS-0002`. The socket is on by default only when the environment is `dev`. Messages are JSON-RPC 2.0, one per line. Tool descriptions are signed with the token, so a client can spot a changed description.

| Tool | Risk | What it does |
| --- | --- | --- |
| `ops.status` | read | State, health, resources, posture, active runs and signal counts |
| `ops.signals` | read | A page of recent signals |
| `ops.norms` | read | What normal looks like for each metric |
| `ops.playbooks` | read | The declared rules with step classes and modes |
| `ops.reactions` | read | Runs with their overlays and expiries |
| `ops.plan` | read | What a rule would change now and the guard's verdict; changes nothing |
| `ops.explain` | read | Why a run happened: trigger, steps, guard, approval, rollback |
| `ops.run` | write | Files a run that waits for a person |
| `ops.revert` | write | Files a revert that waits for a person |

Nothing in an answer holds a tenant id, user id, config value or secret. `ops.run` always ends `waiting`, whatever the rule's `run` mode, and asks the person at the client to read the plan; a yes there is recorded in the audit and never approves. A person approves with `mainsdk playbooks approve <run>`, and an agent that tries gets `MAIN-AGENT-0003`. A rule with a step that is never a reaction is refused with `MAIN-GUARD-0002`.

## Helpers for wiring and tests

| Call | What it does |
| --- | --- |
| `new OpsServer(opts)` | The ops server of one app instance, for tests and custom wiring. |
| `parseOps(section, env)` | Read the `ops` section of `main.yaml` for an environment. |
| `opsIssues(section, ctx)` | List what is wrong in the `ops` section, without throwing. |
| `OPS_TOOL_NAMES(const)` | The names of the nine ops tools. |

## Errors and checks

- `MAIN-OPS-0001`: an `ops` value is invalid (`mainsdk check`).
- `MAIN-OPS-0002`: no token or a wrong one.
- `MAIN-OPS-0004`: a write tool with `write: off`, or a name outside the nine tools.
- `MAIN-OPS-0005`: `mainsdk ops mcp` found no running app.
- `MAIN-OPS-0006`: the ops agent's budget is spent.

See [check codes](https://mainsdk.dev/docs/reference/check-codes).

## Swapping drivers

`ops` has no drivers. It is a server over the app's own state, and the tools answer the same whatever drivers the app uses.

## Testing

`new OpsServer(opts)` builds the server of one app instance for tests, and the testing kit's MCP client in `@mainsdk/testing` talks to it against a fixture app. Use `parseOps` and `opsIssues` to check the `ops` section without throwing. See [Test your app](https://mainsdk.dev/docs/guides/testing).

## Next

- [Playbooks](https://mainsdk.dev/docs/interfaces/playbooks)
- [Signals](https://mainsdk.dev/docs/interfaces/signals)
- [Agents](https://mainsdk.dev/docs/interfaces/agents)
- [Operate your app with an agent](https://mainsdk.dev/docs/guides/operate-your-app-with-an-agent)
