Skip to main content
Glama
mariuszbyahoo

ops-copilot-mcp

README.md
# Ops-Copilot MCP Server

A self-hosted [MCP](https://modelcontextprotocol.io) server (TypeScript / Node.js) that gives
an AI agent (Claude Code, Claude Desktop) a **governed** interface to local infrastructure.
MVP scope: **Docker only**.

The point is not "an agent can call Docker" — it's the **control model**.

## The control model

- **Read freely.** The agent lists containers and reads logs with no ceremony — reads have
  no side effects.
- **Mutate only with a human-confirmed token.** A state-changing tool (`restart_container`)
  uses a **two-phase confirm** protocol: the first call only *previews* and returns a
  single-use token; nothing changes until a second call supplies that token.
- **Deny-by-default.** Operations are explicitly classified as read or mutate. Anything not
  classified is refused — capability is opt-in, not opt-out.
- **Append-only JSONL audit log.** Every invocation — read, preview, allow, deny — is written
  as one JSON line. This is the *primary*, incident-safe record, not a mirror of some other
  store. Each line is independently valid, so a crash mid-write can never corrupt history.

This is the portfolio story: not raw capability, but *governed* capability.

### The confirm-token protocol

```
Agent ──restart_container{container}──────────────▶  Phase 1: preview
                                                     • resolve target (id-prefix or name)
                                                     • issue single-use token, TTL 120s,
                                                       bound to (operation, target)
                                                     • audit: preview
        ◀──"About to restart web-1 (a1b2c3)…       • NO side effect
            confirmToken=9f3a… within 120s"

Agent ──restart_container{container, confirmToken}─▶  Phase 2: execute
                                                     • token must exist, be unexpired,
                                                       unused, and match (op, target) exactly
                                                     • on success → restart, audit: allowed
                                                     • else → refuse, audit: denied
```

The token comes from `crypto.randomBytes(16)`, lives only in memory, is deleted on consume
(single-use), and is bound to an exact `(operation, target)` pair — a token issued to restart
container A can never restart container B, and a reused or expired token is refused with no
side effect.

## Tools

| Tool                 | Type   | Input                                          | Behavior |
|----------------------|--------|------------------------------------------------|----------|
| `ping`               | read   | —                                              | Health check; returns `pong`. |
| `list_containers`    | read   | `{ all?: boolean = false }`                    | Compact text table; running only, or all incl. stopped. |
| `get_container_logs` | read   | `{ container: string, tail?: number = 100 }`   | Last N log lines; resolve by id-prefix or exact name. |
| `restart_container`  | mutate | `{ container: string, confirmToken?: string }` | Two-phase confirm (see above). |

Tool outputs are short, fixed-width, and explicit about errors (prefixed `Error:` with
`isError: true`) so the LLM routes reliably on them.

## Running locally

Prerequisites: Node.js LTS (>= 20) and a running Docker daemon (Docker Desktop on
Windows/macOS, or the socket on Linux).

```bash
npm install
npm run dev        # tsx src/index.ts — runs the server over stdio
npm run build      # tsc -> dist/
npm start          # node dist/index.js — runs the compiled build
npm test           # vitest — unit tests for the confirm-token store
```

The server speaks MCP over **stdio**: stdout is the JSON-RPC protocol channel, and **all logs
go to stderr**. You normally don't run it by hand — an MCP client (Claude Code) launches it.

The audit log is written to `./audit/audit-<yyyyMMddUTC>.jsonl` by default; override the
directory with the `AUDIT_DIR` environment variable. Files roll per UTC day.

### Point Claude Code at it

A project-scoped [`.mcp.json`](./.mcp.json) at the repo root registers this server:

```json
{
  "mcpServers": {
    "ops-copilot-mcp": {
      "command": "npx",
      "args": ["tsx", "src/index.ts"]
    }
  }
}
```

Claude Code launches the command from the project directory. To use the compiled build
instead of `tsx`, run `npm run build` first and change the entry to:

```json
{ "command": "node", "args": ["dist/index.js"] }
```

Then:

1. From this project directory, start Claude Code.
2. Approve the project MCP server when prompted (or run `/mcp` to inspect it).
3. Confirm the tools are listed. Try: *"list all containers"*, then *"restart X"* — the agent
   will preview and hand you a token before anything changes.

If the server **won't connect**, the cause is almost always **stdout pollution** — a stray
`console.log` corrupts the protocol stream. Every log line must go to stderr (`console.error`).

## Architecture

```
MCP Tools      src/tools/*.ts      thin registerTool wrappers; shape text, write audit; no logic
   │
Policy         src/policy/*.ts     deny-by-default classification; issues/consumes confirm tokens
   │
Adapter        src/adapters/*.ts   the only code that talks to Docker (dockerode); plain types out
   │
Audit          src/audit/*.ts      append-only JSONL sink; called at every decision point
```

Dependency rule: tools know Policy/Adapter/Audit; the adapter knows only `dockerode`;
Policy/Audit know nothing of MCP or Docker. SDK types live only in `src/index.ts` and
`src/tools/*`. Dependencies are constructed by hand in `src/index.ts` and injected — no
container, no decorators. This keeps the core unit-testable with no transport and makes the
future stdio→HTTP swap a one-file change.

## Security note

The stdio transport **inherits the trust of the local user** who launches the server. There is
no network listener, no authentication layer, and no sandbox: the server runs with your OS
permissions and talks to your Docker daemon over its local socket / named pipe — which on most
setups is equivalent to root on the host. The confirm-token protocol is a guardrail against an
agent acting *without human intent*; it is **not** a security boundary against a hostile
operator or hostile code already running as you. Run it only on infrastructure you own, keep
the audit log, and treat the future HTTP transport (which *does* cross a trust boundary) as
requiring real authentication before exposure.

## Roadmap (post-MVP — NOT yet built)

The following are deliberately out of scope for the MVP and are **not implemented**:

- **Streamable HTTP transport**, config-switched (`TRANSPORT=stdio|http`). Today: stdio only.
- **More adapters** — GitHub (PRs, workflow runs), Traefik (routers, health).
- **Postgres as a queryable mirror** of the JSONL audit log (the JSONL file stays the primary
  store; Postgres would only be a read-optimized projection).
- **Scoped policy** per operation/target patterns, and richer preview diffs.
```

TDQS

A4.4/5.0

Scored across 4 tools

Disambiguation5/5

Each tool has a clearly distinct purpose: ping for health, list_containers for inventory, get_container_logs for log retrieval, and restart_container for a mutating operation. There is no overlap or ambiguity between them.

Naming Consistency4/5

Tools use lowercase snake_case with a verb_noun pattern (list_containers, get_container_logs, restart_container). The exception is 'ping', which is a single verb but is a conventional health-check name and does not detract from overall consistency.

Tool Count5/5

Four tools form a well-scoped set for a Docker operations copilot, covering health, listing, logs, and restart. The count is appropriate for the narrow domain and each tool earns its place.

Completeness4/5

The core monitoring and restart workflow is covered, but there are minor gaps such as no explicit start/stop container controls. Agents can work around this given the focus on restart and diagnostics.

Maintenance

ActivityStale
ResponsivenessNo issues