公開日 2026年9月20日約5分で読了
MCP Fusion 5.1.0:MCP ツール呼び出しを分布式トレースへ相関させる
2026年9月20日リリース。MCPのツールスパンは孤岛だった:各呼び出しが新しいルートを開始し、mcp.* を話せないバックエンドではクエリすらできなかった。5.1.0 ではすべてのツールスパンに W3C Trace Context の親相関と GenAI/OpenInference の二重エミッションを追加、core に OpenTelemetry 依存は一切ないまま。

著者 Renato Marinho
Founder · Vinkius
リリースノート(2026年9月20日公開)には2つの作業項目、G1 の W3C Trace Context 親相関と G2 の二重規約エミッション、が書かれており、どちらも1つのパッケージに収まる(リリースノート · GitHub の MCP Fusion)。残り15のパッケージはコード1行もなく 5.1.0 へ lockstep で上がる:core への range ^5.0.0 はすでに満たされているため、彼らにとってこのリリースは semver の帳簿作業1行にすぎない。動いたものは少ない、しかしあなたのバックエンドでエージェントのツール呼び出しを見られるかどうかを決めるのは、まさにそこだ。
ウォーターフォールはゲートウェイで止まっていた
このリリースまで、MCPFusionTracer.startSpan は2引数しか取らず、生成されるスパンは常に新しいルートだった。実務では、エージェントの分布式トレースが MCP のゲートウェイで終わる:各ツール呼び出しはバックエンド上で固有の孤立トレースとして現れ、エージェント → ツールを1つのウォーターフォールで追うのはできなかった。もうひとつの盲点の正体も同じだ:プライベートな mcp.* namespace を知らないバックエンドは、「過去1時間の全呼び出し」に答えられない。そんなクエリが動くのは、ベンダーにその namespace を教えていた場所だけだった。
フレームワークは触らない親ハンドル
新しくエクスポートされる型 MCPFusionSpanContext は、構造的に不透明な親ハンドルだ:traceId(小文字16進32文字)、spanId(16)、flags(W3C の trace-flags バイト。サンプル時は 0x01)、加えてオプションの remote、traceState、baggage と、ホスト自身のトレーサーコンテキストのための raw スロット。ホストは受信した traceparent / tracestate / baggage のペア(HTTP ヘッダー、または MCP の params._meta)から1つを構築し、フレームワークがやることはたった1つ、転送である。中身を絶対に覗かない。この不透明さが設計であり、仕方がないのではない。ホストアダプタが traceId / spanId / raw を OpenTelemetry の SpanContext + Context へマッピングするため、@mcpfusion/core は @opentelemetry/* 依存をゼロに保てる。生の OTel Tracer は MCPFusionTracer に代入できない(属性配列の不変性、Context の分離)ため、境界はあなたのコードの中の薄いアダプタであり、パッケージ依存ではない。
パースは意図的に厳しい。ゼロ依存のヘルパーは crypto.randomUUID() を土台にしている:newTraceId()、newSpanId()、generateTraceparent(sampled = true)、parseTraceparent が許すのはバージョン 00、小文字・ゼロでない16進32文字の trace ID、小文字・ゼロでない16進16文字の span ID、2桁の flags のみ;それ以外は undefined を返し、捏造された親は絶対に返さない。tracestate と baggage は W3C の上限(512文字、8192バイト)内でそのまま透過し、extractW3CContext は有効な traceparent を必須とする:有効なヘッダーがなければコンテキストはなく、スパンは新しいルートになる。バイト形式は SwarmGateway が既に出力しているものと同じで、モノレポ横断のトレースはそのまま相互運用できる。
取得はリクエスト単位だ。GroupedToolBuilder と ToolRegistry は慣習的なダックタイプキー ctx.mcpTraceContext(ctx.handoffTraceparent と同じパターン)を読んでツールスパンへ通す、ToolRegistry.routeCall が発火する 404 の未知ツールスパンも同様の扱いを受ける。404 は往々にしてエージェントが「どのツールがあるのか」を見失った瞬間であり、そここそ効いてくる。1つ細かい点:contextFactory がない場合、attachToServer() はプロパティアクセス時に throw するガードプロキシを置く。新しい readMcpTraceContext ヘルパーはその唯一の読み取りを try/catch 内で実行し throw を飲み込むため、tracing を有効にしただけで factory を書かされることがない。コンテキストの不在は「新しいルート」として扱われ、決してエラーにはならない。文書化された制約もある:フレームワークは実行時に OTel の Context を保持しないため、ハンドラー内部の自動計装された下流呼び出し(Prisma、HTTP クライアント、Redis)は MCP スパンの子ではなく兄弟として現れる。受信した親は尊重されるが、ツールスパンが下流全体の active context になるわけではない。
1つのスパン、3つの言葉
各ツールスパンはネイティブの mcp.* と並んで、openinference.span.kind = "TOOL"、gen_ai.operation.name = "tools/call"、tool.name を持つようになる(404 スパンも)。同じスパンが Datadog、Arize、Phoenix で移植可能になり、どれも mcp.* を理解しなくて済む、どこでも openinference.span.kind = TOOL でフィルタできる。出さないものも同じ重みを持つ:ツール境界でのトークン・コスト属性は一切ない。それらはエージェント側の LLM スパンに属するもので、5.1.0 はツールレイヤーでの捏造を拒否している。状態セマンティクスもパイプラインと一貫:AI 側の失敗(検証エラー、未知アクション、未知ツール;404 スパンはメッセージ付きの明示的 UNSET、ERROR ではない)は UNSET、成功は OK、ハンドラーがシステム障害を投げた場合のみ ERROR。LLM が間違ったツールを呼んでも、オンコールは鳴らされない。
OpenTelemetry のトレーサーを動かしている場合
取り込みは2つのスニペットで完結する。まずトレーサーをラップし、任意のコンテキスト引数を正しい場所に届ける:
import { trace, type SpanOptions, type Context } from '@opentelemetry/api';
const otel = trace.getTracer('mcpfusion');
registry.attachToServer(server, {
contextFactory: createContext,
tracing: {
startSpan: (name, options, context) =>
otel.startSpan(name, options as SpanOptions, context?.raw as Context | undefined),
},
});
次に、リクエストごとの context factory で W3C コンテキストを抽出し、慣習キーの下に置く:
const ctx = {
...baseContext,
mcpTraceContext: extractW3CContext(request.headers),
};
そこから先、すべてのツールスパン、404 ルーティングも含む、が呼び出し元のトレースに親子でつながり、GenAI/OpenInference 規約を話すバックエンドはどこからでも見えるようになる。リリースの検証ブロックがそれを裏付ける:core build は TypeScript エラーゼロ、16 のサテライトパッケージはすべてクリーン、core スイートは 272 ファイル / 5,263 テストで失敗ゼロ、@mcpfusion/swarm は 158/158。新しい Tracing.test.ts はツールスパンと 404 スパンの二重エミッションの対称性、W3C ラウンドトリップ、不正入力の拒否、ガードプロキシ耐性、両経路での親コンテキスト伝播を固定する。
