Ship en MCP-server: TypeScript og Python, målt
Den samme server skrevet to gange — tre tools, en ressource, en prompt — og vejet: 94 pakker mod 28, koldstart 145 ms mod 709.
På denne side
Her er hele sprogargumentet, målt, før et eneste ord af det bliver formuleret.
node ./incidents.js 144.5 ms
python incidents.py 709.4 ms
npx incidents-mcp 712.6 msDe første to linjer er den sammenligning, alle vil have. Den tredje linje er den samme TypeScript-server fra første linje, startet på den måde, den faktisk ville blive distribueret — og den lander tre millisekunder fra Python.
Kapitel 26 læste Model Context Protocol op mod dens egen specifikation med rå JSON-RPC, fordi rå JSON-RPC ikke har noget sprog. Dette kapitel har to, og argumentets vægt falder her: den samme server, skrevet to gange. Tre tools, én ressource, én prompt, begge SDK'er, ingen genveje på nogen af siderne. Derefter transports, inspector, 401'eren og tallene, ingen har offentliggjort.
Serveren, og hvorfor den har disse fem ting i sig
Link til afsnittet: Serveren, og hvorfor den har disse fem ting i sigEn hændelseslog. Tre tools, fordi Kapitel 18s opdeling mellem læsninger og skrivninger skal være synlig: search_incidents læser, open_incident skriver og giver et handle tilbage, resolve_incident tager det handle og lukker. Én ressource, incidents://open, fordi læsning af den aktuelle liste er noget, applikationen vedhæfter. Én prompt, postmortem, fordi "skriv det her op" er en persons slash-kommando. Det er Kapitel 26's kontrolhierarki — model, applikation, person — gjort til fem registreringer.
Handle'et betyder mere, end det ser ud til. Kapitel 26 ødelagde en legetøjskalender ved at holde dens state i et array på modulniveau: protokollen har ingen session, så et creation-tool returnerer en opaque identifier, og hvert senere kald tager den som et almindeligt argument. Intet i nogen af filerne antager, at kalderen er den proces, der åbnede den.
Her er det samme tool på begge sprog, registreret side om side:
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.")Læs først, hvad der er det samme, for det er fundet. Begge erklærer et navn, en beskrivelse, to beskrevne string-argumenter og tre annotations; begge er én funktion; ingen af dem nævner JSON-RPC, framing, stdout eller en protokolversion. De to SDK'er er konvergeret mod den samme form, og det er, hvad "Tier 1" skal betyde.1
To forskelle er reelle, og begge vender tilbage senere. TypeScript beskriver argumenter med et schema-bibliotek — Zod her — og schemaet er en værdi, du skriver. Python beskriver dem med funktionens egne type hints og læser dem ved import time, og derfor ved den ting om funktionen, som TypeScript-filen aldrig fortalte den. Og fejlvejen: TypeScript returnerer et tool-resultat med isError, Python raiser. Hold fast i det.
De fire andre registreringer adskiller sig ikke strukturelt. Ressourcen er server.registerResource("open-incidents", "incidents://open", …) mod @server.resource("incidents://open", …); prompten er registerPrompt mod @server.prompt. Den sidste linje i hver fil er transporten: await server.connect(new StdioServerTransport()) mod server.run().
Hele filer: 81 ikke-tomme linjer og 3.060 bytes TypeScript mod 63 og 2.555. Tag det med den mængde salt, det fortjener — linjetællinger måler en formatter lige så meget som et sprog, og derfor står ingen af tallene i oversigtstabellen nedenfor.
Én klient, begge servere
Link til afsnittet: Én klient, begge servereBeviset på, at sproget er usynligt, er én klient kørt to gange, på elleve linjer:
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));Peg den på hver server efter tur. Reelt output, beskåret:
$ 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}"}]Samme tools, samme rækkefølge, samme handle. En TypeScript-klient kan ikke se, hvad serveren er skrevet i, og den spørger aldrig. Det er hele løftet fra en protokol, der holder.
Se nu på whitespace i det andet resultat, for det er ikke kosmetik: Python-SDK'et serialiserer payloads med pydantic_core.to_json(result, fallback=str, indent=2). På ressourcelæsningen med to hændelser i listen er TypeScript-bodyen 136 tegn og 37 o200k_base tokens; Python-bodyen er 185 og 62. Otteogtres procent flere tokens for identiske rækker, betalt af den, der læser ressourcen ind i en prompt, hver gang.
Kataloget har samme historie med en større årsag. Begge servere, samme tre tools, tools/list vejet nøgle for nøgle:
| key | TypeScript | Python |
|---|---|---|
name | 21 | 21 |
description | 46 | 46 |
annotations | 46 | 46 |
inputSchema | 211 | 192 |
outputSchema | — | 187 |
execution | 27 | — |
| total | 342 | 480 |
Pythons input-schemaer er billigere — TypeScripts Zod-bro stempler en $schema og en additionalProperties på hver. Hele forskellen på 138 tokens er et output-schema, som ingen skrev. resolve_incident er annoteret -> Incident, så SDK'et afledte et JSON Schema for returtypen og sendte det med. Det er oprigtigt nyttigt — det er det, der lader en klient validere structuredContent — og det er 187 tokens af dit context window, der ankommer på grund af et type hint. Kapitel 24s regel om, at definitioner skubber det materiale ud, der betyder noget, gælder også schemaer, du ikke vidste, du havde.
Ødelæg det med vilje: fejlbeskeden, der lækkede
Link til afsnittet: Ødelæg det med vilje: fejlbeskeden, der lækkedeDe to fejlveje ovenfor er ikke et stilvalg. Giv hver server et tool, der fejler på den måde, en reel integration fejler, og læs, hvad der når frem til modellen.
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'et lagde en intern adresse, en port, et databasenavn og en service account ind i modellens context. Python-SDK'et lagde intet af det derind; tracebacken gik til stderr og blev på serveren.
Ingen af delene er en bug. Begge er beslutninger, og Python-beslutningen står i dens egen docstring: en ToolError er "en fejl, du forventede", og dens besked returneres "i content, så modellen kan læse den"; alt andet "behandles som et crash: modellen ser kun Error executing tool <name>, og serveren logger tracebacken på ERROR". Klassen for crash-tilfældet siger resten højt — "intet fra originalen når klienten".
Begge adfærdsmønstre er forkerte halvdelen af tiden. Kapitel 18 argumenterede for, at en valideringsfejl skal komme tilbage som et tool-resultat, modellen kan læse og rette, fordi det er den mest værdifulde linje i de fleste integrationer; på Python-siden kræver det, at man eksplicit raiser ToolError, og en bar ValueError smider den nyttige sætning væk. Kapitel 30s argument løber den anden vej: alt, hvad et tool returnerer, lander i en context, som en senere prompt injection kan prøve at læse tilbage ud, og en ikke-gennemgået exception string er den mindst reviderede tekst i dit system.
Reglen, der overlever begge: beslut, per tool, hvad en fejl må sige, og skriv den string selv. Lad aldrig en exceptions standardtekst bestemme, på noget sprog.
Ødelæg det med vilje: én linje på standard output
Link til afsnittet: Ødelæg det med vilje: én linje på standard outputDen officielle tutorial formulerer reglen uden forbehold: "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 Kapitel 26 citerede den normative version — en server "MUST NOT write anything to its stdout that is not a valid MCP message".2
Tilføj én linje til hver server, og læs den rå 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-versionen er værre, og årsagen er ikke MCP. En proces, hvis stdout er en pipe snarere end en terminal, får en block-buffered stream, så den vildfarne linje flushes, når bufferen beslutter det — her ved exit, efter et svar, den blev skrevet før. Korruptionen dukker ikke op der, hvor buggen er. Tilføj flush=True, eller et bibliotek, der flusher, og den flytter sig.
Så kommer den del, der forklarer, hvorfor det her shipper. Fodr den ødelagte server til tre klienter:
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 warningDen syvlinjers parser dør med det samme. Den officielle klient og Inspector trækker på skuldrene — de skipper linjen og fortsætter. En regel, der kun ødelægger de klienter, ingen bruger, er en regel, der når produktion intakt, og derfor er det værd at ødelægge den med vilje her i stedet for i en kundes log.
Inspectors CLI-tilstand er den halvdel, der bliver glemt: npx @modelcontextprotocol/inspector --cli <command> --method tools/list printer et katalog og afslutter, hvilket gør den scriptbar på en måde, browser-UI'et ikke er.3
Tabellen
Link til afsnittet: TabellenBegge SDK'er blev installeret rent, i deres egne mapper, intet delt:
| TypeScript | Python | |
|---|---|---|
| package | @modelcontextprotocol/sdk 1.30.0 + zod 3.25.76 | mcp 2.1.1 |
| seneste implementerede protokolrevision | 2025-11-25 | 2026-07-28 |
| installerede transitive pakker | 94 | 28 |
| installeret størrelse | 13,9 MiB | 44,3 MiB |
| filer på disk | 3.386 | 2.018 |
| tredjepartspakker indlæst for at serve stdio | 8 af 94 | 18 af 28 |
| bar interpreter-start, median | 19,4 ms | 11,1 ms |
spawn → tools/list besvaret, median af 25 | 144,5 ms | 709,4 ms |
tools/list-katalog, o200k_base tokens | 342 | 480 |
Hver række overrasker i en anden retning, og derfor er sammenligningen værd at køre i stedet for at antage.
TypeScript installerer mere end tre gange så mange pakker og mindre end en tredjedel af bytes. 94 dependencies er npm-økosystemet, der er sig selv — fast-deep-equal, es-errors, dunder-proto. Pythons 28 er færre og enorme: cryptography, pydantic-core og uvicorn er kompilerede artefakter. Hvis dit instinkt er, at dependency-antal er det, man skal bekymre sig om, er denne række modeksemplet.
Pythons interpreter starter hurtigere end Nodes, og det er ikke tæt på — 11,1 ms mod 19,4 ms på et tomt program. Så de 565 ms i koldstart-rækken er ikke sproget. Det er SDK'et, og rækken med indlæste pakker siger hvorfor:
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, …En server, hvis eneste I/O er en pipe, importerer en ASGI-webserver, en HTTP-klient og et TLS-bibliotek, før den læser sin første linje. TypeScript-SDK'et shipper også Express, Hono, jose og eventsource — de ligger ulæst på disk, fordi package-grænsen holder dem ude af en server/stdio.js import. Pythons package er én importgraf, så import mcp er det hele: python -X importtime tilskriver 727 ms til import mcp.server.mcpserver — et tal målt under import profiler, og derfor kommer det ud over de 709 ms, den uprofilterede kørsel tager fra spawn til svar — og 269 af dem til mcp.types-undertræet alene — wire-typerne er Pydantic-modeller, én klasse per protokolbesked per revision, og at bygge dem er arbejde udført ved import. Det er et design trade-off, ikke sjusk — ivrige imports er grunden til, at Python-SDK'et kan give dig run(transport="streamable-http") på næste linje uden en ekstra install.
Og så ophæver den sidste række i åbningsblokken argumentet. Pak TypeScript-serveren ordentligt — et bin entry, en shebang, npm link, intet at downloade — og start den gennem npx med --no-install, hvilket er sådan, en publiceret stdio-server faktisk startes:
node ./incidents.js 144.5 ms
npx incidents-mcp 712.6 ms (+568.1 ms of launcher)
python incidents.py 709.4 msLauncheren koster 568 ms per start — fire og en halv gange hele TypeScript-SDK-importen — og den betales ved hver launch, fordi en MCP host starter en stdio-server ved at køre den kommando. Så den ærlige form af "TypeScript starter fem gange hurtigere" er: det gør det, indtil du distribuerer det på normal vis. Det samme forbehold gælder formodentlig uvx; denne maskine havde ingen uv installeret, så den række findes ikke. Intet umålt kommer i tabellen.
To transports, og kun to
Link til afsnittet: To transports, og kun toKapitel 26 dækkede stdios framing. To ting efterlod det til her.
Den første: at køre en server med npx eller uvx er stdio-transporten. Der er ingen separat "package mode". En hosts konfiguration angiver en kommando og argumenter; hosten spawner den og taler over pipes. Derfor er "hvordan distribuerer jeg det her" og "hvilken transport taler den" ét spørgsmål lokalt, og derfor hører launcherens omkostning hjemme i et kapitel om shipping.
Den anden: stdio har slet ingen authorization-sektion, og specifikationen siger det på én linje — implementationer, der bruger stdio, "SHOULD NOT follow this specification, and instead retrieve credentials from the environment".4 Dens sikkerhedsmodel er operativsystemets, og det er dens begrænsning også: en lokal subprocess server præcis én maskine og én bruger.
Den anden live transport er Streamable HTTP: ét endpoint, der accepterer POST, én HTTP-request per JSON-RPC-besked, og en Accept header, der skal liste både application/json og text/event-stream, fordi serveren per request vælger, hvilken af de to den svarer med.5 Kapitel 14 parsede den event stream i hånden, så intet i wire-formatet er nyt — kun det, der omslutter det. Tre forpligtelser i den aktuelle revision er lette at overse, og alle tre kan testes:
Versionsheaderen skal stemme med bodyen
Link til afsnittet: Versionsheaderen skal stemme med bodyenHver POST bærer MCP-Protocol-Version, og dens værdi skal matche protocolVersion inde i requestens egen _meta. Et mismatch er en 400 med en header-mismatch-fejl, ikke et skuldertræk.5
To yderligere headers kræves for compliance
Link til afsnittet: To yderligere headers kræves for complianceMcp-Method spejler metoden på hver request; Mcp-Name spejler params.name eller params.uri på tools/call, resources/read og prompts/get. De findes, så en proxy kan route uden at parse bodies.5
De gamle former er væk og besvarer med en afvisning
Link til afsnittet: De gamle former er væk og besvarer med en afvisningGET-streamen, Mcp-Session-Id og Last-Event-ID resumption blev alle fjernet. En server, der kun taler denne revision, bør svare 405 Method Not Allowed på en GET eller DELETE, ignorere en session-header uden at udstede en, og ignorere Last-Event-ID.5
Nu målingen, der omrammer hele kapitlet. Send en current-revision request til hver server over 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)"}}Konstanterne stemmer med adfærden: Python-SDK'ets LATEST_PROTOCOL_VERSION læser 2026-07-28, TypeScript-SDK'ets læser 2025-11-25. Send header-mismatch-requesten fra trinnet ovenfor, og Python-serveren svarer 400 med fejl -32020 og beskeden "mcp-protocol-version header does not match the request envelope's protocol version"; TypeScript-SDK'et har ingen sådan kode, fordi det ikke implementerer den revision, der definerer den.
Siden, der lister begge som Tier 1, siger også "Each SDK provides the same functionality".1 På datoen nedenfor, for den aktuelle revision, er den sætning aspiratorisk. Tjek LATEST_PROTOCOL_VERSION i det SDK, du er ved at installere; det er én linje, og den eneste påstand i dette kapitel, der stadig betyder noget om et år.
401'eren og sætningen, du skal citere
Link til afsnittet: 401'eren og sætningen, du skal citereFlyt en server væk fra din laptop, og en fremmed klients token dukker op. Det er den halvdel, Kapitel 26 lod ligge, og den halvdel, et multi-user-produkt ikke kan springe over.
Specifikationen placerer MCP-serveren i en OAuth 2.1-rolle og navngiver den: en protected MCP server er en resource server, klienten er en OAuth-klient, og authorization serveren er en andens problem.4 Fra den rolle kommer fire obligatoriske klausuler, citeret i fuld længde, fordi parafrasering er måden, fejlen opstår på:
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" er anti-passthrough-reglen, og det er derfor, hele audience-apparatet findes. En server, der replay'er den bearer token, den fik udleveret, mod en tredjeparts-API, er en confused deputy: den låner sin egen tillid til den, der kaldte den. Reglen forbyder genbrug, ikke kun lagring.
At gøre det håndhævbart kræver fire RFC'er, ét job hver.6 RFC 9728 er sådan, klienten overhovedet finder authorization serveren: MCP-serveren server et protected-resource-metadata-dokument, og en 401 peger på det. RFC 8707 er resource-parameteren — klienten skal sende serverens kanoniske URI i både authorization-requesten og token-requesten, "regardless of whether authorization servers support it", så den udstedte token navngiver sin audience. RFC 9207 lukker loopet fra den anden side: klienten registrerer issueren før redirect og sammenligner den returnerede iss som præcis string, uden normalisering — ingen case folding, ingen udeladelse af default-port, ingen trailing slash. Og RFC 7591, Dynamic Client Registration, er nu deprecated til fordel for Client ID Metadata Documents, "retained for backwards compatibility with authorization servers that do not support" dem.4
Wire det op på begge servere med en token-verifier, der ikke gør andet end at tjekke audience. TypeScript-stigen:
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"}Begge SDK'er server det dokument, og begge peger en 401 på det, hvilket er hele discovery-historien: en klient, der aldrig har set din server, lærer fra en afvisning, hvor den skal autentificere. 403 er et andet dyr — tokenen er fin, scopen er ikke — og challengen navngiver, hvad der mangler, så klienten kan steppe op i stedet for at starte forfra.
To trin adskiller sig, og ingen af forskellene er i specifikationen. TypeScript-SDK'et afviser en token med ingen expiry claim; Python-versionen returnerer 200, fordi expires_at er optional på dens AccessToken, og None betyder "ingen mening". Og Python-403 bærer error_description="Required scope: incidents:read" uden den scope parameter, specifikationen siger, at servere bør inkludere. En verifier er ikke et sted at acceptere en library default: audience-tjekket er dit at skrive på begge sprog, og det er expiry også.
Én ærlig småting fra samme kørsel. En GET på endpointet svarede 404 på Express-wiringen og 400 Bad Request: Missing session ID på Python-versionen, hvor specifikationen beder om 405 Method Not Allowed, og hvor "session ID" er vokabular, denne revision fjernede. Ingen af delene er farlig; begge er formen på et økosystem midt i en migration.
Hvor servere faktisk bor
Link til afsnittet: Hvor servere faktisk borDet sidste stykke shipping er, hvor du publicerer, og det har et svar med et tal. Crawlet i dag, hver server i det officielle registry på dens seneste version:7
| servere | |
|---|---|
| total (seneste version, ikke slettet) | 28.170 |
| aktive / deprecated | 27.853 / 317 |
| shipper mindst én installerbar pakke | 13.065 |
| kun remote — en URL, intet at installere | 14.696 |
| npm | 8.275 |
| PyPI | 3.603 |
| OCI images | 867 |
mcpb bundles | 706 |
| NuGet / Cargo | 107 / 43 |
To læsninger, der peger hver sin vej. Efter publicerede servere fører npm 2,3 til 1 — tallet folk citerer, når de siger, at økosystemet er TypeScript. Efter downloads fører Python: over de seneste tredive dage tog mcp 286,7 millioner mod @modelcontextprotocol/sdk med 194,7 millioner, før fastmcp med 72,1 millioner lægges til.7 Begge er Tier 1, det normative schema er en schema.ts, og den officielle "Build an MCP server"-tutorial åbner på Python-fanen.1 Uanset hvilken halvdel du havde i hovedet, er den anden halvdel også sand.
Og rækken, der betyder mere end nogen af dem: mere end halvdelen af registryet — 14.696 af 28.170 — har intet at installere. De er webservices. Transportoptællingerne stemmer fra den anden side: af 14.290 package-entries deklarerer 13.787 stdio; af 16.640 remote-entries deklarerer 15.570 Streamable HTTP, og 1.070 deklarerer stadig den deprecated HTTP+SSE. Så "en MCP-server er en subprocess på din laptop" beskriver et skrumpende mindretal, og hver eneste af de 14.696 har brug for sektionen ovenfor frem for en environment variable.
Vis detaljer
Bevidst tosproget, og præcedensen for det.
Dette er det eneste tosprogede kapitel i kurset, fordi det ærlige svar splitter: registryet er npm-first, og downloads er Python-first, samtidig, i dag. At skrive én af de to ville forære halvdelen af spørgsmålet væk og beskrive økosystemet forkert imens. Der findes åben præcedens — Hugging Face MCP Course lister blandt sine prerequisites "Experience with at least one programming language (Python or TypeScript examples will be shown)" og underviser i begge.8 En protokol, hvis hele værdi er antallet af implementationer, er et dårligt sted at være ensproget.
Dateret sektion: alt ovenfor, der har en holdbarhedsdato
Link til afsnittet: Dateret sektion: alt ovenfor, der har en holdbarhedsdatoLæst og målt den 7. september 2026, mod protokolrevision 2026-07-28.
| værdi | |
|---|---|
@modelcontextprotocol/sdk | 1.30.0, publiceret 27. juli 2026; 4.322.438 bytes udpakket, 693 filer, 17 direkte dependencies |
| seneste revision, det implementerer | 2025-11-25 |
mcp (PyPI) | 2.1.1, publiceret 25. august 2026; 357.912-byte wheel, plus mcp-types 2.1.1 på 69.656 bytes |
| seneste revision, det implementerer | 2026-07-28 |
| SDK-tiers | TypeScript, Python, C#, Go, Rust på Tier 1; Java, Ruby på Tier 2; Swift, PHP, Kotlin på Tier 3 |
| registry-servere | 28.170 |
| downloads, seneste 30 dage | mcp 286.653.871 · fastmcp 72.097.269 · @modelcontextprotocol/sdk 194.679.333 |
Én migrationsnote, der ikke er et tal. I mcp 2.x blev FastMCP omdøbt til MCPServer, og næsten hver tutorial online åbner stadig med den gamle import. SDK'et shipper et modul, hvis eneste formål er at forklare det, hvilket er den mest hensynsfulde deprecation i dette kapitel:
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.Så hvilken én
Link til afsnittet: Så hvilken énMed tabellen foran dig er anbefalingen kedelig, og det er et godt tegn.
Hvis serveren bor inde i en webapplikation, du allerede kører, så skriv den i TypeScript. Samme proces, samme deploy, samme request handler; Streamable HTTP er et endpoint, du tilføjer ved siden af de andre; og de 13,9 MiB og de 145 ms er gratis, fordi runtime allerede var oppe. Det er de fleste af de 14.696 remote-servere.
Hvis serveren wrapper data tooling, så skriv den i Python. Det, du eksponerer, er pandas, en warehouse-klient, en notebooks mængde af transforms, og en server i et andet sprog ville være et subprocess-kald iført et schema. Syv hundrede millisekunders import i en service, der starter én gang, er ikke en omkostning; i en subprocess, en host relauncher hele dagen, er det.
Og indtil videre overtrumfer revisionsrækken begge. Hvis du har brug for 2026-07-28 — multi-round-trip requests, resultType, cache hints, server/discover — har ét af de to SDK'er det i dag, og det andet har ikke.
Hvor det går hen næste gang
Link til afsnittet: Hvor det går hen næste gangDu kan nu shippe den samme server i begge sprog, forsvare valget med en tabel i stedet for en præference, køre den over begge live transports og give den en token, den vil afvise.
Det, du byggede, er stadig en funktion: et schema, et endpoint, en deterministisk ting, modellen invoker. En hel klasse af viden passer ikke ind i den form — hvordan vi skriver et postmortem, hvilke felter vores hændelsesrapporter skal bruge, rækkefølgen vi gør ting i og hvorfor. Det er procedure, det er prosa, og at tvinge det ind i en tool-beskrivelse er sådan system prompts vokser til to tusind tokens, betalt på hver eneste turn, uanset om samtalen handler om hændelser eller ej.
Kapitel 28 er det andet svar: en mappe med en SKILL.md i, som modellen læser i stedet for at kalde, indlæst i tre niveauer, så referencematerialet næsten intet koster, før det turn hvor det er nødvendigt. Det har ikke noget hovedsprog, og det er det første, det lærer.
Kilder og metode
Link til afsnittet: Kilder og metodeAlt her blev målt den 7. september 2026 på Node 22.22.3 og Python 3.14.4, mod @modelcontextprotocol/sdk 1.30.0 med zod 3.25.76 og mcp 2.1.1, hver installeret i sin egen engangsmappe. Timings er medianer af 25 launches, wall clock fra spawn til linjen med tools/list-svaret; token-tællinger er o200k_base via tiktoken over JSON for hver definition. Ingen betalt API blev kaldt: intet her kræver en model.
De to servere er 81 og 63 ikke-tomme linjer; ét af deres tre tools gengives ovenfor på begge sprog, og de fire andre registreringer adskiller sig kun som beskrevet. Python-SDK'ets error-disclosure policy er citeret fra docstrings for ToolError og UnexpectedToolError i mcp/server/mcpserver/exceptions.py; pretty-printing-standarden er pydantic_core.to_json(result, fallback=str, indent=2) i mcp/server/mcpserver/resources/types.py og utilities/func_metadata.py. Protokolversionskonstanterne er LATEST_PROTOCOL_VERSION i mcp_types/version.py og i TypeScript-SDK'ets types.js, begge læst fra de installerede pakker snarere end fra en changelog.
Referencer
Link til afsnittet: Referencer-
SDKs,
modelcontextprotocol.io/docs/sdk, og Build an MCP server,modelcontextprotocol.io/docs/develop/build-server, begge læst 7. september 2026. Kilde til tier-tabellen, til sætningen "Each SDK provides the same functionality but follows the idioms and best practices of its language", til tutorialens language-tab-rækkefølge (Python, TypeScript, Java, Kotlin, C#, Ruby, Rust, Go) og til logging-reglen citeret omprint()ogstdout. ↩ ↩2 ↩3 ↩4 -
stdio transport,
.../basic/transports/stdio. Kilde til newline-framing ogstdout-purity-reglen. Kapitel 26 læser denne side i fuld længde; den citeres her for den linje, den ødelagte server overtræder. ↩ -
MCP Inspector,
modelcontextprotocol.io/docs/2026-07-28/tools/inspector, læst 7. september 2026. Én package, tre klienter bag én binary — web,--cliog--tui— der deler én core, ét sæt transports og én OAuth-state på disk. CLI'en producerede katalogsporene her. ↩ -
Authorization,
modelcontextprotocol.io/specification/2026-07-28/basic/authorization, læst 7. september 2026. Kilde til resource-server-rollen; de fire token-håndteringsklausuler citeret i fuld længde; kravet om, at servere implementerer RFC 9728, og klienter bruger den til discovery;resource-parameterreglerne og definitionen af canonical URI; issuer-validation-tabellen; deprecation af Dynamic Client Registration;401/403/400-tabellen oginsufficient_scope-challengen; og stdio-undtagelsen, "Implementations using an STDIO transport SHOULD NOT follow this specification, and instead retrieve credentials from the environment." ↩ ↩2 ↩3 ↩4 -
Streamable HTTP,
.../basic/transports/streamable-http, og Transports overview,.../basic/transports. Kilde til single-endpoint POST-reglen, det dobbelteAccept-krav,MCP-Protocol-Version-headeren og dens must-match-the-body-regel,Mcp-Method- ogMcp-Name-headers beskrevet som "REQUIRED for compliance", fjernelsen af GET-streamen, sessions ogLast-Event-ID,405-vejledningen, den obligatoriskeOrigin-validering og klassificeringen af 2024-11-05 HTTP+SSE-transporten som Deprecated under SEP-2596. ↩ ↩2 ↩3 ↩4 -
De fire, specifikationen læner sig op ad, med det draft, den profilerer: The OAuth 2.1 Authorization Framework,
draft-ietf-oauth-v2-1-13. Campbell, B., Bradley, J. og Tschofenig, H., Resource Indicators for OAuth 2.0, RFC 8707, februar 2020 —resource-parameteren og den audience, den binder. Jones, M.B., Hunt, P. og Parecki, A., OAuth 2.0 Protected Resource Metadata, RFC 9728, april 2025 — dokumentet, en401peger på. Meyer zu Selhausen, K. og Fett, D., OAuth 2.0 Authorization Server Issuer Identification, RFC 9207, marts 2022 —iss-parameteren og exact-string-sammenligningen. Richer, J. (ed.) et al., OAuth 2.0 Dynamic Client Registration Protocol, RFC 7591, juli 2015, deprecated til denne brug. Og Jones, M. og Hardt, D., The OAuth 2.0 Authorization Framework: Bearer Token Usage, RFC 6750, oktober 2012, sektion 3, forWWW-Authenticate-challenge-formen ovenfor. ↩ -
Officielt MCP registry,
registry.modelcontextprotocol.io/v0/servers, crawlet 7. september 2026 medversion=latest: 282 sider, 28.170 servere, optalt medregistryTypeover distinkte servernavne. Downloadtal:api.npmjs.org/downloads/point/last-monthfor@modelcontextprotocol/sdk(194.679.333 for 8. august – 6. september 2026) ogpypistats.org/api/packages/<name>/recentformcpogfastmcp, begge læst samme dag. Pakkestørrelser kommer fra npm registry-dokumentet og PyPI JSON API. ↩ ↩2 -
MCP Course, Hugging Face,
huggingface.co/learn/mcp-course, unit 0, læst 7. september 2026: blandt prerequisites, "Experience with at least one programming language (Python or TypeScript examples will be shown)". ↩