Livrer un serveur MCP : TypeScript et Python, mesurés
Le même serveur écrit deux fois — trois outils, une ressource, un prompt — puis pesé : 94 packages contre 28, 145 ms contre 709.
Dans cet article
Voici tout l’argument sur le langage, mesuré, avant même qu’un mot soit écrit.
node ./incidents.js 144.5 ms
python incidents.py 709.4 ms
npx incidents-mcp 712.6 msLes deux premières lignes sont la comparaison que tout le monde veut. La troisième est le même serveur TypeScript que sur la première ligne, lancé comme il serait réellement distribué — et il arrive à trois millisecondes de Python.
Le chapitre 26 lisait le Model Context Protocol à l’aune de sa propre spécification avec du JSON-RPC brut, parce que le JSON-RPC brut n’a pas de langage. Ce chapitre en a deux, et le poids de l’argument se trouve ici : le même serveur, écrit deux fois. Trois outils, une ressource, un prompt, les deux SDK, aucun raccourci d’un côté comme de l’autre. Puis les transports, l’inspector, le 401 et les chiffres que personne n’a publiés.
Le serveur, et pourquoi il contient ces cinq éléments
Lien vers la section : Le serveur, et pourquoi il contient ces cinq élémentsUn journal d’incident. Trois outils, parce que la séparation du chapitre 18 entre lectures et écritures doit être visible : search_incidents lit, open_incident écrit et renvoie un identifiant, resolve_incident prend cet identifiant et clôture. Une ressource, incidents://open, parce que lire la liste actuelle est quelque chose que l’application attache. Un prompt, postmortem, parce que « rédige ceci » est une commande slash d’une personne. C’est la hiérarchie de contrôle du chapitre 26 — modèle, application, personne — transformée en cinq enregistrements.
L’identifiant compte plus qu’il n’y paraît. Le chapitre 26 cassait un calendrier jouet en gardant son état dans un tableau au niveau du module : le protocole n’a pas de session, donc un outil de création renvoie un identifiant opaque et chaque appel ultérieur le prend comme argument ordinaire. Rien, dans aucun des deux fichiers, ne suppose que l’appelant est le processus qui l’a ouvert.
Voici le même outil dans les deux langages, enregistré côte à côte :
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.")Lisez d’abord ce qui est identique, car c’est la découverte. Les deux déclarent un nom, une description, deux arguments string décrits et trois annotations ; les deux sont une seule fonction ; aucun ne mentionne JSON-RPC, le framing, stdout ni une version de protocole. Les deux SDK convergent vers la même forme, ce qui est bien ce que « Tier 1 » est censé signifier.1
Deux différences sont réelles et reviennent plus loin. TypeScript décrit les arguments avec une bibliothèque de schémas — Zod ici — et le schéma est une valeur que vous écrivez. Python les décrit avec les type hints de la fonction elle-même et les lit au moment de l’import, ce qui explique pourquoi il sait des choses sur la fonction que le fichier TypeScript ne lui a jamais dites. Et le chemin d’erreur : TypeScript renvoie un résultat d’outil avec isError, Python lève une exception. Gardez cela en tête.
Les quatre autres enregistrements ne diffèrent par rien de structurel. La ressource est server.registerResource("open-incidents", "incidents://open", …) contre @server.resource("incidents://open", …) ; le prompt est registerPrompt contre @server.prompt. La dernière ligne de chaque fichier est le transport : await server.connect(new StdioServerTransport()) contre server.run().
Fichiers complets : 81 lignes non vides et 3 060 octets de TypeScript contre 63 et 2 555. À prendre avec les réserves nécessaires — le nombre de lignes mesure autant un formateur qu’un langage, raison pour laquelle aucun de ces deux chiffres ne figure dans le tableau de tête ci-dessous.
Un client, deux serveurs
Lien vers la section : Un client, deux serveursLa preuve que le langage est invisible tient dans un client exécuté deux fois, en onze lignes :
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));Pointez-le vers chaque serveur à tour de rôle. Sortie réelle, raccourcie :
$ 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}"}]Mêmes outils, même ordre, même identifiant. Un client TypeScript ne peut pas savoir dans quel langage le serveur est écrit, et il ne le demande jamais. C’est toute la promesse d’un protocole, tenue.
Regardez maintenant les espaces dans le second résultat, car ce n’est pas cosmétique : le SDK Python sérialise les payloads avec pydantic_core.to_json(result, fallback=str, indent=2). Sur la lecture de ressource avec deux incidents dans la liste, le corps TypeScript fait 136 caractères et 37 tokens o200k_base ; le corps Python en fait 185 et 62. Soixante-huit pour cent de tokens en plus pour des lignes identiques, payés par quiconque lit la ressource dans un prompt, à chaque fois.
Le catalogue raconte la même histoire avec une cause plus large. Les deux serveurs, mêmes trois outils, tools/list pesé clé par clé :
| clé | TypeScript | Python |
|---|---|---|
name | 21 | 21 |
description | 46 | 46 |
annotations | 46 | 46 |
inputSchema | 211 | 192 |
outputSchema | — | 187 |
execution | 27 | — |
| total | 342 | 480 |
Les schémas input de Python sont moins coûteux — le pont Zod de TypeScript appose un $schema et un additionalProperties sur chacun. L’écart entier de 138 tokens est un schéma de sortie que personne n’a écrit. resolve_incident est annoté -> Incident, donc le SDK a dérivé un JSON Schema pour le type de retour et l’a expédié. C’est réellement utile — c’est ce qui permet à un client de valider structuredContent — et ce sont 187 tokens de votre context window qui arrivent à cause d’un type hint. La règle du chapitre 24 sur les définitions qui évincent la matière importante s’applique aux schémas dont vous ignoriez l’existence.
Le casser exprès : le message d’erreur qui a fuité
Lien vers la section : Le casser exprès : le message d’erreur qui a fuitéLes deux chemins d’erreur ci-dessus ne sont pas un choix de style. Donnez à chaque serveur un outil qui échoue comme une vraie intégration échoue, puis lisez ce qui atteint le modèle.
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}Le SDK TypeScript a placé une adresse interne, un port, un nom de base de données et un compte de service dans le contexte du modèle. Le SDK Python n’y a rien placé ; la traceback est allée dans stderr et est restée sur le serveur.
Aucun des deux n’est un bug. Ce sont deux décisions, et celle de Python est écrite dans sa propre docstring : un ToolError est « un échec que vous avez anticipé » et son message est renvoyé « dans content pour que le modèle le lise » ; tout le reste « est traité comme un crash : le modèle ne voit que Error executing tool <name>, et le serveur journalise la traceback dans ERROR ». La classe du cas de crash dit le reste tout haut — « rien de l’original n’atteint le client ».
Les deux comportements sont faux la moitié du temps. Le chapitre 18 soutenait qu’une erreur de validation devait revenir sous forme de résultat d’outil lisible et corrigeable par le modèle, parce que c’est la ligne à plus fort levier dans la plupart des intégrations ; côté Python, cela exige de lever explicitement ToolError, et un simple ValueError jette la phrase utile. L’argument du chapitre 30 va dans l’autre sens : tout ce qu’un outil renvoie atterrit dans un contexte qu’une prompt injection ultérieure peut tenter de relire, et une chaîne d’exception non relue est le texte le moins audité de votre système.
La règle qui survit aux deux : décidez, outil par outil, ce qu’un échec est autorisé à dire, et écrivez cette chaîne vous-même. Ne laissez jamais le texte par défaut d’une exception décider, dans aucun langage.
Le casser exprès : une ligne sur la sortie standard
Lien vers la section : Le casser exprès : une ligne sur la sortie standardLe tutoriel officiel énonce la règle sans nuance : « Pour les serveurs fondés sur STDIO : n’écrivez jamais sur stdout. Écrire sur stdout corrompra les messages JSON-RPC et cassera votre serveur. La fonction print() écrit sur stdout par défaut, donc gardez-la entièrement hors d’un serveur STDIO. »1 Le chapitre 26 citait la version normative — un serveur « MUST NOT write anything to its stdout that is not a valid MCP message ».2
Ajoutez une ligne à chaque serveur et lisez le flux brut :
TypeScript incidents server starting
{"result":{"protocolVersion":"2025-11-25", … },"jsonrpc":"2.0","id":1}
Python {"jsonrpc":"2.0","id":1,"result":{ … }}
incidents server startingCelui de Python est pire, et la raison n’est pas MCP. Un processus dont stdout est un pipe plutôt qu’un terminal obtient un flux block-buffered, donc la ligne parasite est flushée quand le buffer le décide — ici, à la sortie, après une réponse alors qu’elle avait été écrite avant. La corruption n’apparaît pas là où se trouve le bug. Ajoutez flush=True, ou une bibliothèque qui flush, et elle se déplace.
Puis vient la partie qui explique pourquoi cela part en production. Donnez le serveur cassé à trois 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 warningLe parseur de sept lignes meurt immédiatement. Le client officiel et l’Inspector haussent les épaules — ils sautent la ligne et continuent. Une règle qui ne casse que les clients que personne n’utilise est une règle qui arrive intacte en production, ce qui explique pourquoi elle mérite d’être cassée exprès ici plutôt que dans le log d’un client.
Le mode CLI de l’Inspector est la moitié qu’on oublie : npx @modelcontextprotocol/inspector --cli <command> --method tools/list imprime un catalogue et quitte, ce qui le rend scriptable d’une manière que l’interface navigateur ne l’est pas.3
Le tableau
Lien vers la section : Le tableauLes deux SDK se sont installés proprement, chacun dans son propre répertoire, rien de partagé :
| TypeScript | Python | |
|---|---|---|
| package | @modelcontextprotocol/sdk 1.30.0 + zod 3.25.76 | mcp 2.1.1 |
| dernière révision du protocole implémentée | 2025-11-25 | 2026-07-28 |
| packages transitifs installés | 94 | 28 |
| taille installée | 13,9 MiB | 44,3 MiB |
| fichiers sur disque | 3 386 | 2 018 |
| packages tiers chargés pour servir stdio | 8 sur 94 | 18 sur 28 |
| démarrage interpréteur nu, médiane | 19,4 ms | 11,1 ms |
spawn → tools/list répondu, médiane sur 25 | 144,5 ms | 709,4 ms |
catalogue tools/list, tokens o200k_base | 342 | 480 |
Chaque ligne surprend dans un sens différent, ce qui explique pourquoi la comparaison mérite d’être exécutée plutôt que supposée.
TypeScript installe plus de trois fois plus de packages et moins d’un tiers des octets. 94 dépendances, c’est l’écosystème npm fidèle à lui-même — fast-deep-equal, es-errors, dunder-proto. Les 28 de Python sont moins nombreux et énormes : cryptography, pydantic-core et uvicorn sont des artefacts compilés. Si votre intuition est que le nombre de dépendances est ce qui doit vous inquiéter, cette ligne est le contre-exemple.
L’interpréteur Python démarre plus vite que Node, et de loin — 11,1 ms contre 19,4 ms sur un programme vide. Donc les 565 ms de la ligne cold start ne viennent pas du langage. Elles viennent du SDK, et la ligne des packages chargés dit pourquoi :
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, …Un serveur dont la seule I/O est un pipe importe un serveur web ASGI, un client HTTP et une bibliothèque TLS avant de lire sa première ligne. Le SDK TypeScript embarque aussi Express, Hono, jose et eventsource — ils restent sur le disque sans être lus, parce que la frontière du package les tient hors d’un import server/stdio.js. Le package Python est un seul graphe d’import, donc import mcp correspond à tout : python -X importtime attribue 727 ms à import mcp.server.mcpserver — un chiffre mesuré sous import profiler, ce qui explique qu’il dépasse les 709 ms que l’exécution non profilée prend de spawn à réponse — et 269 d’entre elles au seul sous-arbre mcp.types — les types du fil sont des modèles Pydantic, une classe par message de protocole et par révision, et les construire est un travail effectué à l’import. C’est un compromis de conception, pas de la négligence — les imports eager sont la raison pour laquelle le SDK Python peut vous donner run(transport="streamable-http") à la ligne suivante sans seconde installation.
Et puis la dernière ligne du bloc d’ouverture défait l’argument. Packagez correctement le serveur TypeScript — une entrée bin, un shebang, npm link, rien à télécharger — et lancez-le via npx avec --no-install, ce qui est la façon dont un serveur stdio publié est réellement démarré :
node ./incidents.js 144.5 ms
npx incidents-mcp 712.6 ms (+568.1 ms of launcher)
python incidents.py 709.4 msLe launcher coûte 568 ms par démarrage — quatre fois et demie l’import complet du SDK TypeScript — et ce coût est payé à chaque lancement, parce qu’un hôte MCP démarre un serveur stdio en exécutant cette commande. Donc la forme honnête de « TypeScript démarre cinq fois plus vite » est : oui, jusqu’à ce que vous le distribuiez de la façon normale. La même réserve s’applique vraisemblablement à uvx ; cette machine n’avait pas uv installé, donc cette ligne n’existe pas. Rien de non mesuré n’entre dans le tableau.
Deux transports, et seulement deux
Lien vers la section : Deux transports, et seulement deuxLe chapitre 26 couvrait le framing de stdio. Deux choses restaient pour ici.
La première : exécuter un serveur avec npx ou uvx est le transport stdio. Il n’existe pas de « mode package » séparé. La configuration d’un hôte nomme une commande et des arguments ; l’hôte la lance et parle sur les pipes. C’est pourquoi « comment distribuer ceci » et « quel transport parle-t-il » sont localement une seule question, et pourquoi le coût du launcher a sa place dans un chapitre sur la livraison.
La seconde : stdio n’a aucune section d’autorisation, et la spécification le dit en une ligne — les implémentations utilisant stdio « SHOULD NOT follow this specification, and instead retrieve credentials from the environment ».4 Son modèle de sécurité est celui du système d’exploitation, et sa limite aussi : un sous-processus local sert exactement une machine et un utilisateur.
L’autre transport actif est Streamable HTTP : un endpoint unique qui accepte POST, une requête HTTP par message JSON-RPC, et un header Accept qui doit lister à la fois application/json et text/event-stream parce que le serveur choisit, pour chaque requête, avec lequel des deux il répond.5 Le chapitre 14 analysait ce flux d’événements à la main, donc rien dans le wire format n’est nouveau — seulement ce qui l’enveloppe. Trois obligations de la révision actuelle sont faciles à manquer et toutes trois sont testables :
Le header de version doit être d’accord avec le corps
Lien vers la section : Le header de version doit être d’accord avec le corpsChaque POST transporte MCP-Protocol-Version, et sa valeur doit correspondre au protocolVersion dans le _meta propre à la requête. Une incohérence est un 400 avec une erreur de header mismatch, pas un haussement d’épaules.5
Deux autres headers sont requis pour être conforme
Lien vers la section : Deux autres headers sont requis pour être conformeMcp-Method reflète la méthode sur chaque requête ; Mcp-Name reflète params.name ou params.uri sur tools/call, resources/read et prompts/get. Ils existent pour qu’un proxy puisse router sans analyser les corps.5
Les anciennes formes ont disparu, et répondent par un refus
Lien vers la section : Les anciennes formes ont disparu, et répondent par un refusLe flux GET, les sessions Mcp-Session-Id et la reprise Last-Event-ID ont tous été supprimés. Un serveur qui ne parle que cette révision devrait répondre 405 Method Not Allowed à un GET ou DELETE, ignorer un header de session sans en émettre un, et ignorer Last-Event-ID.5
Voici maintenant la mesure qui recadre tout le chapitre. Envoyez une requête de révision actuelle à chaque serveur en 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)"}}Les constantes concordent avec le comportement : le LATEST_PROTOCOL_VERSION du SDK Python lit 2026-07-28, celui du SDK TypeScript lit 2025-11-25. Envoyez la requête avec header mismatch de l’étape ci-dessus et le serveur Python répond 400 avec l’erreur -32020 et le message « mcp-protocol-version header does not match the request envelope's protocol version » ; le SDK TypeScript n’a pas ce code, parce qu’il n’implémente pas la révision qui le définit.
La page qui les liste tous deux en Tier 1 dit aussi « Each SDK provides the same functionality ».1 À la date ci-dessous, pour la révision actuelle, cette phrase est ambitieuse. Vérifiez LATEST_PROTOCOL_VERSION dans le SDK que vous êtes sur le point d’installer ; c’est une seule ligne, et la seule affirmation de ce chapitre qui comptera encore dans un an.
Le 401, et la phrase à citer
Lien vers la section : Le 401, et la phrase à citerDéplacez un serveur hors de votre laptop et le client d’un inconnu arrive avec un token. C’est la moitié que le chapitre 26 avait laissée de côté et celle qu’un produit multi-utilisateur ne peut pas ignorer.
La spécification place le serveur MCP dans un rôle OAuth 2.1 et le nomme : un serveur MCP protégé est un serveur de ressources, le client est un client OAuth, et le serveur d’autorisation est le problème de quelqu’un d’autre.4 De ce rôle découlent quatre clauses obligatoires, citées intégralement parce que les paraphraser est précisément l’erreur :
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 » est la règle anti-passthrough, et c’est pourquoi tout l’appareil d’audience existe. Un serveur qui rejoue le bearer token qu’on lui a remis auprès d’une API tierce est un confused deputy : il prête sa propre confiance à quiconque l’a appelé. La règle interdit la réutilisation, pas seulement le stockage.
Rendre cela applicable demande quatre RFC, chacune avec son rôle.6 RFC 9728 explique comment le client trouve le serveur d’autorisation tout court : le serveur MCP sert un document de métadonnées de ressource protégée et un 401 pointe vers lui. RFC 8707 est le paramètre resource — le client doit envoyer l’URI canonique du serveur dans la requête d’autorisation comme dans la requête de token, « regardless of whether authorization servers support it », afin que le token émis nomme son audience. RFC 9207 ferme la boucle par l’autre côté : le client enregistre l’émetteur avant la redirection et compare le iss renvoyé par chaîne exacte, sans normalisation — pas de changement de casse, pas d’élision de port par défaut, pas de slash final. Et RFC 7591, Dynamic Client Registration, est désormais déprécié au profit des Client ID Metadata Documents, « retained for backwards compatibility with authorization servers that do not support » them.4
Câblez cela sur les deux serveurs avec un vérificateur de token qui ne fait rien d’autre que vérifier l’audience. L’échelle TypeScript :
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"}Les deux SDK servent ce document et pointent tous deux un 401 vers lui, ce qui constitue toute l’histoire de découverte : un client qui n’a jamais vu votre serveur apprend où s’authentifier depuis un refus. Le 403 est un autre animal — le token est correct, le scope ne l’est pas — et le challenge nomme ce qui manque pour que le client puisse monter d’un cran plutôt que recommencer.
Deux barreaux diffèrent, et aucune différence n’est dans la spécification. Le SDK TypeScript refuse un token sans claim d’expiration ; celui de Python renvoie 200, parce que expires_at est optionnel sur son AccessToken et que None signifie « pas d’avis ». Et le 403 Python transporte error_description="Required scope: incidents:read" sans le paramètre scope que la spécification dit que les serveurs devraient inclure. Un vérificateur n’est pas un endroit où accepter un défaut de bibliothèque : le contrôle d’audience vous appartient dans les deux langages, et l’expiration aussi.
Un détail honnête de la même exécution. Un GET sur l’endpoint a répondu 404 sur le câblage Express et 400 Bad Request: Missing session ID sur celui de Python, là où la spécification demande 405 Method Not Allowed et où « session ID » est un vocabulaire que cette révision a supprimé. Aucun n’est dangereux ; tous deux ont la forme d’un écosystème en pleine migration.
Où les serveurs vivent vraiment
Lien vers la section : Où les serveurs vivent vraimentLa dernière pièce de la livraison est l’endroit où publier, et elle a une réponse chiffrée. Crawl aujourd’hui de chaque serveur du registre officiel à sa dernière version :7
| serveurs | |
|---|---|
| total (dernière version, non supprimés) | 28 170 |
| actifs / dépréciés | 27 853 / 317 |
| livrent au moins un package installable | 13 065 |
| remote uniquement — une URL, rien à installer | 14 696 |
| npm | 8 275 |
| PyPI | 3 603 |
| images OCI | 867 |
bundles mcpb | 706 |
| NuGet / Cargo | 107 / 43 |
Deux lectures, qui pointent en sens opposés. Par serveurs publiés, npm mène 2,3 à 1 — le chiffre que les gens citent quand ils disent que l’écosystème est TypeScript. Par téléchargements, Python mène : sur les trente derniers jours, mcp a atteint 286,7 millions contre 194,7 millions pour @modelcontextprotocol/sdk, avant d’ajouter fastmcp à 72,1 millions.7 Les deux sont Tier 1, le schéma normatif est un schema.ts, et le tutoriel officiel « Build an MCP server » s’ouvre sur l’onglet Python.1 Quelle que soit la moitié que vous aviez en tête, l’autre moitié est vraie aussi.
Et la ligne qui compte plus que les deux : plus de la moitié du registre — 14 696 sur 28 170 — n’a rien à installer. Ce sont des services web. Les décomptes de transport concordent par l’autre côté : sur 14 290 entrées de packages, 13 787 déclarent stdio ; sur 16 640 entrées remote, 15 570 déclarent Streamable HTTP et 1 070 déclarent encore HTTP+SSE, déprécié. Donc « un serveur MCP est un sous-processus sur votre laptop » décrit une minorité qui rétrécit, et chacun des 14 696 a besoin de la section ci-dessus plutôt que d’une variable d’environnement.
Afficher les détails
Délibérément bilingue, et le précédent qui le justifie.
C’est le seul chapitre bilingue du cours, parce que la réponse honnête se divise : le registre est d’abord npm et les téléchargements d’abord Python, en même temps, aujourd’hui. N’écrire que l’un des deux abandonnerait la moitié de la question et décrirait mal l’écosystème au passage. Il existe un précédent ouvert — le Hugging Face MCP Course liste parmi ses prérequis « Experience with at least one programming language (Python or TypeScript examples will be shown) », et enseigne les deux.8 Un protocole dont toute la valeur tient au nombre d’implémentations est un mauvais endroit pour être monolingue.
Section datée : tout ce qui précède a une durée de vie
Lien vers la section : Section datée : tout ce qui précède a une durée de vieLu et mesuré le 7 septembre 2026, face à la révision du protocole 2026-07-28.
| valeur | |
|---|---|
@modelcontextprotocol/sdk | 1.30.0, publié le 27 juillet 2026 ; 4 322 438 octets décompressés, 693 fichiers, 17 dépendances directes |
| dernière révision implémentée | 2025-11-25 |
mcp (PyPI) | 2.1.1, publié le 25 août 2026 ; wheel de 357 912 octets, plus mcp-types 2.1.1 à 69 656 octets |
| dernière révision implémentée | 2026-07-28 |
| niveaux des SDK | TypeScript, Python, C#, Go, Rust en Tier 1 ; Java, Ruby en Tier 2 ; Swift, PHP, Kotlin en Tier 3 |
| serveurs du registre | 28 170 |
| téléchargements, 30 derniers jours | mcp 286 653 871 · fastmcp 72 097 269 · @modelcontextprotocol/sdk 194 679 333 |
Une note de migration qui n’est pas un chiffre. Dans mcp 2.x, FastMCP a été renommé MCPServer, et presque tous les tutoriels en ligne s’ouvrent encore avec l’ancien import. Le SDK livre un module dont le seul but est d’expliquer cela, ce qui est la dépréciation la plus attentionnée de ce chapitre :
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.Alors lequel choisir
Lien vers la section : Alors lequel choisirAvec le tableau sous les yeux, la recommandation est ennuyeuse, ce qui est bon signe.
Si le serveur vit dans une application web que vous exploitez déjà, écrivez-le en TypeScript. Même processus, même deploy, même gestionnaire de requêtes ; Streamable HTTP est un endpoint que vous ajoutez à côté des autres ; et les 13,9 MiB comme les 145 ms sont gratuits parce que le runtime était déjà lancé. C’est la plupart des 14 696 serveurs remote.
Si le serveur enveloppe de l’outillage data, écrivez-le en Python. Ce que vous exposez est pandas, un client d’entrepôt, l’équivalent des transformations d’un notebook, et un serveur dans un autre langage serait un appel de sous-processus portant un schéma. Sept cents millisecondes d’import dans un service qui démarre une fois ne sont pas un coût ; dans un sous-processus qu’un hôte relance toute la journée, elles le sont.
Et pour l’instant, la ligne de révision l’emporte sur les deux. Si vous avez besoin de 2026-07-28 — requêtes à allers-retours multiples, resultType, cache hints, server/discover — l’un des deux SDK l’a aujourd’hui et l’autre non.
Où cela va ensuite
Lien vers la section : Où cela va ensuiteVous pouvez maintenant livrer le même serveur dans l’un ou l’autre langage, défendre le choix avec un tableau plutôt qu’une préférence, l’exécuter sur les deux transports actifs, et lui donner un token qu’il refusera.
Ce que vous avez construit reste une fonction : un schéma, un endpoint, une chose déterministe que le modèle invoque. Toute une classe de connaissances ne rentre pas dans cette forme — comment nous écrivons un postmortem, quels champs nos rapports d’incident exigent, l’ordre dans lequel nous faisons les choses et pourquoi. C’est une procédure, c’est de la prose, et la forcer dans une description d’outil est la manière dont les system prompts gonflent jusqu’à deux mille tokens payés à chaque tour, que la conversation parle ou non d’incidents.
Le chapitre 28 est l’autre réponse : un dossier avec un SKILL.md dedans que le modèle lit au lieu d’appeler, chargé en trois niveaux afin que le matériel de référence ne coûte presque rien jusqu’au tour où il devient nécessaire. Il n’a pas de langage principal, et c’est la première chose qu’il enseigne.
Sources et méthode
Lien vers la section : Sources et méthodeTout ceci a été mesuré le 7 septembre 2026, sur Node 22.22.3 et Python 3.14.4, face à @modelcontextprotocol/sdk 1.30.0 avec zod 3.25.76 et mcp 2.1.1, chacun installé dans son propre répertoire jetable. Les timings sont des médianes de 25 lancements, en temps mural de spawn jusqu’à la ligne portant la réponse tools/list ; les décomptes de tokens sont o200k_base via tiktoken sur le JSON de chaque définition. Aucune API payante n’a été appelée : rien ici n’a besoin d’un modèle.
Les deux serveurs font 81 et 63 lignes non vides ; l’un de leurs trois outils est reproduit ci-dessus dans les deux langages, et les quatre autres enregistrements ne diffèrent que comme décrit. La politique de divulgation d’erreurs du SDK Python est citée depuis les docstrings de ToolError et UnexpectedToolError dans mcp/server/mcpserver/exceptions.py ; le défaut de pretty-printing est pydantic_core.to_json(result, fallback=str, indent=2) dans mcp/server/mcpserver/resources/types.py et utilities/func_metadata.py. Les constantes de version de protocole sont LATEST_PROTOCOL_VERSION dans mcp_types/version.py et dans le types.js du SDK TypeScript, toutes deux lues dans les packages installés plutôt que dans un changelog.
Références
Lien vers la section : Références-
SDKs,
modelcontextprotocol.io/docs/sdk, et Build an MCP server,modelcontextprotocol.io/docs/develop/build-server, tous deux lus le 7 septembre 2026. Source du tableau des tiers, de la phrase « Each SDK provides the same functionality but follows the idioms and best practices of its language », de l’ordre des onglets de langage du tutoriel (Python, TypeScript, Java, Kotlin, C#, Ruby, Rust, Go), et de la règle de journalisation citée à propos deprint()etstdout. ↩ ↩2 ↩3 ↩4 -
stdio transport,
.../basic/transports/stdio. Source du framing par newline et de la règle de puretéstdout. Le chapitre 26 lit cette page en entier ; elle est citée ici pour la ligne que le serveur cassé viole. ↩ -
MCP Inspector,
modelcontextprotocol.io/docs/2026-07-28/tools/inspector, lu le 7 septembre 2026. Un package, trois clients derrière un binaire — web,--cliet--tui— partageant un cœur, un ensemble de transports et un état OAuth sur disque. La CLI a produit les traces de catalogue ici. ↩ -
Authorization,
modelcontextprotocol.io/specification/2026-07-28/basic/authorization, lu le 7 septembre 2026. Source du rôle de serveur de ressources ; des quatre clauses de traitement des tokens citées intégralement ; de l’exigence que les serveurs implémentent RFC 9728 et que les clients l’utilisent pour la découverte ; des règles du paramètreresourceet de la définition d’URI canonique ; du tableau de validation de l’émetteur ; de la dépréciation de Dynamic Client Registration ; du tableau401/403/400et du challengeinsufficient_scope; et de l’exemption stdio, « 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, et Transports overview,.../basic/transports. Source de la règle POST à endpoint unique, de l’exigence doubleAccept, du headerMCP-Protocol-Versionet de sa règle must-match-the-body, des headersMcp-MethodetMcp-Namedécrits comme « REQUIRED for compliance », de la suppression du flux GET, des sessions et deLast-Event-ID, du guidage405, de la validation obligatoireOrigin, et de la classification du transport HTTP+SSE 2024-11-05 comme Deprecated dans SEP-2596. ↩ ↩2 ↩3 ↩4 -
Les quatre sur lesquelles la spécification s’appuie, avec le draft qu’elle profile : The OAuth 2.1 Authorization Framework,
draft-ietf-oauth-v2-1-13. Campbell, B., Bradley, J. et Tschofenig, H., Resource Indicators for OAuth 2.0, RFC 8707, février 2020 — le paramètreresourceet l’audience qu’il lie. Jones, M.B., Hunt, P. et Parecki, A., OAuth 2.0 Protected Resource Metadata, RFC 9728, avril 2025 — le document vers lequel pointe un401. Meyer zu Selhausen, K. et Fett, D., OAuth 2.0 Authorization Server Issuer Identification, RFC 9207, mars 2022 — le paramètreisset la comparaison par chaîne exacte. Richer, J. (éd.) et al., OAuth 2.0 Dynamic Client Registration Protocol, RFC 7591, juillet 2015, déprécié pour cet usage. Et Jones, M. et Hardt, D., The OAuth 2.0 Authorization Framework: Bearer Token Usage, RFC 6750, octobre 2012, section 3, pour la forme du challengeWWW-Authenticateci-dessus. ↩ -
Registre MCP officiel,
registry.modelcontextprotocol.io/v0/servers, crawlé le 7 septembre 2026 avecversion=latest: 282 pages, 28 170 serveurs, comptés parregistryTypesur des noms de serveurs distincts. Chiffres de téléchargement :api.npmjs.org/downloads/point/last-monthpour@modelcontextprotocol/sdk(194 679 333 du 8 août au 6 septembre 2026) etpypistats.org/api/packages/<name>/recentpourmcpetfastmcp, tous lus le même jour. Les tailles de packages viennent du document de registre npm et de l’API JSON de PyPI. ↩ ↩2 -
MCP Course, Hugging Face,
huggingface.co/learn/mcp-course, unité 0, lu le 7 septembre 2026 : parmi les prérequis, « Experience with at least one programming language (Python or TypeScript examples will be shown) ». ↩