MCP Integration
How agents discover, connect to, and manage MCP (Model Context Protocol) servers for extensible tool access.
Overview
| Agent | MCP Role | SDK Used | Transports | Discovery | Auto-Restart |
|---|---|---|---|---|---|
| Goose | All tools via MCP | rmcp (Rust) | stdio, SSE, streamable-HTTP | Eager at connect | Yes (via extension lifecycle) |
| Grok Build | Alongside built-ins | rmcp 2.1 (Rust) | stdio, SSE, streamable-HTTP | Eager + search_tool/use_tool meta-tools | Yes (3 retries stdio, backoff ladder HTTP) |
| Qwen Code | Alongside built-ins | @modelcontextprotocol/sdk (TS) | stdio, SSE, streamable-HTTP | Deferred (schemas hidden until tool-search) | Yes (polling health 30s) |
| Codex | Client + Server | rmcp (Rust) | stdio, streamable-HTTP | Prewarmed + cached | Best-effort background reconnect |
| Cline | Alongside built-ins | @modelcontextprotocol/sdk (TS) | stdio, SSE, streamable-HTTP | Eager per-server | No auto-respawn (stdio); backoff (HTTP) |
| OpenCode/Kilocode | Alongside built-ins | @modelcontextprotocol/sdk (TS) | stdio, SSE | Eager at connect | Manual restart |
Qwen Code — Deferred Discovery via tool-search
Core files: repos/qwen-code/packages/core/src/tools/tool-search.ts, mcp-client.ts, mcp-client-manager.ts, mcp-tool.ts
Unique Feature: On-Demand Schema Loading
MCP tool schemas are genuinely deferred — hidden from the model until ToolSearch reveals them:
ToolRegistry.getFunctionDeclarations()filters out any tool withshouldDefer && !alwaysLoad && !revealed- The model only sees deferred tool names via a startup reminder (
getDeferredToolSummary()) - When the model uses
tool-search, matching tools are revealed viaregistry.revealDeferredTool(name)+geminiClient.setTools()re-sync - However,
discoverTools()still eagerly callstools/liston every server at connection time — only the schema exposure to the model is deferred, not the underlying RPC
Transport Priority
createTransport() (lines 2035-2240): in-process SDK → httpUrl (streamable HTTP) → url (SSE) → command (stdio)
Health Monitoring
McpClientManager runs a polling health monitor:
- Default 30s interval
- 3 consecutive failures → reconnect
- 5s reconnect delay
- Separate per-call reconnect path with regex-matched connection errors
Namespacing
mcp__<serverName>__<toolName> via generateValidName(). Collisions with built-ins force the MCP tool to its fully-qualified name.
Config
MCPServerConfig in packages/core/src/config/config.ts:750-803:
- Fields:
command/args/env/cwd(stdio),url(SSE),httpUrl(streamable HTTP),headers,timeout,trust,includeTools/excludeTools - Scoped:
project/workspace/system - OAuth: full browser flow, RFC 9728 discovery, keychain storage, proactive probing
Trust Layers (Multiple, Composable)
- Settings-level glob:
mcp.allowed/mcp.excluded - Per-server:
includeTools/excludeTools trust: boolean→ auto-allow only if trusted server AND trusted workspace folder- Project/workspace-scoped servers held behind explicit pending-approval gate
Codex — Both MCP Client AND Server
Core files: repos/codex/codex-rs/codex-mcp/, codex-rmcp-client/, mcp-server/
Unique Feature: Self-as-MCP-Server
codex mcp-server (binary codex-mcp-server) exposes Codex itself as an MCP server:
- Two tools:
codex(start session) andcodex-reply(continue thread) - Approval round-trips (
execCommandApproval/applyPatchApproval) sent back to calling client - Documented as “experimental” in
codex-rs/docs/codex_mcp_interface.md
Transport Config
#![allow(unused)]
fn main() {
pub enum McpServerTransportConfig {
Stdio { command, args, env, env_vars, cwd },
StreamableHttp { url, bearer_token_env_var, http_headers, env_http_headers },
}
}
No standalone SSE config — SSE only as the streaming mechanism inside Streamable HTTP.
Discovery
Hybrid: mcp_prewarm.rs proactively prewarms connections/tool lists at session start (non-blocking); tool_catalog_cache.rs caches tools/list results, invalidated on events (OAuth login, server recovery).
Config Format
TOML [mcp_servers.<name>] with rich fields: startup_timeout_sec, tool_timeout_sec, enabled, required (fails session if server won’t start), per-tool tools.<name>.approval_mode, auth (oauth|chatgpt). Programmatic edits via codex mcp add/remove with TOML-document surgery preserving formatting.
Auth
Full OAuth2/PKCE, browser or silent flow, RFC 8707 resource indicators, OS-keyring storage with .credentials.json fallback. Bearer tokens only accepted via env-var reference (raw literals rejected).
Grok Build — Unified Permission Pipeline + Config Import
Core files: repos/grok-build/crates/codegen/xai-grok-mcp/src/servers.rs, xai-grok-config-types/src/mcp.rs
Architecture
Built-in tools and MCP tools merge into one ToolBridge → ToolRegistry. No separate dispatch path post-registration. Also exposes search_tool/use_tool meta-tools for indirect discovery/invocation.
Unique Feature: Config Import from Other Tools
Auto-imports MCP configs from: .claude.json (Claude Code), .cursor/mcp.json (Cursor), standard .mcp.json — merged in priority order.
Lifecycle
start_mcp_server(): spawns viatokio::process::Commandwithkill_on_drop(true)- Stderr drained to
~/.grok/logs/mcp/<server>.stderr.log - Liveness: 500ms poller (rmcp 2.1
RunningServicelacks a “closed” future) - Restart-on-crash (stdio): 3 attempts, backoff 1s/+4s/+16s, then “parked” (tools unregistered)
- HTTP/SSE: in-place transport reset with 8-step backoff ladder (accommodates rolling redeploys)
Permission Integration
MCP tool calls route through the exact same AccessKind::MCPTool{name, input} pipeline as built-ins. Even auto/YOLO mode classifier-checks MCP calls rather than blanket-approving.
Resilience Patches
- Custom NDJSON transport (
ResilientRwTransport): works around rmcp’s default of replying -32600 on malformed lines (which some servers echo back into a loop) - SSE-flood backoff: patches rmcp 2.1.0 bug where SSE reconnects fire immediately without backoff
Auth
Full OAuth 2.0 via rmcp’s auth feature: Dynamic Client Registration (RFC 7591), cross-process dedup via filesystem lock + generation counter, loopback callback server, plaintext credential store at ~/.grok/mcp_credentials.json (0600 perms).
Cline — UI-Driven Per-Tool Approval
Core files: repos/cline/apps/vscode/src/services/mcp/McpHub.ts, sdk/packages/core/src/extensions/mcp/tools.ts
Architecture
McpHub (1909 lines) owns full lifecycle: reads/validates settings, connects, tears down, restarts, watches settings file, handles auto-approval and OAuth.
Tool Exposure
The SDK generates one first-class dynamic tool per MCP tool (named ${serverName}__${toolName}, hashed/truncated for OpenAI’s 64-char limit) — not a single generic use_mcp_tool(server,tool,args) wrapper.
Config
cline_mcp_settings.json, Zod-validated. Supports both legacy flat fields and newer nested {transport:{type,...}} shape, normalized via .transform(). Per-server: autoApprove: string[], disabled, timeout.
Lifecycle/Hot-Reload
watchMcpSettingsFile() uses chokidar with awaitWriteFinish/atomic, computing content fingerprint to skip self-triggered reconnect loops. Crash handling differs by transport:
- stdio: no auto-respawn (marks
disconnected, requires manual restart) - streamableHttp: exponential-backoff reconnect (6 attempts,
2000*2^nms) - SSE: relies on
ReconnectingEventSourcebuilt-in retry
Auto-Approval
Per-tool checkbox in webview UI, enforced via isToolAutoApproved() which parses serverName__toolName convention — gated behind master switch autoApprovalSettings.actions.useMcp.
Resources & Prompts
Fully supported beyond tools: readResource(), getPrompt(), plus list-side schemas surfaced in dedicated UI rows.
Design Patterns
1. Converging on Official SDKs
All agents use official MCP SDKs (@modelcontextprotocol/sdk for TS, rmcp for Rust) rather than hand-rolling JSON-RPC. The Rust SDK is less battle-tested — Grok Build had to patch around SSE reconnect storms and NDJSON parsing brittleness.
2. Namespace Convention: server__tool
Universal pattern: <serverName>__<toolName> (double underscore). Cline additionally hashes/truncates to 64 chars for OpenAI compatibility.
3. Eager vs Deferred Discovery
| Strategy | Agents | Tradeoff |
|---|---|---|
| Eager (list all at connect) | Goose, Grok Build, Cline | More tokens in system prompt; immediate availability |
| Deferred (schemas hidden until searched) | Qwen Code | Saves context window; adds one extra tool call per discovery |
| Hybrid (eager + meta-tools for indirect access) | Grok Build, Codex | Best of both; search_tool for overflow |
4. Restart Policies Reflect Design Philosophy
| Agent | Stdio Restart | HTTP/SSE Restart | Philosophy |
|---|---|---|---|
| Grok Build | 3 retries, backoff, then park | 8-step backoff ladder | Maximize uptime |
| Qwen Code | Health monitor (30s/3 failures) | Same | Maximize uptime |
| Codex | Best-effort background | Same | Background resilience |
| Cline | No auto-respawn | Backoff reconnect | User control |
5. Permission Integration Spectrum
- Unified (Grok Build): MCP calls flow through the same classifier/policy as built-in tools
- Layered (Qwen Code, Cline): MCP-specific trust flags (
trust,autoApprove) alongside general approval - Config-level (Codex): per-tool
approval_modein TOML
6. Config Portability
Only Grok Build auto-imports configs from other tools (Claude Code, Cursor, .mcp.json). Others require manual re-configuration per tool.