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

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.

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

Um 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:

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

A prova de que a linguagem é invisível é um cliente 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-o a cada servidor, por sua 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 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:

keyTypeScriptPython
name2121
description4646
annotations4646
inputSchema211192
outputSchema187
execution27
total342480

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.

Os 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.

tools/call on a tool that raisesTEXT
TypeScript  {"content":[{"type":"text","text":
              "connect ECONNREFUSED 10.0.3.7:5432 (db-prod-eu, user=reporting)"}],
             "isError":true}

Python      {"content":[{"text":"Error executing tool boom","type":"text"}],
             "isError":true}

O SDK de TypeScript 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.

O 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:

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

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

O de Python é 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:

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

Ambos os SDKs instalaram limpo, nos seus próprios diretórios, sem nada partilhado:

TypeScriptPython
pacote@modelcontextprotocol/sdk 1.30.0 + zod 3.25.76mcp 2.1.1
revisão mais recente do protocolo implementada2025-11-252026-07-28
pacotes transitivos instalados9428
tamanho instalado13.9 MiB44.3 MiB
ficheiros em disco3.3862.018
pacotes de terceiros carregados para servir stdio8 de 9418 de 28
arranque do intérprete 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 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ê:

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 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:

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

O launcher custa 568 ms por arranque — 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.

O 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:

Cada 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

Mcp-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

O 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.

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

Tire 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:

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

A ú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 / depreciados27.853 / 317
distribuem pelo menos um pacote instalável13.065
só remotos — um URL, nada para instalar14.696
npm8.275
PyPI3.603
imagens OCI867
bundles mcpb706
NuGet / Cargo107 / 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.

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

valor
@modelcontextprotocol/sdk1.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 implementa2025-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 implementa2026-07-28
níveis dos SDKsTypeScript, Python, C#, Go, Rust em Tier 1; Java, Ruby em Tier 2; Swift, PHP, Kotlin em Tier 3
servidores no registry28.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 é 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:

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 à 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.

Agora 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 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.


Tudo 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.

  1. 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 sobre print() e stdout. 2 3 4

  2. transport stdio, .../basic/transports/stdio. Fonte do framing por newline e da regra de pureza stdout. O Capítulo 26 lê esta página na íntegra; é citada aqui pela linha que o servidor partido viola.

  3. 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, --cli e --tui — a partilhar um core, um conjunto de transports e um estado OAuth em disco. A CLI produziu aqui os traces de catálogo.

  4. 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â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 desafio insufficient_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

  5. Streamable HTTP, .../basic/transports/streamable-http, e Visão geral dos transports, .../basic/transports. Fonte da regra POST de endpoint único, do requisito dual Accept, do cabeçalho MCP-Protocol-Version e da sua regra de correspondência obrigatória com o corpo, dos cabeçalhos Mcp-Method e Mcp-Name descritos como «REQUIRED for compliance», da remoção do stream GET, de sessões e de Last-Event-ID, da orientação 405, da validação obrigatória Origin, e da classificação do transport HTTP+SSE de 2024-11-05 como Deprecated sob SEP-2596. 2 3 4

  6. 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âmetro resource e 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 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, secção 3, para a forma do desafio WWW-Authenticate acima.

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

  8. 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)».

Pronto para deixar a LIA escolher?

Construa com todos os modelos de IA num só sítio — comece grátis hoje.