docs: publish translated docs in twelve languages and the tool that maintains them (#3280)

This commit is contained in:
Max 2026-08-14 17:07:34 +01:00 • committed by GitHub
commit 52ad0a8876
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 89737 additions and 54 deletions

145
i18n/pt/pages/run/asgi.md Normal file
View file

@ -0,0 +1,145 @@
---
translation:
sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 70b9ece244ca1b0c, cba78e052898c3f6, f06bdb541cb0b469, fb82d526320b7cc3]
tool: 1
---
# Adicione a um app existente {#add-to-an-existing-app}
`mcp.run("streamable-http")` inicia um servidor web para você. Às vezes você não quer isso: seu servidor MCP é uma peça de uma aplicação web maior, ou você já tem um deploy ASGI.
Para esses casos, `mcp.streamable_http_app()` retorna uma **aplicação Starlette**.
Um app Starlette é um app ASGI, então qualquer coisa que hospede ASGI (uvicorn, Hypercorn, outro Starlette, FastAPI) pode hospedar seu servidor MCP.
## O app {#the-app}
```python title="server.py" hl_lines="12"
--8<-- "docs_src/asgi/tutorial001.py"
```
`app` é uma aplicação ASGI comum. Entregue-o a qualquer servidor ASGI:
```console
uvicorn server:app
```
O endpoint MCP fica em `/mcp`, então um cliente se conecta a `http://127.0.0.1:8000/mcp`.
O app já carrega duas coisas:
* Uma rota, `/mcp`: o endpoint Streamable HTTP.
* Um **lifespan** que inicia o `mcp.session_manager`, o objeto que é dono do trabalho em segundo plano de cada sessão ativa.
Execute o app sozinho (`uvicorn server:app`) e você nunca precisa pensar em nenhuma das duas.
!!! tip
`streamable_http_app()` aceita os mesmos argumentos nomeados que `mcp.run("streamable-http", ...)`,
menos `port`: a porta pertence a quem quer que sirva o app. `host` ainda é aceito, mas não faz bind
de nada aqui; **[Deploy e escala](deploy.md)** explica o que ele controla de fato.
**[Executando seu servidor](index.md)** cobre as opções em si.
`mcp.sse_app()` faz o mesmo para o transporte SSE, já superado.
## Só localhost, até você dizer o contrário {#localhost-only-until-you-say-otherwise}
Por padrão, o app responde **apenas** a requisições endereçadas ao localhost. `streamable_http_app()`
não tem como saber atrás de qual hostname vai ser servido, então ativa a proteção contra DNS rebinding com a
allowlist mais segura possível; na sua máquina, isso é exatamente o certo. Depois do deploy atrás de um hostname real,
isso significa que **toda requisição é rejeitada com `421 Misdirected Request`** até você passar em
`transport_security=` uma allowlist do que você realmente serve. Nada do que você construiu sequer é
consultado antes. Essa allowlist, e tudo o mais que existe entre um app funcionando e um hostname real,
é assunto de **[Deploy e escala](deploy.md)**.
## Montando o app {#mounting-it}
No momento em que o servidor MCP é *parte* de uma aplicação maior, você coloca o app dentro de um `Mount`. E no momento em que faz isso, o lifespan vira problema seu:
```python title="server.py" hl_lines="18-21 25-26"
--8<-- "docs_src/asgi/tutorial002.py"
```
* `Mount("/", ...)` mais o caminho padrão `/mcp` mantém o endpoint em `/mcp`. O Starlette testa as rotas em ordem e `Mount("/")` casa com **todo** caminho, então suas próprias rotas vão *antes* dele na lista. Qualquer coisa depois dele fica inalcançável.
* A função `lifespan` entra em `mcp.session_manager.run()` pelo tempo de vida do app **host**. Essa é a linha que todo mundo esquece.
* `mcp.session_manager` só existe *depois* que `streamable_http_app()` foi chamado. É por isso que as rotas são construídas no nível do módulo e o manager só é tocado dentro do lifespan.
A rota `Host` do Starlette funciona do mesmo jeito: troque `Mount("/", ...)` por `Host("mcp.example.com", ...)` para rotear por hostname em vez de por caminho. A regra do lifespan não muda, e a de segurança de transporte também não. Uma rota `Host("mcp.example.com", ...)` só recebe requisições endereçadas àquele hostname, mas a allowlist de Host do próprio transporte (**[Deploy e escala](deploy.md)**) ainda roda primeiro. Sem `"mcp.example.com"` nela, essa rota responde a cada uma delas com um `421`.
!!! warning "O app host é dono do lifespan"
`streamable_http_app()` conecta `session_manager.run()` ao lifespan do Starlette que
retorna, mas **o lifespan de uma subaplicação montada nunca roda**. Monte o app e esse
lifespan embutido vira código morto. Seja qual for o app no topo da sua pilha ASGI, ele precisa entrar em
`mcp.session_manager.run()` no próprio lifespan.
!!! check
Apague a linha `lifespan=lifespan` e inicie o servidor. Ele inicia. A rota resolve.
Aí a primeira requisição a `/mcp` falha com:
```text
RuntimeError: Task group is not initialized. Make sure to use run().
```
Nada inicia o session manager a não ser o `run()` dele.
## Dois servidores, um app {#two-servers-one-app}
Cada `MCPServer` é seu próprio app com seu próprio session manager. Monte quantos quiser; entre em cada manager a partir do único lifespan do host:
```python title="server.py" hl_lines="27-30 35-36"
--8<-- "docs_src/asgi/tutorial003.py"
```
* `AsyncExitStack` entra nos dois managers; eles iniciam juntos e encerram na ordem inversa.
* Os endpoints são `/notes/mcp` e `/tasks/mcp`: o prefixo do mount mais o caminho padrão.
## Mudando o caminho {#changing-the-path}
Aquele `/mcp` no final é o `streamable_http_path`. Defina-o como `"/"` e o prefixo do mount vira o caminho público inteiro:
```python title="server.py" hl_lines="25"
--8<-- "docs_src/asgi/tutorial004.py"
```
Agora os clientes se conectam a `/notes`, não a `/notes/mcp`.
## CORS para clientes no navegador {#cors-for-browser-clients}
Um cliente que roda no navegador precisa de duas permissões suas: para **enviar** seus headers de requisição MCP, e para **ler** o que o MCP manda de volta. As duas são configuração de CORS no app host, e a allowlist de segurança de transporte acima precisa concordar com ela:
```python title="server.py" hl_lines="27-30 33 35-49"
--8<-- "docs_src/asgi/tutorial005.py"
```
* `allow_headers` é a metade que todo mundo esquece. O navegador faz **preflight** de toda requisição MCP, porque `Content-Type: application/json` e os headers de requisição `Mcp-*` não estão na safelist do CORS, e um header que o preflight não concede é uma requisição que o navegador nunca envia. (`allow_headers=["*"]` também funciona: o Starlette responde a um preflight com o que quer que ele tenha pedido.)
* `expose_headers=["Mcp-Session-Id"]` é a metade da leitura. O Streamable HTTP retorna o ID de sessão nesse header de resposta, e os navegadores escondem headers de resposta do JavaScript a menos que o CORS os exponha pelo nome. Sem ele, o cliente nunca consegue fazer sua segunda requisição.
* `allow_origins` é decisão sua, não do MCP. Seja preciso, e espelhe isso em `allowed_origins=` acima: o navegador impõe o CORS, mas o servidor verifica `Origin` por conta própria, e uma origem em que o transporte não confia recebe um `403` mesmo depois de um preflight limpo.
* `allow_methods` lista os três métodos que o Streamable HTTP usa: `POST` para enviar mensagens, `GET` para abrir o stream do servidor para o cliente, `DELETE` para encerrar a sessão.
## Rotas customizadas {#custom-routes}
`@mcp.custom_route()` registra um endpoint HTTP comum no mesmo app, para as coisas que todo serviço em produção precisa e que não têm nada a ver com MCP: um health check, um callback OAuth.
```python title="server.py" hl_lines="15-17"
--8<-- "docs_src/asgi/tutorial006.py"
```
* O handler é Starlette puro: uma função `async` de `Request` para `Response`.
* `streamable_http_app()` recolhe toda rota customizada. `app.routes` agora é `/mcp` e `/health`.
* `GET /health` responde `{"status": "ok"}` sem MCP nenhum à vista.
!!! warning
Rotas customizadas **nunca são autenticadas**, mesmo quando o resto do servidor é. Isso é
proposital: health checks e callbacks OAuth precisam estar acessíveis antes de existir qualquer token.
Não coloque nada privado atrás de uma delas.
## Recapitulando {#recap}
* `mcp.streamable_http_app()` retorna um app Starlette com uma rota, `/mcp`. Qualquer servidor ASGI consegue executá-lo.
* Por padrão, o app responde apenas a requisições endereçadas ao localhost, e atrás de um hostname real rejeita tudo com um `421` até você passar em `transport_security=` uma allowlist. **[Deploy e escala](deploy.md)** cuida disso, e do resto do caminho até a produção.
* `Mount` (ou `Host`) o coloca dentro de um app Starlette ou FastAPI maior.
* **Montar desativa o lifespan embutido.** O lifespan do app host precisa entrar em `mcp.session_manager.run()`, ou a primeira requisição falha.
* Vários servidores em um app significa vários mounts e um lifespan que entra em cada session manager.
* `streamable_http_path="/"` move o endpoint para o próprio prefixo do mount.
* Clientes no navegador precisam de CORS: `allow_headers` para os headers de requisição `Mcp-*`, `expose_headers=["Mcp-Session-Id"]` para a resposta.
* `@mcp.custom_route()` adiciona endpoints HTTP comuns, sem autenticação, ao lado de `/mcp`.
Com o servidor acessível em uma URL real, **[O cliente](../client/index.md)** se conecta a ele com essa URL em vez de um objeto servidor.

View file

@ -0,0 +1,130 @@
---
translation:
sections: [d62c13457fc4a534, 80e73abaca6e0652, d1dc4c54cd00ec9c, 14ad3bc7904036bb, 5225f127bc1b9c77, fe1626fdd5aad1da, 4556cb7ea1a04a31]
tool: 1
---
# Autorização {#authorization}
Sobre Streamable HTTP, seu servidor MCP é um serviço web comum, e você o protege como protege qualquer serviço web: com bearer tokens do OAuth 2.1.
Nos termos do OAuth, seu servidor é um **resource server**. Ele nunca autentica ninguém e nunca emite um token. Ele faz uma coisa só: olha o header `Authorization` de cada requisição e decide se o token que está ali é válido.
Esta página é o lado do servidor. Um cliente que descobre seu servidor de autorização e busca o token está em **[Clientes OAuth](../client/oauth-clients.md)**.
## As três partes {#the-three-parties}
* O **servidor de autorização** autentica as pessoas e emite tokens de acesso. Você não escreve isso. É o seu provedor de identidade (Auth0, Keycloak, Entra, o seu próprio).
* O **resource server** é o seu servidor MCP. Ele verifica o token em cada requisição.
* O **cliente** descobre em qual servidor de autorização você confia, obtém um token dele e o envia de volta para você como `Authorization: Bearer <token>`.
O triângulo inteiro é esse. Tudo nesta página é o item do meio.
## Um verificador de tokens {#a-token-verifier}
O SDK não tem opinião sobre como é um token válido. Você diz a ele, implementando **`TokenVerifier`**:
```python title="server.py" hl_lines="12-14 19-24"
--8<-- "docs_src/authorization/tutorial001.py"
```
* `TokenVerifier` é um protocolo com um único método assíncrono. `verify_token` recebe o token bruto do header `Authorization` e retorna um **`AccessToken`** se ele for válido, `None` se não for. Não há mais nada a implementar.
* Este aqui procura o token em uma tabela. Um de verdade verifica a assinatura de um JWT ou chama o endpoint de introspecção de tokens do servidor de autorização. Esse código é seu; o SDK apenas o chama.
* `token_verifier=` e `auth=` sempre andam juntos. Passe um sem o outro e `MCPServer(...)` levanta um `ValueError` antes mesmo de atender uma requisição.
`AuthSettings` é a face pública do seu resource server:
* `issuer_url`: o servidor de autorização que emite seus tokens.
* `resource_server_url`: a URL pública deste endpoint MCP. Ela indica *a qual* recurso um token se destina, e é onde fica o documento de descoberta.
* `required_scopes`: todo token deve conter todos eles.
!!! tip
`examples/servers/simple-auth/` no repositório do SDK tem um `IntrospectionTokenVerifier` que chama
o endpoint da [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662) de um servidor de autorização real. É o formato que a maioria dos verificadores de produção tem.
## O que você recebe sobre HTTP {#what-you-get-over-http}
A autorização vive em headers HTTP, então só existe nos transportes HTTP. Execute-a no transporte em que você faz o deploy: `mcp.run(transport="streamable-http")` a coloca em `http://127.0.0.1:8000/mcp`, e **[Executando seu servidor](index.md)** tem o resto. O app agora tem duas rotas:
```text
/mcp
/.well-known/oauth-protected-resource/mcp
```
Você registrou uma ferramenta. A segunda rota é do SDK.
### Descoberta {#discovery}
Faça um `GET` nesse caminho well-known e você recebe o **Protected Resource Metadata da [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)**, montado direto a partir do seu `AuthSettings`:
```json
{
"resource": "http://127.0.0.1:8000/mcp",
"authorization_servers": ["https://auth.example.com/"],
"scopes_supported": ["notes:read"],
"bearer_methods_supported": ["header"]
}
```
Esse documento é como um cliente que nunca ouviu falar do seu servidor encontra o caminho de entrada: ele lê `authorization_servers` e vai até lá buscar um token. Você não escreveu nada disso.
!!! check
Chame `/mcp` sem token (ou com um para o qual seu verificador retornou `None`) e a requisição é
barrada na porta:
```text
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", error_description="Authentication required", resource_metadata="http://127.0.0.1:8000/.well-known/oauth-protected-resource/mcp"
{"error": "invalid_token", "error_description": "Authentication required"}
```
Nada foi parseado e nenhuma ferramenta foi executada. E aquele ponteiro `resource_metadata` em `WWW-Authenticate` é
o que torna a descoberta automática: 401 -> documento de metadados -> servidor de autorização -> token -> nova tentativa.
!!! warning
Nada disso protege o `stdio`. Um pipe não tem header `Authorization`, então `token_verifier` nunca é
consultado ali. A fronteira de segurança de um servidor `stdio` é o processo que o iniciou. O mesmo
vale para o `Client(mcp)` em memória que você usa nos testes: ele se conecta direto ao objeto do servidor
e pula a camada HTTP, autorização incluída.
## A identidade de quem chama {#the-callers-identity}
Dentro de qualquer handler, **`get_access_token()`** é o `AccessToken` que seu verificador retornou para a requisição atual:
```python title="server.py" hl_lines="4 32-35"
--8<-- "docs_src/authorization/tutorial002.py"
```
* Funciona em ferramentas, recursos e prompts, e não há nada para passar adiante: o middleware de autenticação o guarda em uma variável de contexto por requisição.
* Você recebe de volta o **mesmo objeto que seu verificador montou**: `client_id`, `scopes`, `subject`, `expires_at` e quaisquer `claims` extras que você anexou. Esse é o gancho para regras por ferramenta: leia os escopos e recuse.
* Fora de uma requisição HTTP autenticada, ele retorna `None`. Em memória e sobre `stdio`, é sempre `None`.
Chame `whoami` com `Authorization: Bearer alice-token` e o modelo lê:
```text
alice (scopes: notes:read)
```
## A metade que o SDK não faz {#the-half-the-sdk-doesnt-do}
O SDK entrega a metade do resource server: verificar, anunciar, recusar. Ele não entrega uma página de login, uma tela de consentimento nem um token.
Para ver as três partes em ação, execute `examples/servers/simple-auth/` do repositório do SDK (um pequeno servidor de autorização e um resource server configurado exatamente como nesta página) e então aponte `examples/clients/simple-auth-client/` para ele e veja a dança completa de descoberta e token.
!!! info
Existe um segundo argumento do construtor, `auth_server_provider=`, que embute um servidor de autorização
completo dentro do seu servidor MCP. Ele é anterior à separação AS/RS em torno da qual a especificação
de autorização do MCP foi construída. Servidores novos não devem recorrer a ele.
Um servidor de autorização também pode aceitar a asserção assinada de um provedor de identidade corporativo no lugar de um usuário clicando em uma tela de consentimento, e o SDK dá suporte aos dois lados dessa troca. O grant, e o cliente que o apresenta, está em **[Asserção de identidade](../client/identity-assertion.md)**.
## Recapitulando {#recap}
* Sobre Streamable HTTP, seu servidor é um **resource server** do OAuth 2.1: ele verifica tokens, nunca os emite.
* `TokenVerifier` é toda a superfície de integração: um método assíncrono, token entra, `AccessToken | None` sai.
* `token_verifier=` e `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` sempre andam juntos.
* O SDK publica o Protected Resource Metadata da [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) em `/.well-known/oauth-protected-resource/...` e responde a requisições não autenticadas com um 401 cujo header `WWW-Authenticate` aponta para ele. A história da descoberta é toda essa.
* `get_access_token()` em qualquer handler diz quem está chamando.
* Autorização é assunto do HTTP. O `stdio` e o cliente em memória nunca a veem.
A metade do cliente (descobrir seu servidor de autorização e buscar o token para você) está em **[Clientes OAuth](../client/oauth-clients.md)**. E um cliente que *afirma* uma identidade em vez de pedir uma ao usuário está em **[Asserção de identidade](../client/identity-assertion.md)**.

179
i18n/pt/pages/run/deploy.md Normal file
View file

@ -0,0 +1,179 @@
---
translation:
sections: [28221886b198784f, f88ea1f1614f3a1d, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, e758745df6fb7b0a]
tool: 1
---
# Deploy e escala {#deploy-scale}
Seu servidor funciona. Agora ele precisa de um hostname de verdade, e de mais de um worker por trás dele.
Quase nada disso é assunto do MCP. Você traz o servidor ASGI, o gerenciador de processos, o balanceador de carga. O que esta página tem é a lista curta das coisas que *são* assunto do MCP: uma configuração que bloqueia todo deploy, e os dois lugares em que "mais de um worker" muda o que o SDK faz.
## Antes de qualquer coisa: a allowlist de Host {#before-anything-else-the-host-allowlist}
`streamable_http_app()` não tem como saber atrás de qual hostname vai ser servido, então assume a resposta mais segura: localhost. Sem `transport_security=`, o app liga a **proteção contra DNS rebinding** e só aceita uma requisição se o header `Host` dela for `127.0.0.1:<port>`, `localhost:<port>` ou `[::1]:<port>`. O header `Origin`, quando existe, tem que ser a forma `http://` do mesmo valor. Na sua máquina isso é exatamente o certo: impede que uma página web maliciosa controle seu servidor local através de um nome DNS que ela religou para `127.0.0.1`.
Depois do deploy atrás de um hostname de verdade, esse mesmo padrão rejeita **toda requisição** até você dizer o contrário. A verificação roda antes de qualquer coisa com cara de MCP, então nada do que você construiu chega a ser consultado:
```text
421 Misdirected Request Invalid Host header the Host is not in the allowlist
403 Forbidden Invalid Origin header the Origin is not in the allowlist
```
`transport_security=` é a correção. Coloque na allowlist o que você realmente serve:
```python title="server.py" hl_lines="2 13-17"
--8<-- "docs_src/deploy/tutorial001.py"
```
* As entradas de `allowed_hosts` são strings exatas: `"mcp.example.com"` casa com um header `Host` sem porta e `"mcp.example.com:*"` casa com qualquer porta. Liste as duas.
* `allowed_origins` só importa para navegadores, porque nada mais envia `Origin`. É o par, do lado do servidor, da configuração de CORS em **[Adicione a um app existente](asgi.md)**.
* Atrás de um proxy reverso que já controla o header `Host`, desligar a verificação é a configuração honesta: `TransportSecuritySettings(enable_dns_rebinding_protection=False)`.
* Passar um `host=` que não seja localhost (por exemplo `host="mcp.example.com"`) **não** coloca esse hostname na allowlist. Só impede que o padrão de localhost arme a proteção, o que deixa todo Host e todo Origin aceitos. Diga o que você quer dizer com `transport_security=` em vez disso.
!!! check
Apague o argumento `transport_security=security` e faça o deploy do app mesmo assim. Ele sobe, `/mcp`
roteia, e toda requisição (inclusive de um `curl` simples) volta assim:
```text
HTTP/1.1 421 Misdirected Request
Invalid Host header
```
Você não vai encontrar essas palavras do lado do cliente. Um `421` é uma resposta HTTP em texto puro, não um
erro JSON-RPC, então o cliente MCP levanta um erro genérico de transporte; o hostname de que ele
não gostou aparece só no log do **servidor**, como um único warning. Um servidor recém-implantado
que recusa toda conexão é uma allowlist de Host até que se prove o contrário.
**[Solução de problemas](../troubleshooting.md)** também começa por aqui.
## Workers, e quem precisa de afinidade {#workers-and-who-has-to-be-sticky}
Quando o hostname responder, coloque mais de um worker atrás dele. Não há botão no SDK para isso; você escala um app Starlette do jeito que escala qualquer app ASGI, entregando o objeto a algo que saiba fazer fork:
```console
uvicorn server:app --workers 4
```
Quatro processos, um socket. E agora a pergunta que todo deploy tem que responder: **uma requisição precisa chegar ao worker que viu a anterior?**
Para um cliente que fala o protocolo **2026-07-28**, não. Uma requisição moderna é um único POST autocontido: nenhum handshake `initialize` antes dela, nenhum `Mcp-Session-Id` na resposta, nada *para onde* uma segunda requisição possa voltar. Roteie para qualquer worker.
Isso não é um modo que você liga. `stateless_http=True` parece que deveria ser, mas o transporte roteia pelo header de requisição `MCP-Protocol-Version`, entrega uma requisição moderna ao handler moderno e **retorna**. A linha que lê `stateless_http` vem *depois* desse retorno. Não é que a flag seja ignorada no caminho 2026-07-28; ela nunca é alcançada. `stateless_http` é um botão só para o ramo **legado**, e o caminho moderno é sem sessão por construção.
Para um cliente legado na versão de spec 2025-11-25 ou anterior, a resposta depende dessa flag:
| Versão de protocolo do cliente | Sessão | O que o balanceador de carga precisa fazer |
| --- | --- | --- |
| **2026-07-28** | Nenhuma. `Mcp-Session-Id` nunca é definido. | Nada. Qualquer worker atende qualquer requisição. |
| **2025-11-25 e anteriores** (o padrão) | `Mcp-Session-Id`, guardado na memória de um worker. | **Sessões com afinidade (sticky sessions).** Uma requisição seguinte que chega a outro worker recebe um `404` *"Session not found"*. |
| **2025-11-25 e anteriores**, com `stateless_http=True` | Nenhuma. | Nada. O custo é o canal de retorno (back-channel) do servidor para o cliente (amostragem (sampling), elicitação por push, `roots/list`) e a retomada de streams. |
Sessões com afinidade e o que o ramo legado custa têm sua própria página, **[Atendendo clientes legados](legacy-clients.md)**; as duas eras em si são **[Versões do protocolo](../protocol-versions.md)**. O que importa aqui é o formato da resposta: *no 2026-07-28 você já é stateless, sem nada para configurar.*
O resto desta página são as duas coisas que ser stateless **não** te compra.
## `requestState` entre workers {#requeststate-across-workers}
Uma ferramenta (tool) **[de múltiplas idas e voltas](../handlers/multi-round-trip.md)** precisa de algo que o cliente tem que ir buscar (uma confirmação, uma escolha, uma credencial), então ela retorna uma pergunta em vez de uma resposta e termina na nova tentativa. Entre as duas rodadas o cliente segura um token opaco `request_state` que o servidor cunhou. Na nova tentativa o servidor tem que abrir esse token de novo.
*Selado com qual chave?* Por padrão, uma que o servidor gerou com `os.urandom(32)` no momento da construção. Com `--workers 4` são quatro construções, em quatro processos: quatro chaves diferentes, nunca gravadas em lugar nenhum, nunca compartilhadas, perdidas no restart.
Aqui está uma ferramenta que pergunta antes de agir, em um servidor que não configura nada:
```python title="server.py" hl_lines="14 20"
--8<-- "docs_src/deploy/tutorial002.py"
```
A primeira rodada chega ao worker A. O worker A sela `refund:120` com a chave **dele** e retorna o token. O cliente coloca a pergunta na frente de uma pessoa, recebe um sim e tenta de novo. A nova tentativa é uma requisição HTTP novinha em folha.
!!! check
Deixe essa nova tentativa chegar ao worker B. B tenta abrir um token que não cunhou, não consegue e recusa a
rodada inteira. `refund` nunca é chamado; o cliente recebe um erro JSON-RPC:
```json
{
"code": -32602,
"message": "Invalid or expired requestState",
"data": {"reason": "invalid_request_state"}
}
```
Essa mensagem é **fixa**. Expirado, adulterado, reenviado contra argumentos diferentes ou (de
longe a causa mais comum em um deploy real) selado por um worker irmão: o cliente ouve
a mesma coisa toda vez, então o que trafega nunca revela qual verificação falhou. O motivo real é um
`WARNING` no log do servidor:
```text
requestState rejected on tools/call: unknown key
```
Uma ferramenta de múltiplas idas e voltas que funcionava com um worker e começou a falhar *às vezes* com
dois é isto. As duas rodadas ainda precisam chegar ao mesmo processo, então ela falha exatamente na mesma
frequência com que seu balanceador de carga as separa.
As duas rodadas são duas requisições HTTP independentes, e várias coisas corriqueiras as separam: um proxy que balanceia por requisição, uma conexão que caiu no meio, um deploy ou um restart, um cliente que persistiu o `request_state` e está retomando de um processo totalmente diferente (**[Conduzindo o loop por conta própria](../handlers/multi-round-trip.md#driving-the-loop-yourself)**). Qualquer uma delas é "um worker diferente".
A correção é um argumento. Ela tem **duas** metades.
```python title="server.py" hl_lines="1 12 14"
--8<-- "docs_src/deploy/tutorial003.py"
```
* **`keys=[...]`** é a metade que todo mundo encontra. Dê a cada instância o mesmo segredo (pelo menos 32 bytes dele), e toda instância consegue abrir o que qualquer irmã cunhou. `keys[0]` sela e toda chave da lista abre, e esse é o anel de rotação; **[Rotacionando chaves](../handlers/multi-round-trip.md#rotating-keys)** mostra como girá-lo sem downtime.
* **O nome do servidor** é a metade que quase ninguém encontra, e o motivo pelo qual novas tentativas entre instâncias continuam falhando depois que você compartilha a chave. Todo token selado carrega o `name` do servidor como uma **claim de audiência**, verificada estritamente na volta. Duas instâncias construídas a partir do mesmo código têm o mesmo nome e nunca percebem isso. Dê nomes diferentes a elas (`MCPServer(f"billing-{POD}")` parece boa higiene de observabilidade), e toda nova tentativa entre instâncias é recusada exatamente como acima, com ou sem chave compartilhada. O log diz `audience` em vez de `unknown key`; o cliente não consegue distinguir.
Cunhe o segredo uma vez e entregue o mesmo valor a toda instância. Este é o comando que a própria mensagem de erro do SDK manda você rodar se passar menos de 32 bytes para ele:
```console
python -c "import secrets; print(secrets.token_hex(32))"
```
!!! warning "Mesmas chaves *e* o mesmo nome"
Um deploy com múltiplas instâncias precisa compartilhar os dois. Se nomes por instância são essenciais para você,
dê à frota uma audiência explícita em vez disso: `RequestStateSecurity(keys=[...], audience="billing")`.
Toda instância então cunha e aceita sob `"billing"`, não importa como se chame.
Todo o resto sobre o selo está em **[Protegendo o `requestState`](../handlers/multi-round-trip.md#protecting-requeststate)**: o que ele vincula, o `ttl` por rodada (600 segundos por padrão), trazer seu próprio codec, por que o padrão não configurado é exatamente o certo em `stdio`. A contribuição inteira desta página é um checklist de dois itens: *mesmas chaves, mesmo nome.*
!!! info
Você está neste caminho mesmo que nunca tenha digitado `InputRequiredResult`. Uma ferramenta cujos parâmetros
usam `Resolve(...)` (**[Dependências](../handlers/dependencies.md)**) é uma ferramenta de múltiplas idas e voltas,
e o SDK cunha e sela o `request_state` dela por ela. Mesma chave padrão, mesma falha entre
workers, mesma correção.
## Notificações de mudança entre réplicas {#change-notifications-across-replicas}
O stream `subscriptions/listen` de um cliente é uma única resposta de longa duração, então fica preso a uma réplica pela vida toda. Um `ctx.notify_resource_updated(...)` publicado em uma réplica **diferente** tem que chegar até ele.
A costura entre os dois é o `SubscriptionBus`. Qualquer bus que você dê a um servidor é aquele em que toda publicação entra e que todo stream aberto escuta, então entregue o mesmo bus a toda réplica:
```python title="server.py" hl_lines="2 7 9"
--8<-- "docs_src/deploy/tutorial004.py"
```
Nada no fan-out se importa com qual objeto de servidor um stream está ligado. Dois servidores segurando um único `InMemorySubscriptionBus` já se comportam assim: abra um stream de listen em um, `edit_note` no outro, e o stream fica sabendo. Esse bus em memória só abrange objetos de servidor dentro de um processo, o que faz dele o modelo, não o deploy:
* Entre processos de verdade, **o SDK não traz nenhum bus que possa te ajudar.** `SubscriptionBus` é um `Protocol` de dois métodos (`publish` e `subscribe`) que você implementa sobre seu próprio backend de pub/sub (Redis, NATS, o que você já roda) e passa como `MCPServer(subscriptions=...)`. **[Assinaturas](../handlers/subscriptions.md#scaling-past-one-process)** tem o esboço e o contrato.
* O bus carrega quatro pequenos eventos tipados, nunca JSON-RPC. Confirmação, filtragem e ciclo de vida do stream ficam no SDK, então seu bus não consegue quebrar o protocolo; ele só consegue mover eventos entre processos.
* Streams **não** são retomáveis e eventos **não** são reenviados. Perder uma réplica derruba os streams dela; os clientes escutam de novo e buscam de novo. Não há event store para compartilhar e nada mais para configurar. Este é o único lugar em que escalar horizontalmente é de fato só mais do mesmo.
## O que o SDK não te dá {#what-the-sdk-does-not-give-you}
Um `MCPServer` é uma implementação de protocolo, não um servidor de aplicação. Os botões de deploy que você vai procurar em seguida estão ausentes de propósito:
* **Sem `workers=`.** `mcp.run("streamable-http")` inicia exatamente um processo uvicorn, e isso é tudo o que ele jamais vai iniciar. Multiprocesso é `streamable_http_app()` entregue ao que você já usa para fazer deploy de ASGI: `uvicorn --workers`, gunicorn, o gerenciador de processos da sua plataforma. Esta página deliberadamente não é um tutorial de nenhum deles; a documentação deles é melhor do que uma cópia aqui seria.
* **Sem rota de health check.** `@mcp.custom_route("/health", methods=["GET"])` é a resposta inteira, e nunca é autenticada mesmo quando o resto do servidor é. Isso está certo para uma sonda de liveness, errado para qualquer coisa privada. **[Adicione a um app existente](asgi.md#custom-routes)** mostra uma.
* **Sem objeto de configurações de produção.** Não há lugar no `MCPServer` para anotar timeouts, TLS, shutdown gracioso ou limites de conexão, porque nada disso é trabalho dele. Isso pertence ao seu servidor ASGI, e você configura lá. **[Executando seu servidor](index.md)** cobre o punhado de configurações que o construtor *de fato* aceita.
* **Nenhum `EventStore` incluído, e no 2026-07-28 nenhum uso para um.** A retomada de streams é uma funcionalidade do ramo legado com estado; uma troca moderna é um POST, uma resposta, e nada para retomar.
## Recapitulando {#recap}
* Por padrão, o app responde apenas a requisições endereçadas ao localhost. `transport_security=TransportSecuritySettings(allowed_hosts=[...], allowed_origins=[...])` é o portão para ir ao ar: até você passar isso, toda requisição atrás de um hostname de verdade é um `421` e o motivo só está no log do servidor.
* No 2026-07-28 não há sessão e nada em que um balanceador de carga possa ter afinidade. `stateless_http=True` é um botão só para o legado porque uma requisição moderna é roteada e respondida antes de essa flag ser lida.
* A chave padrão do `requestState` é `os.urandom(32)`, cunhada por processo. Uma nova tentativa de múltiplas idas e voltas que chega a um worker diferente falha com `-32602` *"Invalid or expired requestState"*.
* A correção é `RequestStateSecurity(keys=[...])` **e** o mesmo nome de servidor em toda instância. O nome é a claim de audiência padrão do token. Mesmas chaves, mesmo nome.
* Notificações de mudança atravessam réplicas por um único `SubscriptionBus` compartilhado. A única implementação do SDK é dentro do processo; o `Protocol` de dois métodos sobre seu próprio pub/sub é seu para escrever.
* Não há `workers=`, nem rota de health, nem objeto de configurações de produção. Traga seu próprio servidor ASGI.
A outra coisa que um hostname de verdade precisa na frente dele é um token: **[Autorização](authorization.md)**.

156
i18n/pt/pages/run/index.md Normal file
View file

@ -0,0 +1,156 @@
---
translation:
sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 7a53ead3e704a7f0, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0]
tool: 1
---
# Executando seu servidor {#running-your-server}
`mcp.run()` inicia o servidor.
A única decisão que você toma é o **transporte**: como os bytes entre seu servidor e o cliente realmente trafegam.
## Escolha um transporte {#pick-a-transport}
| Transporte | O que é | Quando |
|---|---|---|
| `stdio` | O host inicia seu arquivo como um subprocesso e conversa pelo stdin e stdout dele. | Servidores locais. O padrão. |
| `streamable-http` | Um servidor HTTP de verdade, escutando em uma porta. | Tudo o que você faz deploy. |
| `sse` | O transporte HTTP antigo. | Nunca. |
!!! warning
O SSE foi substituído pelo Streamable HTTP na revisão 2025-03-26 do protocolo.
`mcp.run(transport="sse")` ainda funciona, com suas próprias opções `sse_path=` e `message_path=`,
mas existe apenas para clientes que ainda não migraram. Não construa nada novo em cima dele.
## `mcp.run()` {#mcprun}
```python title="server.py" hl_lines="12-13"
--8<-- "docs_src/run/tutorial001.py"
```
* `run()` é síncrono. Ele bloqueia durante toda a vida do servidor.
* Sem argumentos, o transporte é `stdio`.
* Ele fica sob `if __name__ == "__main__":` porque tudo o que carrega seu servidor (`mcp dev`, `mcp run`, `mcp install`, seus testes) **importa** este arquivo. A guarda impede que um import vire um servidor em execução.
### stdio {#stdio}
Não há nada para configurar. O host inicia seu arquivo como processo filho, escreve requisições no stdin dele e lê respostas do stdout.
Execute você mesmo e veja a consequência:
```console
python server.py
```
Nada é impresso, e ele não retorna. Está esperando no stdin que um host fale primeiro.
Isso também significa que o stdout **é o canal de comunicação**. Enquanto serve, o SDK move esse canal para um descritor privado e desvia para o stderr a saída que é *descarregada* (flushed) no stdout (um subprocesso escrevendo no stdout herdado, um `print()` com flush), onde ela não pode corromper o fluxo. A saída descarregada no stdout *antes* de o servidor começar a servir (um script wrapper fazendo echo, um print sem buffer em tempo de import) ainda cai no canal, assim como um `print()` que fica no buffer até o interpretador esvaziá-lo na saída. Para a saída que você realmente quer, o módulo `logging` é a ferramenta certa: o handler dele descarrega cada registro no stderr assim que acontece. Essa história está em **[Logging](../handlers/logging.md)**.
### Experimente {#try-it}
```console
uv run mcp dev server.py
```
O Inspector faz exatamente o que um host de verdade faz: inicia `server.py` como subprocesso e se conecta a ele via stdio.
Você nunca informou uma porta. Não existe nenhuma.
## Streamable HTTP {#streamable-http}
Para colocar o mesmo servidor em uma porta, nomeie o transporte (e suas opções) em `run()`:
```python title="server.py" hl_lines="13"
--8<-- "docs_src/run/tutorial002.py"
```
Essa única linha monta um app Starlette e o serve com uvicorn. Os clientes se conectam em `http://127.0.0.1:3001/mcp`.
Cada transporte tem seus próprios argumentos nomeados, todos em `run()`:
* `host` / `port`: onde escutar. Padrões `127.0.0.1` e `8000`.
* `streamable_http_path`: onde fica o endpoint MCP. Padrão `/mcp`.
* `json_response=True`: responde a cada POST com um único corpo JSON em vez de um fluxo SSE. Esse corpo tem espaço para a resposta e nada mais, então uma ferramenta que chama o cliente de volta no meio da requisição (`ctx.elicit()`, amostragem (sampling)) lança `NoBackChannelError` nesse trecho, e as notificações ligadas à chamada em andamento (progresso de `ctx.report_progress()`, mensagens de log por chamada) são descartadas; o fluxo `GET` avulso continua transportando as que não têm relação.
* `stateless_http=True`: um transporte novo por requisição, sem rastreamento de sessão.
* `max_request_body_size`: maior corpo de POST aceito, em bytes. O padrão é 4 MiB; requisições maiores
recebem HTTP 413 antes do parsing ou da criação da sessão. Aumente apenas quando mensagens MCP legítimas
ultrapassarem esse tamanho.
* `event_store`, `retry_interval`, `transport_security`: retomada e proteção contra DNS rebinding. Podem esperar até você fazer o deploy em algum lugar que não seja o localhost; **[Deploy e escala](deploy.md)** cobre `transport_security`.
!!! warning
As opções de transporte vão para `run()`, **não** para `MCPServer(...)`. O construtor descreve o que
seu servidor *é*: nome, versão, instruções. `run()` descreve como ele é servido. Inverta isso
e o Python responde antes mesmo de o MCP entrar em cena:
```text
TypeError: MCPServer.__init__() got an unexpected keyword argument 'port'
```
`run()` é o caminho curto. No momento em que você precisar de mais (seu servidor montado dentro de um app existente, dois servidores em um só processo, CORS para clientes no navegador), monte o app ASGI você mesmo e entregue a qualquer host ASGI. Isso está em **[Adicione a um app existente](asgi.md)**.
## Configurações do servidor {#server-settings}
Algumas coisas relacionadas à execução não dizem respeito ao transporte. São argumentos do construtor:
```python title="server.py" hl_lines="3"
--8<-- "docs_src/run/tutorial003.py"
```
* `log_level`: passado para `logging.basicConfig()` no momento em que `MCPServer(...)` é construído. Isso configura o logger **raiz**, então define o nível dos seus próprios loggers também, não só os do SDK. Padrão `"INFO"`.
* `debug`: repassado ao app Starlette que os transportes HTTP montam. Padrão `False`.
Ambos vão parar em `mcp.settings`, que você pode ler de volta em tempo de execução.
## O comando `mcp` {#the-mcp-command}
O extra `[cli]` instala uma pequena ferramenta de linha de comando em torno de tudo isso.
`mcp dev` executa seu servidor sob o **MCP Inspector**:
```console
uv run mcp dev server.py
uv run mcp dev server.py --with pandas --with numpy
uv run mcp dev server.py --with-editable .
```
`--with` adiciona pacotes ao ambiente que ele monta; `--with-editable` instala seu próprio pacote nele. Ele precisa de `npx` no seu `PATH`: o Inspector é um app Node.js.
`mcp run` importa o arquivo, encontra o objeto do servidor (um `mcp`, `server` ou `app` no nível do módulo) e chama `run()` nele:
```console
uv run mcp run server.py
uv run mcp run server.py:bookshop
```
O sufixo `:` nomeia o objeto quando ele não se chama `mcp`, `server` ou `app`.
Seu bloco `if __name__ == "__main__":` nunca executa aqui: o próprio `mcp run` chama `run()`, e a única opção que ele repassa é `--transport`.
`mcp install` registra o servidor no **Claude Desktop**, para que o app o inicie por você:
```console
uv run mcp install server.py --name "Bookshop"
uv run mcp install server.py -v API_KEY=abc123 -f .env
```
`-v KEY=VALUE` e `-f .env` gravam variáveis de ambiente nessa entrada. O Claude Desktop inicia seu servidor em um processo próprio. O ambiente do seu shell não está lá.
O Claude Desktop é o único host que `mcp install` conhece. Todos os outros hosts (Claude Code, Cursor, VS Code) aceitam o mesmo comando de inicialização no próprio arquivo de configuração, e **[Conecte a um host de verdade](../get-started/real-host.md)** tem cada um deles.
`mcp version` imprime a versão do SDK instalada.
!!! tip
`mcp dev` e `mcp run` só entendem `MCPServer`. Se você constrói com o `Server` de baixo nível,
você mesmo o executa. Veja **[O Server de baixo nível](../advanced/low-level-server.md)**.
## Recapitulando {#recap}
* Um **transporte** é como os bytes chegam ao seu servidor: `stdio` para um subprocesso local, `streamable-http` para uma porta. O SSE foi substituído.
* `mcp.run()` escolhe o transporte. Sem argumentos é `stdio`, e ele bloqueia.
* Toda opção de transporte (`host`, `port`, `streamable_http_path`, ...) é um argumento de `run()`, nunca de `MCPServer(...)`.
* Mantenha `run()` sob `if __name__ == "__main__":`. Tudo o que carrega seu servidor importa o arquivo primeiro.
* `log_level=` e `debug=` são argumentos do construtor; eles vão parar em `mcp.settings`.
* `mcp dev` para o Inspector, `mcp run` para executar um arquivo, `mcp install` para o Claude Desktop, `mcp version` para a versão.
* O transporte nunca muda o que seu servidor *é*: os três arquivos desta página expõem exatamente a mesma ferramenta.
Quando o próprio `run()` é o limite (seu servidor dentro de um app que já existe), o caminho é **[Adicione a um app existente](asgi.md)**. Um hostname de verdade e mais de um worker é **[Deploy e escala](deploy.md)**. E se alguns dos seus clientes ainda estão na versão 2025-11-25 da especificação ou anterior, **[Servindo clientes legados](legacy-clients.md)** traz as boas notícias.

View file

@ -0,0 +1,132 @@
---
translation:
sections: [3d1663c18edc824c, d4fd37009a13f03d, af9f398a5a8b679a, 470c2dd144294d69, 8e45827e6d24e8c8, 91dfd0ce98ebb03c]
tool: 1
---
# Atendendo clientes legados {#serving-legacy-clients}
O MCP tem duas eras de protocolo: a era do handshake `initialize`, até a versão da especificação `2025-11-25`, e a era moderna, `2026-07-28`. **[Versões do protocolo](../protocol-versions.md)** é a página sobre a divisão em si.
Esta página trata do lado do servidor dessa divisão, e a resposta cabe em uma frase: **o `streamable_http_app()` que você já faz o deploy atende as duas.**
O SDK roteia cada requisição pelo header `MCP-Protocol-Version`. Uma requisição que indica `2026-07-28` vai para o handler moderno. Uma requisição que indica uma versão da era do handshake, ou que não traz header nenhum (que é como o `initialize` de um cliente pré-2026 chega), vai para o transporte que esses clientes esperam: handshake `initialize`, sessões e tudo mais. Isso acontece por requisição, antes do seu código, no mesmo app.
Então um cliente legado não é algo *para* o qual você constrói. É algo que se conecta *ao* servidor que você já escreveu. Você não configura nada.
!!! note
Nada, literalmente. Não existe opção `legacy=`, nem allowlist de versões, nem forma de rejeitar ou
desabilitar uma era: nem em `streamable_http_app()`, nem em `run()`, nem no gerenciador de sessões.
As duas eras estão sempre ativas. O mais próximo de uma chave por era nessa assinatura é
`stateless_http`, e ele é a maior parte desta página.
## Um handler, as duas eras {#one-handler-both-eras}
Aqui está uma ferramenta (tool) que precisa perguntar algo ao usuário, e clientes das duas eras chamando-a:
```python title="server.py" hl_lines="24 37-38"
--8<-- "docs_src/legacy_clients/tutorial001.py"
```
`reserve` precisa de uma coisa que o modelo não forneceu: quantas cópias. `Annotated[..., Resolve(ask_quantity)]` é como uma ferramenta declara isso (**[Dependências](../handlers/dependencies.md)** tem essa história completa). Nada em `reserve` cita uma versão, verifica uma capacidade ou ramifica.
Os dois clientes ficam abertos **ao mesmo tempo**, no mesmo objeto `mcp`. `mode="legacy"` executa o handshake `initialize`: exatamente a conexão que um cliente pré-2026 abre. O outro usa o padrão e cai em `2026-07-28`.
```text
2025-11-25 {'result': "Reserved 2 of 'Dune'."}
2026-07-28 {'result': "Reserved 2 of 'Dune'."}
```
Mesmo servidor, mesmo handler, mesma resposta. A funcionalidade inteira é essa.
Vale parar no *como*, porque os dois clientes receberam a mesma pergunta por dois fios completamente diferentes. A conexão `2026-07-28` não tem canal para o servidor enviar uma requisição, então `Resolve` retornou a pergunta dentro do resultado da ferramenta e o cliente repetiu a chamada com a resposta (**[Requisições de múltiplas idas e voltas](../handlers/multi-round-trip.md)**). A conexão `2025-11-25` não tem nada disso; ali, `Resolve` enviou uma requisição `elicitation/create` ao vivo no meio da chamada e esperou. Você não escreveu nenhum dos dois. `Resolve` lê a versão negociada da conexão e escolhe; o corpo da sua ferramenta vê um `AcceptedElicitation` de qualquer forma.
!!! tip
Essa portabilidade entre eras é *o motivo* de `Resolve` ser a API sobre a qual construir. Seu irmão mais velho, `ctx.elicit()`
(**[Elicitação](../handlers/elicitation.md)**), só envia `elicitation/create`, então só
funciona em uma conexão legada. Em uma `2026-07-28`, a chamada falha. Se uma ferramenta ainda o usa,
a correção é a que você vê acima, não uma verificação de versão.
## Quanto uma sessão legada custa para você {#what-a-legacy-session-costs-you}
O roteamento é grátis. A sessão não.
Uma conexão `2026-07-28` é **sem sessão**: cada requisição é independente, e o handler moderno nunca emite um `Mcp-Session-Id`. Uma conexão legada é o oposto. No momento em que um cliente pré-2026 envia `initialize`, o SDK gera um `Mcp-Session-Id`, retorna-o em um header de resposta e mantém um registro vivo por trás dele para as requisições posteriores do cliente encontrarem: a versão negociada, os streams abertos, uma task em segundo plano conduzindo a sessão.
Esse registro é **um `dict` simples dentro do processo**. Não existe armazenamento distribuído de sessões nem forma de plugar um.
Com um worker, isso é invisível. Com dois, é o problema inteiro: uma requisição que traz um `Mcp-Session-Id` e cai em um worker que não o gerou não encontra nada naquele dict, e a resposta é um `404` (`Session not found`), não o resultado da ferramenta. Então, no momento em que você executa mais de um worker, **clientes legados precisam de roteamento sticky**: toda requisição de uma sessão tem que chegar ao processo que a iniciou. Clientes modernos nunca precisam; eles não têm sessão à qual aderir. **[Deploy e escala](deploy.md)** cobre stickiness e tudo mais sobre executar mais de uma dessas instâncias.
!!! warning
`event_store=` parece a solução e não é. Ele é **retomabilidade** (reenviar eventos SSE
perdidos para um cliente que se reconecta à *mesma* sessão), não um armazenamento de sessões. Ele nunca torna uma
sessão alcançável a partir de outro processo.
## A única chave: `stateless_http` {#the-one-knob-stateless_http}
Se stickiness é um custo que você se recusa a pagar, existe exatamente uma coisa que você pode mudar.
```python title="server.py" hl_lines="28"
--8<-- "docs_src/legacy_clients/tutorial002.py"
```
Esse é o servidor do topo da página mais uma keyword. `stateless_http=True` faz a perna legada construir uma sessão descartável por requisição: nenhum `Mcp-Session-Id` emitido, nada lembrado entre requisições, então qualquer worker pode atender qualquer requisição e o load balancer pode fazer o que quiser.
Duas coisas sobre ele importam mais do que o que ele faz.
**Ele só afeta a perna legada.** As requisições são roteadas pelo header de versão *antes* de `stateless_http` ser lido, então o caminho moderno nunca o vê. Uma conexão `2026-07-28` já é sem sessão e fica exatamente igual com qualquer um dos valores.
**Ele custa os dois canais servidor-para-cliente nessa perna.** Uma sessão que vive por um `POST` não tem stream para o servidor empurrar uma requisição nem stream independente para empurrar notificações. Toda requisição iniciada pelo servidor levanta `NoBackChannelError`: `ctx.elicit()`, as chamadas aposentadas de amostragem (sampling) e roots (**[Funcionalidades obsoletas](../deprecated.md)**) e, sim, `Resolve` fazendo sua pergunta a um cliente *legado*. As notificações nem recebem erro; são descartadas silenciosamente.
!!! note
`json_response=True` não é essa chave, mas cobra metade do mesmo custo em *toda* sessão
legada: um `POST` respondido com um único corpo JSON não tem stream para o canal com escopo de requisição,
então um `ctx.elicit()` no meio da requisição levanta o mesmo `NoBackChannelError` e as notificações ligadas à
requisição são descartadas. O stream independente da sessão fica intacto: notificações não relacionadas
continuam chegando.
!!! check
Faça a coisa errada. `reserve` é exatamente a ferramenta que acabou de atender os dois clientes. Faça o deploy dela com
`stateless_http=True`, conecte os mesmos dois clientes via HTTP e chame-a de cada um.
O cliente moderno ainda recebe `Reserved 2 of 'Dune'.` A perna moderna não mudou.
A chamada do cliente legado não volta como um resultado `is_error` que o modelo poderia ler.
A requisição inteira falha, como um erro de protocolo de nível superior:
```text
mcp.shared.exceptions.MCPError: Cannot send 'elicitation/create': this transport context has no back-channel for server-initiated requests.
```
`Resolve` não salvou você. Em uma conexão `2025-11-25` ele *tem* que enviar `elicitation/create`,
e o canal de que precisa é exatamente o que `stateless_http=True` abriu mão. Código
portável entre eras não é código sem canal de retorno (back-channel).
Então é uma troca real, e ela só existe na perna legada: **com sessão e sticky, ou sem estado e unidirecional.** Se suas ferramentas nunca chamam o cliente de volta, `stateless_http=True` é grátis e você deve usá-lo. Se chamam, mantenha as sessões e mantenha o roteamento sticky.
## Onde seu código realmente se bifurca {#where-your-code-actually-forks}
Quase em lugar nenhum.
Ferramentas, recursos, prompts, saída estruturada, progresso, erros: nenhum deles se importa com qual era chamou. O handshake `initialize`, o `Mcp-Session-Id`, o stream independente, o `DELETE` que encerra uma sessão: o SDK cuida de tudo isso, e um handler nunca vê nada disso. Entrada interativa é *o* lugar em que as eras genuinamente diferem no fio, e `Resolve` existe para que isso não seja problema seu: você acabou de ver uma ferramenta atender as duas.
Sobra exatamente uma coisa, e são as **notificações de mudança**, porque as duas eras escutam em canais diferentes:
* Um cliente `2026-07-28` abre um stream `subscriptions/listen` e lê o barramento de assinaturas. `ctx.notify_resource_updated()` (e `notify_tools_changed()`, `notify_prompts_changed()`, `notify_resources_changed()`) publicam ali, e *somente* ali. **[Assinaturas](../handlers/subscriptions.md)** é essa página.
* Um cliente legado lê o stream independente que sua sessão mantém aberto. `ctx.session.send_resource_updated()` (e `send_tool_list_changed()` e companhia) escrevem na *conexão* que carregou a requisição: para uma sessão legada, esse é o stream independente dela. Uma conexão moderna não tem lugar para isso: via HTTP não existe tal canal, e via stdio os quatro tipos de notificação de mudança trafegam apenas em streams `subscriptions/listen`, então em uma conexão moderna a notificação é descartada silenciosamente.
Via HTTP, nenhuma das duas chamadas alcança os clientes da outra era. Para avisar todo mundo, chame as duas:
```python title="server.py" hl_lines="19-20"
--8<-- "docs_src/legacy_clients/tutorial003.py"
```
Duas linhas, nenhum `if`, nenhuma verificação de versão, e pronto. Essa é a lista inteira de coisas que um handler faz diferente porque um cliente legado existe.
## Recapitulando {#recap}
* Um único `streamable_http_app()` atende as duas eras de protocolo. O SDK roteia cada requisição pelo header `MCP-Protocol-Version`; não há nada para configurar nem chave de era para procurar.
* Um cliente legado custa uma sessão: um registro `Mcp-Session-Id` dentro do processo, sem armazenamento distribuído por trás. Mais de um worker significa **roteamento sticky**, ou o worker errado responde `404 Session not found`. **[Deploy e escala](deploy.md)** tem a história completa de múltiplos workers.
* `stateless_http=True` é a única chave, e ela vale **apenas para a perna legada**. Ela compra balanceamento de carga livre para clientes legados ao preço dos dois canais servidor-para-cliente nessa perna: requisições iniciadas pelo servidor levantam `NoBackChannelError` (um erro de nível superior no cliente, não um resultado `is_error`), e as notificações são descartadas.
* Uma conexão `2026-07-28` é sem sessão de qualquer forma. `stateless_http` nunca a afeta.
* O código do seu handler se bifurca por era em exatamente um lugar: notificações de mudança. `ctx.notify_*` alcança clientes `subscriptions/listen`; `ctx.session.send_*` alcança sessões legadas. Chame os dois.
* Todo o resto (incluindo pedir entrada ao usuário, via `Resolve`) é portável entre eras por construção. Escreva a versão moderna uma vez só.

View file

@ -0,0 +1,117 @@
---
translation:
sections: [bc0227014724fa49, 15738c2f7fd67d86, a2c17bbe3f707e2f, d0d853376f162c06, b6368643fcc1c8d8, 902e33e17564a607]
tool: 1
---
# OpenTelemetry {#opentelemetry}
Seu servidor já é rastreado. Você não precisa adicionar nada.
Todo servidor que você cria emite um span do [OpenTelemetry](https://opentelemetry.io/) para cada
mensagem que processa. Você não escreveu isso e não importa isso. Está lá no momento em que você
chama `MCPServer(...)`.
```python title="server.py"
--8<-- "docs_src/opentelemetry/tutorial001.py"
```
Esse é um servidor completo e rastreado. Chame `search_books` e um span é criado para a chamada. O
mesmo vale para o `Server` de baixo nível: o rastreamento vive nos dois.
## O que você recebe {#what-you-get}
Cada mensagem recebida vira um span `SERVER` com o nome do método e do seu alvo. Então um
`tools/call` para `search_books` é o span `tools/call search_books`, e um `tools/list` simples
é apenas `tools/list`.
Cada span carrega alguns atributos:
* `mcp.method.name` e `mcp.protocol.version`, em todo span.
* `jsonrpc.request.id`, em uma requisição (uma notificação não tem).
* Um handler que lança uma exceção define o status do span como erro. Um resultado de ferramenta com `is_error=True` também.
E como rastrear uma chamada de ferramenta é algo tão comum de se querer, os spans `tools/call`
falam as [convenções semânticas GenAI](https://opentelemetry.io/docs/specs/semconv/gen-ai/) do OpenTelemetry:
* `gen_ai.operation.name`, definido como `"execute_tool"`.
* `gen_ai.tool.name`, definido como a ferramenta sendo chamada.
Um span `prompts/get` recebe `gen_ai.prompt.name` no mesmo espírito. Os métodos de listagem não
carregam chaves `gen_ai.*`, porque não há nada para nomear.
!!! tip
Esses atributos GenAI são o motivo pelo qual uma interface de rastreamento agrupa suas chamadas
de ferramenta do mesmo jeito que agrupa as de qualquer outro agente. Você ganha esse agrupamento
de graça, sem código extra.
## Não custa nada até você querer {#it-costs-nothing-until-you-want-it}
Aqui está a parte que faz de "ligado por padrão" um padrão confortável.
O SDK depende apenas de `opentelemetry-api`, a metade leve do OpenTelemetry. Sem nenhum SDK e
nenhum exporter instalados, criar um span é um no-op. Então os spans que seu servidor está
emitindo agora mesmo não custam quase nada, e ninguém os está coletando.
No dia em que você quiser *vê-los*, instale a outra metade e aponte-a para algum lugar:
```console
uv add opentelemetry-sdk opentelemetry-exporter-otlp
```
Configure um exporter do jeito habitual do OpenTelemetry, e cada span que o SDK vinha criando
em silêncio se acende. O código do seu servidor não muda. Nem uma linha.
!!! info
O [Pydantic Logfire](https://logfire.pydantic.dev/) é um desses backends, e faz a
configuração para você: `pip install logfire`, `logfire.configure()`, e seus spans MCP
aparecem na visualização ao vivo. Ele é construído sobre o OpenTelemetry, então tudo o que
vem abaixo também se aplica a ele.
## Traces que atravessam a rede {#traces-that-cross-the-wire}
Um trace é mais útil quando acompanha uma requisição do cliente até o servidor, em uma única
imagem conectada.
Quando o cliente e o servidor rodam o SDK, essa conexão é automática. O cliente injeta o
[contexto de trace W3C](https://www.w3.org/TR/trace-context/) na requisição, e o servidor o lê de
volta, de modo que o span do servidor fica aninhado sob o span do cliente no mesmo trace. Isso é a
[SEP-414](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/414), e você ganha isso
sem pedir.
Se a mensagem recebida não carrega contexto de trace, por exemplo uma requisição de um cliente que
não é o SDK, o span do servidor simplesmente fica sob o span que já estiver ativo no servidor, em
vez de iniciar um trace órfão novo.
## Desligando {#turning-it-off}
O rastreamento é um middleware, o primeiro da lista do seu servidor. Se você quer mesmo um servidor
que não emite spans, retire-o:
```python
from mcp.server._otel import OpenTelemetryMiddleware
mcp._lowlevel_server.middleware[:] = [
m for m in mcp._lowlevel_server.middleware if not isinstance(m, OpenTelemetryMiddleware)
]
```
!!! warning
Esse import tem um underscore inicial, e isso é de propósito. A classe é provisória, do mesmo
jeito que [`Server.middleware`](../advanced/middleware.md) é provisório, então o caminho de
import é algo que você deve esperar que mude. Você quase nunca precisa disso: sem um exporter
instalado os spans são gratuitos, então a resposta habitual é deixá-los ligados e não instalar
um exporter.
## Recapitulando {#recap}
* Todo `MCPServer` e todo `Server` de baixo nível emite um span `SERVER` por mensagem recebida,
por padrão. Você não escreve nada.
* Os spans carregam `mcp.method.name` e `mcp.protocol.version`; `tools/call` e `prompts/get`
também carregam atributos GenAI para que suas chamadas de ferramenta se agrupem como as de
qualquer outro agente.
* Não custa nada até você instalar um SDK do OpenTelemetry e um exporter, e aí tudo se acende
sem nenhuma mudança no seu servidor.
* O contexto de trace do cliente para o servidor se propaga automaticamente quando os dois lados
rodam o SDK.
O que decide se uma requisição chega a rodar é a **[Autorização](authorization.md)**.