Progress, Cancellation, and Timeouts
request เดียว กับความอดทนนิดหน่อย
หัวข้อที่มีชื่อว่า “request เดียว กับความอดทนนิดหน่อย”tool call ไม่ได้เสร็จทันทีทุกอัน import catalog, รัน test suite, render รายงาน — พวกนี้ใช้เวลาหลายวินาทีถึงหลายนาที และระหว่างนั้นมีสองอย่างที่ผิดพลาดได้: client ยอมแพ้ (timeout) หรือผู้ใช้เปลี่ยนใจแต่ไม่มีใครบอก server progress notification แก้อย่างแรก cancellation แก้อย่างหลัง
ทั้งสองอย่างยังอยู่ ภายใน request เดียว ถ้างานจะอยู่นานกว่า request ที่สมเหตุสมผล — หลายนาทีถึงหลายชั่วโมง หรือต้องรอดจากการหลุด connection — ให้ไปใช้ Tasks extension แทน
Progress
หัวข้อที่มีชื่อว่า “Progress”opt-in ด้วย progressToken
หัวข้อที่มีชื่อว่า “opt-in ด้วย progressToken”progress เริ่มจากฝั่ง client: client ที่ อยาก ได้ update จะใส่ progressToken ไว้ใน _meta ของ request token เป็น string หรือ integer ที่ client เลือกเอง และต้องไม่ซ้ำกันในบรรดา request ที่ยัง active ของ client นั้น
{ "jsonrpc": "2.0", "id": 7, "method": "tools/call", "params": { "name": "import_catalog", "arguments": { "urls": ["https://a.example/1.csv", "https://a.example/2.csv"] }, "_meta": { "progressToken": "imp-7", "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {} } }}ไม่มี token ก็ไม่มี progress — server ต้องส่ง progress เฉพาะกับ token ที่ได้รับมาจริงบน request ที่ยังค้างอยู่
ตัว notification
หัวข้อที่มีชื่อว่า “ตัว notification”{ "jsonrpc": "2.0", "method": "notifications/progress", "params": { "progressToken": "imp-7", "progress": 1, "total": 2, "message": "Imported https://a.example/1.csv" }}กฎที่สำคัญ:
progressต้องเพิ่มขึ้น ทุก notification แม้จะไม่รู้totaltotalและmessageเป็น optional ทั้งสองค่าเป็นทศนิยมได้- server จะ ไม่ส่งเลย หรือส่งกี่ครั้งก็ได้ แต่ ควร rate-limit ไม่ใช่ถล่มส่ง
- notification ต้องหยุด ทันทีที่ request เสร็จ
sequenceDiagram participant C as Client participant S as Server C->>S: tools/call พร้อม progressToken S-->>C: notifications/progress 1 of 3 S-->>C: notifications/progress 2 of 3 S-->>C: notifications/progress 3 of 3 S-->>C: result Note over C,S: หลัง result แล้วไม่มี progress อีก
มันเดินทางผ่านไหน
หัวข้อที่มีชื่อว่า “มันเดินทางผ่านไหน”- Streamable HTTP: server ตอบ POST ด้วย SSE stream ที่ผูกกับ request นั้น progress event มาบน stream นี้ ตามด้วย JSON-RPC response สุดท้ายซึ่งปิด stream progress ไม่มีทางโผล่บน stream ของ
subscriptions/listen - stdio: ทุกอย่างใช้ stdout channel เดียวร่วมกัน client จับคู่ notification กับ request ด้วย
progressToken
server.registerTool( 'import_catalog', { description: 'Import catalog files', inputSchema: z.object({ urls: z.array(z.string()) }) }, async ({ urls }, ctx) => { const progressToken = ctx.mcpReq._meta?.progressToken; for (let i = 0; i < urls.length; i++) { await importOne(urls[i]); if (progressToken !== undefined) { await ctx.mcpReq.notify({ method: 'notifications/progress', params: { progressToken, progress: i + 1, total: urls.length, message: `Imported ${urls[i]}` }, }); } } return { content: [{ type: 'text', text: `Imported ${urls.length} files` }] }; });from mcp.server import MCPServerfrom mcp.server.mcpserver import Context
mcp = MCPServer("catalog")
@mcp.tool()async def import_catalog(urls: list[str], ctx: Context) -> str: """Import book records from a list of catalog URLs.""" for done, url in enumerate(urls, start=1): await import_one(url) # no-op if the client did not send a progressToken await ctx.report_progress(done, total=len(urls), message=f"Imported {url}") return f"Imported {len(urls)} records."Cancellation
หัวข้อที่มีชื่อว่า “Cancellation”client ทิ้ง request ที่ไม่สนใจแล้วได้ วิธี ขึ้นกับ transport เพราะ transport สองแบบมองว่า “request หนึ่งอัน” ในทางกายภาพคืออะไรไม่เหมือนกัน
| Transport | client cancel อย่างไร | ทำไม |
|---|---|---|
| stdio | ส่ง notifications/cancelled พร้อม requestId ของ request นั้น (และ reason ถ้าต้องการ) | channel เดียวใช้ร่วมกัน — ไม่มี stream ต่อ request ให้ปิด |
| Streamable HTTP | ปิด SSE response stream ของ request นั้น | แต่ละ request มี stream ของตัวเอง การตัด connection จึงชัดเจนในตัว ไม่ต้องส่ง notifications/cancelled |
{ "jsonrpc": "2.0", "method": "notifications/cancelled", "params": { "requestId": 7, "reason": "user pressed stop" }}ไม่ว่า transport ไหน server ควร หยุดงานเร็วที่สุดเท่าที่ทำได้ และ ต้องไม่ ส่ง message ใด ๆ ของ request นั้นอีก — ไม่มี progress ที่มาช้า ไม่มี result ส่วน client ก็ควรเมินอะไรก็ตามที่มาถึงหลังจาก cancel ไปแล้ว race มันเกิดขึ้นได้
กฎขอบ ๆ ที่ควรรู้:
- stream ของ
subscriptions/listenถูก cancel แบบเดียวกัน — ปิด stream บน HTTP หรือส่งnotifications/cancelledด้วย id ของ listen request บน stdio - cancellation เป็น best-effort และต้องร่วมมือกัน tool ที่ทำ side effect ที่ย้อนกลับไม่ได้ไปครึ่งทางอาจทำจนจบ ออกแบบ tool ให้สิ่งที่เกิดขึ้นไปแล้วมองเห็นได้ (return handle, ทำให้การเขียน idempotent)
- หมายเหตุยุคเก่า: บน stdio
notifications/cancelledเป็น notification จาก client ไป server ตัวเดียว ที่เหลืออยู่ใน core protocol 2026-07-28
Timeouts
หัวข้อที่มีชื่อว่า “Timeouts”MCP ไม่ได้บังคับ timeout ปล่อยให้ client กำหนดนโยบายเอง และนั่นคือการตัดสินใจออกแบบที่คุณจะต้องทำใน host ทุกตัวที่สร้าง มีสองตัวเลือกที่สมเหตุสมผล:
- deadline คงที่ — ง่ายที่สุด ทุก request ได้ เช่น 60 วินาที ถ้าหมดก็ cancel คาดเดาได้ แต่ฆ่า tool ที่ใช้เวลานานโดยชอบธรรม
- deadline ที่ reset ตาม progress — timer เริ่มนับใหม่ทุกครั้งที่
notifications/progressมาถึง โดยมีเพดานสูงสุดเพื่อไม่ให้ server ที่พูดมากแต่ค้างรันไปตลอดกาล นี่คือ pattern ที่ host ส่วนใหญ่ลงเอยด้วย: server ที่เงียบถือว่าตายแล้ว server ที่ยังพูดอยู่ได้รับความอดทน แต่มีลิมิต
server ควรสมมติว่าตัวเลือก 2 มีอยู่ และส่ง progress กับอะไรก็ตามที่ช้ากว่าสองสามวินาที — มันคือความต่างระหว่าง “กำลัง import 4,000 แถวอยู่” กับ “request timed out”
เมื่อ timeout ทำงาน client ควร cancel จริง ๆ (ปิด stream หรือส่ง notifications/cancelled) ไม่ใช่แค่หยุดฟัง เพื่อให้ server คืน resource ได้
ข้อคิดด้านการออกแบบ
หัวข้อที่มีชื่อว่า “ข้อคิดด้านการออกแบบ”- progress มีไว้ให้ผู้ใช้ ไม่ใช่ model ใส่ข้อความที่คนอ่านรู้เรื่องใน
messageอย่าเข้ารหัสข้อมูลที่ LLM ควรเอาไปทำอะไรต่อ - ห้าม block เพราะ progress การส่ง notification ต้องไม่ทำให้งานชะงัก มองมันเป็น fire-and-forget
- tool ที่ยาวแต่ไม่มี progress คือ bug ใน production ถ้างานรายงาน progress ไม่ได้ มันน่าจะเหมาะกับ Tasks extension มากกว่า