- Home
- Documentation
- mcp
- MCP Server and Tool Authoring
MCP Server and Tool Authoring
MCP server and tool authoring
Section titled “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.