MCP 客户端 · 传输与 OAuth 登录

第 24 篇讲了 MCP 的连接管理与工具目录,这一篇往下一层看 codex-rmcp-client:配置怎样落成 stdio 或可流式 HTTP 两种传输、本地服务器进程怎么起、HTTP 会话过期怎么恢复,以及 codex mcp login 背后的 OAuth 流程、令牌存储和跨进程刷新。对外的 codex mcp-server 在 v0.158.0 里已不存在,代码中只剩 TUI 内部的一个任务工具服务。

作者 David更新于 第 39 篇(共 57 篇)

MCP 客户端 · 传输与 OAuth 登录

MCP 工具调用一篇讲了 codex-mcp 怎样管理连接、汇总工具目录。那一层之下,每个服务器对应一个 RmcpClient,实现在 codex-rmcp-client(codex-rs/rmcp-client/)。它构建在官方 Rust SDK rmcp(workspace 锁定 3.2.0)之上,自己负责三件 SDK 不管的事:进程放在哪里起、HTTP 请求走哪个客户端、OAuth 凭据怎么存和刷新。

怎么用

config.toml 里每个 [mcp_servers.名字] 写 command 就是本地 stdio 服务器,写 url 就是可流式 HTTP 服务器;codex mcp add、list、login、logout 管理它们,codex mcp login 名字 --no-browser 可以在没有本地浏览器的机器上完成 OAuth。字段与命令见手册 MCP 集成。

从配置到传输

两种传输由出现的字段决定,混写会在加载配置时直接报错:

        let transport = if let Some(command) = command {
            throw_if_set("stdio", "url", url.as_ref())?;
            // ...
            throw_if_set("stdio", "oauth", oauth.as_ref())?;
            // ...
        } else if let Some(url) = url {
            throw_if_set("streamable_http", "args", args.as_ref())?;
            // ...
            throw_if_set("streamable_http", "bearer_token", bearer_token.as_ref())?;
            // ...
        } else {
            return Err("invalid transport".to_string());
        };

(codex-rs/config/src/mcp_types.rs:499)

两种传输都不接受明文的 bearer_token,令牌只能经 bearer_token_env_var 从环境变量读取(codex-rs/core/src/config/mod.rs:2312)。RmcpClient 内部分两部分:一份 TransportRecipe 记住“怎样重新建一条传输”,一个 Connecting、Ready、Closed 三态的状态机持有正在运行的 rmcp 服务(codex-rs/rmcp-client/src/rmcp_client.rs:153)。配方单独保存,是为了重试、会话恢复时能原样重建连接。

图表加载中…

stdio:进程在哪里起

StdioServerLauncher 把“服务器进程跑在哪”从 MCP 生命周期里拆出去:LocalStdioServerLauncher 在本机起子进程,ExecutorStdioServerLauncher 通过执行环境的进程接口在远端起,两者都返回同一种字节流传输(codex-rs/rmcp-client/src/stdio_server_launcher.rs:79)。本机进程的环境变量不是整份继承,而是一张小白名单:Unix 上是 HOME、LOGNAME、PATH、SHELL、USER、LANG、LC_ALL、TERM、TMPDIR、TZ 等,再加上 env_vars 点名转发的变量、自定义 CA 证书变量和 env 里写死的值(codex-rs/rmcp-client/src/utils.rs:163)。进程本身这样创建:

        let build_command = || {
            let mut command = Command::new(&resolved_program);
            command.current_dir(&cwd).envs(&envs).args(&args);
            command.process_mode(ProcessMode::NewGroup);
            // MCP uses only stdio; select Explicit to exclude unrelated
            // orchestrator descriptors from the server and commands it launches.
            // Descriptor allowlisting is Unix-only. Windows can still inherit unrelated
            // handles and needs a handle allowlist in the shared spawn backend.
            #[cfg(unix)]
            command.descriptor_policy(DescriptorPolicy::Explicit);
            #[cfg(windows)]
            command.creation_flags(CREATE_NO_WINDOW);
            command
        };

(codex-rs/rmcp-client/src/stdio_server_launcher.rs:286)

独立进程组便于关闭时先温和终止、再强杀整组;Windows 上还尽量把进程放进 Job Object,并先用 which 解析出带扩展名的完整路径,否则 npx 这类 .cmd 脚本起不来。服务器的 stderr 逐行转进 Codex 日志。

可流式 HTTP:认证、会话与超时

HTTP 请求经 StreamableHttpClientAdapter 交给 Codex 统一的 HttpClient,它可能是本机客户端,也可能把请求转发给远程运行时。认证按顺序取用:bearer_token_env_var 或配置头里的 Authorization;宿主注入的认证方,即内置的 Codex Apps 服务器或配置了 auth = "chatgpt" 的官方源服务器,用当前 ChatGPT 会话;本地保存的 OAuth 令牌;都没有就不带认证连接。服务器不再提供 OAuth 元数据时,已存的 access token 还会被当作普通 bearer token 继续用。

按 MCP 规范,可流式 HTTP 服务器用 404 表示会话已失效。RmcpClient 遇到这种错误会用保存的配方和初始化参数重新握手,然后把失败的操作再执行一次:

            Err(error) if Self::is_session_expired_404(&error) => {
                self.reinitialize_after_session_expiry(&service).await?;
                let recovered_service = self.service().await?;
                Self::run_service_operation_with_transient_retries(

(codex-rs/rmcp-client/src/rmcp_client.rs:1397)

恢复过程由一个信号量串行化,多个并发请求同时撞上 404 只重连一次。操作超时只计“活跃时间”:服务器发来 elicitation/create 向用户要输入时,计时暂停,等用户答完再继续(codex-rs/rmcp-client/src/rmcp_client.rs:237)。协议版本默认按 2025-06-18 握手,开发中的功能开关 mcp_2026_07_28 打开后,优先协商 2026-07-28、不支持时回退(codex-rs/rmcp-client/src/protocol_mode.rs:9)。

OAuth 登录

codex mcp login 由 codex-rs/cli/src/mcp_cmd.rs 解析参数,再交给 codex-rmcp-client 的 OauthLoginFlow:

  1. scope:命令行 --scopes 优先,其次是配置的 scopes,最后用服务器声明的 scopes_supported;只有自动发现的 scope 被服务商拒绝时,才去掉 scope 重试一次(codex-rs/codex-mcp/src/mcp/auth.rs:158)。
  2. 客户端注册:配置了 oauth.client_id 就直接用;否则默认 auto——服务器声明支持 CIMD 时用 ChatGPT 托管的 Codex 客户端元数据文档,否则走动态客户端注册(DCR)。
  3. 回调:在 127.0.0.1 上起一个临时 HTTP 服务,端口默认由系统分配,回调地址形如 http://127.0.0.1:端口/callback,必要时再附上区分服务器的回调 ID;配置的回调地址不是本机时改为监听 0.0.0.0。配置了 oauth_resource 时授权链接附带 resource 参数,整个流程默认等 300 秒(codex-rs/rmcp-client/src/perform_oauth_login.rs:553)。
  4. 浏览器:用 webbrowser 打开授权页,打不开就把链接打印出来让用户自己复制。

--no-browser 不开浏览器,也不关掉本地回调,而是让两条路赛跑:

    let callback = timeout(flow.timeout, async {
        tokio::select! {
            callback = &mut flow.rx => callback.context("OAuth callback was cancelled"),
            input = read_callback(authorization_url.clone()) => {
                parse_callback_url(&input?, &flow.redirect_uri, &authorization_url)
            }
        }
    })

(codex-rs/rmcp-client/src/oauth_callback_input.rs:75)

粘贴进来的地址先做检查:不超过 64 KiB,并且必须对得上这次的回调地址;issuer 与本次登录不符就拒绝,报错信息也不回显粘贴内容。codex mcp add 添加 HTTP 服务器后会先探测一次,发现支持 OAuth 就直接开始登录。

令牌存储与刷新

mcp_oauth_credentials_store 默认 auto:先读系统钥匙串(服务名 Codex MCP Credentials),没有再读 $CODEX_HOME/.credentials.json;一旦从某处读到,这个客户端整个生命周期都钉在那里,刷新时不会切换到可能过期的另一份。条目的键是服务器名加上 URL 的 SHA-256 前缀(codex-rs/rmcp-client/src/oauth.rs:1016)。钥匙串后端在 Windows 上默认是加密的本地 secrets 文件(密钥放在钥匙串里),其他平台直接写系统钥匙串。

每次 MCP 操作开始前、计时之外,客户端检查 access token:离过期不到 30 秒就先刷新(codex-rs/rmcp-client/src/oauth.rs:95)。刷新是一个跨进程的事务:

//! Cross-process serialization for one MCP OAuth credential's refresh transaction.
//!
//! The guard is intentionally acquired before the authoritative credential reread and retained
//! through provider refresh and persistence. This prevents two processes from replaying the same
//! rotating refresh token or observing a partially persisted transaction.

(codex-rs/rmcp-client/src/oauth/refresh_lock.rs:1)

锁文件在 $CODEX_HOME/mcp-oauth-locks/,拿到锁后重新读一遍存储,发现别的进程已经刷新过就直接采用;向服务商请求限时 45 秒,整个事务放在独立任务里执行,调用方取消也打断不了写回(codex-rs/rmcp-client/src/oauth/refresh_transaction.rs:36)。/mcp 与 codex mcp list 显示的认证状态按同样的先后判断:环境变量令牌、Authorization 头、已存令牌,都没有才花最多 5 秒去探测服务器是否支持 OAuth(codex-rs/rmcp-client/src/auth_status.rs:148)。

Codex 自己还是 MCP 服务器吗

对外不是。v0.158.0 的 Subcommand 枚举里只有管理外部服务器的 mcp(codex-rs/cli/src/main.rs:164),没有 mcp-server;手册记载它在 v0.154.0 被移除,想用程序驱动 Codex,应改走 app-server 的 JSON-RPC 协议。rmcp-client/src/bin/ 下的几个服务器只供测试。

代码里剩下的唯一一个 rmcp 服务端实现在 TUI 内部:TUI 连接外部 app-server 时,可以把自己托管的任务管理工具(命名空间 codex_tui,含 list_threads、read_thread、create_thread、fork_thread 等)放进一个只监听 127.0.0.1、端口随机、带一次性 bearer token 的可流式 HTTP MCP 服务,再以 mcp_servers.codex_tui 的配置注入新线程(codex-rs/tui/src/dynamic_tools_mcp.rs:107)。这是前端与服务端之间的内部通道,不是给第三方的入口。

和《从 LLM 到 Coding Agent》对照

那本书的 MCP 一章把 MCP 当作“外部工具的标准协议”,重点在工具如何接入统一的工具抽象。真正落到生产里,大部分代码花在协议之外:进程放在哪、继承哪些环境变量和文件描述符、会话过期怎么恢复、OAuth 客户端怎么注册、轮换的 refresh token 怎样在多个进程之间不被重放。Codex 把这些都收进 codex-rmcp-client,上层的连接管理只需要面对一个 RmcpClient。


上一篇:插件与 marketplace · 打包分发扩展 · 下一篇:多 agent · 子代理的派生与协作

本页目录