Blog • MCP / TypeScript SDK

Après la mise à niveau vers MCP TypeScript SDK 2.4.0, quelle couche JSON faut-il valider avec Schema sur structuredContent de tools/call ?

Le 23 octobre 2026, le MCP TypeScript SDK a livré @modelcontextprotocol/server 2.4.0. Sur la révision 2025-11-25, les outils avec z.object().meta({ id }) renvoient structuredContent à la racine, sans enveloppe result automatique.

Beaucoup de scripts d'audit passent encore Ajv sur structuredContent.result; après la mise à jour client, des réponses valides deviennent rouges. D'anciens serveurs peuvent renvoyer le legacy avec result. protocol et metaIdTool sur l'enregistrement d'audit choisissent validate_root ou legacy_wrapper.

Les limites de connexion MCP et d'identifiants sont dans sécurité MCP. Un JSON conforme au schéma ne suffit pas, voir schema insuffisant. La sortie structurée Bedrock est une autre enveloppe, dans sortie structurée Bedrock. Cet article ne couvre que la couche Schema après 2.4.0 et la branche du garde.

Gardez ceci : 2.4.0 change la forme structuredContent des outils meta id sur 2025-11-25, pas tools/list. Ensuite : notes de version, comparaison unwrap, enregistrement d'audit, pièges $id, retours du garde et comparaison des deux couches JSON dans JSONNote.

2.4.0 déroule structuredContent pour les outils meta id sur 2025-11-25

Le changement et la migration sont dans version @modelcontextprotocol/server 2.4.0. Le SDK TypeScript n'insère plus automatiquement result côté client pour un schema de sortie avec meta id; si le serveur renvoie encore seulement result, l'audit doit voir legacy_wrapper.

La même version resserre Ajv : fast-uri 3.1.8 parse $id comme URI et refuse my_tool:output. Utilisez my_tool_output ou $defs sans deux-points dans $id. Sans lien avec unwrap, mais échoue dans la même pipeline.

À quoi ressemble structuredContent après unwrap ?

Sur le protocole 2025-11-25, les clients 2.4.0 attendent region et count à la racine structuredContent. Premier échantillon après mise à niveau, second legacy avec result.

Attendu en 2.4.0 : champs à la racine.
{
  "structuredContent": {
    "region": "apac",
    "count": 42
  }
}
Legacy : seule clé result encore possible.
{
  "structuredContent": {
    "result": {
      "region": "apac",
      "count": 42
    }
  }
}

Ne présumez pas que tout est unwrapped. Si seule result existe, validez l'objet intérieur et journalisez legacy_wrapper pour compter les serveurs non redémarrés.

Comment l'enregistrement d'audit décrit-il ce tools/call ?

Le garde a besoin de protocol, du nom d'outil et de savoir si la sortie utilise z.object().meta({ id }). L'exemple met metaIdTool true et protocol 2025-11-25. tool manquant ou protocol faux renvoie schema d'abord.

Enregistrement : protocol et metaIdTool choisissent la règle unwrap.
{
  "tool": "get_report",
  "protocol": "2025-11-25",
  "metaIdTool": true
}

Fixez l'enregistrement avec le schema ci-dessous. Outils sans meta id : validate_root; absence de champs → missing, pas legacy_wrapper.

Schema d'audit. Forme d'abord, puis validate_root ou 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" }
  }
}

Pourquoi $id sur le schema de sortie fait échouer Ajv ?

2.4.0 aligne Ajv avec fast-uri 3.1.8. fast-uri parse $id comme URI; le deux-points de my_tool:output devient un scheme invalide. Passez à my_tool_output ou $defs avec $ref. Sans lien avec la couche structuredContent, mais même CI que unwrap.

Exemple de $id refusé par fast-uri.
{
  "$id": "my_tool:output",
  "type": "object",
  "additionalProperties": false,
  "required": ["region"],
  "properties": {
    "region": { "type": "string" }
  }
}
Réponse et enregistrementLe garde renvoieCouche pour Schema
metaIdTool, protocol 2025-11-25, region à la racinevalidate_rootSchema de sortie sur la racine structuredContent.
Même enregistrement, structuredContent seulement resultlegacy_wrapperSchema sur l'objet dans structuredContent.result.
Enregistrement sans tool ou protocol hors enumschemaCorrigez d'abord le JSON d'audit; ne devinez pas le chemin.
Enregistrement valide, body sans structuredContentmissingTraiter comme réponse incomplète; ne pas laisser passer.

Comment structured_content_target choisit entre quatre branches ?

Ordre : valider record; lire body.structuredContent. metaIdTool et 2025-11-25, seule clé result → legacy_wrapper, sinon validate_root. metaIdTool false → validate_root. Contenu non objet → missing.

Ne fusionnez pas legacy_wrapper et validate_root en un seul passed. Comptez séparément pour voir les appels encore enveloppés. Règles legacy sur outils plain → faux négatifs.

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

Comment comparer racine et result dans JSONNote ?

  1. 1
    Fixer l'enregistrement d'audit avec Schema

    Ouvrez JSON Schema. Schema d'audit au premier champ, record tools/call au second. protocol ou metaIdTool faux signalés d'abord; ne pas choisir de chemin si la forme est mauvaise.

  2. 2
    Format pour la couche structuredContent

    Ouvrez JSON Format, collez tout le résultat tools/call. Voyez si structuredContent est racine plate ou seulement result.

  3. 3
    Diff avant et après unwrap

    Ouvrez JSON Diff. À gauche racine 2.4.0, à droite legacy result. Le chemin surligné est la ligne à changer.

Questions fréquentes

Le protocole 2026-07-28 déroule-t-il aussi structuredContent ?

Pour l'enveloppe result des meta id, l'unwrap 2.4.0 est lié à 2025-11-25; 2026-07-28 n'a pas rechangé cette couche. Lisez les notes SDK en changeant de révision, sans supposer un second unwrap automatique en 2026-07-28.

Les clients 2026-07-28 retirent x-mcp-header des outils number de la liste—impact sur la validation ?

Ne change pas la couche Schema de structuredContent. Changement défensif tools/list : les outils number n'exposent plus x-mcp-header. Auditez le corps tools/call, n'inférez pas unwrap depuis list.

metaIdTool false doit-il utiliser legacy_wrapper ?

Non. legacy_wrapper seulement pour outils meta id sur 2025-11-25. Outils plain → validate_root; structuredContent absent ou non objet → missing.

Toujours valider structuredContent.result pour compatibilité ?

Non. Sur 2025-11-25 les champs sont à la racine; valider seulement result échoue sur du valide et rate l'absence de result. Branches du garde ou content.result seulement si legacy_wrapper.

Ajv signale $id URI invalide—corriger la réponse ou le schema ?

Corrigez le fichier schema de sortie, pas la réponse tools/call. Remplacez les deux-points dans $id ou utilisez $defs. Unwrap et parsing $id sont distincts mais échouent souvent dans le même CI.

Conclusion

2.4.0 met les outils meta id sur 2025-11-25 à la racine, mais le legacy peut garder result. Les audits doivent brancher, pas coder .result en dur.

Décision : protocol 2025-11-25 et metaIdTool true, champs à la racine → validate_root ; seule result → legacy_wrapper et valider l'intérieur ; audit schema échoue → schema ; pas de structuredContent → missing ; pas de deux-points dans $id de sortie.

Ensuite : Schema pour l'audit, Format pour la couche structuredContent, Diff unwrap, corriger $id de sortie.

Retour au blog