📘 mcpgw 图解教程 · 目录 第一部分 · 宏观全景 03 / 15
第一部分 · 宏观全景

一次工具调用的生命周期

把前两课串起来:我们跟随一次「客户端想调用某个上游工具」的完整数据流, 看 3 个元工具是如何接力把请求从客户端送到上游、再把结果带回来的。

一次调用的四步生命周期

1

连接 & 看到 3 个元工具

客户端连上 downstream(stdio 或 HTTP),调 list_tools, 看到的永远是 search_tools / get_tool_details / call_tool 这 3 个元工具。

2

search_tools("…") —— 检索候选

metatools 在不可变的 GatewaySnapshot 上跑检索策略(BM25 或 Vector), 返回一组按相关性排好序的候选 ToolSummary(限定名 + 描述)。

3

get_tool_details(qualified_name) —— 看清入参

catalog 取出该工具的完整 ToolDef(含 input schema), 让模型知道要传哪些参数、各是什么类型。

4

call_tool(qualified_name, args) —— 路由执行

metatools 经 catalog 查出该限定名对应的 (server, tool) ——绝不靠拆 __ 去猜——再路由到 upstream 对应 handle 转发,带每调用超时。

🔬 细节 / 代码对应:快照与重建

检索(步骤 2/3/4 的读路径)始终发生在一份不可变快照上,所以读路径无锁: 网关用 ArcSwap<GatewaySnapshot> 持有当前快照。 当上游发来 tools/list_changed 时,后台 rebuild_snapshotbuild-then-swap(先在旁边重建好新快照,再原子换上),整个过程不阻塞正在进行的检索。

crates/metatools/src/tools.rs · search_tools / get_tool_details / call_tool
// 读路径:在不可变快照上检索,无需加锁
pub async fn search_tools(snap: &GatewaySnapshot, query: &str, top_k: usize)
    -> Vec<ToolSummary> { snap.strategy.search(query, top_k).await ... }

// 路由:经 catalog 查 (server, tool),绝不拆 "__"
pub async fn call_tool(snap, registry, name, arguments) -> Result<_, MetaError> {
    let def = snap.catalog.get(name)?;          // (server, tool)
    let handle = registry.get(&def.server)?;      // 找到对应上游
    handle.call_tool(&def.name, arguments).await  // 转发(带超时)
}

快照重建逻辑见 crates/gateway/src/lib.rs · GatewayState::rebuild_snapshot

⚠️ 注意
网关的日志全部走 stderrstdout 专门留给 MCP 协议帧。 在 stdio 传输下,往 stdout 打任何普通日志都会污染协议流、让客户端解析失败——这是一条硬规则。
✅ 关键要点

记住这条三步心智模型即可:

检索
search_tools
详情
get_tool_details
执行
call_tool
💡 设计亮点
这正是「渐进式披露」在数据流层面的体现:无论上游有多少工具、如何增删, LLM 永远只面对 3 个稳定入口。复杂度(检索排序、快照重建、路由转发、超时隔离) 全被收进网关内部,对外暴露的契约始终不变。