MCP Server 출시하기: TypeScript와 Python 실측 비교
같은 server를 두 번 구현해 비교합니다. 도구 3개, 리소스 1개, prompt 1개. 패키지 94개 vs 28개, cold start 145ms vs 709ms.
이 페이지에서
언어 논쟁 전체를, 말로 시작하기 전에 먼저 측정해 보면 이렇습니다.
node ./incidents.js 144.5 ms
python incidents.py 709.4 ms
npx incidents-mcp 712.6 ms처음 두 줄은 모두가 원하는 비교입니다. 세 번째 줄은 첫 줄의 같은 TypeScript server를 실제 배포 방식대로 실행한 것입니다. 그리고 Python과 3밀리초 차이로 도착합니다.
26장은 순수 JSON-RPC로 Model Context Protocol을 그 자체의 명세에 대고 읽었습니다. 순수 JSON-RPC에는 언어가 없기 때문입니다. 이 장에는 두 언어가 있고, 논점의 무게는 여기에 실립니다. 같은 server를 두 번 작성했습니다. 도구 3개, 리소스 1개, prompt 1개, 두 SDK, 어느 쪽에도 지름길은 없습니다. 그다음 transport, inspector, 401, 그리고 아무도 공개하지 않은 숫자들입니다.
Server, 그리고 그 안에 이 다섯 가지가 들어간 이유
섹션 링크: Server, 그리고 그 안에 이 다섯 가지가 들어간 이유사고 로그입니다. 도구는 3개입니다. 18장의 읽기와 쓰기 분리가 보여야 하기 때문입니다. search_incidents는 읽고, open_incident는 쓰고 handle을 돌려주며, resolve_incident는 그 handle을 받아 닫습니다. 리소스는 하나, incidents://open입니다. 현재 목록 읽기는 application이 붙이는 것이기 때문입니다. prompt는 하나, postmortem입니다. “이걸 정리해 줘”는 사람의 slash command이기 때문입니다. 이것이 26장의 제어 계층 — model, application, person — 을 다섯 개의 registration으로 바꾼 형태입니다.
Handle은 보기보다 중요합니다. 26장은 module-level 배열에 상태를 보관해 장난감 calendar를 망가뜨렸습니다. protocol에는 session이 없으므로 creation 도구는 opaque identifier를 반환하고, 이후 모든 call은 그것을 평범한 argument로 받습니다. 두 파일 어느 쪽도 caller가 그것을 연 process라고 가정하지 않습니다.
같은 도구를 두 언어로 나란히 등록하면 이렇습니다.
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 }) }] };
},
);@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.")먼저 같은 것을 읽어야 합니다. 그것이 발견이기 때문입니다. 둘 다 name, description, 설명이 붙은 두 string argument, annotation 세 개를 선언합니다. 둘 다 함수 하나입니다. 어느 쪽도 JSON-RPC, framing, stdout 또는 protocol version을 언급하지 않습니다. 두 SDK는 같은 형태로 수렴했고, 그것이 “Tier 1”이 뜻해야 하는 바입니다.1
실제 차이는 두 가지이고, 둘 다 나중에 다시 돌아옵니다. TypeScript는 schema library — 여기서는 Zod — 로 argument를 설명하고, 그 schema는 직접 작성하는 값입니다. Python은 함수 자체의 type hint로 argument를 설명하고 import 시점에 그것을 읽습니다. 그래서 TypeScript 파일이 알려주지 않은 함수에 관한 사실을 Python은 압니다. 그리고 error path입니다. TypeScript는 isError가 붙은 tool result를 반환하고, Python은 raise합니다. 이것을 기억해 두세요.
나머지 네 registration에는 구조적 차이가 없습니다. 리소스는 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바이트이고 Python은 63줄, 2,555바이트입니다. 적당히 걸러서 보세요. 줄 수는 언어만큼 formatter도 측정하기 때문에, 아래 headline 표에는 어느 숫자도 넣지 않았습니다.
하나의 client, 두 server
섹션 링크: 하나의 client, 두 server언어가 보이지 않는다는 증거는 같은 client를 두 번 실행하는 것입니다. 11줄이면 됩니다.
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를 차례로 가리키게 합니다. 실제 output을 줄이면 이렇습니다.
$ 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}"}]같은 도구, 같은 순서, 같은 handle입니다. TypeScript client는 server가 무엇으로 작성되었는지 알 수 없고, 묻지도 않습니다. protocol의 약속 전체가 지켜지고 있는 것입니다.
이제 두 번째 result의 whitespace를 보세요. 장식이 아닙니다. Python SDK는 pydantic_core.to_json(result, fallback=str, indent=2)로 payload를 serialise합니다. 목록에 incident 두 개가 있는 resource read에서 TypeScript body는 136자와 o200k_base token 37개입니다. Python body는 185자와 62개입니다. 동일한 row에 token이 68퍼센트 더 들어가며, resource를 prompt로 읽어들이는 사람이 매번 그 비용을 냅니다.
Catalogue도 원인은 더 크지만 같은 이야기를 합니다. 두 server, 같은 세 도구, tools/list를 key별로 재면 이렇습니다.
| key | TypeScript | Python |
|---|---|---|
name | 21 | 21 |
description | 46 | 46 |
annotations | 46 | 46 |
inputSchema | 211 | 192 |
outputSchema | — | 187 |
execution | 27 | — |
| total | 342 | 480 |
Python의 input schema는 더 저렴합니다. TypeScript의 Zod bridge는 각각에 $schema와 additionalProperties를 찍습니다. 138-token 격차 전체는 아무도 작성하지 않은 output schema입니다. resolve_incident에는 -> Incident annotation이 붙어 있어서 SDK가 return type의 JSON Schema를 추론해 함께 보냈습니다. 실제로 유용합니다. client가 structuredContent를 validate할 수 있게 해 주는 것이 바로 이것입니다. 그리고 type hint 하나 때문에 context window에 187 token이 들어옵니다. 24장의 규칙, 즉 정의가 중요한 자료를 밀어낸다는 말은 자신에게 그런 schema가 있는 줄 몰랐을 때도 적용됩니다.
일부러 망가뜨리기: 새어 나온 error message
섹션 링크: 일부러 망가뜨리기: 새어 나온 error message위의 두 error path는 style 선택이 아닙니다. 실제 integration이 실패하는 방식으로 실패하는 도구를 각 server에 주고, 무엇이 model에 도달하는지 읽어 보세요.
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는 내부 주소, port, database name, service account를 model의 context에 넣었습니다. Python SDK는 그중 아무것도 넣지 않았습니다. traceback은 stderr로 갔고 server에 남았습니다.
어느 쪽도 bug는 아닙니다. 둘 다 결정이고, Python 쪽 결정은 자체 docstring에 적혀 있습니다. ToolError는 “예상한 실패”이고 그 message는 “model이 읽을 수 있도록 content에” 반환됩니다. 그 밖의 모든 것은 “crash로 처리됩니다. model은 Error executing tool <name>만 보고, server는 traceback을 ERROR에 logging합니다.” crash case의 class는 나머지를 직접 말합니다. “원본에서 온 어떤 것도 client에 도달하지 않습니다.”
두 동작 모두 절반의 경우에는 틀립니다. 18장은 validation error가 model이 읽고 고칠 수 있는 tool result로 돌아와야 한다고 주장했습니다. 대부분의 integration에서 그것이 leverage가 가장 큰 한 줄이기 때문입니다. Python 쪽에서는 ToolError를 명시적으로 raise해야 하고, bare ValueError는 유용한 문장을 버립니다. 30장의 논점은 반대로 갑니다. tool이 반환하는 모든 것은 나중의 prompt injection이 다시 읽어내려 시도할 수 있는 context에 들어가고, 검토되지 않은 exception string은 시스템에서 가장 audit가 덜 된 text입니다.
둘을 모두 통과하고 남는 규칙은 이것입니다. 도구마다 failure가 무엇을 말할 수 있는지 결정하고, 그 string을 직접 작성하세요. 어느 언어에서도 exception의 default text가 결정하게 두지 마세요.
일부러 망가뜨리기: standard output의 한 줄
섹션 링크: 일부러 망가뜨리기: standard output의 한 줄공식 tutorial은 단서를 달지 않고 규칙을 말합니다. “STDIO-based server의 경우: stdout에 절대 쓰지 마세요. stdout에 쓰면 JSON-RPC message가 손상되어 server가 깨집니다. print() function은 기본적으로 stdout에 쓰므로 STDIO server에서는 완전히 제외하세요.”1 26장은 normative version을 인용했습니다. server는 “유효한 MCP message가 아닌 어떤 것도 자신의 stdout에 쓰면 안 됩니다.”2
각 server에 한 줄을 추가하고 raw stream을 읽어 보세요.
TypeScript incidents server starting
{"result":{"protocolVersion":"2025-11-25", … },"jsonrpc":"2.0","id":1}
Python {"jsonrpc":"2.0","id":1,"result":{ … }}
incidents server startingPython 쪽이 더 나쁩니다. 이유는 MCP가 아닙니다. stdout가 terminal이 아니라 pipe인 process는 block-buffered stream을 얻으므로, 엉뚱한 줄은 buffer가 정하는 때 flush됩니다. 여기서는 exit 시점, 그것보다 먼저 쓰인 response 뒤입니다. 손상은 bug가 있는 위치에 나타나지 않습니다. flush=True를 추가하거나 flush하는 library를 넣으면 위치가 이동합니다.
그리고 이게 왜 출시되는지 설명하는 부분입니다. 깨진 server를 client 세 개에 넣어 보세요.
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 warning7줄짜리 parser는 즉시 죽습니다. 공식 client와 Inspector는 둘 다 어깨를 으쓱합니다. 그 줄을 skip하고 계속 갑니다. 아무도 쓰지 않는 client에서만 깨지는 규칙은 production까지 멀쩡히 도달하는 규칙입니다. 그래서 customer log에서가 아니라 여기서 일부러 깨뜨릴 가치가 있습니다.
Inspector의 CLI mode는 잊히는 절반입니다. npx @modelcontextprotocol/inspector --cli <command> --method tools/list는 catalogue를 출력하고 종료하므로 browser UI와 달리 script로 다룰 수 있습니다.3
두 SDK는 각자 directory에, 공유 없이, 깨끗하게 설치했습니다.
| TypeScript | Python | |
|---|---|---|
| package | @modelcontextprotocol/sdk 1.30.0 + zod 3.25.76 | mcp 2.1.1 |
| 구현한 최신 protocol revision | 2025-11-25 | 2026-07-28 |
| 설치된 transitive package | 94 | 28 |
| 설치 size | 13.9 MiB | 44.3 MiB |
| disk의 file 수 | 3,386 | 2,018 |
| stdio 제공을 위해 load된 third-party package | 94개 중 8개 | 28개 중 18개 |
| bare interpreter start, median | 19.4 ms | 11.1 ms |
spawn → tools/list 응답, 25회 median | 144.5 ms | 709.4 ms |
tools/list catalogue, o200k_base token | 342 | 480 |
모든 row가 서로 다른 방향으로 놀라게 합니다. 그래서 이 비교는 추측이 아니라 실행할 가치가 있습니다.
TypeScript는 세 배가 넘는 package를 설치하지만 byte는 3분의 1도 안 됩니다. dependency 94개는 npm ecosystem이 늘 그렇듯 자신답게 행동한 결과입니다. fast-deep-equal, es-errors, dunder-proto입니다. Python의 28개는 더 적고 거대합니다. cryptography, pydantic-core, uvicorn는 compiled artefact입니다. 걱정해야 할 것이 dependency 개수라고 본능적으로 느낀다면, 이 row가 반례입니다.
Python interpreter는 Node보다 더 빨리 시작하고, 차이도 작지 않습니다 — 빈 program에서 11.1 ms 대 19.4 ms입니다. 그러니 cold-start row의 565 ms는 언어가 아닙니다. SDK입니다. loaded-packages row가 이유를 말합니다.
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, …I/O가 pipe뿐인 server가 첫 줄을 읽기도 전에 ASGI web server, HTTP client, TLS library를 import합니다. TypeScript SDK도 Express, Hono, jose, eventsource를 함께 ship합니다. 하지만 package boundary가 그것들을 server/stdio.js import 밖에 두기 때문에 disk에 읽히지 않은 채 남아 있습니다. Python package는 하나의 import graph이므로 import mcp가 곧 전체입니다. python -X importtime는 import mcp.server.mcpserver에 727 ms가 든다고 attribution합니다. import profiler 아래에서 측정한 수치라 profile하지 않은 run이 spawn부터 answer까지 걸리는 709 ms보다 크게 나옵니다. 그리고 그중 269 ms는 mcp.types subtree 하나에 들어갑니다. wire type은 Pydantic model이고, protocol message마다 revision마다 class가 하나씩 있으며, 그것을 만드는 일은 import 시점에 수행됩니다. 이것은 sloppy함이 아니라 design trade입니다. eager import 덕분에 Python SDK는 다음 줄에서 별도 install 없이 run(transport="streamable-http")를 건네줄 수 있습니다.
그리고 opening block의 마지막 row가 그 주장을 다시 뒤집습니다. TypeScript server를 제대로 package하세요. bin entry, shebang, npm link, download할 것은 없음. 그리고 npx를 통해 --no-install로 실행하세요. published stdio server가 실제로 시작되는 방식입니다.
node ./incidents.js 144.5 ms
npx incidents-mcp 712.6 ms (+568.1 ms of launcher)
python incidents.py 709.4 msLauncher는 start마다 568 ms를 씁니다. TypeScript SDK import 전체의 네 배 반입니다. 그리고 매 launch마다 냅니다. MCP host가 stdio server를 시작하는 방식은 그 command를 실행하는 것이기 때문입니다. 그래서 “TypeScript가 다섯 배 빨리 시작한다”의 정직한 형태는 이렇습니다. 그렇다. 정상적인 방식으로 배포하기 전까지는. 같은 caveat은 아마 uvx에도 적용될 것입니다. 이 machine에는 uv가 설치되어 있지 않았으므로 그 row는 없습니다. 측정하지 않은 것은 표에 넣지 않습니다.
두 transport, 그리고 오직 두 transport
섹션 링크: 두 transport, 그리고 오직 두 transport26장은 stdio의 framing을 다뤘습니다. 여기로 남겨 둔 것은 두 가지입니다.
첫째: npx 또는 uvx로 server를 실행하는 것 자체가 stdio transport입니다. 별도의 “package mode”는 없습니다. host configuration은 command와 argument의 이름을 댑니다. host는 그것을 spawn하고 pipe를 통해 대화합니다. 그래서 local에서는 “이걸 어떻게 배포하지”와 “어떤 transport를 말하지”가 하나의 질문이고, launcher cost가 shipping을 다루는 장에 속하는 이유입니다.
둘째: stdio에는 authorization section이 전혀 없고, specification도 한 줄로 그렇게 말합니다. stdio를 사용하는 implementation은 “이 specification을 따르지 않아야 하며, 대신 environment에서 credential을 가져와야 합니다.”4 security model은 operating system의 것이고, 한계도 그렇습니다. local subprocess는 정확히 한 machine과 한 user를 serve합니다.
다른 live transport는 Streamable HTTP입니다. POST를 받는 단일 endpoint, JSON-RPC message당 HTTP request 하나, 그리고 server가 request마다 두 방식 중 무엇으로 답할지 고르므로 Accept header에는 반드시 application/json와 text/event-stream가 모두 나열되어야 합니다.5 14장은 그 event stream을 손으로 parsing했으므로 wire format에 새로울 것은 없습니다. 새로워진 것은 그것을 감싸는 것뿐입니다. current revision의 세 의무는 놓치기 쉽고, 셋 다 test할 수 있습니다.
Version header는 body와 일치해야 합니다
섹션 링크: Version header는 body와 일치해야 합니다모든 POST는 MCP-Protocol-Version를 싣고, 그 값은 request 자체의 _meta 안에 있는 protocolVersion와 match해야 합니다. mismatch는 header-mismatch error가 있는 400이지, 대충 넘길 일이 아닙니다.5
Compliance를 위해 header 두 개가 더 필요합니다
섹션 링크: Compliance를 위해 header 두 개가 더 필요합니다Mcp-Method는 모든 request에서 method를 mirror합니다. Mcp-Name는 tools/call, resources/read, prompts/get에서 params.name 또는 params.uri를 mirror합니다. body를 parse하지 않고도 proxy가 route할 수 있게 하려고 존재합니다.5
예전 shape는 사라졌고, 거부로 답합니다
섹션 링크: 예전 shape는 사라졌고, 거부로 답합니다GET stream, Mcp-Session-Id, Last-Event-ID resumption은 모두 제거되었습니다. 이 revision만 말하는 server는 GET 또는 DELETE에 405 Method Not Allowed로 답하고, session header는 mint하지 않은 채 무시하며, Last-Event-ID도 무시해야 합니다.5
이제 장 전체를 다시 보이게 하는 측정입니다. current-revision request를 각 server에 HTTP로 보내세요.
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)"}}상수는 동작과 일치합니다. Python SDK의 LATEST_PROTOCOL_VERSION는 2026-07-28를 읽고, TypeScript SDK의 것은 2025-11-25를 읽습니다. 위 단계의 header-mismatch request를 보내면 Python server는 error -32020와 message “mcp-protocol-version header does not match the request envelope's protocol version”로 400를 답합니다. TypeScript SDK에는 그런 code가 없습니다. 그것을 정의한 revision을 구현하지 않았기 때문입니다.
둘을 모두 Tier 1로 나열한 page는 “각 SDK는 같은 functionality를 제공합니다”라고도 말합니다.1 아래 날짜 기준 current revision에 대해 그 문장은 aspirational입니다. 설치하려는 SDK의 LATEST_PROTOCOL_VERSION를 확인하세요. 한 줄이고, 이 장에서 1년 뒤에도 여전히 중요할 유일한 주장입니다.
401, 그리고 인용해야 할 문장
섹션 링크: 401, 그리고 인용해야 할 문장Server를 laptop 밖으로 옮기면 모르는 사람의 client가 token을 들고 나타납니다. 이것은 26장이 남겨 둔 절반이고, multi-user product가 건너뛸 수 없는 절반입니다.
Specification은 MCP server를 OAuth 2.1 role에 넣고 이름을 붙입니다. protected MCP server는 resource server이고, client는 OAuth client이며, authorization server는 다른 누군가의 문제입니다.4 그 role에서 mandatory clause 네 개가 나옵니다. paraphrase하다가 실수가 생기므로 통째로 인용합니다.
OAuth 2.1 resource server로서의 역할을 수행하는 MCP server는 OAuth 2.1 Section 5.2에 설명된 대로 access token을 validate해야 합니다. MCP server는 RFC 8707 Section 2에 따라 access token이 intended audience로서 자신을 위해 특별히 발급되었는지 validate해야 합니다. […] MCP client는 MCP server의 authorization server가 발급한 token이 아닌 token을 MCP server에 보내면 안 됩니다. MCP server는 자신의 resource와 함께 사용할 수 있는 유효한 token만 accept해야 합니다. MCP server는 다른 token을 accept하거나 transit하면 안 됩니다.4
“accept하거나 transit하면 안 된다”는 anti-passthrough rule이고, audience 장치 전체가 존재하는 이유입니다. 자신이 받은 bearer token을 third-party API에 replay하는 server는 confused deputy입니다. 자신을 호출한 사람에게 자신의 trust를 빌려주는 것입니다. 이 규칙은 storage뿐 아니라 reuse를 금지합니다.
이를 enforce 가능하게 하려면 네 RFC가 필요하고, 각각 job이 하나씩 있습니다.6 RFC 9728은 client가 authorization server를 처음 찾아내는 방식입니다. MCP server가 protected-resource-metadata document를 serve하고 401가 그것을 가리킵니다. RFC 8707은 resource parameter입니다. client는 authorization server가 support하는지와 무관하게 authorization request와 token request 둘 다에 server의 canonical URI를 보내야 하며, 그래야 발급된 token이 audience를 명명합니다. RFC 9207은 반대편에서 loop를 닫습니다. client는 redirect 전에 issuer를 기록하고, 돌아온 iss를 exact string으로 비교합니다. normalisation은 없습니다. case folding도, default-port 생략도, trailing slash도 없습니다. 그리고 RFC 7591, Dynamic Client Registration은 이제 Client ID Metadata Document를 선호하면서 deprecated되었습니다. 이를 support하지 않는 authorization server와의 “backwards compatibility를 위해 유지”됩니다.4
Audience만 확인하는 token verifier로 두 server를 연결합니다. TypeScript ladder는 이렇습니다.
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":[…]}}{"resource":"http://127.0.0.1:8931/mcp",
"authorization_servers":["https://auth.example.com/"],
"scopes_supported":["incidents:read","incidents:write"],
"resource_name":"Incidents"}두 SDK 모두 그 document를 serve하고, 둘 다 401가 그것을 가리키게 합니다. 이것이 discovery story 전체입니다. 당신의 server를 본 적 없는 client가 refusal에서 authenticate 위치를 배웁니다. 403는 다른 동물입니다. token은 괜찮지만 scope가 아닙니다. challenge는 무엇이 빠졌는지 이름을 대므로 client가 처음부터 다시 시작하지 않고 step up할 수 있습니다.
두 rung이 다르고, 어느 차이도 specification 안에 있지 않습니다. TypeScript SDK는 expiry claim이 없는 token을 거부합니다. Python 쪽은 200를 반환합니다. AccessToken에서 expires_at이 optional이고, None은 “의견 없음”을 뜻하기 때문입니다. 그리고 Python 403는 specification이 server가 포함해야 한다고 말하는 scope parameter 없이 error_description="Required scope: incidents:read"를 싣습니다. verifier는 library default를 받아들일 곳이 아닙니다. audience check는 어느 언어에서든 직접 작성해야 하고, expiry도 마찬가지입니다.
같은 run에서 나온 정직한 nit 하나. endpoint에 GET을 보내면 Express wiring은 404로, Python 쪽은 400 Bad Request: Missing session ID로 답했습니다. specification은 405 Method Not Allowed를 요구하고, “session ID”는 이 revision에서 제거된 vocabulary입니다. 어느 쪽도 위험하지는 않습니다. 둘 다 migration 중인 ecosystem의 모양입니다.
Server는 실제로 어디에 사는가
섹션 링크: Server는 실제로 어디에 사는가Shipping의 마지막 조각은 어디에 publish하느냐이고, 여기에는 숫자가 있는 답이 있습니다. 오늘 official registry의 모든 server를 latest version 기준으로 crawl했습니다.7
| servers | |
|---|---|
| total (latest version, not deleted) | 28,170 |
| active / deprecated | 27,853 / 317 |
| ship at least one installable package | 13,065 |
| remote only — URL만 있고 설치할 것은 없음 | 14,696 |
| npm | 8,275 |
| PyPI | 3,603 |
| OCI images | 867 |
mcpb bundles | 706 |
| NuGet / Cargo | 107 / 43 |
서로 반대 방향을 가리키는 두 가지 해석이 있습니다. published server 기준으로는 npm이 2.3 대 1로 앞섭니다 — 사람들이 ecosystem이 TypeScript라고 말할 때 인용하는 숫자입니다. download 기준으로는 Python이 앞섭니다. 지난 30일 동안 mcp는 2억 8,670만을 기록했고, @modelcontextprotocol/sdk는 1억 9,470만이었습니다. 여기에 fastmcp의 7,210만을 더하기 전입니다.7 둘 다 Tier 1이고, normative schema는 schema.ts이며, 공식 “Build an MCP server” tutorial은 Python tab에서 시작합니다.1 머릿속에 어느 절반을 갖고 있었든, 다른 절반도 사실입니다.
그리고 둘보다 더 중요한 row가 있습니다. registry의 절반 이상 — 28,170개 중 14,696개 — 은 설치할 것이 없습니다. 그것들은 web service입니다. transport tally도 반대편에서 동의합니다. package entry 14,290개 중 13,787개가 stdio를 declare합니다. remote entry 16,640개 중 15,570개가 Streamable HTTP를 declare하고, 1,070개는 여전히 deprecated HTTP+SSE를 declare합니다. 그러니 “MCP server는 laptop의 subprocess다”라는 설명은 줄어드는 minority를 설명할 뿐이며, 14,696개 모두에는 environment variable이 아니라 위 section이 필요합니다.
세부 정보 보기
의도적으로 bilingual이며, 그 precedent도 있습니다.
이것은 course에서 유일한 bilingual chapter입니다. 정직한 답이 갈라지기 때문입니다. registry는 npm-first이고 download는 Python-first입니다. 동시에, 오늘 그렇습니다. 둘 중 하나만 쓰면 질문의 절반을 넘겨주고, 그렇게 하면서 ecosystem을 잘못 설명하게 됩니다. 공개된 precedent도 있습니다. Hugging Face MCP Course는 prerequisite 중 하나로 “적어도 하나의 programming language 경험이 있어야 합니다. Python 또는 TypeScript example이 표시됩니다”를 나열하고, 둘 다 가르칩니다.8 가치 전체가 implementation 수에 있는 protocol에서 monolingual은 좋지 않습니다.
날짜가 붙는 section: 위 내용 중 shelf life가 있는 모든 것
섹션 링크: 날짜가 붙는 section: 위 내용 중 shelf life가 있는 모든 것2026년 9월 7일에 protocol revision 2026-07-28 기준으로 읽고 측정했습니다.
| value | |
|---|---|
@modelcontextprotocol/sdk | 1.30.0, 2026년 7월 27일 publish; unpacked 4,322,438 bytes, 693 files, direct dependencies 17개 |
| 구현한 latest revision | 2025-11-25 |
mcp (PyPI) | 2.1.1, 2026년 8월 25일 publish; 357,912-byte wheel, plus mcp-types 2.1.1 at 69,656 bytes |
| 구현한 latest revision | 2026-07-28 |
| SDK tiers | TypeScript, Python, C#, Go, Rust는 Tier 1; Java, Ruby는 Tier 2; Swift, PHP, Kotlin은 Tier 3 |
| registry servers | 28,170 |
| downloads, last 30 days | mcp 286,653,871 · fastmcp 72,097,269 · @modelcontextprotocol/sdk 194,679,333 |
숫자가 아닌 migration note 하나. mcp 2.x에서 FastMCP는 MCPServer로 이름이 바뀌었고, online tutorial 거의 전부가 여전히 old import로 시작합니다. SDK는 오직 그것을 설명하기 위한 module을 ship합니다. 이 장에서 가장 배려심 있는 deprecation입니다.
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이 이미 올라와 있었기 때문에 공짜입니다. 14,696개의 remote server 대부분이 여기에 해당합니다.
Server가 data tooling을 감싼다면 Python으로 작성하세요. 당신이 expose하는 것은 pandas, warehouse client, notebook 분량의 transform입니다. 다른 언어의 server라면 schema를 걸친 subprocess call일 뿐입니다. 한 번 시작하는 service에서 import 700 ms는 cost가 아닙니다. host가 하루 종일 다시 launch하는 subprocess에서는 cost입니다.
그리고 지금은 revision row가 둘 다를 override합니다. 2026-07-28 — multi-round-trip request, resultType, cache hint, server/discover — 이 필요하다면, 두 SDK 중 하나는 오늘 갖고 있고 다른 하나는 없습니다.
다음은 어디인가
섹션 링크: 다음은 어디인가이제 같은 server를 어느 언어로든 ship할 수 있고, preference 대신 표로 선택을 방어할 수 있으며, 두 live transport 위에서 실행하고, token을 건네도 거부하게 만들 수 있습니다.
당신이 만든 것은 여전히 함수입니다. schema, endpoint, model이 invoke하는 deterministic한 것입니다. 지식의 한 부류 전체는 그 shape에 맞지 않습니다. 우리는 postmortem을 어떻게 쓰는지, incident report에는 어떤 field가 필요한지, 우리가 어떤 순서로 일을 하고 왜 그렇게 하는지 같은 것들입니다. 그것은 procedure이고 prose입니다. 그것을 tool description에 억지로 넣는 것은 system prompt가 모든 turn마다 2,000 token으로 자라도록 만드는 방식입니다. 대화가 incident와 관련이 있든 없든 매번 비용을 냅니다.
28장은 다른 답입니다. model이 call하는 대신 읽는 SKILL.md가 들어 있는 folder입니다. 세 level로 load되므로 reference material은 필요한 turn 전까지 거의 cost가 들지 않습니다. main language가 없고, 그것이 이 장이 처음 가르치는 것입니다.
Sources and method
섹션 링크: Sources and method여기 있는 모든 것은 2026년 9월 7일, Node 22.22.3과 Python 3.14.4에서, zod 3.25.76이 함께 있는 @modelcontextprotocol/sdk 1.30.0 및 mcp 2.1.1 기준으로 측정했습니다. 각각은 별도의 throwaway directory에 설치했습니다. Timing은 25회 launch의 median이며, wall clock은 spawn부터 tools/list response를 실은 line까지입니다. token count는 각 definition의 JSON에 대해 tiktoken를 통해 o200k_base로 계산했습니다. paid API는 호출하지 않았습니다. 여기에는 model이 필요하지 않습니다.
두 server는 공백 줄 제외 81줄과 63줄입니다. 세 도구 중 하나는 위에 두 언어로 재현했고, 나머지 네 registration은 설명한 차이만 있습니다. Python SDK의 error-disclosure policy는 mcp/server/mcpserver/exceptions.py에 있는 ToolError와 UnexpectedToolError의 docstring에서 인용했습니다. pretty-printing default는 mcp/server/mcpserver/resources/types.py와 utilities/func_metadata.py의 pydantic_core.to_json(result, fallback=str, indent=2)입니다. protocol-version constant는 mcp_types/version.py의 LATEST_PROTOCOL_VERSION와 TypeScript SDK의 types.js에 있으며, changelog가 아니라 installed package에서 읽었습니다.
-
SDKs,
modelcontextprotocol.io/docs/sdk, 및 Build an MCP server,modelcontextprotocol.io/docs/develop/build-server, 둘 다 2026년 9월 7일 읽음. tier table, “각 SDK는 같은 functionality를 제공하지만 해당 language의 idiom과 best practice를 따른다”는 sentence, tutorial의 language-tab order (Python, TypeScript, Java, Kotlin, C#, Ruby, Rust, Go), 그리고print()와stdout에 관해 인용한 logging rule의 source입니다. ↩ ↩2 ↩3 ↩4 -
stdio transport,
.../basic/transports/stdio. newline framing과stdoutpurity rule의 source입니다. 26장은 이 page를 전부 읽습니다. 여기서는 broken server가 위반하는 line 때문에 cite합니다. ↩ -
MCP Inspector,
modelcontextprotocol.io/docs/2026-07-28/tools/inspector, 2026년 9월 7일 읽음. 하나의 binary 뒤에 web,--cli,--tui세 client가 있고, 하나의 core, 하나의 transport set, disk의 하나의 OAuth state를 공유합니다. 여기의 catalogue trace는 CLI가 생성했습니다. ↩ -
Authorization,
modelcontextprotocol.io/specification/2026-07-28/basic/authorization, 2026년 9월 7일 읽음. resource-server role, 전문 인용한 네 token-handling clause, server가 RFC 9728을 implement하고 client가 discovery에 그것을 사용해야 한다는 requirement,resourceparameter rule과 canonical-URI definition, issuer-validation table, Dynamic Client Registration deprecation,401/403/400table과insufficient_scopechallenge, 그리고 stdio exemption — “STDIO transport를 사용하는 implementation은 이 specification을 따르지 않아야 하며, 대신 environment에서 credential을 가져와야 합니다.” — 의 source입니다. ↩ ↩2 ↩3 ↩4 -
Streamable HTTP,
.../basic/transports/streamable-http, 및 Transports overview,.../basic/transports. single-endpoint POST rule, dualAcceptrequirement,MCP-Protocol-Versionheader와 body match rule, “compliance에 REQUIRED”라고 설명된Mcp-Method및Mcp-Nameheader, GET stream, session,Last-Event-ID의 removal,405guidance, mandatoryOriginvalidation, 그리고 2024-11-05 HTTP+SSE transport를 SEP-2596 아래 Deprecated로 classify한 것의 source입니다. ↩ ↩2 ↩3 ↩4 -
Specification이 기대는 네 문서와 그것이 profile하는 draft: 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 —resourceparameter와 그것이 bind하는 audience. Jones, M.B., Hunt, P. and Parecki, A., OAuth 2.0 Protected Resource Metadata, RFC 9728, April 2025 —401가 가리키는 document. Meyer zu Selhausen, K. and Fett, D., OAuth 2.0 Authorization Server Issuer Identification, RFC 9207, March 2022 —issparameter와 exact-string comparison. Richer, J. (ed.) et al., OAuth 2.0 Dynamic Client Registration Protocol, RFC 7591, July 2015, 이 용도에서는 deprecated. 그리고 Jones, M. and Hardt, D., The OAuth 2.0 Authorization Framework: Bearer Token Usage, RFC 6750, October 2012, section 3, 위WWW-Authenticatechallenge shape에 대한 source입니다. ↩ -
Official MCP registry,
registry.modelcontextprotocol.io/v0/servers, 2026년 9월 7일version=latest로 crawl: 282 pages, 28,170 servers, distinct server name에 대해registryType로 집계. Download figure:@modelcontextprotocol/sdk에 대한api.npmjs.org/downloads/point/last-month(2026년 8월 8일–9월 6일 194,679,333) 및mcp와fastmcp에 대한pypistats.org/api/packages/<name>/recent, 둘 다 같은 날 읽음. Package size는 npm registry document와 PyPI JSON API에서 가져왔습니다. ↩ ↩2 -
MCP Course, Hugging Face,
huggingface.co/learn/mcp-course, unit 0, 2026년 9월 7일 읽음: prerequisite 중 “적어도 하나의 programming language 경험이 있어야 합니다. Python 또는 TypeScript example이 표시됩니다.” ↩