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.
- Les clients 2.4.0 sur 2025-11-25 attendent la racine structuredContent comme payload de sortie pour les outils meta id.
- Les pipelines qui ne valident que .result fausseront après la mise à niveau; lire la forme de réponse avant le chemin schema.
- Un serveur legacy peut encore renvoyer seulement result; le garde doit renvoyer legacy_wrapper et valider l'objet dans result.
- Évitez des $id comme my_tool:output sur le JSON Schema de sortie; Ajv 3.1.8 avec fast-uri les refuse—underscores ou $defs.
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.
{
"structuredContent": {
"region": "apac",
"count": 42
}
}
{
"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.
{
"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": "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.
{
"$id": "my_tool:output",
"type": "object",
"additionalProperties": false,
"required": ["region"],
"properties": {
"region": { "type": "string" }
}
}
| Réponse et enregistrement | Le garde renvoie | Couche pour Schema |
|---|---|---|
| metaIdTool, protocol 2025-11-25, region à la racine | validate_root | Schema de sortie sur la racine structuredContent. |
| Même enregistrement, structuredContent seulement result | legacy_wrapper | Schema sur l'objet dans structuredContent.result. |
| Enregistrement sans tool ou protocol hors enum | schema | Corrigez d'abord le JSON d'audit; ne devinez pas le chemin. |
| Enregistrement valide, body sans structuredContent | missing | Traiter 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.
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 ?
- 1Fixer 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.
- 2Format pour la couche structuredContent
Ouvrez JSON Format, collez tout le résultat tools/call. Voyez si structuredContent est racine plate ou seulement result.
- 3Diff 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.