- Home
- Documentation
- MCP
- MCP server and tool authoring
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
Section titled “Architecture at a glance”Config sources (.xcsh/.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) -> MCPTool/DeferredMCPTool bridge exposes tools as mcp_<server>_<tool> -> AgentSession.refreshMCPTools replaces live MCP tools immediately1) Server config model and validation
Section titled “1) Server config model and validation”src/mcp/types.ts defines the authoring shape used by MCP config writers and runtime:
stdio(default whentypemissing): requirescommand, optionalargs,env,cwdhttp: requiresurl, optionalheaderssse: requiresurl, optionalheaders(kept for compatibility)- shared fields:
enabled,timeout,auth
validateServerConfig() (src/mcp/config.ts) enforces transport basics:
- rejects configs that set both
commandandurl - requires
commandfor stdio - requires
urlfor http/sse - rejects unknown
type
config-writer.ts applies this validation for add/update operations and also validates server names:
- non-empty
- max 100 chars
- only
[a-zA-Z0-9_.-]
Transport pitfalls
Section titled “Transport pitfalls”typeomitted means stdio. If you intended HTTP/SSE but omittedtype,commandbecomes mandatory.sseis still accepted but treated as HTTP transport internally (createHttpTransport).- Validation is structural, not reachability: a syntactically valid URL can still fail at connect time.
2) Discovery, normalization, and precedence
Section titled “2) Discovery, normalization, and precedence”Capability-based discovery
Section titled “Capability-based discovery”loadAllMCPConfigs() (src/mcp/config.ts) loads canonical MCPServer items via loadCapability(mcpCapability.id).
The capability layer (src/capability/index.ts) then:
- loads providers in priority order
- dedupes by
server.name(first win = highest priority) - validates deduped items
Result: duplicate server names across sources are not merged. One definition wins; lower-priority duplicates are shadowed.
.mcp.json and related files
Section titled “.mcp.json and related files”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 .xcsh/... and tool-specific config dirs). Authoring guidance:
- Prefer
.xcsh/mcp.json(project) or~/.xcsh/mcp.json(user) for explicit control. - Use root
mcp.json/.mcp.jsonwhen you need fallback compatibility. - Reusing the same server name in multiple sources causes precedence shadowing, not merge.
Normalization behavior
Section titled “Normalization behavior”convertToLegacyConfig() (src/mcp/config.ts) maps canonical MCPServer to runtime MCPServerConfig.
Key behavior:
- transport inferred as
server.transport ?? (command ? "stdio" : url ? "http" : "stdio") - disabled servers (
enabled === false) are dropped before connection - optional fields are preserved when present
Environment expansion during discovery
Section titled “Environment expansion during discovery”mcp-json.ts expands env placeholders in string fields with expandEnvVarsDeep():
- supports
${VAR}and${VAR:-default} - unresolved values remain literal
${VAR}strings
mcp-json.ts also performs runtime type checks for user JSON and logs warnings for invalid enabled/timeout values instead of hard-failing the whole file.
3) Auth and runtime value resolution
Section titled “3) Auth and runtime value resolution”MCPManager.prepareConfig()/#resolveAuthConfig() (src/mcp/manager.ts) is the final pre-connect pass.
OAuth credential injection
Section titled “OAuth credential injection”If config has:
auth: { type: "oauth", credentialId: "..." }and credential exists in auth storage:
http/sse: injectsAuthorization: Bearer <access_token>headerstdio: injectsOAUTH_ACCESS_TOKENenv var
If credential lookup fails, manager logs a warning and continues with unresolved auth.
Header/env value resolution
Section titled “Header/env value resolution”Before connect, manager resolves each header/env value via resolveConfigValue() (src/config/resolve-config-value.ts):
- value starting with
!=> execute shell command, use trimmed stdout (cached) - otherwise, treat value as environment variable name first (
process.env[name]), fallback to literal value - unresolved command/env values are omitted from final headers/env map
Operational caveat: this means a mistyped secret command/env key can silently remove that header/env entry, producing downstream 401/403 or server startup failures.
4) Tool bridge: MCP -> agent-callable tools
Section titled “4) Tool bridge: MCP -> agent-callable tools”src/mcp/tool-bridge.ts converts MCP tool definitions into CustomTools.
Naming and collision domain
Section titled “Naming and collision domain”Tool names are generated as:
mcp_<sanitized_server_name>_<sanitized_tool_name>Rules:
- lowercases
- non-
[a-z_]chars become_ - repeated underscores collapse
- redundant
<server>_prefix in tool name is stripped once
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
Section titled “Schema mapping”convertSchema() keeps MCP JSON Schema mostly as-is but patches object schemas missing properties with {} for provider compatibility.
Execution mapping
Section titled “Execution mapping”MCPTool.execute() / DeferredMCPTool.execute():
- calls MCP
tools/call - flattens MCP content into displayable text
- returns structured details (
serverName,mcpToolName, provider metadata) - maps server-reported
isErrortoError: ...text result - maps thrown transport/runtime failures to
MCP error: ... - preserves abort semantics by translating AbortError into
ToolAbortError
5) Operator lifecycle: add/edit/remove and live updates
Section titled “5) Operator lifecycle: add/edit/remove and live updates”Interactive mode exposes /mcp in src/modes/controllers/mcp-command-controller.ts.
Supported operations:
add(wizard or quick-add)remove/rmenable/disabletestreauth/unauthreload
Config writes are atomic (writeMCPConfigFile: temp file + rename).
After changes, controller calls #reloadMCP():
mcpManager.disconnectAll()mcpManager.discoverAndConnect()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
Section titled “Mode differences”- Interactive/TUI mode:
/mcpgives in-app UX (wizard, OAuth flow, connection status text, immediate runtime rebinding). - SDK/headless integration:
discoverAndLoadMCPTools()(src/mcp/loader.ts) returns loaded tools + per-server errors; no/mcpcommand UX.
6) User-visible error surfaces
Section titled “6) User-visible error surfaces”Common error strings users/operators see:
- add/update validation failures:
Invalid server config: ...Server "<name>" already exists in <path>
- quick-add argument issues:
Use either --url or -- <command...>, not both.--token requires --url (HTTP/SSE transport).
- connect/test failures:
Failed to connect to "<name>": <message>- timeout help text suggests increasing timeout
- auth help text for
401/403
- auth/OAuth flows:
Authentication required ... OAuth endpoints could not be discoveredOAuth flow timed out. Please try again.OAuth authentication failed: ...
- disabled server usage:
Server "<name>" is disabled. Run /mcp enable <name> first.
Bad source JSON in discovery is generally handled as warnings/logs; config-writer paths throw explicit errors.
7) Practical authoring guidance
Section titled “7) Practical authoring guidance”For robust MCP authoring in this codebase:
- Keep server names globally unique across all MCP-capable config sources.
- Prefer alphanumeric/underscore names to avoid sanitized-name collisions in generated
mcp_*tool names. - Use explicit
typeto avoid accidental stdio defaults. - Treat
enabled: falseas hard-off: server is omitted from runtime connect set. - For OAuth configs, store a valid
credentialId; otherwise auth injection is skipped. - If using command-based secret resolution (
!cmd), verify command output is stable and non-empty.