Chuyển đến nội dung
27/30Chương 27 trên 30

Ship một MCP Server: TypeScript và Python, đo đạc rõ ràng

Cùng một server viết hai lần — 3 tools, 1 resource, 1 prompt — rồi cân đo: 94 packages so với 28, cold start 145 ms so với 709.

Trên trang này

Đây là toàn bộ cuộc tranh luận về ngôn ngữ, đã được đo, trước khi nói thêm một lời nào.

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

Hai dòng đầu là phép so sánh ai cũng muốn. Dòng thứ ba là cùng TypeScript server ở dòng đầu, được khởi chạy theo đúng cách nó thực sự sẽ được phân phối — và nó chỉ cách Python ba mili giây.

Chương 26 đã đọc Model Context Protocol theo chính đặc tả của nó bằng JSON-RPC thô, vì JSON-RPC thô không có ngôn ngữ. Chương này có hai ngôn ngữ, và sức nặng của lập luận nằm ở đây: cùng một server, được viết hai lần. Ba tools, một resource, một prompt, cả hai SDK, không bên nào đi đường tắt. Rồi đến transports, inspector, 401, và những con số chưa ai công bố.

Một incident log. Ba tools, vì phần tách reads và writes của Chương 18 phải hiện rõ: search_incidents đọc, open_incident ghi và trả lại một handle, resolve_incident nhận handle đó rồi đóng. Một resource, incidents://open, vì đọc danh sách hiện tại là việc application gắn vào. Một prompt, postmortem, vì “viết phần này lại” là slash command của một người. Đó là hệ phân cấp điều khiển của Chương 26 — model, application, person — được biến thành năm registrations.

Handle quan trọng hơn vẻ ngoài của nó. Chương 26 đã làm hỏng một lịch đồ chơi bằng cách giữ trạng thái trong một mảng cấp module: protocol không có session, nên một creation tool trả về một định danh mờ và mọi lần gọi sau nhận nó như một đối số bình thường. Không có gì trong cả hai file giả định caller là process đã mở nó.

Đây là cùng một tool trong cả hai ngôn ngữ, đăng ký đặt cạnh nhau:

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.")

Hãy đọc phần giống nhau trước, vì đó là phát hiện. Cả hai đều khai báo một tên, một mô tả, hai đối số string có mô tả và ba annotations; cả hai là một function; không bên nào nhắc đến JSON-RPC, framing, stdout hay một protocol version. Hai SDK đã hội tụ về cùng một hình dạng, đúng như “Tier 1” đáng ra phải có nghĩa.1

Hai khác biệt là thật và cả hai sẽ quay lại sau. TypeScript mô tả đối số bằng một schema library — ở đây là Zod — và schema là một value bạn viết. Python mô tả chúng bằng chính type hints của function và đọc chúng lúc import, đó là lý do nó biết những điều về function mà file TypeScript chưa từng nói với nó. Và nhánh lỗi: TypeScript trả về một tool result với isError, Python thì raise. Hãy giữ ý đó.

Bốn registrations còn lại không khác gì về cấu trúc. Resource là server.registerResource("open-incidents", "incidents://open", …) so với @server.resource("incidents://open", …); prompt là registerPrompt so với @server.prompt. Dòng cuối của mỗi file là transport: await server.connect(new StdioServerTransport()) so với server.run().

Toàn bộ file: 81 dòng không trống và 3.060 byte TypeScript so với 63 dòng và 2.555 byte. Hãy xem con số đó với lượng muối nó xứng đáng nhận — số dòng đo formatter nhiều chẳng kém gì đo ngôn ngữ, đó là lý do không con số nào nằm trong bảng tiêu đề bên dưới.

Bằng chứng rằng ngôn ngữ là vô hình là một client chạy hai lần, trong mười một dòng:

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));

Trỏ nó lần lượt vào từng server. Output thật, đã rút gọn:

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}"}]

Cùng tools, cùng thứ tự, cùng handle. Một TypeScript client không thể biết server được viết bằng gì, và nó không bao giờ hỏi. Đó là toàn bộ lời hứa của một protocol, vẫn đứng vững.

Giờ hãy nhìn whitespace trong kết quả thứ hai, vì nó không phải chuyện thẩm mỹ: Python SDK serialises payloads với pydantic_core.to_json(result, fallback=str, indent=2). Khi đọc resource với hai incidents trong danh sách, body TypeScript dài 136 ký tự và 37 o200k_base tokens; body Python dài 185 ký tự và 62 tokens. Nhiều hơn sáu mươi tám phần trăm tokens cho các hàng giống hệt nhau, được trả bởi bất kỳ ai đọc resource vào một prompt, mỗi lần.

Catalogue cũng kể cùng một câu chuyện với nguyên nhân lớn hơn. Cả hai servers, cùng ba tools, tools/list được cân theo từng key:

keyTypeScriptPython
name2121
description4646
annotations4646
inputSchema211192
outputSchema187
execution27
total342480

Các input schemas của Python rẻ hơn — cầu nối Zod của TypeScript đóng dấu một $schema và một additionalProperties lên mỗi schema. Toàn bộ khoảng cách 138-token là một output schema không ai viết. resolve_incident được annotate là -> Incident, nên SDK suy ra JSON Schema cho return type và gửi nó đi. Nó thực sự hữu ích — đó là thứ cho phép client validate structuredContent — và đó là 187 tokens trong context window của bạn xuất hiện chỉ vì một type hint. Quy tắc của Chương 24 về definitions chen chỗ phần material quan trọng cũng áp dụng cho những schemas bạn không biết mình có.

Hai nhánh lỗi ở trên không phải lựa chọn phong cách. Hãy đưa cho mỗi server một tool thất bại theo cách một integration thật sự thất bại, rồi đọc những gì tới được 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 đã đưa một địa chỉ nội bộ, một port, một database name và một service account vào context của model. Python SDK không đưa thứ nào trong đó vào; traceback đi tới stderr và ở lại server.

Không bên nào là bug. Cả hai là quyết định, và quyết định của Python được viết ngay trong docstring của nó: một ToolError là “một failure bạn đã dự liệu” và message của nó được trả về “trong content để model đọc”; bất cứ thứ gì khác “được xem như crash: model chỉ thấy Error executing tool <name>, và server log traceback ở ERROR”. Class cho trường hợp crash nói nốt phần còn lại — “không gì từ bản gốc tới được client”.

Cả hai hành vi đều sai trong một nửa số trường hợp. Chương 18 lập luận rằng validation error nên quay lại như một tool result để model có thể đọc và sửa, vì đó là dòng có đòn bẩy cao nhất trong hầu hết integrations; ở phía Python việc đó đòi hỏi raise ToolError rõ ràng, còn một ValueError trần trụi sẽ ném mất câu hữu ích. Lập luận của Chương 30 đi theo hướng ngược lại: mọi thứ tool trả về đều rơi vào một context mà prompt injection sau đó có thể cố đọc ngược ra, và exception string chưa được review là văn bản ít được kiểm toán nhất trong hệ thống của bạn.

Quy tắc sống sót qua cả hai: hãy quyết định, theo từng tool, một failure được phép nói gì, và tự viết string đó. Đừng bao giờ để default text của exception quyết định, ở bất kỳ ngôn ngữ nào.

Cố ý làm hỏng: một dòng trên standard output

Liên kết đến mục: Cố ý làm hỏng: một dòng trên standard output

Tutorial chính thức nêu quy tắc không vòng vo: “Với STDIO-based servers: Đừng bao giờ viết ra stdout. Viết ra stdout sẽ làm hỏng JSON-RPC messages và phá server của bạn. Function print() mặc định viết ra stdout, nên hãy loại nó hoàn toàn khỏi STDIO server.”1 Chương 26 đã trích phiên bản chuẩn tắc — một server “MUST NOT write anything to its stdout that is not a valid MCP message”.2

Thêm một dòng vào mỗi server và đọc 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

Bản Python tệ hơn, và lý do không phải MCP. Một process có stdout là pipe thay vì terminal sẽ nhận một stream được block-buffer, nên dòng lạc đó được flush bất cứ khi nào buffer quyết định — ở đây, lúc exit, sau một response mà nó đã được viết trước đó. Corruption không xuất hiện ở nơi bug nằm. Thêm flush=True, hoặc một library có flush, và nó sẽ di chuyển.

Rồi đến phần giải thích vì sao chuyện này vẫn ship. Đưa server hỏng cho ba 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 bảy dòng chết ngay lập tức. Client chính thức và Inspector đều nhún vai — chúng bỏ qua dòng đó và tiếp tục. Một quy tắc chỉ làm hỏng các clients không ai dùng là một quy tắc đi vào production nguyên vẹn, đó là lý do đáng để cố ý phá nó ở đây thay vì trong log của khách hàng.

CLI mode của Inspector là nửa thường bị quên: npx @modelcontextprotocol/inspector --cli <command> --method tools/list in ra catalogue rồi thoát, khiến nó scriptable theo cách browser UI không làm được.3

Cả hai SDK đều cài sạch, vào thư mục riêng của chúng, không có gì dùng chung:

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

Mỗi hàng gây bất ngờ theo một hướng khác, đó là lý do phép so sánh này đáng chạy thay vì đoán.

TypeScript cài nhiều hơn ba lần số packages và ít hơn một phần ba số bytes. 94 dependencies là hệ sinh thái npm đúng với bản chất của nó — fast-deep-equal, es-errors, dunder-proto. 28 của Python thì ít hơn và khổng lồ: cryptography, pydantic-coreuvicorn là compiled artefacts. Nếu bản năng của bạn là số lượng dependency mới là điều cần lo, hàng này là phản ví dụ.

Interpreter của Python khởi động nhanh hơn Node, và khoảng cách không hề sát — 11.1 ms so với 19.4 ms trên chương trình rỗng. Vậy 565 ms trong hàng cold-start không phải do ngôn ngữ. Nó là SDK, và hàng loaded-packages cho biết vì sao:

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, …

Một server mà I/O duy nhất là pipe lại import một ASGI web server, một HTTP client và một TLS library trước khi đọc dòng đầu tiên. TypeScript SDK cũng ship Express, Hono, joseeventsource — chúng nằm trên disk chưa được đọc, vì package boundary giữ chúng khỏi một import server/stdio.js. Package của Python là một import graph, nên import mcp là tất cả: python -X importtime quy 727 ms cho import mcp.server.mcpserver — con số đo dưới import profiler, đó là lý do nó cao hơn 709 ms mà lần chạy không profile mất từ spawn đến answer — và 269 ms trong số đó cho riêng subtree mcp.types — wire types là Pydantic models, mỗi class cho mỗi protocol message ở mỗi revision, và xây chúng là công việc làm lúc import. Đó là trade-off thiết kế, không phải cẩu thả — eager imports là lý do Python SDK có thể đưa cho bạn run(transport="streamable-http") ngay dòng kế tiếp mà không cần cài thêm.

Rồi hàng cuối của khối mở đầu đảo ngược lập luận. Package TypeScript server đúng cách — một entry bin, một shebang, npm link, không có gì để tải — và launch nó qua npx với --no-install, đúng cách một stdio server đã publish thực sự được start:

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 tốn 568 ms mỗi lần start — gấp bốn lần rưỡi toàn bộ import của TypeScript SDK — và chi phí đó trả ở mỗi lần launch, vì một MCP host start một stdio server bằng cách chạy command đó. Vậy dạng trung thực của câu “TypeScript start nhanh hơn năm lần” là: đúng vậy, cho đến khi bạn phân phối nó theo cách bình thường. Caveat tương tự có lẽ áp dụng cho uvx; máy này không cài uv, nên hàng đó không tồn tại. Thứ không đo thì không đưa vào bảng.

Chương 26 đã nói về framing của stdio. Hai điều còn để lại cho đây.

Thứ nhất: chạy một server bằng npx hoặc uvx chính là stdio transport. Không có “package mode” riêng. Cấu hình của host nêu command và arguments; host spawn nó và nói chuyện qua pipes. Đó là lý do “tôi phân phối thứ này thế nào” và “nó nói transport nào” là cùng một câu hỏi ở local, và lý do chi phí launcher thuộc về một chương về shipping.

Thứ hai: stdio hoàn toàn không có phần authorization, và đặc tả nói vậy trong một dòng — implementations dùng stdio “SHOULD NOT follow this specification, and instead retrieve credentials from the environment”.4 Security model của nó là security model của hệ điều hành, và giới hạn của nó cũng vậy: một subprocess local phục vụ đúng một máy và một user.

Transport còn sống kia là Streamable HTTP: một endpoint duy nhất nhận POST, một HTTP request cho mỗi JSON-RPC message, và một header Accept phải liệt kê cả application/json lẫn text/event-stream vì server chọn theo từng request xem nó trả lời bằng cái nào.5 Chương 14 đã parse event stream đó bằng tay, nên không có gì mới trong wire format — chỉ mới ở thứ bao quanh nó. Ba nghĩa vụ của revision hiện tại dễ bị bỏ lỡ và cả ba đều testable:

Mỗi POST mang MCP-Protocol-Version, và value của nó phải khớp với protocolVersion bên trong chính _meta của request. Mismatch là 400 với header-mismatch error, không phải một cái nhún vai.5

Mcp-Method phản chiếu method trên mọi request; Mcp-Name phản chiếu params.name hoặc params.uri trên tools/call, resources/readprompts/get. Chúng tồn tại để proxy có thể route mà không cần parse bodies.5

Các shape cũ đã biến mất, và trả lời bằng từ chối

Liên kết đến mục: Các shape cũ đã biến mất, và trả lời bằng từ chối

GET stream, Mcp-Session-Id và resumption Last-Event-ID đều đã bị gỡ. Một server chỉ nói revision này nên trả lời 405 Method Not Allowed cho GET hoặc DELETE, ignore session header mà không mint header mới, và ignore Last-Event-ID.5

Giờ là phép đo làm đổi khung toàn bộ chương. Gửi một request revision hiện tại tới mỗi server qua 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)"}}

Các constants khớp với hành vi: LATEST_PROTOCOL_VERSION của Python SDK đọc 2026-07-28, còn của TypeScript SDK đọc 2025-11-25. Gửi request header-mismatch từ bước trên và Python server trả lời 400 với error -32020 và message “mcp-protocol-version header does not match the request envelope's protocol version”; TypeScript SDK không có code như vậy, vì nó không implement revision định nghĩa code đó.

Trang liệt kê cả hai ở Tier 1 cũng nói “Each SDK provides the same functionality”.1 Vào ngày bên dưới, với revision hiện tại, câu đó là một khát vọng. Hãy check LATEST_PROTOCOL_VERSION trong SDK bạn sắp cài; đó là một dòng, và là claim duy nhất trong chương này vẫn còn quan trọng sau một năm nữa.

Đưa một server ra khỏi laptop của bạn và client của một người lạ xuất hiện với một token. Đây là nửa Chương 26 để nguyên và là nửa một product nhiều user không thể bỏ qua.

Đặc tả đặt MCP server vào một vai trò OAuth 2.1 và gọi tên nó: một protected MCP server là một resource server, client là OAuth client, và authorization server là vấn đề của người khác.4 Từ vai trò đó, bốn mệnh đề bắt buộc, trích nguyên khối vì diễn giải lại là cách sai lầm xảy ra:

MCP servers, khi đóng vai trò là OAuth 2.1 resource server, MUST validate access tokens như mô tả trong OAuth 2.1 Section 5.2. MCP servers MUST validate rằng access tokens được issue specifically for them as the intended audience, theo 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” là quy tắc chống passthrough, và đó là lý do toàn bộ cơ chế audience tồn tại. Một server replay bearer token nó nhận được tới API bên thứ ba là một confused deputy: nó cho bất kỳ ai gọi nó mượn trust của chính nó. Quy tắc cấm việc reuse, không chỉ cấm storage.

Để điều đó enforceable cần bốn RFC, mỗi RFC một việc.6 RFC 9728 là cách client tìm authorization server ngay từ đầu: MCP server phục vụ một protected-resource-metadata document và một 401 trỏ tới đó. RFC 8707 là parameter resource — client phải gửi canonical URI của server trong cả authorization request lẫn token request, “regardless of whether authorization servers support it”, để token được issue nêu audience của nó. RFC 9207 khép vòng từ phía kia: client ghi lại issuer trước khi redirect và so sánh iss được trả về bằng exact string, không normalisation — không case folding, không bỏ default-port, không trailing slash. Và RFC 7591, Dynamic Client Registration, nay đã deprecated để nhường cho Client ID Metadata Documents, “retained for backwards compatibility with authorization servers that do not support” chúng.4

Nối dây việc đó trên cả hai servers bằng một token verifier không làm gì ngoài check audience. Thang 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"}

Cả hai SDK đều phục vụ document đó và cả hai trỏ một 401 vào nó, đó là toàn bộ câu chuyện discovery: một client chưa từng thấy server của bạn học được nơi authenticate từ một lời từ chối. 403 là con vật khác — token ổn, scope thì không — và challenge nêu thứ còn thiếu để client có thể step up thay vì bắt đầu lại.

Hai bậc khác nhau, và không khác biệt nào nằm trong đặc tả. TypeScript SDK từ chối token không có expiry claim; Python trả về 200, vì expires_at là optional trên AccessToken của nó và None nghĩa là “không có ý kiến”. Và 403 của Python mang error_description="Required scope: incidents:read" mà không có parameter scope mà đặc tả nói servers nên include. Verifier không phải nơi để chấp nhận default của library: audience check là việc của bạn viết ở cả hai ngôn ngữ, expiry cũng vậy.

Một nit trung thực từ cùng lần chạy. GET trên endpoint trả lời 404 trên wiring Express và 400 Bad Request: Missing session ID trên bản Python, nơi đặc tả yêu cầu 405 Method Not Allowed và nơi “session ID” là từ vựng revision này đã gỡ. Không cái nào nguy hiểm; cả hai là hình dạng của một hệ sinh thái đang giữa migration.

Mảnh cuối của shipping là nơi bạn publish, và nó có câu trả lời bằng một con số. Crawl hôm nay, mọi server trong registry chính thức ở latest version của nó: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

Hai cách đọc, chỉ về hai hướng ngược nhau. Tính theo số servers đã publish, npm dẫn 2,3 so với 1 — con số mọi người trích khi nói ecosystem là TypeScript. Tính theo downloads, Python dẫn: trong ba mươi ngày gần nhất mcp đạt 286,7 triệu so với @modelcontextprotocol/sdk ở 194,7 triệu, trước khi cộng thêm fastmcp ở 72,1 triệu.7 Cả hai đều là Tier 1, normative schema là một schema.ts, và tutorial chính thức “Build an MCP server” mở ở tab Python.1 Dù bạn giữ nửa nào trong đầu, nửa kia cũng đúng.

Và hàng quan trọng hơn cả hai: hơn nửa registry — 14.696 trong 28.170 — không có gì để cài. Đó là web services. Các thống kê transport xác nhận từ phía kia: trong 14.290 package entries, 13.787 declare stdio; trong 16.640 remote entries, 15.570 declare Streamable HTTP và 1.070 vẫn declare HTTP+SSE đã deprecated. Vậy “một MCP server là subprocess trên laptop của bạn” mô tả một thiểu số đang thu hẹp, và từng server trong 14.696 server kia cần phần ở trên thay vì một environment variable.

Hiện chi tiết

Cố ý song ngữ, và tiền lệ cho việc đó.

Đây là chương song ngữ duy nhất trong khóa học, vì câu trả lời trung thực bị tách đôi: registry là npm-first và downloads là Python-first, cùng lúc, hôm nay. Viết một trong hai sẽ bỏ mất nửa câu hỏi và mô tả sai ecosystem trong lúc làm vậy. Có tiền lệ công khai — Hugging Face MCP Course liệt kê trong prerequisites của nó “Experience with at least one programming language (Python or TypeScript examples will be shown)”, và dạy cả hai.8 Một protocol mà toàn bộ giá trị nằm ở số lượng implementations là nơi tệ để đơn ngữ.

Phần có ngày: mọi thứ ở trên có hạn dùng

Liên kết đến mục: Phần có ngày: mọi thứ ở trên có hạn dùng

Đọc và đo vào 7 tháng 9 năm 2026, với 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

Một ghi chú migration không phải con số. Trong mcp 2.x, FastMCP được đổi tên thành MCPServer, và gần như mọi tutorial online vẫn mở đầu bằng import cũ. SDK ship một module chỉ có một mục đích là giải thích điều đó, đây là deprecation chu đáo nhất trong chương này:

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.

Với bảng trước mặt, khuyến nghị trở nên nhàm chán, và đó là dấu hiệu tốt.

Nếu server sống bên trong một web application bạn đã chạy, hãy viết nó bằng TypeScript. Cùng process, cùng deploy, cùng request handler; Streamable HTTP là một endpoint bạn thêm cạnh các endpoint khác; và 13,9 MiB cùng 145 ms là miễn phí vì runtime đã chạy sẵn. Đó là phần lớn trong 14.696 remote servers.

Nếu server bọc data tooling, hãy viết nó bằng Python. Thứ bạn expose là pandas, một warehouse client, lượng transforms cỡ một notebook, và một server bằng ngôn ngữ khác sẽ là một subprocess call đội lốt schema. Bảy trăm mili giây import trong một service start một lần không phải chi phí; trong một subprocess mà host relaunch cả ngày thì có.

Và hiện tại, hàng revision vượt lên trên cả hai. Nếu bạn cần 2026-07-28 — multi-round-trip requests, resultType, cache hints, server/discover — một trong hai SDK có nó hôm nay và cái kia thì chưa.

Giờ bạn có thể ship cùng một server bằng cả hai ngôn ngữ, bảo vệ lựa chọn bằng một bảng thay vì sở thích, chạy nó qua cả hai live transports, và đưa cho nó một token mà nó sẽ từ chối.

Thứ bạn xây vẫn là một function: một schema, một endpoint, một thứ deterministic mà model invoke. Cả một lớp knowledge không khớp với shape đó — cách chúng tôi viết postmortem, những fields incident reports của chúng tôi cần, thứ tự chúng tôi làm việc và lý do. Nó là procedure, nó là prose, và ép nó vào tool description là cách system prompts phình lên hai nghìn tokens phải trả ở từng turn dù cuộc trò chuyện có nói về incidents hay không.

Chương 28 là câu trả lời còn lại: một folder có SKILL.md bên trong để model đọc thay vì gọi, được load ở ba cấp để reference material gần như không tốn gì cho đến turn cần đến nó. Nó không có ngôn ngữ chính, và đó là điều đầu tiên nó dạy.


Mọi thứ ở đây được đo vào 7 tháng 9 năm 2026, trên Node 22.22.3 và Python 3.14.4, với @modelcontextprotocol/sdk 1.30.0 cùng zod 3.25.76 và mcp 2.1.1, mỗi package được cài vào thư mục tạm riêng. Timings là median của 25 lần launch, wall clock từ spawn đến dòng mang response tools/list; token counts là o200k_base qua tiktoken trên JSON của từng definition. Không gọi paid API nào: không có gì ở đây cần model.

Hai servers có 81 và 63 dòng không trống; một trong ba tools của chúng được tái hiện ở trên bằng cả hai ngôn ngữ, và bốn registrations còn lại chỉ khác như đã mô tả. Chính sách error-disclosure của Python SDK được trích từ docstrings của ToolErrorUnexpectedToolError trong mcp/server/mcpserver/exceptions.py; default pretty-printing là pydantic_core.to_json(result, fallback=str, indent=2) trong mcp/server/mcpserver/resources/types.pyutilities/func_metadata.py. Các protocol-version constants là LATEST_PROTOCOL_VERSION trong mcp_types/version.py và trong types.js của TypeScript SDK, đều đọc từ packages đã cài thay vì từ changelog.

  1. SDKs, modelcontextprotocol.io/docs/sdk, và Build an MCP server, modelcontextprotocol.io/docs/develop/build-server, đều đọc ngày 7 tháng 9 năm 2026. Nguồn của tier table, câu “Each SDK provides the same functionality but follows the idioms and best practices of its language”, thứ tự language-tab của tutorial (Python, TypeScript, Java, Kotlin, C#, Ruby, Rust, Go), và quy tắc logging được trích về print()stdout. 2 3 4

  2. stdio transport, .../basic/transports/stdio. Nguồn của newline framing và quy tắc purity stdout. Chương 26 đọc toàn bộ trang này; nó được cite ở đây cho dòng mà server bị phá vi phạm.

  3. MCP Inspector, modelcontextprotocol.io/docs/2026-07-28/tools/inspector, đọc ngày 7 tháng 9 năm 2026. Một package, ba clients sau một binary — web, --cli--tui — dùng chung một core, một bộ transports và một OAuth state trên disk. CLI tạo các catalogue traces ở đây.

  4. Authorization, modelcontextprotocol.io/specification/2026-07-28/basic/authorization, đọc ngày 7 tháng 9 năm 2026. Nguồn của vai trò resource-server; bốn mệnh đề xử lý token được trích đầy đủ; yêu cầu servers implement RFC 9728 và clients dùng nó cho discovery; các quy tắc parameter resource và định nghĩa canonical-URI; bảng issuer-validation; việc deprecate Dynamic Client Registration; bảng 401/403/400 và challenge insufficient_scope; cùng ngoại lệ 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, và Transports overview, .../basic/transports. Nguồn của quy tắc single-endpoint POST, yêu cầu dual Accept, header MCP-Protocol-Version và quy tắc phải khớp body của nó, các header Mcp-MethodMcp-Name được mô tả là “REQUIRED for compliance”, việc gỡ GET stream, sessions và Last-Event-ID, guidance 405, validation Origin bắt buộc, và phân loại transport HTTP+SSE 2024-11-05 là Deprecated theo SEP-2596. 2 3 4

  6. Bốn RFC mà đặc tả dựa vào, cùng draft mà nó profile: The OAuth 2.1 Authorization Framework, draft-ietf-oauth-v2-1-13. Campbell, B., Bradley, J. và Tschofenig, H., Resource Indicators for OAuth 2.0, RFC 8707, tháng 2 năm 2020 — parameter resource và audience mà nó bind. Jones, M.B., Hunt, P. và Parecki, A., OAuth 2.0 Protected Resource Metadata, RFC 9728, tháng 4 năm 2025 — document mà một 401 trỏ tới. Meyer zu Selhausen, K. và Fett, D., OAuth 2.0 Authorization Server Issuer Identification, RFC 9207, tháng 3 năm 2022 — parameter iss và so sánh exact-string. Richer, J. (ed.) et al., OAuth 2.0 Dynamic Client Registration Protocol, RFC 7591, tháng 7 năm 2015, deprecated cho use này. Và Jones, M. và Hardt, D., The OAuth 2.0 Authorization Framework: Bearer Token Usage, RFC 6750, tháng 10 năm 2012, section 3, cho shape challenge WWW-Authenticate ở trên.

  7. Official MCP registry, registry.modelcontextprotocol.io/v0/servers, crawl ngày 7 tháng 9 năm 2026 với version=latest: 282 pages, 28.170 servers, tally bằng registryType trên distinct server names. Số liệu download: api.npmjs.org/downloads/point/last-month cho @modelcontextprotocol/sdk (194.679.333 cho 8 tháng 8 – 6 tháng 9 năm 2026) và pypistats.org/api/packages/<name>/recent cho mcpfastmcp, đều đọc cùng ngày. Package sizes lấy từ npm registry document và PyPI JSON API. 2

  8. MCP Course, Hugging Face, huggingface.co/learn/mcp-course, unit 0, đọc ngày 7 tháng 9 năm 2026: trong prerequisites có “Experience with at least one programming language (Python or TypeScript examples will be shown)”.


Tạo bởi

David Vicente Campos

Nhà sáng lập NeuraLIA Labs & Đồng sáng lập MyRealFood

Tôi là kỹ sư máy tính tốt nghiệp Đại học León. Tôi đồng sáng lập MyRealFood, nơi tôi, với vai trò CTO, đã xây dựng ứng dụng mà hàng triệu người đã dùng để ăn uống lành mạnh hơn, và tôi sáng lập NeuraLIA Labs, nơi tôi xây dựng các sản phẩm AI. Ở đây, tôi viết về những gì tôi đã phải hiểu trong quá trình đó, theo cách mà tôi ước đã có ai đó giải thích cho mình.

Tìm hiểu thêm về tác giả

Xuất bản bởi NeuraLIA Labs.

Nhận bài viết mới trong hộp thư

Tin AI, hướng dẫn và cập nhật sản phẩm — một email ngắn khi chúng tôi có nội dung đáng để bạn đọc.

Thích nhắn tin hơn? Vẫn nội dung đó, ở đây:Cộng đồng WhatsApp (mở trong tab mới)Kênh Telegram (mở trong tab mới)

Mục lục khóa học

Abstract agent runtime sorting documents, memory blocks and pointer nodes inside a bounded context frame.
context-engineeringĐọc 16 phút

Kỹ thuật ngữ cảnh cho tác nhân AI dài hạn

Tác nhân chạy lâu không thất bại chỉ vì cửa sổ nhỏ. Chúng thất bại khi tệp, đầu ra công cụ và lịch sử cũ lấn át nhiệm vụ mà tác nhân phải hoàn thành.

Sẵn sàng để LIA chọn giúp bạn chưa?

Xây dựng cùng mọi mô hình AI ở một nơi — bắt đầu miễn phí ngay hôm nay.