Jaeger AI Gateway 统一 MCP 工具路由(RFC 0008)深度解析:从 ACP 扩展方法到网关托管 MCP 服务器的架构演进
【免费下载链接】jaegerCNCF Jaeger, a Distributed Tracing Platform项目地址: https://gitcode.com/GitHub_Trending/ja/jaeger
本篇技术指南以 docs/rfc/0008-ai-gateway-mcp-tool-routing.md 为核心骨架,结合当前仓库中 AI Gateway(
jaegerai包)、mcptools库与 jaeger-query 服务装配代码的源码证据,完整剖析 Jaeger 将"每一个 AI 工具调用"(遥测工具与 UI 工具)收敛到单一网关托管 MCP 分发面的设计动机、术语规约、目标架构、实现状态与后续路线图。读完本文,你将理解:为什么 RFC 0002 曾拒绝而 RFC 0008 又复活"网关托管 MCP 服务器";/api/ai/mcp/(共享端点)与/api/ai/mcp/<mcpRouteID>/(turn 作用域端点)两个挂载点如何并存;ai.mcp_base_url回环地址推断的完整判定逻辑;以及 UI 工具 fire-and-forget 分发与jaeger_mcp独立扩展退役的前因后果。
1. 背景与动机:三个叠加的结构性问题
RFC 0008 开篇用"MCP 运动"(MCP movement)概括其目标:把仓库里每一个 AI sidecar 的工具调用——无论遥测还是 UI——全部经由网关托管的单个 MCP 分发面收敛。在此之前,每个 sidecar 都重复实现同一套管道:一个拨号 Jaeger 遥测工具的 MCP 客户端、一份逐 turn 的 UI 工具处理逻辑、一个 ACP WebSocket。更严重的是,两类工具调用走了两条不同的路径:
- 遥测工具调用完全绕过网关。每个 sidecar 内嵌自己的 MCP 客户端,直接拨号独立的
jaeger_mcp:16687服务器。网关看不到这些调用,因此无法对它们进行追踪(tracing)、日志、鉴权或限流。RFC 将其称为控制反转(inversion-of-control, IoC)违规——这是核心问题。 - 每个 sidecar 要维护两套分发协议。UI 工具走
agent → gateway → browser,通过 ACP 扩展方法_meta/jaegertracing.io/tools/call;遥测工具走 sidecar 自己的 MCP 客户端。每个 sidecar 作者都必须同时实现并理解两者。 - 逐 sidecar 重复造轮子。MCP 客户端与 UI 工具机制在 Python Gemini sidecar、进行中的 Claude Code 桥接(RFC 0008 引用的 PR #8631)以及任何未来的 sidecar 中被反复重写。
值得注意的范畴差异:RFC 0002 解决的是UI 工具分发;RFC 0008 的驱动问题是遥测工具分发与 sidecar 之间的重复建设——这是一个不同的、重新打开了 RFC 0002 已关闭设计决策的问题。
2. 术语规约(M4 里程碑已落地):先正名,再谈设计
RFC 0008 §2 指出,该子系统最大的可读性问题是一词多义:"session" 指代四种不同的东西,"stream" 三种,前端工具有两个名字("contextual" 和 "UI"),两个端点又被冠以想要保留给协议会话的单词。因此 §2 在给出设计之前先固定词汇表——因为后续所有内容都依赖它。
治理原则(已决):
- "session"——仅指协议级会话:ACP session(
session/new)与MCP 传输 session(Mcp-Session-Id,由 SDK 管理)。绝不指网关内部的逐 turn 簿记。 - "turn"——一次聊天交换 = 一次
POST /api/ai/chat= 一次 run = 一个 ACP session 生命周期。网关的逐 turn 状态使用该词。 - "stream"——仅指到浏览器的 SSE 通道。
- "UI tools"——前端声明、浏览器执行的工具。统一为这一个名字,不再叫 "contextual tools"。
- 两个 MCP 挂载点命名为共享端点(shared endpoint,无 turn id,仅遥测)与turn 作用域端点(turn-scoped endpoint,逐 turn,遥测 + 该 turn 的 UI 工具)。
- 处理器按协议层命名——HTTP handler、ACP handler、MCP middleware——而非 "dispatcher" 这类泛化动词。
- 按路由的实现称为端点(
endpoint_chat、endpoint_turn_mcp),由网关 HTTP handler 挂载。 - 外部 AG-UI 标识符(
threadId、runId)保持不变——它们属于该协议。
概念与标识符对照表:
| 概念 | 原命名 | 提议命名 |
|---|---|---|
| 服务器铸造的逐 turn 路由 id(URL 路径段 + 注册表键) | mcpSessionID、{sessionID} | mcpRouteID、{mcpRouteID} |
| 前端/浏览器执行的工具 | "contextual tools" 与 "UI tools" 并用 | UI tools(单一名字) |
| 无状态遥测挂载点 | "session-free endpoint" | 共享端点 |
| 逐 turn 挂载点 | "session-scoped endpoint" | turn 作用域端点 |
| ACP session id | ACPSessionId | 不变(真正的 session) |
| MCP 传输 session id | Mcp-Session-Id | 不变(SDK 所有) |
代码组件(文件与类型)映射(RFC 原文 §2 表格,均已在当前仓库落地为 M4 命名):
| 文件 → 类型 | 角色 | 问题 | 提议命名 |
|---|---|---|---|
session_streams.go→sessionStreams,session | 逐 turn 注册表 + 逐 turn 状态 | "session"+"stream" 粘在同一张 map 上 | turn_registry.go→turnRegistry,turnState |
contextual_tools.go→ContextualToolsStore | 扩展方法 UI 工具存储(遗留路径) | "contextual" 是 "UI" 的第二个名字 | 随扩展方法在 M6 删除;此前沿用 UI-tools 词汇 |
handler.go→Handler(+RegisterRoutes) | 网关HTTP handler | 泛化 "handler";routes.go只有 5 行 | http_handler.go(吸收routes.go) |
handler.go→ChatHandler | chat 端点(/api/ai/chat) | 它是端点而非 "handler" | endpoint_chat.go→chatEndpoint |
mcp_endpoint.go→mcpSessionHandler | turn 作用域 MCP 端点 | "session";且它不是全部 MCP | endpoint_turn_mcp.go→turnScopedEndpoint |
dispatcher.go→newDispatcher | 入站ACP handler | "dispatcher" 过度抬高普通 handler | acp_handler.go→acpHandler |
mcp_ui_tools.go→uiDispatchMiddleware | MCP middleware | "dispatch" 是泛化动词 | uiToolsMiddleware(内部dispatch*助手同理) |
streaming_client.go→streamingClient | 到浏览器的 SSE 写入器 | "stream" 保留给 SSE,因此一致 | 不变 |
routes.go | Handler上的 5 行RegisterRoutes | 整个文件只为放一个方法 | 删除——并入http_handler.go |
translation.go、trace_propagation.go、ws_adapter.go | AG-UI↔ACP 翻译、追踪传播、WebSocket 适配 | 无 | 不变 |
选mcpRouteID而非turnID/runID的理由:后两者与客户端侧的runId冲突(三个逐 turn id 生命周期相同,仅凭来源与顺序区分);streamID/streamKey会加重已饱和的 "stream" 词族;任何含 "session" 的命名都会与两个真实协议会话冲突。mcpRouteID按职责命名——路由某个 turn 的 MCP 回调——且不与任何现有用法重叠。callbackID或rendezvousID是可选同义词。
为什么现在改名是廉价的(RFC 原文论证):
mcpSessionID与{sessionID}路径段自 PR #8910 就已存在,但端点处于休眠状态——其 URL 在 PR #9009 之前未向任何 sidecar 公布,也没有 sidecar 消费这些名字——因此外部没有东西绑定它们。一旦 URL 公布并形成 sidecar 契约(M6),改名就会变贵。当前仓库代码已按新命名落地,例如 turn_registry.go 中的mcpRouteID参数、register/get方法。
3. 修正先前的记录:基线架构与两个被推翻的前提
RFC 0008 §3 指出先前的记录(RFC 0002、内部 Tool Routing Design doc、issue #8890)中已有不准确或从未准确的表述,并逐条修正。
3.1 运动开始前的基线架构
RFC 0008 §3.1 给出基线架构图:
Browser ──[AG-UI / HTTP+SSE]──► Gateway (jaeger-query :16686) ──[ACP / WebSocket]──► Sidecar ──► LLM │ │ UI tool call: Telemetry tool call: agent → gateway via ACP ext-method sidecar dials jaeger_mcp:16687 _meta/jaegertracing.io/tools/call DIRECTLY — bypasses the gateway (fire-and-forget; browser executes (gateway is blind to these calls) the side effect off the SSE stream)- chat 端点(
POST /api/ai/chat)桥接 AG-UI ↔ ACP(RFC 0002 的设计,源码见 endpoint_chat.go)。 - UI 工具(此遗留路径上叫 contextual tools):浏览器在每次聊天请求的
RunAgentInput.tools中逐 turn 声明;网关将该快照附加到NewSessionRequest.Meta(命名空间键jaegertracing.io/contextual-tools,名称加ui_前缀)通告给 sidecar;sidecar 将其注册为 LLM 可调用工具。LLM 调用时,sidecar 经 ACP 扩展方法_meta/jaegertracing.io/tools/call分派回网关;网关 fire-and-forget 确认,并在浏览器 SSE 流上并行发出TOOL_CALL_*事件,由浏览器执行实际副作用(PR #8423,常量定义见 acp_handler.go 中的ExtMethodJaegerToolCall = "_meta/jaegertracing.io/tools/call"与UIToolPrefix = "ui_")。 - 遥测工具:由独立
jaeger_mcpOTel 扩展在:16687端口提供服务,每个 sidecar 直接拨号。
3.2 RFC 0002 曾拒绝网关托管 MCP 服务器;更广泛的遥测问题使其复活
RFC 0002 §5.2 拒绝逐 turn 网关托管 MCP 服务器,§5.4 选择 ACP 扩展方法,驱动因素有二:(1)最小化数据流数量——复用唯一打开的 ACP WebSocket 而非新增连接;(2)假设 ACP 正是为此而生——agent 能在同一 ACP 连接上回送工具调用(即 MCP-over-ACP 将提供的能力)。
两个前提此后都发生了偏移:
- (2) 是错的。如今 ACP 没有可用的同连接工具调用路径:MCP-over-ACP 是 UNSTABLE、未完成的草案(见 §3.3)。"复用 WebSocket 是干净选择"的假设不成立。
- (1) 变得更糟而非更好。经由网关托管 MCP 服务器走 HTTP 路由工具调用增加了一条数据流——一条回到进程内的第二条连接,即便在
localhost上——与最小化数据流相反。配置成本大多可避免:当整个往返都在回环上时网关推断回环 base URL(§4.3),只有其他拓扑需要ai.mcp_base_url覆盖。但额外的连接本身仍然存在。
因此网关托管服务器在 RFC 0002 自己的标准上并非严格获胜——这是有意的权衡,由一个RFC 0002 未解决的问题来证明其合理性:遥测工具调用完全绕过网关(§1),而目标是把网关放到每一个工具调用的路径上。与 RFC 0002 反对意见的调和点是:问题从来不是 agent 是否拨号一个 MCP 服务器——MCP 就是 agent 消费工具的方式——而是拨号哪一个:直接拨号jaeger_mcp(或任何外部服务器)让网关失明(违规),而拨号网关自己的MCP 端点则让网关保持在每次调用的路径上,能够看见、追踪、把关流量。接受额外数据流与 URL 配置,是这种可见性的代价。
行动项:RFC 0002 获得指向本 RFC 的横幅说明,注明其 Alternative B 在更广泛的问题陈述下复活;其历史分析原样保留(当前仓库中 docs/rfc/0002-ai-gateway-contextual-tools.md 顶部确实已加上 superseded note)。
3.3 MCP-over-ACP:ACP SDK 的真实状态
"一切保持在单条 WebSocket 上"的传输(MCP-over-ACP)很重要,因为整个"先 HTTP、后 ACP"的计划依赖它。它只存在于GoACP SDK(coder/acp-go-sdk)中——网关正是用 Go SDK。已发布的 Gemini sidecar 是 Python,进行中的 Claude Code sidecar(PR #8631)是运行在claude-agent-acp上的 Node.js——两者的 ACP SDK 都不提供该隧道。因此它是未来机会,而非今天任何 sidecar 可走的路径。在 Go SDK 中它也是 UNSTABLE、未完成的:固定的 v0.13.0 与 #8890 引用的 v0.13.5 在该特性上完全一致,且都只提供握手脚手架,而非即用隧道:
mcp/connect/mcp/disconnect:仅握手管道。SDK 在 agent 侧生成发送器(AgentSideConnection.UnstableConnectMcp/UnstableDisconnectMcp),在 client 侧生成入站分派分支——但分派委托给消费者必须实现的可选接口方法(UnstableConnectMcp/UnstableDisconnectMcp),未实现则返回MethodNotFound。SDK 提供路由,不提供行为。mcp/message:仅类型。这是承载实际 MCP 载荷(tools/list、tools/call)的方法。SDK 只有其类型与常量——与 connect/disconnect 不同,没有生成的发送器、没有分派分支。消费者必须用底层SendRequest/通知原语手工驱动。- 所有三者均显式标记UNSTABLE——每个都带有生成注释"not part of the spec yet, and may be removed or changed at any point"(
McpServer.Http是稳定的;Stdio是强制的)。
对设计的两点后果:
- 两端都有真功夫,没有一端"就绪"。网关必须自行实现 connect/disconnect 接口方法与整个
mcp/message桥;而两个 sidecar 根本无法消费该传输,因为它只存在于 Go SDK 中。网关侧可行——POC PR #8854 用一个分派文件实现——但"先 HTTP"在两端都避免了实现工作,而不仅是 sidecar 侧。 - 传输是移动靶。MCP-over-ACP 是开放的草案 RFD,SDK 标记"随时可能移除或更改"。因此HTTP——稳定的 MCP 传输——是持久默认,MCP-over-ACP 是待 RFD 落定且两端实现后再添加的优化,而非把所有东西迁移过去的目的地。这与先前记录中"HTTP = 垫脚石,ACP = 目的地"的框架正好相反。
4. 目标设计:一个网关托管的 MCP 分发面
4.1 单一分发面:网关自己实现 MCP 方法
网关托管一个 MCP 服务器并自己实现MCP 方法(不是HTTP 重定向或透明代理):
| MCP 方法 | 行为 |
|---|---|
initialize | 通告tools能力。 |
tools/list | 返回遥测工具+调用方 turn 的 UI 工具(名称冲突时 UI 胜出)。 |
tools/call | 按名称路由。UI 工具:在浏览器 SSE 流上发出其TOOL_CALL_*事件(浏览器执行副作用),并立即向调用方返回合成的 "dispatched" 结果——网关不等浏览器(fire-and-forget)。其他任何名称:执行遥测工具并返回真实结果。 |
UI 与遥测的判定在网关的tools/call处理中只做一次——即叠加在 MCP 服务器上的 UI 工具中间件——网关完全观察它(M3 为其提供追踪)。sidecar 停止重复实现工具管道(到jaeger_mcp的工具翻译桥与独立的 UI 工具路径),只需把 MCP 客户端指向单一网关 URL。
源码证据:mcptools.NewServer在 server.go 中注册 9 个遥测工具:get_services、get_span_names、search_traces、get_span_details、get_trace_errors、get_trace_topology、get_critical_path、get_service_dependencies、read_skill(内含渐进式披露的 skill playbook,见 mcptools/README.md)。UI 工具由 mcp_ui_tools.go 的uiToolsMiddleware以接收中间件(receiving middleware)方式叠加:tools/list时把该 turn 的 UI 工具追加到遥测列表(同名时 UI 遮蔽遥测,appendUITools模拟AddTool的按名替换语义);tools/call时若名字属于该 turn 声明的 UI 工具,则调用emitUIToolCall返回合成 ack;其余调用穿透到遥测处理器。该中间件还顺带做了防御:空参数返回IsError的CallToolResult而非 panic,畸形 UI 工具被跳过并记日志。
4.2 共享端点与 turn 作用域端点
查询端口上有两个挂载点:
/api/ai/mcp/ → shared — telemetry tools only; stateless; for external MCP clients (Cursor, IDEs) /api/ai/mcp/<mcpRouteID>/ → turn-scoped — telemetry + this turn's UI tools; for the sidecar mid-chatturn 作用域路径携带逐 turn id(mcpRouteID),网关据此查该 turn 的 UI 工具快照与 SSE 流。共享端点替代独立的jaeger_mcp:16687,面向外部 MCP 客户端(Cursor、IDE 以及其他非 Jaeger 自家聊天 sidecar 的 AI 工具)。让 id 可选,正是一个实现服务两类受众的关键。
源码证据:共享端点由 server.go 的registerMCPTools挂载(mcptools.NewHandler+r.Handle(prefix+"/", ...));turn 作用域端点在 endpoint_turn_mcp.go 中定义路由常量:
const ( routeMCPPrefix = "/api/ai/mcp/" routeTurnMCPNoSlash = routeMCPPrefix + "{mcpRouteID}" routeTurnMCP = routeTurnMCPNoSlash + "/" )同时注册带斜杠与不带斜杠两种形态是刻意的:若缺 no-slash 模式,拨号/api/ai/mcp/<id>(无尾斜杠)的客户端会落入共享子树模式而非 turn 作用域处理器。turnScopedEndpoint.ServeHTTP先查turnRegistry.get(mcpRouteID),未知或过期 id 返回 404("非活跃 turn"是客户端错误);随后剥离前缀、把mcpRouteID存入请求 context,再委托给mcptools.WrapHTTP包装的流式 HTTP 处理器。turn 注册表本身在 turn_registry.go 中实现:register铸造 UUID 路由 id 并返回幂等 closer(聊天处理器 defer 调用),get返回 nil 表示非活跃 turn。
4.3 传输选型:HTTP 为永久传输,MCP-over-ACP 推迟
turn 作用域端点通过HTTP(标准 streamable-HTTP MCP)提供,这是永久传输:它适用于任何会说 MCP 的 agent,没有替换计划。第二条传输 MCP-over-ACP 经评估后推迟为未来增强(§6)。
URL 公布:网关在NewSessionRequest.mcpServers中公布 URL,但仅当 agent 在其InitializeResponse中通告了mcpCapabilities.http时——向无法消费的 agent 公布一种传输会让其会话失败。源码见 endpoint_chat.go 的announceMCP:mcpRouteID == "" || h.mcpBaseURL == "" || !caps.McpCapabilities.Http时返回空列表,否则返回type: "http"、name: "jaeger"、url: h.mcpBaseURL + h.basePath + routeMCPPrefix + mcpRouteID + "/"的McpServer。
决策——整个往返都在回环上时推断回环 base URL;否则要求配置。ai.mcp_base_url是sidecar回拨网关的地址。未设置时,网关推断自己的回环地址,但仅当往返的每一段都成立:
ai.agent_url是回环地址(sidecar 同地部署,其localhost即网关的);- 查询服务器绑定到回环或通配符(绑定到单一具体接口如
10.0.0.5:16686时回环上无人应答); - TLS 关闭(服务器证书的 SAN 是操作者拨号网关所用名称,几乎从不覆盖回环主机,推断出的
https://URL 会在 sidecar 侧证书校验失败)。
这覆盖了常见的单主机部署,零配置。通配符绑定公布为localhost;回环绑定按原样公布(在双栈主机上localhost可能解析到网关未绑定的地址族)。任一段不满足,网关什么都不公布,而非公布一个有充分理由怀疑是错误的地址。
两个被否决的极端:无条件localhost 默认会向远端 sidecar(其agent_url非回环)公布一个坏地址,失败要等 agent 在 turn 中途拨号才暴露;无默认/一律不公布虽安全,却让每个同地部署都不得不配置网关本可轻松推断的地址。把推断限制在完整往返成立时,保留了二者安全的一半。
源码证据:完整实现位于 flags.go 的resolveMCPBaseURL(显式base_url优先;tlsEnabled、!isLoopbackURL(AgentURL)、boundHost非回环/非通配符、端口为0或空均返回"")、loopbackAnnounceHost(通配符 →localhost,回环 → 原样,具体非回环接口 →"")、isLoopbackURL与hostIP(解析而非字符串匹配 "localhost",接受LOCALHOST或/etc/hosts别名;localhost短路为 127.0.0.1;名称解析受hostLookupTimeout = 2s约束以免拖慢启动)。对应测试在 flags_test.go。该函数在 server.go 中以aiCfg.resolveMCPBaseURL(ctx, queryOpts.HTTP.NetAddr.Endpoint, queryOpts.HTTP.TLS.HasValue())调用——因为推断需要查询 HTTP 端点与 TLS 设置,它们属于QueryOptions而非AIConfig。
限制——转发回环。路径上存在转发器时回环可达性不对称:容器中以-p 127.0.0.1:16688:16688发布、或经kubectl port-forward到达的 sidecar,网关在localhost可达,但 sidecar 自己的localhost是另一个网络命名空间,网关不在那里监听。配置中没有任何东西能区分这与真正的同地部署——两者字面上都是agent_url: ws://localhost:16688——因此这类部署必须设置ai.mcp_base_url。sidecar 的拨号随后失败;agent 是报告失败并在无工具状态下继续,还是直接使该 turn 失败,取决于具体 agent——这也是在session/new时记录已公布 URL 的理由,使原因无论哪种情况都可见。
可达性探针(M8,提议中)。网关可在启动时探测解析后的 base URL(推断或配置),类似 ACP agent 健康探针,使拼写错误或未绑定端口在启动时而非 turn 中途暴露。但探针运行在网关上,只能确认网关能到达该 URL,而非sidecar能——它在上述转发回环情形下会通过,而该情形恰恰是推断无法检测的。确认 sidecar 可达需要 agent 侧信号——例如该端点是否在 turn 中真的被拨号过。
传输对比表(🟢 好 / 🟡 部分 / 🔴 差):
| 判据 | 扩展方法(遗留,已退役) | HTTP(选定) | MCP-over-ACP(推迟) |
|---|---|---|---|
| 复用唯一打开的连接 | 🟢 | 🔴 二次 HTTP 回拨进进程 | 🟢 |
| 一条路径覆盖遥测与 UI | 🔴 仅 UI | 🟢 | 🟢 |
| 网关观察/把关每次调用(IoC) | 🟡 仅 UI | 🟢 | 🟢 |
| 今天 sidecar SDK 能说 | 🟢 | 🟢 | 🔴 Python/Node SDK 缺此能力 |
| 无需配置外部可达 URL | 🟢 | 🟡 全回环部署可推断;否则ai.mcp_base_url | 🟢 |
标准 MCPtools/call(无自定义方法) | 🔴 Jaeger 自定义_meta/…/tools/call | 🟢 | 🟢 |
| 稳定线缆契约 | 🟡 Jaeger 定义:稳定但专用 | 🟢 标准、稳定 | 🔴 UNSTABLE 草案 |
HTTP 是唯一标准、稳定且今天 sidecar 能说的选项;扩展方法退役(§4.4),MCP-over-ACP 推迟(§6)。
4.4 UI 工具分发今天即 fire-and-forget(实现限制),扩展方法退役
今天网关分发 UI 工具的方式:在浏览器 SSE 流上发出TOOL_CALL_*事件,并立即向调用方返回合成 ack——旧路径返回{acknowledged: true}(acp_handler.go的handleJaegerToolCall),MCP 路径返回"…dispatched to the browser"的CallToolResult(mcp_ui_tools.go 的emitUIToolCall)。网关不等待、也无法接收浏览器的结果。
这是当前实现的属性,而非 UI 工具或 AG-UI 的属性。浏览器↔网关这一段是单次POST /api/ai/chat,其响应是单向 SSE 流(服务器→浏览器);没有通道让浏览器在 turn 中途回传值,RFC 0002 §6.6 刻意选择合成 ack 而非构建回传通道(POST /api/ai/tool-result端点 + 每次调用 rendezvous 状态)。AG-UI 本身支持返回结构化结果的前端工具调用(外加 HITL 中断与共享状态同步),所以get_current_viewport、read_selection这类查询型 UI 工具并非范畴性不可能——只是在浏览器→网关结果通道存在前不受支持。这超出本 RFC 范围(§6)。
agent→网关传输与此完全正交。在任何传输上,网关对 UI 工具调用返回 ack、对遥测调用返回真实结果,因此从 agent 侧看,调用无论哪种方式都是同步的——扩展方法的{acknowledged: true}是普通请求/响应,仅因没有浏览器结果可返回才只做 ack,而非传输承载不了结果。从 ACP 扩展方法切换到 MCPtools/call因此不触及浏览器段。MCPtools/call路由取代自定义扩展方法——二者不共存:Gemini sidecar 迁移(M6)后,扩展方法路径——其在 ACP handler 中的 case 加上ContextualToolsStore——被移除。
源码证据:acp_handler.go中handleJaegerToolCall的完整校验链——JSON 反序列化失败、空sessionId、空name、纯前缀名(ui_剥空)、未在前端快照中注册的名字,一律返回acp.NewInvalidParams;成功则记录 Info 日志(会话 id、剥前缀后的名字、前缀名、参数字节数——不记录参数内容以防 PII/超长/日志噪声,完整参数仅在 Debug 级输出)并返回{result: {acknowledged: true}, isError: false}。前缀缺失时不拒绝而是告警放行,以兼容分阶段前缀铺设期间的旧 sidecar。
4.5jaeger_mcp并入查询扩展
遥测工具不再位于独立jaeger_mcp扩展中;它们并入查询扩展,成为mcptools库(PR #8894)。这直接遵循 §4.1:
- 将 UI 工具作为 MCP 工具提供,需要一个网关内部的 MCP 服务器——一个能访问逐 turn UI 状态的服务器——而网关位于查询扩展中。因此无论有没有
jaeger_mcp,网内 MCP 服务器都必须存在。 - 独立
jaeger_mcp扩展(ADR-002)对查询扩展有运行时依赖:它通过GetExtension(host)在启动时获取QueryService。独立扩展边界除了那次耦合什么也没买到。 - 既然网内 MCP 服务器反正需要,独立扩展就是冗余的。把
jaeger_mcp的工具处理器并入查询扩展(它们本就把*querysvc.QueryService作为参数),消除了跨扩展耦合——mcptools.NewServer现在直接接收QueryService——并让一个 MCP 实现(mcptools库)同时支撑共享与 turn 作用域端点(今天经由两个服务器实例;合并为一个属清理里程碑 M7)。
独立扩展及其:16687监听器退役;查询端口(:16686)上的共享/api/ai/mcp/挂载点取代它们服务外部 MCP 客户端(Cursor、IDE 等非 Jaeger 自家聊天 sidecar 的 AI 工具)。
源码证据:mcptools包文档明确写道"Package mcptools provides the Jaeger telemetry MCP tools as a reusable library";NewServer(telset, queryAPI, cfg)直接接收*querysvc.QueryService(注释解释这正是为了消除对 jaegerquery 扩展包的主机查找依赖与 import 环);NewHandler是NewServer+WrapHTTP的薄组合,供共享端点使用;WrapHTTP不自行绑定监听器,返回的 handler 由调用方挂到现有 mux。配置默认值在 config.go:DefaultMaxSpanDetailsPerRequest = 20、DefaultMaxSearchResults = 100、DefaultMaxReadFileSize = 512 * 1024、mcpSessionTimeout = 5 * time.Minute(SSE 恢复与流 id 关联所需)。
4.6 外部 MCP 服务器:直通(pass-through),而非代理
网关在进程内服务 Jaeger 自己的遥测(§4.5);它不代理其他 MCP 服务器。ACP 的NewSessionRequest.mcpServers是列表,因此对操作者可能配置的任何外部 MCP 服务器,架构立场是直通——网关公布它们,agent 直接拨号——而非经网关路由。经网关代理外部服务器以使其工具调用同样受到追踪与把关,是可能的未来增强(§6)。外部服务器配置与代理今天均未构建。
5. 实现状态与路线图
RFC 0008 顶部状态表完整罗列了各里程碑(截至文档更新时):
| 里程碑 | 内容 | 状态 |
|---|---|---|
| M0 | 基线:UI 工具经 ACP 扩展方法;遥测经独立jaeger_mcp:16687 | ✅ 运动前已发布 — PR #8423 |
| M1 | 将jaeger_mcp并入查询扩展:退役独立扩展;其工具成为mcptools库并直接接收QueryService | ✅ 完成 — PR #8894 |
| M2 | turn 作用域 MCP 端点/api/ai/mcp/<id>/+ turn 注册表(PR #8910),随后经其提供遥测+逐 turn UI 工具(PR #8973)——在 URL 公布前保持休眠 | ✅ 完成 |
| M3 | 工具调用可观测性:GenAI span 属性 + 网关↔sidecar 追踪传播 | ✅ 完成 — PR #8942 |
| M4 | 术语清理——应用 §2(移除session/stream过载;UI 工具单一名字;重命名端点/注册表/id) | ✅ 完成 — PR #9017 |
| M5 | 经HTTP(ai.mcp_base_url)向 sidecar 公布 turn 作用域端点 | ✅ 完成 — PR #9009 |
| M6 | 将 Gemini sidecar 迁移到网关 MCP URL;删除其专用jaeger_mcp桥与扩展方法路径 | ⏳ 待办 |
| M7 | 将共享与 turn 作用域挂载合并到一个mcp.Server(今天两个实例;turn 作用域中间件在无 turn 时已降级为仅遥测) | ⏳ 提议(清理) |
| M7.1 | 收割共享端点的 MCP 会话(mcptools.WrapHTTP返回裸http.Handler,共享挂载无 closer,会话比查询服务器活得久;turn 作用域挂载已在 PR #9009 获得) | ⏳ 提议(清理) |
| M7.2 | 将UI 工具从ai.enable_mcp解耦(该门控在 M6 移除 ACP 路径后会让enable_mcp: false+ 聊天开启的部署没有任何 UI 工具;需在 M6 前定夺) | ⏳ 提议 |
| M8 | 启动时探测ai.mcp_base_url可达性再公布(类似 ACP agent 健康探针) | ⏳ 提议 |
| — | Claude Code sidecar(平行轨道;消费同一 URL) | ⏳ 进行中 — PR #8631 |
Spike(不并入主线):POC PR #8854 端到端验证了两种传输。
当前仓库中的源码对应
- M1/M2 已落地:
mcptools库(server.go、config.go);jaegerai包中turnRegistry、turnScopedEndpoint、uiToolsMiddleware与路由常量均已按 M4 命名(turn_registry.go、endpoint_turn_mcp.go、mcp_ui_tools.go、http_handler.go)。 - M3 已落地:
mcptools的追踪/指标中间件(middleware.go)为tools/call生成gen_ai.tool.call.arguments/resultspan 属性(经maxSpanAttrChars = 65536截断,标记前置于首以在 UI 预览中幸存、且不伪装成可解析 JSON;按 rune 边界截断以保住 UTF-8),并记录jaeger.mcp.tool.calls计数器与jaeger.mcp.tool.duration直方图;网关侧injectTraceContextIntoMeta(trace_propagation.go)在 ACP prompt 边界按 SEP-414 约定注入traceparent/tracestate/baggage(复用jtracer已安装的全局复合传播器),sidecar 提取后即把自身 agentic-loop span 挂到该请求 span 之下,而非另起一条断开的 trace。 - M5 已落地:
announceMCP(endpoint_chat.go)+resolveMCPBaseURL(flags.go)+ server.go 的装配;Handler.Close收割 turn 作用域端点的 MCP 会话(ServerSession.Close是 SDK 唯一暴露的拆除入口,Sessions()在锁下克隆快照,迭代中关闭安全)。 - M7.1 现状佐证:共享端点的
registerMCPTools直接返回http.Handler,无 closer 逻辑——正是 RFC 提议让mcptools拥有拆除逻辑的原因。
配置示例(当前仓库 cmd/jaeger/config.yaml)
extensions: jaeger_query: ai: agent_url: ws://localhost:16688 # 聊天 sidecar 的 ACP WebSocket 地址 # 在查询端口的 /api/ai/mcp/ 进程内提供 Jaeger 遥测 MCP 工具 # (取代已退役的独立 jaeger_mcp 扩展)。存在即启用端点;移除该块即禁用。 # 可选键:base_url(sidecar 回拨网关的地址,见 resolveMCPBaseURL 的推断逻辑), # skills_dir(操作者自定义 skill 目录,由 read_skill 在 custom/ 下提供)。 mcp: {}全回环部署无需base_url;容器发布端口或kubectl port-forward的 sidecar 必须显式设置它。
6. 明确排除在外的未来增强
RFC 0008 §6 列出经考虑并刻意推迟的项,每项决策所在章节均已注明:
- MCP-over-ACP 传输(§4.3、§3.3)。让工具流量保持在单条 WebSocket 上有吸引力,但该传输是 UNSTABLE、未完成的草案 RFD,可能永远不会定稿;其
mcp/message桥未实现;没有任何 sidecar SDK 支持。HTTP 是永久传输;仅当 RFD 被接受且 SDK 实现后才可能重新评估。 - 查询型 UI 工具(§4.4)。向 LLM 返回数据的 UI 工具(读 viewport、选择、表单值)。网关↔sidecar 协议中没有任何东西阻止它们;它们需要浏览器→网关结果通道与 UI 侧集成,应属未来的独立 RFC。
- 网关代理外部 MCP 服务器(§4.6)。外部 MCP 服务器走直通——agent 直接拨号。经网关路由它们以使其工具调用同样受追踪与把关,是可能的后续增强。
7. 结论与参考脉络
RFC 0008 是一次针对既有 AI 网关工具路由决策的系统性再架构:以"网关位于每一次工具调用路径上"(IoC)为核心原则,把遥测工具从绕过网关的独立jaeger_mcp收编进查询扩展的mcptools库,把 UI 工具从 Jaeger 私有 ACP 扩展方法迁移到标准 MCPtools/call,并以共享/turn 作用域双端点 + 回环 base URL 推断覆盖了"外部 MCP 客户端"与"自家聊天 sidecar"两类受众,同时用一套严格术语规约(M4)消除了子系统内 "session/stream/handler/dispatcher" 的词汇过载。截至本文,M1–M5 已在当前仓库源码中落地,M6(Gemini sidecar 迁移与扩展方法退役)是下一个关键节点。
建议继续阅读的仓库资料:
- docs/rfc/0008-ai-gateway-mcp-tool-routing.md — 本文依据的 RFC 原文(含完整状态表与附录来源)
- docs/rfc/0002-ai-gateway-contextual-tools.md — 被本 RFC 部分取代的历史决策(顶部带 superseded banner)
- docs/rfc/0012-mcp-server-extension.md — 原始 MCP 服务器扩展提案(含 9 个遥测工具的输入/输出 JSON 示例与
jaeger_mcp目录结构) - docs/adr/002-mcp-server.md — MCP 服务器扩展的决策记录
- jaegerai/README.md — AI Gateway 包的架构与组件说明(含 mermaid 图)
- mcptools/README.md — MCP 端点启用、
skills_dir自定义 skill 编写指南 - flags.go、server.go —
ai.mcp_base_url推断与端点装配源码 - middleware.go — M3 工具调用可观测性实现
【免费下载链接】jaegerCNCF Jaeger, a Distributed Tracing Platform项目地址: https://gitcode.com/GitHub_Trending/ja/jaeger
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考