Hosting MCP servers
This page explains where an MCP server runs once it is used by the agents of a project, how the platform isolates it per project, and how to start, stop, upgrade and delete it. It is written for developers and tenant administrators; the installation of the hosting services themselves belongs to the operators and is linked at the end.
Three ways to host a server
The MCP gateway of the Control Plane reaches internal addresses only (ADR-0170). Every server is therefore hosted in one of three places.
| Hosting | What runs | Who operates it | Isolation | Entry in the gateway |
|---|---|---|---|---|
| Internal host of the company | An HTTP MCP server the company develops and runs | The team that owns the server | The host named in EAI_MCP_INTERNAL_HOSTS (exact host, host:port, *.suffix or CIDR) | Registered with POST /v1/mcp/servers, attached to a project or shared by the tenant |
Navin Manager, service mcp-host | A stdio command (npx, uvx) exposed over streamable HTTP | The tenant administrator, from the MCP page of the Navin Manager | One child process per server and per project, clean environment, own folder in mode 0700, own token | Published by POST /v1/mcp/hosted/sync in its project |
| MCP Runtime (ToolHive Operator) | A container image of the company MCP catalogue | Each user, from the portal | One instance per user, project and template, in a namespace per project | Created when the instance is ready, callable by its owner and its agents only |
The MCP Runtime is described in MCP Runtime per user and project. This page covers the first two and the rules shared by all three.
An online MCP service is never called. A preset of the Navin library marked hosting: online (portal badge
"Online service: internal address required") is installed only with the internal address where the company hosts
it.
Isolation per project
A server attached to a project is listed and called only from that project (ADR-0165):
- The project of a request is the
X-Projectheader, or the project an API key is bound to. - From another project, GET /v1/mcp/servers/{server_id}/tools, the discovery, the status, the catalogue and the
deletion answer 403
mcp_server_project_mismatch, and the gateway answers a JSON-RPC error with the same code, without calling the server. Every refusal is audited. - A request without a project context is refused (
project_context_required), except fortenant_adminandsecurity_officer, who keep the view of every project. - A server without a project is shared by the tenant and visible from every project.
- Inside a project, PUT /v1/mcp/servers/{server_id}/access narrows the callers to a list of users and agents.
| Request | Servers listed by GET /v1/mcp/servers |
|---|---|
| With a project context | The servers of that project and the servers shared by the tenant |
With ?project=<id-or-slug> | The servers of that project only (403 mcp_project_forbidden when it is not the request project) |
Tenant wide (tenant_admin, security_officer) | Every server of the tenant |
Host a server on an internal host
-
Deploy the server inside the network of the company. Use the streamable HTTP or the SSE transport and an address whose host the operator lists in
EAI_MCP_INTERNAL_HOSTS. Every address the name resolves to must be private: a name that resolves to a public address is refused at connect time.Expected result: the server answers
initializefrom the network of the Control Plane.If it fails: 403
mcp_online_forbiddenat registration or call; ask the operator to list the internal host. -
Register it in its project. Follow MCP Development Guide: POST /v1/mcp/servers with
projectand anopenbao://credential reference.Expected result: 201, the server is listed in its project only.
If it fails: 403
mcp_project_forbiddenwhen the body names a project other than the request project. -
Discover and approve its tools. POST /v1/mcp/servers/{server_id}/discover, then approve the versions.
Expected result: GET /v1/mcp/catalog shows the tools as
approved.If it fails: 502
upstream_error; check that the server answers tools/list.
Host a stdio server in the Navin Manager
The service mcp-host of the Navin Manager (runtime navin-mcp-host, navin-manager/mcp-host) starts each stdio
server of a project in its own child process, without a shell, with memory, file and core limits, stops it after
its idle time and restarts it after a crash (failed beyond 5 crashes per minute). Each server is exposed at
/t/{tenant}/p/{project}/s/{name}/mcp with its own token; the host keeps only the sha256 digest of the token and
answers the same 401 to any wrong token. It starts the servers offline (npm, uv and pip without download).
-
Install the service
mcp-host(operator). The operator installs it from the catalogue of the Navin Manager, in single or replicated mode, and lists the address of its instances inEAI_EGRESS_PRIVATE_ALLOWLISTof the Control Plane. See MCP hosting service.Expected result: the preflight checks (
npx,uvx) pass and the service is running.If it fails: follow the troubleshooting of the service page.
-
Host the server of the project. On the MCP page of the Navin Manager, choose the tenant, the project and the preset or the command, enter the secrets (sealed by the Manager) and host it.
Expected result: the Manager writes
servers.jsonon the instance, the runtime reloads it and the server answers.If it fails: the connection test of the page shows the error of the child process.
-
Publish it in the project. In the portal, Navin library > MCP > Sync the servers hosted by the Navin Manager (
tenant_admin), or POST /v1/mcp/hosted/sync with?project=<slug>.client.mcp.sync_hosted(project="alpha")Expected result: the answer lists
created,updated,disabledandskipped. The token of each server is kept in the Credential Broker atopenbao://mcp/hosted/<id>, never in a table nor an answer; the server of the project gets a bearer authentication, its allowed tools and itshosted_ref.If it fails:
skippedgives a reason per entry (other_tenant,invalid_id,invalid_name,invalid_url,token_missing,project_not_found,not_active,project_moved); 403mcp_project_forbiddenwhenprojectdiffers from the request project. -
Discover and approve its tools. As for any server.
Expected result: the agents of the project call it through the gateway.
If it fails: see MCP Development Guide.
Lifecycle
| Operation | Internal host | Navin Manager (mcp-host) | MCP Runtime |
|---|---|---|---|
| Start | The owner team starts the server; register it once | Host it on the MCP page of the Manager, then sync | POST /v1/projects/{project_id}/mcp/instances, then any call wakes it |
| Stop for a while | POST /v1/mcp/servers/{server_id}/status disabled (applied at once, audited) | Remove or disable it in the Manager, then sync: the server is disabled | POST /v1/projects/{project_id}/mcp/instances/{instance_id}/stop |
| Start again | POST /v1/mcp/servers/{server_id}/status active (promotion request, 202) | Host it again, then sync: the server becomes active | POST /v1/projects/{project_id}/mcp/instances/{instance_id}/start, or the next call |
| Upgrade | Deploy the new build at the same URL, then discover again | Change the command or the version in the Manager (the process restarts, sessions are invalidated), sync, discover again | Publish a new image in the company catalogue; the next generation takes it |
| Renew the credential | Write the new secret at the same openbao:// reference | "..." menu of the Manager, renew the token, then sync | Every generation gets new credentials |
| Delete | DELETE /v1/mcp/servers/{server_id} (tenant_admin) | Remove it in the Manager, then sync (the entry is disabled) | DELETE /v1/projects/{project_id}/mcp/instances/{instance_id} |
Disabling a server cuts every agent of the project that uses it. Enabling it again goes through a promotion request that a second person approves.
Screens and routes
| Screen | Where | Routes |
|---|---|---|
| MCP studio | Portal, studio/mcp | POST /v1/mcp/servers, POST /v1/mcp/servers/{server_id}/discover, GET /v1/mcp/catalog |
| MCP servers | Portal, capabilities/mcp | GET /v1/mcp/servers, GET /v1/mcp/servers/{server_id}/tools, POST /v1/mcp/servers/{server_id}/status, DELETE /v1/mcp/servers/{server_id}, POST /v1/mcp/gateway/{server_id} |
| Navin library > MCP | Portal, studio/navin-library | POST /v1/projects/{project_id}/navin-library/mcp-presets/{name}/install, POST /v1/mcp/hosted/sync, PUT /v1/mcp/servers/{server_id}/access |
| Navin library > MCP instances | Portal, studio/navin-library | GET /v1/projects/{project_id}/mcp/instances and the instance routes |
| MCP page | Navin Manager | Hosting, token renewal and connection test of the stdio servers |
| MCP runtime page | Navin Manager | Operator state, instances, destruction and restart |
Verify
- GET /v1/mcp/servers sent with the
X-Projectof the project lists the server; sent with another project it does not. - A call from another project answers the JSON-RPC error
mcp_server_project_mismatchand the audit holds the refusal.
Next steps
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
403 mcp_online_forbidden | Host public or not listed as internal | Host the server internally and list its host |
403 mcp_server_project_mismatch | Call from another project or without a project context | Send the X-Project of the project of the server |
Sync answers skipped with project_not_found | The project slug of the Manager does not exist in the tenant | Create the project or host the server in an existing project |
Sync answers skipped with project_moved | The hosted server was published in another project before | Remove it in the Manager and host it again in the right project |
Hosted server disabled after a sync | The Manager no longer hosts it | Host it again, then sync |
409 mcp_preset_needs_endpoint at preset installation | stdio preset without HTTP address | Host it in the Navin Manager, then give its URL or sync |