Skip to content

MCP servers

IronClaw can extend an agent with the tools of a Model Context Protocol (MCP) server — local (a stdio subprocess) or remote (a streamable‑HTTP endpoint) — without weakening the sandbox. The reference design that IronClaw was built to harden wired MCP with a blind approval surface ("approve this server" → it brings whatever tools it likes). IronClaw closes that gap: a human approves a named server and a named set of tools, every call is gated and audited, and the MCP server never runs inside — or is reachable from — the agent sandbox.

This is opt‑in. With no --mcp-catalog the daemon exposes no MCP surface at all and a sandbox can never reach one.

The security model in one picture

   AGENT SANDBOX (network=none, no runtime, read-only rootfs)
        │  only endpoint: a per-session unix socket
        │  GET /tools        POST /call            ← a plain HTTP shim, never MCP
   ┌──────────────────────────── HOST ───────────────────────────────┐
   │  MCP BROKER (the choke point)                                    │
   │   • per-SESSION socket = the trusted identity (not a header)     │
   │   • deny-by-default: every list/call checked vs the gateway-     │
   │     approved grant for that session's agent group               │
   │   • audits every op (server, tool, status, bytes, duration)      │
   │   • expands ${ENV} secrets here, never logs them                 │
   │        │ speaks real MCP (JSON-RPC 2.0)                           │
   │        ├── LOCAL  → a hardened container (network=none, ro,       │
   │        │            non-root, dropped caps) running the stdio     │
   │        │            server  (untrusted code is isolated)         │
   │        └── REMOTE → the endpoint over HTTPS (TLS required)        │
   └──────────────────────────────────────────────────────────────────┘

Properties:

  • Not directly from the sandbox. The sandbox holds no MCP client, no network, and no credentials. Its only MCP endpoint is the host broker socket; everything else is the broker's job.
  • Gateway‑approved, not blind. Granting an agent access to a server + tools is a ChangeMCPAccess capability change. It passes a deterministic verifier (the server must be configured) and then the mandatory human‑approval floor — it shows up in Approvals like any other capability change.
  • Deny‑by‑default per call. Even after a grant, the broker refuses any tool the grant does not name and any tool the server does not declare. A revoked grant stops working immediately (grants are resolved live, per call).
  • Audited. Every list and call emits an audit record (session, server, tool, status, bytes, duration) to the daemon's structured logs — never the arguments or credential values.
  • Encrypted in transit. Remote servers require https (plain http is allowed only for a loopback host, for local testing). The sandbox↔broker hop is a host‑local unix socket — no network, nothing on the wire.
  • Isolated. A local server is third‑party code, so by default it runs in a hardened, network=none container (Docker, optionally with the gVisor runtime), not as a bare host process.
  • Secrets stay host‑side. Write ${ENV_VAR} in a server's env/headers; the broker expands it from the host environment at connect time. The catalog never stores the raw value, the API masks it, and it never reaches the agent.

Enabling MCP

Start the control plane with a catalog path (and, in production, container isolation):

ic-controlplane \
  --mcp-catalog   /var/lib/ironclaw/mcp-catalog.json \
  --mcp-isolation container \
  --mcp-runtime   runsc \        # optional: run local servers under gVisor
  --mcp-image     ironclaw-mcp:latest   # default image for local servers

--dev enables MCP automatically with a catalog under the state dir and --mcp-isolation=none (a dev box may have no container runtime) — it logs a warning that local servers then run unisolated. Never use none in production.

Flag Meaning
--mcp-catalog PATH enables MCP; the 0600 JSON file of configured servers
--mcp-isolation container\|none how local (stdio) servers run; container is the default
--mcp-runtime NAME OCI runtime for isolation, passed as docker --runtime (e.g. runsc)
--mcp-image REF default container image for local servers with no image set

Adding a server (web console → MCP tab)

  1. Name it (github, files, …) and pick Local (stdio) or Remote (HTTP).
  2. Local: give the Command and Arguments (e.g. npx / -y @modelcontextprotocol/server-github), and optionally an isolation Image and Environment (GITHUB_TOKEN=${GITHUB_TOKEN}). Remote: give the https URL and an optional Auth header (Bearer ${GITHUB_TOKEN}).
  3. Save server. This is operator infrastructure config — it grants no agent anything yet.
  4. Discover tools & grant on the server's card connects to it, lists its tools, and lets you pick a subset + an agent. Request grant submits the approval.
  5. Approve it in Approvals. On the agent's next launch the broker exposes exactly those tools as ‹server›__‹tool› (e.g. github__create_issue).

…or via the API

# configure a local server
curl -X PUT $API/v1/registry/mcp-servers/files \
  -d '{"transport":"stdio","command":"mcp-files","args":["--root","/data"]}'

# configure a remote server (secrets as ${ENV} references)
curl -X PUT $API/v1/registry/mcp-servers/github \
  -d '{"transport":"http","url":"https://mcp.example.com/rpc",
       "headers":{"Authorization":"Bearer ${GITHUB_TOKEN}"}}'

# discover its tools
curl -X POST $API/v1/registry/mcp-servers/github/probe

# grant an agent a named subset (-> Approvals -> approve)
curl -X POST $API/v1/ui/config/change \
  -d '{"kind":"mcp_access","agentGroupID":"team-a","requestedBy":"you",
       "after":{"server":"github","tools":["create_issue","list_issues"]}}'

An empty tools array grants every tool the server declares (the human approves the server wholesale). The broker still refuses any tool the server does not actually expose.

What the agent sees

The agent gets ordinary tools named ‹server›__‹tool› with the server's own JSON schema. Calling one forwards to the broker; a policy denial or an upstream error comes back as a tool error the model can read. An agent that wants a server it does not have can ask for it with the request_capability_change tool (kind mcp_access) — which is still just a request a human must approve.

Trying it without a real server

cmd/mcp-sample is a tiny, credential‑free MCP server (tools echo and add) that runs over stdio or HTTP:

mcp-sample                 # stdio  → configure as a local server (command = its path)
mcp-sample --http :9000    # HTTP   → configure as a remote server (url = http://127.0.0.1:9000)

It is the fixture the end‑to‑end tests use and a safe first server to wire through the whole flow.

Notes & limits

  • A local server runs network=none. A server that genuinely needs the internet is better modeled as a remote server the host dials over TLS.
  • The broker shares one upstream connection per server across sessions; per‑session isolation is enforced at the tool surface (each session sees only its grants).
  • Removing a server from the catalog does not revoke existing grants (that is a separate gateway change); the broker simply can no longer reach it.

See threat-model.md for the STRIDE treatment and contract.md (RFC‑0005) for the one frozen‑contract value MCP adds.