MusePi

MCP runtime lifecycle

This document describes how MCP servers are discovered, connected, exposed as tools, refreshed, and torn down in the coding-agent runtime.

Lifecycle at a glance

  1. SDK startup kicks off MCP discovery (unless MCP is disabled): headless/SDK sessions await discoverAndLoadMCPTools(); interactive sessions (hasUI: true) create the manager up front and defer discoverAndConnect() until the session is live.
  2. Discovery (loadAllMCPConfigs) resolves MCP server configs from capability sources, filters disabled/project/Exa entries and browser MCP servers when the built-in browser tool is enabled, and preserves source metadata.
  3. Manager connect phase (MCPManager.connectServers) starts per-server connect + tools/list in parallel.
  4. Fast startup gate waits up to 250ms, then may return:
    • fully loaded MCPTools,
    • failures per server,
    • or cached DeferredMCPTools for still-pending servers.
  5. SDK wiring merges MCP tools into runtime tool registry for the session.
  6. Post-connect enrichment best-effort loads resources, resource templates, prompts, and optional resource subscriptions.
  7. Live session can refresh MCP tools via /mcp flows (disconnectAll + rediscover + session.refreshMCPTools) and can reconnect individual servers on transport close or /mcp reconnect.
  8. Teardown happens when callers invoke disconnectServer/disconnectAll; manager also clears MCP tool/resource/prompt registrations for disconnected servers.

Discovery and load phase

Entry path from SDK

createAgentSession() in src/sdk.ts performs MCP startup when enableMCP is true (default). There are two paths:

Both paths:

If enableMCP is false, MCP discovery is skipped entirely.

Config discovery and filtering

loadAllMCPConfigs() (src/mcp/config.ts) loads canonical MCP server items through capability discovery, then converts to legacy MCPServerConfig.

Filtering behavior:

Result includes both configs and sources (metadata used later for provider labeling).

Discovery-level failure behavior

discoverAndLoadMCPTools() distinguishes two failure classes:

So startup does not fail the whole agent session when individual MCP servers fail.

Manager state model

MCPManager tracks runtime lifecycle with separate registries:

getConnectionStatus(name) derives status from these maps:

Connection establishment and startup timing

Per-server connect pipeline

For each discovered server in connectServers():

  1. store/update source metadata,
  2. skip if already connected/pending/reconnecting,
  3. validate transport fields (validateServerConfig),
  4. resolve auth/shell substitutions (#resolveAuthConfig),
  5. call connectToServer(name, resolvedConfig) with manager notification/request handlers,
  6. wire HTTP OAuth refresh and transport onClose reconnect handling,
  7. call listTools(connection),
  8. cache tool definitions (MCPToolCache.set) best-effort,
  9. best-effort load resources, resource templates, prompts, and subscriptions after tools load.

connectToServer() behavior (src/mcp/client.ts):

Fast startup gate + deferred fallback

connectServers() waits on a race between:

After 250ms:

This is a hybrid startup model: fast return with deferred handles when cache is available, late background registration when it is not.

Background completion behavior

Each pending toolsPromise also has a background continuation that eventually:

Tool exposure and live-session availability

Startup registration

discoverAndLoadMCPTools() converts manager tools into LoadedCustomTool[] and decorates paths (mcp:<server> via <providerName> when known).

createAgentSession() then pushes these tools into customTools, which are wrapped and added to the runtime tool registry with names like mcp__<server>_<tool>.

Tool calls

Both return structured tool output and convert remaining transport/tool errors into MCP error: ... tool content (abort remains abort).

Refresh/reload paths (startup vs live reload)

Initial startup path

Interactive reload path

/mcp reload path (src/modes/controllers/mcp-command-controller.ts) does:

  1. mcpManager.disconnectAll(),
  2. mcpManager.discoverAndConnect(),
  3. session.refreshMCPTools(mcpManager.getTools()).

session.refreshMCPTools() (src/session/agent-session.ts) removes all mcp__ tools, re-wraps latest MCP tools, and re-activates tool set so MCP changes apply without restarting session.

There is also a follow-up path for late connections: after waiting for a specific server, if status becomes connected, it re-runs session.refreshMCPTools(...) so newly available tools are rebound in-session.

Server-initiated notifications

MCP servers may push JSON-RPC notification frames at any point after initialize completes. The transport surfaces them via onNotification; the manager fans them out in two paths:

  1. Internal refresh for known methods:
    • notifications/tools/list_changedrefreshServerTools
    • notifications/resources/list_changedrefreshServerResources
    • notifications/resources/updated#onResourcesChanged (only for currently subscribed URIs)
    • notifications/prompts/list_changedrefreshServerPrompts
  2. Listener fanout: every notification (including the known ones AND server-custom methods) is delivered to registered listeners AFTER the internal refresh runs. Registered via MCPManager.addNotificationListener(listener), which returns an unsubscribe function. Multiple listeners are supported; each is invoked with independent error isolation — a synchronous throw in one listener does not prevent others from firing (thrown errors are logged at debug).

sdk.ts registers one listener that bridges to the extension runner’s mcp_notification event, so extensions receive every server-initiated frame with { server, method, params }. The listener is captured with postmortem so it is released on session teardown.

Health, reconnect, and partial failure behavior

Current runtime behavior is connection-event driven:

Operationally:

Teardown semantics

Server-level teardown

disconnectServer(name):

Global teardown

disconnectAll():

In current wiring, explicit teardown is used in MCP command flows (for reload/remove/disable). Startup stores the manager on the session; callers that need deterministic MCP shutdown should invoke manager disconnect methods.

Failure modes and guarantees

Scenario Behavior Hard fail vs best-effort
Discovery throws (capability/config load path) Loader returns empty tools + synthetic .mcp.json error Best-effort session startup
Invalid server config Server skipped with validation error entry Best-effort per server
Connect timeout/init failure Server error recorded; others continue Best-effort per server
tools/list still pending at startup with cache hit Deferred tools returned immediately Best-effort fast startup
tools/list still pending at startup without cache No tools at startup; background continuation registers them via #onToolsChanged when ready Best-effort late registration
Late background tool-load failure Logged after startup gate Best-effort logging
Runtime dropped transport Manager attempts reconnect; stale tools remain while reconnecting and future calls may retry once or fail with MCP errors Best-effort automatic recovery

Public API surface

src/mcp/index.ts re-exports loader/manager/client APIs for external callers. src/sdk.ts exposes discoverMCPServers() as a convenience wrapper returning the same loader result shape.

Implementation files