Blog • MCP / TypeScript SDK

Nach dem Upgrade auf MCP TypeScript SDK 2.4.0: welche JSON-Schicht soll Schema bei tools/call structuredContent prüfen?

Am 23. Oktober 2026 lieferte das MCP TypeScript SDK @modelcontextprotocol/server 2.4.0. Unter Protokollrevision 2025-11-25 liegen Felder von structuredContent bei Tools mit z.object().meta({ id }) direkt an der Wurzel, ohne result-Hülle.

Viele Audit-Skripte validieren noch structuredContent.result; nach dem Client-Upgrade werden gültige Antworten rot. Ältere Server oder nicht neu gestartete Prozesse liefern weiter legacy mit result. protocol und metaIdTool auf dem Audit-Datensatz wählen validate_root oder legacy_wrapper.

MCP-Verbindung und Credential-Grenzen stehen in MCP-Sicherheit. Schema-gültig reicht nicht, siehe Schema reicht nicht. Bedrock Structured Output ist ein anderer Umschlag, in Bedrock Structured Output. Dieser Artikel behandelt nur die Schema-Schicht nach 2.4.0 und die Gate-Verzweigung.

Zuerst dies: 2.4.0 ändert die structuredContent-Form für meta-id-Tools unter 2025-11-25, nicht tools/list selbst. Unten: Release, Unwrap-Vergleich, Audit-Datensatz, $id-Fallen, Gate-Rückgaben und der JSONNote-Vergleich der zwei JSON-Schichten.

2.4.0 unwrappt structuredContent für meta-id-Tools unter 2025-11-25

Änderung und Migration stehen in Release @modelcontextprotocol/server 2.4.0. Das TypeScript SDK fügt clientseitig kein result mehr für Output-Schemas mit meta id ein; liefert der Server weiter nur result, muss das Audit legacy_wrapper erkennen.

Dieselbe Version verschärft Ajv: fast-uri 3.1.8 parst $id als URI und lehnt my_tool:output ab. Nutzen Sie my_tool_output oder $defs statt Doppelpunkte in $id. Unabhängig vom Unwrap, scheitert in derselben Pipeline.

Wie sieht structuredContent nach Unwrap aus?

Unter Protokoll 2025-11-25 erwarten 2.4.0-Clients Felder wie region und count an der structuredContent-Wurzel. Erstes Beispiel nach Upgrade, zweites legacy mit result.

2.4.0 erwartet: Felder an der Wurzel.
{
  "structuredContent": {
    "region": "apac",
    "count": 42
  }
}
Legacy: alleiniger Schlüssel result möglich.
{
  "structuredContent": {
    "result": {
      "region": "apac",
      "count": 42
    }
  }
}

Nicht jede Antwort ist unwrapped. Ist nur result der Schlüssel, Schema auf das innere Objekt und legacy_wrapper loggen, um nicht neu gestartete Server zu zählen.

Wie beschreibt der Audit-Datensatz diesen tools/call?

Das Gate braucht protocol, Toolname und ob Output mit z.object().meta({ id }) deklariert wurde. Beispiel: metaIdTool true, protocol 2025-11-25. Fehlendes tool oder falsches protocol liefert zuerst schema, nicht validate_root erzwingen.

Audit-Datensatz: protocol und metaIdTool wählen die Unwrap-Regel.
{
  "tool": "get_report",
  "protocol": "2025-11-25",
  "metaIdTool": true
}

Form des Audit-Datensatzes per Schema unten fixieren. Normale Tools mit metaIdTool false nutzen validate_root; fehlen Felder, liefert missing, nicht legacy_wrapper.

Audit-Schema. Erst Form, dann validate_root oder 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" }
  }
}

Warum scheitert Ajv sofort am $id des Output-Schemas?

2.4.0 hebt Ajv für fast-uri 3.1.8 an. fast-uri parst $id als URI; der Doppelpunkt in my_tool:output wird zum Schema und ist ungültig. my_tool_output oder $defs mit $ref nutzen. Unabhängig von der structuredContent-Schicht, trifft oft dieselbe CI wie Unwrap.

Beispiel-$id, das fast-uri ablehnt.
{
  "$id": "my_tool:output",
  "type": "object",
  "additionalProperties": false,
  "required": ["region"],
  "properties": {
    "region": { "type": "string" }
  }
}
Antwort und DatensatzGate liefertSchicht für Schema
metaIdTool, protocol 2025-11-25, region an der Wurzelvalidate_rootOutput-Schema auf structuredContent-Wurzel.
Gleicher Datensatz, structuredContent nur resultlegacy_wrapperOutput-Schema auf Objekt in structuredContent.result.
Audit ohne tool oder protocol nicht in enumschemaZuerst Audit-JSON reparieren, Pfad nicht raten.
Datensatz ok, body ohne structuredContentmissingAls unvollständige Antwort behandeln, nicht still passieren.

Wie wählt structured_content_target zwischen vier Zweigen?

Reihenfolge: record per Audit-Schema; body.structuredContent lesen. Bei metaIdTool und 2025-11-25 nur Schlüssel result → legacy_wrapper, sonst validate_root. metaIdTool false immer validate_root. Kein Objekt → missing.

legacy_wrapper und validate_root nicht zu einem Boolean passed zusammenlegen. Getrennt zählen, um result-Hüllen nach Upgrade zu sehen. Legacy-Regeln auf plain Tools erzeugen Fehl-Negative.

Gate: 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"

Wie vergleicht man Wurzel und result in JSONNote?

  1. 1
    Audit-Datensatz per Schema fixieren

    Öffnen Sie JSON Schema. Audit-Schema ins erste Feld, record aus tools/call ins zweite. Falsches protocol oder metaIdTool zuerst markieren; bei schlechter Form keinen Pfad wählen.

  2. 2
    Format zur structuredContent-Schicht

    Öffnen Sie JSON formatieren, gesamtes tools/call-Ergebnis einfügen. Prüfen, ob structuredContent flache Wurzel oder nur result ist.

  3. 3
    Diff: unwrapped vs wrapped

    Öffnen Sie JSON Diff. Links 2.4.0-Wurzel, rechts legacy result. Der hervorgehobene Pfad ist die Zeile im Audit-Skript.

Häufige Fragen

Unwrappt Protokoll 2026-07-28 structuredContent auch?

Für die meta-id-result-Hülle hängt Unwrap in 2.4.0 an 2025-11-25; 2026-07-28 änderte diese Schicht nicht erneut. SDK-Notes lesen beim Revisionswechsel, aber 2026-07-28 unwrappt nicht automatisch noch einmal.

Entfernen 2026-07-28-Clients x-mcp-header bei number tools aus der Liste—Relevanz für Validierung?

Ändert nicht, welche Schicht structuredContent-Schema bekommt. Defensive tools/list-Änderung: number tools exposieren x-mcp-header nicht mehr. Audit schema weiter tools/call-Body, Unwrap nicht aus list-Annotation ableiten.

metaIdTool false trotzdem legacy_wrapper?

Nein. legacy_wrapper nur für Tools mit meta-id-Output unter 2025-11-25. Plain tools immer validate_root; fehlt structuredContent oder kein Objekt → missing.

Immer structuredContent.result validieren für Kompatibilität?

Nein. Unter 2025-11-25 liegen Felder an der Wurzel; nur result prüfen scheitert gültige Antworten und übersieht fehlendes result. Gate-Zweige oder content.result nur bei legacy_wrapper.

Ajv meldet ungültige $id URI—Antwort oder Schema fixen?

Output-Schema-Datei fixen, nicht die tools/call-Antwort. Doppelpunkte in $id durch Unterstriche oder $defs-Referenzen ersetzen. Unwrap und $id-Parsing sind getrennt, scheitern aber oft in einem CI-Job.

Fazit

2.4.0 liefert meta-id-Tools unter 2025-11-25 flach an der Wurzel, legacy-Server können result weiter liefern. Audits müssen verzweigen, nicht .result fest codieren.

Entscheidung: protocol 2025-11-25 und metaIdTool true, Felder an der Wurzel → validate_root; nur result → legacy_wrapper und Innenobjekt prüfen; Audit scheitert → schema; kein structuredContent → missing; keine Doppelpunkte in Output-$id.

Als Nächstes: Audit per Schema, Format für die structuredContent-Schicht, Diff unwrapped vs wrapped, Output-$id korrigieren.

Zurück zum Blog