MCP 客户端 · 连上外部工具服务器
MCP(Model Context Protocol)让 grok 接入外部工具服务器。这一章看 xai-grok-mcp 的三种传输、单飞握手状态机、韧性传输、远程 MCP 的 OAuth 流程,以及新版协议的协商。
MCP 客户端 · 连上外部工具服务器
MCP(Model Context Protocol)是让 agent 接入外部工具服务器的标准协议——数据库、Jira、Figma 等等。grok 的 MCP 客户端在 xai-grok-mcp,基于 rmcp 3.2。
一个工程细节:rmcp 3.x 要求 reqwest ≥ 0.13.2,而其余 workspace 还在用 reqwest 0.12。xai-grok-mcp 于是把 rmcp + reqwest 0.13 私有隔离在自己 crate 里,再 pub use rmcp 让全仓库只从这一个命名空间取 MCP 模型类型。用一个 crate 隔离有版本冲突的依赖是 Rust 大仓库常见的解耦手法。
三种传输
PendingTransport 有四个变体(Stdio / Http / HttpAuth / Acp),归结为三种连法:
- Stdio:spawn 一个子进程,按行分隔的 JSON-RPC 通信(最常见,
npx some-mcp-server)。 - HTTP:
StreamableHttpClientTransport,连远程 HTTP MCP 服务器,需要 OAuth 的走HttpAuth。配置里写sse只是个标签,同样走 streamable HTTP。 - ACP 反向:连进程内的 SDK MCP 服务器,走 ACP 反向通道(
x.ai/mcp/sdk_call)。
Stdio 有个韧性设计:ResilientRwTransport 能扛住一行解不了的 stdout(跳过 + 报个事件),而不是像 rmcp 原版那样整条传输崩掉——因为真实的 MCP 服务器常往 stdout 打非协议的杂日志。子进程 Drop 时还会杀掉整个进程组(killpg),不留孤儿。
状态机 + 单飞握手
McpClient 的状态有四个:Empty、Pending、Initializing、Ready。构造时就是 Pending(传输已配好、等握手),握手成功进 Ready;失败时能重建的传输(Http / HttpAuth / Acp)退回 Pending 等下次重试,Stdio 的子进程已被消耗,只能进终态 Empty。握手用一个**单飞(single-flight)**模式,值得学:
关键:用 std::mem::replace(state, Initializing) 拿走旧状态,谁拿到 Pending 谁就成为握手的唯一执行者,其余并发调用都 park 在 Notify 上等(先订阅通知再看状态,防止漏掉唤醒)。配一个 RAII InitGuard——握手被取消/panic 时,在 Drop 里 try_lock 把能重建的传输回退成 Pending 并唤醒等待者。它是尽力而为:Stdio 没有可恢复的传输,try_lock 抢不到锁时状态也会留在 Initializing,这时靠等待者的超时兜底(报「init still in progress」),而不是永远卡住。这是「并发初始化只做一次、还要 cancel-safe」的教科书写法。
远程 MCP 的 OAuth
连需要登录的远程 MCP,走 OAuth(rmcp 的 AuthorizationManager:元数据发现、动态注册、PKCE 都靠 rmcp 实现,grok 给发现套了 5 秒超时,也支持在配置里自带 oauth_client_id)。交互流程:起一个 loopback 回调服务器(axum)、开浏览器,然后 select! 三路等待——回调、每 2 秒轮询一次凭据文件(别的进程/tab 可能已登)、600 秒超时报错。去重做了两层:跨进程用文件锁(flock,仅 Unix;拿到锁先复查凭据,别人已登就直接复用),进程内用 IN_FLIGHT_AUTH map——避免同时弹好几个浏览器窗口。token 存 ~/.grok/mcp_credentials.json(原子写、0o600),每次读改写都在跨进程锁下进行,并带新鲜度保护,防止慢的写者把刚轮换的 refresh token 写回旧值。
会话启动时的鉴权判定只看磁盘、不探网络(McpOauthDiscovery::Disk),交互与否行为一致:存有 token 就走 OAuth 传输;有凭据条目却没 token(登录没做完)就直接报 AuthRequired——源码注释说得明白,免得起一个会在 OAuth 质询上崩掉的 HTTP worker;什么都没有就按普通 HTTP 连,握手被 401 拒了再记为「待认证」。只有刷新、诊断、用户主动认证这类显式操作才做网络探测;非交互下探测结论不确定时,会先发一个匿名请求:被拒判为需要登录,连不上则报可重试的 Unreachable。fail-closed 只落在「确定需要登录」上,网络抖动不会让会话永久丢掉这个服务器——又一个「按威胁模型选 fail-open 还是 fail-closed」的决策。
新版协议与 elicitation
HTTP 和 ACP 传输握手前,先用 server/discover 探测 2026-07-28 版协议(SEP-2575),失败或超时再回退到旧的 initialize(固定报 2025-11-25);Stdio 只走旧握手——探测一个慢启动的子进程并不安全。新版里一次 tools/call 可以多轮往返(SEP-2322):服务器中途要用户补信息(填表单或打开一个 URL),grok 经 x.ai/mcp/elicit 交给界面处理。MCP 的 resources / prompts 目前仍未接入。
和《从 LLM 到 Coding Agent》对照
对照 mcp:最小实现里接 MCP 大概是「连个 stdio 服务器、列工具、调用」。grok 的增量在真实世界的粗粝:三种传输、脏 stdout 韧性、cancel-safe 单飞握手、多层去重的 OAuth、进程组清理,以及新旧协议的协商。下一章看连上之后,那些远程工具怎么进工具注册表。
上一篇:Skills · 下一篇:MCP 工具进注册表