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

Deploying Remote Servers

protocol version 2026-07-28 ทำให้ MCP เป็น protocol แบบ stateless: ทุก request พก version, capability และ identity มาใน _meta และ server ต้องไม่ พึ่ง request ก่อนหน้าบน connection เดียวกัน ไม่มี Mcp-Session-Id ไม่มี handshake ไม่มี GET stream ที่ต้องเปิดค้างไว้

ในเชิงปฏิบัติการ นั่นหมายความว่า:

  • load balancer อะไรก็ใช้ได้ ไม่ต้องมี sticky session ไม่ต้อง consistent hashing round-robin ก็พอ
  • autoscaling ง่ายมาก instance ใหม่ตอบ request ถัดไปได้ทันทีแบบ cold state ที่อยู่นานอยู่หลัง handle ที่ชัดเจนใน database ไม่ใช่ใน process
  • restart ได้อย่างปลอดภัย request ที่ค้างอยู่หายไป client ส่งใหม่ด้วย id ใหม่ และ client เปิด subscriptions/listen stream ใหม่เอง
flowchart LR
  c1["client A"] --> lb["HTTPS load balancer"]
  c2["client B"] --> lb
  lb --> s1["instance 1"]
  lb --> s2["instance 2"]
  lb --> s3["instance 3"]
  s1 --> db["shared state store, keyed by handle"]
  s2 --> db
  s3 --> db
fleet แบบ stateless หลัง load balancer ธรรมดา

ถ้าคุณเริ่มอยากได้ sticky session แปลว่าคุณซ่อน state ไว้ใน process ย้ายมันไปไว้หลัง handle

createMcpHandler ของ TypeScript SDK สร้าง handler แบบ web-standard (function สไตล์ fetch) ซึ่งรันบน Cloudflare Workers, Deno และ Bun ได้โดยไม่ต้องแก้ ส่วน Node ก็ห่ออีกชั้นเดียว

// server.ts — shared between runtimes
import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';
export const handler = createMcpHandler(() => {
const server = new McpServer({ name: 'notes', version: '1.4.0' });
server.registerTool(
'add-note',
{ description: 'Save a note', inputSchema: z.object({ text: z.string() }) },
async ({ text }) => ({ content: [{ type: 'text', text: `Saved: ${text}` }] }),
);
return server;
});
// worker.ts — Cloudflare Workers / Deno / Bun: the handler is the app
export default handler;
// node.ts — Node behind Express
import { createMcpExpressApp } from '@modelcontextprotocol/express';
import { toNodeHandler } from '@modelcontextprotocol/node';
const app = createMcpExpressApp({ host: '0.0.0.0', allowedHosts: ['mcp.example.com'] });
const node = toNodeHandler(handler);
app.all('/mcp', (req, res) => void node(req, res, req.body));
app.get('/healthz', (_req, res) => res.send('ok'));
app.listen(8080);

ใน Python มีสองปุ่มที่สำคัญบน production stateless_http=True คือโหมด per-request แบบยุคใหม่ ส่วน json_response=True ทำให้ server ตอบด้วย JSON body เดียวแทน SSE stream ทุกครั้งที่ทำได้ ซึ่งเป็นมิตรกับ proxy และ log ง่ายกว่า แต่แลกกับการเสีย notifications/progress ระหว่าง call เลือกตาม server: service lookup ที่เร็วอยากได้ JSON ส่วน importer ที่รันนานอยากได้ SSE

รูปแบบ factory ต่อ request (createMcpHandler(() => ...)) ตั้งใจให้เป็นแบบนี้: SDK สร้าง McpServer ใหม่ทุก request เพื่อไม่ให้อะไรรั่วข้ามผู้เรียก ทำ factory ให้ถูก — สร้าง client และ pool นอก factory แล้ว closure เข้าไปใช้

Streamable HTTP คือ POST ธรรมดา แต่ response บางอันเป็น SSE stream และ subscriptions/listen คือ stream ที่เปิดค้างไว้นานเท่าที่ client ยังสนใจ reverse proxy มักตั้งค่า default ที่ทำให้ทั้งสองอย่างพัง:

การตั้งค่าเหตุผลค่าทั่วไป
ปิด response bufferingSSE ที่ถูก buffer จะมาถึงเป็นก้อนเดียวตอนจบserver ส่ง X-Accel-Buffering: no; nginx proxy_buffering off
read / idle timeout ยาวlisten stream อาจเงียบเป็นนาทีระหว่าง notificationหลายนาทีถึงหลายชั่วโมง เฉพาะ route /mcp
keep-aliveintermediary ปิด connection ที่เงียบserver ส่ง SSE comment line :\r\n เป็นระยะ; client เมินมัน
HTTP/1.1 หรือ HTTP/2 ไปยัง upstreamHTTP/1.0 stream chunked response ไม่ได้proxy_http_version 1.1 บน nginx
ขีดจำกัด request bodyargument ของ tool ใหญ่ได้แต่ไม่ไร้ขอบเขต1–10 MB แล้วตอบ 413

spec แนะนำ X-Accel-Buffering: no บนทุก SSE response ก็เพราะ proxy สไตล์ nginx buffer เป็น default SDK ตั้งให้อยู่แล้ว แต่ proxy ตรงกลางยัง override ได้ ดังนั้นให้ตรวจด้วย stream จริง

  • validate Origin server ต้อง ตอบ 403 เมื่อมี Origin ที่ไม่ถูกต้อง นี่คือการป้องกัน DNS rebinding createMcpExpressApp({ allowedHosts }) ทำให้ ตั้งเป็น hostname จริงของคุณ
  • HTTPS เท่านั้น terminate TLS ที่ edge แล้ว bind แอปกับ interface ส่วนตัว ตอนรันบนเครื่องให้ bind 127.0.0.1 ไม่ใช่ 0.0.0.0
  • authenticate ทุก request module authorization พูดถึง pattern ของ OAuth resource server ไปแล้ว บน production ทุก POST ไปที่ /mcp ต้องพก bearer token ที่คุณตรวจ audience และ scope
  • rate limit ต่อ token ไม่ใช่ต่อ IP agent ที่อยู่หลัง NAT เดียวกันดูเหมือน IP เดียว
  • จำกัดทุกอย่าง: ขนาด request, เวลารันของ tool, ความซับซ้อนของ schema (anyOf tree ที่ลึกคือช่องทาง DoS ต่อ validator) และจำนวน listen stream พร้อมกันต่อ principal
  • validate input ของ tool เอง ถึง schema จะทำอยู่แล้ว เพราะ schema ไม่รู้ business rule ของคุณ

transport mirror field ของ body ไปเป็น HTTP header ก็เพื่อให้ infrastructure route และวัดผลได้โดยไม่ต้อง parse JSON:

POST /mcp HTTP/1.1
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: execute_sql
Mcp-Param-Region: us-west1
  • Mcp-Method ให้ dashboard latency และ error ต่อ method ที่ load balancer
  • Mcp-Name ให้ metric ต่อ tool และให้ WAF block tool อันตรายตัวเดียวได้โดยไม่แตะ body
  • Mcp-Param-{Name} ที่มาจาก x-mcp-header ใน inputSchema ของ tool ให้ gateway route ตาม tenant หรือ region ก่อน request ไปถึง instance

server หลัง proxy ยังต้อง ตรวจว่าค่าใน header ตรงกับ body (-32020 เมื่อไม่ตรง) เพื่อให้ intermediary กับ instance ไม่มีวันเห็นต่างกันว่า request นี้คืออะไร ห้ามใส่ความลับหรือ PII ใน parameter ที่มี x-mcp-header เพราะ header มองเห็นได้ทุก hop

client cache tools/list ตาม ttlMs และ model จำชื่อ tool ของคุณไว้ในบทสนทนายาว ๆ เปลี่ยนแปลงอย่างระมัดระวัง:

  1. bump serverInfo.version ทุก release client log มันไว้ คุณจะอยากเห็นใน bug report
  2. ให้การเปลี่ยน tool เป็นแบบเพิ่มเท่านั้น parameter optional ใหม่ที่มี default, tool ใหม่, enum ที่กว้างขึ้น ห้ามเปลี่ยนชื่อ tool หรือทำให้ parameter optional กลายเป็น required ในที่เดิม
  3. deprecate ใน description ก่อน: "Deprecated: use search_issues_v2. Will be removed after 2026-12-01." model อ่านมัน คนก็อ่าน
  4. ส่ง notifications/tools/list_changed ให้ client ที่ listen toolsListChanged เมื่อชุด tool เปลี่ยนจริง และลด ttlMs ในช่วง migration
  5. รองรับ protocol สองยุค ถ้ายังมี client 2025-11-25 อยู่: TypeScript handler รองรับทั้งคู่โดย default ตั้ง legacy: 'reject' เฉพาะเมื่อมั่นใจแล้ว
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY dist ./dist
ENV NODE_ENV=production PORT=8080
EXPOSE 8080
USER node
CMD ["node", "dist/node.js"]

เพิ่ม route GET /healthz ธรรมดา (นอก /mcp) สำหรับ liveness probe ของ orchestrator เพราะ MCP endpoint เองพูดแค่ POST ถ้า probe ด้วย GET จะได้ 405 ซึ่ง health checker ส่วนใหญ่ถือว่าล้มเหลว

ทำไม server ยุค 2026-07-28 ถึงไม่ต้องใช้ sticky session บน load balancer?
X-Accel-Buffering: no ทำอะไรบน SSE response?
json_response=True ใน Python SDK แลกกับอะไร?
ข้อไหนคือวิธี evolve tool บน production ที่ปลอดภัย?