MCP Server shippen: TypeScript und Python im Messvergleich
Derselbe Server zweimal: drei Tools, eine Ressource, ein prompt. 94 Pakete gegen 28, Cold Start 145 ms gegen 709.
Auf dieser Seite
Hier ist das gesamte Sprachargument, gemessen, bevor auch nur ein Wort davon gemacht wird.
node ./incidents.js 144.5 ms
python incidents.py 709.4 ms
npx incidents-mcp 712.6 msDie ersten beiden Zeilen sind der Vergleich, den alle wollen. Die dritte Zeile ist derselbe TypeScript-Server aus der ersten Zeile, gestartet so, wie er tatsächlich verteilt würde — und er landet drei Millisekunden von Python entfernt.
Kapitel 26 hat das Model Context Protocol anhand seiner eigenen Spezifikation mit rohem JSON-RPC gelesen, weil rohes JSON-RPC keine Sprache hat. Dieses Kapitel hat zwei, und das Gewicht des Arguments liegt hier: derselbe Server, zweimal geschrieben. Drei Tools, eine Ressource, ein prompt, beide SDKs, keine Abkürzungen auf einer Seite. Dann die Transports, der Inspector, der 401 und die Zahlen, die niemand veröffentlicht hat.
Der Server, und warum diese fünf Dinge darin sind
Link zum Abschnitt: Der Server, und warum diese fünf Dinge darin sindEin Incident-Log. Drei Tools, weil die Trennung aus Kapitel 18 zwischen Reads und Writes sichtbar sein muss: search_incidents liest, open_incident schreibt und gibt ein Handle zurück, resolve_incident nimmt dieses Handle und schließt. Eine Ressource, incidents://open, weil das Lesen der aktuellen Liste etwas ist, das die Anwendung anhängt. Ein prompt, postmortem, weil „schreib das auf“ der Slash-Befehl einer Person ist. Das ist die Kontrollhierarchie aus Kapitel 26 — Modell, Anwendung, Person — in fünf Registrierungen übersetzt.
Das Handle ist wichtiger, als es aussieht. Kapitel 26 hat einen Spielzeugkalender kaputtgemacht, indem es seinen Zustand in einem Array auf Modulebene hielt: Das Protokoll hat keine Session, also gibt ein Creation-Tool einen opaken Identifier zurück und jeder spätere Call nimmt ihn als gewöhnliches Argument. Nichts in einer der beiden Dateien nimmt an, dass der Aufrufer der Prozess ist, der sie geöffnet hat.
Hier ist dasselbe Tool in beiden Sprachen, nebeneinander registriert:
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.")Lies zuerst, was gleich ist, denn das ist der Befund. Beide deklarieren einen Namen, eine Beschreibung, zwei beschriebene String-Argumente und drei Annotationen; beide sind eine Funktion; keines erwähnt JSON-RPC, Framing, stdout oder eine Protokollversion. Die beiden SDKs sind auf dieselbe Form konvergiert, und genau das soll „Tier 1“ bedeuten.1
Zwei Unterschiede sind real, und beide kommen später wieder. TypeScript beschreibt Argumente mit einer Schema-Library — hier Zod — und das Schema ist ein Wert, den du schreibst. Python beschreibt sie mit den Type Hints der Funktion selbst und liest sie zur Importzeit, weshalb es Dinge über die Funktion weiß, die die TypeScript-Datei ihm nie gesagt hat. Und der Fehlerpfad: TypeScript gibt ein Tool-Ergebnis mit isError zurück, Python wirft. Merk dir das.
Die anderen vier Registrierungen unterscheiden sich strukturell durch nichts. Die Ressource ist server.registerResource("open-incidents", "incidents://open", …) gegen @server.resource("incidents://open", …); der prompt ist registerPrompt gegen @server.prompt. Die letzte Zeile jeder Datei ist der Transport: await server.connect(new StdioServerTransport()) gegen server.run().
Ganze Dateien: 81 nicht-leere Zeilen und 3.060 Bytes TypeScript gegen 63 und 2.555. Nimm das mit der nötigen Prise Salz — Zeilenzahlen messen einen Formatter genauso sehr wie eine Sprache, weshalb keine der beiden Zahlen in der Headline-Tabelle unten steht.
Ein Client, beide Server
Link zum Abschnitt: Ein Client, beide ServerDer Beweis, dass die Sprache unsichtbar ist, ist ein Client, zweimal ausgeführt, in elf Zeilen:
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));Richte ihn nacheinander auf jeden Server. Echte Ausgabe, gekürzt:
$ 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}"}]Gleiche Tools, gleiche Reihenfolge, gleiches Handle. Ein TypeScript-Client kann nicht erkennen, worin der Server geschrieben ist, und er fragt nie danach. Das ist das ganze Versprechen eines Protokolls, das hier hält.
Schau jetzt auf die Whitespaces im zweiten Ergebnis, denn sie sind nicht kosmetisch: Das Python-SDK serialisiert Payloads mit pydantic_core.to_json(result, fallback=str, indent=2). Beim Lesen der Ressource mit zwei Incidents in der Liste hat der TypeScript-Body 136 Zeichen und 37 o200k_base tokens; der Python-Body hat 185 und 62. Achtundsechzig Prozent mehr tokens für identische Zeilen, bezahlt von demjenigen, der die Ressource in einen prompt liest, jedes Mal.
Der Katalog erzählt dieselbe Geschichte mit einer größeren Ursache. Beide Server, dieselben drei Tools, tools/list Schlüssel für Schlüssel gewogen:
| key | TypeScript | Python |
|---|---|---|
name | 21 | 21 |
description | 46 | 46 |
annotations | 46 | 46 |
inputSchema | 211 | 192 |
outputSchema | — | 187 |
execution | 27 | — |
| total | 342 | 480 |
Pythons Input-Schemas sind günstiger — TypeScripts Zod-Bridge stempelt auf jedes einen $schema und einen additionalProperties. Die gesamte Lücke von 138 tokens ist ein Output-Schema, das niemand geschrieben hat. resolve_incident ist mit -> Incident annotiert, also hat das SDK ein JSON Schema für den Rückgabetyp abgeleitet und mitgeschickt. Es ist wirklich nützlich — es ermöglicht einem Client, structuredContent zu validieren — und es sind 187 tokens deiner context window, die wegen eines Type Hint ankommen. Die Regel aus Kapitel 24, dass Definitionen das verdrängen, worauf es ankommt, gilt auch für Schemas, von denen du nicht wusstest, dass du sie hast.
Absichtlich kaputtmachen: die Fehlermeldung, die geleakt ist
Link zum Abschnitt: Absichtlich kaputtmachen: die Fehlermeldung, die geleakt istDie beiden Fehlerpfade oben sind keine Stilfrage. Gib jedem Server ein Tool, das so scheitert, wie eine echte Integration scheitert, und lies, was beim Modell ankommt.
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}Das TypeScript-SDK hat eine interne Adresse, einen Port, einen Datenbanknamen und ein Service Account in den Kontext des Modells gelegt. Das Python-SDK hat nichts davon dort abgelegt; der Traceback ging nach stderr und blieb auf dem Server.
Keines davon ist ein Bug. Beides sind Entscheidungen, und die Python-Entscheidung steht in ihrem eigenen Docstring: Ein ToolError ist „ein Fehler, den du erwartet hast“, und seine Nachricht wird „in content zurückgegeben, damit das Modell sie lesen kann“; alles andere „wird als Crash behandelt: Das Modell sieht nur Error executing tool <name>, und der Server loggt den Traceback unter ERROR“. Die Klasse für den Crash-Fall sagt den Rest laut — „nichts vom Original erreicht den Client“.
Beide Verhaltensweisen sind die Hälfte der Zeit falsch. Kapitel 18 argumentierte, dass ein Validierungsfehler als Tool-Ergebnis zurückkommen sollte, das das Modell lesen und korrigieren kann, weil das in den meisten Integrationen die Zeile mit dem höchsten Hebel ist; auf der Python-Seite erfordert das, explizit ToolError zu werfen, und ein nacktes ValueError wirft den nützlichen Satz weg. Das Argument aus Kapitel 30 läuft in die andere Richtung: Alles, was ein Tool zurückgibt, landet in einem Kontext, den eine spätere prompt injection wieder herauszulesen versuchen kann, und ein ungeprüfter Exception-String ist der am wenigsten auditierte Text in deinem System.
Die Regel, die beides überlebt: Entscheide pro Tool, was ein Fehler sagen darf, und schreib diesen String selbst. Lass niemals den Default-Text einer Exception entscheiden, in keiner der beiden Sprachen.
Absichtlich kaputtmachen: eine Zeile auf Standard Output
Link zum Abschnitt: Absichtlich kaputtmachen: eine Zeile auf Standard OutputDas offizielle Tutorial formuliert die Regel ohne Absicherung: „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 zitierte die normative Version — ein Server „MUST NOT write anything to its stdout that is not a valid MCP message“.2
Füge jedem Server eine Zeile hinzu und lies den 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 startingDer Python-Server ist schlimmer, und der Grund ist nicht MCP. Ein Prozess, dessen stdout eine Pipe statt ein Terminal ist, bekommt einen blockgepufferten Stream, also wird die Streuzeile geflusht, wann immer der Buffer entscheidet — hier beim Exit, nach einer Antwort, vor der sie geschrieben wurde. Die Korruption erscheint nicht dort, wo der Bug ist. Füge flush=True hinzu, oder eine Library, die flusht, und sie wandert.
Dann der Teil, der erklärt, warum so etwas shipped. Füttere den kaputten Server an drei 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 warningDer Sieben-Zeilen-Parser stirbt sofort. Der offizielle Client und der Inspector zucken beide mit den Schultern — sie überspringen die Zeile und machen weiter. Eine Regel, die nur die Clients bricht, die niemand benutzt, erreicht die Produktion intakt; deshalb lohnt es sich, sie hier absichtlich zu brechen, statt im Log eines Kunden.
Der CLI-Modus des Inspector ist die Hälfte, die vergessen wird: npx @modelcontextprotocol/inspector --cli <command> --method tools/list druckt einen Katalog und beendet sich, wodurch er scriptbar wird auf eine Weise, die die Browser-UI nicht ist.3
Die Tabelle
Link zum Abschnitt: Die TabelleBeide SDKs wurden sauber installiert, in eigene Verzeichnisse, nichts geteilt:
| TypeScript | Python | |
|---|---|---|
| package | @modelcontextprotocol/sdk 1.30.0 + zod 3.25.76 | mcp 2.1.1 |
| latest protocol revision implemented | 2025-11-25 | 2026-07-28 |
| transitive packages installed | 94 | 28 |
| installed size | 13.9 MiB | 44.3 MiB |
| files on disk | 3,386 | 2,018 |
| third-party packages loaded to serve stdio | 8 of 94 | 18 of 28 |
| bare interpreter start, median | 19.4 ms | 11.1 ms |
spawn → tools/list answered, median of 25 | 144.5 ms | 709.4 ms |
tools/list catalogue, o200k_base tokens | 342 | 480 |
Jede Zeile überrascht in eine andere Richtung, weshalb es sich lohnt, den Vergleich auszuführen, statt ihn anzunehmen.
TypeScript installiert mehr als dreimal so viele Pakete und weniger als ein Drittel der Bytes. 94 Dependencies sind das npm-Ökosystem, wie es eben ist — fast-deep-equal, es-errors, dunder-proto. Pythons 28 sind weniger und riesig: cryptography, pydantic-core und uvicorn sind kompilierte Artefakte. Wenn dein Instinkt sagt, dass die Anzahl der Dependencies das Problem ist, ist diese Zeile das Gegenbeispiel.
Pythons Interpreter startet schneller als Node, und zwar deutlich — 11,1 ms gegen 19,4 ms bei einem leeren Programm. Die 565 ms in der Cold-Start-Zeile sind also nicht die Sprache. Es ist das SDK, und die Zeile zu geladenen Paketen sagt warum:
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, …Ein Server, dessen einziges I/O eine Pipe ist, importiert einen ASGI-Webserver, einen HTTP-Client und eine TLS-Library, bevor er seine erste Zeile liest. Das TypeScript-SDK bringt Express, Hono, jose und eventsource ebenfalls mit — sie liegen ungelesen auf der Platte, weil die Package-Grenze sie aus einem server/stdio.js-Import heraushält. Pythons Package ist ein Import-Graph, also ist import mcp alles davon: python -X importtime schreibt 727 ms import mcp.server.mcpserver zu — ein Wert, der unter dem Import-Profiler gemessen wurde, weshalb er über den 709 ms liegt, die der unprofilierte Lauf von Spawn bis Antwort braucht — und 269 davon allein dem mcp.types-Subtree. Die Wire Types sind Pydantic-Modelle, eine Klasse pro Protokollnachricht pro Revision, und sie zu bauen ist Arbeit beim Import. Das ist ein Design Trade-off, keine Schlamperei — eager Imports sind der Grund, warum das Python-SDK dir in der nächsten Zeile run(transport="streamable-http") geben kann, ohne eine zweite Installation.
Und dann hebt die letzte Zeile des Eröffnungsblocks das Argument wieder auf. Packe den TypeScript-Server ordentlich — ein bin-Eintrag, ein Shebang, npm link, nichts herunterzuladen — und starte ihn über npx mit --no-install, also so, wie ein veröffentlichter stdio-Server tatsächlich gestartet wird:
node ./incidents.js 144.5 ms
npx incidents-mcp 712.6 ms (+568.1 ms of launcher)
python incidents.py 709.4 msDer Launcher kostet 568 ms pro Start — viereinhalbmal so viel wie der gesamte Import des TypeScript-SDK — und er wird bei jedem Launch bezahlt, weil ein MCP Host einen stdio-Server startet, indem er diesen Command ausführt. Die ehrliche Form von „TypeScript startet fünfmal schneller“ lautet also: tut es, bis du es auf die normale Weise verteilst. Dieselbe Einschränkung gilt vermutlich für uvx; auf dieser Maschine war kein uv installiert, also existiert diese Zeile nicht. Nichts Ungemessenes kommt in die Tabelle.
Zwei Transports, und nur zwei
Link zum Abschnitt: Zwei Transports, und nur zweiKapitel 26 behandelte stdios Framing. Zwei Dinge blieben für hier übrig.
Das erste: Einen Server mit npx oder uvx auszuführen ist der stdio-Transport. Es gibt keinen separaten „Package-Modus“. Die Konfiguration eines Hosts nennt einen Command und Argumente; der Host spawnt ihn und spricht über die Pipes. Deshalb sind „wie verteile ich das“ und „welchen Transport spricht es“ lokal eine Frage, und deshalb gehören die Kosten des Launchers in ein Kapitel über Shipping.
Das zweite: stdio hat überhaupt keinen Abschnitt zur Autorisierung, und die Spezifikation sagt das in einer Zeile — Implementierungen, die stdio verwenden, „SHOULD NOT follow this specification, and instead retrieve credentials from the environment“.4 Sein Sicherheitsmodell ist das des Betriebssystems, und seine Grenze auch: Ein lokaler Subprozess bedient genau eine Maschine und einen Benutzer.
Der andere Live-Transport ist Streamable HTTP: ein einzelner Endpoint, der POST akzeptiert, ein HTTP-Request pro JSON-RPC-Nachricht, und ein Accept-Header, der sowohl application/json als auch text/event-stream auflisten muss, weil der Server pro Request auswählt, mit welchem von beiden er antwortet.5 Kapitel 14 hat diesen Event Stream von Hand geparst, also ist am Wire Format nichts neu — nur das, was es umhüllt. Drei Pflichten der aktuellen Revision übersieht man leicht, und alle drei sind testbar:
Der Versions-Header muss zum Body passen
Link zum Abschnitt: Der Versions-Header muss zum Body passenJeder POST trägt MCP-Protocol-Version, und sein Wert muss zum protocolVersion im eigenen _meta des Requests passen. Ein Mismatch ist ein 400 mit einem Header-Mismatch-Fehler, kein Schulterzucken.5
Zwei weitere Header sind für Compliance erforderlich
Link zum Abschnitt: Zwei weitere Header sind für Compliance erforderlichMcp-Method spiegelt die Methode bei jedem Request; Mcp-Name spiegelt params.name oder params.uri bei tools/call, resources/read und prompts/get. Sie existieren, damit ein Proxy routen kann, ohne Bodys zu parsen.5
Die alten Formen sind weg und antworten mit Ablehnung
Link zum Abschnitt: Die alten Formen sind weg und antworten mit AblehnungDer GET-Stream, Mcp-Session-Id und Last-Event-ID-Resumption wurden alle entfernt. Ein Server, der nur diese Revision spricht, sollte auf GET oder DELETE mit 405 Method Not Allowed antworten, einen Session-Header ignorieren, ohne einen zu minten, und Last-Event-ID ignorieren.5
Jetzt die Messung, die das ganze Kapitel neu rahmt. Sende einen Request der aktuellen Revision per HTTP an jeden Server.
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)"}}Die Konstanten stimmen mit dem Verhalten überein: LATEST_PROTOCOL_VERSION des Python-SDK liest 2026-07-28, die des TypeScript-SDK liest 2025-11-25. Sende den Header-Mismatch-Request aus dem Schritt oben, und der Python-Server antwortet 400 mit Fehler -32020 und der Nachricht „mcp-protocol-version header does not match the request envelope's protocol version“; das TypeScript-SDK hat keinen solchen Code, weil es die Revision nicht implementiert, die ihn definiert.
Die Seite, die beide als Tier 1 listet, sagt auch „Each SDK provides the same functionality“.1 Am unten genannten Datum ist dieser Satz für die aktuelle Revision aspirational. Prüfe LATEST_PROTOCOL_VERSION im SDK, das du gleich installierst; es ist eine Zeile und die einzige Aussage in diesem Kapitel, die in einem Jahr noch zählen wird.
Der 401, und der Satz zum Zitieren
Link zum Abschnitt: Der 401, und der Satz zum ZitierenVerschiebe einen Server von deinem Laptop, und der Client eines Fremden taucht mit einem token auf. Das ist die Hälfte, die Kapitel 26 ausgelassen hat, und die Hälfte, die ein Multi-User-Produkt nicht überspringen kann.
Die Spezifikation setzt den MCP Server in eine OAuth-2.1-Rolle und benennt sie: Ein geschützter MCP Server ist ein Resource Server, der Client ist ein OAuth Client, und der Authorization Server ist das Problem von jemand anderem.4 Aus dieser Rolle folgen vier verpflichtende Klauseln, vollständig zitiert, weil Paraphrasieren genau der Weg ist, wie der Fehler entsteht:
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“ ist die Anti-Passthrough-Regel, und deshalb gibt es den ganzen Audience-Apparat. Ein Server, der den Bearer token, den man ihm gegeben hat, bei einer Third-Party-API wieder abspielt, ist ein confused deputy: Er verleiht sein eigenes Vertrauen an denjenigen, der ihn aufgerufen hat. Die Regel verbietet die Wiederverwendung, nicht nur die Speicherung.
Das erzwingbar zu machen braucht vier RFCs, jeweils mit einem Job.6 RFC 9728 ist, wie der Client den Authorization Server überhaupt findet: Der MCP Server stellt ein Protected-Resource-Metadata-Dokument bereit und ein 401 zeigt darauf. RFC 8707 ist der resource-Parameter — der Client muss die kanonische URI des Servers sowohl im Authorization Request als auch im Token Request senden, „regardless of whether authorization servers support it“, damit der ausgegebene token seine Audience benennt. RFC 9207 schließt die Schleife von der anderen Seite: Der Client notiert den Issuer vor dem Redirect und vergleicht den zurückgegebenen iss als exakten String, ohne Normalisierung — kein Case Folding, kein Weglassen von Default-Ports, kein trailing Slash. Und RFC 7591, Dynamic Client Registration, ist jetzt zugunsten von Client ID Metadata Documents deprecated, „retained for backwards compatibility with authorization servers that do not support“ them.4
Verdrahte das auf beiden Servern mit einem Token-Verifier, der nichts tut, außer die Audience zu prüfen. Die TypeScript-Leiter:
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 SDKs stellen dieses Dokument bereit, und beide lassen ein 401 darauf zeigen, was die gesamte Discovery-Story ist: Ein Client, der deinen Server noch nie gesehen hat, lernt aus einer Ablehnung, wo er sich authentifizieren muss. Der 403 ist ein anderes Tier — der token ist in Ordnung, der Scope nicht — und die Challenge benennt, was fehlt, damit der Client nachlegen kann, statt von vorne anzufangen.
Zwei Sprossen unterscheiden sich, und keiner der Unterschiede steht in der Spezifikation. Das TypeScript-SDK lehnt einen token mit keinem Expiry-Claim ab; das Python-SDK gibt 200 zurück, weil expires_at auf seinem AccessToken optional ist und None „keine Meinung“ bedeutet. Und Pythons 403 trägt error_description="Required scope: incidents:read" ohne den scope-Parameter, den die Spezifikation Servern empfiehlt. Ein Verifier ist kein Ort, um einen Library-Default zu akzeptieren: Der Audience-Check ist in beiden Sprachen deiner, und der Expiry-Check auch.
Ein ehrlicher Nit aus demselben Lauf. Ein GET auf den Endpoint antwortete 404 beim Express-Wiring und 400 Bad Request: Missing session ID beim Python-Wiring, wo die Spezifikation 405 Method Not Allowed verlangt und wo „Session ID“ Vokabular ist, das diese Revision entfernt hat. Nichts davon ist gefährlich; beides ist die Form eines Ökosystems mitten in der Migration.
Wo Server tatsächlich leben
Link zum Abschnitt: Wo Server tatsächlich lebenDas letzte Stück beim Shipping ist, wo du veröffentlichst, und es hat eine Antwort mit einer Zahl. Heute gecrawlt, jeder Server im offiziellen Registry in seiner neuesten Version:7
| servers | |
|---|---|
| total (latest version, not deleted) | 28,170 |
| active / deprecated | 27,853 / 317 |
| ship at least one installable package | 13,065 |
| remote only — a URL, nothing to install | 14,696 |
| npm | 8,275 |
| PyPI | 3,603 |
| OCI images | 867 |
mcpb bundles | 706 |
| NuGet / Cargo | 107 / 43 |
Zwei Lesarten, die in entgegengesetzte Richtungen zeigen. Nach veröffentlichten Servern führt npm 2,3 zu 1 — die Zahl, die Leute zitieren, wenn sie sagen, das Ökosystem sei TypeScript. Nach Downloads führt Python: In den letzten dreißig Tagen kam mcp auf 286,7 Millionen gegen @modelcontextprotocol/sdk mit 194,7 Millionen, bevor fastmcp mit 72,1 Millionen dazukommt.7 Beide sind Tier 1, das normative Schema ist ein schema.ts, und das offizielle Tutorial „Build an MCP server“ öffnet auf dem Python-Tab.1 Welche Hälfte davon du auch im Kopf hattest, die andere Hälfte stimmt ebenfalls.
Und die Zeile, die wichtiger ist als beide: Mehr als die Hälfte des Registry — 14.696 von 28.170 — hat nichts zu installieren. Das sind Web Services. Die Transport-Zählungen bestätigen es von der anderen Seite: Von 14.290 Package-Einträgen deklarieren 13.787 stdio; von 16.640 Remote-Einträgen deklarieren 15.570 Streamable HTTP und 1.070 noch das deprecated HTTP+SSE. „Ein MCP Server ist ein Subprozess auf deinem Laptop“ beschreibt also eine schrumpfende Minderheit, und jeder einzelne der 14.696 braucht den Abschnitt oben statt einer Umgebungsvariable.
Details anzeigen
Absichtlich zweisprachig, und der Präzedenzfall dafür.
Das ist das einzige zweisprachige Kapitel im Kurs, weil die ehrliche Antwort sich teilt: Das Registry ist npm-first und die Downloads sind Python-first, gleichzeitig, heute. Nur eine der beiden Sprachen zu schreiben würde die Hälfte der Frage verschenken und dabei das Ökosystem falsch beschreiben. Es gibt einen offenen Präzedenzfall — der Hugging Face MCP Course listet unter seinen Voraussetzungen „Experience with at least one programming language (Python or TypeScript examples will be shown)“ und lehrt beides.8 Ein Protokoll, dessen ganzer Wert in der Zahl seiner Implementierungen liegt, ist ein schlechter Ort für Einsprachigkeit.
Datierter Abschnitt: alles oben, was ein Haltbarkeitsdatum hat
Link zum Abschnitt: Datierter Abschnitt: alles oben, was ein Haltbarkeitsdatum hatGelesen und gemessen am 7. September 2026, gegen Protokollrevision 2026-07-28.
| value | |
|---|---|
@modelcontextprotocol/sdk | 1.30.0, published 27 July 2026; 4,322,438 bytes unpacked, 693 files, 17 direct dependencies |
| latest revision it implements | 2025-11-25 |
mcp (PyPI) | 2.1.1, published 25 August 2026; 357,912-byte wheel, plus mcp-types 2.1.1 at 69,656 bytes |
| latest revision it implements | 2026-07-28 |
| SDK tiers | TypeScript, Python, C#, Go, Rust at Tier 1; Java, Ruby at Tier 2; Swift, PHP, Kotlin at Tier 3 |
| registry servers | 28,170 |
| downloads, last 30 days | mcp 286,653,871 · fastmcp 72,097,269 · @modelcontextprotocol/sdk 194,679,333 |
Eine Migrationsnotiz, die keine Zahl ist. In mcp 2.x wurde FastMCP in MCPServer umbenannt, und fast jedes Tutorial online beginnt noch mit dem alten Import. Das SDK liefert ein Modul mit, dessen einziger Zweck darin besteht, das zu erklären; das ist die rücksichtsvollste Deprecation in diesem 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.Also welches
Link zum Abschnitt: Also welchesMit der Tabelle vor dir ist die Empfehlung langweilig, was ein gutes Zeichen ist.
Wenn der Server in einer Webanwendung lebt, die du bereits betreibst, schreib ihn in TypeScript. Derselbe Prozess, derselbe deploy, derselbe Request Handler; Streamable HTTP ist ein Endpoint, den du neben die anderen setzt; und die 13,9 MiB und die 145 ms sind gratis, weil die Runtime ohnehin schon lief. Das sind die meisten der 14.696 Remote-Server.
Wenn der Server Data Tooling wrappt, schreib ihn in Python. Was du exponierst, ist pandas, ein Warehouse-Client, ein Notebook voller Transforms, und ein Server in einer anderen Sprache wäre ein Subprozess-Call mit Schema als Verkleidung. Siebenhundert Millisekunden Import in einem Service, der einmal startet, sind kein Kostenpunkt; in einem Subprozess, den ein Host den ganzen Tag neu startet, schon.
Und vorerst sticht die Revision-Zeile beides. Wenn du 2026-07-28 brauchst — Multi-Round-Trip-Requests, resultType, Cache-Hints, server/discover — hat eines der beiden SDKs das heute, das andere nicht.
Wohin es als Nächstes geht
Link zum Abschnitt: Wohin es als Nächstes gehtDu kannst jetzt denselben Server in beiden Sprachen shippen, die Wahl mit einer Tabelle statt einer Vorliebe verteidigen, ihn über beide Live-Transports laufen lassen und ihm einen token geben, den er ablehnen wird.
Was du gebaut hast, ist immer noch eine Funktion: ein Schema, ein Endpoint, eine deterministische Sache, die das Modell aufruft. Eine ganze Klasse von Wissen passt nicht in diese Form — wie wir ein Postmortem schreiben, welche Felder unsere Incident Reports brauchen, in welcher Reihenfolge wir Dinge tun und warum. Es ist Prozedur, es ist Prosa, und es in eine Tool-Beschreibung zu zwingen, ist der Weg, wie System-prompts auf zweitausend tokens wachsen, die bei jedem einzelnen Turn bezahlt werden, egal ob es im Gespräch um Incidents geht oder nicht.
Kapitel 28 ist die andere Antwort: ein Ordner mit einem SKILL.md darin, den das Modell liest, statt ihn aufzurufen, in drei Ebenen geladen, sodass das Referenzmaterial fast nichts kostet, bis es in diesem Turn gebraucht wird. Es hat keine Hauptsprache, und das ist das Erste, was es lehrt.
Quellen und Methode
Link zum Abschnitt: Quellen und MethodeAlles hier wurde am 7. September 2026 auf Node 22.22.3 und Python 3.14.4 gemessen, gegen @modelcontextprotocol/sdk 1.30.0 mit zod 3.25.76 und mcp 2.1.1, jeweils in ein eigenes Wegwerfverzeichnis installiert. Timings sind Mediane aus 25 Starts, Wall Clock von spawn bis zur Zeile mit der tools/list-Antwort; token-Zählungen sind o200k_base via tiktoken über das JSON jeder Definition. Keine bezahlte API wurde aufgerufen: Nichts hier braucht ein Modell.
Die beiden Server haben 81 und 63 nicht-leere Zeilen; eines ihrer drei Tools ist oben in beiden Sprachen reproduziert, und die anderen vier Registrierungen unterscheiden sich nur wie beschrieben. Die Error-Disclosure-Policy des Python-SDK wird aus den Docstrings von ToolError und UnexpectedToolError in mcp/server/mcpserver/exceptions.py zitiert; der Pretty-Printing-Default ist pydantic_core.to_json(result, fallback=str, indent=2) in mcp/server/mcpserver/resources/types.py und utilities/func_metadata.py. Die Protokollversionskonstanten sind LATEST_PROTOCOL_VERSION in mcp_types/version.py und im types.js des TypeScript-SDK, beide aus den installierten Packages gelesen und nicht aus einem Changelog.
Referenzen
Link zum Abschnitt: Referenzen-
SDKs,
modelcontextprotocol.io/docs/sdk, und Build an MCP server,modelcontextprotocol.io/docs/develop/build-server, beide gelesen am 7. September 2026. Quelle der Tier-Tabelle, des Satzes „Each SDK provides the same functionality but follows the idioms and best practices of its language“, der Sprach-Tab-Reihenfolge des Tutorials (Python, TypeScript, Java, Kotlin, C#, Ruby, Rust, Go) und der zitierten Logging-Regel zuprint()undstdout. ↩ ↩2 ↩3 ↩4 -
stdio transport,
.../basic/transports/stdio. Quelle des Newline-Framings und derstdout-Purity-Regel. Kapitel 26 liest diese Seite vollständig; sie wird hier für die Zeile zitiert, gegen die der kaputte Server verstößt. ↩ -
MCP Inspector,
modelcontextprotocol.io/docs/2026-07-28/tools/inspector, gelesen am 7. September 2026. Ein Package, drei Clients hinter einem Binary — Web,--cliund--tui— die einen Core, ein Set von Transports und einen OAuth-Zustand auf Disk teilen. Die CLI erzeugte die Katalog-Traces hier. ↩ -
Authorization,
modelcontextprotocol.io/specification/2026-07-28/basic/authorization, gelesen am 7. September 2026. Quelle der Resource-Server-Rolle; der vier vollständig zitierten Token-Handling-Klauseln; der Anforderung, dass Server RFC 9728 implementieren und Clients es für Discovery verwenden; derresource-Parameterregeln und der Definition der kanonischen URI; der Issuer-Validation-Tabelle; der Deprecation von Dynamic Client Registration; der Tabelle401/403/400und derinsufficient_scope-Challenge; sowie der stdio-Ausnahme: „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, und Transports overview,.../basic/transports. Quelle der Single-Endpoint-POST-Regel, der dualenAccept-Anforderung, desMCP-Protocol-Version-Headers und seiner Must-match-the-body-Regel, der als „REQUIRED for compliance“ beschriebenen HeaderMcp-MethodundMcp-Name, der Entfernung des GET-Streams, von Sessions undLast-Event-ID, der405-Guidance, der verpflichtendenOrigin-Validation und der Einstufung des HTTP+SSE-Transports von 2024-11-05 als Deprecated unter SEP-2596. ↩ ↩2 ↩3 ↩4 -
Die vier, auf die sich die Spezifikation stützt, mit dem Draft, den sie profiliert: 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, Februar 2020 — derresource-Parameter und die Audience, die er bindet. Jones, M.B., Hunt, P. and Parecki, A., OAuth 2.0 Protected Resource Metadata, RFC 9728, April 2025 — das Dokument, auf das ein401zeigt. Meyer zu Selhausen, K. and Fett, D., OAuth 2.0 Authorization Server Issuer Identification, RFC 9207, März 2022 — deriss-Parameter und der Exact-String-Vergleich. Richer, J. (ed.) et al., OAuth 2.0 Dynamic Client Registration Protocol, RFC 7591, Juli 2015, für diese Nutzung deprecated. Und Jones, M. and Hardt, D., The OAuth 2.0 Authorization Framework: Bearer Token Usage, RFC 6750, Oktober 2012, Abschnitt 3, für dieWWW-Authenticate-Challenge-Form oben. ↩ -
Offizielles MCP Registry,
registry.modelcontextprotocol.io/v0/servers, gecrawlt am 7. September 2026 mitversion=latest: 282 Seiten, 28.170 Server, gezählt mitregistryTypeüber unterschiedliche Servernamen. Download-Zahlen:api.npmjs.org/downloads/point/last-monthfür@modelcontextprotocol/sdk(194.679.333 für 8. August – 6. September 2026) undpypistats.org/api/packages/<name>/recentfürmcpundfastmcp, beide am selben Tag gelesen. Package-Größen stammen aus dem npm-Registry-Dokument und der PyPI JSON API. ↩ ↩2 -
MCP Course, Hugging Face,
huggingface.co/learn/mcp-course, Unit 0, gelesen am 7. September 2026: unter den Voraussetzungen „Experience with at least one programming language (Python or TypeScript examples will be shown)“. ↩