Skip to main content
Mount an existing external MCP server so its tools appear in your running server. The runtime mounts other MCP servers and re-presents their tools as its own. You declare each server in the manifest’s mcp section; the runtime probes it at startup and binds its tools alongside your local ones.

Declare a server in the manifest

Each mcp entry names one server. Set exactly one transport per entry — the runtime infers the transport from whichever field you set:
  • url for a remote HTTP or SSE server (with optional headers),
  • uds for a Unix domain socket,
  • command (plus args / env) for a stdio server the runtime launches.
manifest.yml
The official image carries the package runners uvx and npx, so a stdio server launched as uvx <package> or npx <package> starts on the image as it is; the runner fetches the package on its first use. A command with a directory part is launched as given. A bare command is found at its first executable match, in this order:
  1. the entry’s own env.PATH, when it sets one;
  2. a command an installed mcp-server package exposes under exactly that name — the console scripts its own package declares, run from that package’s own environment;
  3. the runtime’s PATH, only when the entry’s env sets no PATH.
A bare command with no executable match fails the server’s start with a message that names the PATH searched and the exposed command names, and any file of that name that is not executable. The server is launched with its env exactly as the entry declares it, plus a short safe-list of the runtime’s own environment (HOME, PATH, USER, …); nothing else is added, and a PATH the entry sets replaces the inherited one. A marketplace mcp-server plugin that ships a package names one of its own console scripts as its command: the install builds the package into its own environment and refuses, with a 409, a command the package does not declare or that another installed server package already exposes. Use include / exclude on an entry to expose only a subset of the mounted server’s tools.

Inspect and reload

1

Check binding status

Snapshot the live MCP binding state — which servers attached, which were skipped by a failed viability probe, and each attached server’s recent dispatch health.
The payload carries three blocks: bound maps each attached server’s title to the tool names it serves, failed lists the servers a viability probe skipped — each entry carrying the server title, a coarse status, a category (auth, unreachable for a transport error or a start that outran the start budget, starting for a start still in progress when the caller stopped waiting, which binds when the start completes, preset_store when the preset store could not be read while the server’s tools were being bound, or error), a redacted message, and the http_status the failure carried (null for a pure transport failure) — and health reports each server’s most recent tool-dispatch outcome, keyed by title (a skipped server carries the block at its empty values).
Each health entry has four fields, detailed in the health block reference below.
2

List a server's tools

List every tool a server advertised when it was bound, before the entry’s selection, with whether the entry picks it and whether it is bound.
The command reads GET /api/mcp-status/{title}/advertised-tools, a read-tier route: the answering worker answers from the listing it bound the server from, and never starts the server or connects to it. Each row gives the tool’s served name, whether the selection picks it (selected), whether the live binding carries it (bound), and, for a picked tool that is not bound, its unbound_reason: preset_conflict when a preset owns its name, unusable_schema when its input schema cannot be adapted, or pending_bind when no bind has applied the entry’s current selection yet (a bind is in progress, or the next reload binds it). A name the entry’s include (or else exclude) list carries that the server no longer advertises follows as a not advertised row (unadvertised in the JSON).With no listing held, the read refuses: 503 with code mcp-server-starting while the server’s start is in progress, 503 with code mcp-server-not-loaded when the answering worker has not loaded the server, and 502 with the recorded cause when its last start or bind failed. An unknown title is a 404.
3

Replace the set at runtime

Write the full replacement mcp list to a JSON file, then apply it. This hot-reloads the section without restarting the server. set remains the bootstrap and restore path — use it to lay down the whole section at once.
4

Add or remove one entry

Change a single server without rewriting the whole list. tai mcp add appends the entries in a JSON file — one entry object, a bare array, or an object carrying an "entries" list — and refuses a title that already exists unless you pass --replace, which swaps the existing entry in place. tai mcp remove deletes one entry by its title. Both hot-reload the section.
remove is a persisted change — it edits the manifest, so the server stays gone across a restart. deregister is a runtime-only detach — it drops the server’s tools from the live registry but leaves the manifest untouched, so the next reload brings it back.
5

Reload one server

Re-probe a single server by its manifest title and re-attach it if it is viable. A successful reload also drops that server’s pooled dispatch session, so the next tool call opens a fresh connection instead of reusing a stale one. A reload that finds the server unreachable reports it unavailable and leaves the session as is — once the server is back, the next tool call reconnects on its own; while it stays down, calls return the structured error described below.
A server that fails its viability probe is skipped, not fatal — the rest of the manifest still loads. Inspect skipped servers under tai mcp status.

The health block

Each health entry has four fields: health covers every bound and failed title. A server that has never been called carries the block at its empty values — last_success, last_error, and failing_since null, consecutive_failures 0. A server that has only ever succeeded carries last_success with last_error and failing_since null. An auth-blocked dispatch records nothing: it signals credential state, not connection health, so it leaves the health block untouched. The health is this worker’s own passive view — per process, so a fleet report reads each worker’s dispatches separately.

When a mounted server’s tools fail

A tool call against a mounted server whose connection has dropped gets one automatic reconnect attempt. If the reconnect succeeds the call runs on the fresh session. If the server is still unavailable, the call returns a structured error whose code is upstream_mcp_unavailable, with a message that names the entry by its configured title. The raw disconnect detail never reaches that message — it is logged at the dispatch seam, not surfaced to the caller. A dispatch that fails for another reason — a transport error, a timeout, or an output-schema mismatch — returns a structured error whose code is mcp_dispatch_failed, naming the tool and the server rather than a generic 500; the raw detail is logged and recorded on that server’s health entry. A mounted entry that carries a connection’s reference but whose url is not the server its provider defines for that sub-service is refused before any credential is sent: the call fails with a message that names the entry and both repairs. Reconnecting the account (or, for a provider without sign-in, saving the connection’s services again) rewrites the entry the connector wrote for that connection. Any other entry that carries the reference is not the connector’s own, so a reconnect leaves it as it is and it keeps being refused until the reference is removed from it.

See also