ADR-0165: MCP servers hosted in the Navin Manager and isolation per project of the MCP gateway
Context
Most MCP servers of the Navin library are local commands (stdio: npx, uvx). The MCP gateway of the Control Plane only speaks the streamable HTTP transport: an infrastructure must host those commands, expose them over HTTP and isolate them per tenant and project. In the platform, an MCP server attached to a project also had to be usable from that project only.
Decision
- Runtime
navin-mcp-host(navin-manager/mcp-host, Python standard library only): one child process per server, started without a shell, clean environment (nothing inherited from the host), working folder<state_dir>/<tenant>/<project>/<name>in 0700, memory, file and core limits, stop after idle time, restart after a crash and failing state beyond 5 crashes per minute. Each server is exposed over streamable HTTP at/t/{tenant}/p/{project}/s/{name}/mcpwith its own token (only the sha256 digest is on the host); the token of another server or project gets 401 with no hint about the server. The allowed tools are enforced on the host too./healthzand/metrics(Prometheus) serve monitoring. - Catalogue service
mcp-hostof the Manager: installation, single and replicated modes (a server is placed on one instance by its id), preflight checks (npx,uvx). The MCP page of the Manager hosts, removes and renews the token of a stdio server of a project ("..." menu); the Manager writesservers.jsonon the instance, reloaded live by the runtime. - Publication: the Manager lists the hosted servers of a tenant (permission
mcp:publish, admin role only). POST/v1/mcp/hosted/syncof the Control Plane publishes them in their project: token kept in the Credential Broker (openbao://mcp/hosted/<id>), server of the project created or updated (hosted_ref, V198), servers no longer hosted disabled. No answer and no audit event holds a token. The address of the mcp-host instances must be named inEAI_EGRESS_PRIVATE_ALLOWLIST. - Isolation per project in the MCP gateway: a server attached to a project is listed and called only from that project (
X-Projectheader or an API key bound to the project). From another project the gateway answers a JSON-RPC errormcp_server_project_mismatchwith no upstream call, and the tools, discovery, status, catalogue and delete routes answer 403; every refusal is audited. A server without a project stays shared by the tenant; tenant_admin and security_officer without a project context keep the view of every project.
Alternatives considered
- Third party proxies (mcp-proxy, supergateway): one shared process with no clean environment nor token per server, and one more dependency to maintain on the host.
- Running the stdio commands in the Control Plane: would mix third party code with the process that holds the authorisations of the platform.
Consequences
A stdio server of a project becomes usable by the agents of that project only, without a secret leaving the Manager or the Credential Broker. The Manager must be linked to the platform (EAI_PLATFORM_MANAGER_URL, EAI_PLATFORM_MANAGER_TOKEN_REF).
Evidence
navin-manager/mcp-host/tests, navin-manager/server/tests/test_mcp_hosting.py, services/control-plane/tests/test_mcp_project_isolation.py, services/control-plane/tests/test_mcp_hosted_sync.py, portal/tests/navin-library.test.ts.