Een MCP Server shippen: TypeScript en Python, gemeten
Dezelfde server twee keer gebouwd: drie tools, een resource, een prompt. 94 packages tegenover 28; cold start 145 ms tegenover 709.
Op deze pagina
Hier is het volledige taalargument, gemeten, voordat er één woord van wordt gemaakt.
node ./incidents.js 144.5 ms
python incidents.py 709.4 ms
npx incidents-mcp 712.6 msDe eerste twee regels zijn de vergelijking die iedereen wil. De derde regel is dezelfde TypeScript-server uit de eerste regel, gestart zoals hij in de praktijk zou worden gedistribueerd — en hij eindigt drie milliseconden van Python.
Hoofdstuk 26 las het Model Context Protocol tegen zijn eigen specificatie met rauwe JSON-RPC, omdat rauwe JSON-RPC geen taal heeft. Dit hoofdstuk heeft er twee, en daar valt het gewicht van het argument: dezelfde server, twee keer geschreven. Drie tools, één resource, één prompt, beide SDK’s, geen shortcuts aan welke kant dan ook. Daarna de transports, de inspector, de 401 en de cijfers die niemand heeft gepubliceerd.
De server, en waarom deze vijf dingen erin zitten
Link naar de sectie: De server, en waarom deze vijf dingen erin zittenEen incidentlog. Drie tools, omdat de splitsing uit Hoofdstuk 18 tussen lezen en schrijven zichtbaar moet zijn: search_incidents leest, open_incident schrijft en geeft een handle terug, resolve_incident neemt die handle en sluit af. Eén resource, incidents://open, omdat het lezen van de huidige lijst iets is wat de application koppelt. Eén prompt, postmortem, omdat “schrijf dit uit” iemands slash command is. Dat is de controlehiërarchie van Hoofdstuk 26 — model, application, persoon — omgezet in vijf registraties.
De handle is belangrijker dan hij lijkt. Hoofdstuk 26 brak een speelgoedkalender door zijn state in een module-level array te bewaren: het protocol heeft geen sessie, dus een creation tool retourneert een opaque identifier en elke latere call neemt die als gewoon argument. Niets in een van beide bestanden neemt aan dat de caller het proces is dat hem heeft geopend.
Hier staat dezelfde tool in beide talen, naast elkaar geregistreerd:
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.")Lees eerst wat hetzelfde is, want dat is de bevinding. Beide declareren een naam, een description, twee beschreven string-argumenten en drie annotations; beide zijn één function; geen van beide noemt JSON-RPC, framing, stdout of een protocolversie. De twee SDK’s zijn naar dezelfde vorm toegegroeid, en dat is wat “Tier 1” hoort te betekenen.1
Twee verschillen zijn echt en allebei komen ze later terug. TypeScript beschrijft argumenten met een schema library — hier Zod — en het schema is een value die je schrijft. Python beschrijft ze met de eigen type hints van de function en leest die bij import time, waardoor het dingen over de function weet die het TypeScript-bestand nooit heeft verteld. En het error path: TypeScript retourneert een tool result met isError, Python raiset. Onthoud dat.
De andere vier registraties verschillen structureel nergens. De resource is server.registerResource("open-incidents", "incidents://open", …) tegenover @server.resource("incidents://open", …); de prompt is registerPrompt tegenover @server.prompt. De laatste regel van elk bestand is de transport: await server.connect(new StdioServerTransport()) tegenover server.run().
Volledige bestanden: 81 niet-lege regels en 3.060 bytes TypeScript tegenover 63 en 2.555. Neem dat met de korrel zout die het verdient — regeltellingen meten een formatter net zo goed als een taal, en daarom staat geen van beide cijfers in de headline-tabel hieronder.
Eén client, beide servers
Link naar de sectie: Eén client, beide serversHet bewijs dat de taal onzichtbaar is: één client twee keer draaien, in elf regels:
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));Wijs hem om de beurt naar elke server. Echte output, ingekort:
$ 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}"}]Dezelfde tools, dezelfde volgorde, dezelfde handle. Een TypeScript-client kan niet zien waarin de server is geschreven, en vraagt het nooit. Dat is de hele belofte van een protocol, en die houdt stand.
Kijk nu naar de whitespace in het tweede result, want die is niet cosmetisch: de Python SDK serialiseert payloads met pydantic_core.to_json(result, fallback=str, indent=2). Bij de resource read met twee incidents in de lijst is de TypeScript-body 136 tekens en 37 o200k_base tokens; de Python-body is 185 en 62. Achtenzestig procent meer tokens voor identieke rijen, betaald door degene die de resource in een prompt leest, elke keer opnieuw.
De catalogus vertelt hetzelfde verhaal met een grotere oorzaak. Beide servers, dezelfde drie tools, tools/list gewogen per key:
| key | TypeScript | Python |
|---|---|---|
name | 21 | 21 |
description | 46 | 46 |
annotations | 46 | 46 |
inputSchema | 211 | 192 |
outputSchema | — | 187 |
execution | 27 | — |
| totaal | 342 | 480 |
Python’s input schemas zijn goedkoper — TypeScript’s Zod bridge stempelt op elk schema een $schema en een additionalProperties. Het volledige gat van 138 token is een output schema dat niemand heeft geschreven. resolve_incident is geannoteerd met -> Incident, dus de SDK leidde een JSON Schema af voor het return type en shipte dat mee. Het is echt nuttig — het laat een client structuredContent valideren — en het zijn 187 tokens van je context window die aankomen vanwege een type hint. De regel uit Hoofdstuk 24 over definitions die het materiaal verdringen dat ertoe doet, geldt ook voor schemas waarvan je niet wist dat je ze had.
Breek het expres: de error message die lekte
Link naar de sectie: Breek het expres: de error message die lekteDe twee error paths hierboven zijn geen stijlkeuze. Geef elke server een tool die faalt zoals een echte integratie faalt, en lees wat het model bereikt.
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}De TypeScript SDK zette een intern adres, een poort, een databasenaam en een service account in de context van het model. De Python SDK zette er niets van neer; de traceback ging naar stderr en bleef op de server.
Geen van beide is een bug. Het zijn beslissingen, en de Python-beslissing staat in zijn eigen docstring: een ToolError is “een failure die je had voorzien” en het bericht ervan wordt teruggegeven “in content for the model to read”; al het andere “wordt behandeld als een crash: het model ziet alleen Error executing tool <name>, en de server logt de traceback op ERROR”. De class voor de crashcase zegt de rest hardop — “niets van het originele bereikt de client”.
Beide behaviours zijn de helft van de tijd verkeerd. Hoofdstuk 18 stelde dat een validation error moet terugkomen als een tool result dat het model kan lezen en corrigeren, omdat dat in de meeste integraties de regel met de meeste leverage is; aan de Python-kant vereist dat expliciet ToolError raisen, en een kale ValueError gooit de nuttige zin weg. Het argument uit Hoofdstuk 30 loopt de andere kant op: alles wat een tool retourneert belandt in een context waar een latere prompt injection uit kan proberen terug te lezen, en een niet-gereviewde exception string is de minst geaudite tekst in je systeem.
De regel die beide overleeft: beslis per tool wat een failure mag zeggen, en schrijf die string zelf. Laat nooit de default tekst van een exception beslissen, in geen van beide talen.
Breek het expres: één regel op standard output
Link naar de sectie: Breek het expres: één regel op standard outputDe officiële tutorial stelt de regel zonder voorbehoud: “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 Hoofdstuk 26 citeerde de normatieve versie — een server “MUST NOT write anything to its stdout that is not a valid MCP message”.2
Voeg één regel toe aan elke server en lees de 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 startingDe Python-versie is erger, en de reden is niet MCP. Een proces waarvan stdout een pipe is in plaats van een terminal krijgt een block-buffered stream, dus de verdwaalde regel wordt geflusht wanneer de buffer dat beslist — hier bij exit, na een response waar hij vóór was geschreven. De corruption verschijnt niet waar de bug zit. Voeg flush=True toe, of een library die flusht, en hij verplaatst.
Dan het deel dat verklaart waarom dit shipped. Voer de kapotte server aan drie clients:
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 warningDe parser van zeven regels sterft meteen. De officiële client en de Inspector halen hun schouders op — ze slaan de regel over en gaan door. Een regel die alleen clients breekt die niemand gebruikt, is een regel die intact productie bereikt, en daarom is het de moeite waard hem hier expres te breken in plaats van in de log van een klant.
De CLI-modus van de Inspector is de helft die wordt vergeten: npx @modelcontextprotocol/inspector --cli <command> --method tools/list print een catalogus en sluit af, waardoor hij scriptable is op een manier die de browser-UI niet is.3
De tabel
Link naar de sectie: De tabelBeide SDK’s installeerden schoon, in hun eigen directories, niets gedeeld:
| TypeScript | Python | |
|---|---|---|
| package | @modelcontextprotocol/sdk 1.30.0 + zod 3.25.76 | mcp 2.1.1 |
| laatste geïmplementeerde protocolrevisie | 2025-11-25 | 2026-07-28 |
| geïnstalleerde transitieve packages | 94 | 28 |
| geïnstalleerde grootte | 13,9 MiB | 44,3 MiB |
| bestanden op disk | 3.386 | 2.018 |
| third-party packages geladen om stdio te serven | 8 van 94 | 18 van 28 |
| bare interpreter start, mediaan | 19,4 ms | 11,1 ms |
spawn → tools/list beantwoord, mediaan van 25 | 144,5 ms | 709,4 ms |
tools/list catalogus, o200k_base tokens | 342 | 480 |
Elke rij verrast in een andere richting, en daarom is de vergelijking het waard om te draaien in plaats van te veronderstellen.
TypeScript installeert meer dan drie keer zoveel packages en minder dan een derde van de bytes. 94 dependencies is het npm-ecosysteem dat zichzelf is — fast-deep-equal, es-errors, dunder-proto. Python’s 28 zijn minder talrijk en enorm: cryptography, pydantic-core en uvicorn zijn compiled artefacts. Als je instinct zegt dat dependency count het probleem is, is deze rij het tegenvoorbeeld.
Python’s interpreter start sneller dan Node’s, en het scheelt veel — 11,1 ms tegenover 19,4 ms op een leeg programma. Dus de 565 ms in de cold-start-rij is niet de taal. Het is de SDK, en de rij met geladen packages zegt waarom:
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, …Een server waarvan de enige I/O een pipe is, importeert een ASGI-webserver, een HTTP-client en een TLS-library voordat hij zijn eerste regel leest. De TypeScript SDK shipt ook Express, Hono, jose en eventsource — ze staan ongelezen op disk, omdat de package boundary ze buiten een server/stdio.js import houdt. Python’s package is één import graph, dus import mcp is alles: python -X importtime schrijft 727 ms toe aan import mcp.server.mcpserver — een cijfer gemeten onder de import profiler, daarom komt het boven de 709 ms uit die de ongeprofilede run nodig heeft van spawn tot antwoord — en 269 daarvan aan de mcp.types subtree alleen — de wire types zijn Pydantic models, één class per protocol message per revision, en ze bouwen is werk dat bij import gebeurt. Dat is een design trade, geen slordigheid — eager imports zijn waarom de Python SDK je op de volgende regel run(transport="streamable-http") kan geven zonder tweede install.
En dan haalt de laatste rij van het openingsblok het argument onderuit. Package de TypeScript-server goed — een bin entry, een shebang, npm link, niets om te downloaden — en start hem via npx met --no-install, zoals een gepubliceerde stdio server in werkelijkheid wordt gestart:
node ./incidents.js 144.5 ms
npx incidents-mcp 712.6 ms (+568.1 ms of launcher)
python incidents.py 709.4 msDe launcher kost 568 ms per start — vierënhalf keer de volledige TypeScript SDK import — en die betaal je bij elke launch, omdat een MCP host een stdio server start door dat command te draaien. Dus de eerlijke vorm van “TypeScript start vijf keer sneller” is: dat klopt, totdat je hem op de normale manier distribueert. Hetzelfde voorbehoud geldt vermoedelijk voor uvx; deze machine had geen uv geïnstalleerd, dus die rij bestaat niet. Niets ongemetens gaat de tabel in.
Twee transports, en maar twee
Link naar de sectie: Twee transports, en maar tweeHoofdstuk 26 behandelde de framing van stdio. Twee dingen liet het voor hier liggen.
Het eerste: een server draaien met npx of uvx is de stdio transport. Er is geen aparte “package mode”. De configuratie van een host noemt een command en argumenten; de host spawnt het en praat over de pipes. Daarom zijn “hoe distribueer ik dit” en “welke transport spreekt het” lokaal één vraag, en daarom hoort de kost van de launcher thuis in een hoofdstuk over shipping.
Het tweede: stdio heeft helemaal geen authorization section, en de specification zegt dat in één regel — implementations die stdio gebruiken “SHOULD NOT follow this specification, and instead retrieve credentials from the environment”.4 Het security model is dat van het operating system, en de limit ook: een lokale subprocess bedient precies één machine en één user.
De andere live transport is Streamable HTTP: één endpoint dat POST accepteert, één HTTP request per JSON-RPC message, en een Accept header die zowel application/json als text/event-stream moet noemen omdat de server per request kiest met welke van de twee hij antwoordt.5 Hoofdstuk 14 parseerde die event stream met de hand, dus niets in de wire format is nieuw — alleen wat eromheen zit. Drie verplichtingen van de huidige revisie zijn makkelijk te missen en alle drie zijn testbaar:
De version header moet overeenkomen met de body
Link naar de sectie: De version header moet overeenkomen met de bodyElke POST draagt MCP-Protocol-Version, en de value moet overeenkomen met de protocolVersion binnen de eigen _meta van de request. Een mismatch is een 400 met een header-mismatch error, geen schouderophalen.5
Nog twee headers zijn vereist voor compliance
Link naar de sectie: Nog twee headers zijn vereist voor complianceMcp-Method spiegelt de method op elke request; Mcp-Name spiegelt params.name of params.uri op tools/call, resources/read en prompts/get. Ze bestaan zodat een proxy kan routen zonder bodies te parsen.5
De oude vormen zijn weg, en antwoorden met een weigering
Link naar de sectie: De oude vormen zijn weg, en antwoorden met een weigeringDe GET stream, Mcp-Session-Id en Last-Event-ID resumption zijn allemaal verwijderd. Een server die alleen deze revisie spreekt, moet 405 Method Not Allowed antwoorden op een GET of DELETE, een session header negeren zonder er een te minten, en Last-Event-ID negeren.5
Nu de meting die het hele hoofdstuk anders kadert. Stuur een current-revision request naar elke 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)"}}De constants komen overeen met het behaviour: Python SDK’s LATEST_PROTOCOL_VERSION leest 2026-07-28, TypeScript SDK’s leest 2025-11-25. Stuur de header-mismatch request uit de stap hierboven en de Python-server antwoordt 400 met error -32020 en het bericht “mcp-protocol-version header does not match the request envelope's protocol version”; de TypeScript SDK heeft zulke code niet, omdat hij de revisie die dit definieert niet implementeert.
De pagina die beide als Tier 1 noemt, zegt ook “Each SDK provides the same functionality”.1 Op de datum hieronder is die zin voor de huidige revisie aspirational. Check LATEST_PROTOCOL_VERSION in de SDK die je gaat installeren; het is één regel, en de enige claim in dit hoofdstuk die over een jaar nog belangrijk is.
De 401, en de zin om te citeren
Link naar de sectie: De 401, en de zin om te citerenVerplaats een server van je laptop en de client van een vreemde komt langs met een token. Dit is de helft die Hoofdstuk 26 liet liggen en de helft die een multi-user product niet kan overslaan.
De specification zet de MCP server in een OAuth 2.1-rol en geeft hem een naam: een protected MCP server is een resource server, de client is een OAuth client, en de authorization server is iemand anders’ probleem.4 Vanuit die rol volgen vier verplichte clausules, volledig geciteerd omdat parafraseren precies is hoe de fout ontstaat:
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” is de anti-passthrough-regel, en daarom bestaat het hele audience-apparaat. Een server die de bearer token die hij kreeg doorspeelt naar een third-party API is een confused deputy: hij leent zijn eigen trust uit aan wie hem ook aanriep. De regel verbiedt hergebruik, niet alleen opslag.
Dat afdwingbaar maken kost vier RFC’s, elk met één taak.6 RFC 9728 is hoe de client überhaupt de authorization server vindt: de MCP server serveert een protected-resource-metadata document en een 401 wijst ernaar. RFC 8707 is de resource parameter — de client moet de canonical URI van de server meesturen in zowel de authorization request als de token request, “regardless of whether authorization servers support it”, zodat de uitgegeven token zijn audience noemt. RFC 9207 sluit de lus vanaf de andere kant: de client registreert de issuer vóór redirect en vergelijkt de teruggegeven iss als exacte string, zonder normalisatie — geen case folding, geen default-port elision, geen trailing slash. En RFC 7591, Dynamic Client Registration, is nu deprecated ten gunste van Client ID Metadata Documents, “retained for backwards compatibility with authorization servers that do not support” them.4
Wire dat op beide servers met een token verifier die niets doet behalve de audience checken. De 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"}Beide SDK’s serven dat document en beide wijzen er een 401 naartoe, en dat is het volledige discovery-verhaal: een client die je server nooit heeft gezien, leert waar hij moet authenticeren uit een weigering. De 403 is iets anders — de token is prima, de scope niet — en de challenge noemt wat ontbreekt zodat de client kan opschalen in plaats van opnieuw te beginnen.
Twee treden verschillen, en geen van beide verschillen staat in de specification. De TypeScript SDK weigert een token met geen expiry claim; de Python-versie retourneert 200, omdat expires_at optioneel is op zijn AccessToken en None “geen mening” betekent. En de Python 403 draagt error_description="Required scope: incidents:read" zonder de scope parameter die de specification zegt dat servers zouden moeten opnemen. Een verifier is geen plek om een library default te accepteren: de audience check is in beide talen van jou om te schrijven, en de expiry ook.
Eén eerlijke nit uit dezelfde run. Een GET op het endpoint antwoordde 404 op de Express-wiring en 400 Bad Request: Missing session ID op de Python-wiring, waar de specification om 405 Method Not Allowed vraagt en waar “session ID” vocabulary is dat deze revisie heeft verwijderd. Geen van beide is gevaarlijk; allebei zijn ze de vorm van een ecosysteem midden in een migratie.
Waar servers echt leven
Link naar de sectie: Waar servers echt levenHet laatste stuk van shipping is waar je publiceert, en daarop is er een antwoord met een getal. Vandaag gecrawld, elke server in de officiële registry op zijn laatste versie:7
| servers | |
|---|---|
| totaal (laatste versie, niet verwijderd) | 28.170 |
| actief / deprecated | 27.853 / 317 |
| shipt minstens één installeerbaar package | 13.065 |
| alleen remote — een URL, niets te installeren | 14.696 |
| npm | 8.275 |
| PyPI | 3.603 |
| OCI images | 867 |
mcpb bundles | 706 |
| NuGet / Cargo | 107 / 43 |
Twee lezingen, in tegengestelde richtingen. Qua gepubliceerde servers leidt npm met 2,3 tegen 1 — het getal dat mensen citeren wanneer ze zeggen dat het ecosysteem TypeScript is. Qua downloads leidt Python: in de afgelopen dertig dagen haalde mcp 286,7 miljoen tegenover @modelcontextprotocol/sdk met 194,7 miljoen, nog vóór fastmcp met 72,1 miljoen erbij komt.7 Beide zijn Tier 1, het normatieve schema is een schema.ts, en de officiële tutorial “Build an MCP server” opent op de Python-tab.1 Welke helft je ook in je hoofd had, de andere helft is ook waar.
En de rij die belangrijker is dan allebei: meer dan de helft van de registry — 14.696 van 28.170 — heeft niets om te installeren. Dat zijn web services. De transport-tellingen bevestigen het vanaf de andere kant: van 14.290 package entries declareren er 13.787 stdio; van 16.640 remote entries declareren er 15.570 Streamable HTTP en 1.070 nog de deprecated HTTP+SSE. Dus “een MCP server is een subprocess op je laptop” beschrijft een krimpende minderheid, en elk van die 14.696 heeft de sectie hierboven nodig in plaats van een environment variable.
Details tonen
Bewust tweetalig, en het precedent ervoor.
Dit is het enige tweetalige hoofdstuk in de course, omdat het eerlijke antwoord splitst: de registry is npm-first en de downloads zijn Python-first, tegelijk, vandaag. Eén van de twee schrijven zou de helft van de vraag weggeven en het ecosysteem ondertussen verkeerd beschrijven. Er is een precedent in het openbaar — de Hugging Face MCP Course noemt onder prerequisites “Experience with at least one programming language (Python or TypeScript examples will be shown)”, en onderwijst beide.8 Een protocol waarvan de hele waarde het aantal implementaties is, is een slechte plek om monolinguaal te zijn.
Gedateerde sectie: alles hierboven met een houdbaarheidsdatum
Link naar de sectie: Gedateerde sectie: alles hierboven met een houdbaarheidsdatumGelezen en gemeten op 7 september 2026, tegen protocolrevisie 2026-07-28.
| waarde | |
|---|---|
@modelcontextprotocol/sdk | 1.30.0, gepubliceerd 27 juli 2026; 4.322.438 bytes unpacked, 693 bestanden, 17 directe dependencies |
| laatste revisie die hij implementeert | 2025-11-25 |
mcp (PyPI) | 2.1.1, gepubliceerd 25 augustus 2026; wheel van 357.912 bytes, plus mcp-types 2.1.1 met 69.656 bytes |
| laatste revisie die hij implementeert | 2026-07-28 |
| SDK-tiers | TypeScript, Python, C#, Go, Rust op Tier 1; Java, Ruby op Tier 2; Swift, PHP, Kotlin op Tier 3 |
| registry servers | 28.170 |
| downloads, laatste 30 dagen | mcp 286.653.871 · fastmcp 72.097.269 · @modelcontextprotocol/sdk 194.679.333 |
Eén migratienotitie die geen getal is. In mcp 2.x werd FastMCP hernoemd naar MCPServer, en bijna elke tutorial online opent nog met de oude import. De SDK shipt een module waarvan het enige doel is om dat uit te leggen, de meest attente deprecation in dit hoofdstuk:
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.Dus welke
Link naar de sectie: Dus welkeMet de tabel voor je is de aanbeveling saai, en dat is een goed teken.
Als de server leeft binnen een web application die je al runt, schrijf hem in TypeScript. Hetzelfde proces, dezelfde deploy, dezelfde request handler; Streamable HTTP is een endpoint dat je naast de andere toevoegt; en de 13,9 MiB en de 145 ms zijn gratis omdat de runtime al draaide. Dat zijn de meeste van de 14.696 remote servers.
Als de server data tooling wrapt, schrijf hem in Python. Wat je blootstelt is pandas, een warehouse client, de transforms van een notebook, en een server in een andere taal zou een subprocess call zijn met een schema aan. Zevenhonderd milliseconden import in een service die één keer start is geen cost; in een subprocess die een host de hele dag opnieuw launcht, is het dat wel.
En voorlopig overrulet de revisierij beide. Als je 2026-07-28 nodig hebt — multi-round-trip requests, resultType, cache hints, server/discover — heeft één van de twee SDK’s het vandaag en de andere niet.
Waar dit hierna heen gaat
Link naar de sectie: Waar dit hierna heen gaatJe kunt nu dezelfde server in beide talen shippen, de keuze verdedigen met een tabel in plaats van een voorkeur, hem over beide live transports draaien en hem een token geven die hij weigert.
Wat je hebt gebouwd is nog steeds een function: een schema, een endpoint, een deterministic ding dat het model aanroept. Een hele klasse knowledge past niet in die vorm — hoe wij een postmortem schrijven, welke velden onze incident reports nodig hebben, de volgorde waarin we dingen doen en waarom. Het is procedure, het is prose, en het in een tool description proppen is hoe system prompts groeien tot tweeduizend tokens die je op elke afzonderlijke turn betaalt, of het gesprek nu over incidents gaat of niet.
Hoofdstuk 28 is het andere antwoord: een folder met een SKILL.md erin die het model leest in plaats van callt, geladen in drie niveaus zodat het referentiemateriaal bijna niets kost tot de turn waarin het nodig is. Het heeft geen hoofdtaal, en dat is het eerste wat het leert.
Bronnen en methode
Link naar de sectie: Bronnen en methodeAlles hier is gemeten op 7 september 2026, op Node 22.22.3 en Python 3.14.4, tegen @modelcontextprotocol/sdk 1.30.0 met zod 3.25.76 en mcp 2.1.1, elk geïnstalleerd in een eigen wegwerpdirectory. Timings zijn medianen van 25 launches, wall clock van spawn tot de regel met de tools/list response; token counts zijn o200k_base via tiktoken over de JSON van elke definition. Er is geen betaalde API aangeroepen: niets hier heeft een model nodig.
De twee servers zijn 81 en 63 niet-lege regels; één van hun drie tools is hierboven in beide talen gereproduceerd, en de andere vier registraties verschillen alleen zoals beschreven. Het error-disclosure policy van de Python SDK is geciteerd uit de docstrings van ToolError en UnexpectedToolError in mcp/server/mcpserver/exceptions.py; de pretty-printing default is pydantic_core.to_json(result, fallback=str, indent=2) in mcp/server/mcpserver/resources/types.py en utilities/func_metadata.py. De protocol-version constants zijn LATEST_PROTOCOL_VERSION in mcp_types/version.py en in de types.js van de TypeScript SDK, beide gelezen uit de geïnstalleerde packages in plaats van uit een changelog.
Referenties
Link naar de sectie: Referenties-
SDKs,
modelcontextprotocol.io/docs/sdk, en Build an MCP server,modelcontextprotocol.io/docs/develop/build-server, beide gelezen 7 september 2026. Bron van de tier table, van de zin “Each SDK provides the same functionality but follows the idioms and best practices of its language”, van de language-tab order van de tutorial (Python, TypeScript, Java, Kotlin, C#, Ruby, Rust, Go), en van de logging rule geciteerd overprint()enstdout. ↩ ↩2 ↩3 ↩4 -
stdio transport,
.../basic/transports/stdio. Bron van de newline framing en destdoutpurity rule. Hoofdstuk 26 leest deze pagina volledig; hij wordt hier geciteerd voor de regel die de kapotte server overtreedt. ↩ -
MCP Inspector,
modelcontextprotocol.io/docs/2026-07-28/tools/inspector, gelezen 7 september 2026. Eén package, drie clients achter één binary — web,--clien--tui— die één core, één set transports en één OAuth state op disk delen. De CLI produceerde de catalogue traces hier. ↩ -
Authorization,
modelcontextprotocol.io/specification/2026-07-28/basic/authorization, gelezen 7 september 2026. Bron van de resource-server role; de vier volledig geciteerde token-handling clauses; de requirement dat servers RFC 9728 implementeren en clients die gebruiken voor discovery; deresourceparameter rules en de canonical-URI definition; de issuer-validation table; de deprecation van Dynamic Client Registration; de401/403/400table en deinsufficient_scopechallenge; en de stdio exemption, “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, en Transports overview,.../basic/transports. Bron van de single-endpoint POST rule, de dualeAcceptrequirement, deMCP-Protocol-Versionheader en zijn must-match-the-body rule, deMcp-MethodenMcp-Nameheaders beschreven als “REQUIRED for compliance”, de removal van de GET stream, sessions enLast-Event-ID, de405guidance, de mandatoryOriginvalidation, en de classification van de 2024-11-05 HTTP+SSE transport als Deprecated onder SEP-2596. ↩ ↩2 ↩3 ↩4 -
De vier waarop de specification leunt, met de draft die hij profileert: The OAuth 2.1 Authorization Framework,
draft-ietf-oauth-v2-1-13. Campbell, B., Bradley, J. en Tschofenig, H., Resource Indicators for OAuth 2.0, RFC 8707, februari 2020 — deresourceparameter en de audience die hij bindt. Jones, M.B., Hunt, P. en Parecki, A., OAuth 2.0 Protected Resource Metadata, RFC 9728, april 2025 — het document waar een401naar wijst. Meyer zu Selhausen, K. en Fett, D., OAuth 2.0 Authorization Server Issuer Identification, RFC 9207, maart 2022 — deissparameter en de exact-string comparison. Richer, J. (ed.) et al., OAuth 2.0 Dynamic Client Registration Protocol, RFC 7591, juli 2015, deprecated voor dit gebruik. En Jones, M. en Hardt, D., The OAuth 2.0 Authorization Framework: Bearer Token Usage, RFC 6750, oktober 2012, sectie 3, voor deWWW-Authenticatechallenge shape hierboven. ↩ -
Officiële MCP registry,
registry.modelcontextprotocol.io/v0/servers, gecrawld 7 september 2026 metversion=latest: 282 pagina’s, 28.170 servers, geteld doorregistryTypeover distinct server names. Download figures:api.npmjs.org/downloads/point/last-monthvoor@modelcontextprotocol/sdk(194.679.333 voor 8 augustus – 6 september 2026) enpypistats.org/api/packages/<name>/recentvoormcpenfastmcp, beide dezelfde dag gelezen. Package sizes komen uit het npm registry document en de PyPI JSON API. ↩ ↩2 -
MCP Course, Hugging Face,
huggingface.co/learn/mcp-course, unit 0, gelezen 7 september 2026: onder de prerequisites, “Experience with at least one programming language (Python or TypeScript examples will be shown)”. ↩