ข้ามไปยังเนื้อหา

Structured Output and Annotations

array content ของ tool คือสิ่งที่ language model อ่าน แต่ยิ่งนานไป ผู้บริโภค ผลลัพธ์ของ tool ก็ยิ่งไม่ใช่ model — แต่เป็นโค้ด: host ที่ generate wrapper แบบมี type, sandbox แบบ “code mode” ที่ต่อ tool เป็นลูกโซ่, MCP App ที่ render chart ผู้บริโภคพวกนี้อยากได้ JSON ที่รู้ shape ไม่ใช่ prose ที่ต้องมา parse

MCP ให้ทั้งสองช่องทางในผลลัพธ์เดียว:

{
"jsonrpc": "2.0",
"id": 5,
"result": {
"resultType": "complete",
"content": [ { "type": "text", "text": "{\"name\":\"Travel mug\",\"price\":24}" } ],
"structuredContent": { "name": "Travel mug", "price": 24 }
}
}
  • structuredContent — JSON value อะไรก็ได้ (object, array, string, number, boolean หรือ null) ถ้า tool ประกาศ outputSchema ค่านี้ ต้อง ตรงตาม schema นั้น
  • content — ยังคงต้องมี เพื่อ backward compatibility tool ที่คืน structured content ควรคืน JSON ที่ serialize แล้วเป็น block text ด้วย เพื่อให้ client รุ่นเก่าและ model ยังได้อะไรไปบ้าง
flowchart LR
  S["Tool result"] --> C["content[] text"]
  S --> SC["structuredContent"]
  C --> LLM["Language model อ่าน prose"]
  SC --> Code["Host code validate กับ outputSchema"]
  SC --> App["MCP App render UI"]
ผู้บริโภคสองแบบ ช่องทางสองช่อง

อย่าสับสนกับฟีเจอร์ “structured outputs” ของ LLM structuredContent คือข้อมูลที่ server ผลิต ไม่เกี่ยวอะไรกับวิธีที่ model generate text

{
"name": "product_details",
"description": "Look up one product by its exact name",
"inputSchema": { "type": "object", "properties": { "name": { "type": "string" } }, "required": ["name"] },
"outputSchema": {
"type": "object",
"properties": { "name": { "type": "string" }, "price": { "type": "number" } },
"required": ["name", "price"]
}
}

สัญญาที่มาพร้อมกัน:

  • server ต้อง คืน structuredContent ที่ตรงตาม outputSchema
  • client ควร validate structuredContent กับ schema นั้น (และทำตามกฎ $ref — ห้าม fetch $ref ที่ชี้ไป network โดยอัตโนมัติ)
  • outputSchema เป็น JSON Schema 2020-12 อะไรก็ได้ รวมถึง array ที่ root:
{ "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" } }, "required": ["id"] } }

ทำไปทำไม? host ที่รู้ shape ของ output จะ generate function แบบมี type ได้ (productDetails(name): Promise<{name: string; price: number}>) สำหรับ programmatic tool calling, ต่อ tool ของคุณเข้ากับตัวอื่นโดยไม่ต้องให้ข้อมูลไหลผ่าน model และปฏิเสธผลลัพธ์ที่ผิดรูปก่อนมันจะไปปนเปื้อนบทสนทนา tool ที่ไม่มี outputSchema จะตกไปอยู่ในสภาพ “มันเป็น string นะ โชคดี”

server.registerTool(
'product_details',
{
description: 'Look up one product by its exact name',
inputSchema: z.object({ name: z.string() }),
outputSchema: z.object({ name: z.string(), price: z.number() }),
},
async ({ name }) => {
const product = catalog.find((p) => p.name === name);
if (!product) {
return { content: [{ type: 'text', text: `No product named ${name}` }], isError: true };
}
const output = { name: product.name, price: product.price };
return {
content: [{ type: 'text', text: JSON.stringify(output) }],
structuredContent: output,
};
},
);

เมื่อตั้ง outputSchema แล้ว ผลลัพธ์ต้องมี structuredContent และ SDK จะ validate มันกับ schema ก่อนส่ง

host คือคนตัดสินใจว่าจะถามคนก่อนรัน tool ไหม มันอ่านโค้ดของคุณไม่ได้ ดังนั้นบอกมันด้วย annotation:

{
"name": "clear_catalog",
"title": "Clear the catalog",
"description": "Remove every product from the catalog",
"annotations": { "readOnlyHint": false, "destructiveHint": true, "idempotentHint": true, "openWorldHint": false }
}
Hintค่าเริ่มต้นความหมาย
readOnlyHintfalsetool ไม่แก้ไข environment ของมัน
destructiveHinttrueถ้าไม่ใช่ read-only tool อาจทำการอัปเดตแบบทำลาย (ลบ, เขียนทับ) ไม่ใช่แค่เพิ่มอย่างเดียว
idempotentHintfalseเรียกซ้ำด้วย argument เดิมไม่มีผลเพิ่มเติม
openWorldHinttruetool ติดต่อกับสิ่งภายนอก (เว็บ, third-party API) ไม่ใช่ระบบปิด

host ที่ทำตัวดีจะทำอะไรกับสิ่งเหล่านี้:

  • อนุมัติ tool ที่ readOnlyHint: true อัตโนมัติ; ถามยืนยันกับตัวที่ destructive
  • retry tool ที่ idempotentHint: true ได้อย่างปลอดภัยหลัง timeout; ไม่ retry ตัวที่ไม่ idempotent แบบสุ่มสี่สุ่มห้า
  • ติดธง tool ที่ openWorldHint: true บน UI เพื่อให้ user รู้ว่าข้อมูลอาจออกนอกเครื่อง

ค่าเริ่มต้นถูกตั้งให้ มองโลกในแง่ร้าย โดยตั้งใจ: tool ที่ไม่มี annotation จะถูกมองว่าอาจ destructive, ไม่ idempotent และ open-world ดังนั้นตั้งมันซะ — tool ค้นหาแบบ read-only ที่ไม่ประกาศ readOnlyHint จะสร้างหน้ายืนยันที่ไม่จำเป็นขึ้นมา

spec พูดตรง ๆ: client ต้องถือว่า tool annotation เชื่อถือไม่ได้ เว้นแต่มาจาก server ที่เชื่อถือ server ที่ประสงค์ร้ายจะติดป้าย rm -rf ว่า read-only ก็ได้ annotation ช่วยให้ประสบการณ์กับ server ที่คุณเชื่อถือดีขึ้น แต่ไม่ใช่ security boundary host ยังควรแสดง input ของ tool ให้ user ดูก่อนส่ง และ user ยังต้องปฏิเสธการเรียกใดก็ได้

server.registerTool(
'clear_catalog',
{
title: 'Clear the catalog',
description: 'Remove every product from the catalog',
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
},
async () => {
catalog.length = 0;
return { content: [{ type: 'text', text: 'Catalog cleared' }] };
},
);

name คือตัวระบุที่ model ใช้ใน tools/call; ต้องทำตามกฎการตั้งชื่อและไม่ควรเปลี่ยนพร่ำเพรื่อ title คือข้อความอิสระสำหรับคน: “Search the product catalog” แทนที่จะเป็น search_catalog host แสดง title ในหน้ายืนยันและตัวเลือก tool และถอยไปใช้ name เมื่อไม่มี resource และ prompt ก็แยกแบบเดียวกัน

tool, prompt, resource และตัว server เองแนบ icons ได้:

{ "icons": [ { "src": "https://example.com/search.png", "mimeType": "image/png", "sizes": ["48x48"] } ] }

อนุญาตเฉพาะ URI แบบ https: และ data:; client ต้องปฏิเสธ javascript:, file: และ scheme อื่น, fetch icon โดยไม่แนบ credential และตรวจว่า origin ของ icon ตรงกับของ server client ที่ render icon ต้องรองรับ PNG และ JPEG และควรรองรับ SVG (พร้อม sanitize — SVG มี script ได้) และ WebP มอง icon เป็นของแถมสำหรับ host แบบกราฟิก อย่าให้ server ของคุณต้องพึ่งมัน

มีอีก property หนึ่งที่โผล่ใน inputSchema ได้: "x-mcp-header": "Region" บน parameter ชนิด primitive จะบอก client แบบ Streamable HTTP ให้สะท้อน argument นั้นไปเป็น header Mcp-Param-Region เพื่อให้ load balancer route ได้โดยไม่ต้อง parse body มันเป็นเรื่องของ transport ที่มีข้อจำกัดเข้มงวด — อยู่ในบท Streamable HTTP ห้ามใช้กับข้อมูลลับเด็ดขาด

tool ประกาศ outputSchema server ต้องทำอะไร?
ค่าเริ่มต้นของ destructiveHint คืออะไรเมื่อ tool ไม่มี annotation?
client ต้องปฏิบัติกับ tool annotation จาก server ที่ไม่รู้จักอย่างไร?
icon src แบบไหนที่ client ต้องปฏิเสธ?