docs: publish translated docs in twelve languages and the tool that maintains them (#3280)
This commit is contained in:
parent
14aa889992
commit
52ad0a8876
6 changed files with 89737 additions and 54 deletions
145
i18n/pt/pages/run/asgi.md
Normal file
145
i18n/pt/pages/run/asgi.md
Normal 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.
|
||||
130
i18n/pt/pages/run/authorization.md
Normal file
130
i18n/pt/pages/run/authorization.md
Normal 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
179
i18n/pt/pages/run/deploy.md
Normal 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
156
i18n/pt/pages/run/index.md
Normal 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.
|
||||
132
i18n/pt/pages/run/legacy-clients.md
Normal file
132
i18n/pt/pages/run/legacy-clients.md
Normal 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ó.
|
||||
117
i18n/pt/pages/run/opentelemetry.md
Normal file
117
i18n/pt/pages/run/opentelemetry.md
Normal 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)**.
|
||||
Loading…
Reference in a new issue