Saltar al contenido
27/30Capítulo 27 de 30

Publica un servidor MCP: TypeScript y Python, medidos

El mismo servidor escrito dos veces — tres herramientas, un recurso y un prompt — y medido: 94 paquetes frente a 28; 145 ms frente a 709.

En esta página

Aquí tienes todo el argumento sobre lenguajes, medido, antes de formular una sola 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

Las dos primeras líneas son la comparación que todo el mundo quiere. La tercera línea es el mismo servidor TypeScript de la primera línea, lanzado como se distribuiría de verdad, y se queda a tres milisegundos de Python.

El capítulo 26 leyó el Model Context Protocol contra su propia especificación con JSON-RPC puro, porque JSON-RPC puro no tiene lenguaje. Este capítulo tiene dos, y el peso del argumento cae aquí: el mismo servidor, escrito dos veces. Tres herramientas, un recurso, un prompt, ambos SDK, sin atajos en ningún lado. Luego los transportes, el inspector, el 401 y las cifras que nadie ha publicado.

El servidor, y por qué contiene estas cinco cosas

Enlace a la sección: El servidor, y por qué contiene estas cinco cosas

Un registro de incidentes. Tres herramientas, porque la separación del capítulo 18 entre lecturas y escrituras tiene que ser visible: search_incidents lee, open_incident escribe y devuelve un handle, resolve_incident toma ese handle y cierra. Un recurso, incidents://open, porque leer la lista actual es algo que adjunta la aplicación. Un prompt, postmortem, porque «redacta esto» es el comando con barra de una persona. Esa es la jerarquía de control del capítulo 26 — modelo, aplicación, persona — convertida en cinco registros.

El handle importa más de lo que parece. El capítulo 26 rompió un calendario de juguete al mantener su estado en un array a nivel de módulo: el protocolo no tiene sesión, así que una herramienta de creación devuelve un identificador opaco y cada llamada posterior lo recibe como un argumento ordinario. Nada en ninguno de los dos archivos asume que quien llama sea el proceso que lo abrió.

Aquí está la misma herramienta en ambos lenguajes, registrada 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.")

Lee primero lo que es igual, porque ese es el hallazgo. Ambos declaran un nombre, una descripción, dos argumentos string descritos y tres anotaciones; ambos son una función; ninguno menciona JSON-RPC, framing, stdout ni una versión del protocolo. Los dos SDK convergieron en la misma forma, que es lo que se supone que significa «Tier 1».1

Hay dos diferencias reales y ambas vuelven más adelante. TypeScript describe los argumentos con una biblioteca de esquemas — aquí, Zod — y el esquema es un valor que escribes. Python los describe con los type hints de la propia función y los lee en tiempo de importación, por eso sabe cosas sobre la función que el archivo TypeScript nunca le dijo. Y la ruta de error: TypeScript devuelve un resultado de herramienta con isError, Python lanza. Quédate con eso.

Los otros cuatro registros no difieren en nada estructural. El recurso es server.registerResource("open-incidents", "incidents://open", …) frente a @server.resource("incidents://open", …); el prompt es registerPrompt frente a @server.prompt. La última línea de cada archivo es el transporte: await server.connect(new StdioServerTransport()) frente a server.run().

Archivos completos: 81 líneas no vacías y 3.060 bytes de TypeScript frente a 63 y 2.555. Tómalo con la cautela que merece: los recuentos de líneas miden tanto un formateador como un lenguaje, y por eso ninguno de esos números aparece en la tabla principal de abajo.

La prueba de que el lenguaje es invisible es un cliente ejecutado dos veces, en once líneas:

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úntalo a cada servidor por turnos. Salida 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}"}]

Mismas herramientas, mismo orden, mismo handle. Un cliente TypeScript no puede saber en qué está escrito el servidor, y nunca lo pregunta. Esa es toda la promesa de un protocolo, cumpliéndose.

Ahora mira los espacios en blanco del segundo resultado, porque no son cosméticos: el SDK de Python serializa payloads con pydantic_core.to_json(result, fallback=str, indent=2). En la lectura del recurso con dos incidentes en la lista, el cuerpo de TypeScript tiene 136 caracteres y 37 tokens o200k_base; el cuerpo de Python tiene 185 y 62. Un sesenta y ocho por ciento más de tokens para filas idénticas, pagados por quien lea el recurso en un prompt, cada vez.

El catálogo cuenta la misma historia con una causa mayor. Ambos servidores, las mismas tres herramientas, tools/list pesado clave por clave:

claveTypeScriptPython
name2121
description4646
annotations4646
inputSchema211192
outputSchema187
execution27
total342480

Los esquemas de input de Python son más baratos: el puente Zod de TypeScript estampa un $schema y un additionalProperties en cada uno. Toda la brecha de 138 tokens es un esquema de output que nadie escribió. resolve_incident está anotado como -> Incident, así que el SDK derivó un JSON Schema para el tipo de retorno y lo envió. Es realmente útil — es lo que permite a un cliente validar structuredContent — y son 187 tokens de tu context window que llegan por un type hint. La regla del capítulo 24 sobre definiciones que desplazan el material que importa también se aplica a esquemas que no sabías que tenías.

Rómpelo a propósito: el mensaje de error que se filtró

Enlace a la sección: Rómpelo a propósito: el mensaje de error que se filtró

Las dos rutas de error anteriores no son una elección de estilo. Dale a cada servidor una herramienta que falle como falla una integración real y lee lo que llega al 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}

El SDK de TypeScript puso una dirección interna, un puerto, el nombre de una base de datos y una cuenta de servicio en el contexto del modelo. El SDK de Python no puso nada de eso ahí; el traceback fue a stderr y se quedó en el servidor.

Ninguno de los dos es un bug. Ambos son decisiones, y la de Python está escrita en su propia docstring: un ToolError es «un fallo que anticipaste» y su mensaje se devuelve «en content para que el modelo lo lea»; cualquier otra cosa «se trata como un crash: el modelo solo ve Error executing tool <name>, y el servidor registra el traceback en ERROR». La clase para el caso de crash dice el resto en voz alta: «nada del original llega al cliente».

Ambos comportamientos están mal la mitad de las veces. El capítulo 18 argumentaba que un error de validación debería volver como un resultado de herramienta que el modelo pueda leer y corregir, porque esa es la línea de mayor leverage en la mayoría de integraciones; en Python eso exige lanzar ToolError explícitamente, y un ValueError sin más tira a la basura la frase útil. El argumento del capítulo 30 va en la dirección contraria: todo lo que devuelve una herramienta aterriza en un contexto que una inyección de prompt posterior puede intentar volver a leer, y una cadena de excepción sin revisar es el texto menos auditado de tu sistema.

La regla que sobrevive a ambos: decide, por herramienta, qué puede decir un fallo, y escribe tú esa cadena. Nunca dejes que el texto por defecto de una excepción decida, en ningún lenguaje.

Rómpelo a propósito: una línea en la salida estándar

Enlace a la sección: Rómpelo a propósito: una línea en la salida estándar

El tutorial oficial enuncia la regla sin matices: «Para servidores basados en STDIO: nunca escribas en stdout. Escribir en stdout corromperá los mensajes JSON-RPC y romperá tu servidor. La función print() escribe en stdout por defecto, así que mantenla completamente fuera de un servidor STDIO».1 El capítulo 26 citó la versión normativa: un servidor «MUST NOT write anything to its stdout that is not a valid MCP message».2

Añade una línea a cada servidor y lee el stream 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

El de Python es peor, y la razón no es MCP. Un proceso cuyo stdout es una tubería en lugar de una terminal obtiene un stream con buffer por bloques, así que la línea suelta se vuelca cuando el buffer decide: aquí, al salir, después de una respuesta antes de la cual se había escrito. La corrupción no aparece donde está el bug. Añade flush=True, o una biblioteca que haga flush, y se mueve.

Luego viene la parte que explica por qué esto llega a producción. Alimenta el servidor 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

El parser de siete líneas muere de inmediato. El cliente oficial y el Inspector se encogen de hombros: se saltan la línea y siguen. Una regla que solo rompe los clientes que nadie usa es una regla que llega intacta a producción, por eso merece la pena romperla a propósito aquí y no en el log de un cliente.

El modo CLI del Inspector es la mitad que se olvida: npx @modelcontextprotocol/inspector --cli <command> --method tools/list imprime un catálogo y sale, lo que lo hace scriptable de una forma que la UI del navegador no es.3

Ambos SDK se instalaron limpiamente, en sus propios directorios, sin nada compartido:

TypeScriptPython
paquete@modelcontextprotocol/sdk 1.30.0 + zod 3.25.76mcp 2.1.1
última revisión del protocolo implementada2025-11-252026-07-28
paquetes transitivos instalados9428
tamaño instalado13,9 MiB44,3 MiB
archivos en disco3.3862.018
paquetes de terceros cargados para servir stdio8 de 9418 de 28
arranque de intérprete desnudo, 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 en una dirección distinta, y por eso vale la pena ejecutar la comparación en vez de asumirla.

TypeScript instala más del triple de paquetes y menos de un tercio de bytes. 94 dependencias es el ecosistema npm siendo él mismo: fast-deep-equal, es-errors, dunder-proto. Las 28 de Python son menos y enormes: cryptography, pydantic-core y uvicorn son artefactos compilados. Si tu instinto te dice que lo preocupante es el número de dependencias, esta fila es el contraejemplo.

El intérprete de Python arranca más rápido que el de Node, y no por poco: 11,1 ms frente a 19,4 ms en un programa vacío. Así que los 565 ms de la fila de arranque en frío no son el lenguaje. Es el SDK, y la fila de paquetes cargados explica por 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 cuya única E/S es una tubería importa un servidor web ASGI, un cliente HTTP y una biblioteca TLS antes de leer su primera línea. El SDK de TypeScript también trae Express, Hono, jose y eventsource: se quedan en disco sin leerse, porque el límite del paquete los mantiene fuera de una importación server/stdio.js. El paquete de Python es un único grafo de importación, así que import mcp es todo: python -X importtime atribuye 727 ms a import mcp.server.mcpserver — una cifra medida bajo el profiler de importación, por eso sale por encima de los 709 ms que tarda la ejecución sin perfilar desde spawn hasta responder — y 269 de ellos solo al subárbol mcp.types. Los tipos de cable son modelos Pydantic, una clase por mensaje de protocolo y por revisión, y construirlos es trabajo hecho al importar. Es un trade-off de diseño, no dejadez: las importaciones eager son la razón por la que el SDK de Python puede darte run(transport="streamable-http") en la línea siguiente sin una segunda instalación.

Y entonces la última fila del bloque inicial deshace el argumento. Empaqueta el servidor TypeScript como es debido — una entrada bin, un shebang, npm link, nada que descargar — y lánzalo mediante npx con --no-install, que es como se inicia realmente un servidor 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

El launcher cuesta 568 ms por arranque — cuatro veces y media la importación entera del SDK de TypeScript — y se paga en cada lanzamiento, porque un host MCP inicia un servidor stdio ejecutando ese comando. Así que la forma honesta de «TypeScript arranca cinco veces más rápido» es: lo hace, hasta que lo distribuyes de la manera normal. La misma salvedad presumiblemente aplica a uvx; esta máquina no tenía uv instalado, así que esa fila no existe. Nada no medido entra en la tabla.

El capítulo 26 cubrió el framing de stdio. Dejó dos cosas para aquí.

La primera: ejecutar un servidor con npx o uvx es el transporte stdio. No hay un «modo paquete» separado. La configuración de un host nombra un comando y argumentos; el host lo lanza y habla por las tuberías. Por eso «cómo distribuyo esto» y «qué transporte habla» son una sola pregunta en local, y por eso el coste del launcher pertenece a un capítulo sobre publicar.

La segunda: stdio no tiene ninguna sección de autorización, y la especificación lo dice en una línea: las implementaciones que usan stdio «SHOULD NOT follow this specification, and instead retrieve credentials from the environment».4 Su modelo de seguridad es el del sistema operativo, y también su límite: un subproceso local sirve exactamente a una máquina y a un usuario.

El otro transporte vivo es Streamable HTTP: un único endpoint que acepta POST, una petición HTTP por mensaje JSON-RPC, y una cabecera Accept que debe listar tanto application/json como text/event-stream porque el servidor elige en cada petición con cuál de los dos responde.5 El capítulo 14 parseó ese event stream a mano, así que no hay nada nuevo en el formato de cable: solo en lo que lo envuelve. Tres obligaciones de la revisión actual son fáciles de pasar por alto y las tres son comprobables:

La cabecera de versión debe concordar con el cuerpo

Enlace a la sección: La cabecera de versión debe concordar con el cuerpo

Cada POST lleva MCP-Protocol-Version, y su valor debe coincidir con el protocolVersion dentro del propio _meta de la petición. Una discrepancia es un 400 con un error de header-mismatch, no un encogimiento de hombros.5

Mcp-Method refleja el método en cada petición; Mcp-Name refleja params.name o params.uri en tools/call, resources/read y prompts/get. Existen para que un proxy pueda enrutar sin parsear cuerpos.5

Las formas antiguas han desaparecido y responden con una negativa

Enlace a la sección: Las formas antiguas han desaparecido y responden con una negativa

El stream GET, Mcp-Session-Id y la reanudación Last-Event-ID se eliminaron. Un servidor que solo habla esta revisión debería responder 405 Method Not Allowed a un GET o DELETE, ignorar una cabecera de sesión sin acuñar una, e ignorar Last-Event-ID.5

Ahora, la medición que reencuadra todo el capítulo. Envía una petición de la revisión actual a cada servidor 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)"}}

Las constantes concuerdan con el comportamiento: el LATEST_PROTOCOL_VERSION del SDK de Python lee 2026-07-28, el del SDK de TypeScript lee 2025-11-25. Envía la petición con cabecera discrepante del paso anterior y el servidor Python responde 400 con el error -32020 y el mensaje «mcp-protocol-version header does not match the request envelope's protocol version»; el SDK de TypeScript no tiene tal código, porque no implementa la revisión que lo define.

La página que lista ambos como Tier 1 también dice «Each SDK provides the same functionality».1 En la fecha de abajo, para la revisión actual, esa frase es aspiracional. Comprueba LATEST_PROTOCOL_VERSION en el SDK que estás a punto de instalar; es una línea, y la única afirmación de este capítulo que seguirá importando dentro de un año.

Saca un servidor de tu portátil y aparecerá el cliente de un desconocido con un token. Esta es la mitad que el capítulo 26 dejó aparte y la mitad que un producto multiusuario no puede saltarse.

La especificación coloca el servidor MCP en un rol OAuth 2.1 y le da nombre: un servidor MCP protegido es un resource server, el cliente es un cliente OAuth, y el authorization server es problema de otro.4 Desde ese rol, cuatro cláusulas obligatorias, citadas enteras porque parafrasearlas es como se comete el 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» es la regla anti-passthrough, y por eso existe todo el aparato de audiencia. Un servidor que reproduce el bearer token que le han entregado ante una API de terceros es un confused deputy: presta su propia confianza a quien lo llamó. La regla prohíbe la reutilización, no solo el almacenamiento.

Hacer que eso sea exigible requiere cuatro RFC, con una función para cada uno.6 RFC 9728 es cómo el cliente encuentra el authorization server: el servidor MCP sirve un documento de protected-resource-metadata y un 401 apunta a él. RFC 8707 es el parámetro resource: el cliente debe enviar el URI canónico del servidor en ambas peticiones, la de autorización y la de token, «independientemente de si los authorization servers lo soportan», para que el token emitido nombre su audiencia. RFC 9207 cierra el bucle desde el otro lado: el cliente registra el issuer antes de redirigir y compara el iss devuelto por cadena exacta, sin normalización: sin case folding, sin omitir el puerto por defecto, sin barra final. Y RFC 7591, Dynamic Client Registration, ahora está obsoleto en favor de Client ID Metadata Documents, «retenido por compatibilidad hacia atrás con authorization servers que no los soportan».4

Conecta eso en ambos servidores con un verificador de token que no haga nada salvo comprobar la audiencia. La escalera 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 SDK sirven ese documento y ambos apuntan un 401 hacia él, que es toda la historia de descubrimiento: un cliente que nunca ha visto tu servidor aprende dónde autenticarse a partir de una negativa. El 403 es otro animal: el token está bien, el scope no; y el challenge nombra lo que falta para que el cliente pueda subir un escalón en vez de empezar de cero.

Dos peldaños difieren, y ninguna diferencia está en la especificación. El SDK de TypeScript rechaza un token sin claim de expiración; el de Python devuelve 200, porque expires_at es opcional en su AccessToken y None significa «sin opinión». Y el 403 de Python lleva error_description="Required scope: incidents:read" sin el parámetro scope que la especificación dice que los servidores deberían incluir. Un verificador no es lugar para aceptar el valor por defecto de una biblioteca: la comprobación de audiencia la escribes tú en cualquiera de los dos lenguajes, y la expiración también.

Una pega honesta de la misma ejecución. Un GET en el endpoint respondió 404 en el wiring de Express y 400 Bad Request: Missing session ID en el de Python, donde la especificación pide 405 Method Not Allowed y donde «session ID» es vocabulario que esta revisión eliminó. Ninguno es peligroso; ambos son la forma de un ecosistema a mitad de migración.

La última pieza de publicar es dónde lo haces, y tiene una respuesta con número. Rastreados hoy, todos los servidores del registry oficial en su última versión:7

servidores
total (última versión, no eliminados)28.170
activos / obsoletos27.853 / 317
publican al menos un paquete instalable13.065
solo remotos — una URL, nada que instalar14.696
npm8.275
PyPI3.603
imágenes OCI867
bundles mcpb706
NuGet / Cargo107 / 43

Dos lecturas, apuntando en direcciones opuestas. Por servidores publicados, npm lidera 2,3 a 1: la cifra que cita la gente cuando dice que el ecosistema es TypeScript. Por descargas, Python lidera: en los últimos treinta días mcp obtuvo 286,7 millones frente a @modelcontextprotocol/sdk con 194,7 millones, antes de sumar fastmcp con 72,1 millones.7 Ambos son Tier 1, el esquema normativo es un schema.ts, y el tutorial oficial «Build an MCP server» se abre en la pestaña de Python.1 Sea cual sea la mitad que tenías en la cabeza, la otra mitad también es cierta.

Y la fila que importa más que cualquiera de las dos: más de la mitad del registry — 14.696 de 28.170 — no tiene nada que instalar. Son servicios web. Los recuentos de transporte coinciden desde el otro lado: de 14.290 entradas de paquetes, 13.787 declaran stdio; de 16.640 entradas remotas, 15.570 declaran Streamable HTTP y 1.070 aún declaran el HTTP+SSE obsoleto. Así que «un servidor MCP es un subproceso en tu portátil» describe una minoría que mengua, y cada uno de esos 14.696 necesita la sección anterior en lugar de una variable de entorno.

Mostrar detalles

Deliberadamente bilingüe, y el precedente.

Este es el único capítulo bilingüe del curso, porque la respuesta honesta se divide: el registry es npm-first y las descargas son Python-first, al mismo tiempo, hoy. Escribir uno de los dos entregaría la mitad de la pregunta y describiría mal el ecosistema al hacerlo. Hay un precedente abierto: el MCP Course de Hugging Face enumera entre sus prerrequisitos «Experience with at least one programming language (Python or TypeScript examples will be shown)» y enseña ambos.8 Un protocolo cuyo valor entero está en el número de implementaciones es un mal lugar para ser monolingüe.

Leído y medido el 7 de septiembre de 2026, contra la revisión del protocolo 2026-07-28.

valor
@modelcontextprotocol/sdk1.30.0, publicado el 27 de julio de 2026; 4.322.438 bytes desempaquetados, 693 archivos, 17 dependencias directas
última revisión que implementa2025-11-25
mcp (PyPI)2.1.1, publicado el 25 de agosto de 2026; wheel de 357.912 bytes, más mcp-types 2.1.1 con 69.656 bytes
última revisión que implementa2026-07-28
tiers de SDKTypeScript, Python, C#, Go, Rust en Tier 1; Java, Ruby en Tier 2; Swift, PHP, Kotlin en Tier 3
servidores del registry28.170
descargas, últimos 30 díasmcp 286.653.871 · fastmcp 72.097.269 · @modelcontextprotocol/sdk 194.679.333

Una nota de migración que no es un número. En mcp 2.x, FastMCP se renombró a MCPServer, y casi todos los tutoriales online aún se abren con la importación antigua. El SDK trae un módulo cuyo único propósito es explicarlo, que es la deprecación más considerada de este 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.

Con la tabla delante, la recomendación es aburrida, lo cual es buena señal.

Si el servidor vive dentro de una aplicación web que ya ejecutas, escríbelo en TypeScript. Mismo proceso, mismo deploy, mismo manejador de peticiones; Streamable HTTP es un endpoint que añades junto a los demás; y los 13,9 MiB y los 145 ms salen gratis porque el runtime ya estaba levantado. Esa es la mayoría de los 14.696 servidores remotos.

Si el servidor envuelve herramientas de datos, escríbelo en Python. Lo que estás exponiendo es pandas, un cliente de warehouse, las transformaciones de un notebook y un servidor en otro lenguaje sería una llamada a subproceso vestida de esquema. Setecientos milisegundos de importación en un servicio que arranca una vez no son un coste; en un subproceso que un host relanza todo el día, sí lo son.

Y por ahora, la fila de revisión pesa más que ambas. Si necesitas 2026-07-28 — peticiones multi-round-trip, resultType, cache hints, server/discover — uno de los dos SDK lo tiene hoy y el otro no.

Ahora puedes publicar el mismo servidor en cualquiera de los dos lenguajes, defender la elección con una tabla en vez de una preferencia, ejecutarlo sobre ambos transportes vivos y entregarle un token que rechazará.

Lo que has construido sigue siendo una función: un esquema, un endpoint, algo determinista que invoca el modelo. Toda una clase de conocimiento no encaja en esa forma: cómo escribimos nosotros un postmortem, qué campos necesitan nuestros informes de incidentes, el orden en que hacemos las cosas y por qué. Es procedimiento, es prosa, y forzarlo dentro de una descripción de herramienta es como los system prompts crecen hasta dos mil tokens pagados en cada turno, tanto si la conversación va de incidentes como si no.

El capítulo 28 es la otra respuesta: una carpeta con un SKILL.md dentro que el modelo lee en lugar de llamar, cargada en tres niveles para que el material de referencia no cueste casi nada hasta el turno en que se necesita. No tiene lenguaje principal, y eso es lo primero que enseña.


Todo lo de aquí se midió el 7 de septiembre de 2026, en Node 22.22.3 y Python 3.14.4, contra @modelcontextprotocol/sdk 1.30.0 con zod 3.25.76 y mcp 2.1.1, cada uno instalado en su propio directorio desechable. Los tiempos son medianas de 25 lanzamientos, wall clock desde spawn hasta la línea que contiene la respuesta tools/list; los recuentos de tokens son o200k_base mediante tiktoken sobre el JSON de cada definición. No se llamó a ninguna API de pago: nada de esto necesita un modelo.

Los dos servidores tienen 81 y 63 líneas no vacías; una de sus tres herramientas se reproduce arriba en ambos lenguajes, y los otros cuatro registros difieren solo como se ha descrito. La política de divulgación de errores del SDK de Python se cita de las docstrings de ToolError y UnexpectedToolError en mcp/server/mcpserver/exceptions.py; el valor por defecto de pretty-printing es pydantic_core.to_json(result, fallback=str, indent=2) en mcp/server/mcpserver/resources/types.py y utilities/func_metadata.py. Las constantes de versión del protocolo son LATEST_PROTOCOL_VERSION en mcp_types/version.py y en el types.js del SDK de TypeScript, ambas leídas de los paquetes instalados y no de un changelog.

  1. SDKs, modelcontextprotocol.io/docs/sdk, y Build an MCP server, modelcontextprotocol.io/docs/develop/build-server, ambos leídos el 7 de septiembre de 2026. Fuente de la tabla de tiers, de la frase «Each SDK provides the same functionality but follows the idioms and best practices of its language», del orden de pestañas por lenguaje del tutorial (Python, TypeScript, Java, Kotlin, C#, Ruby, Rust, Go), y de la regla de logging citada sobre print() y stdout. 2 3 4

  2. stdio transport, .../basic/transports/stdio. Fuente del framing por saltos de línea y de la regla de pureza stdout. El capítulo 26 lee esta página completa; se cita aquí por la línea que viola el servidor roto.

  3. MCP Inspector, modelcontextprotocol.io/docs/2026-07-28/tools/inspector, leído el 7 de septiembre de 2026. Un paquete, tres clientes detrás de un binario — web, --cli y --tui — que comparten un core, un conjunto de transportes y un estado OAuth en disco. La CLI produjo aquí las trazas de catálogo.

  4. Authorization, modelcontextprotocol.io/specification/2026-07-28/basic/authorization, leído el 7 de septiembre de 2026. Fuente del rol de resource-server; las cuatro cláusulas de manejo de tokens citadas completas; el requisito de que los servidores implementen RFC 9728 y los clientes lo usen para descubrimiento; las reglas del parámetro resource y la definición de URI canónico; la tabla de validación de issuer; la deprecación de Dynamic Client Registration; la tabla 401/403/400 y el challenge insufficient_scope; y la 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, y Transports overview, .../basic/transports. Fuente de la regla POST de endpoint único, el requisito dual Accept, la cabecera MCP-Protocol-Version y su regla de coincidencia obligatoria con el cuerpo, las cabeceras Mcp-Method y Mcp-Name descritas como «REQUIRED for compliance», la eliminación del stream GET, las sesiones y Last-Event-ID, la guía 405, la validación obligatoria Origin, y la clasificación del transporte HTTP+SSE de 2024-11-05 como Deprecated bajo SEP-2596. 2 3 4

  6. Los cuatro en los que se apoya la especificación, con el borrador que perfila: The OAuth 2.1 Authorization Framework, draft-ietf-oauth-v2-1-13. Campbell, B., Bradley, J. y Tschofenig, H., Resource Indicators for OAuth 2.0, RFC 8707, febrero de 2020 — el parámetro resource y la audiencia que vincula. Jones, M.B., Hunt, P. y Parecki, A., OAuth 2.0 Protected Resource Metadata, RFC 9728, abril de 2025 — el documento al que apunta un 401. Meyer zu Selhausen, K. y Fett, D., OAuth 2.0 Authorization Server Issuer Identification, RFC 9207, marzo de 2022 — el parámetro iss y la comparación por cadena exacta. Richer, J. (ed.) et al., OAuth 2.0 Dynamic Client Registration Protocol, RFC 7591, julio de 2015, obsoleto para este uso. Y Jones, M. y Hardt, D., The OAuth 2.0 Authorization Framework: Bearer Token Usage, RFC 6750, octubre de 2012, sección 3, para la forma del challenge WWW-Authenticate anterior.

  7. Registry oficial de MCP, registry.modelcontextprotocol.io/v0/servers, rastreado el 7 de septiembre de 2026 con version=latest: 282 páginas, 28.170 servidores, contabilizados por registryType sobre nombres de servidor distintos. Cifras de descargas: api.npmjs.org/downloads/point/last-month para @modelcontextprotocol/sdk (194.679.333 del 8 de agosto al 6 de septiembre de 2026) y pypistats.org/api/packages/<name>/recent para mcp y fastmcp, ambos leídos el mismo día. Los tamaños de paquete vienen del documento del registry de npm y de la API JSON de PyPI. 2

  8. MCP Course, Hugging Face, huggingface.co/learn/mcp-course, unidad 0, leído el 7 de septiembre de 2026: entre los prerrequisitos, «Experience with at least one programming language (Python or TypeScript examples will be shown)».

¿Listo para dejar que elija LIA?

Crea con todos los modelos de IA en un mismo sitio. Empieza gratis hoy.