MCP 和执行器支持的传输适配器
这一层像一组“转接头”,夹在 MCP 客户端的主流程和真实通信方式之间。库门面把常用入口摆出来;进程内通道负责重新连上内置服务器;执行器进程通道把标准输入输出包装成可收发 JSON 消息的管子,并分开记录错误日志。HTTP 部分有的直接发网络请求,有的把请求转交远端执行环境;适配器再把 RMCP 的发消息、开流、关会话翻译成 HTTP。遇到认证失败时会读权限提示;流式连接不稳时会先重试。
客户端库界面
crate 根公开 RMCP 客户端 API,以及高级使用者基于其构建的传输抽象。
rmcp-client/src/lib.rs源码 ↗
这个文件本身不写具体业务,也没有函数。它做的事像商场一楼的导览牌:先把库内部的各个模块挂进来,比如认证状态、OAuth 登录、HTTP 客户端适配、本地进程通信、标准输入输出服务器启动器等;再把外部最常用的类型和函数统一“摆到门口”。这样,其他代码想创建 MCP 客户端、检查是否需要登录、保存或删除 OAuth 令牌、启动本地 stdio 服务时,不需要去翻很多内部文件,只要从这个库的顶层导入即可。这里也刻意区分了公开导出和 crate 内部可见导出,例如 load_oauth_tokens 只给本库内部用,避免外部代码依赖太细的实现细节。
rmcp-client/src/in_process_transport.rs源码 ↗
这个文件解决的是“客户端和同进程里的服务器怎么重新连上”的问题。这里的“同进程”可以理解成两个人在同一间屋子里传纸条,不需要真正走网络。文件里定义了一个 InProcessTransportFactory 特征,也就是一份约定:谁实现了它,谁就必须提供 open 这个能力,用来创建一条新的 DuplexStream。DuplexStream 是 Tokio 提供的双向字节流,可以同时读和写,像一根本地的双向管道。重要的是,注释说明实现者应该先把配对的服务器端启动好,再把客户端这一端返回。这样 RmcpClient 保存这个工厂后,之后断线重连时,只需要再调用它,不必关心背后是哪种内置服务器、怎么搭起来的。
执行器支持的 stdio 传输
此传输使用 stdio 流,将 RMCP 消息流桥接到由执行器管理的子进程。
rmcp-client/src/executor_process_transport.rs源码 ↗
MCP 服务器有时不是客户端自己直接启动的,而是由一个“执行器”代为启动,可能还在远端机器上。这个文件就是中间的翻译员。rmcp 要发送消息时,它把结构化的 JSON-RPC 消息转成 JSON 字节,末尾加一个换行符,再交给执行器写进子进程 stdin。执行器推送 stdout/stderr 事件回来时,它把 stdout 当成真正的协议消息流:攒够一整行,就解析成 rmcp 能懂的消息;stderr 则只写日志,方便排查问题。这里还有一个 LineBuffer,像一个临时收纳盒,专门处理“半行数据先到、剩下半行后到”的情况。文件也会记住事件序号,避免重复处理;如果事件流漏了,还会从执行器补读。最后,当传输关闭或对象被丢弃时,它会尽量终止那个 MCP 服务器进程,避免留下后台孤儿进程。
LineBuffer::extend_from_slice59–61 ↗
fn extend_from_slice(&mut self, bytes: &[u8])
作用:把新收到的一段字节追加到行缓冲区里。它用来处理网络或进程输出常见的情况:一条消息可能不会一次完整到达。
数据流:进去的是一小段原始字节 → 它把这些字节接到已有缓存后面 → 出来没有返回值,但缓存里的内容变多了,后面就能继续找完整的一行。
调用关系:当进程 stdout 或 stderr 有新字节到达时,ExecutorProcessTransport::push_process_output 和 ExecutorProcessTransport::push_stderr 会用它先把数据攒起来,再等后续函数按行取出。
调用图:外部调用 1 个(extend_from_slice)。
LineBuffer::take_line63–74 ↗
fn take_line(&mut self) -> Option<BytesMut>
作用:从缓存里取出一整行,也就是找到换行符之前的内容。它让“按行传输”的 MCP 消息可以从零散字节里被切出来。
数据流:进去的是缓冲区当前已有的字节 → 它从上次没扫过的位置开始找换行符;找到了就切出换行符前的一行,并把这行从缓存中移走;没找到就记录已经扫到哪里 → 返回一行字节,或者返回空表示还不够一行。
调用关系:ExecutorProcessTransport::take_stdout_message 用它切 stdout 中的协议消息;ExecutorProcessTransport::push_stderr 用它切 stderr 日志行。它是整套“字节变成一条消息”的第一步。
调用图:外部调用 3 个(len, split_to, memchr)。
LineBuffer::take_remaining76–83 ↗
fn take_remaining(&mut self) -> Option<BytesMut>
作用:把缓存里剩下的所有字节一次性取走。它主要用于进程已经结束时,处理最后一行没有换行符的残留内容。
数据流:进去的是缓冲区里尚未取出的字节 → 如果缓存为空就什么也不做;如果不为空就把全部内容拿出来,并清空缓存 → 返回剩余字节,或者返回空。
调用关系:ExecutorProcessTransport::take_stdout_message 在进程关闭后用它尝试解析最后一条未换行的 stdout 消息;ExecutorProcessTransport::flush_stderr 用它把最后没换行的 stderr 也写进日志。
调用图:外部调用 2 个(is_empty, split)。
ExecutorProcessTransport::new135–151 ↗
fn new(process: Arc<dyn ExecProcess>, program_name: String) -> Self
作用:创建一个新的执行器进程传输通道。它把已经启动好的进程、事件订阅、stdout/stderr 缓冲区和状态标记都准备好。
数据流:进去的是执行器返回的进程句柄和一个给日志看的程序名 → 它立刻订阅这个进程的事件流,并初始化两个空的行缓冲区以及关闭状态、序号等记录 → 出来是一个可交给 rmcp 使用的 ExecutorProcessTransport。
调用关系:它由 launch_server 在启动 MCP 服务器后调用。后续 rmcp 会通过这个对象调用 send、receive 和 close,真正完成协议通信。
调用图:被 1 处调用(launch_server);外部调用 1 个(default)。
ExecutorProcessTransport::next_process_id153–159 ↗
fn next_process_id() -> ProcessId
作用:生成一个新的逻辑进程编号。这个编号不是操作系统里的 pid,而是客户端和执行器之间用来区分不同 MCP 进程的名字牌。
数据流:进去没有外部参数 → 它从全局计数器取下一个数字,拼成类似 mcp-stdio-1 的字符串 → 出来是一个 ProcessId,可用于启动或登记新的执行器进程。
调用关系:它由 launch_server 在准备启动 MCP stdio 服务器时调用,保证同一会话里多个服务器不会拿到相同的逻辑编号。
调用图:调用 1 个内部函数(from);被 1 处调用(launch_server);外部调用 1 个(format!)。
ExecutorProcessTransport::send165–190 ↗
fn send(
&mut self,
item: TxJsonRpcMessage<RoleClient>,
) -> impl Future<Output = std::result::Result<(), Self::Error>> + Send + 'static
作用:把 rmcp 要发出的结构化消息写进 MCP 服务器进程的 stdin。简单说,它负责“客户端说话”。
数据流:进去的是一条 TxJsonRpcMessage,也就是准备发给服务器的 JSON-RPC 消息 → 它把消息序列化成 JSON 字节,末尾加换行符,然后调用执行器进程的 write → 如果执行器接受写入就返回成功;如果进程不存在、stdin 关了或还在启动,就返回对应的输入输出错误。
调用关系:rmcp 在需要发送请求或响应时调用它。它不自己理解业务含义,只负责把 rmcp 的消息变成 stdio 协议要求的“一行 JSON”。
ExecutorProcessTransport::receive192–194 ↗
fn receive(&mut self) -> impl Future<Output = Option<RxJsonRpcMessage<RoleClient>>> + Send
作用:让 rmcp 等待并取得服务器发回来的下一条消息。简单说,它负责“客户端听话”。
数据流:进去没有新的业务参数,但会使用对象里保存的事件流、缓冲区和关闭状态 → 它把实际工作交给 receive_message → 出来是一条解析好的 RxJsonRpcMessage,或者在通道结束时返回空。
调用关系:这是 rmcp Transport 接口的一部分。rmcp 调它时,它马上转给 ExecutorProcessTransport::receive_message,由后者处理事件、缓冲、解析和结束条件。
调用图:调用 1 个内部函数(receive_message)。
ExecutorProcessTransport::close196–200 ↗
async fn close(&mut self) -> std::result::Result<(), Self::Error>
作用:主动关闭这条传输,并要求执行器终止 MCP 服务器进程。它用于正常收尾,避免服务器进程继续留在后台运行。
数据流:进去没有额外参数,但会使用保存的进程句柄 → 它调用进程的 terminate;成功后把 terminated 标记设为真 → 返回成功或输入输出错误。
调用关系:rmcp 在关闭传输时调用它。它设置 terminated 后,后面的 ExecutorProcessTransport::drop 就知道不需要再重复安排终止。
ExecutorProcessTransport::receive_message204–258 ↗
async fn receive_message(&mut self) -> Option<RxJsonRpcMessage<RoleClient>>
作用:这是接收消息的主循环。它一边等执行器推送进程事件,一边把 stdout 拼成一条条 JSON-RPC 消息交给 rmcp。
数据流:进去的是对象当前的 stdout/stderr 缓冲、事件接收器、关闭标记和最后处理过的事件序号 → 它先尝试从已有 stdout 缓冲解析消息;如果没有,就等待新的进程事件;收到输出就分流到 stdout 或 stderr,收到关闭或失败就更新状态,事件丢失时尝试补读 → 出来是一条收到的消息,或者在进程结束且没有剩余消息时返回空。
调用关系:ExecutorProcessTransport::receive 会调用它。它是接收侧的调度中心,会用到 take_stdout_message、push_process_output_if_new、note_seq、recover_lagged_events 和 flush_stderr。
调用图:调用 6 个内部函数(recv, flush_stderr, note_seq, push_process_output_if_new, recover_lagged_events, take_stdout_message);被 1 处调用(receive);外部调用 1 个(warn!)。
ExecutorProcessTransport::note_seq260–262 ↗
fn note_seq(&mut self, seq: u64)
作用:记录已经看到的最高事件序号。序号可以理解成事件流水号,用来判断哪些输出已经处理过。
数据流:进去的是一个事件序号 → 它把 last_seq 更新为当前记录和新序号里的较大值 → 没有返回值,但对象记住了最新进度。
调用关系:receive_message 在收到 Exited 或 Closed 这类生命周期事件时调用它。这个记录后面会帮助 recover_lagged_events 从正确位置补读。
调用图:被 1 处调用(receive_message)。
ExecutorProcessTransport::should_accept_seq264–270 ↗
fn should_accept_seq(&mut self, seq: u64) -> bool
作用:判断某个输出事件是不是新的,防止同一段输出被重复处理。它像检票员,只放行没见过的流水号。
数据流:进去的是一个输出块的序号 → 如果这个序号不大于 last_seq,就认为已经处理过并返回 false;如果是新的,就更新 last_seq 并返回 true → 同时改变对象里的最新序号记录。
调用关系:ExecutorProcessTransport::push_process_output_if_new 调用它。它把去重逻辑集中在一个地方,避免正常推送和补读恢复时重复塞进 stdout/stderr。
调用图:被 1 处调用(push_process_output_if_new)。
ExecutorProcessTransport::recover_lagged_events272–296 ↗
async fn recover_lagged_events(&mut self) -> io::Result<()>
作用:当事件接收器提示“有事件漏掉了”时,它从执行器那里补读遗漏的输出。这样就尽量不丢 MCP 服务器已经发出的消息。
数据流:进去的是对象记录的 last_seq,以及进程句柄 → 它调用进程 read,从 last_seq 之后读取保留的输出块;每个输出块再走 push_process_output_if_new;最后更新 last_seq,并根据返回结果标记失败或关闭 → 返回成功,或者返回输入输出错误。
调用关系:receive_message 在收到事件流 Lagged 错误时调用它。它补到的输出仍然交给 push_process_output_if_new,和正常推送路径共用同一套分流和去重逻辑。
调用图:调用 1 个内部函数(push_process_output_if_new);被 1 处调用(receive_message);外部调用 1 个(warn!)。
ExecutorProcessTransport::push_process_output_if_new298–303 ↗
fn push_process_output_if_new(&mut self, chunk: ProcessOutputChunk)
作用:接收一个进程输出块,并且只在它确实是新输出时才处理。它避免同一段 stdout 或 stderr 因为补读机制被处理两遍。
数据流:进去的是一个带序号、来源流和字节内容的 ProcessOutputChunk → 它先用 should_accept_seq 检查序号;如果旧了就直接丢弃,如果新了就交给 push_process_output → 没有返回值,但可能改动 stdout/stderr 缓冲。
调用关系:receive_message 处理实时推送输出时会调用它;recover_lagged_events 补读历史输出时也会调用它。它是实时路径和恢复路径之间的共同入口。
调用图:调用 2 个内部函数(push_process_output, should_accept_seq);被 2 处调用(receive_message, recover_lagged_events)。
ExecutorProcessTransport::push_process_output305–320 ↗
fn push_process_output(&mut self, chunk: ProcessOutputChunk)
作用:把进程输出按来源分开处理:stdout 是协议消息,stderr 是诊断日志。这样错误日志不会被误当成 MCP 消息解析。
数据流:进去的是一个输出块 → 它取出里面的原始字节;如果来源是 stdout 或 pty,就追加到 stdout 缓冲;如果来源是 stderr,就交给 push_stderr 按日志处理 → 没有返回值,但会更新缓冲区或写日志。
调用关系:push_process_output_if_new 在确认输出是新的之后调用它。它再根据输出流类型,把 stdout 交给 LineBuffer::extend_from_slice,把 stderr 交给 ExecutorProcessTransport::push_stderr。
调用图:调用 2 个内部函数(extend_from_slice, push_stderr);被 1 处调用(push_process_output_if_new)。
ExecutorProcessTransport::take_stdout_message322–344 ↗
fn take_stdout_message(&mut self, allow_partial: bool) -> Option<RxJsonRpcMessage<RoleClient>>
作用:从 stdout 缓冲里取出并解析下一条 MCP JSON-RPC 消息。它把“一行 JSON 字节”变成 rmcp 能使用的消息对象。
数据流:进去的是 stdout 缓冲和 allow_partial 标记;allow_partial 表示进程已经关了时能不能接受最后一行没有换行符 → 它先取一整行,必要时取剩余半行,去掉行尾可能存在的回车符,再尝试按 JSON-RPC 消息解析;解析失败就记录调试日志并继续找下一行 → 成功时返回一条消息,没有可用内容时返回空。
调用关系:receive_message 每轮都会先调用它,优先处理已经攒好的 stdout。它依赖 LineBuffer::take_line、LineBuffer::take_remaining 和 trim_trailing_carriage_return,把底层字节整理成协议消息。
调用图:调用 1 个内部函数(take_line);被 1 处调用(receive_message);外部调用 3 个(trim_trailing_carriage_return, debug!, take_remaining)。
ExecutorProcessTransport::push_stderr346–358 ↗
fn push_stderr(&mut self, bytes: &[u8])
作用:处理 MCP 服务器写到 stderr 的内容,并按行写入日志。stderr 不属于协议消息,只用于排查服务器启动失败或运行报错。
数据流:进去的是一段 stderr 字节 → 它追加到 stderr 缓冲,然后尽可能一行一行取出;每行会去掉末尾回车符,再转成可读文本写日志 → 没有返回值,但可能消耗 stderr 缓冲并产生日志。
调用关系:push_process_output 在发现输出来源是 stderr 时调用它。它和 stdout 解析路径分开,保证错误输出不会污染 rmcp 的 JSON-RPC 消息流。
调用图:调用 2 个内部函数(extend_from_slice, take_line);被 1 处调用(push_process_output);外部调用 2 个(trim_trailing_carriage_return, info!)。
ExecutorProcessTransport::flush_stderr360–369 ↗
fn flush_stderr(&mut self)
作用:在进程结束时,把 stderr 缓冲里最后剩下但没换行的内容也写进日志。这样最后一句错误信息不会因为缺少换行而丢掉。
数据流:进去的是当前 stderr 缓冲 → 如果没有剩余内容就直接结束;如果有,就一次性取出并写成一条日志 → 没有返回值,但会清空 stderr 缓冲里的残留内容。
调用关系:receive_message 在确认通道关闭、准备返回结束之前调用它。它补上 push_stderr 只按完整行输出日志时留下的最后一截。
调用图:被 1 处调用(receive_message);外部调用 2 个(info!, take_remaining)。
ExecutorProcessTransport::trim_trailing_carriage_return371–376 ↗
fn trim_trailing_carriage_return(mut line: BytesMut) -> BytesMut
作用:去掉一行末尾可能存在的回车符。它兼容 Windows 风格换行,也就是一行可能以“回车加换行”结束。
数据流:进去的是一行字节 → 它检查最后一个字节是不是回车符 \r;如果是就删掉,不是就保持原样 → 返回清理后的字节行。
调用关系:take_stdout_message 在解析 JSON 前调用它,push_stderr 在写日志前也调用它。这样无论消息还是日志,都不会多出一个看不见但碍事的回车字符。
ExecutorProcessTransport::drop384–406 ↗
fn drop(&mut self)
作用:当这个传输对象被销毁时,尽量兜底终止 MCP 服务器进程。它防止调用方忘记 close 后留下还在运行的子进程。
数据流:进去没有显式参数,但会读取对象里的 terminated 标记、进程句柄和程序名 → 如果已经正常终止过,就什么也不做;否则尝试取得当前 Tokio 运行时,Tokio 是 Rust 里常用的异步任务调度器;拿到后启动一个后台任务调用 terminate;拿不到或终止失败就写警告日志 → 没有返回值,但可能安排一次异步终止操作。
调用关系:它不是被业务代码直接调用,而是在 ExecutorProcessTransport 生命周期结束时由 Rust 自动触发。它和 close 配合:close 是主动收尾,drop 是最后保险。
HTTP 能力实现
这些执行器侧和编排器侧客户端提供具体的共享 HTTP 能力,供高级 MCP HTTP 适配使用。
exec-server/src/client/reqwest_http_client.rs源码 ↗
这个文件解决的是“谁来真的上网发请求”的问题。项目里别的部分只需要说清楚方法、网址、请求头、请求体、是否要流式返回;这里负责检查这些信息是否合法,然后用 reqwest(Rust 里常用的 HTTP 客户端库)把请求发出去。它有两种工作方式:一种是普通请求,等整个响应内容都读完后一次性返回;另一种是流式请求,先返回状态码和响应头,后面的响应内容像水管流水一样一块一块往外送。这样大文件或长连接响应就不用一次全塞进内存。文件还会处理超时、自定义证书、非法网址、非法请求头,以及发送失败时的日志。可以把它想成快递柜台:外面的人填单,这里检查地址和格式,真的把包裹寄出去,再把回执和包裹内容交回来。
ReqwestHttpClient::build_client53–62 ↗
fn build_client(timeout_ms: Option<u64>) -> Result<reqwest::Client, ExecServerError>
作用:创建一个真正能发 HTTP 请求的 reqwest 客户端。它会按需要设置超时时间,并接入项目支持的自定义证书。
数据流:输入是一个可选的超时时间,单位是毫秒。它先做一个 reqwest 客户端构造器;如果给了超时,就把毫秒转成时间长度并设置进去;然后交给项目里的证书构建函数补上自定义 CA(可以理解为额外信任的证书机构)。成功时输出可用的 HTTP 客户端,失败时输出项目自己的请求错误。
调用关系:它是底层准备步骤,由 ReqwestHttpRequestRunner::new 调用。也就是说,每次要创建请求执行器时,都会先通过它把真正干活的网络客户端准备好。
调用图:被 1 处调用(new);外部调用 3 个(from_millis, builder, build_reqwest_client_with_custom_ca)。
ReqwestHttpClient::http_request66–83 ↗
fn http_request(
&self,
params: HttpRequestParams,
) -> BoxFuture<'_, Result<HttpRequestResponse, ExecServerError>>
作用:发一个普通 HTTP 请求,并等响应正文全部读完后再返回。适合小到中等大小、可以一次性拿完的响应。
数据流:输入是项目内部的 HTTP 请求参数,包括方法、网址、请求头、请求体、超时等。它先创建 ReqwestHttpRequestRunner,然后强制把 stream_response 设为 false,表示不要流式返回;执行请求后拿到完整响应,并把错误转换成 ExecServerError。输出是包含状态码、响应头和完整响应正文的 HttpRequestResponse。
调用关系:它实现了 HttpClient 这个统一接口,所以项目其他地方可以不关心底层是不是 reqwest。它把真正的发送和读取工作交给 ReqwestHttpRequestRunner::new 和 ReqwestHttpRequestRunner::run。
调用图:调用 1 个内部函数(new)。
ReqwestHttpClient::http_request_stream85–110 ↗
fn http_request_stream(
&self,
params: HttpRequestParams,
) -> BoxFuture<'_, Result<(HttpRequestResponse, HttpResponseBodyStream), ExecServerError>>
作用:发一个支持流式读取响应正文的 HTTP 请求。它先返回响应的基本信息,再把正文包装成一个本地可读的流。
数据流:输入同样是 HTTP 请求参数。它先创建请求执行器,然后把 stream_response 设为 true,告诉底层不要立刻读完整个正文;run 返回状态码、响应头和一个待继续读取的响应流。它检查这个流是否真的存在,再把 reqwest 的响应流包装成 HttpResponseBodyStream。输出是响应信息加上后续可读取的正文流;如果没有拿到流,会返回协议错误。
调用关系:它也是 HttpClient 统一接口的一部分。它调用 ReqwestHttpRequestRunner::new 准备客户端,调用 ReqwestHttpRequestRunner::run 发请求,再调用 HttpResponseBodyStream::local 把本地 reqwest 响应变成项目通用的流对象。
ReqwestHttpRequestRunner::new114–118 ↗
fn new(timeout_ms: Option<u64>) -> Result<Self, JSONRPCErrorError>
作用:创建一个请求执行器。执行器里装着已经配置好的 reqwest 客户端,之后就可以用它来发请求。
数据流:输入是可选的超时时间。它调用 ReqwestHttpClient::build_client 创建底层 HTTP 客户端;如果创建失败,就把错误包装成 JSON-RPC 的内部错误。成功时输出 ReqwestHttpRequestRunner,其中保存着可复用的 reqwest::Client。
调用关系:它位于“准备发请求”的入口位置。ReqwestHttpClient::http_request、ReqwestHttpClient::http_request_stream,以及其他需要直接跑请求的地方都会先调用它,然后再调用 ReqwestHttpRequestRunner::run。
调用图:调用 1 个内部函数(build_client);被 3 处调用(http_request, http_request_stream, http_request)。
ReqwestHttpRequestRunner::run120–185 ↗
async fn run(
&self,
params: HttpRequestParams,
) -> Result<(HttpRequestResponse, Option<PendingReqwestHttpBodyStream>), JSONRPCErrorError>
作用:这是实际发 HTTP 请求的核心函数。它检查请求参数,组装网络请求,发送出去,然后按普通或流式模式返回结果。
数据流:输入是 HttpRequestParams,里面有请求 ID、方法、网址、请求头、请求体、是否流式返回等。它先把方法字符串转成 HTTP 方法,把网址字符串解析成 URL,并只允许 http 和 https;再把项目格式的请求头转成 reqwest 能用的请求头。如果有请求体,就放进请求里。然后它发送请求:发送失败会记录日志并返回内部错误;发送成功后,它取出状态码和响应头。如果要求流式返回,它不读取正文,而是把响应保存到 PendingReqwestHttpBodyStream 里交出去;如果不是流式,它会把整个响应正文读成字节数组后返回。
调用关系:它是本文件最关键的执行点。上层的普通请求和流式请求都会走到这里。它会调用 ReqwestHttpRequestRunner::build_headers 转换请求头,调用 ReqwestHttpRequestRunner::response_headers 转换响应头,遇到发送失败时调用 log_send_error 记录更详细的原因。
调用图:调用 3 个内部函数(log_send_error, internal_error, invalid_params);外部调用 7 个(from_bytes, build_headers, response_headers, parse, new, request, format!)。
ReqwestHttpRequestRunner::stream_body187–244 ↗
async fn stream_body(
pending_stream: PendingReqwestHttpBodyStream,
notifications: RpcNotificationSender,
)
作用:把一个已经开始的流式 HTTP 响应正文,一块一块发送成通知。它用于远端或下游需要逐段接收响应内容的场景。
数据流:输入是 PendingReqwestHttpBodyStream,里面有请求 ID 和还没读完的 reqwest 响应;还输入一个 RpcNotificationSender,用来发通知。它从响应里不断读取字节块,每读到一块就生成一条 HttpRequestBodyDeltaNotification,带上递增的序号、这块数据和 done=false。如果读取中出错,它发送一条 done=true 且带错误信息的通知后结束。读到末尾时,它发送一条 done=true 且没有错误的通知。它主要改动的是外部可见的通知流,而不是返回一个值。
调用关系:它接在 ReqwestHttpRequestRunner::run 的流式模式之后使用。run 先拿到响应头和待读取的正文;stream_body 负责后半段,把正文通过 send_body_delta 继续推给接收方。
调用图:外部调用 2 个(new, send_body_delta)。
ReqwestHttpRequestRunner::build_headers246–261 ↗
fn build_headers(headers: Vec<HttpHeader>) -> Result<HeaderMap, JSONRPCErrorError>
作用:把项目内部表示的请求头,转换成 reqwest 能直接发送的请求头格式。它也负责发现不合法的请求头名称或值。
数据流:输入是一组 HttpHeader,每个包含 name 和 value。它逐个把 name 转成 HeaderName,把 value 转成 HeaderValue;如果某个名称或值不符合 HTTP 规则,就返回 invalid_params 错误,告诉调用方参数本身有问题。成功时输出 HeaderMap,也就是 reqwest 能放进请求里的请求头集合。
调用关系:它是 ReqwestHttpRequestRunner::run 组装请求时的一个小关卡。run 在真正发送前调用它,确保坏的请求头不会被带到网络层。
ReqwestHttpRequestRunner::response_headers263–273 ↗
fn response_headers(headers: &HeaderMap) -> Vec<HttpHeader>
作用:把 reqwest 返回的响应头,转换成项目协议里使用的响应头列表。
数据流:输入是 reqwest 的 HeaderMap。它遍历每个响应头,把名称转成字符串;值也尝试转成普通字符串。那些不能安全转成字符串的值会被跳过。输出是 Vec<HttpHeader>,也就是项目内部能序列化、能传给其他模块的响应头列表。
调用关系:它在 ReqwestHttpRequestRunner::run 收到服务器响应后使用。run 需要把底层库的响应格式变成项目自己的协议格式,所以会调用它。
调用图:外部调用 1 个(iter)。
log_send_error276–287 ↗
fn log_send_error(method: &Method, error: reqwest::Error)
作用:在 HTTP 请求发送失败时,记录一条更有用的警告日志。它会标出是不是超时、是不是连接失败,并尽量保留底层错误原因。
数据流:输入是本次请求的方法和 reqwest 返回的错误。它先去掉错误里的 URL,避免日志里泄露完整地址或敏感查询参数;再调用 error_source_chain 收集更底层的错误链。最后用 tracing 写一条 warn 级别日志。它不返回业务结果,只产生日志这个副作用。
调用关系:它由 ReqwestHttpRequestRunner::run 在 request.send 失败时调用。run 负责把错误返回给调用方,而 log_send_error 负责留下排查问题用的现场记录。
调用图:调用 1 个内部函数(error_source_chain);被 1 处调用(run);外部调用 2 个(without_url, warn!)。
error_source_chain289–297 ↗
fn error_source_chain(error: &reqwest::Error) -> Option<String>
作用:把一个 reqwest 错误背后的层层原因整理成一段文字。这样日志里不只看到表面错误,也能看到更底层的失败原因。
数据流:输入是 reqwest::Error。它从 error.source() 开始,一层一层往下找原因,把每一层错误转成字符串并收集起来。如果找到了至少一个原因,就用冒号连接成一段文字返回;如果没有更底层原因,就返回空。
调用关系:它是 log_send_error 的辅助函数。log_send_error 需要更完整的错误背景时,会调用它来整理错误链,然后一起写进日志。
调用图:被 1 处调用(log_send_error);外部调用 3 个(new, source, to_string)。
exec-server/src/client/rpc_http_client.rs源码 ↗
这个文件解决的是“谁来真正发 HTTP 请求”的问题。调度端只负责指挥,不直接碰网络;它把请求包装成 JSON-RPC(用 JSON 格式表达的一次远程函数调用)里的 http/request 消息,发给远端运行环境。普通请求会把响应体一次性收回来。流式请求则像收快递分批到货:先拿到响应头和基本信息,后面的正文片段会通过 http/request/bodyDelta 通知不断送回来。为了不把不同请求的正文片段混在一起,代码会给每个流式响应生成一个本连接内唯一的 request_id。它还创建一个异步通道(可以理解成任务之间传纸条的队列)来暂存正文片段,最多排 256 个片段。若请求失败,它会立刻撤销登记,避免留下没人收的队列。
ExecServerClient::http_request73–78 ↗
fn http_request(
&self,
params: HttpRequestParams,
) -> BoxFuture<'_, Result<HttpRequestResponse, ExecServerError>>
作用:发一个普通 HTTP 请求,并把响应正文一次性缓冲回来。调用者用它时,不需要知道背后是远端运行环境在真正访问网络。
数据流:进去的是一份 HTTP 请求参数。函数会把参数里的“流式响应”开关关掉,然后通过共享的 JSON-RPC 连接发送 http/request 调用。出来的是远端返回的 HTTP 响应结果;如果远端或连接出错,就返回错误。
调用关系:它是 ExecServerClient 给 HTTP 客户端接口提供的普通请求入口。上层代码想要完整响应体时会走这里;它把真正的发送工作交给底层 JSON-RPC 调用,不自己建立网络连接。
调用图:外部调用 1 个(http_request)。
ExecServerClient::http_request_stream82–87 ↗
fn http_request_stream(
&self,
params: HttpRequestParams,
) -> BoxFuture<'_, Result<(HttpRequestResponse, HttpResponseBodyStream), ExecServerError>>
作用:发一个 HTTP 请求,但不把响应正文一次性攒完,而是返回一个可以逐段读取正文的流。适合下载大文件或响应很长的场景,避免一次性占太多内存。
数据流:进去的是 HTTP 请求参数。函数会打开“流式响应”开关,生成一个新的 request_id,创建一个异步通道来接收正文片段,并把这个通道登记到连接内部。随后它发送远程 HTTP 请求。成功时,出来的是响应信息加一个 HttpResponseBodyStream,调用者之后可以从这个流里读正文片段;失败时,它会删除刚才的登记,避免脏数据残留。
调用关系:它用于上层代码需要边到边读响应体的时候。它会创建登记对象和通道,把正文片段的接收交给共享连接上的通知处理流程,再用 HttpResponseBodyStream::remote 包成调用者能读取的流。这样,远端不断发来的 body delta 通知就能准确送到对应请求。
调用图:调用 2 个内部函数(new, remote);外部调用 3 个(clone, http_request_stream, channel)。
可流式 HTTP 适配
这些组件将共享 HTTP 能力适配为 RMCP 可流式 HTTP 语义,包括感知身份验证的错误解析,以及围绕初始化的重试。
rmcp-client/src/http_client_adapter.rs源码 ↗
RMCP 是一套用 JSON-RPC 消息通信的协议,JSON-RPC 可以简单理解成“用 JSON 格式发请求、回响应”。这个文件里的 StreamableHttpClientAdapter 就是协议和 HTTP 之间的翻译员。它会给请求加上必要的头,比如内容类型、会话 ID、授权信息;把要发送的消息转成 JSON;再通过共享的 HttpClient 发 POST、GET 或 DELETE。它还会检查服务器回来的状态码:比如 404 可能表示会话过期,401 表示需要登录授权,403 可能表示权限范围不够。返回内容如果是普通 JSON,它会解析成协议消息;如果是 text/event-stream,也就是 SSE(服务器持续推送事件的一条长连接),它会包装成可持续读取的流。这样上层只需要说“我要发 RMCP 消息”,不用自己处理 HTTP 细节、错误分类和流式响应。
StreamableHttpClientAdapter::new70–80 ↗
fn new(
http_client: Arc<dyn HttpClient>,
default_headers: HeaderMap,
auth_provider: Option<SharedAuthProvider>,
) -> Self
作用:创建一个新的 HTTP 转接器,把底层发请求的工具、默认请求头、可选的认证来源装在一起。后面所有 RMCP 的 HTTP 通信都会靠这个对象完成。
数据流:进去的是一个共享的 HttpClient、默认 HTTP 头、以及可能存在的认证提供者 → 函数把它们保存到 StreamableHttpClientAdapter 里 → 出来的是一个可复用、可克隆的适配器对象。
调用关系:它在创建传输层时被 create_pending_transport 和 create_oauth_transport_and_runtime 调用,相当于启动一条 RMCP HTTP 通道前先把“转接头”组装好。
调用图:被 2 处调用(create_pending_transport, create_oauth_transport_and_runtime)。
StreamableHttpClientAdapter::post_message86–229 ↗
async fn post_message(
&self,
uri: Arc<str>,
message: ClientJsonRpcMessage,
session_id: Option<Arc<str>>,
auth_token: Option<String>,
custom_headers: Ha
作用:把一条 RMCP 客户端消息通过 HTTP POST 发给服务器,并根据服务器回应判断是已接收、返回 JSON,还是打开了一条 SSE 事件流。它是发送协议消息的主入口。
数据流:进去的是目标地址、要发的 JSON-RPC 消息、可选会话 ID、可选授权令牌和自定义请求头 → 它提取消息方法和请求 ID,合并默认头和自定义头,加认证、Accept、Content-Type、会话 ID 等信息,把消息序列化成 JSON 字节,再交给 HttpClient 发 POST 流式请求 → 出来可能是 Accepted、JSON 响应、SSE 流,或者带明确原因的错误;它也会在失败时读取响应体,尽量给出可读的错误摘要。
调用关系:这是 RMCP 发送消息时的核心流程。它会调用 add_auth_headers 补认证头,client_jsonrpc_message_fields 提取日志和重试判断用的信息,insert_header 安全写入请求头,protocol_headers 转成底层 HttpClient 认识的格式;收到响应后用 response_header、status_is_success、collect_body、parse_json_rpc_error、sse_stream_from_body 等函数把 HTTP 结果翻译回 RMCP 能理解的结果。HTTP 请求本身失败时,它会交给 log_post_message_http_error 记录一条避免泄露敏感信息的警告。
调用图:调用 12 个内部函数(add_auth_headers, client_jsonrpc_message_fields, collect_body, insert_header, log_post_message_http_error, parse_json_rpc_error, protocol_headers, response_header, retryable_post_response_status, sse_stream_from_body (+2 more));外部调用 16 个(new, clone, from_static, new, AuthRequired, Client, InsufficientScope, UnexpectedContentType, UnexpectedServerResponse, Json (+6 more))。
StreamableHttpClientAdapter::delete_session231–280 ↗
async fn delete_session(
&self,
uri: Arc<str>,
session: Arc<str>,
auth_token: Option<String>,
custom_headers: HashMap<HeaderName, reqwest::header::HeaderValue>,
作用:向服务器发送 HTTP DELETE,请求关闭某个 RMCP 会话。它用于告诉服务器“这个会话不用了,可以清理”。
数据流:进去的是目标地址、会话 ID、可选授权令牌和自定义请求头 → 它合并请求头,加认证和 Mcp-Session-Id,然后通过 HttpClient 发 DELETE → 如果服务器成功或表示不支持 DELETE 但可忽略,就返回成功;如果返回其他失败状态,就返回带状态码的错误。
调用关系:它是会话收尾时使用的函数。流程上和 post_message、get_stream 共用 add_auth_headers、insert_header、protocol_headers、status_is_success 这些小工具,保证请求头格式和状态码判断方式一致。
调用图:调用 4 个内部函数(add_auth_headers, insert_header, protocol_headers, status_is_success);外部调用 4 个(clone, from_static, UnexpectedServerResponse, format!)。
StreamableHttpClientAdapter::get_stream282–367 ↗
async fn get_stream(
&self,
uri: Arc<str>,
session_id: Arc<str>,
last_event_id: Option<String>,
auth_token: Option<String>,
custom_headers: HashMap<Head
作用:用 HTTP GET 打开或恢复一条服务器事件流,也就是 SSE 长连接。它让客户端能持续收到服务器推送的 RMCP 消息或事件。
数据流:进去的是目标地址、会话 ID、可选的 last_event_id、可选授权令牌和自定义请求头 → 它组装请求头,告诉服务器自己接受事件流或 JSON,并带上会话信息和断点事件 ID,然后用 HttpClient 发 GET 流式请求 → 出来是一条可以不断读 SSE 事件的流;如果服务器不支持、会话过期、状态码异常或内容类型不对,就返回对应错误。
调用关系:它在需要独立拉取服务器推送流时运行。它先借助 add_auth_headers、insert_header、protocol_headers 准备请求,再用 response_header 和 is_streamable_http_content_type 确认服务器真的返回了可流式处理的内容,最后把底层字节流交给 sse_stream_from_body 变成 SSE 事件流。
调用图:调用 7 个内部函数(add_auth_headers, insert_header, is_streamable_http_content_type, protocol_headers, response_header, sse_stream_from_body, status_is_success);外部调用 6 个(clone, from_static, Client, UnexpectedContentType, UnexpectedServerResponse, format!)。
StreamableHttpClientAdapter::add_auth_headers371–375 ↗
fn add_auth_headers(&self, headers: &mut HeaderMap)
作用:把统一认证提供者里的授权请求头加到当前请求里。这样每个请求不用重复写认证拼装代码。
数据流:进去的是一组准备发送的 HTTP 头 → 如果适配器里有认证提供者,它就取出认证头并合并进去 → 出来的还是同一组 HTTP 头,但可能多了 Authorization 等认证相关内容。
调用关系:post_message、delete_session 和 get_stream 都会先调用它。它像一个统一盖章的步骤,保证三类 HTTP 请求都能带上同一套认证信息。
调用图:被 3 处调用(delete_session, get_stream, post_message);外部调用 1 个(extend)。
body_preview378–393 ↗
fn body_preview(body: impl Into<String>) -> String
作用:把服务器返回的正文变成一段适合放进错误信息里的预览。它会限制长度,避免错误日志里塞进超大的内容。
数据流:进去的是一段响应正文文本 → 它检查长度,如果超过限制,就在合法的字符边界处截断,并追加“被截断多少字节”的提示 → 出来是一段较短、可安全展示的字符串。
调用关系:它主要服务于 post_message 的错误处理。当服务器返回了不是预期 JSON 或内容类型不对的正文时,post_message 会用它生成简短提示,让人排查问题时能看到一点线索,又不至于日志爆炸。
client_jsonrpc_message_fields395–426 ↗
fn client_jsonrpc_message_fields(
message: &ClientJsonRpcMessage,
) -> (Option<String>, Option<String>)
作用:从一条客户端 JSON-RPC 消息里提取“方法名”和“请求 ID”。这些信息主要用于日志和判断某些请求是否适合特殊处理。
数据流:进去的是一条 ClientJsonRpcMessage,可能是请求、响应、通知或错误 → 它按消息种类找出方法名和 ID:请求有方法和 ID,通知通常只有方法,响应和错误通常只有 ID → 出来是两个可选字符串。
调用关系:post_message 在真正发 HTTP 前调用它。这样一旦 HTTP 失败,log_post_message_http_error 能记录是哪类 RMCP 方法失败;后面判断某些 POST 错误是否可按 JSON-RPC 错误处理时,也会用到方法名。
调用图:被 1 处调用(post_message)。
log_post_message_http_error428–456 ↗
fn log_post_message_http_error(
uri: &str,
mcp_method: Option<&str>,
mcp_request_id: Option<&str>,
has_session_id: bool,
has_authorization_header: bool,
)
作用:在 POST 请求本身失败时写一条警告日志,帮助排查连接问题。它记录的是地址的结构信息和请求特征,而不是完整敏感内容。
数据流:进去的是目标地址、RMCP 方法名、请求 ID、是否有会话 ID、是否有授权头 → 它尝试解析 URL,只取协议、主机、路径、是否带查询参数等信息,再连同消息特征写入警告日志 → 出来没有返回值,只产生一条日志。
调用关系:它只被 post_message 在底层 HTTP 请求报错时调用。它不修复错误,但给运维或开发者留下线索,同时避免直接把完整 URL 或令牌写进日志。
调用图:被 1 处调用(post_message);外部调用 2 个(parse, warn!)。
insert_header458–471 ↗
fn insert_header(
headers: &mut HeaderMap,
name: HeaderName,
value: String,
map_error: impl FnOnce(String) -> Error,
) -> std::result::Result<(), StreamableHttpError<Error>>
作用:安全地往 HTTP 头里放一个键值。它会先检查这个值是不是合法的 HTTP 头值,避免发出格式坏掉的请求。
数据流:进去的是可修改的 HeaderMap、头名称、字符串形式的头值、以及把错误转换成业务错误的方法 → 它把字符串转成 reqwest 认识的 HeaderValue,成功后插入 HeaderMap;如果值非法,就返回一个客户端错误 → 出来是成功标记,或一个说明哪个头不合法的错误。
调用关系:post_message、delete_session 和 get_stream 都用它添加 Accept、Content-Type、Authorization、会话 ID 等头。它把“写请求头并处理格式错误”这件事集中起来,避免每个流程重复写。
调用图:被 3 处调用(delete_session, get_stream, post_message);外部调用 2 个(insert, from_str)。
is_streamable_http_content_type473–480 ↗
fn is_streamable_http_content_type(content_type: &str) -> bool
作用:判断服务器返回的 Content-Type 是否是这个协议能流式处理的类型。它接受 SSE 事件流,也接受 JSON。
数据流:进去的是一个内容类型字符串 → 它检查开头是不是 text/event-stream 或 application/json → 出来是真或假,表示这个响应内容能不能按 Streamable HTTP 的规则继续处理。
调用关系:get_stream 在打开服务器流后调用它。只有通过这个检查,get_stream 才会把底层字节流交给 sse_stream_from_body;否则会立刻报“内容类型不符合预期”。
调用图:被 1 处调用(get_stream)。
protocol_headers482–492 ↗
fn protocol_headers(headers: &HeaderMap) -> Vec<HttpHeader>
作用:把 reqwest 使用的请求头格式,转换成底层 HttpClient 使用的简单头列表。它是两个 HTTP 表示方式之间的小翻译器。
数据流:进去的是 HeaderMap → 它逐个读取头名和头值,只保留能转成普通字符串的头 → 出来是一组 HttpHeader,每个里面有 name 和 value。
调用关系:post_message、delete_session 和 get_stream 在调用底层 HttpClient 前都会用它。因为 HttpClient 不直接吃 HeaderMap,所以这一步是发请求前必须经过的格式转换。
调用图:被 3 处调用(delete_session, get_stream, post_message);外部调用 1 个(iter)。
response_header494–500 ↗
fn response_header(headers: &[HttpHeader], name: impl AsRef<str>) -> Option<String>
作用:从服务器返回的头列表里按名字找一个头,而且不区分大小写。HTTP 头名本来就应该大小写不敏感,所以这能避免因为大小写不同找不到。
数据流:进去的是响应头列表和要找的头名 → 它遍历列表,用大小写不敏感的方式比较名字 → 出来是找到的头值字符串,找不到就返回空。
调用关系:post_message 用它读取 Content-Type、Mcp-Session-Id、WWW-Authenticate 等响应头;get_stream 用它读取 Content-Type。它是响应解释阶段的小工具。
调用图:被 2 处调用(get_stream, post_message);外部调用 2 个(as_ref, iter)。
status_is_success502–504 ↗
fn status_is_success(status: u16) -> bool
作用:判断一个 HTTP 状态码是不是成功状态。简单说,就是看它是不是 2xx 这一类。
数据流:进去的是数字形式的 HTTP 状态码 → 它先把数字转换成标准 StatusCode,再问这个状态码是否代表成功 → 出来是真或假;如果状态码本身不合法,也会当作不成功。
调用关系:post_message、delete_session 和 get_stream 都用它统一判断响应是否成功。这样不同请求不会各自用一套不一致的成功标准。
调用图:被 3 处调用(delete_session, get_stream, post_message);外部调用 1 个(from_u16)。
retryable_post_response_status506–515 ↗
fn retryable_post_response_status(mcp_method: Option<&str>, status: u16) -> bool
作用:判断某个 POST 返回的失败状态,是否属于“这个特定 RMCP 方法可以考虑重试或不要立刻当成普通 JSON-RPC 错误”的情况。
数据流:进去的是 RMCP 方法名和 HTTP 状态码 → 它先确认状态码有效,再看是不是常见的临时失败,比如超时、限流、服务器暂时不可用;同时还要求方法名是 initialize、notifications/initialized 或 tools/list 这几个指定方法 → 出来是真或假。
调用关系:post_message 在处理非成功响应时调用它。它内部再调用 is_retryable_http_status 判断 HTTP 状态本身是否属于可重试类别,然后结合 RMCP 方法名做更窄的判断。
调用图:调用 1 个内部函数(is_retryable_http_status);被 1 处调用(post_message);外部调用 2 个(from_u16, matches!)。
is_retryable_http_status517–527 ↗
fn is_retryable_http_status(status: StatusCode) -> bool
作用:判断一个 HTTP 状态码是不是通常可以稍后再试的临时问题。比如请求超时、被限流、网关错误或服务暂时不可用。
数据流:进去的是一个标准 StatusCode → 它和一组固定的临时失败状态做匹配 → 出来是真或假。
调用关系:它只被 retryable_post_response_status 调用。它负责纯 HTTP 层面的判断,外层函数再结合具体 RMCP 方法决定最终结果。
调用图:被 1 处调用(retryable_post_response_status);外部调用 1 个(matches!)。
parse_json_rpc_error529–534 ↗
fn parse_json_rpc_error(body: &[u8]) -> Option<ServerJsonRpcMessage>
作用:尝试把一段响应正文解析成 JSON-RPC 错误消息。它只在正文确实是协议错误时返回结果。
数据流:进去的是服务器响应的原始字节 → 它按 ServerJsonRpcMessage 解析 JSON,并确认解析出来的是 Error 类型 → 出来是这个错误消息;如果解析失败或不是错误消息,就返回空。
调用关系:post_message 在 HTTP 状态码失败但正文像 JSON 时调用它。这样服务器如果用 JSON-RPC 的格式解释错误,客户端就能把它当作协议消息返回,而不是只给一个笼统的 HTTP 错误。
调用图:被 1 处调用(post_message)。
collect_body536–549 ↗
async fn collect_body(
body_stream: &mut HttpResponseBodyStream,
) -> std::result::Result<Vec<u8>, StreamableHttpError<StreamableHttpClientAdapterError>>
作用:把一段流式响应体完整读完,收集成一整块字节。它适合处理需要一次性解析的 JSON 或错误正文。
数据流:进去的是一个可继续接收数据块的 HttpResponseBodyStream → 它不断 recv 读取下一块字节,直到流结束;读取失败就转成适配器错误 → 出来是一整个 Vec<u8> 字节数组。
调用关系:post_message 在需要解析 JSON 响应、解析 JSON-RPC 错误,或生成错误正文预览时调用它。和 sse_stream_from_body 不同,它不是边来边处理,而是先把正文全部收齐。
调用图:调用 1 个内部函数(recv);被 1 处调用(post_message);外部调用 1 个(new)。
sse_stream_from_body551–562 ↗
fn sse_stream_from_body(
body_stream: HttpResponseBodyStream,
) -> BoxStream<'static, std::result::Result<Sse, sse_stream::Error>>
作用:把底层 HTTP 响应的字节流,包装成 SSE 事件流。SSE 可以理解成服务器不断往客户端推送一条条文本事件的长连接格式。
数据流:进去的是 HttpResponseBodyStream,也就是一条不断吐出字节块的响应体 → 它把每个字节块转成 SSE 解析器能读的形式,遇到读取错误时包装成输入输出错误 → 出来是一条 BoxStream,调用者可以持续从里面拿到 Sse 事件或错误。
调用关系:post_message 在服务器用 text/event-stream 回应 POST 时调用它;get_stream 在 GET 成功打开事件流后也调用它。它是从“HTTP 字节流”到“协议事件流”的最后一道转换。
调用图:被 2 处调用(get_stream, post_message);外部调用 2 个(from_byte_stream, unfold)。
rmcp-client/src/http_client_adapter/www_authenticate.rs源码 ↗
当客户端请求服务器时,服务器可能用 WWW-Authenticate 这个 HTTP 头告诉客户端:你需要重新认证,或者缺少某个权限。这个文件就像一个“认证提示翻译器”,专门从这些头里找 Bearer 认证,也就是常见的令牌认证提示,并进一步确认错误是不是 insufficient_scope,意思是“权限范围不够”。它会小心解析 HTTP 头,因为这里的逗号、分号、引号和反斜杠都有特殊含义,不能简单按字符乱切。文件先把头拆成不会破坏引号内容的小段,再识别认证方案、参数名和值,最后只接受格式合法的 scope。如果找到了有效提示,就返回原始头内容和服务器要求的权限范围。这样上层代码在发送消息失败时,可以知道是不是应该提示用户授权更多权限。
BearerChallenge::add_parameter33–48 ↗
fn add_parameter(&mut self, name: &str, value: Option<String>)
作用:这个函数把一个认证参数放进当前的 Bearer 挑战里,只关心 error 和 scope 两个名字。它还会防止同一个参数重复出现,或者出现没有值的参数,因为这些情况会让含义变得不可靠。
数据流:输入是一个参数名和一个可能存在的参数值。它先判断参数名是不是 error 或 scope,不是就直接忽略;如果是,就把值记到对应位置。原来没有值且这次有值时会保存下来;如果没给值、重复给值,或之前已经判定无效,就把这个参数标成无效。
调用关系:它是解析 Bearer 认证提示时的“小记账员”。parse_bearer_insufficient_scope 在读到参数后会调用它,把零散的参数逐个塞进 BearerChallenge,等参数收集完后再交给 BearerChallenge::into_insufficient_scope 判断是否真的有用。
调用图:外部调用 1 个(Value)。
BearerChallenge::into_insufficient_scope50–62 ↗
fn into_insufficient_scope(self) -> Option<BearerInsufficientScope>
作用:这个函数把已经收集好的 Bearer 挑战转换成“权限范围不够”的结果。只有当 error 明确等于 insufficient_scope 时,它才认为这是目标错误。
数据流:输入是一个完整的 BearerChallenge。它检查里面的 error:如果不是有效的 insufficient_scope,就输出空结果;如果是,再检查 scope 是否存在且格式合法。合法就带着权限范围输出,不合法或缺失就输出一个没有具体权限范围的结果。
调用关系:它是 Bearer 挑战解析的最后一道判断。parse_bearer_insufficient_scope 在遇到新挑战或读完整个头时调用它;它自己会调用 valid_scope 来确认服务器给的权限范围是不是符合规则。
调用图:调用 1 个内部函数(valid_scope)。
insufficient_scope_challenge67–81 ↗
fn insufficient_scope_challenge(
headers: &[HttpHeader],
) -> Option<InsufficientScopeChallenge>
作用:这个函数从一组 HTTP 头里找出第一个表示“权限范围不够”的 WWW-Authenticate 头。上层请求失败时会用它判断服务器是不是在要求更多授权。
数据流:输入是一组 HTTP 头。它逐个查看名字是不是 WWW-Authenticate,不是就跳过;是的话就把头的值交给解析函数。只要某个头里解析出 Bearer 的 insufficient_scope,它就返回原始头值和可能的所需权限范围;如果全都没有匹配,就返回空结果。
调用关系:它是这个文件对外提供给同模块使用的入口。调用图显示它会被 post_message 使用,也就是发送消息收到响应后,如果服务器拒绝请求,就通过它理解认证失败的具体原因;它把真正的文本解析工作交给 parse_bearer_insufficient_scope。
调用图:被 1 处调用(post_message);外部调用 1 个(iter)。
parse_bearer_insufficient_scope98–128 ↗
fn parse_bearer_insufficient_scope(header: &str) -> Option<BearerInsufficientScope>
作用:这个函数解析单个 WWW-Authenticate 头的文本,寻找 Bearer 认证里是否包含 insufficient_scope 错误。它处理的是最麻烦的部分:HTTP 头里可能有多个认证挑战和多个参数。
数据流:输入是一整段认证头字符串。它先用 split_unquoted_segments 按逗号和分号拆段,但不会拆开引号里的内容;然后逐段判断这是参数还是新认证挑战。如果正在读取 Bearer 挑战,就把参数加进去;如果遇到新的挑战,就先结算前一个 Bearer 是否是权限不足。最后输出找到的权限不足信息,或者空结果。
调用关系:它是核心解析器,由 insufficient_scope_challenge 调用。它会把拆分工作交给 split_unquoted_segments,把参数识别交给 parse_auth_param,把新挑战开头识别交给 parse_challenge_start,并用 BearerChallenge 暂存和判断解析结果。
调用图:调用 3 个内部函数(parse_auth_param, parse_challenge_start, split_unquoted_segments);外部调用 1 个(default)。
parse_challenge_start130–142 ↗
fn parse_challenge_start(segment: &str) -> Option<ChallengeStart<'_>>
作用:这个函数判断某一小段是不是一个认证挑战的开头,比如 Bearer ...。它会分出认证方案名,以及开头后面可能紧跟的第一个参数。
数据流:输入是一小段字符串。它先去掉首尾空白,再找第一个空白字符;空白前面当作认证方案名,后面尝试按认证参数解析。只要方案名是合法的 HTTP token,就输出方案名和可选参数;否则输出空结果。
调用关系:它由 parse_bearer_insufficient_scope 在遇到不像普通参数的小段时调用。它会调用 is_http_token 检查方案名是否合法,也会调用 parse_auth_param 解析后面可能紧跟的参数。
调用图:调用 2 个内部函数(is_http_token, parse_auth_param);被 1 处调用(parse_bearer_insufficient_scope)。
parse_auth_param144–148 ↗
fn parse_auth_param(segment: &str) -> Option<AuthParameter<'_>>
作用:这个函数解析认证参数,也就是形如 name=value 的小片段。它负责确认参数名合法,并把参数值解码成普通字符串。
数据流:输入是一小段字符串。它先按第一个等号拆成名字和值;名字去掉空白后必须是合法 HTTP token。通过检查后,它把值交给 parse_auth_param_value 处理,最后输出参数名和可能解析出来的值;如果格式不对,就输出空结果。
调用关系:它是多个解析步骤共用的“小零件”。parse_bearer_insufficient_scope 用它识别普通参数,parse_challenge_start 用它解析挑战开头后面可能附带的参数;它再调用 is_http_token 和 parse_auth_param_value 完成细节检查。
调用图:调用 2 个内部函数(is_http_token, parse_auth_param_value);被 2 处调用(parse_bearer_insufficient_scope, parse_challenge_start)。
parse_auth_param_value150–166 ↗
fn parse_auth_param_value(value: &str) -> Option<String>
作用:这个函数解析认证参数的值。HTTP 允许值是普通 token,也允许用双引号包起来;这个函数把两种写法统一变成普通字符串。
数据流:输入是等号右边的值。如果它以双引号开头,就要求也以双引号结尾,并把里面的反斜杠转义去掉,例如 \" 变成 ";如果不是引号形式,就检查它是不是合法 HTTP token。成功时输出解码后的字符串,失败时输出空结果。
调用关系:它只被 parse_auth_param 调用,是参数解析的底层步骤。它会调用 is_http_token 来验证非引号形式的值是否合法。
调用图:调用 1 个内部函数(is_http_token);被 1 处调用(parse_auth_param);外部调用 1 个(with_capacity)。
split_unquoted_segments168–196 ↗
fn split_unquoted_segments(header: &str) -> Option<Vec<&str>>
作用:这个函数把认证头拆成一段一段,但只在引号外面的逗号和分号处拆。这样可以避免把引号里的真实内容误当成分隔符。
数据流:输入是一整段认证头字符串。它从左到右扫描,记录当前是否在双引号里、是否刚遇到反斜杠转义;只有在不在引号内时,逗号和分号才会切出一个新片段。如果最后发现引号没闭合或转义没结束,就输出空结果;否则输出所有片段。
调用关系:它由 parse_bearer_insufficient_scope 最先调用,相当于先把一长串头内容切成安全的小块。后续的参数解析和挑战解析都建立在它的拆分结果上。
调用图:被 1 处调用(parse_bearer_insufficient_scope);外部调用 1 个(new)。
valid_scope198–205 ↗
fn valid_scope(scope: &str) -> bool
作用:这个函数检查服务器给出的权限范围 scope 是否符合 Bearer 认证标准。它确保权限 token 不为空,也不包含标准不允许的字符。
数据流:输入是一个 scope 字符串。它按空格拆成多个权限 token,然后逐个检查:每个 token 不能为空,并且每个字节只能是标准允许的可见字符。全部通过就输出真,否则输出假。
调用关系:它被 BearerChallenge::into_insufficient_scope 调用,用来决定是否保留服务器给出的 scope。如果 scope 不合法,整体仍可被识别为权限不足,但不会把这个不可靠的权限范围交给上层。
调用图:被 1 处调用(into_insufficient_scope)。
is_http_token207–229 ↗
fn is_http_token(value: &str) -> bool
作用:这个函数检查一段字符串是不是合法的 HTTP token。HTTP token 可以理解为头字段里一种“不带引号的安全单词”。
数据流:输入是一段字符串。它先确认字符串不为空,再检查每个字节是不是英文字母、数字,或 HTTP 规则允许的一小组符号。全部符合就输出真,否则输出假。
调用关系:它是这个文件的通用校验工具,被 parse_challenge_start、parse_auth_param 和 parse_auth_param_value 使用。它保证认证方案名、参数名、普通参数值这些基础部件不会带入非法格式。
调用图:被 3 处调用(parse_auth_param, parse_auth_param_value, parse_challenge_start)。
rmcp-client/src/streamable_http_retry.rs源码 ↗
MCP 可以理解成客户端和工具服务器之间的一套对话协议。客户端连接服务器时,第一步要“握手”,也就是互相确认能不能正常通信。这个文件专门处理一种容易出临时问题的连接方式:streamable HTTP(用 HTTP 建立可持续收发消息的通道)。它先判断当前连接方式要不要重试:本地进程和标准输入输出连接不重试,HTTP 连接才重试。然后它最多尝试三次,中间按 250 毫秒、1000 毫秒短暂停顿。每次失败后,它不会盲目重试,而是检查错误是不是“可能一会儿就好”的类型,比如请求超时、服务器忙、网关错误、HTTP 流中断等。认证失败、权限不够、会话过期这类“再试也没用”的错误不会重试。它还尊重总超时时间,像给整个握手过程设了一个闹钟,闹钟响了就不再等。
RmcpClient::connect_pending_transport_with_initialize_retries26–99 ↗
async fn connect_pending_transport_with_initialize_retries(
&self,
initial_transport: PendingTransport,
client_service: ElicitationClientService,
timeout: Option<Durati
作用:这是这个文件的主流程:拿一个还没真正连上的传输通道,尝试完成 MCP 握手;如果是 HTTP 连接并且遇到临时错误,就等一下再试。它让客户端在服务器刚好不稳定时更耐心,而不是一碰到小波动就失败。
数据流:进去的是一个待连接的传输通道、客户端服务对象和可选的总超时时间。它先判断这种通道是否允许重试,再一次次调用真正的连接函数;失败时检查错误能不能重试,能重试就按预设延迟睡一会儿,同时不断计算剩余超时时间。出来的是已经运行起来的 MCP 服务和可能存在的 OAuth 持久化对象;如果所有尝试都失败,出来的是最后的错误或超时错误。
调用关系:它位于连接初始化的外层,像一个“带重试的包装器”。真正建立连接的活儿交给 connect_pending_transport,需要重新创建通道时交给 create_pending_transport;判断时间交给 remaining_initialize_timeout,等待重试交给 sleep_with_retry_deadline,判断错误是否值得再试交给 RmcpClient::is_retryable_initialize_error。
调用图:调用 2 个内部函数(remaining_initialize_timeout, sleep_with_retry_deadline);外部调用 10 个(from_millis, connect_pending_transport, create_pending_transport, is_retryable_initialize_error, anyhow!, clone, once, timeout, unreachable!, warn!)。
RmcpClient::is_retryable_initialize_error101–110 ↗
fn is_retryable_initialize_error(error: &anyhow::Error) -> bool
作用:这个函数判断一次 MCP 初始化失败,是否属于“可以再试一次”的失败。它会顺着错误链往里看,因为真正原因常常被包在好几层错误里面。
数据流:进去的是一个通用错误对象。它查看这个错误以及它背后的来源错误,寻找握手失败或客户端初始化失败这类具体错误;找到后再交给更细的判断函数。出来的是 true 或 false,表示是否值得重试。
调用关系:它被 RmcpClient::connect_pending_transport_with_initialize_retries 用在每次握手失败之后。它自己不决定等待多久,也不重新连接,只负责回答一个问题:这次失败像不像临时故障。
调用图:外部调用 1 个(chain)。
RmcpClient::is_retryable_client_initialize_error112–135 ↗
fn is_retryable_client_initialize_error(error: &rmcp::service::ClientInitializeError) -> bool
作用:这个函数专门判断 MCP 客户端初始化阶段的错误哪些可以重试。它区分错误发生在“发送初始化请求”还是“发送初始化完成通知”这两个步骤,因为这两步常见的临时故障不完全一样。
数据流:进去的是一个客户端初始化错误。它看错误发生的上下文,再检查底层是不是 streamable HTTP 的传输错误;如果是初始化请求发送失败,就按 HTTP 临时错误规则判断;如果是初始化完成通知失败,还把传输通道关闭也当成可重试。出来的是一个布尔值。
调用关系:它是 RmcpClient::is_retryable_initialize_error 的下一层细分判断。真正识别 HTTP 错误细节的工作又交给 RmcpClient::is_retryable_streamable_http_error。
RmcpClient::is_retryable_streamable_http_error137–165 ↗
fn is_retryable_streamable_http_error(
error: &StreamableHttpError<StreamableHttpClientAdapterError>,
) -> bool
作用:这个函数判断 streamable HTTP 连接里的具体错误,是不是临时问题。它把网络请求失败、某些服务器内部错误、HTTP 流中断、服务器暂时不可用等情况列为可重试。
数据流:进去的是一个 streamable HTTP 错误。它按错误类型逐个匹配:网络请求失败直接认为可重试;特定 JSON-RPC 内部错误要看消息前缀;协议里的 HTTP 响应流失败也可重试;意外 HTTP 响应会继续解析状态码。出来的是 true 或 false;认证、权限、会话过期、内容格式不对等错误会明确返回 false。
调用关系:它被 RmcpClient::is_retryable_client_initialize_error 用来做最底层的错误识别。遇到“意外服务器响应”这种只有文字描述的情况时,它会把文字交给 is_retryable_unexpected_server_response 继续拆。
调用图:调用 1 个内部函数(is_retryable_unexpected_server_response)。
is_retryable_unexpected_server_response168–183 ↗
fn is_retryable_unexpected_server_response(message: &str) -> bool
作用:这个函数从一段“意外服务器响应”的文字里抠出 HTTP 状态码,再判断这个状态码是不是适合重试。它处理的是错误已经变成字符串、但里面还藏着状态码的情况。
数据流:进去的是一段错误消息文本。它先确认文本以 HTTP 开头,然后读取后面的数字,转成标准 HTTP 状态码;如果任何一步解析失败,就认为不能重试。解析成功后,结果交给状态码判断函数,最后出来 true 或 false。
调用关系:它只在 RmcpClient::is_retryable_streamable_http_error 看到意外服务器响应时被调用。它本身不懂哪些状态码该重试,而是把这个决定交给 is_retryable_http_status。
调用图:调用 1 个内部函数(is_retryable_http_status);被 1 处调用(is_retryable_streamable_http_error);外部调用 1 个(from_u16)。
is_retryable_http_status185–195 ↗
fn is_retryable_http_status(status: StatusCode) -> bool
作用:这个函数列出哪些 HTTP 状态码表示“可能只是暂时不行,可以稍后再试”。比如请求超时、请求太多、服务器内部错误、网关错误、服务不可用、网关超时。
数据流:进去的是一个 HTTP 状态码。它把这个状态码和一小组可重试状态码做比较。出来的是 true 或 false,不会改动任何外部状态。
调用关系:它是 HTTP 状态码判断的最后一站,由 is_retryable_unexpected_server_response 调用。上层负责从错误消息里拿到状态码,它只负责判断这个码的含义。
调用图:被 1 处调用(is_retryable_unexpected_server_response);外部调用 1 个(matches!)。
remaining_initialize_timeout197–210 ↗
fn remaining_initialize_timeout(
timeout: Option<Duration>,
deadline: Option<Instant>,
) -> Result<Option<Duration>>
作用:这个函数计算 MCP 握手总超时时间还剩多少。它防止重试过程把用户设置的总等待时间偷偷拖长。
数据流:进去的是原始超时时长和根据它算出的截止时间。没有截止时间时,它返回“没有限制”;有截止时间时,它用当前时间算剩余时长。如果已经没时间了,就生成一个超时错误;否则返回剩余时间。
调用关系:它被 RmcpClient::connect_pending_transport_with_initialize_retries 在每次连接尝试前调用。需要报错时,它会调用 initialize_timeout_error 来生成统一格式的超时消息。
调用图:调用 1 个内部函数(initialize_timeout_error);被 1 处调用(connect_pending_transport_with_initialize_retries);外部调用 1 个(now)。
initialize_timeout_error212–215 ↗
fn initialize_timeout_error(timeout: Option<Duration>, fallback: Duration) -> anyhow::Error
作用:这个函数生成一条统一的“和 MCP 服务器握手超时了”的错误信息。这样不同地方报超时时,用户看到的是一致、清楚的说法。
数据流:进去的是可选的原始超时时长,以及一个备用时长。它优先使用原始超时时长;如果没有,就用备用时长。出来的是一个错误对象,内容说明握手等待了多久后超时。
调用关系:它被 remaining_initialize_timeout 用来报超时,也在连接重试流程里用于构造最终超时错误。它不参与连接,只负责把超时这件事说清楚。
调用图:被 1 处调用(remaining_initialize_timeout);外部调用 1 个(anyhow!)。
sleep_with_retry_deadline217–228 ↗
async fn sleep_with_retry_deadline(delay: Duration, deadline: Option<Instant>) -> bool
作用:这个异步函数负责在两次重试之间暂停一会儿,同时尊重总截止时间。它像一个“带闹钟的等待”:如果剩余时间不够等完,就告诉上层别再继续了。
数据流:进去的是想等待的时长和可选的截止时间。没有截止时间时,它直接睡够指定时长并返回 true;有截止时间时,它先算剩余时间,若已经到点就返回 false,否则只允许在剩余时间内睡眠。出来的是 true 或 false,表示这次等待是否完整完成。
调用关系:它被 RmcpClient::connect_pending_transport_with_initialize_retries 用在初始化失败后的重试间隔里,也被 run_service_operation_with_transient_retries 用在其他临时失败重试流程里。它不判断错误原因,只负责安全地等一等。
调用图:被 2 处调用(run_service_operation_with_transient_retries, connect_pending_transport_with_initialize_retries);外部调用 3 个(now, sleep, timeout)。