Блог • 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 меняет форму 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.

Ожидание 2.4.0: поля в корне structuredContent.
{
  "structuredContent": {
    "region": "apac",
    "count": 42
  }
}
Legacy: единственный ключ result возможен.
{
  "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.

Запись аудита: protocol и metaIdTool задают правило unwrap.
{
  "tool": "get_report",
  "protocol": "2025-11-25",
  "metaIdTool": true
}

Зафиксируйте запись schema ниже. Обычные tools с metaIdTool false — validate_root; нет полей — missing, не legacy_wrapper.

Schema аудита. Сначала форма, затем validate_root или 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, который отклонит fast-uri.
{
  "$id": "my_tool:output",
  "type": "object",
  "additionalProperties": false,
  "required": ["region"],
  "properties": {
    "region": { "type": "string" }
  }
}
Ответ и записьШлюз возвращаетСлой для Schema
metaIdTool, protocol 2025-11-25, region в корнеvalidate_rootOutput schema по корню structuredContent.
Та же запись, structuredContent только resultlegacy_wrapperOutput schema по объекту в structuredContent.result.
Запись без tool или protocol вне enumschemaСначала исправьте JSON аудита; не угадывайте путь.
Запись валидна, в body нет structuredContentmissingСчитать неполным ответом; не пропускать молча.

Как 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 дают ложные пропуски.

Шлюз: schema, legacy_wrapper, validate_root, missing.
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. 1
    Зафиксировать запись аудита через Schema

    Откройте JSON Schema. Schema аудита в первое поле, record из tools/call во второе. Неверный protocol или metaIdTool сначала; при плохой форме путь не выбирать.

  2. 2
    Format для слоя structuredContent

    Откройте JSON Формат, вставьте весь результат tools/call. Смотрите, плоский корень structuredContent или только result.

  3. 3
    Diff до и после 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 выхода.

Назад в блог