Saltar ao contido
27/30Capítulo 27 de 30

Publica un MCP server: TypeScript e Python, medidos

O mesmo server escrito dúas veces — tres ferramentas, un recurso, un prompt — e pesado: 94 packages fronte a 28; cold start de 145 ms fronte a 709.

Nesta páxina

Aquí está todo o argumento sobre a linguaxe, medido, antes de formular nin unha palabra.

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

As dúas primeiras liñas son a comparación que todo o mundo quere. A terceira liña é o mesmo server TypeScript da primeira, lanzado como se distribuiría de verdade — e queda a tres milisegundos de Python.

O capítulo 26 leu o Model Context Protocol contra a súa propia especificación con JSON-RPC en bruto, porque JSON-RPC en bruto non ten linguaxe. Este capítulo ten dúas, e o peso do argumento cae aquí: o mesmo server, escrito dúas veces. Tres ferramentas, un recurso, un prompt, ambos SDKs, sen atallos en ningún dos lados. Despois os transportes, o inspector, o 401 e os números que ninguén publicou.

O server, e por que leva estas cinco cousas dentro

Ligazón á sección: O server, e por que leva estas cinco cousas dentro

Un rexistro de incidencias. Tres ferramentas, porque a separación do capítulo 18 entre lecturas e escrituras ten que verse: search_incidents le, open_incident escribe e devolve un handle, resolve_incident toma ese handle e pecha. Un recurso, incidents://open, porque ler a lista actual é algo que achega a aplicación. Un prompt, postmortem, porque «escribe isto» é o comando slash dunha persoa. Esa é a xerarquía de control do capítulo 26 — modelo, aplicación, persoa — convertida en cinco rexistros.

O handle importa máis do que parece. O capítulo 26 rompeu un calendario de xoguete ao gardar o seu estado nun array a nivel de módulo: o protocolo non ten sesión, así que unha ferramenta de creación devolve un identificador opaco e cada chamada posterior recíbeo como un argumento normal. Nada en ningún dos dous ficheiros asume que quen chama é o proceso que o abriu.

Aquí está a mesma ferramenta nas dúas linguaxes, rexistrada lado a lado:

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.")

Le primeiro o que é igual, porque ese é o achado. Ambas declaran un nome, unha descrición, dous argumentos string descritos e tres anotacións; ambas son unha función; ningunha menciona JSON-RPC, framing, stdout nin unha versión do protocolo. Os dous SDKs converxeron na mesma forma, que é o que se supón que significa «Tier 1».1

Dúas diferenzas son reais e ambas volven máis adiante. TypeScript describe argumentos cunha biblioteca de schemas — aquí Zod — e o schema é un valor que escribes. Python descríbeos cos propios type hints da función e léos no momento de import, por iso sabe cousas sobre a función que o ficheiro TypeScript nunca lle contou. E a ruta de erro: TypeScript devolve un resultado de ferramenta con isError, Python lanza. Queda con iso.

Os outros catro rexistros non difiren en nada estrutural. O recurso é server.registerResource("open-incidents", "incidents://open", …) fronte a @server.resource("incidents://open", …); o prompt é registerPrompt fronte a @server.prompt. A última liña de cada ficheiro é o transporte: await server.connect(new StdioServerTransport()) fronte a server.run().

Ficheiros completos: 81 liñas non baleiras e 3.060 bytes de TypeScript fronte a 63 e 2.555. Tómao coa reserva que merece: os recontos de liñas miden un formatter tanto como unha linguaxe, por iso ningún dos dous números está na táboa principal de abaixo.

A proba de que a linguaxe é invisible é un cliente executado dúas veces, en once liñas:

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));

Apúntao a cada server por quenda. Saída real, recortada:

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

Mesmas ferramentas, mesma orde, mesmo handle. Un cliente TypeScript non pode saber en que está escrito o server, e nunca o pregunta. Esa é toda a promesa dun protocolo, manténdose.

Agora mira os espazos en branco no segundo resultado, porque non son cosméticos: o SDK de Python serializa payloads con pydantic_core.to_json(result, fallback=str, indent=2). Na lectura do recurso con dúas incidencias na lista, o corpo de TypeScript ten 136 caracteres e 37 tokens o200k_base; o corpo de Python ten 185 e 62. Un sesenta e oito por cento máis de tokens para filas idénticas, pagados por quen lea o recurso dentro dun prompt, cada vez.

O catálogo conta a mesma historia cunha causa maior. Ambos servers, as mesmas tres ferramentas, tools/list pesado clave a clave:

claveTypeScriptPython
name2121
description4646
annotations4646
inputSchema211192
outputSchema187
execution27
total342480

Os schemas de input de Python son máis baratos: a ponte Zod de TypeScript estampa un $schema e un additionalProperties en cada un. A diferenza enteira de 138 tokens é un schema de saída que ninguén escribiu. resolve_incident está anotado -> Incident, así que o SDK derivou un JSON Schema para o tipo de retorno e enviouno. É realmente útil — é o que permite que un cliente valide structuredContent — e son 187 tokens da túa context window que chegan por culpa dun type hint. A regra do capítulo 24 sobre como as definicións desprazan o material que importa tamén se aplica aos schemas que non sabías que tiñas.

Rómpeno adrede: a mensaxe de erro que se filtrou

Ligazón á sección: Rómpeno adrede: a mensaxe de erro que se filtrou

As dúas rutas de erro de enriba non son unha cuestión de estilo. Dálle a cada server unha ferramenta que falle como falla unha integración real, e le o que chega ao modelo.

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}

O SDK de TypeScript puxo un enderezo interno, un porto, un nome de base de datos e unha conta de servizo no contexto do modelo. O SDK de Python non puxo nada diso; o traceback foi a stderr e quedou no server.

Ningunha das dúas cousas é un bug. Ambas son decisións, e a de Python está escrita no seu propio docstring: un ToolError é «un fallo que anticipaches» e a súa mensaxe devólvese «en content para que o modelo a lea»; calquera outra cousa «trátase como un crash: o modelo só ve Error executing tool <name>, e o server rexistra o traceback en ERROR». A clase para o caso de crash di o resto en voz alta: «nada do orixinal chega ao cliente».

Ambos comportamentos son incorrectos a metade das veces. O capítulo 18 defendía que un erro de validación debería volver como resultado de ferramenta que o modelo poida ler e corrixir, porque esa é a liña de maior impacto na maioría das integracións; no lado de Python iso require lanzar ToolError explicitamente, e un ValueError nu tira a frase útil ao lixo. O argumento do capítulo 30 vai na dirección contraria: todo o que devolve unha ferramenta aterra nun contexto que unha prompt injection posterior pode tentar reler, e unha string de excepción sen revisar é o texto menos auditado do teu sistema.

A regra que sobrevive a ambas: decide, por ferramenta, que pode dicir un fallo, e escribe ti esa string. Nunca deixes que o texto por defecto dunha excepción decida, en ningunha das dúas linguaxes.

Rómpeno adrede: unha liña en standard output

Ligazón á sección: Rómpeno adrede: unha liña en standard output

O tutorial oficial formula a regra sen matices: «Para servers baseados en STDIO: Never write to stdout. Writing to stdout will corrupt the JSON-RPC messages and break your server. A función print() escribe en stdout por defecto, así que mantena completamente fóra dun server STDIO.»1 O capítulo 26 citou a versión normativa: un server «MUST NOT write anything to its stdout that is not a valid MCP message».2

Engade unha liña a cada server e le o stream en bruto:

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

O de Python é peor, e a razón non é MCP. Un proceso cuxo stdout é un pipe en vez dun terminal recibe un stream con buffer por bloques, así que a liña perdida faise flush cando o buffer decide — aquí, ao saír, despois dunha resposta antes da cal fora escrita. A corrupción non aparece onde está o bug. Engade flush=True, ou unha biblioteca que faga flush, e móvese.

Despois vén a parte que explica por que isto se publica. Pásalle o server roto a tres clientes:

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

O parser de sete liñas morre de inmediato. O cliente oficial e o Inspector encóllense de ombros: saltan a liña e continúan. Unha regra que só rompe os clientes que ninguén usa é unha regra que chega intacta a produción, por iso paga a pena rompela adrede aquí e non no log dun cliente.

O modo CLI do Inspector é a metade que se esquece: npx @modelcontextprotocol/inspector --cli <command> --method tools/list imprime un catálogo e sae, o que o fai scriptable dun xeito que a UI do navegador non é.3

Ambos SDKs instaláronse limpos, nos seus propios directorios, sen nada compartido:

TypeScriptPython
package@modelcontextprotocol/sdk 1.30.0 + zod 3.25.76mcp 2.1.1
última revisión do protocolo implementada2025-11-252026-07-28
packages transitivos instalados9428
tamaño instalado13,9 MiB44,3 MiB
ficheiros en disco3.3862.018
packages de terceiros cargados para servir stdio8 de 9418 de 28
arranque do intérprete nu, mediana19,4 ms11,1 ms
spawn → tools/list respondido, mediana de 25144,5 ms709,4 ms
catálogo tools/list, tokens o200k_base342480

Cada fila sorprende nunha dirección distinta, por iso a comparación merece executarse en vez de asumirse.

TypeScript instala máis do triplo de packages e menos dun terzo dos bytes. 94 dependencias é o ecosistema npm sendo el mesmo — fast-deep-equal, es-errors, dunder-proto. As 28 de Python son menos e enormes: cryptography, pydantic-core e uvicorn son artefactos compilados. Se o teu instinto é que o número de dependencias é o que debería preocuparte, esta fila é o contraexemplo.

O intérprete de Python arranca máis rápido que Node, e non por pouco — 11,1 ms fronte a 19,4 ms nun programa baleiro. Así que os 565 ms da fila de cold start non son a linguaxe. É o SDK, e a fila de packages cargados explica por que:

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 server cuxo único I/O é un pipe importa un server web ASGI, un cliente HTTP e unha biblioteca TLS antes de ler a súa primeira liña. O SDK de TypeScript tamén trae Express, Hono, jose e eventsource — quedan no disco sen lerse, porque o límite do package os mantén fóra dun import server/stdio.js. O package de Python é un único grafo de import, así que import mcp é todo: python -X importtime atribúe 727 ms a import mcp.server.mcpserver — unha cifra medida baixo o import profiler, por iso sae por riba dos 709 ms que a execución sen profiler tarda desde spawn ata responder — e 269 deles só ao subárbore mcp.types: os tipos do fío son modelos Pydantic, unha clase por mensaxe de protocolo e por revisión, e construílos é traballo feito no import. Iso é unha decisión de deseño, non deixadez: os imports ansiosos son a razón pola que o SDK de Python pode darche run(transport="streamable-http") na liña seguinte sen unha segunda instalación.

E entón a última fila do bloque inicial desfai o argumento. Empaqueta o server TypeScript correctamente — unha entrada bin, unha shebang, npm link, nada que descargar — e lánzao mediante npx con --no-install, que é como se inicia realmente un server stdio publicado:

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

O launcher custa 568 ms por arranque — catro veces e media todo o import do SDK de TypeScript — e págase en cada lanzamento, porque un host MCP inicia un server stdio executando ese comando. Así que a forma honesta de «TypeScript arranca cinco veces máis rápido» é: si, ata que o distribúes da maneira normal. A mesma advertencia aplícase presumiblemente a uvx; esta máquina non tiña uv instalado, así que esa fila non existe. Nada non medido entra na táboa.

O capítulo 26 cubriu o framing de stdio. Deixou dúas cousas para aquí.

A primeira: executar un server con npx ou uvx é o transporte stdio. Non hai un «modo package» separado. A configuración dun host nomea un comando e argumentos; o host lánzao e fala polos pipes. Por iso «como distribúo isto» e «que transporte fala» son unha soa pregunta en local, e por iso o custo do launcher pertence a un capítulo sobre publicar.

A segunda: stdio non ten ningunha sección de autorización, e a especificación dio nunha liña: as implementacións que usan stdio «SHOULD NOT follow this specification, and instead retrieve credentials from the environment».4 O seu modelo de seguridade é o do sistema operativo, e o seu límite tamén: un subprocesso local serve exactamente unha máquina e un usuario.

O outro transporte vivo é Streamable HTTP: un único endpoint que acepta POST, unha petición HTTP por cada mensaxe JSON-RPC, e un header Accept que debe listar tanto application/json como text/event-stream porque o server escolle en cada petición con cal dos dous responde.5 O capítulo 14 parseou ese event stream á man, así que nada no wire format é novo — só o que o envolve. Tres obrigas da revisión actual son fáciles de pasar por alto e as tres son comprobables:

Cada POST leva MCP-Protocol-Version, e o seu valor debe coincidir co protocolVersion dentro do _meta da propia petición. Un desaxuste é un 400 cun erro de header-mismatch, non un encoller de ombros.5

Dous headers máis son obrigatorios para cumprir

Ligazón á sección: Dous headers máis son obrigatorios para cumprir

Mcp-Method reflicte o método en cada petición; Mcp-Name reflicte params.name ou params.uri en tools/call, resources/read e prompts/get. Existen para que un proxy poida enrutar sen parsear corpos.5

As formas antigas desapareceron, e responden cun rexeitamento

Ligazón á sección: As formas antigas desapareceron, e responden cun rexeitamento

O stream GET, Mcp-Session-Id e a continuación Last-Event-ID foron eliminados. Un server que só fale esta revisión debería responder 405 Method Not Allowed a un GET ou DELETE, ignorar un header de sesión sen crear un, e ignorar Last-Event-ID.5

Agora a medición que reformula todo o capítulo. Envía unha petición da revisión actual a cada server por 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)"}}

As constantes concordan co comportamento: o LATEST_PROTOCOL_VERSION do SDK de Python le 2026-07-28, o do SDK de TypeScript le 2025-11-25. Envía a petición con header-mismatch do paso anterior e o server Python responde 400 con erro -32020 e a mensaxe «mcp-protocol-version header does not match the request envelope's protocol version»; o SDK de TypeScript non ten ese código, porque non implementa a revisión que o define.

A páxina que lista ambos como Tier 1 tamén di «Each SDK provides the same functionality».1 Na data de abaixo, para a revisión actual, esa frase é aspiracional. Comproba LATEST_PROTOCOL_VERSION no SDK que estás a piques de instalar; é unha liña, e a única afirmación deste capítulo que seguirá importando dentro dun ano.

Move un server fóra do teu portátil e aparece o cliente dun descoñecido cun token. Esta é a metade que o capítulo 26 deixou quieta e a metade que un produto multiusuario non pode saltar.

A especificación pon o MCP server nun rol OAuth 2.1 e dálle nome: un MCP server protexido é un resource server, o cliente é un cliente OAuth, e o authorization server é problema doutro.4 Dese rol saen catro cláusulas obrigatorias, citadas completas porque parafrasealas é como se comete o erro:

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» é a regra anti-passthrough, e é a razón pola que existe todo o aparello de audience. Un server que reproduce o bearer token que lle entregaron nunha API de terceiros é un deputy confundido: presta a súa propia confianza a quen o chamou. A regra prohibe a reutilización, non só o almacenamento.

Facer iso aplicable require catro RFCs, unha tarefa cada unha.6 RFC 9728 é como o cliente atopa o authorization server: o MCP server serve un documento de protected-resource-metadata e un 401 apunta a el. RFC 8707 é o parámetro resource: o cliente debe enviar o URI canónico do server en ambas a petición de autorización e a petición de token, «independentemente de se os authorization servers o soportan», para que o token emitido nomee a súa audience. RFC 9207 pecha o circuíto desde o outro lado: o cliente rexistra o issuer antes de redirixir e compara o iss devolto por string exacta, sen normalización — sen case folding, sen elisión de porto por defecto, sen barra final. E RFC 7591, Dynamic Client Registration, está agora deprecado en favor dos Client ID Metadata Documents, «retained for backwards compatibility with authorization servers that do not support» them.4

Monta iso en ambos servers cun verificador de token que non fai nada máis ca comprobar a audience. A escaleira 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"}

Ambos SDKs serven ese documento e ambos fan que un 401 apunte a el, que é toda a historia de descubrimento: un cliente que nunca viu o teu server aprende onde autenticarse a partir dun rexeitamento. O 403 é outro animal: o token está ben, o scope non; e o challenge nomea o que falta para que o cliente poida subir de nivel en vez de empezar de novo.

Dous chanzos difiren, e ningunha diferenza está na especificación. O SDK de TypeScript rexeita un token sen claim de caducidade; o de Python devolve 200, porque expires_at é opcional no seu AccessToken e None significa «sen opinión». E o 403 de Python leva error_description="Required scope: incidents:read" sen o parámetro scope que a especificación di que os servers deberían incluír. Un verificador non é sitio para aceptar o default dunha biblioteca: a comprobación de audience é túa en calquera linguaxe, e a de caducidade tamén.

Unha pega honesta da mesma execución. Un GET no endpoint respondeu 404 no wiring de Express e 400 Bad Request: Missing session ID no de Python, cando a especificación pide 405 Method Not Allowed e onde «session ID» é vocabulario que esta revisión eliminou. Ningunha das dúas cousas é perigosa; ambas son a forma dun ecosistema en plena migración.

A última peza de publicar é onde publicas, e ten unha resposta con número. Rastrexados hoxe, todos os servers do rexistro oficial na súa última versión:7

servers
total (última versión, non eliminados)28.170
activos / deprecados27.853 / 317
publican polo menos un package instalable13.065
só remotos — un URL, nada que instalar14.696
npm8.275
PyPI3.603
imaxes OCI867
bundles mcpb706
NuGet / Cargo107 / 43

Dúas lecturas, apuntando en direccións opostas. Por servers publicados, npm gaña 2,3 a 1 — o número que a xente cita cando di que o ecosistema é TypeScript. Por descargas, Python gaña: nos últimos trinta días mcp fixo 286,7 millóns fronte a @modelcontextprotocol/sdk con 194,7 millóns, antes de engadir fastmcp con 72,1 millóns.7 Ambos son Tier 1, o schema normativo é un schema.ts, e o tutorial oficial «Build an MCP server» abre na pestana de Python.1 Calquera metade desa idea que tiveses na cabeza, a outra metade tamén é certa.

E a fila que importa máis ca calquera das dúas: máis da metade do rexistro — 14.696 de 28.170 — non ten nada que instalar. Son servizos web. Os recontos de transporte concordan polo outro lado: de 14.290 entradas de package, 13.787 declaran stdio; de 16.640 entradas remotas, 15.570 declaran Streamable HTTP e 1.070 aínda declaran o HTTP+SSE deprecado. Así que «un MCP server é un subprocesso no teu portátil» describe unha minoría cada vez menor, e cada un deses 14.696 necesita a sección anterior en vez dunha variable de contorno.

Mostrar detalles

Deliberadamente bilingüe, e o precedente para selo.

Este é o único capítulo bilingüe do curso, porque a resposta honesta parte en dous: o rexistro é npm-first e as descargas son Python-first, ao mesmo tempo, hoxe. Escribir só unha das dúas linguaxes cedería a metade da pregunta e describiría mal o ecosistema mentres o fai. Hai precedente aberto: o Hugging Face MCP Course lista entre os seus requisitos «Experience with at least one programming language (Python or TypeScript examples will be shown)», e ensina ambas.8 Un protocolo cuxo valor enteiro é o número de implementacións é un mal sitio para ser monolingüe.

Sección datada: todo o anterior que ten vida útil

Ligazón á sección: Sección datada: todo o anterior que ten vida útil

Lido e medido o 7 de setembro de 2026, contra a revisión do protocolo 2026-07-28.

valor
@modelcontextprotocol/sdk1.30.0, publicado o 27 de xullo de 2026; 4.322.438 bytes descomprimidos, 693 ficheiros, 17 dependencias directas
última revisión que implementa2025-11-25
mcp (PyPI)2.1.1, publicado o 25 de agosto de 2026; wheel de 357.912 bytes, máis mcp-types 2.1.1 con 69.656 bytes
última revisión que implementa2026-07-28
niveis dos SDKTypeScript, Python, C#, Go, Rust en Tier 1; Java, Ruby en Tier 2; Swift, PHP, Kotlin en Tier 3
servers do rexistro28.170
descargas, últimos 30 díasmcp 286.653.871 · fastmcp 72.097.269 · @modelcontextprotocol/sdk 194.679.333

Unha nota de migración que non é un número. En mcp 2.x, FastMCP pasou a chamarse MCPServer, e case todos os tutoriais online aínda abren co import antigo. O SDK trae un módulo cuxo único propósito é explicar iso, que é a deprecación máis considerada deste capítulo:

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.

Coa táboa diante, a recomendación é aburrida, e iso é bo sinal.

Se o server vive dentro dunha aplicación web que xa executas, escríbeo en TypeScript. Mesmo proceso, mesmo deploy, mesmo request handler; Streamable HTTP é un endpoint que engades ao lado dos outros; e os 13,9 MiB e os 145 ms saen gratis porque o runtime xa estaba levantado. Esa é a maioría dos 14.696 servers remotos.

Se o server envolve ferramentas de datos, escríbeo en Python. O que estás expoñendo é pandas, un cliente de warehouse, as transformacións dun notebook, e un server noutra linguaxe sería unha chamada a subprocesso levando posto un schema. Setecentos milisegundos de import nun servizo que arranca unha vez non son un custo; nun subprocesso que un host relanza todo o día, si.

E por agora, a fila da revisión pesa máis ca ambas. Se necesitas 2026-07-28 — peticións multi-round-trip, resultType, cache hints, server/discover — un dos dous SDKs tena hoxe e o outro non.

Agora podes publicar o mesmo server en calquera das dúas linguaxes, defender a elección cunha táboa en vez dunha preferencia, executalo sobre os dous transportes vivos e entregarlle un token que vai rexeitar.

O que construíches segue a ser unha función: un schema, un endpoint, algo determinista que o modelo invoca. Toda unha clase de coñecemento non encaixa nesa forma: como escribimos nós un postmortem, que campos necesitan os nosos informes de incidencias, a orde na que facemos as cousas e por que. É procedemento, é prosa, e forzalo nunha descrición de ferramenta é como os system prompts medran ata dous mil tokens pagados en cada quenda, fale ou non a conversa de incidencias.

O capítulo 28 é a outra resposta: un cartafol cun SKILL.md dentro que o modelo le en vez de chamar, cargado en tres niveis para que o material de referencia non custe case nada ata a quenda na que se necesita. Non ten linguaxe principal, e iso é o primeiro que ensina.


Todo isto foi medido o 7 de setembro de 2026, en Node 22.22.3 e Python 3.14.4, contra @modelcontextprotocol/sdk 1.30.0 con zod 3.25.76 e mcp 2.1.1, cada un instalado no seu propio directorio desbotable. Os tempos son medianas de 25 lanzamentos, wall clock desde spawn ata a liña que leva a resposta tools/list; os recontos de token son o200k_base vía tiktoken sobre o JSON de cada definición. Non se chamou ningunha API de pago: nada aquí necesita un modelo.

Os dous servers teñen 81 e 63 liñas non baleiras; unha das súas tres ferramentas reprodúcese arriba nas dúas linguaxes, e os outros catro rexistros só difiren como se describiu. A política de divulgación de erros do SDK de Python está citada dos docstrings de ToolError e UnexpectedToolError en mcp/server/mcpserver/exceptions.py; o default de pretty-printing é pydantic_core.to_json(result, fallback=str, indent=2) en mcp/server/mcpserver/resources/types.py e utilities/func_metadata.py. As constantes de versión de protocolo son LATEST_PROTOCOL_VERSION en mcp_types/version.py e no types.js do SDK de TypeScript, ambas lidas dos packages instalados en vez dun changelog.

  1. SDKs, modelcontextprotocol.io/docs/sdk, e Build an MCP server, modelcontextprotocol.io/docs/develop/build-server, ambos lidos o 7 de setembro de 2026. Fonte da táboa de niveis, da frase «Each SDK provides the same functionality but follows the idioms and best practices of its language», da orde das pestanas de linguaxe no tutorial (Python, TypeScript, Java, Kotlin, C#, Ruby, Rust, Go), e da regra de logging citada sobre print() e stdout. 2 3 4

  2. stdio transport, .../basic/transports/stdio. Fonte do newline framing e da regra de pureza stdout. O capítulo 26 le esta páxina completa; cítase aquí pola liña que infrinxe o server roto.

  3. MCP Inspector, modelcontextprotocol.io/docs/2026-07-28/tools/inspector, lido o 7 de setembro de 2026. Un package, tres clientes tras un binario — web, --cli e --tui — compartindo un core, un conxunto de transportes e un estado OAuth en disco. A CLI produciu aquí as trazas de catálogo.

  4. Authorization, modelcontextprotocol.io/specification/2026-07-28/basic/authorization, lido o 7 de setembro de 2026. Fonte do rol de resource-server; das catro cláusulas de manexo de token citadas completas; do requisito de que os servers implementen RFC 9728 e os clientes o usen para o descubrimento; das regras do parámetro resource e da definición de URI canónico; da táboa de validación de issuer; da deprecación de Dynamic Client Registration; da táboa 401/403/400 e do challenge insufficient_scope; e da exención de 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, e Transports overview, .../basic/transports. Fonte da regra POST de endpoint único, do requisito dual Accept, do header MCP-Protocol-Version e da súa regra de coincidir co corpo, dos headers Mcp-Method e Mcp-Name descritos como «REQUIRED for compliance», da eliminación do stream GET, das sesións e Last-Event-ID, da guía 405, da validación obrigatoria Origin e da clasificación do transporte HTTP+SSE 2024-11-05 como Deprecated baixo SEP-2596. 2 3 4

  6. As catro nas que se apoia a especificación, co draft que perfila: The OAuth 2.1 Authorization Framework, draft-ietf-oauth-v2-1-13. Campbell, B., Bradley, J. e Tschofenig, H., Resource Indicators for OAuth 2.0, RFC 8707, febreiro de 2020 — o parámetro resource e a audience á que se vincula. Jones, M.B., Hunt, P. e Parecki, A., OAuth 2.0 Protected Resource Metadata, RFC 9728, abril de 2025 — o documento ao que apunta un 401. Meyer zu Selhausen, K. e Fett, D., OAuth 2.0 Authorization Server Issuer Identification, RFC 9207, marzo de 2022 — o parámetro iss e a comparación por string exacta. Richer, J. (ed.) et al., OAuth 2.0 Dynamic Client Registration Protocol, RFC 7591, xullo de 2015, deprecado para este uso. E Jones, M. e Hardt, D., The OAuth 2.0 Authorization Framework: Bearer Token Usage, RFC 6750, outubro de 2012, sección 3, para a forma do challenge WWW-Authenticate anterior.

  7. Rexistro oficial MCP, registry.modelcontextprotocol.io/v0/servers, rastrexado o 7 de setembro de 2026 con version=latest: 282 páxinas, 28.170 servers, contados por registryType sobre nomes de server distintos. Cifras de descargas: api.npmjs.org/downloads/point/last-month para @modelcontextprotocol/sdk (194.679.333 do 8 de agosto ao 6 de setembro de 2026) e pypistats.org/api/packages/<name>/recent para mcp e fastmcp, todos lidos o mesmo día. Os tamaños dos packages veñen do documento do rexistro npm e da API JSON de PyPI. 2

  8. MCP Course, Hugging Face, huggingface.co/learn/mcp-course, unidade 0, lido o 7 de setembro de 2026: entre os requisitos, «Experience with at least one programming language (Python or TypeScript examples will be shown)».

Listo para deixar que LIA escolla por ti?

Crea con todos os modelos de IA nun só sitio: empeza gratis hoxe mesmo.