ブログ • MCP / TypeScript SDK
MCP TypeScript SDK 2.4.0 に上げたあと、tools/call の structuredContent は JSON のどの層を Schema 検証するか?
2026年10月23日、MCP TypeScript SDK は @modelcontextprotocol/server 2.4.0 を出した。プロトコル改訂 2025-11-25 では、z.object().meta({ id }) で出力を宣言したツールの structuredContent は result で包まれず、根にフィールドが並ぶ。
多くの監査スクリプトは structuredContent.result に Ajv をかけるままで、クライアント更新後に合法応答が赤になる。古いサーバや未再起動のプロセスは result 付き legacy を返しうる。protocol と metaIdTool で validate_root と legacy_wrapper を選ぶ。
MCP 接続と資格情報の境界は MCP セキュリティ。schema が合法でも足りない話は schema 不足。Bedrock の構造化出力は別の封筒で、Bedrock 構造化出力。本記事は 2.4.0 以降 structuredContent の Schema 層とゲート分岐だけを書く。
- 2.4.0 クライアントは 2025-11-25 で meta id ツールの structuredContent 根が出力 payload であることを期待する。
- .result だけ検証する流水线は更新後に誤報する。応答形を見て schema パスを選ぶ。
- legacy サーバは result 単一キーを返しうる。ゲートは legacy_wrapper を返し、result 内を Schema 検証する。
- 出力 JSON Schema の $id に my_tool:output のような URI を避ける。Ajv 3.1.8 と fast-uri は拒否する。アンダースコアか $defs を使う。
先にこれだけ:2.4.0 が変えるのは 2025-11-25 上の meta id ツールの structuredContent 形であり、tools/list 自体ではない。以下はリリース、unwrap 比較、監査レコード、$id の落とし穴、ゲート戻り値、JSONNote での二層 JSON 対照。
2.4.0 は 2025-11-25 で meta id ツールの structuredContent を unwrap
変更と移行は @modelcontextprotocol/server 2.4.0 リリース。TypeScript SDK は meta id 付き出力 schema に result 包装を自動挿入しない。サーバが result 単一キーを返すなら legacy_wrapper を識別する。
同じリリースで Ajv 依存が厳しくなり、fast-uri 3.1.8 は $id を URI として my_tool:output を拒否する。$id にコロンを書かず my_tool_output か $defs を使う。unwrap とは別だが同じ検証流水线で落ちる。
unwrap 後の structuredContent は?
プロトコル 2025-11-25 では 2.4.0 クライアントは region や count を structuredContent 根に期待する。下の第一が更新後、第二が legacy の result 包装。
{
"structuredContent": {
"region": "apac",
"count": 42
}
}
{
"structuredContent": {
"result": {
"region": "apac",
"count": 42
}
}
}
すべて unwrap 済みと決めつけない。result だけで中身が payload なら、中で出力 schema を走らせ、legacy_wrapper をログに残して未再起動サーバを数える。
監査レコードは tools/call をどう書くか?
ゲートには protocol、ツール名、z.object().meta({ id }) 出力かが要る。サンプルは metaIdTool true、protocol 2025-11-25。tool 欠落や protocol 誤りは先に schema を返し、validate_root を強制しない。
{
"tool": "get_report",
"protocol": "2025-11-25",
"metaIdTool": true
}
下の schema で監査レコードの形を固定する。metaIdTool false の通常ツールも validate_root だが、structuredContent に業務フィールドが無い場合は missing で扱い、legacy_wrapper を当てはめない。
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"additionalProperties": false,
"required": ["tool", "protocol", "metaIdTool"],
"properties": {
"tool": { "type": "string", "minLength": 1 },
"protocol": { "enum": ["2025-11-25", "2026-07-28"] },
"metaIdTool": { "type": "boolean" }
}
}
出力 schema の $id で Ajv が落ちる理由は?
2.4.0 は Ajv を fast-uri 3.1.8 と組み合わせる。fast-uri は $id を URI とみなし、my_tool:output のコロンは scheme になり不正になる。my_tool_output か $defs と $ref に直す。structuredContent 層とは別だが unwrap と同じ CI でよく落ちる。
{
"$id": "my_tool:output",
"type": "object",
"additionalProperties": false,
"required": ["region"],
"properties": {
"region": { "type": "string" }
}
}
| 応答とレコード | ゲート戻り | Schema をかける層 |
|---|---|---|
| metaIdTool、protocol 2025-11-25、根に region 等 | validate_root | structuredContent 根に出力 schema。 |
| 同じ記録だが structuredContent は result のみ | legacy_wrapper | structuredContent.result 内に出力 schema。 |
| 監査で tool 欠落か protocol が enum 外 | schema | 監査 JSON を直してから。応答パスを当てない。 |
| 記録は合法だが body に structuredContent なし | missing | 不完全応答として扱い、黙って通さない。 |
structured_content_target は四分支をどう選ぶか?
順序:監査 schema で record を検証し、body.structuredContent を読む。metaIdTool かつ 2025-11-25 で content が result 単一キーなら legacy_wrapper、それ以外は validate_root。metaIdTool false は常に validate_root。content が object でなければ missing。
legacy_wrapper と validate_root を passed 一つに畳まない。レポートで分けて数え、更新後も result 包装が残る呼び出しを見る。plain ツールに legacy 規則を当てると真陽性が漏れになる。
def actual_type(data: object) -> str:
if data is None:
return "null"
if isinstance(data, bool):
return "boolean"
if isinstance(data, list):
return "array"
if isinstance(data, int):
return "integer"
if isinstance(data, float):
return "number"
if isinstance(data, str):
return "string"
if isinstance(data, dict):
return "object"
raise TypeError(type(data))
def validate(data: object, schema: dict) -> list[str]:
errors: list[str] = []
if not isinstance(schema, dict):
return errors
types = schema.get("type")
if isinstance(types, str):
types = [types]
actual = actual_type(data)
if types and not any(t == actual or (t == "number" and actual == "integer") for t in types):
errors.append("type")
if "enum" in schema and not any(value == data for value in schema["enum"]):
errors.append("enum")
if isinstance(data, str) and "minLength" in schema and len(data) < schema["minLength"]:
errors.append("minLength")
if isinstance(data, dict) and schema.get("properties"):
for key in schema.get("required") or []:
if key not in data:
errors.append(f"required:{key}")
if schema.get("additionalProperties") is False:
for key in data:
if key not in schema["properties"]:
errors.append(f"additional:{key}")
for key, sub in schema["properties"].items():
if key in data and data[key] is not None:
errors.extend(validate(data[key], sub))
return errors
def structured_content_target(record: dict, body: dict, schema: dict) -> str:
if validate(record, schema):
return "schema"
content = body.get("structuredContent")
if not isinstance(content, dict):
return "missing"
if record.get("metaIdTool") and record.get("protocol") == "2025-11-25":
if set(content.keys()) == {"result"} and isinstance(content.get("result"), dict):
return "legacy_wrapper"
return "validate_root"
return "validate_root"
JSONNote で根と result をどう対照するか?
- 1Schema で監査レコードを固定
開く JSON Schema。1欄に監査 schema、2欄に tools/call から抜いた record。protocol や metaIdTool の誤りが先に赤くなり、形が壊れた時点でパスを選ばない。
- 2フォーマットで structuredContent 層を確認
開く JSON フォーマット。tools/call 結果全体を貼り、structuredContent が扁平根か result 単一キーかを見て schema 層を決める。
- 3Diff で unwrap 前後を比較
開く JSON Diff。左に 2.4.0 期待根、右に legacy result。Diff で光るキー路径が監査脚本の修正行。
よくある質問
プロトコル 2026-07-28 も structuredContent を unwrap するか?
meta id の result 包装について、2.4.0 の unwrap は 2025-11-25 に紐づき、2026-07-28 はこの層を再変更していない。改訂を変えるときは SDK リリースを読むが、2026-07-28 が再 unwrap するとは限らない。
2026-07-28 クライアントが number ツールの x-mcp-header を一覧から外すのは検証に効くか?
structuredContent の Schema 層は変わらない。tools/list 表示の防御的変更で、number ツールは x-mcp-header を列挙に出さない。監査は tools/call 本体を schema し、list の annotation から unwrap を推測しない。
metaIdTool false でも legacy_wrapper か?
いいえ。legacy_wrapper は meta id 出力かつ 2025-11-25 のツールだけ。通常ツールは常に validate_root。structuredContent が無いか object でなければ missing。
新旧兼容のため常に structuredContent.result を検証できるか?
できない。2025-11-25 では 2.4.0 が根に置くので result だけだと合法が失敗し、result キー消失も見逃す。ゲート分岐を使うか legacy_wrapper 時だけ content.result を読む。
Ajv が $id URI 非法と言ったら応答か schema か?
直すのは出力 schema であり tools/call 応答ではない。$id のコロンをアンダースコアにするか $defs 参照にする。応答の unwrap と $id 解析は別問題だが同一 CI で連続して落ちる。
まとめ
2.4.0 は 2025-11-25 の meta id ツールを structuredContent 根直出しにしたが、legacy サーバは result 包装を返しうる。監査は .result 固定ではなく分岐が要る。
結論:protocol 2025-11-25 かつ metaIdTool true で根に業務フィールドなら validate_root。result 単一キーなら legacy_wrapper で result 内を検証。監査が schema 不通なら schema。structuredContent 無しは missing。出力 $id にコロンを書かない。
次:Schema で監査を固定し、フォーマットで structuredContent 層を見て、Diff で unwrap 前後を比べ、出力 $id を直す。