ADR-0012: The trust boundary for user-managed MCP connections
Status: Accepted Date: 2026-07-15 Owner: Founder / Architecture Executes into: PRD-006, v1.3 roadmap
1. Context
Section titled “1. Context”wovyr-tools::mcp (RM-AIM-P3 ECO-301, shipped 2026-07-14) lets Wovyr connect to an
external MCP server and proxy its tools into a ToolRegistry. It is
programmatic only — no persisted connection, no API, no UI. PRD-006
scopes making it a first-class, dashboard-managed capability. Doing that safely
requires deciding, up front, how much trust an MCP connection carries — because
the two transports McpClient already supports are not equally dangerous:
StdioTransportspawns an arbitrary local process (program,args) and speaks JSON-RPC over its stdin/stdout. Registering aStdioconnection is choosing to execute an arbitrary local command with the platform process’s own privileges — verified directly during PRD-006’s scoping work (npx -y @modelcontextprotocol/server-filesystem <dir>, a real child process, no sandboxing at the transport layer at all).HttpTransportPOSTs JSON-RPC to a URL. No local execution, but a URL an operator or tenant supplies is exactly the shape of every SSRF vector this codebase has already had to close once, forhttp_get(RM-GA-P1 SEC-304): loopback/link-local/private/cloud-metadata addresses, DNS rebinding between the resolve and the connect.
This codebase already has a precedent for exactly the Stdio risk: the shell
tool is not in ToolRegistry::with_builtins() — it requires an explicit,
operator-only opt-in (with_shell()/WOVYR_ENABLE_SHELL_TOOL, SEC-301) precisely
because handing every agent arbitrary command execution by default is not a
decision a hosted deployment should make silently. A dashboard “Add MCP Server”
button that lets any authenticated user type a shell command and have it run
server-side would quietly reintroduce the exact risk SEC-301 already closed —
just through a different door.
2. Decision
Section titled “2. Decision”- A
Stdio-transport connection is exactly as privileged as theshelltool, and is gated identically. It requires:- An explicit operator opt-in at the deployment level
(
WOVYR_ENABLE_MCP_STDIO=1), refused otherwise regardless of caller — the same shape asWOVYR_ENABLE_SHELL_TOOL. A tenant cannot reach this on their own no matter what role they hold; only the operator’s own process environment can. - A distinct RBAC tier above ordinary write access:
mcp:admin, notmcp:write— mirroring thekms:write/kms:adminsplit already in this codebase for “routine write” vs. “materially higher blast radius.” Creating, editing, or refreshing aStdioconnection requiresmcp:admin; anHttpconnection only requiresmcp:write.
- An explicit operator opt-in at the deployment level
(
- An
Http-transport connection reuseshttp_get’s SEC-304 SSRF guard verbatim — the sameresolve_and_guard/DNS-pinned-client mechanism, extracted to a shared helper rather than re-derived. Refused at registration time and at every subsequent call, identically tohttp_gettoday. No second, possibly-divergent SSRF implementation. - A connection’s credential is a secret reference, never a value. If an
Httpconnection needs an auth header, the connection record stores aSecretRefintowovyr-secrets’sVault, tenant-scoped exactly like every other secret in this codebase. The connection store itself is never an acceptable place for a raw credential to live, encrypted or not — it already has a durable, tenant-scoped, audited place to live. - No sandboxing of a
Stdioconnection’s spawned process in v1 — stated as a residual risk, not silently assumed away.Stdioconnections run with the platform process’s own OS privileges, unconfined by the sandbox spectrum (NativeSandbox/ContainerSandbox/WasiSandbox/etc.) that other dynamic-code paths in this codebase already have. Gate 1 above (operator opt-in +mcp:admin) is v1’s entire mitigation for this — it controls who can create such a connection, not how contained it runs once created. A sandboxed variant (spawn insideContainerSandboxthe waycode_executealready can) is a named, explicit follow-on, not a commitment made by this ADR. - The wire protocol and registry-proxy mechanics are unmodified.
McpClient,StdioTransport,HttpTransport, and theToolproxy thatregister_intoproduces do not change. Everything above is enforced in the new management layer PRD-006 scopes (the connection store, the API routes, the agent-manifest resolution step) — never insidewovyr-tools::mcpitself.
3. Consequences
Section titled “3. Consequences”Positive
- Every genuinely new risk this feature introduces (arbitrary local execution, SSRF, credential storage) is closed by reusing a mechanism this codebase has already built, tested, and shipped for a structurally identical problem — no novel security design, no new class of bug to discover the hard way.
- The
Stdio/Httpdistinction is legible to an operator at decision time: the dashboard can (and per PRD-006 MCX-302, must) show why theStdiooption is unavailable rather than silently hiding or, worse, silently allowing it. - A future sandboxed-
Stdioslice has a clear, already-anticipated landing spot (the existingSandboxBackend/TrustClassspectrum) rather than needing its own design from scratch.
Negative / accepted costs
Stdioconnections stay a genuinely unsandboxed capability in v1 — a deployment that enablesWOVYR_ENABLE_MCP_STDIOis accepting that anymcp:admin-scoped principal can execute arbitrary local commands, exactly as they already accept for theshelltool. This is a real, standing risk, not a solved one — it is scoped down to “who,” not eliminated.- Two RBAC tiers for one feature (
mcp:writevsmcp:admin, split by transport) is more nuanced than a singlemcp:writegate would be — the dashboard and docs must explain this distinction clearly or it will surprise an operator expecting one uniform “MCP” permission. - Extracting SEC-304’s guard into a shared helper touches
wovyr-tools’ existing, already-testedhttp_getcode path — a real, if small, refactor risk against working code, to be done under the existinghttp_getSSRF test suite, not a rewrite.
4. Alternatives Considered
Section titled “4. Alternatives Considered”- One uniform
mcp:writescope for both transports — rejected: it would either under-gateStdio(treating remote-code-execution risk the same as “point at a URL”) or over-gateHttp(making a harmless remote-lookup connection require admin rights for no reason). The split costs a small amount of documentation clarity for a real reduction in blast radius. - Sandbox
Stdioconnections from day one (e.g., always spawn insideContainerSandbox) — rejected for v1: real added complexity (container runtime dependency, egress-lockdown wiring, cross-platform story) for a feature whose primary near-term use case (PRD-006 UC1: a docs-directory filesystem server) doesn’t need it to be useful, and the existingshell-tool precedent already accepts unsandboxed-but-gated as the interim stance. Revisit if a design partner’s real workload needs it. - Ban
Stdiotransport entirely for v1, shipHttp-only — rejected: a large share of real-world MCP servers (filesystem, git, local dev tools) are stdio-only; shipping without it would make the feature far less useful for the exact “connect a local tool” use case that motivates PRD-006, and theshell-tool precedent shows this codebase already has a workable gating pattern rather than needing to avoid the risk outright. - Store connection credentials inline, encrypted with a connection-specific
key — rejected:
wovyr-secrets’sVaultalready solves tenant-scoped credential storage, rotation, and at-rest encryption; a second, bespoke encryption scheme for one feature’s credentials would be duplicated, untested machinery solving an already-solved problem.
5. Current Status (2026-07-15)
Section titled “5. Current Status (2026-07-15)”Accepted; no code exists yet. PRD-006 defines the requirements this decision
gates (MCX-102/103/104/105); the v1.3 roadmap phases them. Nothing in this ADR
changes any shipped contract — wovyr-tools::mcp (ECO-301) is unmodified by
this decision, only wrapped by a new management layer that has not been built.