v1.x and v2 disagree on validating an error result's structuredContent — and the v1.x comment describes the v2 behaviour
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 初心者へのやさしさ
- 48/100
- issue の種類
- バグ
- 明瞭さ
- おおむね明確
- 活発さ
- 活発
- 技術スタック
- typescript
調査の方向性
src/client/index.ts:740 と packages/client/src/client/client.ts:2448 の検証ガードを比較し、次に server/mcp.js の validateToolOutput を調べて、tools/list のキャッシュの違いを再現します。意図されたエラー結果の動作が決定され、v1.x と v2 のパスおよびコメントが一致し、誤解を招くドキュメントまたはガイダンスが修正されれば完了です。
索引モデルが issue の本文から書いたものです。
説明
Summary
Since the v2 split on 2026-07-27, the two published lines disagree about whether a tool result with isError: true has its structuredContent validated against the tool's outputSchema — and the v1.x source comment describes the v2 behaviour rather than its own.
v2 / main — packages/client/src/client/client.ts:2448 (shipped in @modelcontextprotocol/[email protected]):
// Only validate structured content if present (not when there's an error)
if (result.structuredContent !== undefined && !result.isError) {
v1.x — src/client/index.ts:740 (shipped in @modelcontextprotocol/[email protected], still the latest dist-tag, not deprecated):
// Only validate structured content if present (not when there's an error)
if (result.structuredContent) {
Identical comment, different condition. On v1.x the parenthetical is simply untrue: an error result carrying structuredContent is validated, and callTool throws McpError -32602 before the caller can read the payload.
Note the guard directly above it on both lines does test isError (!result.structuredContent && !result.isError), which is what makes the omission below read as an oversight rather than a decision.
Reproduction
Self-contained, against @modelcontextprotocol/[email protected] — SDK server + SDK client over InMemoryTransport, no third-party code:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { z } from "zod";
const server = new McpServer({ name: "repro", version: "1.0.0" });
server.registerTool(
"get_score",
{ description: "Returns a score.", inputSchema: {}, outputSchema: { score: z.number() } },
async () => ({
isError: true,
content: [{ type: "text", text: JSON.stringify({ code: "AUTH_REQUIRED" }) }],
structuredContent: { code: "AUTH_REQUIRED" },
}),
);
const client = new Client({ name: "c", version: "1.0.0" });
const [ct, st] = InMemoryTransport.createLinkedPair();
await Promise.all([server.connect(st), client.connect(ct)]);
await client.listTools(); // <-- the only difference
await client.callTool({ name: "get_score", arguments: {} });
--- WITHOUT tools/list (validator not cached) ---
OK -> {"code":"AUTH_REQUIRED"}
--- WITH tools/list first (validator cached) ---
THREW -> McpError: MCP error -32602: Structured content does not match the tool's
output schema: data must have required property 'score', ...
Two things worth drawing out:
- The server half sends this happily.
validateToolOutputinserver/mcp.jsshort-circuits with an explicitif (result.isError) { return; }. So the v1.x SDK will emit a result that the v1.x SDK then refuses to receive. - The failure is conditional on unrelated prior state. The same call succeeds or throws depending only on whether
tools/listwas called earlier, since that is what populates_cachedToolOutputValidators. Clients that list before calling — i.e. essentially all real ones — always get the throw; a test client that never lists never sees it. That asymmetry makes this easy to miss in a test suite and guaranteed in production.
Why I'm filing rather than commenting on the existing threads
This has been reported and fixed five times, and I don't want to add a sixth duplicate — the divergence itself is the new part, and it only came into existence with the v2 split:
| #1428 | Fix targeting v1.x, from MCP Inspector being unable to display JetBrains' error payloads. Closed after review discussion. |
| #1690 | Same fix. Closed by its author believing main already skipped on error. |
| #1943 | The canonical issue. Closed not_planned as superseded by #1945. |
| #1945 | Open, now unmergeable: main was fixed independently, and the second file it patched (experimental/tasks/client.ts) was removed by #2128. Probably closeable. |
| #1947 | Same fix again, opened and closed within a day in favour of #1945. |
I'd also rather not relitigate the substantive question. On #1428 @cliffhall made the case that validating whenever structuredContent is present is the correct behaviour, and asked reasonably what an error result's structuredContent is even for. I find that persuasive — in our own server we resolved this by simply not setting structuredContent on error results, which costs nothing since the payload is already in content[0].text.
But main now does the opposite of what that review concluded, so whichever answer is right, the two lines currently can't both be.
What I'm asking for
Whichever of these fits — I'm happy to send the PR for any of them:
- Say which behaviour is intended. If v1.x is correct, v2 has a regression. If v2 is correct, v1.x has the bug five people have now reported.
- At minimum, fix the v1.x comment. If the v1.x behaviour is deliberate,
// Only validate structured content if present (not when there's an error)is actively misleading and is the proximate cause of the repeat reports. Something like// Validated whenever present — error results are NOT exempt; see #1428would stop the cycle for a one-line change. - If servers must not put
structuredContenton error results, document it where server authors will see it. Nothing in the tool-registration docs says so today, andregisterToolaccepts it without complaint on both lines.
Related but not the same: SEP-2145 (still open) would make output-validation failures surface as tool execution errors rather than protocol errors, which would change how this failure presents but not whether it happens.
Real-world impact
We ship a hosted MCP server with 15 tools that all declare outputSchema. Every one returned its AUTH_REQUIRED / PRO_REQUIRED body — price, trial terms, signup link — as structuredContent, and every listing client silently converted that into a thrown McpError instead. Unauthenticated users got a protocol exception where the actionable message should have been. Our test suite missed it for months for exactly the reason above: our test client never called listTools().
Environment
@modelcontextprotocol/[email protected](and 1.29.0 — the block is byte-identical), Node 20+, macOS- Verified against
v1.x@a9f6eb7andmainas of 2026-09-02
- 主要言語
- TypeScript
- スター
- 13.5k
- フォーク
- 2.2k
- 平均マージ
- 3日 11時間
- マージ済み PR(30日)
- 32
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートなし
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
modelcontextprotocol/typescript-sdk のほかの issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
modelcontextprotocol/typescript-sdk#2920 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
modelcontextprotocol/typescript-sdk#2919 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
v1 v2
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
modelcontextprotocol/typescript-sdk#2916 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
v1 v2
難易度 1/5 1時間未満 初心者へのやさしさ 90/100
modelcontextprotocol/typescript-sdk#2867 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
v1 v2
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
modelcontextprotocol/typescript-sdk#2854 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
modelcontextprotocol/typescript-sdk の issue をすべて見る
似ている issue
-
難易度 1/5 1時間未満 初心者へのやさしさ 72/100
betagouv/mon-entreprise#4699 ·
メンテナーはふだん 3 日以内に返信
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
jaegertracing/jaeger-ui#4547 · コメント 3 件 ·
メンテナーはふだん 1 日以内に返信
-
ai-driven-qa
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
linagora/twake-calendar-frontend#1467 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
need4deed-org/sdk#267 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
auth0/universal-login#414 ·
メンテナーはふだん 1 日以内に返信