Pular para o conteúdo
27/30Capítulo 27 de 30

Publique um servidor MCP: TypeScript e Python, medidos

O mesmo servidor escrito duas vezes — três tools, um resource e um prompt — e pesado: 94 pacotes contra 28; cold start de 145 ms contra 709.

Nesta página

Aqui está todo o argumento sobre linguagem, medido, antes de qualquer palavra dele ser feita.

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 duas primeiras linhas são a comparação que todo mundo quer. A terceira linha é o mesmo servidor TypeScript da primeira, iniciado do jeito como ele realmente seria distribuído — e fica a três milissegundos do Python.

O Capítulo 26 leu o Model Context Protocol contra sua própria especificação com JSON-RPC bruto, porque JSON-RPC bruto não tem linguagem. Este capítulo tem duas, e o peso do argumento cai aqui: o mesmo servidor, escrito duas vezes. Três tools, um resource, um prompt, os dois SDKs, sem atalhos de nenhum lado. Depois os transports, o inspetor, o 401 e os números que ninguém publicou.

O servidor, e por que ele tem estas cinco coisas

Link para a seção: O servidor, e por que ele tem estas cinco coisas

Um log de incidentes. Três tools, porque a separação do Capítulo 18 entre leituras e escritas precisa ficar visível: search_incidents lê, open_incident escreve e devolve um identificador, resolve_incident recebe esse identificador e fecha. Um resource, incidents://open, porque ler a lista atual é algo que a aplicação anexa. Um prompt, postmortem, porque “escreva isso” é um comando slash de uma pessoa. Essa é a hierarquia de controle do Capítulo 26 — modelo, aplicação, pessoa — transformada em cinco registros.

O identificador importa mais do que parece. O Capítulo 26 quebrou um calendário de brinquedo ao manter seu estado em um array no nível do módulo: o protocolo não tem sessão, então uma tool de criação retorna um identificador opaco e toda chamada posterior o recebe como um argumento comum. Nada em nenhum dos arquivos presume que quem chama é o processo que o abriu.

Aqui está a mesma tool nos dois idiomas, 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.")

Leia primeiro o que é igual, porque esse é o achado. Ambos declaram um nome, uma descrição, dois argumentos string descritos e três annotations; ambos são uma função; nenhum menciona JSON-RPC, enquadramento, stdout ou uma versão de protocolo. Os dois SDKs convergiram para o mesmo formato, que é o que “Tier 1” deveria significar.1

Duas diferenças são reais e ambas voltam depois. TypeScript descreve argumentos com uma biblioteca de schema — Zod aqui — e o schema é um valor que você escreve. Python os descreve com as type hints da própria função e as lê no momento do import, por isso sabe coisas sobre a função que o arquivo TypeScript nunca contou. E o caminho de erro: TypeScript retorna um resultado de tool com isError, Python lança. Guarde isso.

Os outros quatro registros não diferem em nada estrutural. O resource é server.registerResource("open-incidents", "incidents://open", …) contra @server.resource("incidents://open", …); o prompt é registerPrompt contra @server.prompt. A última linha de cada arquivo é o transport: await server.connect(new StdioServerTransport()) contra server.run().

Arquivos completos: 81 linhas não vazias e 3.060 bytes de TypeScript contra 63 e 2.555. Leve isso com a ressalva merecida — contagem de linhas mede tanto o formatador quanto a linguagem, e é por isso que nenhum dos números aparece na tabela de destaque abaixo.

A prova de que a linguagem é invisível é um client executado duas vezes, em onze linhas:

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

Aponte para cada servidor, um por vez. Saída real, aparada:

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 tools, mesma ordem, mesmo identificador. Um client TypeScript não consegue saber em que o servidor foi escrito, e nunca pergunta. Essa é toda a promessa de um protocolo, funcionando.

Agora olhe para o espaço em branco no segundo resultado, porque ele não é cosmético: o SDK Python serializa payloads com pydantic_core.to_json(result, fallback=str, indent=2). Na leitura do resource com dois incidentes na lista, o corpo TypeScript tem 136 caracteres e 37 tokens o200k_base; o corpo Python tem 185 e 62. Sessenta e oito por cento mais tokens para linhas idênticas, pagos por quem lê o resource para dentro de um prompt, todas as vezes.

O catálogo conta a mesma história com uma causa maior. Os dois servidores, as mesmas três tools, tools/list pesado chave por chave:

keyTypeScriptPython
name2121
description4646
annotations4646
inputSchema211192
outputSchema187
execution27
total342480

Os schemas de input do Python são mais baratos — a ponte Zod do TypeScript carimba um $schema e um additionalProperties em cada um. A lacuna inteira de 138 tokens é um schema de output que ninguém escreveu. resolve_incident é anotado -> Incident, então o SDK derivou um JSON Schema para o tipo de retorno e o enviou. É genuinamente útil — é o que permite a um client validar structuredContent — e são 187 tokens da sua context window chegando por causa de uma type hint. A regra do Capítulo 24 sobre definições ocuparem o espaço do material que importa se aplica a schemas que você nem sabia que tinha.

Quebre de propósito: a mensagem de erro que vazou

Link para a seção: Quebre de propósito: a mensagem de erro que vazou

Os dois caminhos de erro acima não são uma escolha de estilo. Dê a cada servidor uma tool que falha como uma integração real falha, e leia 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 TypeScript colocou um endereço interno, uma porta, um nome de banco de dados e uma conta de serviço no context do modelo. O SDK Python não colocou nada disso ali; o traceback foi para stderr e ficou no servidor.

Nenhum dos dois é bug. Ambos são decisões, e a do Python está escrita em sua própria docstring: um ToolError é “uma falha que você antecipou” e sua mensagem é retornada “em content para o modelo ler”; qualquer outra coisa “é tratada como crash: o modelo vê apenas Error executing tool <name>, e o servidor registra o traceback em ERROR”. A classe para o caso de crash diz o resto em voz alta — “nada do original chega ao client”.

Os dois comportamentos estão errados metade do tempo. O Capítulo 18 argumentou que um erro de validação deve voltar como resultado de tool que o modelo possa ler e corrigir, porque essa é a linha de maior alavancagem na maioria das integrações; no lado Python, isso exige lançar ToolError explicitamente, e um ValueError simples joga fora a frase útil. O argumento do Capítulo 30 vai na direção oposta: tudo que uma tool retorna cai em um context que uma prompt injection posterior pode tentar ler de volta, e uma string de exceção não revisada é o texto menos auditado do seu sistema.

A regra que sobrevive aos dois: decida, por tool, o que uma falha pode dizer, e escreva essa string você mesmo. Nunca deixe o texto padrão de uma exceção decidir, em nenhuma das linguagens.

Quebre de propósito: uma linha na saída padrão

Link para a seção: Quebre de propósito: uma linha na saída padrão

O tutorial oficial afirma a regra sem hesitar: “Para servidores baseados em STDIO: nunca escreva em stdout. Escrever em stdout corromperá as mensagens JSON-RPC e quebrará seu servidor. A função print() escreve em stdout por padrão, então mantenha-a totalmente fora de um servidor STDIO.”1 O Capítulo 26 citou a versão normativa — um servidor “MUST NOT write anything to its stdout that is not a valid MCP message”.2

Adicione uma linha a cada servidor e leia o fluxo 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 Python é pior, e o motivo não é MCP. Um processo cujo stdout é um pipe em vez de um terminal recebe um fluxo com buffer em bloco, então a linha perdida é descarregada quando o buffer decide — aqui, na saída, depois de uma resposta antes da qual ela tinha sido escrita. A corrupção não aparece onde está o bug. Adicione flush=True, ou uma biblioteca que faz flush, e ela muda de lugar.

Depois vem a parte que explica por que isso chega à produção. Alimente três clients com o servidor quebrado:

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 linhas morre imediatamente. O client oficial e o Inspector dão de ombros — pulam a linha e seguem em frente. Uma regra que só quebra os clients que ninguém usa é uma regra que chega intacta à produção, por isso vale a pena quebrá-la de propósito aqui, e não no log de um cliente.

O modo CLI do Inspector é a metade que as pessoas esquecem: npx @modelcontextprotocol/inspector --cli <command> --method tools/list imprime um catálogo e sai, o que o torna scriptável de um jeito que a UI no navegador não é.3

Os dois SDKs instalaram limpo, em seus próprios diretórios, sem nada compartilhado:

TypeScriptPython
pacote@modelcontextprotocol/sdk 1.30.0 + zod 3.25.76mcp 2.1.1
revisão de protocolo mais recente implementada2025-11-252026-07-28
pacotes transitivos instalados9428
tamanho instalado13,9 MiB44,3 MiB
arquivos em disco3.3862.018
pacotes de terceiros carregados para servir stdio8 de 9418 de 28
início do interpretador vazio, mediana19,4 ms11,1 ms
spawn → tools/list respondido, mediana de 25144,5 ms709,4 ms
catálogo tools/list, tokens o200k_base342480

Cada linha surpreende em uma direção diferente, e é por isso que vale rodar a comparação em vez de presumir.

TypeScript instala mais de três vezes os pacotes e menos de um terço dos bytes. 94 dependências é o ecossistema npm sendo ele mesmo — fast-deep-equal, es-errors, dunder-proto. As 28 do Python são menos numerosas e enormes: cryptography, pydantic-core e uvicorn são artefatos compilados. Se seu instinto é achar que a contagem de dependências é o que deve preocupar, esta linha é o contraexemplo.

O interpretador do Python inicia mais rápido que o Node, e nem chega perto — 11,1 ms contra 19,4 ms em um programa vazio. Então os 565 ms na linha de cold start não são a linguagem. É o SDK, e a linha de pacotes carregados mostra 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, …

Um servidor cujo único I/O é um pipe importa um servidor web ASGI, um client HTTP e uma biblioteca TLS antes de ler sua primeira linha. O SDK TypeScript também traz Express, Hono, jose e eventsource — eles ficam no disco, não lidos, porque a fronteira do pacote os mantém fora de um import server/stdio.js. O pacote Python é um único grafo de import, então import mcp é tudo isso: python -X importtime atribui 727 ms a import mcp.server.mcpserver — um valor medido sob o import profiler, por isso sai acima dos 709 ms que a execução sem profiler leva de spawn a resposta — e 269 deles apenas à subárvore mcp.types — os tipos de wire são modelos Pydantic, uma classe por mensagem de protocolo por revisão, e construí-los é trabalho feito no import. Isso é um trade-off de design, não desleixo — imports eager são o motivo pelo qual o SDK Python pode entregar run(transport="streamable-http") na linha seguinte sem uma segunda instalação.

E então a última linha do bloco de abertura desfaz o argumento. Empacote o servidor TypeScript corretamente — uma entrada bin, um shebang, npm link, nada para baixar — e inicie-o por npx com --no-install, que é como um servidor stdio publicado realmente é iniciado:

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 início — quatro vezes e meia o import inteiro do SDK TypeScript — e é pago em toda execução, porque um host MCP inicia um servidor stdio rodando esse comando. Então a forma honesta de “TypeScript inicia cinco vezes mais rápido” é: inicia, até você distribuí-lo do jeito normal. A mesma ressalva presumivelmente se aplica a uvx; esta máquina não tinha uv instalado, então essa linha não existe. Nada que não foi medido entra na tabela.

O Capítulo 26 cobriu o enquadramento do stdio. Duas coisas ele deixou para cá.

A primeira: executar um servidor com npx ou uvx é o transport stdio. Não existe um “modo pacote” separado. A configuração de um host nomeia um comando e argumentos; o host o inicia e conversa pelos pipes. É por isso que “como distribuo isto” e “qual transport ele fala” são uma pergunta só localmente, e por isso o custo do launcher pertence a um capítulo sobre publicar.

A segunda: stdio não tem nenhuma seção de autorização, e a especificação diz isso em uma linha — implementações usando stdio “SHOULD NOT follow this specification, and instead retrieve credentials from the environment”.4 Seu modelo de segurança é o do sistema operacional, e seu limite também: um subprocesso local atende exatamente uma máquina e um usuário.

O outro transport vivo é Streamable HTTP: um único endpoint que aceita POST, uma requisição HTTP por mensagem JSON-RPC e um header Accept que deve listar tanto application/json quanto text/event-stream porque o servidor escolhe, por requisição, com qual dos dois responder.5 O Capítulo 14 analisou esse event stream manualmente, então nada no wire format é novo — apenas o que o envolve. Três obrigações da revisão atual são fáceis de perder e todas as três são testáveis:

O header de versão deve concordar com o corpo

Link para a seção: O header de versão deve concordar com o corpo

Todo POST carrega MCP-Protocol-Version, e seu valor deve corresponder ao protocolVersion dentro do _meta da própria requisição. Uma divergência é um 400 com erro de header mismatch, não um dar de ombros.5

Mais dois headers são exigidos para conformidade

Link para a seção: Mais dois headers são exigidos para conformidade

Mcp-Method espelha o método em toda requisição; Mcp-Name espelha params.name ou params.uri em tools/call, resources/read e prompts/get. Eles existem para que um proxy possa rotear sem analisar corpos.5

Os formatos antigos se foram e respondem com recusa

Link para a seção: Os formatos antigos se foram e respondem com recusa

O stream GET, Mcp-Session-Id e a retomada Last-Event-ID foram todos removidos. Um servidor que só fala esta revisão deve responder 405 Method Not Allowed a GET ou DELETE, ignorar um header de sessão sem emitir um, e ignorar Last-Event-ID.5

Agora a medição que reenquadra o capítulo inteiro. Envie uma requisição da revisão atual para cada servidor via 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 concordam com o comportamento: o LATEST_PROTOCOL_VERSION do SDK Python lê 2026-07-28, o do SDK TypeScript lê 2025-11-25. Envie a requisição com header mismatch da etapa acima e o servidor Python responde 400 com erro -32020 e a mensagem “mcp-protocol-version header does not match the request envelope's protocol version”; o SDK TypeScript não tem esse código, porque não implementa a revisão que o define.

A página que lista ambos como Tier 1 também diz “Each SDK provides the same functionality”.1 Na data abaixo, para a revisão atual, essa frase é aspiracional. Verifique LATEST_PROTOCOL_VERSION no SDK que você está prestes a instalar; é uma linha, e a única afirmação deste capítulo que ainda importará daqui a um ano.

Tire um servidor do seu notebook e o client de um desconhecido aparece com um token. Esta é a metade que o Capítulo 26 deixou de lado e a metade que um produto multiusuário não pode pular.

A especificação coloca o servidor MCP em um papel OAuth 2.1 e o nomeia: um servidor MCP protegido é um resource server, o client é um client OAuth, e o servidor de autorização é problema de outra pessoa.4 A partir desse papel, quatro cláusulas obrigatórias, citadas inteiras porque parafraseá-las é como o erro acontece:

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 é por isso que todo o aparato de audience existe. Um servidor que reproduz em uma API de terceiros o bearer token que recebeu é um confused deputy: empresta sua própria confiança a quem o chamou. A regra proíbe a reutilização, não apenas o armazenamento.

Tornar isso aplicável exige quatro RFCs, uma função para cada.6 RFC 9728 é como o client encontra o servidor de autorização: o servidor MCP serve um documento de protected-resource-metadata e um 401 aponta para ele. RFC 8707 é o parâmetro resource — o client deve enviar o URI canônico do servidor tanto na requisição de autorização quanto na requisição de token, “regardless of whether authorization servers support it”, para que o token emitido nomeie sua audience. RFC 9207 fecha o ciclo do outro lado: o client registra o issuer antes de redirecionar e compara o iss retornado por string exata, sem normalização — sem case folding, sem elisão de porta padrão, sem barra final. E RFC 7591, Dynamic Client Registration, agora está depreciada em favor de Client ID Metadata Documents, “retained for backwards compatibility with authorization servers that do not support” them.4

Conecte isso nos dois servidores com um verificador de token que não faz nada além de checar a audience. A escada 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"}

Os dois SDKs servem esse documento e ambos apontam um 401 para ele, que é toda a história de descoberta: um client que nunca viu seu servidor aprende onde autenticar a partir de uma recusa. O 403 é outro bicho — o token está bom, o scope não — e o challenge nomeia o que falta para que o client possa subir de nível em vez de começar de novo.

Dois degraus diferem, e nenhuma diferença está na especificação. O SDK TypeScript recusa um token sem claim de expiração; o Python retorna 200, porque expires_at é opcional em seu AccessToken e None significa “sem opinião”. E o 403 do Python carrega error_description="Required scope: incidents:read" sem o parâmetro scope que a especificação diz que servidores deveriam incluir. Um verificador não é lugar para aceitar padrão de biblioteca: o check de audience é seu para escrever em qualquer linguagem, e a expiração também.

Um detalhe honesto da mesma execução. Um GET no endpoint respondeu 404 na fiação Express e 400 Bad Request: Missing session ID na Python, onde a especificação pede 405 Method Not Allowed e onde “session ID” é vocabulário que esta revisão removeu. Nenhum é perigoso; ambos são o formato de um ecossistema no meio de uma migração.

A última peça de publicar é onde você publica, e ela tem uma resposta com número. Vasculhados hoje, todos os servidores no registro oficial em sua versão mais recente:7

servidores
total (versão mais recente, não excluídos)28.170
ativos / depreciados27.853 / 317
publicam pelo menos um pacote instalável13.065
somente remotos — uma URL, nada para instalar14.696
npm8.275
PyPI3.603
imagens OCI867
bundles mcpb706
NuGet / Cargo107 / 43

Duas leituras, apontando para lados opostos. Por servidores publicados, npm lidera por 2,3 para 1 — o número que as pessoas citam quando dizem que o ecossistema é TypeScript. Por downloads, Python lidera: nos últimos trinta dias, mcp recebeu 286,7 milhões contra 194,7 milhões de @modelcontextprotocol/sdk, antes de somar fastmcp com 72,1 milhões.7 Ambos são Tier 1, o schema normativo é um schema.ts, e o tutorial oficial “Build an MCP server” abre na aba Python.1 Qualquer metade disso que você tinha na cabeça, a outra metade também é verdadeira.

E a linha que importa mais que qualquer uma das duas: mais da metade do registro — 14.696 de 28.170 — não tem nada para instalar. São web services. As contagens de transport concordam pelo outro lado: de 14.290 entradas de pacote, 13.787 declaram stdio; de 16.640 entradas remotas, 15.570 declaram Streamable HTTP e 1.070 ainda declaram o HTTP+SSE depreciado. Então “um servidor MCP é um subprocesso no seu notebook” descreve uma minoria cada vez menor, e cada um dos 14.696 precisa da seção acima, não de uma variável de ambiente.

Mostrar detalhes

Deliberadamente bilíngue, e o precedente para isso.

Este é o único capítulo bilíngue do curso, porque a resposta honesta se divide: o registro é npm-first e os downloads são Python-first, ao mesmo tempo, hoje. Escrever apenas um dos dois entregaria metade da pergunta e descreveria mal o ecossistema no processo. Há precedente em aberto — o Hugging Face MCP Course lista entre os pré-requisitos “Experience with at least one programming language (Python or TypeScript examples will be shown)” e ensina ambos.8 Um protocolo cujo valor inteiro é o número de implementações é um péssimo lugar para ser monolíngue.

Seção datada: tudo acima que tem prazo de validade

Link para a seção: Seção datada: tudo acima que tem prazo de validade

Lido e medido em 7 de setembro de 2026, contra a revisão de protocolo 2026-07-28.

valor
@modelcontextprotocol/sdk1.30.0, publicado em 27 de julho de 2026; 4.322.438 bytes descompactados, 693 arquivos, 17 dependências diretas
revisão mais recente que implementa2025-11-25
mcp (PyPI)2.1.1, publicado em 25 de agosto de 2026; wheel de 357.912 bytes, mais mcp-types 2.1.1 com 69.656 bytes
revisão mais recente que implementa2026-07-28
tiers de SDKTypeScript, Python, C#, Go, Rust em Tier 1; Java, Ruby em Tier 2; Swift, PHP, Kotlin em Tier 3
servidores no registro28.170
downloads, últimos 30 diasmcp 286.653.871 · fastmcp 72.097.269 · @modelcontextprotocol/sdk 194.679.333

Uma nota de migração que não é número. Em mcp 2.x, FastMCP foi renomeado para MCPServer, e quase todo tutorial online ainda abre com o import antigo. O SDK entrega um módulo cujo único propósito é explicar isso, que é a depreciação mais atenciosa 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.

Com a tabela à sua frente, a recomendação é entediante, o que é um bom sinal.

Se o servidor vive dentro de uma aplicação web que você já executa, escreva-o em TypeScript. Mesmo processo, mesmo deploy, mesmo request handler; Streamable HTTP é um endpoint que você adiciona ao lado dos outros; e os 13,9 MiB e os 145 ms são grátis porque o runtime já estava de pé. Esse é o caso da maioria dos 14.696 servidores remotos.

Se o servidor envolve tooling de dados, escreva-o em Python. O que você está expondo é pandas, um client de warehouse, transformações do tamanho de um notebook, e um servidor em outra linguagem seria uma chamada de subprocesso vestindo um schema. Setecentos milissegundos de import em um serviço que inicia uma vez não são custo; em um subprocesso que um host relança o dia inteiro, são.

E, por enquanto, a linha de revisão se sobrepõe às duas. Se você precisa de 2026-07-28 — requisições com múltiplas idas e voltas, resultType, cache hints, server/discover — um dos dois SDKs tem isso hoje e o outro não.

Você agora consegue publicar o mesmo servidor em qualquer uma das linguagens, defender a escolha com uma tabela em vez de uma preferência, executá-lo nos dois transports vivos e entregar a ele um token que ele recusará.

O que você construiu ainda é uma function: um schema, um endpoint, uma coisa determinística que o modelo invoca. Uma classe inteira de conhecimento não cabe nesse formato — como nós escrevemos um postmortem, quais campos nossos relatórios de incidente precisam, a ordem em que fazemos as coisas e por quê. É procedimento, é prosa, e forçá-lo para dentro de uma descrição de tool é como system prompts crescem para dois mil tokens pagos em cada turno, esteja ou não a conversa falando de incidentes.

O Capítulo 28 é a outra resposta: uma pasta com um SKILL.md dentro que o modelo em vez de chamar, carregada em três níveis para que o material de referência custe quase nada até o turno em que é necessário. Ele não tem linguagem principal, e essa é a primeira coisa que ensina.


Tudo aqui foi medido em 7 de setembro de 2026, no Node 22.22.3 e Python 3.14.4, contra @modelcontextprotocol/sdk 1.30.0 com zod 3.25.76 e mcp 2.1.1, cada um instalado em seu próprio diretório descartável. Tempos são medianas de 25 inicializações, wall clock de spawn até a linha que carrega a resposta tools/list; contagens de token são o200k_base via tiktoken sobre o JSON de cada definição. Nenhuma API paga foi chamada: nada aqui precisa de um modelo.

Os dois servidores têm 81 e 63 linhas não vazias; uma de suas três tools é reproduzida acima nas duas linguagens, e os outros quatro registros diferem apenas como descrito. A política de divulgação de erro do SDK Python é citada das docstrings de ToolError e UnexpectedToolError em mcp/server/mcpserver/exceptions.py; o padrão de pretty-printing é pydantic_core.to_json(result, fallback=str, indent=2) em mcp/server/mcpserver/resources/types.py e utilities/func_metadata.py. As constantes de versão de protocolo são LATEST_PROTOCOL_VERSION em mcp_types/version.py e no types.js do SDK TypeScript, ambas lidas dos pacotes instalados, não de um changelog.

  1. SDKs, modelcontextprotocol.io/docs/sdk, e Build an MCP server, modelcontextprotocol.io/docs/develop/build-server, ambos lidos em 7 de setembro de 2026. Fonte da tabela de tiers, da frase “Each SDK provides the same functionality but follows the idioms and best practices of its language”, da ordem das abas de linguagem do 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 enquadramento por newline e da regra de pureza de stdout. O Capítulo 26 lê essa página inteira; ela é citada aqui pela linha que o servidor quebrado viola.

  3. MCP Inspector, modelcontextprotocol.io/docs/2026-07-28/tools/inspector, lido em 7 de setembro de 2026. Um pacote, três clients por trás de um binário — web, --cli e --tui — compartilhando um núcleo, um conjunto de transports e um estado OAuth em disco. A CLI produziu os rastros de catálogo aqui.

  4. Authorization, modelcontextprotocol.io/specification/2026-07-28/basic/authorization, lido em 7 de setembro de 2026. Fonte do papel de resource server; das quatro cláusulas de manipulação de token citadas integralmente; do requisito de que servidores implementem RFC 9728 e clients o usem para descoberta; das regras do parâmetro resource e da definição de URI canônico; da tabela de validação de issuer; da depreciação de Dynamic Client Registration; da tabela 401/403/400 e do challenge insufficient_scope; e da isenção 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 de POST em endpoint único, do requisito duplo de Accept, do header MCP-Protocol-Version e de sua regra de corresponder obrigatoriamente ao corpo, dos headers Mcp-Method e Mcp-Name descritos como “REQUIRED for compliance”, da remoção do stream GET, de sessions e Last-Event-ID, da orientação 405, da validação obrigatória de Origin e da classificação do transport HTTP+SSE de 2024-11-05 como Deprecated sob SEP-2596. 2 3 4

  6. As quatro nas quais a especificação se apoia, com o draft que ela 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, fevereiro de 2020 — o parâmetro resource e a audience que ele vincula. Jones, M.B., Hunt, P. e Parecki, A., OAuth 2.0 Protected Resource Metadata, RFC 9728, abril de 2025 — o documento para o qual um 401 aponta. Meyer zu Selhausen, K. e Fett, D., OAuth 2.0 Authorization Server Issuer Identification, RFC 9207, março de 2022 — o parâmetro iss e a comparação por string exata. Richer, J. (ed.) et al., OAuth 2.0 Dynamic Client Registration Protocol, RFC 7591, julho de 2015, depreciado para este uso. E Jones, M. e Hardt, D., The OAuth 2.0 Authorization Framework: Bearer Token Usage, RFC 6750, outubro de 2012, seção 3, para o formato do challenge WWW-Authenticate acima.

  7. Registro oficial MCP, registry.modelcontextprotocol.io/v0/servers, rastreado em 7 de setembro de 2026 com version=latest: 282 páginas, 28.170 servidores, contabilizados por registryType sobre nomes distintos de servidor. Números de download: api.npmjs.org/downloads/point/last-month para @modelcontextprotocol/sdk (194.679.333 de 8 de agosto a 6 de setembro de 2026) e pypistats.org/api/packages/<name>/recent para mcp e fastmcp, ambos lidos no mesmo dia. Tamanhos de pacote vêm do documento do registro npm e da API JSON do PyPI. 2

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

Pronto para deixar a LIA escolher por você?

Crie com todos os modelos de IA em um só lugar — comece grátis hoje.