Ves al contingut
27/30Capítol 27 de 30

Publica un servidor MCP: TypeScript i Python, mesurats

El mateix servidor escrit dues vegades — tres eines, un recurs, un prompt — i pesat: 94 paquets contra 28, cold start de 145 ms contra 709.

En aquesta pàgina

Aquí tens tot l’argument sobre el llenguatge, mesurat, abans de formular-ne ni una paraula.

spawn → tools/list answered, median of 25 launchesTEXT
node ./incidents.js         144.5 ms
python incidents.py         709.4 ms
npx incidents-mcp           712.6 ms

Les dues primeres línies són la comparació que tothom vol. La tercera línia és el mateix servidor TypeScript de la primera, llançat de la manera com realment es distribuiria, i queda a tres mil·lisegons de Python.

El capítol 26 va llegir el Model Context Protocol contra la seva pròpia especificació amb JSON-RPC cru, perquè el JSON-RPC cru no té llenguatge. Aquest capítol en té dos, i el pes de l’argument cau aquí: el mateix servidor, escrit dues vegades. Tres eines, un recurs, un prompt, tots dos SDKs, sense dreceres a cap banda. Després, els transports, l’inspector, el 401 i les xifres que ningú no ha publicat.

El servidor, i per què hi ha aquestes cinc coses

Enllaç a la secció: El servidor, i per què hi ha aquestes cinc coses

Un registre d’incidències. Tres eines, perquè la separació del capítol 18 entre lectures i escriptures ha de ser visible: search_incidents llegeix, open_incident escriu i retorna un identificador, resolve_incident agafa aquest identificador i tanca. Un recurs, incidents://open, perquè llegir la llista actual és una cosa que hi adjunta l’aplicació. Un prompt, postmortem, perquè «redacta això» és l’ordre de barra d’una persona. Aquesta és la jerarquia de control del capítol 26 — model, aplicació, persona — convertida en cinc registres.

L’identificador importa més del que sembla. El capítol 26 va trencar un calendari de joguina mantenint-ne l’estat en un array a nivell de mòdul: el protocol no té sessió, així que una eina de creació retorna un identificador opac i cada crida posterior el rep com un argument ordinari. Res en cap dels dos fitxers no assumeix que qui crida sigui el procés que l’ha obert.

Aquí tens la mateixa eina en tots dos llenguatges, registrada una al costat de l’altra:

incidents.tsTS
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 }) }] };
  },
);
incidents.pyPYTHON
@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.")

Llegeix primer què és igual, perquè aquesta és la troballa. Tots dos declaren un nom, una descripció, dos arguments de cadena descrits i tres anotacions; tots dos són una sola funció; cap no esmenta JSON-RPC, framing, stdout ni cap versió de protocol. Els dos SDKs han convergit en la mateixa forma, que és el que se suposa que ha de voler dir «Tier 1».1

Hi ha dues diferències reals, i totes dues tornen més endavant. TypeScript descriu els arguments amb una biblioteca d’esquemes — aquí, Zod — i l’esquema és un valor que escrius. Python els descriu amb els type hints propis de la funció i els llegeix en temps d’importació, per això sap coses sobre la funció que el fitxer TypeScript no li ha dit mai. I el camí d’error: TypeScript retorna un resultat d’eina amb isError, Python llença una excepció. Guarda-ho.

Els altres quatre registres no difereixen en res estructural. El recurs és server.registerResource("open-incidents", "incidents://open", …) contra @server.resource("incidents://open", …); el prompt és registerPrompt contra @server.prompt. L’última línia de cada fitxer és el transport: await server.connect(new StdioServerTransport()) contra server.run().

Fitxers sencers: 81 línies no buides i 3.060 bytes de TypeScript contra 63 i 2.555. Pren-t’ho amb la sal que es mereix: els recomptes de línies mesuren tant un formatador com un llenguatge, i per això cap de les dues xifres no és a la taula de titulars de sota.

La prova que el llenguatge és invisible és un client executat dues vegades, en onze línies:

client.tsTS
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));

Apunta’l a cada servidor per torns. Sortida real, retallada:

TEXT
$ 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}"}]

Mateixes eines, mateix ordre, mateix identificador. Un client TypeScript no pot saber en què està escrit el servidor, i no ho pregunta mai. Aquesta és tota la promesa d’un protocol, complerta.

Ara mira els espais en blanc del segon resultat, perquè no és cosmètic: l’SDK de Python serialitza payloads amb pydantic_core.to_json(result, fallback=str, indent=2). En la lectura del recurs amb dues incidències a la llista, el cos de TypeScript té 136 caràcters i 37 tokens o200k_base; el cos de Python en té 185 i 62. Un seixanta-vuit per cent més de tokens per a files idèntiques, pagats per qui llegeixi el recurs dins d’un prompt, cada vegada.

El catàleg explica la mateixa història amb una causa més gran. Tots dos servidors, les mateixes tres eines, tools/list pesat clau per clau:

keyTypeScriptPython
name2121
description4646
annotations4646
inputSchema211192
outputSchema187
execution27
total342480

Els esquemes d’entrada de Python són més barats: el pont Zod de TypeScript hi estampa un $schema i un additionalProperties a cadascun. Tot el buit de 138 tokens és un esquema de sortida que ningú no va escriure. resolve_incident està anotat com a -> Incident, així que l’SDK va derivar un JSON Schema per al tipus de retorn i el va enviar. És realment útil — és el que permet que un client validi structuredContent — i són 187 tokens de la teva context window que arriben per culpa d’un type hint. La regla del capítol 24 sobre les definicions que desplacen el material que importa també s’aplica als esquemes que no sabies que tenies.

Trenca-ho expressament: el missatge d’error que es va filtrar

Enllaç a la secció: Trenca-ho expressament: el missatge d’error que es va filtrar

Els dos camins d’error de dalt no són una tria d’estil. Dona a cada servidor una eina que falli com falla una integració real, i llegeix què arriba al model.

tools/call on a tool that raisesTEXT
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}

L’SDK de TypeScript va posar una adreça interna, un port, un nom de base de dades i un compte de servei dins del context del model. L’SDK de Python no hi va posar res d’això; el traceback va anar a stderr i es va quedar al servidor.

Cap de les dues coses és un bug. Totes dues són decisions, i la de Python està escrita en el seu propi docstring: un ToolError és «un error que havies previst» i el seu missatge es retorna «a content perquè el model el llegeixi»; qualsevol altra cosa «es tracta com una fallada: el model només veu Error executing tool <name>, i el servidor registra el traceback a ERROR». La classe per al cas de fallada diu la resta en veu alta: «res de l’original arriba al client».

Tots dos comportaments són incorrectes la meitat del temps. El capítol 18 sostenia que un error de validació hauria de tornar com un resultat d’eina que el model pugui llegir i corregir, perquè aquesta és la línia de màxim palanquejament en la majoria d’integracions; al costat de Python això exigeix llençar ToolError explícitament, i un ValueError nu llença a les escombraries la frase útil. L’argument del capítol 30 va en l’altra direcció: tot el que retorna una eina acaba en un context que una prompt injection posterior pot intentar llegir, i una cadena d’excepció no revisada és el text menys auditat del teu sistema.

La regla que sobreviu a totes dues coses: decideix, eina per eina, què pot dir una fallada, i escriu tu mateix aquesta cadena. No deixis mai que el text per defecte d’una excepció decideixi, en cap llenguatge.

Trenca-ho expressament: una línia a la sortida estàndard

Enllaç a la secció: Trenca-ho expressament: una línia a la sortida estàndard

El tutorial oficial enuncia la regla sense matisos: «Per als servidors basats en STDIO: no escriguis mai a stdout. Escriure a stdout corromprà els missatges JSON-RPC i trencarà el servidor. La funció print() escriu a stdout per defecte, així que mantén-la completament fora d’un servidor STDIO.»1 El capítol 26 en citava la versió normativa: un servidor «MUST NOT write anything to its stdout that is not a valid MCP message».2

Afegeix una línia a cada servidor i llegeix el flux cru:

raw stdout, first two linesTEXT
TypeScript  incidents server starting
            {"result":{"protocolVersion":"2025-11-25", … },"jsonrpc":"2.0","id":1}

Python      {"jsonrpc":"2.0","id":1,"result":{ … }}
            incidents server starting

El de Python és pitjor, i la raó no és MCP. Un procés el stdout del qual és una canonada en lloc d’un terminal obté un flux amb buffer de blocs, així que la línia intrusa es buida quan el buffer decideix: aquí, en sortir, després d’una resposta que havia estat escrita abans. La corrupció no apareix on és el bug. Afegeix flush=True, o una biblioteca que faci flush, i es mou.

Després ve la part que explica per què això arriba a producció. Alimenta tres clients amb el servidor trencat:

TEXT
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 warning

El parser de set línies mor immediatament. El client oficial i l’Inspector s’ho empassen: salten la línia i continuen. Una regla que només trenca els clients que ningú no fa servir és una regla que arriba intacta a producció, i per això val la pena trencar-la expressament aquí i no pas al log d’un client.

El mode CLI de l’Inspector és la meitat que s’oblida: npx @modelcontextprotocol/inspector --cli <command> --method tools/list imprimeix un catàleg i surt, cosa que el fa scriptable d’una manera que la UI de navegador no ho és.3

Tots dos SDKs es van instal·lar netament, cadascun al seu directori, sense res compartit:

TypeScriptPython
package@modelcontextprotocol/sdk 1.30.0 + zod 3.25.76mcp 2.1.1
darrera revisió del protocol implementada2025-11-252026-07-28
paquets transitius instal·lats9428
mida instal·lada13.9 MiB44.3 MiB
fitxers al disc3.3862.018
paquets de tercers carregats per servir stdio8 de 9418 de 28
arrencada de l’intèrpret nu, mediana19.4 ms11.1 ms
spawn → tools/list respost, mediana de 25144.5 ms709.4 ms
catàleg tools/list, tokens o200k_base342480

Cada fila sorprèn en una direcció diferent, i per això val la pena executar la comparació en lloc de suposar-la.

TypeScript instal·la més del triple de paquets i menys d’un terç dels bytes. 94 dependències és l’ecosistema npm fent de les seves: fast-deep-equal, es-errors, dunder-proto. Les 28 de Python són menys i enormes: cryptography, pydantic-core i uvicorn són artefactes compilats. Si el teu instint és que el nombre de dependències és el que ha de preocupar-te, aquesta fila és el contraexemple.

L’intèrpret de Python arrenca més ràpid que Node, i no és ni a prop: 11.1 ms contra 19.4 ms en un programa buit. Així que els 565 ms de la fila de cold start no són el llenguatge. És l’SDK, i la fila de paquets carregats diu per què:

third-party modules loaded to answer one tools/list over stdioTEXT
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 servidor l’únic I/O del qual és una canonada importa un servidor web ASGI, un client HTTP i una biblioteca TLS abans de llegir la primera línia. L’SDK de TypeScript també porta Express, Hono, jose i eventsource, però es queden al disc sense llegir, perquè el límit del paquet els deixa fora d’una importació server/stdio.js. El paquet de Python és un sol graf d’importació, així que import mcp és tot: python -X importtime atribueix 727 ms a import mcp.server.mcpserver — una xifra mesurada sota el profiler d’importació, i per això surt per sobre dels 709 ms que triga l’execució sense profiler des de l’spawn fins a respondre — i 269 d’aquests ms al subarbre mcp.types tot sol: els tipus de cable són models Pydantic, una classe per missatge de protocol i per revisió, i construir-los és feina feta en importar. Això és una decisió de disseny, no deixadesa: les importacions ansioses són el motiu pel qual l’SDK de Python et pot donar run(transport="streamable-http") a la línia següent sense una segona instal·lació.

I llavors l’última fila del bloc inicial desfà l’argument. Empaqueta bé el servidor TypeScript — una entrada bin, un shebang, npm link, res per descarregar — i llança’l amb npx i --no-install, que és com s’inicia realment un servidor stdio publicat:

median of 25, spawn → tools/list answeredTEXT
node ./incidents.js       144.5 ms
npx incidents-mcp         712.6 ms      (+568.1 ms of launcher)
python incidents.py       709.4 ms

El launcher costa 568 ms per arrencada — quatre vegades i mitja tota la importació de l’SDK de TypeScript — i es paga a cada llançament, perquè un host MCP inicia un servidor stdio executant aquesta ordre. Així que la forma honesta de «TypeScript arrenca cinc vegades més ràpid» és: ho fa, fins que el distribueixes de la manera normal. El mateix advertiment presumiblement s’aplica a uvx; aquesta màquina no tenia cap uv instal·lat, així que aquesta fila no existeix. Res no mesurat no entra a la taula.

El capítol 26 va cobrir el framing de stdio. Va deixar dues coses per aquí.

La primera: executar un servidor amb npx o uvx és el transport stdio. No hi ha cap «mode paquet» separat. La configuració d’un host anomena una ordre i uns arguments; l’host l’executa i parla per les canonades. Per això «com ho distribueixo» i «quin transport parla» són una sola pregunta en local, i per això el cost del launcher pertany a un capítol sobre publicar.

La segona: stdio no té cap secció d’autorització, i l’especificació ho diu en una línia: les implementacions que fan servir stdio «SHOULD NOT follow this specification, and instead retrieve credentials from the environment».4 El seu model de seguretat és el del sistema operatiu, i també ho és el seu límit: un subprocess local serveix exactament una màquina i un usuari.

L’altre transport viu és Streamable HTTP: un únic endpoint que accepta POST, una petició HTTP per missatge JSON-RPC, i una capçalera Accept que ha de llistar tant application/json com text/event-stream perquè el servidor tria per petició amb quin dels dos respon.5 El capítol 14 va analitzar aquest event stream a mà, així que no hi ha res nou en el format de cable: només el que l’embolcalla. Tres obligacions de la revisió actual són fàcils de passar per alt, i totes tres són comprovables:

La capçalera de versió ha de concordar amb el cos

Enllaç a la secció: La capçalera de versió ha de concordar amb el cos

Cada POST porta MCP-Protocol-Version, i el seu valor ha de coincidir amb el protocolVersion dins del _meta propi de la petició. Una discrepància és un 400 amb un error de capçalera no coincident, no una arronsada d’espatlles.5

Calen dues capçaleres més per complir l’especificació

Enllaç a la secció: Calen dues capçaleres més per complir l’especificació

Mcp-Method reflecteix el mètode en cada petició; Mcp-Name reflecteix params.name o params.uri a tools/call, resources/read i prompts/get. Existeixen perquè un proxy pugui encaminar sense analitzar cossos.5

Les formes antigues han desaparegut, i responen amb una negativa

Enllaç a la secció: Les formes antigues han desaparegut, i responen amb una negativa

El stream GET, Mcp-Session-Id i la represa Last-Event-ID es van eliminar. Un servidor que només parla aquesta revisió hauria de respondre 405 Method Not Allowed a un GET o DELETE, ignorar una capçalera de sessió sense encunyar-ne cap, i ignorar Last-Event-ID.5

Ara la mesura que reformula tot el capítol. Envia una petició de la revisió actual a cada servidor per HTTP.

POST /mcp, MCP-Protocol-Version: 2026-07-28TEXT
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 constants concorden amb el comportament: el LATEST_PROTOCOL_VERSION de l’SDK de Python diu 2026-07-28, el de l’SDK de TypeScript diu 2025-11-25. Envia la petició amb capçalera no coincident del pas anterior i el servidor Python respon 400 amb l’error -32020 i el missatge «mcp-protocol-version header does not match the request envelope's protocol version»; l’SDK de TypeScript no té aquest codi, perquè no implementa la revisió que el defineix.

La pàgina que els llista tots dos a Tier 1 també diu «Each SDK provides the same functionality».1 En la data de sota, per a la revisió actual, aquesta frase és aspiracional. Comprova LATEST_PROTOCOL_VERSION a l’SDK que estàs a punt d’instal·lar; és una línia, i l’única afirmació d’aquest capítol que encara importarà d’aquí a un any.

Mou un servidor fora del teu portàtil i apareix el client d’un desconegut amb un token. Aquesta és la meitat que el capítol 26 va deixar de banda i la meitat que un producte multiusuari no pot saltar-se.

L’especificació posa el servidor MCP en un rol OAuth 2.1 i l’anomena: un servidor MCP protegit és un servidor de recursos, el client és un client OAuth, i el servidor d’autorització és problema d’algú altre.4 D’aquest rol en surten quatre clàusules obligatòries, citades senceres perquè parafrasejar-les és com es comet l’error:

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» és la regla anti-passthrough, i és per això que existeix tot l’aparell d’audience. Un servidor que reenvia a una API de tercers el bearer token que li han donat és un deputy confús: presta la seva pròpia confiança a qui l’ha cridat. La regla prohibeix la reutilització, no només l’emmagatzematge.

Fer que això sigui aplicable requereix quatre RFCs, una feina cadascun.6 RFC 9728 és com el client troba el servidor d’autorització: el servidor MCP serveix un document de metadades de recurs protegit i un 401 hi apunta. RFC 8707 és el paràmetre resource: el client ha d’enviar l’URI canònic del servidor tant a la petició d’autorització com a la petició de token, «regardless of whether authorization servers support it», perquè el token emès indiqui la seva audience. RFC 9207 tanca el bucle des de l’altra banda: el client registra l’emissor abans de redirigir i compara el iss retornat com una cadena exacta, sense normalització: ni plegat de majúscules/minúscules, ni elisió de port per defecte, ni barra final. I RFC 7591, Dynamic Client Registration, ara està deprecada en favor dels Client ID Metadata Documents, «retained for backwards compatibility with authorization servers that do not support» them.4

Connecta-ho en tots dos servidors amb un verificador de token que no faci res més que comprovar l’audience. L’escala de TypeScript:

POST /mcp — TypeScript, with requireBearerAuthTEXT
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":[…]}}
GET /.well-known/oauth-protected-resource/mcpTEXT
{"resource":"http://127.0.0.1:8931/mcp",
 "authorization_servers":["https://auth.example.com/"],
 "scopes_supported":["incidents:read","incidents:write"],
 "resource_name":"Incidents"}

Tots dos SDKs serveixen aquest document i tots dos hi fan apuntar un 401, que és tota la història de descoberta: un client que no ha vist mai el teu servidor aprèn on autenticar-se a partir d’una negativa. El 403 és una altra bèstia: el token és correcte, l’abast no; i el challenge anomena què falta perquè el client pugui pujar de nivell en lloc de tornar a començar.

Dos esglaons difereixen, i cap diferència no és a l’especificació. L’SDK de TypeScript rebutja un token sense claim d’expiració; el de Python retorna 200, perquè expires_at és opcional al seu AccessToken i None vol dir «cap opinió». I el 403 de Python porta error_description="Required scope: incidents:read" sense el paràmetre scope que l’especificació diu que els servidors haurien d’incloure. Un verificador no és lloc per acceptar el valor per defecte d’una biblioteca: la comprovació d’audience l’has d’escriure tu en qualsevol llenguatge, i l’expiració també.

Una pega honesta de la mateixa execució. Un GET a l’endpoint va respondre 404 amb el cablejat d’Express i 400 Bad Request: Missing session ID amb el de Python, quan l’especificació demana 405 Method Not Allowed i on «session ID» és vocabulari que aquesta revisió va eliminar. Cap de les dues coses és perillosa; totes dues són la forma d’un ecosistema en plena migració.

L’última peça de publicar és on ho publiques, i té una resposta amb un número. Rastrejats avui, tots els servidors del registre oficial en la seva darrera versió:7

servidors
total (darrera versió, no eliminats)28.170
actius / deprecats27.853 / 317
publiquen almenys un paquet instal·lable13.065
només remots — una URL, res per instal·lar14.696
npm8.275
PyPI3.603
imatges OCI867
paquets mcpb706
NuGet / Cargo107 / 43

Dues lectures, en direccions oposades. Per servidors publicats, npm lidera 2,3 a 1: la xifra que la gent cita quan diu que l’ecosistema és TypeScript. Per descàrregues, lidera Python: durant els últims trenta dies mcp va tenir 286,7 milions contra @modelcontextprotocol/sdk amb 194,7 milions, abans de sumar-hi fastmcp amb 72,1 milions.7 Tots dos són Tier 1, l’esquema normatiu és un schema.ts, i el tutorial oficial «Build an MCP server» s’obre a la pestanya de Python.1 Sigui quina sigui la meitat que tenies al cap, l’altra meitat també és certa.

I la fila que importa més que qualsevol de les dues: més de la meitat del registre — 14.696 de 28.170 — no té res per instal·lar. Són serveis web. Els recomptes de transport coincideixen des de l’altre costat: de 14.290 entrades de paquet, 13.787 declaren stdio; de 16.640 entrades remotes, 15.570 declaren Streamable HTTP i 1.070 encara declaren l’HTTP+SSE deprecat. Així que «un servidor MCP és un subprocess al teu portàtil» descriu una minoria que s’encongeix, i cadascun d’aquests 14.696 necessita la secció anterior en lloc d’una variable d’entorn.

Mostra els detalls

Deliberadament bilingüe, i el precedent.

Aquest és l’únic capítol bilingüe del curs, perquè la resposta honesta es parteix: el registre és primer npm i les descàrregues són primer Python, alhora, avui. Escriure’n només un dels dos regalaria mitja pregunta i descriuria malament l’ecosistema mentre ho fa. Hi ha precedent en obert: el Hugging Face MCP Course llista entre els prerequisits «Experience with at least one programming language (Python or TypeScript examples will be shown)», i ensenya tots dos.8 Un protocol el valor sencer del qual és el nombre d’implementacions és un mal lloc per ser monolingüe.

Secció datada: tot el de dalt que té data de caducitat

Enllaç a la secció: Secció datada: tot el de dalt que té data de caducitat

Llegit i mesurat el 7 de setembre de 2026, contra la revisió de protocol 2026-07-28.

valor
@modelcontextprotocol/sdk1.30.0, publicat el 27 de juliol de 2026; 4.322.438 bytes desempaquetats, 693 fitxers, 17 dependències directes
darrera revisió que implementa2025-11-25
mcp (PyPI)2.1.1, publicat el 25 d’agost de 2026; wheel de 357.912 bytes, més mcp-types 2.1.1 amb 69.656 bytes
darrera revisió que implementa2026-07-28
nivells d’SDKTypeScript, Python, C#, Go, Rust a Tier 1; Java, Ruby a Tier 2; Swift, PHP, Kotlin a Tier 3
servidors del registre28.170
descàrregues, últims 30 diesmcp 286.653.871 · fastmcp 72.097.269 · @modelcontextprotocol/sdk 194.679.333

Una nota de migració que no és un número. A mcp 2.x, FastMCP es va reanomenar MCPServer, i gairebé tots els tutorials en línia encara s’obren amb l’import antic. L’SDK inclou un mòdul l’únic propòsit del qual és explicar això, que és la deprecació més considerada d’aquest capítol:

from mcp.server.fastmcp import FastMCPTEXT
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.

Amb la taula al davant, la recomanació és avorrida, que és un bon senyal.

Si el servidor viu dins d’una aplicació web que ja executes, escriu-lo en TypeScript. Mateix procés, mateix deploy, mateix gestor de peticions; Streamable HTTP és un endpoint que afegeixes al costat dels altres; i els 13.9 MiB i els 145 ms són de franc perquè el runtime ja estava aixecat. Això és la majoria dels 14.696 servidors remots.

Si el servidor embolcalla eines de dades, escriu-lo en Python. El que exposes és pandas, un client de warehouse, transformacions dignes d’un notebook, i un servidor en un altre llenguatge seria una crida de subprocess disfressada amb un esquema. Set-cents mil·lisegons d’importació en un servei que arrenca una vegada no són un cost; en un subprocess que un host rellança tot el dia, sí.

I, de moment, la fila de revisió passa per sobre de totes dues. Si necessites 2026-07-28 — peticions multi-round-trip, resultType, pistes de cache, server/discover — un dels dos SDKs ho té avui i l’altre no.

Ara pots publicar el mateix servidor en qualsevol dels dos llenguatges, defensar l’elecció amb una taula en lloc d’una preferència, executar-lo pels dos transports vius i donar-li un token que rebutjarà.

El que has construït continua sent una funció: un esquema, un endpoint, una cosa determinista que el model invoca. Tota una classe de coneixement no encaixa en aquesta forma: com nosaltres escrivim un postmortem, quins camps necessiten els nostres informes d’incidència, l’ordre en què fem les coses i per què. És procediment, és prosa, i forçar-ho dins d’una descripció d’eina és com els system prompts creixen fins a dos mil tokens pagats a cada torn, tant si la conversa va d’incidències com si no.

El capítol 28 és l’altra resposta: una carpeta amb un SKILL.md a dins que el model llegeix en lloc de cridar, carregada en tres nivells perquè el material de referència no costi gairebé res fins al torn en què cal. No té llenguatge principal, i això és el primer que ensenya.


Tot això es va mesurar el 7 de setembre de 2026, amb Node 22.22.3 i Python 3.14.4, contra @modelcontextprotocol/sdk 1.30.0 amb zod 3.25.76 i mcp 2.1.1, cadascun instal·lat al seu propi directori d’un sol ús. Els temps són medianes de 25 llançaments, temps de paret des de spawn fins a la línia que porta la resposta tools/list; els recomptes de tokens són o200k_base via tiktoken sobre el JSON de cada definició. No es va cridar cap API de pagament: res d’aquí no necessita un model.

Els dos servidors són 81 i 63 línies no buides; una de les seves tres eines es reprodueix més amunt en tots dos llenguatges, i els altres quatre registres només difereixen tal com s’ha descrit. La política de divulgació d’errors de l’SDK de Python se cita dels docstrings de ToolError i UnexpectedToolError a mcp/server/mcpserver/exceptions.py; el valor per defecte de pretty-printing és pydantic_core.to_json(result, fallback=str, indent=2) a mcp/server/mcpserver/resources/types.py i utilities/func_metadata.py. Les constants de versió de protocol són LATEST_PROTOCOL_VERSION a mcp_types/version.py i a types.js de l’SDK de TypeScript, totes dues llegides dels paquets instal·lats i no pas d’un changelog.

  1. SDKs, modelcontextprotocol.io/docs/sdk, i Build an MCP server, modelcontextprotocol.io/docs/develop/build-server, tots dos llegits el 7 de setembre de 2026. Font de la taula de nivells, de la frase «Each SDK provides the same functionality but follows the idioms and best practices of its language», de l’ordre de pestanyes de llenguatge del tutorial (Python, TypeScript, Java, Kotlin, C#, Ruby, Rust, Go), i de la regla de logging citada sobre print() i stdout. 2 3 4

  2. stdio transport, .../basic/transports/stdio. Font del framing per salts de línia i de la regla de puresa stdout. El capítol 26 llegeix aquesta pàgina sencera; se cita aquí per la línia que viola el servidor trencat.

  3. MCP Inspector, modelcontextprotocol.io/docs/2026-07-28/tools/inspector, llegit el 7 de setembre de 2026. Un paquet, tres clients darrere d’un binari — web, --cli i --tui — que comparteixen un core, un conjunt de transports i un estat OAuth al disc. La CLI va produir les traces de catàleg d’aquí.

  4. Authorization, modelcontextprotocol.io/specification/2026-07-28/basic/authorization, llegit el 7 de setembre de 2026. Font del rol de servidor de recursos; de les quatre clàusules de gestió de tokens citades íntegrament; del requisit que els servidors implementin RFC 9728 i que els clients el facin servir per a la descoberta; de les regles del paràmetre resource i la definició d’URI canònic; de la taula de validació d’emissor; de la deprecació de Dynamic Client Registration; de la taula 401/403/400 i el challenge insufficient_scope; i de l’exempció stdio, «Implementations using an STDIO transport SHOULD NOT follow this specification, and instead retrieve credentials from the environment.» 2 3 4

  5. Streamable HTTP, .../basic/transports/streamable-http, i Transports overview, .../basic/transports. Font de la regla POST d’endpoint únic, del requisit dual Accept, de la capçalera MCP-Protocol-Version i la seva regla de concordança obligatòria amb el cos, de les capçaleres Mcp-Method i Mcp-Name descrites com a «REQUIRED for compliance», de l’eliminació del stream GET, les sessions i Last-Event-ID, de la guia 405, de la validació obligatòria Origin i de la classificació del transport HTTP+SSE de 2024-11-05 com a Deprecated sota SEP-2596. 2 3 4

  6. Els quatre en què es recolza l’especificació, amb l’esborrany que perfila: The OAuth 2.1 Authorization Framework, draft-ietf-oauth-v2-1-13. Campbell, B., Bradley, J. i Tschofenig, H., Resource Indicators for OAuth 2.0, RFC 8707, febrer de 2020 — el paràmetre resource i l’audience que vincula. Jones, M.B., Hunt, P. i Parecki, A., OAuth 2.0 Protected Resource Metadata, RFC 9728, abril de 2025 — el document al qual apunta un 401. Meyer zu Selhausen, K. i Fett, D., OAuth 2.0 Authorization Server Issuer Identification, RFC 9207, març de 2022 — el paràmetre iss i la comparació de cadena exacta. Richer, J. (ed.) et al., OAuth 2.0 Dynamic Client Registration Protocol, RFC 7591, juliol de 2015, deprecat per a aquest ús. I Jones, M. i Hardt, D., The OAuth 2.0 Authorization Framework: Bearer Token Usage, RFC 6750, octubre de 2012, secció 3, per a la forma del challenge WWW-Authenticate de dalt.

  7. Registre oficial MCP, registry.modelcontextprotocol.io/v0/servers, rastrejat el 7 de setembre de 2026 amb version=latest: 282 pàgines, 28.170 servidors, comptats amb registryType sobre noms de servidor diferents. Xifres de descàrregues: api.npmjs.org/downloads/point/last-month per a @modelcontextprotocol/sdk (194.679.333 del 8 d’agost al 6 de setembre de 2026) i pypistats.org/api/packages/<name>/recent per a mcp i fastmcp, tots llegits el mateix dia. Les mides de paquets provenen del document del registre npm i de l’API JSON de PyPI. 2

  8. MCP Course, Hugging Face, huggingface.co/learn/mcp-course, unitat 0, llegit el 7 de setembre de 2026: entre els prerequisits, «Experience with at least one programming language (Python or TypeScript examples will be shown)».

A punt per deixar que triï LIA?

Crea amb tots els models d'IA en un sol lloc — comença gratis avui mateix.