Skip to content

MCP servers

Subspace speaks the Model Context Protocol (MCP): each MCP server is a page in the mcp/ directory, an agent binds the servers it needs, and their tools appear to the model as mcp.<server>.<tool> with the server’s real JSON Schema. Binding is opt-in and per agent, the toolset is snapshotted and journaled at run start, and a trust note lets you review a server’s tools before you ever bind it, because server-provided tool descriptions are a prompt-injection surface.

A mcp/ page carries a metadata.mcp block. There is one page per server:

---
mcp:
transport: { http: https://mcp.example.com/v1 }
headers: { Authorization: EXAMPLE_MCP_TOKEN }
trust: { search: always, delete_record: never }
---
# example-tools
Field Meaning
transport { http: url } for a remote server, or { stdio: 'command args…' } to spawn a local process.
headers HTTP header name to ops.secrets name. Resolved at connect time, never stored in the page.
env Stdio child env var to ops.secrets name. The child sees only these plus PATH.
trust Per-tool gate override, { tool: 'always' | 'never' }. Default is always (gated).
enabled Set false to suspend the server. The toggle is authoritative.

Secrets resolve through the capability broker from ops.secrets at connect time, so rotating a token is a row update.

Binding is opt-in. A definition lists servers in mcp: string[]; absent means none. This is deliberately stricter than skills (which default to all): an agent reaches an external server only when its definition names it.

---
agent:
mcp: [example-tools]
tools: [kb.search, "mcp.example-tools.search"] # allowlist applies over the union
---

At run start the bound servers’ toolsets are snapshotted into the journaled definition message (failure-tolerant per server, so one dead server does not sink the run) and registered for that run as mcp.<server>.<tool>, passing the server’s real JSON Schema to the model. The definition’s tools allowlist, if present, applies over the union of built-in and MCP tools.

Connections are process state, not run state. Clients reconnect lazily per call with secrets resolved at connect time, and connections are cached. A dead server yields a tool-result error string, never a run failure, so a flaky server degrades gracefully. Every MCP call takes an effect claim keyed on the call id (mcp.<server>.<tool>:<callId>) because third-party tools cannot be assumed idempotent: a crash mid-call refuses the silent re-run.

Server-provided tool names and descriptions are prompt-injection surface: they reach the model, so you should read them before binding. Every mcp/ page renders an MCP server · trust note panel above its outline.

  1. List served tools

    Click list served tools. This connects to the server on that explicit click only, never on page open, and shows each mcp.<server>.<tool> with its description.

  2. Review the gate state

    Each tool shows whether it is approval-gated or ungated (from trust), so you see exactly what a bound agent could do without further approval.

  3. Then bind

    Only after review do you add the server to a definition’s mcp: list. Review before first bind is the whole point of the panel.

/settings/mcp lists every readable mcp/ page plus any SUBSPACE_MCP_SERVERS env entries that no page shadows, each with a source badge (repo-managed, invalid, disabled).

  • New server creates an mcp/ page from a stdio command or an http url.
  • The per-server checkbox toggles metadata.mcp.enabled.
  • Both go through agents.mcpServerSave and are admin-only: a stdio entry is a command the host executes, so it shares the trust posture of os.exec. Changes ride the ordinary audited command path, so GitOps-managed pages reject as read-only and env entries cannot be edited here.
  • Per-server list served tools reuses the trust-note flow (agents.mcpTools, connecting only on the click); a disabled server still lists for review-before-enable.
  • Clicking a page-sourced server name opens its mcp/ page for headers, env, and trust edits.