ข้ามไปยังเนื้อหา
27/30บทที่ 27 จาก 30

Ship MCP Server: วัดกันจริงด้วย TypeScript และ Python

เซิร์ฟเวอร์เดียวกันเขียนสองครั้ง—สาม tools, หนึ่ง resource, หนึ่ง prompt—แล้วชั่งน้ำหนัก: 94 packages เทียบ 28 และ cold start 145 ms เทียบ 709

ในหน้านี้

นี่คือข้อถกเถียงเรื่องภาษาแบบครบถ้วน วัดให้เห็นก่อนจะเริ่มพูดสักคำ

spawn → tools/list answered, median of 25 launchesTEXT
node ./incidents.js         144.5 ms
python incidents.py         709.4 ms
npx incidents-mcp           712.6 ms

สองบรรทัดแรกคือการเปรียบเทียบที่ทุกคนอยากเห็น บรรทัดที่สามคือเซิร์ฟเวอร์ TypeScript ตัวเดียวกับบรรทัดแรก แต่เปิดใช้ในแบบที่จะถูกแจกจ่ายจริง — และมันจบที่ห่างจาก Python เพียงสามมิลลิวินาที

บทที่ 26 อ่าน Model Context Protocol เทียบกับสเปกของมันเองด้วย JSON-RPC ดิบ เพราะ JSON-RPC ดิบไม่มีภาษา บทนี้มีสองภาษา และน้ำหนักของข้อถกเถียงอยู่ตรงนี้: เซิร์ฟเวอร์เดียวกัน เขียนสองครั้ง สาม tools, หนึ่ง resource, หนึ่ง prompt, ทั้งสอง SDKs, ไม่มีทางลัดฝั่งไหน จากนั้นจึงไปที่ transports, inspector, 401 และตัวเลขที่ยังไม่มีใครเผยแพร่

เซิร์ฟเวอร์ และเหตุผลที่มีห้าสิ่งนี้อยู่ในนั้น

ลิงก์ไปยังส่วน: เซิร์ฟเวอร์ และเหตุผลที่มีห้าสิ่งนี้อยู่ในนั้น

บันทึกเหตุการณ์ หนึ่งชุดมีสาม tools เพราะการแยกระหว่างอ่านกับเขียนของ บทที่ 18 ต้องเห็นได้ชัด: search_incidents อ่าน, open_incident เขียนและส่ง handle กลับมา, resolve_incident รับ handle นั้นแล้วปิด หนึ่ง resource คือ incidents://open เพราะการอ่านรายการปัจจุบันคือสิ่งที่ แอปพลิเคชัน แนบไว้ หนึ่ง prompt คือ postmortem เพราะ “เขียนสรุปเรื่องนี้” คือ slash command ของคน นั่นคือลำดับชั้นการควบคุมของบทที่ 26 — model, application, person — แปลงเป็นการลงทะเบียนห้ารายการ

handle สำคัญกว่าที่เห็น บทที่ 26 ทำปฏิทินของเล่นพังด้วยการเก็บสถานะไว้ในอาร์เรย์ระดับโมดูล: โปรโตคอลไม่มี session ดังนั้น tool สำหรับสร้างจึงส่ง opaque identifier กลับมา และทุก call หลังจากนั้นรับมันเป็น argument ปกติ ไม่มีอะไรในไฟล์ใดสมมติว่า caller คือ process ที่เปิดมันขึ้นมา

นี่คือ tool เดียวกันในทั้งสองภาษา ลงทะเบียนวางข้างกัน:

incidents.tsTS
server.registerTool(
  "resolve_incident",
  {
    description:
      "Close an incident by handle and record its cause.",
    inputSchema: {
      id: z.string().describe(
        "The handle returned by open_incident, e.g. INC-3."),
      cause: z.string().describe(
        "One sentence. What actually broke."),
    },
    annotations: {
      readOnlyHint: false,
      destructiveHint: true,
      idempotentHint: true,
    },
  },
  async ({ id, cause }) => {
    const at = OPEN.findIndex((i) => i.id === id);
    if (at < 0) {
      return { isError: true, content: [{ type: "text",
        text: `No open incident ${id}. ` +
              `Call search_incidents first.` }] };
    }
    const [done] = OPEN.splice(at, 1);
    return { content: [{ type: "text",
      text: JSON.stringify({ ...done, cause }) }] };
  },
);
incidents.pyPYTHON
@server.tool(
    description=
      "Close an incident by handle and record its cause.",
    annotations=ToolAnnotations(
        readOnlyHint=False,
        destructiveHint=True,
        idempotentHint=True,
    ),
)
def resolve_incident(
    id: Annotated[str, Field(description=
        "The handle returned by open_incident, e.g. INC-3.")],
    cause: Annotated[str, Field(description=
        "One sentence. What actually broke.")],
) -> Incident:
    for at, i in enumerate(OPEN):
        if i["id"] == id:
            done = OPEN.pop(at)
            return {**done, "cause": cause}
    raise ValueError(
        f"No open incident {id}. Call search_incidents first.")

อ่านสิ่งที่ เหมือนกัน ก่อน เพราะนั่นคือข้อค้นพบ ทั้งคู่ประกาศชื่อ คำอธิบาย string arguments สองตัวที่มีคำอธิบาย และ annotations สามรายการ ทั้งคู่เป็น function เดียว ทั้งคู่ไม่กล่าวถึง JSON-RPC, framing, stdout หรือ protocol version เลย SDKs ทั้งสองมาบรรจบที่รูปทรงเดียวกัน ซึ่งคือสิ่งที่ “Tier 1” ควรหมายถึง1

มีความต่างจริงสองอย่าง และทั้งสองจะย้อนกลับมาในภายหลัง TypeScript อธิบาย arguments ด้วย schema library — ในที่นี้คือ Zod — และ schema เป็นค่าที่คุณเขียน Python อธิบายด้วย type hints ของ function เองและอ่านตอน import ซึ่งเป็นเหตุผลที่มันรู้เรื่องบางอย่างเกี่ยวกับ function ที่ไฟล์ TypeScript ไม่เคยบอกมันไว้ และเส้นทาง error: TypeScript ส่ง tool result กลับพร้อม isError, Python raise จดจำจุดนี้ไว้

การลงทะเบียนอีกสี่รายการไม่ได้ต่างกันในเชิงโครงสร้าง resource คือ server.registerResource("open-incidents", "incidents://open", …) เทียบกับ @server.resource("incidents://open", …); prompt คือ registerPrompt เทียบกับ @server.prompt บรรทัดสุดท้ายของแต่ละไฟล์คือ transport: await server.connect(new StdioServerTransport()) เทียบกับ server.run()

ไฟล์เต็ม: TypeScript 81 บรรทัดที่ไม่ว่างและ 3,060 bytes เทียบกับ Python 63 บรรทัดและ 2,555 bytes รับตัวเลขนี้พร้อมเกลือที่มันควรได้รับ — จำนวนบรรทัดวัด formatter พอ ๆ กับภาษา ซึ่งเป็นเหตุผลที่ไม่มีตัวเลขไหนอยู่ในตารางพาดหัวด้านล่าง

หลักฐานว่าภาษามองไม่เห็นคือ client หนึ่งตัวรันสองครั้งในสิบเอ็ดบรรทัด:

client.tsTS
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

const client = new Client({ name: "incident-cli", version: "1.0.0" });
await client.connect(new StdioClientTransport({
  command: process.argv[2], args: process.argv.slice(3) }));

const { tools } = await client.listTools();
console.log("tools:", tools.map((t) => t.name).join(", "));

const opened = await client.callTool({ name: "open_incident",
  arguments: { title: "Queue backed up", severity: "sev2" } });
console.log("open_incident ->", JSON.stringify(opened.content));

ชี้มันไปที่แต่ละ server ตามลำดับ ผลลัพธ์จริง ตัดให้สั้น:

TEXT
$ node client.ts node incidents.ts
tools: search_incidents, open_incident, resolve_incident
open_incident -> [{"type":"text","text":"{\"id\":\"INC-3\"}"}]

$ node client.ts ./py/.venv/bin/python ./py/incidents.py
tools: search_incidents, open_incident, resolve_incident
open_incident -> [{"type":"text","text":"{\n  \"id\": \"INC-3\"\n}"}]

tools เดียวกัน ลำดับเดียวกัน handle เดียวกัน client TypeScript บอกไม่ได้ว่า server เขียนด้วยอะไร และมันไม่เคยถาม นั่นคือคำสัญญาทั้งหมดของโปรโตคอลที่ยังคงยืนอยู่

ตอนนี้ดู whitespace ในผลลัพธ์ที่สอง เพราะมันไม่ใช่แค่ความสวยงาม: Python SDK serialize payloads ด้วย pydantic_core.to_json(result, fallback=str, indent=2) ในการอ่าน resource ที่มีสอง incidents อยู่ในรายการ body ของ TypeScript ยาว 136 อักขระและ 37 o200k_base tokens; body ของ Python ยาว 185 และ 62 เพิ่มขึ้นหกสิบแปดเปอร์เซ็นต์ของ tokens สำหรับแถวที่เหมือนกันทุกประการ จ่ายโดยใครก็ตามที่อ่าน resource เข้า prompt ทุกครั้ง

catalogue เล่าเรื่องเดียวกันด้วยสาเหตุที่ใหญ่กว่า ทั้งสอง servers, tools สามตัวเหมือนกัน, tools/list ชั่งน้ำหนักทีละ key:

keyTypeScriptPython
name2121
description4646
annotations4646
inputSchema211192
outputSchema187
execution27
total342480

schema ของ input ฝั่ง Python ถูกกว่า — bridge ของ Zod ใน TypeScript ประทับ $schema และ additionalProperties ลงบนแต่ละตัว ช่องว่าง 138-token ทั้งหมดคือ output schema ที่ไม่มีใครเขียน resolve_incident ถูก annotate เป็น -> Incident ดังนั้น SDK จึง derive JSON Schema สำหรับ return type แล้วส่งไป มันมีประโยชน์จริง — นี่คือสิ่งที่ทำให้ client validate structuredContent ได้ — และมันคือ 187 tokens ใน context window ของคุณที่มาถึงเพราะ type hint กฎของ บทที่ 24 เรื่อง definitions ที่เบียดวัสดุสำคัญออกไป ใช้กับ schemas ที่คุณไม่รู้ว่าคุณมีด้วย

เส้นทาง error สองแบบข้างต้นไม่ใช่การเลือกสไตล์ ใส่ tool ที่ล้มเหลวแบบ integration จริง ๆ ให้แต่ละ server แล้วอ่านว่าสิ่งใดไปถึง model

tools/call on a tool that raisesTEXT
TypeScript  {"content":[{"type":"text","text":
              "connect ECONNREFUSED 10.0.3.7:5432 (db-prod-eu, user=reporting)"}],
             "isError":true}

Python      {"content":[{"text":"Error executing tool boom","type":"text"}],
             "isError":true}

TypeScript SDK ใส่ internal address, port, database name และ service account เข้าไปใน context ของ model ส่วน Python SDK ไม่ใส่อะไรเหล่านั้นเลย; traceback ไปที่ stderr และอยู่บน server

ไม่มีอันไหนเป็นบั๊ก ทั้งคู่คือการตัดสินใจ และฝั่ง Python เขียนไว้ใน docstring ของตัวเอง: ToolError คือ “ความล้มเหลวที่คุณคาดไว้” และข้อความของมันจะถูกส่งกลับ “ใน content เพื่อให้ model อ่าน”; อย่างอื่น “ถือเป็น crash: model เห็นแค่ Error executing tool <name> และ server log traceback ที่ ERROR” class สำหรับกรณี crash พูดส่วนที่เหลืออย่างชัดเจน — “ไม่มีอะไรจากต้นฉบับไปถึง client”

พฤติกรรมทั้งสองผิดครึ่งหนึ่งของเวลา บทที่ 18 แย้งว่า validation error ควรกลับมาเป็น tool result ที่ model อ่านและแก้ได้ เพราะนั่นคือบรรทัดที่ให้ leverage สูงสุดใน integrations ส่วนใหญ่; ฝั่ง Python ต้อง raise ToolError โดยตรง และ ValueError เปล่า ๆ จะโยนประโยคที่มีประโยชน์ทิ้งไป ข้อโต้แย้งของ บทที่ 30 วิ่งไปอีกทาง: ทุกอย่างที่ tool ส่งกลับจะลงไปอยู่ใน context ที่ prompt injection ภายหลังอาจพยายามอ่านกลับออกมา และ exception string ที่ยังไม่ผ่านการ review คือข้อความที่ถูก audit น้อยที่สุดในระบบของคุณ

กฎที่รอดทั้งสองด้าน: ตัดสินใจต่อ tool ว่า failure ได้รับอนุญาตให้พูดอะไร และเขียน string นั้นเอง อย่าให้ข้อความ default ของ exception ตัดสินใจ ไม่ว่าในภาษาไหน

ทำให้พังโดยตั้งใจ: หนึ่งบรรทัดบน standard output

ลิงก์ไปยังส่วน: ทำให้พังโดยตั้งใจ: หนึ่งบรรทัดบน standard output

tutorial อย่างเป็นทางการระบุกฎโดยไม่กั๊ก: “For STDIO-based servers: Never write to stdout. Writing to stdout will corrupt the JSON-RPC messages and break your server. The print() function writes to stdout by default, so keep it out of a STDIO server entirely.”1 บทที่ 26 quote เวอร์ชัน normative ไว้ — server “MUST NOT write anything to its stdout that is not a valid MCP message”2

เพิ่มหนึ่งบรรทัดให้แต่ละ server แล้วอ่าน raw stream:

raw stdout, first two linesTEXT
TypeScript  incidents server starting
            {"result":{"protocolVersion":"2025-11-25", … },"jsonrpc":"2.0","id":1}

Python      {"jsonrpc":"2.0","id":1,"result":{ … }}
            incidents server starting

ตัว Python แย่กว่า และเหตุผลไม่ใช่ MCP process ที่มี stdout เป็น pipe แทน terminal จะได้ stream แบบ block-buffered ดังนั้นบรรทัดหลุดจึงถูก flush เมื่อ buffer ตัดสินใจ — ที่นี่คือตอน exit, หลัง response ที่มันถูกเขียนก่อนหน้า corruption ไม่ปรากฏตรงที่บั๊กอยู่ เพิ่ม flush=True หรือ library ที่ flush แล้วมันย้ายที่

จากนั้นคือส่วนที่อธิบายว่าทำไมสิ่งนี้ถึงถูก ship ป้อน server ที่พังให้ clients สามตัว:

TEXT
naive parser, dirty server   SyntaxError: Unexpected token 'i',
                             "incidents "... is not valid JSON
SDK client, dirty server     tools: search_incidents, open_incident, resolve_incident
MCP Inspector, dirty server  full catalogue, no warning

parser เจ็ดบรรทัดตายทันที client อย่างเป็นทางการและ Inspector ทั้งคู่ยักไหล่ — ข้ามบรรทัดนั้นแล้วไปต่อ กฎที่ทำให้พังเฉพาะ clients ที่ไม่มีใครใช้คือกฎที่จะไปถึง production แบบครบถ้วน นั่นคือเหตุผลที่คุ้มค่าจะทำให้พังโดยตั้งใจที่นี่ แทนที่จะไปเจอใน log ของลูกค้า

โหมด CLI ของ Inspector คือครึ่งที่มักถูกลืม: npx @modelcontextprotocol/inspector --cli <command> --method tools/list พิมพ์ catalogue แล้วออก ทำให้ script ได้ในแบบที่ browser UI ทำไม่ได้3

SDKs ทั้งคู่ install ได้เรียบร้อยใน directories ของตัวเอง ไม่มีอะไรแชร์กัน:

TypeScriptPython
package@modelcontextprotocol/sdk 1.30.0 + zod 3.25.76mcp 2.1.1
latest protocol revision implemented2025-11-252026-07-28
transitive packages installed9428
installed size13.9 MiB44.3 MiB
files on disk3,3862,018
third-party packages loaded to serve stdio8 of 9418 of 28
bare interpreter start, median19.4 ms11.1 ms
spawn → tools/list answered, median of 25144.5 ms709.4 ms
tools/list catalogue, o200k_base tokens342480

ทุกแถวทำให้ประหลาดใจคนละทิศทาง นั่นคือเหตุผลที่การเปรียบเทียบนี้คุ้มจะรันจริงแทนที่จะเดา

TypeScript install packages มากกว่าสามเท่าแต่ใช้ bytes น้อยกว่าหนึ่งในสาม dependencies 94 ตัวคือ ecosystem ของ npm เป็นตัวมันเอง — fast-deep-equal, es-errors, dunder-proto ส่วน 28 ตัวของ Python น้อยกว่าแต่ใหญ่โต: cryptography, pydantic-core และ uvicorn เป็น compiled artefacts ถ้าสัญชาตญาณของคุณบอกว่า dependency count คือสิ่งที่ควรกังวล แถวนี้คือ counter-example

interpreter ของ Python เริ่มเร็วกว่า Node และห่างกันชัดเจน — 11.1 ms เทียบกับ 19.4 ms บนโปรแกรมว่าง ดังนั้น 565 ms ในแถว cold-start ไม่ใช่ภาษา มันคือ SDK และแถว loaded-packages บอกว่าทำไม:

third-party modules loaded to answer one tools/list over stdioTEXT
TypeScript   8 of 94   sdk, zod, zod-to-json-schema, ajv, ajv-formats,
                       fast-deep-equal, fast-uri, json-schema-traverse

Python      18 of 28   mcp, mcp_types, pydantic, pydantic_core, anyio,
                       starlette, uvicorn, sse_starlette, httpx2,
                       cryptography, _cffi_backend, opentelemetry, click, …

server ที่มี I/O เพียง pipe import ASGI web server, HTTP client และ TLS library ก่อนจะอ่านบรรทัดแรก TypeScript SDK ก็ ship Express, Hono, jose และ eventsource ด้วย — พวกมันนอนอยู่บนดิสก์โดยไม่ถูกอ่าน เพราะขอบเขต package กันมันออกจาก import server/stdio.js package ของ Python เป็น import graph เดียว ดังนั้น import mcp คือทั้งหมด: python -X importtime ระบุว่า 727 ms อยู่ที่ import mcp.server.mcpserver — ตัวเลขที่วัดภายใต้ import profiler จึงออกมาสูงกว่า 709 ms ที่ run แบบไม่ profile ใช้ตั้งแต่ spawn จน answer — และ 269 ms อยู่ที่ subtree mcp.types เพียงอย่างเดียว — wire types เป็น Pydantic models, หนึ่ง class ต่อ protocol message ต่อ revision และการสร้างมันคืองานที่ทำตอน import นี่คือ design trade ไม่ใช่ความชุ่ย — eager imports คือเหตุผลที่ Python SDK ส่ง run(transport="streamable-http") ให้คุณในบรรทัดถัดไปได้โดยไม่ต้อง install เพิ่ม

แล้วแถวสุดท้ายของบล็อกเปิดเรื่องก็ล้มข้อโต้แย้งนั้นเอง package server TypeScript ให้ถูกต้อง — entry bin, shebang, npm link, ไม่มีอะไรต้อง download — แล้ว launch ผ่าน npx ด้วย --no-install ซึ่งเป็นวิธีที่ published stdio server ถูกเริ่มจริง:

median of 25, spawn → tools/list answeredTEXT
node ./incidents.js       144.5 ms
npx incidents-mcp         712.6 ms      (+568.1 ms of launcher)
python incidents.py       709.4 ms

launcher มีค่าใช้จ่าย 568 ms ต่อ start — สี่เท่าครึ่งของ import ทั้ง TypeScript SDK — และต้องจ่ายทุก launch เพราะ MCP host เริ่ม stdio server ด้วยการรัน command นั้น ดังนั้นรูปแบบที่ซื่อสัตย์ของ “TypeScript starts five times faster” คือ: ใช่ จนกว่าคุณจะแจกจ่ายมันด้วยวิธีปกติ caveat เดียวกันน่าจะใช้กับ uvx; เครื่องนี้ไม่มี uv ติดตั้ง ดังนั้นแถวนั้นจึงไม่มีอยู่ สิ่งที่ไม่ได้วัดไม่เข้าไปในตาราง

บทที่ 26 ครอบคลุม framing ของ stdio แล้ว เหลือสองเรื่องไว้สำหรับที่นี่

ข้อแรก: การรัน server ด้วย npx หรือ uvx คือ stdio transport ไม่มี “package mode” แยกต่างหาก configuration ของ host ระบุ command และ arguments; host spawn มันแล้วคุยผ่าน pipes นั่นคือเหตุผลที่ “ฉันจะแจกจ่ายสิ่งนี้อย่างไร” และ “มันพูด transport ไหน” เป็นคำถามเดียวกันในเครื่อง และเหตุผลที่ต้นทุนของ launcher อยู่ในบทว่าด้วยการ ship

ข้อที่สอง: stdio ไม่มีส่วน authorization เลย และ specification พูดไว้ในบรรทัดเดียว — implementations ที่ใช้ stdio “SHOULD NOT follow this specification, and instead retrieve credentials from the environment”4 security model ของมันคือของ operating system และขีดจำกัดของมันก็เช่นกัน: local subprocess ให้บริการได้พอดีหนึ่งเครื่องและหนึ่ง user

transport ที่ยังมีชีวิตอีกตัวคือ Streamable HTTP: endpoint เดียวที่รับ POST, หนึ่ง HTTP request ต่อ JSON-RPC message และ header Accept ที่ต้อง list ทั้ง application/json และ text/event-stream เพราะ server เลือกต่อ request ว่าจะตอบด้วยอันไหน5 บทที่ 14 parse event stream นั้นด้วยมือแล้ว ดังนั้น wire format ไม่มีอะไรใหม่ — มีแค่สิ่งที่ห่อมันไว้ obligation สามข้อของ revision ปัจจุบันพลาดง่าย และทั้งสาม test ได้:

ทุก POST มี MCP-Protocol-Version และค่าของมันต้อง match กับ protocolVersion ภายใน _meta ของ request เอง mismatch คือ 400 พร้อม header-mismatch error ไม่ใช่ยักไหล่5

Mcp-Method mirror method บนทุก request; Mcp-Name mirror params.name หรือ params.uri บน tools/call, resources/read และ prompts/get ทั้งสองมีไว้เพื่อให้ proxy route ได้โดยไม่ต้อง parse bodies5

รูปทรงเก่าหายไปแล้ว และต้องตอบด้วยการปฏิเสธ

ลิงก์ไปยังส่วน: รูปทรงเก่าหายไปแล้ว และต้องตอบด้วยการปฏิเสธ

GET stream, Mcp-Session-Id และ resumption Last-Event-ID ถูกถอดออกทั้งหมด server ที่พูดเฉพาะ revision นี้ควรตอบ 405 Method Not Allowed ต่อ GET หรือ DELETE, ignore session header โดยไม่ mint ใหม่ และ ignore Last-Event-ID5

ตอนนี้คือการวัดที่ปรับกรอบทั้งบทใหม่ ส่ง current-revision request ไปยังแต่ละ server ผ่าน HTTP

POST /mcp, MCP-Protocol-Version: 2026-07-28TEXT
Python   200  {"result":{"resultType":"complete","cacheScope":"private","ttlMs":0,
              "tools":[…],"_meta":{"io.modelcontextprotocol/serverInfo":{…}}}}

TypeScript    {"error":{"code":-32000,"message":"Bad Request: Unsupported protocol
              version: 2026-07-28 (supported versions: 2025-11-25, 2025-06-18,
              2025-03-26, 2024-11-05, 2024-10-07)"}}

constants สอดคล้องกับพฤติกรรม: LATEST_PROTOCOL_VERSION ของ Python SDK อ่าน 2026-07-28, ของ TypeScript SDK อ่าน 2025-11-25 ส่ง header-mismatch request จากขั้นด้านบน แล้ว Python server ตอบ 400 พร้อม error -32020 และข้อความ “mcp-protocol-version header does not match the request envelope's protocol version”; TypeScript SDK ไม่มี code แบบนั้น เพราะมันยังไม่ implement revision ที่ define สิ่งนี้

หน้าที่ list ทั้งสองไว้ที่ Tier 1 ยังบอกด้วยว่า “Each SDK provides the same functionality”1 ณ วันที่ด้านล่าง สำหรับ revision ปัจจุบัน ประโยคนั้นเป็นความใฝ่ฝัน ตรวจ LATEST_PROTOCOL_VERSION ใน SDK ที่คุณกำลังจะ install; มันคือหนึ่งบรรทัด และเป็น claim เดียวในบทนี้ที่จะยังสำคัญในอีกหนึ่งปี

ย้าย server ออกจากแล็ปท็อปของคุณ แล้ว client ของคนแปลกหน้ามาพร้อม token นี่คือครึ่งที่บทที่ 26 เว้นไว้ และครึ่งที่ product แบบ multi-user ข้ามไม่ได้

specification วาง MCP server ไว้ในบทบาท OAuth 2.1 และตั้งชื่อให้มัน: protected MCP server คือ resource server, client คือ OAuth client และ authorization server เป็นปัญหาของคนอื่น4 จากบทบาทนั้น มี mandatory clauses สี่ข้อ quote เต็มเพราะการ paraphrase คือทางที่ทำให้พลาด:

MCP servers, acting in their role as an OAuth 2.1 resource server, MUST validate access tokens as described in OAuth 2.1 Section 5.2. MCP servers MUST validate that access tokens were issued specifically for them as the intended audience, according to RFC 8707 Section 2. […] MCP clients MUST NOT send tokens to the MCP server other than ones issued by the MCP server's authorization server. MCP servers MUST only accept tokens that are valid for use with their own resources. MCP servers MUST NOT accept or transit any other tokens.4

“Must not accept or transit” คือกฎ anti-passthrough และเป็นเหตุผลที่ apparatus เรื่อง audience มีอยู่ทั้งหมด server ที่ replay bearer token ที่มันได้รับไปยัง third-party API คือ confused deputy: มันยืมความน่าเชื่อถือของตัวเองให้ใครก็ตามที่เรียกมัน กฎห้ามการ reuse ไม่ใช่แค่การ storage

การทำให้บังคับใช้ได้ต้องใช้ RFCs สี่ฉบับ ฉบับละหนึ่งงาน6 RFC 9728 คือวิธีที่ client หา authorization server ตั้งแต่แรก: MCP server ให้บริการ protected-resource-metadata document และ 401 ชี้ไปหามัน RFC 8707 คือ parameter resource — client ต้องส่ง canonical URI ของ server ใน ทั้ง authorization request และ token request “regardless of whether authorization servers support it” เพื่อให้ token ที่ออกมาระบุ audience ของมัน RFC 9207 ปิด loop จากอีกฝั่ง: client บันทึก issuer ก่อน redirect และเทียบ iss ที่ส่งกลับด้วย string ตรงตัว ไม่มี normalisation — ไม่มี case folding, ไม่มี default-port elision, ไม่มี trailing slash และ RFC 7591, Dynamic Client Registration, ตอนนี้ deprecated แล้วเพื่อหลีกทางให้ Client ID Metadata Documents, “retained for backwards compatibility with authorization servers that do not support” เอกสารเหล่านั้น4

ต่อสิ่งนั้นเข้ากับทั้งสอง servers ด้วย token verifier ที่ไม่ทำอะไรนอกจากเช็ค audience ลำดับขั้นของ TypeScript:

POST /mcp — TypeScript, with requireBearerAuthTEXT
no token            401  WWW-Authenticate: Bearer error="invalid_token",
                         error_description="Missing Authorization header",
                         scope="incidents:read",
                         resource_metadata="…/.well-known/oauth-protected-resource/mcp"
aud=other server    401  error_description="token audience is not this server"
no exp claim        401  error_description="Token has no expiration time"
right aud, no scope 403  error="insufficient_scope", scope="incidents:read"
right aud + scope   200  {"result":{"tools":[…]}}
GET /.well-known/oauth-protected-resource/mcpTEXT
{"resource":"http://127.0.0.1:8931/mcp",
 "authorization_servers":["https://auth.example.com/"],
 "scopes_supported":["incidents:read","incidents:write"],
 "resource_name":"Incidents"}

SDKs ทั้งคู่ serve document นั้นและทั้งคู่ชี้ 401 ไปหามัน ซึ่งคือเรื่อง discovery ทั้งหมด: client ที่ไม่เคยเห็น server ของคุณเรียนรู้ว่าจะ authenticate ที่ไหนจากการถูกปฏิเสธ 403 เป็นคนละเรื่อง — token ใช้ได้ แต่ scope ไม่ใช่ — และ challenge ระบุสิ่งที่ขาดเพื่อให้ client step up แทนที่จะเริ่มใหม่

บันไดสองขั้นต่างกัน และไม่มีความต่างใดอยู่ใน specification TypeScript SDK ปฏิเสธ token ที่ ไม่มี expiry claim; ฝั่ง Python ส่ง 200 กลับ เพราะ expires_at เป็น optional บน AccessToken ของมัน และ None หมายถึง “no opinion” ส่วน 403 ของ Python ถือ error_description="Required scope: incidents:read" โดยไม่มี parameter scope ที่ specification บอกว่า servers ควร include verifier ไม่ใช่ที่สำหรับยอมรับ library default: audience check เป็นสิ่งที่คุณต้องเขียนเองในทั้งสองภาษา และ expiry ก็เช่นกัน

ข้อจุกจิกที่ซื่อสัตย์จาก run เดียวกัน GET บน endpoint ตอบ 404 ใน wiring ของ Express และ 400 Bad Request: Missing session ID ในฝั่ง Python ในขณะที่ specification ขอ 405 Method Not Allowed และ “session ID” เป็น vocabulary ที่ revision นี้ถอดออกแล้ว ไม่มีอันไหนอันตราย ทั้งคู่คือรูปทรงของ ecosystem ที่กำลัง migrate

ชิ้นสุดท้ายของการ ship คือคุณ publish ที่ไหน และมันมีคำตอบพร้อมตัวเลข crawl วันนี้ server ทุกตัวใน official registry ที่ latest version:7

servers
total (latest version, not deleted)28,170
active / deprecated27,853 / 317
ship at least one installable package13,065
remote only — a URL, nothing to install14,696
npm8,275
PyPI3,603
OCI images867
mcpb bundles706
NuGet / Cargo107 / 43

การอ่านสองแบบชี้ไปคนละทาง ตามจำนวน servers ที่ publish, npm นำ 2.3 ต่อ 1 — ตัวเลขที่คน quote เมื่อต้องการบอกว่า ecosystem เป็น TypeScript ตาม downloads, Python นำ: ในสามสิบวันที่ผ่านมา mcp ได้ 286.7 ล้าน เทียบกับ @modelcontextprotocol/sdk ที่ 194.7 ล้าน ก่อนจะบวก fastmcp ที่ 72.1 ล้าน7 ทั้งสองเป็น Tier 1, normative schema คือ schema.ts และ tutorial อย่างเป็นทางการ “Build an MCP server” เปิดที่ tab Python1 ไม่ว่าครึ่งไหนอยู่ในหัวคุณ อีกครึ่งก็จริงเช่นกัน

และแถวที่สำคัญกว่าทั้งคู่: มากกว่าครึ่งของ registry — 14,696 จาก 28,170 — ไม่มีอะไรให้ install พวกนั้นคือ web services ยอดรวม transport เห็นตรงกันจากอีกฝั่ง: จาก package entries 14,290 รายการ มี 13,787 รายการประกาศ stdio; จาก remote entries 16,640 รายการ มี 15,570 รายการประกาศ Streamable HTTP และ 1,070 รายการยังประกาศ HTTP+SSE ที่ deprecated แล้ว ดังนั้น “MCP server คือ subprocess บนแล็ปท็อปของคุณ” อธิบาย minority ที่กำลังหดตัว และทุกหนึ่งใน 14,696 นั้นต้องใช้ section ข้างบนแทน environment variable

แสดงรายละเอียด

ตั้งใจให้ bilingual และ precedent ของมัน

นี่คือบทเดียวใน course ที่ bilingual เพราะคำตอบที่ซื่อสัตย์แยกเป็นสองฝั่ง: registry เป็น npm-first และ downloads เป็น Python-first พร้อมกัน ณ วันนี้ การเขียนแค่หนึ่งในสองจะยกครึ่งคำถามทิ้งและอธิบาย ecosystem ผิดไปพร้อมกัน มี precedent แบบเปิดเผย — Hugging Face MCP Course ระบุ prerequisites ว่า “Experience with at least one programming language (Python or TypeScript examples will be shown)” และสอนทั้งคู่8 โปรโตคอลที่มูลค่าทั้งหมดอยู่ที่จำนวน implementations ไม่ใช่ที่ที่ควรพูดภาษาเดียว

Section ลงวันที่: ทุกอย่างข้างบนที่มีอายุการใช้งาน

ลิงก์ไปยังส่วน: Section ลงวันที่: ทุกอย่างข้างบนที่มีอายุการใช้งาน

อ่านและวัดเมื่อ 7 กันยายน 2026 เทียบกับ protocol revision 2026-07-28

value
@modelcontextprotocol/sdk1.30.0, published 27 July 2026; 4,322,438 bytes unpacked, 693 files, 17 direct dependencies
latest revision it implements2025-11-25
mcp (PyPI)2.1.1, published 25 August 2026; 357,912-byte wheel, plus mcp-types 2.1.1 at 69,656 bytes
latest revision it implements2026-07-28
SDK tiersTypeScript, Python, C#, Go, Rust at Tier 1; Java, Ruby at Tier 2; Swift, PHP, Kotlin at Tier 3
registry servers28,170
downloads, last 30 daysmcp 286,653,871 · fastmcp 72,097,269 · @modelcontextprotocol/sdk 194,679,333

หมายเหตุ migration หนึ่งข้อที่ไม่ใช่ตัวเลข ใน mcp 2.x, FastMCP ถูก rename เป็น MCPServer และ tutorial ออนไลน์แทบทุกอันยังเปิดด้วย import เก่า SDK ship module ที่มีจุดประสงค์เดียวคืออธิบายเรื่องนั้น ซึ่งเป็น deprecation ที่ใส่ใจที่สุดในบทนี้:

from mcp.server.fastmcp import FastMCPTEXT
ModuleNotFoundError: No module named 'mcp.server.fastmcp'. This is mcp 2.x,
where FastMCP was renamed to MCPServer (from mcp.server.mcpserver import
MCPServer) and other APIs changed; see the migration guide … or pin 'mcp<2'
to keep running v1 code.

เมื่อมีตารางอยู่ตรงหน้า recommendation น่าเบื่อ ซึ่งเป็นสัญญาณที่ดี

ถ้า server อยู่ใน web application ที่คุณรันอยู่แล้ว ให้เขียนด้วย TypeScript process เดียวกัน, deploy เดียวกัน, request handler เดียวกัน; Streamable HTTP คือ endpoint ที่คุณเพิ่มข้าง ๆ อันอื่น; และ 13.9 MiB กับ 145 ms เป็นของฟรีเพราะ runtime เปิดอยู่แล้ว นั่นคือส่วนใหญ่ของ remote servers 14,696 ตัว

ถ้า server ห่อ data tooling ให้เขียนด้วย Python สิ่งที่คุณ expose คือ pandas, warehouse client, transforms เท่ากับ notebook หนึ่งเล่ม และ server ในภาษาอื่นจะเป็น subprocess call ที่สวม schema ไว้ การ import เจ็ดร้อยมิลลิวินาทีใน service ที่ start ครั้งเดียวไม่ใช่ cost; ใน subprocess ที่ host relaunch ทั้งวัน มันใช่

และตอนนี้ แถว revision override ทั้งคู่ ถ้าคุณต้องใช้ 2026-07-28 — multi-round-trip requests, resultType, cache hints, server/discover — SDK หนึ่งในสองมีวันนี้ และอีกตัวยังไม่มี

ตอนนี้คุณ ship server เดียวกันได้ในภาษาใดภาษาหนึ่ง ป้องกันการเลือกด้วยตารางแทนความชอบ รันมันบน transports ที่ยังมีชีวิตทั้งสองแบบ และส่ง token ให้มันปฏิเสธได้แล้ว

สิ่งที่คุณสร้างยังคงเป็น function: schema, endpoint, สิ่ง deterministic ที่ model invoke ความรู้ทั้ง class ไม่เข้ากับรูปทรงนั้น — วิธีที่ เรา เขียน postmortem, fields ที่ incident reports ของเราต้องมี, ลำดับที่เราทำสิ่งต่าง ๆ และเหตุผล มันคือ procedure, มันคือ prose และการบังคับมันเข้าไปใน tool description คือวิธีที่ system prompts โตเป็นสองพัน tokens ที่ต้องจ่ายทุก turn ไม่ว่าบทสนทนาจะเกี่ยวกับ incidents หรือไม่

บทที่ 28 คือคำตอบอีกแบบ: folder ที่มี SKILL.md อยู่ในนั้นซึ่ง model อ่าน แทนที่จะ call, โหลดเป็นสามระดับเพื่อให้ reference material แทบไม่เสียค่าใช้จ่ายจนกว่าจะถึง turn ที่ต้องใช้ มันไม่มีภาษาหลัก และนั่นคือสิ่งแรกที่มันสอน


ทุกอย่างที่นี่วัดเมื่อ 7 กันยายน 2026 บน Node 22.22.3 และ Python 3.14.4 เทียบกับ @modelcontextprotocol/sdk 1.30.0 พร้อม zod 3.25.76 และ mcp 2.1.1 โดยแต่ละตัว install ใน throwaway directory ของตัวเอง Timings เป็น medians ของ 25 launches, wall clock จาก spawn ถึงบรรทัดที่ถือ response tools/list; token counts คือ o200k_base ผ่าน tiktoken บน JSON ของแต่ละ definition ไม่มีการเรียก paid API: ไม่มีอะไรที่นี่ต้องใช้ model

servers สองตัวมี 81 และ 63 บรรทัดที่ไม่ว่าง; หนึ่งในสาม tools ของมันถูก reproduce ข้างบนในทั้งสองภาษา และการลงทะเบียนอีกสี่รายการต่างกันเฉพาะตามที่อธิบายไว้ นโยบาย error-disclosure ของ Python SDK quote จาก docstrings ของ ToolError และ UnexpectedToolError ใน mcp/server/mcpserver/exceptions.py; default pretty-printing คือ pydantic_core.to_json(result, fallback=str, indent=2) ใน mcp/server/mcpserver/resources/types.py และ utilities/func_metadata.py constants ของ protocol-version คือ LATEST_PROTOCOL_VERSION ใน mcp_types/version.py และใน types.js ของ TypeScript SDK ทั้งคู่อ่านจาก packages ที่ install แล้ว ไม่ใช่จาก changelog

  1. SDKs, modelcontextprotocol.io/docs/sdk, และ Build an MCP server, modelcontextprotocol.io/docs/develop/build-server, ทั้งคู่อ่าน 7 กันยายน 2026 แหล่งที่มาของ tier table, ประโยค “Each SDK provides the same functionality but follows the idioms and best practices of its language”, ลำดับ language-tab ของ tutorial (Python, TypeScript, Java, Kotlin, C#, Ruby, Rust, Go), และกฎ logging ที่ quote เรื่อง print() และ stdout 2 3 4

  2. stdio transport, .../basic/transports/stdio แหล่งที่มาของ newline framing และกฎ purity stdout บทที่ 26 อ่านหน้านี้เต็ม; อ้างที่นี่เพื่อบรรทัดที่ server ที่ทำให้พังละเมิด

  3. MCP Inspector, modelcontextprotocol.io/docs/2026-07-28/tools/inspector, อ่าน 7 กันยายน 2026 หนึ่ง package, สาม clients หลัง binary เดียว — web, --cli และ --tui — ใช้ core เดียวกัน, transports ชุดเดียวกัน และ OAuth state ชุดเดียวกันบนดิสก์ CLI สร้าง catalogue traces ที่นี่

  4. Authorization, modelcontextprotocol.io/specification/2026-07-28/basic/authorization, อ่าน 7 กันยายน 2026 แหล่งที่มาของบทบาท resource-server; clauses การจัดการ token สี่ข้อที่ quote เต็ม; requirement ให้ servers implement RFC 9728 และ clients ใช้มันสำหรับ discovery; กฎ parameter resource และนิยาม canonical-URI; ตาราง issuer-validation; การ deprecation ของ Dynamic Client Registration; ตาราง 401/403/400 และ challenge insufficient_scope; และข้อยกเว้น stdio, “Implementations using an STDIO transport SHOULD NOT follow this specification, and instead retrieve credentials from the environment.” 2 3 4

  5. Streamable HTTP, .../basic/transports/streamable-http, และ Transports overview, .../basic/transports แหล่งที่มาของกฎ single-endpoint POST, requirement dual Accept, header MCP-Protocol-Version และกฎ must-match-the-body ของมัน, headers Mcp-Method และ Mcp-Name ที่อธิบายว่า “REQUIRED for compliance”, การถอด GET stream, sessions และ Last-Event-ID, guidance 405, validation Origin ที่ mandatory และการจัดประเภท transport HTTP+SSE ปี 2024-11-05 เป็น Deprecated ภายใต้ SEP-2596 2 3 4

  6. สี่ฉบับที่ specification พึ่งพา พร้อม draft ที่มัน profile: The OAuth 2.1 Authorization Framework, draft-ietf-oauth-v2-1-13 Campbell, B., Bradley, J. and Tschofenig, H., Resource Indicators for OAuth 2.0, RFC 8707, February 2020 — parameter resource และ audience ที่มัน bind Jones, M.B., Hunt, P. and Parecki, A., OAuth 2.0 Protected Resource Metadata, RFC 9728, April 2025 — document ที่ 401 ชี้ไป Meyer zu Selhausen, K. and Fett, D., OAuth 2.0 Authorization Server Issuer Identification, RFC 9207, March 2022 — parameter iss และการเทียบ exact-string Richer, J. (ed.) et al., OAuth 2.0 Dynamic Client Registration Protocol, RFC 7591, July 2015, deprecated สำหรับ use นี้ และ Jones, M. and Hardt, D., The OAuth 2.0 Authorization Framework: Bearer Token Usage, RFC 6750, October 2012, section 3, สำหรับรูปทรง challenge WWW-Authenticate ด้านบน

  7. Official MCP registry, registry.modelcontextprotocol.io/v0/servers, crawl 7 กันยายน 2026 ด้วย version=latest: 282 pages, 28,170 servers, tally โดย registryType บน distinct server names ตัวเลข download: api.npmjs.org/downloads/point/last-month สำหรับ @modelcontextprotocol/sdk (194,679,333 สำหรับ 8 สิงหาคม – 6 กันยายน 2026) และ pypistats.org/api/packages/<name>/recent สำหรับ mcp และ fastmcp ทั้งหมดอ่านวันเดียวกัน Package sizes มาจาก npm registry document และ PyPI JSON API 2

  8. MCP Course, Hugging Face, huggingface.co/learn/mcp-course, unit 0, อ่าน 7 กันยายน 2026: ใน prerequisites มี “Experience with at least one programming language (Python or TypeScript examples will be shown)”


สร้างโดย

David Vicente Campos

ผู้ก่อตั้ง NeuraLIA Labs และผู้ร่วมก่อตั้ง MyRealFood

ผมเป็นวิศวกรคอมพิวเตอร์ที่จบจากมหาวิทยาลัยเลออน ผมร่วมก่อตั้ง MyRealFood ที่ที่ผมในฐานะ CTO ได้สร้างแอปซึ่งผู้คนหลายล้านคนใช้เพื่อกินให้ดีขึ้น และผมก่อตั้ง NeuraLIA Labs ที่ที่ผมสร้างผลิตภัณฑ์ AI ที่นี่ผมเขียนถึงสิ่งที่ผมต้องทำความเข้าใจระหว่างทาง ในแบบที่ผมเคยหวังว่าจะมีใครสักคนอธิบายให้ผมฟัง

เพิ่มเติมเกี่ยวกับผู้เขียน

เผยแพร่โดย NeuraLIA Labs

รับโพสต์ใหม่ในกล่องจดหมาย

ข่าว AI คู่มือ และอัปเดตผลิตภัณฑ์ — อีเมลสั้น ๆ เมื่อเรามีสิ่งที่คุ้มเวลาของคุณ

ชอบแบบข้อความมากกว่าไหม รับเนื้อหาเดียวกันได้ที่นี่:คอมมูนิตี้ WhatsApp (เปิดในแท็บใหม่)ช่อง Telegram (เปิดในแท็บใหม่)

ดัชนีคอร์ส

Abstract software decision engine with branching paths, probability nodes, and glowing gates.
jevอ่าน 5 นาที

โมเดล AI Jev สร้างมาเพื่อการตัดสินใจ ไม่ใช่การเขียนความเรียง

Jev ของ TypeSafe AI กำลังได้รับความสนใจ เพราะมองความฉลาดของซอฟต์แวร์เป็นปัญหาความน่าจะเป็น: เลือกกิ่งที่ถูกต้อง แนบความมั่นใจ และหลีกเลี่ยงการจ่ายเงินให้ LLM เขียนข้อความเมื่อโค้ดต้องการการตัดสินใจ

Abstract legal research workspace with documents, search nodes and governance controls.
openaiอ่าน 4 นาที

Astra for Law ของ OpenAI คือระบบ AI ด้านกฎหมาย ไม่ใช่โมเดลใหม่

การเปิดตัวด้านกฎหมายของ OpenAI ไม่ได้เน้นโมเดลฐานรากใหม่เท่ากับระบบที่ล้อมรอบโมเดลนั้น: การค้นคืนเฉพาะโดเมน เครื่องมือที่เชื่อถือได้ สิทธิ์ เบนช์มาร์ก และเส้นทางการตรวจทาน

Abstract agent runtime sorting documents, memory blocks and pointer nodes inside a bounded context frame.
context-engineeringอ่าน 4 นาที

วิศวกรรมบริบทสำหรับเอเจนต์ AI ที่ทำงานระยะยาว

เอเจนต์ที่ทำงานต่อเนื่องไม่ได้ล้มเหลวเพียงเพราะหน้าต่างบริบทเล็กเกินไป แต่ล้มเหลวเมื่อไฟล์ ผลลัพธ์จากเครื่องมือ และประวัติที่ค้างเก่าบดบังงานที่เอเจนต์ควรทำให้เสร็จ

พร้อมให้ LIA เลือกโมเดลให้แล้วหรือยัง?

สร้างงานด้วยโมเดล AI ทุกตัวในที่เดียว เริ่มฟรีวันนี้