Lançar um servidor MCP: TypeScript e Python, medidos
O mesmo servidor escrito duas vezes — três ferramentas, um recurso e um prompt — medido: 94 pacotes contra 28, cold start de 145 ms contra 709.
Nesta página
Aqui está todo o argumento sobre linguagens, medido, antes de qualquer palavra ser escrita.
node ./incidents.js 144.5 ms
python incidents.py 709.4 ms
npx incidents-mcp 712.6 msAs duas primeiras linhas são a comparação que toda a gente quer. A terceira linha é o mesmo servidor TypeScript da primeira linha, lançado da forma como seria realmente distribuído — e fica a três milissegundos de Python.
O Capítulo 26 leu o Model Context Protocol contra a 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 ferramentas, um recurso, um prompt, ambos os SDKs, sem atalhos de nenhum lado. Depois, os transports, o inspector, o 401 e os números que ninguém publicou.
O servidor, e porque tem estas cinco coisas
Ligação para a secção: O servidor, e porque tem estas cinco coisasUm registo de incidentes. Três ferramentas, porque a separação do Capítulo 18 entre leituras e escritas tem de ser visível: search_incidents lê, open_incident escreve e devolve um identificador, resolve_incident recebe esse identificador e fecha. Um recurso, incidents://open, porque ler a lista atual é algo que a aplicação anexa. Um prompt, postmortem, porque «escreve isto» é o comando slash de uma pessoa. Essa é a hierarquia de controlo do Capítulo 26 — modelo, aplicação, pessoa — transformada em cinco registos.
O identificador importa mais do que parece. O Capítulo 26 partiu um calendário de brincar ao manter o estado num array ao nível do módulo: o protocolo não tem sessão, por isso uma ferramenta de criação devolve um identificador opaco e todas as chamadas posteriores recebem-no como um argumento normal. Nada em qualquer um dos ficheiros assume que quem chama é o processo que o abriu.
Aqui está a mesma ferramenta nas duas linguagens, registada lado a lado:
server.registerTool(
"resolve_incident",
{
description:
"Close an incident by handle and record its cause.",
inputSchema: {
id: z.string().describe(
"The handle returned by open_incident, e.g. INC-3."),
cause: z.string().describe(
"One sentence. What actually broke."),
},
annotations: {
readOnlyHint: false,
destructiveHint: true,
idempotentHint: true,
},
},
async ({ id, cause }) => {
const at = OPEN.findIndex((i) => i.id === id);
if (at < 0) {
return { isError: true, content: [{ type: "text",
text: `No open incident ${id}. ` +
`Call search_incidents first.` }] };
}
const [done] = OPEN.splice(at, 1);
return { content: [{ type: "text",
text: JSON.stringify({ ...done, cause }) }] };
},
);@server.tool(
description=
"Close an incident by handle and record its cause.",
annotations=ToolAnnotations(
readOnlyHint=False,
destructiveHint=True,
idempotentHint=True,
),
)
def resolve_incident(
id: Annotated[str, Field(description=
"The handle returned by open_incident, e.g. INC-3.")],
cause: Annotated[str, Field(description=
"One sentence. What actually broke.")],
) -> Incident:
for at, i in enumerate(OPEN):
if i["id"] == id:
done = OPEN.pop(at)
return {**done, "cause": cause}
raise ValueError(
f"No open incident {id}. Call search_incidents first.")Leia primeiro o que é igual, porque essa é a conclusão. Ambas declaram um nome, uma descrição, dois argumentos string descritos e três anotações; ambas são uma função; nenhuma menciona JSON-RPC, framing, stdout ou uma versão do protocolo. Os dois SDKs convergiram para a mesma forma, que é o que «Tier 1» deve significar.1
Duas diferenças são reais e ambas regressam mais à frente. TypeScript descreve argumentos com uma biblioteca de schema — Zod, aqui — e o schema é um valor que se escreve. Python descreve-os com os type hints da própria função e lê-os no momento do import, razão pela qual sabe coisas sobre a função que o ficheiro TypeScript nunca lhe disse. E o caminho de erro: TypeScript devolve um resultado de ferramenta com isError, Python lança. Guarde isso.
Os outros quatro registos não diferem em nada estrutural. O recurso é server.registerResource("open-incidents", "incidents://open", …) contra @server.resource("incidents://open", …); o prompt é registerPrompt contra @server.prompt. A última linha de cada ficheiro é o transport: await server.connect(new StdioServerTransport()) contra server.run().
Ficheiros completos: 81 linhas não vazias e 3.060 bytes de TypeScript contra 63 e 2.555. Tome isto com a devida cautela — contagens de linhas medem tanto um formatter como uma linguagem, razão pela qual nenhum dos números aparece na tabela principal abaixo.
Um cliente, ambos os servidores
Ligação para a secção: Um cliente, ambos os servidoresA prova de que a linguagem é invisível é um cliente executado duas vezes, em onze linhas:
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-o a cada servidor, por sua vez. Saída real, aparada:
$ 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 ordem, mesmo identificador. Um cliente TypeScript não consegue saber em que linguagem o servidor está escrito, e nunca pergunta. É a promessa inteira de um protocolo a cumprir-se.
Agora olhe para o espaçamento no segundo resultado, porque não é cosmético: o SDK de Python serializa payloads com pydantic_core.to_json(result, fallback=str, indent=2). Na leitura do recurso com dois incidentes na lista, o corpo TypeScript tem 136 caracteres e 37 token o200k_base; o corpo Python tem 185 e 62. Mais sessenta e oito por cento de tokens para linhas idênticas, pagos por quem lê o recurso para dentro de um prompt, todas as vezes.
O catálogo conta a mesma história com uma causa maior. Ambos os servidores, as mesmas três ferramentas, tools/list pesado chave a chave:
| key | TypeScript | Python |
|---|---|---|
name | 21 | 21 |
description | 46 | 46 |
annotations | 46 | 46 |
inputSchema | 211 | 192 |
outputSchema | — | 187 |
execution | 27 | — |
| total | 342 | 480 |
Os schemas de input de Python são mais baratos — a ponte Zod de TypeScript carimba um $schema e um additionalProperties em cada um. A diferença inteira de 138 tokens é um schema de output que ninguém escreveu. resolve_incident está anotado com -> Incident, por isso o SDK derivou um JSON Schema para o tipo de retorno e enviou-o. É genuinamente útil — é o que permite a um cliente validar structuredContent — e são 187 tokens da sua context window a chegar por causa de um type hint. A regra do Capítulo 24 sobre definições que expulsam o material que importa aplica-se a schemas que não sabia que tinha.
Partir de propósito: a mensagem de erro que escapou
Ligação para a secção: Partir de propósito: a mensagem de erro que escapouOs dois caminhos de erro acima não são uma escolha de estilo. Dê a cada servidor uma ferramenta que falha como uma integração real falha, e leia o que chega ao modelo.
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 colocou um endereço interno, uma porta, um nome de base de dados e uma conta de serviço no contexto do modelo. O SDK de Python não colocou lá nada disso; o traceback foi para stderr e ficou no servidor.
Nenhum dos dois é um bug. Ambos são decisões, e a de Python está escrita na sua própria docstring: um ToolError é «uma falha que antecipou» e a sua mensagem é devolvida «em content para o modelo ler»; qualquer outra coisa «é tratada como um crash: o modelo vê apenas Error executing tool <name>, e o servidor regista o traceback em ERROR». A classe para o caso de crash diz o resto em voz alta — «nada do original chega ao cliente».
Ambos os comportamentos estão errados metade das vezes. O Capítulo 18 argumentou que um erro de validação deve regressar como um resultado de ferramenta que o modelo consiga ler e corrigir, porque essa é a linha de maior alavancagem na maioria das integrações; no lado de Python, isso exige lançar ToolError explicitamente, e um ValueError nu deita fora a frase útil. O argumento do Capítulo 30 segue no sentido oposto: tudo o que uma ferramenta devolve acaba num contexto que uma prompt injection posterior pode tentar ler de volta, e uma string de exceção não revista é o texto menos auditado do seu sistema.
A regra que sobrevive a ambos: decida, ferramenta a ferramenta, o que uma falha tem autorização para dizer, e escreva essa string você mesmo. Nunca deixe o texto predefinido de uma exceção decidir, em nenhuma das linguagens.
Partir de propósito: uma linha no standard output
Ligação para a secção: Partir de propósito: uma linha no standard outputO tutorial oficial declara a regra sem hesitar: «Para servidores baseados em STDIO: nunca escreva para stdout. Escrever para stdout corrompe as mensagens JSON-RPC e parte o servidor. A função print() escreve para stdout por predefinição, por isso 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 stream bruto:
TypeScript incidents server starting
{"result":{"protocolVersion":"2025-11-25", … },"jsonrpc":"2.0","id":1}
Python {"jsonrpc":"2.0","id":1,"result":{ … }}
incidents server startingO de Python é pior, e a razão não é MCP. Um processo cujo stdout é um pipe em vez de um terminal recebe um stream com block buffering, por isso a linha perdida é descarregada quando o buffer decide — aqui, ao sair, depois de uma resposta antes da qual tinha sido escrita. A corrupção não aparece onde o bug está. Adicione flush=True, ou uma biblioteca que faça flush, e ela move-se.
Depois, a parte que explica porque isto chega a produção. Alimente três clientes com o servidor partido:
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 warningO parser de sete linhas morre imediatamente. O cliente oficial e o Inspector encolhem os ombros — saltam a linha e continuam. Uma regra que só parte os clientes que ninguém usa é uma regra que chega intacta a produção, razão pela qual vale a pena parti-la de propósito aqui em vez de no log de um cliente.
O modo CLI do Inspector é a metade que costuma ser esquecida: npx @modelcontextprotocol/inspector --cli <command> --method tools/list imprime um catálogo e sai, o que o torna scriptable de uma forma que a UI do browser não é.3
A tabela
Ligação para a secção: A tabelaAmbos os SDKs instalaram limpo, nos seus próprios diretórios, sem nada partilhado:
| TypeScript | Python | |
|---|---|---|
| pacote | @modelcontextprotocol/sdk 1.30.0 + zod 3.25.76 | mcp 2.1.1 |
| revisão mais recente do protocolo implementada | 2025-11-25 | 2026-07-28 |
| pacotes transitivos instalados | 94 | 28 |
| tamanho instalado | 13.9 MiB | 44.3 MiB |
| ficheiros em disco | 3.386 | 2.018 |
| pacotes de terceiros carregados para servir stdio | 8 de 94 | 18 de 28 |
| arranque do intérprete vazio, mediana | 19.4 ms | 11.1 ms |
spawn → tools/list respondido, mediana de 25 | 144.5 ms | 709.4 ms |
catálogo tools/list, tokens o200k_base | 342 | 480 |
Cada linha surpreende numa direção diferente, que é precisamente por isso que a comparação vale a pena executar em vez de assumir.
TypeScript instala mais de três vezes os pacotes e menos de um terço dos bytes. 94 dependências é o ecossistema npm a ser ele próprio — fast-deep-equal, es-errors, dunder-proto. As 28 de Python são menos e enormes: cryptography, pydantic-core e uvicorn são artefactos compilados. Se o seu instinto é que a contagem de dependências é o problema, esta linha é o contraexemplo.
O intérprete de Python arranca mais depressa do que Node, e nem é renhido — 11.1 ms contra 19.4 ms num programa vazio. Portanto, os 565 ms na linha do cold-start não são a linguagem. É o SDK, e a linha dos pacotes carregados mostra porquê:
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 cliente HTTP e uma biblioteca TLS antes de ler a primeira linha. O SDK de TypeScript também traz Express, Hono, jose e eventsource — ficam em disco, não lidos, porque a fronteira do pacote os mantém fora de um import server/stdio.js. O pacote de Python é um só grafo de import, por isso import mcp é tudo: python -X importtime atribui 727 ms a import mcp.server.mcpserver — um valor medido sob o profiler de import, razão pela qual fica acima dos 709 ms que a execução sem profiler leva de spawn até responder — e 269 deles apenas à subárvore mcp.types — os wire types são modelos Pydantic, uma classe por mensagem do protocolo por revisão, e construí-los é trabalho feito no import. Isso é uma troca de design, não desleixo — imports eager são a razão pela qual o SDK de Python pode entregar-lhe run(transport="streamable-http") na linha seguinte sem uma segunda instalação.
E depois a última linha do bloco inicial desfaz o argumento. Empacote corretamente o servidor TypeScript — uma entrada bin, um shebang, npm link, nada para descarregar — e lance-o através de npx com --no-install, que é como um servidor stdio publicado é realmente iniciado:
node ./incidents.js 144.5 ms
npx incidents-mcp 712.6 ms (+568.1 ms of launcher)
python incidents.py 709.4 msO launcher custa 568 ms por arranque — quatro vezes e meia o import inteiro do SDK de TypeScript — e é pago em cada lançamento, porque um host MCP inicia um servidor stdio ao executar esse comando. Portanto, a forma honesta de «TypeScript arranca cinco vezes mais depressa» é: arranca, até o distribuir da forma normal. A mesma ressalva presumivelmente aplica-se a uvx; esta máquina não tinha uv instalado, por isso essa linha não existe. Nada não medido entra na tabela.
Dois transports, e apenas dois
Ligação para a secção: Dois transports, e apenas doisO Capítulo 26 cobriu o framing de stdio. Deixou duas coisas para aqui.
A primeira: executar um servidor com npx ou uvx é o transport stdio. Não existe um «modo de pacote» separado. A configuração de um host nomeia um comando e argumentos; o host lança-o e fala pelos pipes. É por isso que «como distribuo isto» e «que transport fala» são uma só pergunta localmente, e por isso o custo do launcher pertence a um capítulo sobre shipping.
A segunda: stdio não tem secção de autorização nenhuma, e a especificação di-lo numa linha — implementações que usam stdio «SHOULD NOT follow this specification, and instead retrieve credentials from the environment».4 O seu modelo de segurança é o do sistema operativo, e o seu limite também: um subprocesso local serve exatamente uma máquina e um utilizador.
O outro transport vivo é Streamable HTTP: um único endpoint que aceita POST, um pedido HTTP por mensagem JSON-RPC, e um cabeçalho Accept que deve listar application/json e text/event-stream porque o servidor escolhe, por pedido, com qual dos dois responde.5 O Capítulo 14 analisou esse event stream à mão, por isso nada no wire format é novo — apenas o que o envolve. Três obrigações da revisão atual são fáceis de falhar e as três são testáveis:
O cabeçalho de versão tem de concordar com o corpo
Ligação para a secção: O cabeçalho de versão tem de concordar com o corpoCada POST transporta MCP-Protocol-Version, e o seu valor tem de corresponder ao protocolVersion dentro do _meta do próprio pedido. Uma divergência é um 400 com erro de header mismatch, não um encolher de ombros.5
Mais dois cabeçalhos são exigidos para conformidade
Ligação para a secção: Mais dois cabeçalhos são exigidos para conformidadeMcp-Method espelha o método em todos os pedidos; Mcp-Name espelha params.name ou params.uri em tools/call, resources/read e prompts/get. Existem para que um proxy possa encaminhar sem analisar corpos.5
As formas antigas desapareceram e respondem com recusa
Ligação para a secção: As formas antigas desapareceram e respondem com recusaO stream GET, Mcp-Session-Id e a retoma Last-Event-ID foram todos removidos. Um servidor que só fala esta revisão deve responder 405 Method Not Allowed a um GET ou DELETE, ignorar um cabeçalho de sessão sem criar um, e ignorar Last-Event-ID.5
Agora a medição que reenquadra o capítulo inteiro. Envie um pedido da revisão atual para cada servidor por HTTP.
Python 200 {"result":{"resultType":"complete","cacheScope":"private","ttlMs":0,
"tools":[…],"_meta":{"io.modelcontextprotocol/serverInfo":{…}}}}
TypeScript {"error":{"code":-32000,"message":"Bad Request: Unsupported protocol
version: 2026-07-28 (supported versions: 2025-11-25, 2025-06-18,
2025-03-26, 2024-11-05, 2024-10-07)"}}As constantes concordam com o comportamento: o LATEST_PROTOCOL_VERSION do SDK de Python lê 2026-07-28, o do SDK de TypeScript lê 2025-11-25. Envie o pedido de header mismatch do passo 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 de 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 está prestes a instalar; é uma linha, e a única afirmação deste capítulo que continuará a importar daqui a um ano.
O 401, e a frase a citar
Ligação para a secção: O 401, e a frase a citarTire um servidor do seu portátil e o cliente de um estranho aparece com um token. Esta é a metade que o Capítulo 26 deixou de lado e a metade que um produto multiutilizador não pode saltar.
A especificação coloca o servidor MCP num papel OAuth 2.1 e dá-lhe um nome: um servidor MCP protegido é um resource server, o cliente é um cliente OAuth, e o authorization server é problema de outra pessoa.4 A partir desse papel, quatro cláusulas obrigatórias, citadas por inteiro porque parafraseá-las é como o erro acontece:
Servidores MCP, no seu papel de OAuth 2.1 resource server, MUST validate access tokens as described in OAuth 2.1 Section 5.2. Servidores MCP MUST validate that access tokens were issued specifically for them as the intended audience, according to RFC 8707 Section 2. […] Clientes MCP MUST NOT send tokens to the MCP server other than ones issued by the MCP server's authorization server. Servidores MCP MUST only accept tokens that are valid for use with their own resources. Servidores MCP MUST NOT accept or transit any other tokens.4
«Must not accept or transit» é a regra anti-passthrough, e é por isso que existe todo o aparato de audience. Um servidor que reproduz numa API de terceiros o bearer token que recebeu é um confused deputy: empresta a 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 cliente encontra sequer o authorization server: o servidor MCP serve um documento de protected-resource-metadata e um 401 aponta para ele. RFC 8707 é o parâmetro resource — o cliente tem de enviar o URI canónico do servidor em ambos o pedido de autorização e o pedido de token, «regardless of whether authorization servers support it», para que o token emitido nomeie a sua audience. RFC 9207 fecha o ciclo pelo outro lado: o cliente regista o emissor antes de redirecionar e compara o iss devolvido por string exata, sem normalização — sem case folding, sem elisão da porta predefinida, sem barra final. E RFC 7591, Dynamic Client Registration, está agora depreciado em favor de Client ID Metadata Documents, «retained for backwards compatibility with authorization servers that do not support» them.4
Ligue isso em ambos os servidores com um verificador de token que não faz nada além de verificar a audience. A escada de TypeScript:
no token 401 WWW-Authenticate: Bearer error="invalid_token",
error_description="Missing Authorization header",
scope="incidents:read",
resource_metadata="…/.well-known/oauth-protected-resource/mcp"
aud=other server 401 error_description="token audience is not this server"
no exp claim 401 error_description="Token has no expiration time"
right aud, no scope 403 error="insufficient_scope", scope="incidents:read"
right aud + scope 200 {"result":{"tools":[…]}}{"resource":"http://127.0.0.1:8931/mcp",
"authorization_servers":["https://auth.example.com/"],
"scopes_supported":["incidents:read","incidents:write"],
"resource_name":"Incidents"}Ambos os SDKs servem esse documento e ambos apontam um 401 para ele, que é toda a história de discovery: um cliente que nunca viu o seu servidor aprende onde se autenticar a partir de uma recusa. O 403 é outro animal — o token está bom, o scope não — e o desafio nomeia o que falta para que o cliente possa subir de nível em vez de recomeçar.
Dois degraus diferem, e nenhuma diferença está na especificação. O SDK de TypeScript recusa um token sem claim de expiração; o de Python devolve 200, porque expires_at é opcional no seu AccessToken e None significa «sem opinião». E o 403 de Python transporta error_description="Required scope: incidents:read" sem o parâmetro scope que a especificação diz que os servidores devem incluir. Um verificador não é lugar para aceitar uma predefinição da biblioteca: a verificação de audience é sua para escrever em qualquer linguagem, e a expiração também.
Uma nota honesta da mesma execução. Um GET no endpoint respondeu 404 na ligação Express e 400 Bad Request: Missing session ID na de Python, quando a especificação pede 405 Method Not Allowed e quando «session ID» é vocabulário que esta revisão removeu. Nenhuma é perigosa; ambas são a forma de um ecossistema a meio de uma migração.
Onde os servidores vivem realmente
Ligação para a secção: Onde os servidores vivem realmenteA última peça de shipping é onde se publica, e tem uma resposta com número. Rastejado hoje, todos os servidores do registry oficial na sua versão mais recente:7
| servidores | |
|---|---|
| total (versão mais recente, não eliminado) | 28.170 |
| ativos / depreciados | 27.853 / 317 |
| distribuem pelo menos um pacote instalável | 13.065 |
| só remotos — um URL, nada para instalar | 14.696 |
| npm | 8.275 |
| PyPI | 3.603 |
| imagens OCI | 867 |
bundles mcpb | 706 |
| NuGet / Cargo | 107 / 43 |
Duas leituras, em direções opostas. Por servidores publicados, npm lidera 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 teve 286,7 milhões contra @modelcontextprotocol/sdk com 194,7 milhões, 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 no separador Python.1 Qualquer que fosse a metade que tinha na cabeça, a outra metade também é verdadeira.
E a linha que importa mais do que qualquer uma dessas: mais de metade do registry — 14.696 de 28.170 — não tem nada para instalar. São web services. As contagens de transports confirmam pelo outro lado: de 14.290 entradas de pacotes, 13.787 declaram stdio; de 16.640 entradas remotas, 15.570 declaram Streamable HTTP e 1.070 ainda declaram o depreciado HTTP+SSE. Portanto, «um servidor MCP é um subprocesso no seu portátil» descreve uma minoria em diminuição, e cada um dos 14.696 precisa da secção acima em vez de uma variável de ambiente.
Mostrar detalhes
Deliberadamente bilingue, e o precedente para isso.
Este é o único capítulo bilingue do curso, porque a resposta honesta divide-se: o registry é npm-first e os downloads são Python-first, ao mesmo tempo, hoje. Escrever apenas uma das duas linguagens entregaria metade da pergunta e descreveria mal o ecossistema no processo. Há precedente em aberto — o Hugging Face MCP Course lista entre os seus 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 mau lugar para ser monolingue.
Secção datada: tudo acima que tem prazo de validade
Ligação para a secção: Secção datada: tudo acima que tem prazo de validadeLido e medido em 7 de setembro de 2026, contra a revisão do protocolo 2026-07-28.
| valor | |
|---|---|
@modelcontextprotocol/sdk | 1.30.0, publicado a 27 de julho de 2026; 4.322.438 bytes desempacotados, 693 ficheiros, 17 dependências diretas |
| revisão mais recente que implementa | 2025-11-25 |
mcp (PyPI) | 2.1.1, publicado a 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 implementa | 2026-07-28 |
| níveis dos SDKs | TypeScript, Python, C#, Go, Rust em Tier 1; Java, Ruby em Tier 2; Swift, PHP, Kotlin em Tier 3 |
| servidores no registry | 28.170 |
| downloads, últimos 30 dias | mcp 286.653.871 · fastmcp 72.097.269 · @modelcontextprotocol/sdk 194.679.333 |
Uma nota de migração que não é um número. Em mcp 2.x, FastMCP foi renomeado para MCPServer, e quase todos os tutoriais online ainda abrem com o import antigo. O SDK traz um módulo cujo único propósito é explicar isso, que é a depreciação mais atenciosa deste capítulo:
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.Então qual deles
Ligação para a secção: Então qual delesCom a tabela à frente, a recomendação é aborrecida, o que é bom sinal.
Se o servidor vive dentro de uma aplicação web que já executa, escreva-o em TypeScript. Mesmo processo, mesmo deploy, mesmo request handler; Streamable HTTP é um endpoint que adiciona ao lado dos outros; e os 13.9 MiB e os 145 ms são gratuitos porque o runtime já estava em execução. É a maioria dos 14.696 servidores remotos.
Se o servidor envolve tooling de dados, escreva-o em Python. O que está a expor é pandas, um cliente de warehouse, um notebook cheio de transforms, e um servidor noutra linguagem seria uma chamada a subprocesso a usar um schema. Setecentos milissegundos de import num serviço que arranca uma vez não são custo; num subprocesso que um host relança o dia todo, são.
E, por agora, a linha da revisão sobrepõe-se a ambos. Se precisa de 2026-07-28 — pedidos multi-round-trip, resultType, cache hints, server/discover — um dos dois SDKs tem isso hoje e o outro não.
Para onde isto vai a seguir
Ligação para a secção: Para onde isto vai a seguirAgora consegue lançar o mesmo servidor em qualquer uma das linguagens, defender a escolha com uma tabela em vez de uma preferência, executá-lo sobre ambos os transports vivos, e entregar-lhe um token que ele vai recusar.
O que construiu continua a ser uma função: um schema, um endpoint, uma coisa determinística que o modelo invoca. Há toda uma classe de conhecimento que não cabe nessa forma — como nós escrevemos um postmortem, de que campos os nossos relatórios de incidente precisam, a ordem em que fazemos as coisas e porquê. É procedimento, é prosa, e forçá-lo para uma descrição de ferramenta é como system prompts crescem até dois mil tokens pagos em cada turno, quer a conversa seja sobre incidentes quer não.
O Capítulo 28 é a outra resposta: uma pasta com um SKILL.md que o modelo lê em vez de chamar, carregada em três níveis para que o material de referência quase não custe nada até ao turno em que é necessário. Não tem linguagem principal, e essa é a primeira coisa que ensina.
Fontes e método
Ligação para a secção: Fontes e métodoTudo aqui foi medido a 7 de setembro de 2026, em 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 no seu próprio diretório descartável. Os tempos são medianas de 25 lançamentos, wall clock de spawn até à linha que transporta a resposta tools/list; as contagens de tokens são o200k_base via tiktoken sobre o JSON de cada definição. Não foi chamada nenhuma API paga: nada aqui precisa de um modelo.
Os dois servidores têm 81 e 63 linhas não vazias; uma das suas três ferramentas é reproduzida acima em ambas as linguagens, e os outros quatro registos diferem apenas como descrito. A política de divulgação de erros do SDK de Python é citada das docstrings de ToolError e UnexpectedToolError em mcp/server/mcpserver/exceptions.py; a predefiniçã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 do protocolo são LATEST_PROTOCOL_VERSION em mcp_types/version.py e no types.js do SDK de TypeScript, ambas lidas a partir dos pacotes instalados em vez de um changelog.
Referências
Ligação para a secção: Referências-
SDKs,
modelcontextprotocol.io/docs/sdk, e Build an MCP server,modelcontextprotocol.io/docs/develop/build-server, ambos lidos a 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 dos separadores de linguagem do tutorial (Python, TypeScript, Java, Kotlin, C#, Ruby, Rust, Go), e da regra de logging citada sobreprint()estdout. ↩ ↩2 ↩3 ↩4 -
transport stdio,
.../basic/transports/stdio. Fonte do framing por newline e da regra de purezastdout. O Capítulo 26 lê esta página na íntegra; é citada aqui pela linha que o servidor partido viola. ↩ -
MCP Inspector,
modelcontextprotocol.io/docs/2026-07-28/tools/inspector, lido a 7 de setembro de 2026. Um pacote, três clientes por trás de um binário — web,--clie--tui— a partilhar um core, um conjunto de transports e um estado OAuth em disco. A CLI produziu aqui os traces de catálogo. ↩ -
Autorização,
modelcontextprotocol.io/specification/2026-07-28/basic/authorization, lido a 7 de setembro de 2026. Fonte do papel de resource server; das quatro cláusulas de tratamento de tokens citadas na íntegra; do requisito de que os servidores implementem RFC 9728 e os clientes a usem para discovery; das regras do parâmetroresourcee da definição de URI canónico; da tabela de validação de issuer; da depreciação de Dynamic Client Registration; da tabela401/403/400e do desafioinsufficient_scope; e da exceção stdio, «Implementations using an STDIO transport SHOULD NOT follow this specification, and instead retrieve credentials from the environment.» ↩ ↩2 ↩3 ↩4 -
Streamable HTTP,
.../basic/transports/streamable-http, e Visão geral dos transports,.../basic/transports. Fonte da regra POST de endpoint único, do requisito dualAccept, do cabeçalhoMCP-Protocol-Versione da sua regra de correspondência obrigatória com o corpo, dos cabeçalhosMcp-MethodeMcp-Namedescritos como «REQUIRED for compliance», da remoção do stream GET, de sessões e deLast-Event-ID, da orientação405, da validação obrigatóriaOrigin, e da classificação do transport HTTP+SSE de 2024-11-05 como Deprecated sob SEP-2596. ↩ ↩2 ↩3 ↩4 -
As quatro em que a especificação se apoia, com o 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, fevereiro de 2020 — o parâmetroresourcee a audience que associa. Jones, M.B., Hunt, P. e Parecki, A., OAuth 2.0 Protected Resource Metadata, RFC 9728, abril de 2025 — o documento para o qual um401aponta. Meyer zu Selhausen, K. e Fett, D., OAuth 2.0 Authorization Server Issuer Identification, RFC 9207, março de 2022 — o parâmetroisse 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, secção 3, para a forma do desafioWWW-Authenticateacima. ↩ -
Registry MCP oficial,
registry.modelcontextprotocol.io/v0/servers, rastreado a 7 de setembro de 2026 comversion=latest: 282 páginas, 28.170 servidores, contados porregistryTypesobre nomes de servidor distintos. Números de downloads:api.npmjs.org/downloads/point/last-monthpara@modelcontextprotocol/sdk(194.679.333 entre 8 de agosto e 6 de setembro de 2026) epypistats.org/api/packages/<name>/recentparamcpefastmcp, todos lidos no mesmo dia. Tamanhos de pacotes vindos do documento do registry npm e da API JSON do PyPI. ↩ ↩2 -
MCP Course, Hugging Face,
huggingface.co/learn/mcp-course, unidade 0, lido a 7 de setembro de 2026: entre os pré-requisitos, «Experience with at least one programming language (Python or TypeScript examples will be shown)». ↩