Блог • MCP / TypeScript SDK
После обновления до MCP TypeScript SDK 2.4.0: какой слой JSON проверять Schema для structuredContent в tools/call?
23 октября 2026 MCP TypeScript SDK выпустил @modelcontextprotocol/server 2.4.0. В редакции протокола 2025-11-25 у tools с z.object().meta({ id }) поля structuredContent лежат в корне, без автоматической обёртки result.
Многие аудиты всё ещё гоняют Ajv по structuredContent.result; после обновления клиента валидные ответы краснеют. Старые серверы могут отдавать legacy с result. protocol и metaIdTool в записи аудита выбирают validate_root или legacy_wrapper.
Границы подключения MCP и учётных данных — в безопасности MCP. Одного валидного schema мало, см. schema недостаточно. Structured output Bedrock — другой конверт, в structured output Bedrock. Эта статья только о слое Schema после 2.4.0 и ветвлении шлюза.
- Клиенты 2.4.0 на 2025-11-25 ждут корень structuredContent как payload выхода для meta id tools.
- Конвейеры, проверяющие только .result, дадут ложные срабатывания; сначала форма ответа, потом путь schema.
- Legacy-сервер может вернуть только result; шлюз должен вернуть legacy_wrapper и проверить объект внутри result.
- Избегайте $id вроде my_tool:output в JSON Schema выхода; Ajv 3.1.8 с fast-uri отклонит—подчёркивания или $defs.
Сначала это: 2.4.0 меняет форму structuredContent для meta id tools на 2025-11-25, не сам tools/list. Далее: релиз, сравнение unwrap, запись аудита, ловушки $id, возвраты шлюза и сравнение двух слоёв JSON в JSONNote.
2.4.0 разворачивает structuredContent для meta id tools на 2025-11-25
Изменение и миграция — в релизе @modelcontextprotocol/server 2.4.0. TypeScript SDK больше не вставляет result на клиенте для output schema с meta id; если сервер всё ещё отдаёт только result, аудит должен распознать legacy_wrapper.
Тот же релиз ужесточает Ajv: fast-uri 3.1.8 парсит $id как URI и отклоняет my_tool:output. Используйте my_tool_output или $defs без двоеточия в $id. Не про unwrap, но падает в той же pipeline.
Как выглядит structuredContent после unwrap?
По протоколу 2025-11-25 клиенты 2.4.0 ждут region и count в корне structuredContent. Первый образец после обновления, второй — legacy с result.
{
"structuredContent": {
"region": "apac",
"count": 42
}
}
{
"structuredContent": {
"result": {
"region": "apac",
"count": 42
}
}
}
Не считайте, что всё уже unwrapped. Если единственный ключ result, гоняйте schema по внутреннему объекту и пишите legacy_wrapper, чтобы считать не перезапущенные серверы.
Как запись аудита описывает этот tools/call?
Шлюзу нужны protocol, имя tool и был ли output объявлен через 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 ниже. Обычные tools с metaIdTool false — validate_root; нет полей — 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" }
}
}
Почему $id в schema выхода сразу валит Ajv?
2.4.0 поднимает Ajv под fast-uri 3.1.8. fast-uri парсит $id как URI; двоеточие в my_tool:output даёт scheme и строка невалидна. Замените на my_tool_output или $defs с $ref. Не про слой structuredContent, но часто в том же CI, что unwrap.
{
"$id": "my_tool:output",
"type": "object",
"additionalProperties": false,
"required": ["region"],
"properties": {
"region": { "type": "string" }
}
}
| Ответ и запись | Шлюз возвращает | Слой для Schema |
|---|---|---|
| metaIdTool, protocol 2025-11-25, region в корне | validate_root | Output schema по корню structuredContent. |
| Та же запись, structuredContent только result | legacy_wrapper | Output schema по объекту в structuredContent.result. |
| Запись без tool или protocol вне enum | schema | Сначала исправьте JSON аудита; не угадывайте путь. |
| Запись валидна, в body нет structuredContent | missing | Считать неполным ответом; не пропускать молча. |
Как structured_content_target выбирает among четырёх веток?
Порядок: validate record; читать body.structuredContent. metaIdTool и 2025-11-25, единственный ключ result → legacy_wrapper, иначе validate_root. metaIdTool false — validate_root. Не объект → missing.
Не схлопывайте legacy_wrapper и validate_root в один passed. Считайте отдельно, чтобы видеть вызовы с result после обновления. Legacy-правила для plain tools дают ложные пропуски.
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?
- 1Зафиксировать запись аудита через Schema
Откройте JSON Schema. Schema аудита в первое поле, record из tools/call во второе. Неверный protocol или metaIdTool сначала; при плохой форме путь не выбирать.
- 2Format для слоя structuredContent
Откройте JSON Формат, вставьте весь результат tools/call. Смотрите, плоский корень structuredContent или только result.
- 3Diff до и после unwrap
Откройте JSON Diff. Слева корень 2.4.0, справа legacy result. Подсвеченный путь — строка в скрипте аудита.
Вопросы
Разворачивает ли протокол 2026-07-28 structuredContent?
Для обёртки result у meta id unwrap в 2.4.0 привязан к 2025-11-25; 2026-07-28 этот слой снова не менял. Читайте релиз SDK при смене ревизии, но не ждите повторного unwrap от 2026-07-28.
Клиенты 2026-07-28 убирают x-mcp-header у number tools из списка — влияет на проверку?
Не меняет слой Schema для structuredContent. Защитное изменение tools/list: number tools больше не показывают x-mcp-header клиенту. Аудируйте body tools/call, не выводите unwrap из annotation в list.
Нужен ли legacy_wrapper при metaIdTool false?
Нет. legacy_wrapper только для tools с meta id output на 2025-11-25. Plain tools — validate_root; нет structuredContent или не объект → missing.
Всегда проверять structuredContent.result для совместимости?
Нет. На 2025-11-25 поля в корне; проверка только result валит валидное и пропускает исчезновение result. Ветки шлюза или content.result только при legacy_wrapper.
Ajv жалуется на невалидный $id URI — править ответ или schema?
Правьте файл output schema, не ответ tools/call. Замените двоеточия в $id или используйте $defs. Unwrap и разбор $id — разное, но часто падают подряд в одном CI.
Итог
2.4.0 даёт meta id tools на 2025-11-25 плоский корень, legacy-серверы могут вернуть result. Аудит должен ветвиться, а не зашивать .result.
Решение: protocol 2025-11-25 и metaIdTool true, поля в корне → validate_root; только result → legacy_wrapper и validate внутри; audit не проходит schema → schema; нет structuredContent → missing; без двоеточий в $id выхода.
Дальше: Schema для аудита, Format для слоя structuredContent, Diff unwrap, исправить $id выхода.