MusePi

MCP server and tool authoring

This document explains how MCP server definitions become callable mcp__* tools in coding-agent, and what operators should expect when configs are invalid, duplicated, disabled, or auth-gated.

Architecture at a glance

Config sources (.musepi/.claude/.cursor/.vscode/mcp.json, mcp.json, etc.)
  -> discovery providers normalize to canonical MCPServer
  -> capability loader dedupes by server name (higher provider priority wins)
  -> loadAllMCPConfigs converts to MCPServerConfig + skips enabled:false
  -> MCPManager connects/listTools (with auth/header/env resolution)
  -> manager best-effort loads resources/prompts and subscribes to resource updates when enabled
  -> MCPTool/DeferredMCPTool bridge exposes tools as mcp__<server>_<tool>
  -> AgentSession.refreshMCPTools replaces live MCP tools immediately

1) Server config model and validation

src/mcp/types.ts defines the authoring shape used by MCP config writers and runtime:

validateServerConfig() (src/mcp/config.ts) enforces transport basics:

config-writer.ts applies this validation for add/update operations and also validates server names:

Transport pitfalls

2) Discovery, normalization, and precedence

Capability-based discovery

loadAllMCPConfigs() (src/mcp/config.ts) loads canonical MCPServer items via loadCapability(mcpCapability.id).

The capability layer (src/capability/index.ts) then:

  1. loads providers in priority order
  2. dedupes by server.name (first win = highest priority)
  3. validates deduped items

Result: duplicate server names across sources are not merged. One definition wins; lower-priority duplicates are shadowed.

The dedicated fallback provider in src/discovery/mcp-json.ts reads project-root mcp.json and .mcp.json (low priority).

In practice MCP servers also come from higher-priority providers (for example native .musepi/... and tool-specific config dirs). Authoring guidance:

Normalization behavior

convertToLegacyConfig() (src/mcp/config.ts) maps canonical MCPServer to runtime MCPServerConfig.

Key behavior:

Environment expansion during discovery

OMP-native MCP config (.musepi/mcp.json, ~/.musepi/agent/mcp.json, plus their .mcp.json variants) expands ${VAR} and ${VAR:-default} placeholders recursively before converting to runtime config. It also accepts boolean/string forms for enabled (true, false, 1, 0) and numeric strings for timeout.

The standalone fallback provider in src/discovery/mcp-json.ts reads project-root mcp.json and .mcp.json, expands the same ${...} placeholders, and type-checks enabled/timeout without coercing string values.

Invalid enabled/timeout values are ignored with warnings rather than failing the whole file.

3) Auth and runtime value resolution

MCPManager.prepareConfig()/#resolveAuthConfig() (src/mcp/manager.ts) is the final pre-connect pass.

OAuth credential injection

If config has:

auth: { type: "oauth", credentialId: "..." }

and credential exists in auth storage:

If credential lookup fails, manager logs a warning and continues with unresolved auth.

Header/env value resolution

Before connect, manager resolves stdio env values and HTTP/SSE headers values via resolveConfigValue() (src/config/resolve-config-value.ts):

Operational caveat: a mistyped ! secret command can silently remove that header/env entry, producing downstream 401/403 or server startup failures. A mistyped environment variable name is sent literally unless that literal happens to be meaningful to the server.

4) Tool bridge: MCP -> agent-callable tools

src/mcp/tool-bridge.ts converts MCP tool definitions into CustomTools.

Naming and collision domain

Tool names are generated as:

mcp__<sanitized_server_name>_<sanitized_tool_name>

Rules:

This avoids many collisions, but not all. Different raw names can still sanitize to the same identifier (for example my-server and my.server both sanitize similarly), and registry insertion is last-write-wins.

Schema mapping

tool-bridge.ts passes each MCP inputSchema through normalizeSchemaForMCP() before registering it as a CustomTool schema.

Execution mapping

MCPTool.execute() / DeferredMCPTool.execute():

5) Operator lifecycle: add/edit/remove and live updates

Interactive mode exposes /mcp in src/modes/controllers/mcp-command-controller.ts.

Supported operations:

Config writes are atomic (writeMCPConfigFile: temp file + rename).

After changes, controller calls #reloadMCP():

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

refreshMCPTools() replaces all mcp__ registry entries and immediately re-activates the latest MCP tool set, so changes take effect without restarting the session.

Mode differences

6) User-visible error surfaces

Common error strings users/operators see:

Bad source JSON in discovery is generally handled as warnings/logs; config-writer paths throw explicit errors.

7) Practical authoring guidance

For robust MCP authoring in this codebase:

  1. Keep server names globally unique across all MCP-capable config sources.
  2. Prefer names that remain distinct after MCP tool-name sanitization to avoid generated mcp__ collisions.
  3. Use explicit type to avoid accidental stdio defaults.
  4. Treat enabled: false as hard-off: server is omitted from runtime connect set.
  5. For OAuth configs, store a valid credentialId; otherwise auth injection is skipped.
  6. If using command-based secret resolution (!cmd), verify command output is stable and non-empty.

Implementation files