docs: publish translated docs in twelve languages and the tool that maintains them (#3280)
This commit is contained in:
parent
14aa889992
commit
52ad0a8876
50 changed files with 89737 additions and 54 deletions
165
i18n/pt/pages/advanced/apps.md
Normal file
165
i18n/pt/pages/advanced/apps.md
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
---
|
||||
translation:
|
||||
sections: [0355618e5f4d5fe4, 1821eaf50f2d0b64, 82e0b28ebd3abf5a, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487]
|
||||
tool: 1
|
||||
---
|
||||
# MCP Apps {#mcp-apps}
|
||||
|
||||
Um **MCP App** é uma ferramenta (tool) com uma cara: junto com os dados, a ferramenta aponta para um
|
||||
documento HTML que o host renderiza como uma superfície interativa.
|
||||
|
||||
Duas partes, sempre duas partes:
|
||||
|
||||
1. **Uma ferramenta** que faz o trabalho e retorna dados, como qualquer outra ferramenta.
|
||||
2. **Um recurso `ui://`** contendo o HTML que o host mostra para ela.
|
||||
|
||||
A ferramenta carrega uma referência `_meta.ui.resourceUri` ao recurso. O host busca esse
|
||||
recurso com `resources/read`, renderiza em um **iframe em sandbox** e envia o resultado
|
||||
da ferramenta para dentro desse iframe via `postMessage`. Seu servidor nunca envia nem recebe
|
||||
nenhuma mensagem `ui/*`: esse tráfego fica entre o host e o iframe. Você serve uma ferramenta
|
||||
e um documento HTML; o host cuida do espetáculo.
|
||||
|
||||
O SDK entrega isso como a extensão embutida `Apps` (`io.modelcontextprotocol/ui`).
|
||||
Se [Extensões](extensions.md) são novidade para você, dê uma olhada naquela página primeiro. Um minuto,
|
||||
e depois volte aqui.
|
||||
|
||||
## Um relógio com uma cara {#a-clock-with-a-face}
|
||||
|
||||
```python title="server.py" hl_lines="19 22 30 32"
|
||||
--8<-- "docs_src/apps/tutorial001.py"
|
||||
```
|
||||
|
||||
Quatro movimentos:
|
||||
|
||||
* `Apps()`: uma única instância guarda suas ferramentas ligadas a UI e os recursos delas.
|
||||
* `@apps.tool(resource_uri="ui://clock/app.html")`: uma ferramenta comum, mais o
|
||||
carimbo `_meta.ui.resourceUri`. Tudo o que `@mcp.tool()` aceita (name, title,
|
||||
description, ...) passa direto.
|
||||
* `apps.add_html_resource("ui://clock/app.html", CLOCK_HTML)`: o recurso
|
||||
correspondente, servido como `text/html;profile=mcp-app`. É exatamente esse MIME type que
|
||||
diz ao host "isto é um app, renderize".
|
||||
* `MCPServer("clock", extensions=[apps])`: você opta por participar. O servidor agora anuncia
|
||||
`io.modelcontextprotocol/ui` em `capabilities.extensions`.
|
||||
|
||||
O HTML em si escuta o `postMessage` do host e mostra o resultado. Para apps
|
||||
de verdade, use o SDK de navegador oficial [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps)
|
||||
dentro do seu HTML. Ele dá a você `ontoolresult`, `callServerTool`,
|
||||
`getHostContext` e `onhostcontextchanged` em vez de eventos de mensagem crus.
|
||||
|
||||
## Degradação elegante {#graceful-degradation}
|
||||
|
||||
Nem todo cliente renderiza apps. A especificação é direta sobre o que isso significa para você:
|
||||
|
||||
> As ferramentas **DEVEM** retornar um array `content` significativo mesmo quando há UI disponível.
|
||||
|
||||
O modelo lê `content`; o iframe é para humanos. Um host com suporte a UI ainda passa
|
||||
o resultado em texto para o modelo, e um cliente só de texto recebe *apenas* isso. Então o
|
||||
padrão canônico é uma ferramenta, duas respostas. Olhe `get_time` de novo:
|
||||
|
||||
```python title="server.py" hl_lines="23-27"
|
||||
--8<-- "docs_src/apps/tutorial001.py"
|
||||
```
|
||||
|
||||
`client_supports_apps(ctx)` é `True` somente quando o cliente declarou a
|
||||
extensão `io.modelcontextprotocol/ui` **e** listou `text/html;profile=mcp-app`
|
||||
nas suas configurações `mimeTypes`. O campo é obrigatório, então um cliente que o omite
|
||||
não conta. É exatamente isso que `main()` no mesmo arquivo declara: a
|
||||
metade cliente da negociação, e a resposta rica volta.
|
||||
|
||||
!!! warning
|
||||
Nunca retorne um placeholder como `"[Rendered UI]"` como único conteúdo. Se o
|
||||
texto de fallback é inútil, a ferramenta é inútil para todo cliente só de texto e para
|
||||
o próprio modelo. Escreva a frase.
|
||||
|
||||
## Trancando o iframe {#locking-the-iframe-down}
|
||||
|
||||
O lado do recurso carrega os metadados de segurança: o que o iframe pode carregar, quais
|
||||
permissões do navegador ele quer, como gostaria de ser enquadrado:
|
||||
|
||||
```python title="server.py" hl_lines="9 19-22"
|
||||
--8<-- "docs_src/apps/tutorial002.py"
|
||||
```
|
||||
|
||||
`csp` e `permissions` são **pedidos ao host**, não comportamento do servidor. O host
|
||||
monta a Content-Security-Policy e a Permissions-Policy do iframe a partir deles, e
|
||||
pode recusar. Faça detecção de funcionalidade no seu JS em vez de presumir que foi concedido.
|
||||
|
||||
`ResourceCsp`, campo por campo (nome em Python, chave no protocolo, o que o host faz com ele):
|
||||
|
||||
| Python | Protocolo (`_meta.ui.csp`) | Controla |
|
||||
|---|---|---|
|
||||
| `connect_domains` | `connectDomains` | `connect-src`: para onde `fetch`/XHR podem ir |
|
||||
| `resource_domains` | `resourceDomains` | `img-src`, `style-src`, ...: assets estáticos |
|
||||
| `frame_domains` | `frameDomains` | `frame-src`: iframes aninhados |
|
||||
| `base_uri_domains` | `baseUriDomains` | `base-uri`: para onde `<base>` pode apontar |
|
||||
|
||||
`ResourcePermissions`: cada campo solicita uma permissão do navegador para o iframe.
|
||||
|
||||
| Python | Protocolo (`_meta.ui.permissions`) |
|
||||
|---|---|
|
||||
| `camera` | `camera` |
|
||||
| `microphone` | `microphone` |
|
||||
| `geolocation` | `geolocation` |
|
||||
| `clipboard_write` | `clipboardWrite` |
|
||||
|
||||
!!! note
|
||||
CSP e permissões vivem no **recurso**, nunca na ferramenta. Os metadados de ferramenta
|
||||
da especificação não têm lugar para eles, e os hosts os ignoram ali. O SDK torna o
|
||||
erro irrepresentável: `@apps.tool()` simplesmente não tem parâmetro `csp`.
|
||||
|
||||
### Visibilidade {#visibility}
|
||||
|
||||
`visibility=["app"]` em uma ferramenta diz "isto existe para o iframe, não para o modelo":
|
||||
|
||||
* `"model"`: o modelo pode chamá-la.
|
||||
* `"app"`: o iframe pode chamá-la (via `callServerTool`).
|
||||
* Omitido: ambos, que é o padrão.
|
||||
|
||||
Filtrar é trabalho do **host**. Seu servidor lista as ferramentas só de app em `tools/list`
|
||||
como qualquer outra; o host as esconde do modelo. Não filtre no lado do servidor.
|
||||
|
||||
## As regras que o SDK impõe {#the-rules-the-sdk-enforces}
|
||||
|
||||
Todas estas falham na inicialização, não em produção:
|
||||
|
||||
* Um `resource_uri` ou URI de recurso que não seja `ui://...` é um `ValueError` no
|
||||
momento da decoração/registro.
|
||||
* Uma ferramenta ligada a uma URI **sem recurso registrado correspondente** é um `ValueError`
|
||||
quando `MCPServer(extensions=[apps])` consome a extensão. Uma ferramenta que anuncia
|
||||
um HTML que dá 404 em `resources/read` é uma configuração errada, então o servidor se recusa
|
||||
a ser construído.
|
||||
* `meta={"ui": ...}` em `@apps.tool()` é um `ValueError`. O decorator é dono de
|
||||
`_meta["ui"]`; diga isso com `resource_uri=` e `visibility=`. Outras chaves em `meta=`
|
||||
são mescladas normalmente ao lado.
|
||||
|
||||
Nem o SDK ext-apps em TypeScript nem o FastMCP pegam nenhum desses casos hoje; preferimos
|
||||
que você descubra antes que um host descubra.
|
||||
|
||||
## Além do HTML inline {#beyond-inline-html}
|
||||
|
||||
`add_html_resource` cobre o caso comum: uma string de HTML. Para qualquer outra coisa,
|
||||
HTML em disco ou conteúdo gerado, construa o recurso você mesmo e entregue:
|
||||
|
||||
```python title="server.py" hl_lines="12 18"
|
||||
--8<-- "docs_src/apps/tutorial003.py"
|
||||
```
|
||||
|
||||
`add_resource` preenche o MIME type `text/html;profile=mcp-app` quando o recurso
|
||||
não define um explicitamente, e rejeita uma incompatibilidade explícita: um recurso `ui://`
|
||||
sob qualquer outro MIME type é um que nenhum host vai renderizar.
|
||||
|
||||
!!! tip
|
||||
Mirando um host pré-GA que ainda lê a chave plana depreciada
|
||||
`_meta["ui/resourceUri"]`? Mescle você mesmo:
|
||||
`@apps.tool(resource_uri="ui://x", meta={"ui/resourceUri": "ui://x"})`.
|
||||
O objeto `ui` aninhado é o formato da especificação; a chave plana está de saída.
|
||||
|
||||
## Veja rodando {#see-it-run}
|
||||
|
||||
A história `apps` em `examples/stories/` é esta página como um par executável: um servidor
|
||||
com uma ferramenta de relógio ligada a UI e um cliente que negocia Apps, lê o
|
||||
`_meta.ui.resourceUri` da ferramenta, busca o HTML e chama a ferramenta.
|
||||
|
||||
```bash
|
||||
uv run python -m stories.apps.client
|
||||
```
|
||||
172
i18n/pt/pages/advanced/extensions.md
Normal file
172
i18n/pt/pages/advanced/extensions.md
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
---
|
||||
translation:
|
||||
sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff]
|
||||
tool: 1
|
||||
---
|
||||
# Extensões {#extensions}
|
||||
|
||||
Uma **extensão** é um pacote opcional de comportamento MCP reunido sob um único identificador.
|
||||
|
||||
Em um servidor, ela pode contribuir com ferramentas (tools), recursos e novos métodos de requisição, e pode envolver `tools/call`. Em um cliente, ela pode reivindicar formatos extras de resultado de `tools/call` e observar notificações de fornecedores. Cada lado se anuncia no seu próprio `capabilities.extensions`, e nada muda para quem não pediu nada. Esse é o contrato ([SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)), e ele tem uma regra de ouro: **extensões vêm desligadas por padrão**.
|
||||
|
||||
## Usando uma extensão {#using-an-extension}
|
||||
|
||||
Passe as instâncias na construção:
|
||||
|
||||
```python title="server.py"
|
||||
--8<-- "docs_src/extensions/tutorial001.py"
|
||||
```
|
||||
|
||||
Pronto. O servidor agora anuncia `io.modelcontextprotocol/ui` em `capabilities.extensions` e serve tudo o que a extensão contribui.
|
||||
|
||||
`Apps` é a extensão de referência embutida, e ela tem uma página própria: **[MCP Apps](apps.md)**.
|
||||
|
||||
!!! note
|
||||
As extensões são fixadas na construção. Não existe um `add_extension` para chamar depois: o mapa de capacidades de um servidor não deve mudar enquanto há clientes conectados a ele.
|
||||
|
||||
O mapa de capacidades viaja em `server/discover`, que é um caminho da **2026-07-28**. Um handshake `initialize` legado não tem onde colocá-lo, então um cliente legado simplesmente não enxerga a extensão. Projete pensando nisso: uma extensão *amplia* um servidor, ela não pode ser a única forma de usá-lo.
|
||||
|
||||
## Escrevendo a sua {#writing-your-own}
|
||||
|
||||
Herde de `Extension` e sobrescreva apenas o que precisar. Todo método tem um padrão.
|
||||
|
||||
### O identificador {#the-identifier}
|
||||
|
||||
```python
|
||||
--8<-- "docs_src/extensions/tutorial002.py"
|
||||
```
|
||||
|
||||
O identificador é uma string `vendor-prefix/name` que segue a gramática de chaves `_meta` da especificação: rótulos separados por ponto (cada um começa com uma letra e termina com uma letra ou dígito), uma barra e então o nome. Ele é validado **quando a classe é definida**, então um erro de digitação não espera o servidor subir:
|
||||
|
||||
```text
|
||||
TypeError: Stamps.identifier must be a `vendor-prefix/name` string
|
||||
(reverse-DNS prefix required), got 'stamps'
|
||||
```
|
||||
|
||||
Use como prefixo um domínio que você controla. `io.modelcontextprotocol/*` é reservado para extensões especificadas pelo próprio projeto MCP.
|
||||
|
||||
### Contribuindo com ferramentas {#contributing-tools}
|
||||
|
||||
A menor extensão útil é uma ferramenta e um mapa de configurações:
|
||||
|
||||
```python title="server.py" hl_lines="17 19-20 22-23 26"
|
||||
--8<-- "docs_src/extensions/tutorial003.py"
|
||||
```
|
||||
|
||||
* `tools()` retorna `ToolBinding`s. O servidor registra cada uma exatamente como se você tivesse chamado `mcp.add_tool(...)` por conta própria: mesma geração de schema, mesma injeção de `Context`, tudo igual.
|
||||
* `settings()` é o valor anunciado em `capabilities.extensions["com.example/stamps"]`. Retorne `{}` (o padrão) para anunciar a extensão sem configurações.
|
||||
* A extensão nunca recebe o servidor. Ela declara contribuições como dados; o `MCPServer` as consome. Não existe um `self.server` para modificar.
|
||||
|
||||
E `main()` é a prova, um cliente em memória direto contra `mcp`:
|
||||
|
||||
```python title="server.py" hl_lines="29-34"
|
||||
--8<-- "docs_src/extensions/tutorial003.py"
|
||||
```
|
||||
|
||||
### Servindo seus próprios métodos {#serving-your-own-methods}
|
||||
|
||||
Uma extensão pode registrar **novos métodos de requisição**: seus próprios verbos, servidos ao lado dos da especificação:
|
||||
|
||||
```python title="server.py" hl_lines="16-22 31 40-48"
|
||||
--8<-- "docs_src/extensions/tutorial004.py"
|
||||
```
|
||||
|
||||
* `SearchParams` herda de `RequestParams`, então o envelope `_meta` de 2026 é analisado de forma uniforme e seu handler recebe parâmetros validados, nunca um dict cru. Limite o que o cliente controla: `Field(ge=1, le=100)` rejeita um `limit` absurdo antes que seu código aloque qualquer coisa para ele.
|
||||
* `require_client_extension(ctx, EXTENSION_ID)` é a barreira: um cliente que não declarou a extensão recebe o erro `-32021` (capacidade obrigatória do cliente ausente), com o payload `requiredCapabilities` legível por máquina que a especificação pede.
|
||||
* `protocol_versions=frozenset({"2026-07-28"})` fixa o método em uma única versão de protocolo. Em qualquer outra versão o cliente recebe `METHOD_NOT_FOUND`, exatamente como se o método não existisse ali. Para esse cliente, não existe.
|
||||
|
||||
Os métodos são **estritamente aditivos**. O SDK impõe isso na construção, não em tempo de execução:
|
||||
|
||||
* Um `MethodBinding` para um método definido pela especificação (`tools/list`, `completion/complete`, ...) lança `ValueError` quando o binding é construído. Os verbos centrais pertencem ao servidor.
|
||||
* Duas extensões vinculando o mesmo método lançam quando a segunda se registra. A última escrita vencer é como plugins corrompem uns aos outros; não fazemos isso.
|
||||
* Um conjunto `protocol_versions` vazio também lança: um método que nunca pode ser servido é um bug, não uma configuração.
|
||||
|
||||
### O lado do cliente {#the-client-side}
|
||||
|
||||
O `main()` do mesmo arquivo é a história inteira do cliente, com as duas metades:
|
||||
|
||||
```python title="server.py" hl_lines="54-58"
|
||||
--8<-- "docs_src/extensions/tutorial004.py"
|
||||
```
|
||||
|
||||
* `Client(..., extensions=[advertise(EXTENSION_ID)])` declara a extensão. As declarações viram `ClientCapabilities.extensions`: em uma conexão 2026-07-28 o mapa viaja no envelope `_meta` de cada requisição, então o servidor o vê em **toda** requisição; em uma conexão legada ele vai no handshake `initialize`. O código do servidor não se importa com qual: `require_client_extension(ctx, ...)` e `ctx.session.check_client_capability(...)` leem a fonte certa nos dois caminhos.
|
||||
* Métodos de fornecedor descem uma camada para `client.session.send_request(...)`; `Client` só ganha métodos de primeira classe para verbos da especificação. `send_request` aceita qualquer subclasse de `Request`, então a requisição do fornecedor passa como está.
|
||||
|
||||
### Interceptando `tools/call` {#intercepting-toolscall}
|
||||
|
||||
O único hook interceptador. Sobrescreva `intercept_tool_call` para observar, curto-circuitar ou vetar uma chamada de ferramenta:
|
||||
|
||||
```python title="server.py" hl_lines="17-24"
|
||||
--8<-- "docs_src/extensions/tutorial005.py"
|
||||
```
|
||||
|
||||
* `params` é o `CallToolRequestParams` validado: você recebe `params.name` e `params.arguments` sem tocar em JSON cru. É também o que decide qual chamada de ferramenta é executada: passar um contexto reescrito por `call_next` muda o que o handler observa em `ctx`, não a invocação da ferramenta. Reescrita de requisição no nível do protocolo pertence ao [Middleware](middleware.md).
|
||||
* `call_next(ctx)` executa o resto da cadeia e retorna o resultado do handler. Retorne-o sem alterações (observar), retorne outra coisa (substituir) ou lance um `MCPError` (recusar). O que você retornar é serializado como qualquer resultado de handler, incluindo o carimbo de identidade `serverInfo` da era 2026, então um interceptador que curto-circuita nunca produz uma resposta anônima ou fora do schema.
|
||||
* Com várias extensões, os interceptadores se aninham na ordem de registro: a primeira extensão em `extensions=[...]` é a mais externa.
|
||||
* A implementação padrão é um repasse direto, e um servidor cujas extensões nunca sobrescrevem esse hook mantém o handler puro de `tools/call` intocado. Você não paga pelo que não usa.
|
||||
|
||||
O hook envolve `tools/call` e nada mais. Para preocupações que valem para toda mensagem, use o [Middleware](middleware.md). É para isso que ele serve.
|
||||
|
||||
## Usando uma extensão de cliente {#using-a-client-extension}
|
||||
|
||||
Uma **extensão de cliente** é o mesmo contrato visto do lado consumidor: um pacote de comportamento do lado do cliente reunido sob um único identificador. Passe as instâncias para `Client(extensions=[...])` e chame as ferramentas normalmente:
|
||||
|
||||
```python title="client.py" hl_lines="66-68"
|
||||
--8<-- "docs_src/extensions/tutorial006.py"
|
||||
```
|
||||
|
||||
`call_tool("buy", ...)` retorna um `CallToolResult` comum, como toda outra chamada. O que a extensão mudou: o servidor agora pode responder a `buy` com um **formato de resultado** `receipt` em vez de um resultado final, e `Receipts` o finaliza (aqui, resgatando o recibo com uma chamada seguinte) antes de `call_tool` retornar. Nada muda no ponto da chamada.
|
||||
|
||||
Tire a extensão e nada disso existe: a barreira do servidor recusa um cliente que não a declarou (erro -32021), e um formato reivindicado vindo de um servidor que pula a barreira falha na validação, exatamente como a especificação exige para um `resultType` não reconhecido. Desligado por padrão, nas duas pontas da conexão.
|
||||
|
||||
Para anunciar um identificador **sem** nenhum comportamento do lado do cliente (o servidor faz a barreira pela capacidade, o cliente não faz nada, como no cliente de busca acima), use `advertise()`:
|
||||
|
||||
```python
|
||||
from mcp.client import advertise
|
||||
|
||||
client = Client(mcp, extensions=[advertise("com.example/search")])
|
||||
```
|
||||
|
||||
## Escrevendo uma extensão de cliente {#writing-a-client-extension}
|
||||
|
||||
Herde de `ClientExtension` e sobrescreva apenas o que precisar. Três tipos de contribuição, cada um com um padrão: `settings()`, `claims()` e `notifications()`.
|
||||
|
||||
```python title="client.py" hl_lines="17-18 43-44 46-47"
|
||||
--8<-- "docs_src/extensions/tutorial006.py"
|
||||
```
|
||||
|
||||
* O identificador segue a mesma gramática do servidor, validada quando a classe é definida.
|
||||
* `claims()` retorna `ResultClaim`s: uma tag de protocolo, o modelo que a analisa e o resolvedor que a finaliza. O modelo precisa fixar a tag com `result_type: Literal["receipt"]` e não pode herdar dos tipos de resultado centrais do verbo; as duas coisas são impostas quando a claim é construída. Campos de fornecedor como `receipt_token` viajam pela conexão como estão: um formato substituído chega ao cliente literalmente.
|
||||
* O resolvedor recebe o modelo analisado e um `ClaimContext`; `ctx.session` é o mesmo handle público que `client.session`, então as chamadas seguintes são chamadas comuns de sessão. Ele retorna o `CallToolResult` normal do verbo.
|
||||
* `settings()` é o valor anunciado em `ClientCapabilities.extensions[identifier]`, lido uma vez na construção do `Client`.
|
||||
|
||||
`notifications()` declara notificações de servidor de fornecedor a observar:
|
||||
|
||||
```python
|
||||
def notifications(self) -> Sequence[NotificationBinding[Any]]:
|
||||
return [NotificationBinding(method="notifications/receipts", params_type=ReceiptEvent, handler=self.on_receipt)]
|
||||
```
|
||||
|
||||
O handler recebe parâmetros validados um de cada vez, na ordem de despacho. Ele observa; não pode vetar nem responder.
|
||||
|
||||
Duas regras discretas. As claims ficam ativas apenas em conexões 2026-07-28, e o anúncio de capacidade as acompanha: em uma conexão legada as claims se dissolvem e o identificador sai do anúncio junto com elas, então o cliente nunca anuncia uma extensão cujos formatos ele rejeitaria. E quando você mesmo quer o formato reivindicado em vez do resolvedor, chame `client.session.call_tool(..., allow_claimed=True)`; sem essa flag, um formato reivindicado que chega a um chamador no nível da sessão lança `UnexpectedClaimedResult`.
|
||||
|
||||
### Verbos de extensão {#extension-verbs}
|
||||
|
||||
Os métodos de requisição próprios de uma extensão não precisam de registro no lado do cliente. Um tipo de requisição de fornecedor herda de `mcp.types.Request` e passa por `client.session.send_request`, como em [Servindo seus próprios métodos](#serving-your-own-methods). Um acréscimo: quando uma chave de params precisa viajar no header `Mcp-Name` (especificações de extensão como tasks exigem isso para seus verbos), o tipo de requisição declara `name_param`:
|
||||
|
||||
```python title="client.py" hl_lines="22-25 46-47"
|
||||
--8<-- "docs_src/extensions/tutorial007.py"
|
||||
```
|
||||
|
||||
A sessão espelha `params["jobId"]` em `Mcp-Name` em todo caminho de envio, e um valor ausente falha de forma explícita em vez de omitir silenciosamente um header obrigatório.
|
||||
|
||||
## O que uma extensão não pode fazer {#what-an-extension-cannot-do}
|
||||
|
||||
A superfície de contribuição é **fechada** de propósito. No servidor: configurações, ferramentas, recursos, métodos, um interceptador de `tools/call`. No cliente: configurações, claims de resultado, bindings de notificação. Uma extensão não pode:
|
||||
|
||||
* **Alcançar o host.** Ela declara dados; não guarda nenhuma referência ao servidor nem ao cliente.
|
||||
* **Substituir comportamento central.** Métodos da especificação e tags de resultado centrais são rejeitados na construção (`initialize` é reservado pelo runner sem exceção); já um binding de notificação encoberto pelo vocabulário central fica em silêncio com um aviso.
|
||||
* **Registrar-se depois.** Depois que `MCPServer(...)` ou `Client(...)` retorna, o conjunto de extensões é o que é.
|
||||
|
||||
Se você está brigando com essas paredes, não está escrevendo uma extensão. Está escrevendo um fork. As paredes são a funcionalidade: um usuário que lê `extensions=[Apps(), Stamps()]` sabe *tudo* o que essas duas podem ter tocado.
|
||||
34
i18n/pt/pages/advanced/index.md
Normal file
34
i18n/pt/pages/advanced/index.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
translation:
|
||||
sections: [ca6988b7503cd2d3]
|
||||
tool: 1
|
||||
---
|
||||
# Avançado {#advanced}
|
||||
|
||||
Tudo o que um servidor ou cliente comum precisa tem seu lugar por assunto nas seções acima.
|
||||
Esta seção reúne as saídas de emergência a que você recorre quando a camada de conveniência
|
||||
do `MCPServer` atrapalha:
|
||||
|
||||
* **[O Server de baixo nível](low-level-server.md)**: a classe sobre a qual o `MCPServer` é construído.
|
||||
Schemas escritos à mão, handlers `on_*`, nada verificado para você e métodos JSON-RPC
|
||||
personalizados criados por você.
|
||||
* **[Paginação](pagination.md)** e **[Middleware](middleware.md)**: duas coisas que você
|
||||
*só* consegue fazer no `Server` de baixo nível.
|
||||
* **[Extensões](extensions.md)** e **[MCP Apps](apps.md)**: a superfície de
|
||||
extensão do protocolo. Componha pacotes de extensão em um servidor ou escreva os seus.
|
||||
|
||||
Algumas coisas que você poderia, com razão, procurar aqui ficam onde você de fato
|
||||
as usaria:
|
||||
|
||||
* **Autorização** está em **[Executando seu servidor](../run/index.md)**, porque você
|
||||
protege um servidor onde faz o deploy dele.
|
||||
* **OAuth**, **asserção de identidade**, conexão a **vários servidores** e o
|
||||
**cache** de respostas estão todos em **[Clientes](../client/index.md)**.
|
||||
* **Requisições com várias idas e voltas** e **Assinaturas** estão em
|
||||
**[Dentro do seu handler](../handlers/index.md)**, porque ambas são coisas que um
|
||||
handler *faz*.
|
||||
* **Templates de URI** está em **[Servidores](../servers/index.md)**, ao lado de Recursos.
|
||||
* **[Versões do protocolo](../protocol-versions.md)** e
|
||||
**[Funcionalidades descontinuadas](../deprecated.md)** têm, cada uma, sua própria página de nível superior.
|
||||
|
||||
Se você não tem certeza de que precisa desta seção, não precisa.
|
||||
207
i18n/pt/pages/advanced/low-level-server.md
Normal file
207
i18n/pt/pages/advanced/low-level-server.md
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
---
|
||||
translation:
|
||||
sections: [2c79b6338e09b7ac, 7edc43b3fae11314, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, b3530fcf4d11fd56, ebc33704fbd74262, cd0e9c933350390e]
|
||||
tool: 1
|
||||
---
|
||||
# O Server de baixo nível {#the-low-level-server}
|
||||
|
||||
`@mcp.tool()` é uma camada. Por baixo dela existe uma segunda classe de servidor, `Server`, que fala MCP cru: você entrega os objetos do protocolo e ela os coloca no fio, sem alterar nada.
|
||||
|
||||
O `MCPServer` é construído em cima dela. Você desce um nível quando a camada de conveniência atrapalha:
|
||||
|
||||
* Você precisa emitir um schema **exato** (carregado de um arquivo, gerado a partir de um banco de dados), não um derivado de uma assinatura Python.
|
||||
* Você precisa de controle total do resultado: `_meta`, `is_error`, cada chave de `structured_content`.
|
||||
* Você precisa tratar um método que o MCP não define.
|
||||
|
||||
Para todo o resto, fique no `MCPServer`.
|
||||
|
||||
## A mesma ferramenta, à mão {#the-same-tool-by-hand}
|
||||
|
||||
Esta é a ferramenta (tool) `search_books` que **[Ferramentas](../servers/tools.md)** escreve em nove linhas de `@mcp.tool()`, com o açúcar removido:
|
||||
|
||||
```python title="server.py" hl_lines="22 26 32"
|
||||
--8<-- "docs_src/lowlevel/tutorial001.py"
|
||||
```
|
||||
|
||||
Três coisas mudaram, e elas são a API de baixo nível inteira:
|
||||
|
||||
* **Os handlers são parâmetros do construtor.** `on_list_tools=` e `on_call_tool=` entram em `Server(...)`. Não há decoradores aqui embaixo, e todo handler tem o mesmo formato: `async (ctx, params) -> result`.
|
||||
* **Você escreve o schema de entrada.** `Tool.input_schema` é um `dict` JSON Schema comum. Ninguém o deriva de anotações de tipo, porque não há anotações de tipo de onde derivar.
|
||||
* **Você monta o resultado.** `CallToolResult(content=[TextContent(...)])`, à mão. Nada é encapsulado, convertido ou inferido de uma anotação de retorno.
|
||||
|
||||
`params` é a requisição já parseada: `CallToolRequestParams` dá `.name` e `.arguments`. `ctx` é um `ServerRequestContext`: `ctx.session` para falar de volta com o cliente, `ctx.lifespan_context`, `ctx.request_id` e `ctx.meta`, o `_meta` de entrada da requisição.
|
||||
|
||||
!!! info
|
||||
Se você já usou FastAPI, já conhece essa relação. O `MCPServer` é a camada de decoradores e anotações de tipo; o `Server` é o Starlette por baixo. Eles não são rivais: o `MCPServer` constrói um `Server` e registra nele handlers exatamente como esses.
|
||||
|
||||
### Experimente {#try-it}
|
||||
|
||||
Não existe Inspector para este aqui: `mcp dev` e `mcp run` só aceitam um `MCPServer`. O `Client` em memória não se importa; ele recebe um `Server` de baixo nível exatamente como recebe um `MCPServer`:
|
||||
|
||||
```python title="main.py"
|
||||
import asyncio
|
||||
|
||||
from mcp import Client
|
||||
|
||||
from server import server
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(server) as client:
|
||||
result = await client.call_tool("search_books", {"query": "dune", "limit": 5})
|
||||
print(result.content)
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
```text
|
||||
[TextContent(type='text', text="Found 3 books matching 'dune' (showing up to 5).", annotations=None, meta=None)]
|
||||
```
|
||||
|
||||
O mesmo texto que a versão com `@mcp.tool()` produziu. Duas diferenças honestas:
|
||||
|
||||
* `result.structured_content` é `None`. O servidor de alto nível encapsula um `-> str` em `{"result": ...}` para você; aqui ninguém monta o que você não montou.
|
||||
* `list_tools` retorna o schema que **você** digitou, caractere por caractere. A versão de alto nível tinha `"title": "Query"` em cada propriedade e um `"title": "search_booksArguments"` na raiz: artefatos do Pydantic. Aqui embaixo, se está no fio, foi você quem colocou lá.
|
||||
|
||||
## Nada é verificado por você {#nothing-is-checked-for-you}
|
||||
|
||||
O `MCPServer` rejeita um argumento ruim antes mesmo de a sua função executar, validando a chamada contra o schema que ele gerou (**[Ferramentas](../servers/tools.md)**).
|
||||
|
||||
O `Server` não faz isso. O seu `input_schema` é *anunciado* ao cliente; ele nunca é *aplicado* a `params.arguments`.
|
||||
|
||||
!!! check
|
||||
Chame `search_books` sem `limit` e o seu `args["limit"]` levanta `KeyError`. O cliente vê:
|
||||
|
||||
```text
|
||||
MCPError: Internal server error
|
||||
```
|
||||
|
||||
Um erro JSON-RPC, código `-32603`, com uma mensagem deliberadamente genérica: o SDK não vaza o seu traceback para um chamador remoto. O modelo nunca descobre o que fez de errado, então não consegue tentar de novo. (Em um teste, `raise_exceptions=True` expõe a exceção real; veja **[Testes](../get-started/testing.md)**.)
|
||||
|
||||
Isso se generaliza. Uma exceção levantada de um handler de baixo nível é **sempre** um erro de protocolo, nunca um resultado de ferramenta com `is_error=True`. Se você quer que o modelo leia a falha e se recupere, valide `params.arguments` você mesmo e retorne `CallToolResult(content=[TextContent(...)], is_error=True)`. Os dois tipos de falha são o assunto de **[Tratando erros](../servers/handling-errors.md)**.
|
||||
|
||||
## Duas ferramentas, um handler {#two-tools-one-handler}
|
||||
|
||||
`on_call_tool` é o único ponto de entrada para todas as ferramentas do servidor. Você roteia por `params.name`:
|
||||
|
||||
```python title="server.py" hl_lines="38-43"
|
||||
--8<-- "docs_src/lowlevel/tutorial002.py"
|
||||
```
|
||||
|
||||
* `list_tools` anuncia as duas. `call_tool` despacha pelo nome.
|
||||
* O ramo `else` importa: o `Server` encaminha sem reclamar um `tools/call` para um nome que você nunca listou direto para o seu handler. Levantar uma exceção ali transforma a chamada no mesmo `-32603` de cima.
|
||||
|
||||
## Saída estruturada, à mão {#structured-output-by-hand}
|
||||
|
||||
Declare `output_schema` na `Tool` e coloque `structured_content` no resultado. Os dois são seus:
|
||||
|
||||
```python title="server.py" hl_lines="19-23 36"
|
||||
--8<-- "docs_src/lowlevel/tutorial003.py"
|
||||
```
|
||||
|
||||
Chame e o resultado carrega as duas representações:
|
||||
|
||||
```json
|
||||
{
|
||||
"content": [{"type": "text", "text": "Found 3 books matching 'dune'."}],
|
||||
"structuredContent": {"matches": 3, "query": "dune"},
|
||||
"isError": false,
|
||||
"resultType": "complete",
|
||||
"_meta": {"io.modelcontextprotocol/serverInfo": {"name": "Bookshop", "version": "2.0.0"}}
|
||||
}
|
||||
```
|
||||
|
||||
O bloco `_meta` é o carimbo de identidade do servidor: o SDK o adiciona a todo resultado da era 2026, com a `version` vinda do construtor (um servidor que não define nenhuma reporta uma string vazia). Um servidor que não deve se identificar pode remover a chave com um middleware, que é dono dos resultados que retorna.
|
||||
|
||||
O servidor nunca compara os dois campos. O `Client` deste SDK compara: retorne um `structured_content` que não satisfaz o `output_schema` que você declarou e `call_tool` levanta um `RuntimeError` que começa com `Invalid structured content returned by tool search_books` e segue citando a falha do `jsonschema`. Prometer um schema é barato; cumprir a promessa é com você. A escada inteira de tipos de retorno e schemas está em **[Saída estruturada](../servers/structured-output.md)**.
|
||||
|
||||
## `_meta`: para a aplicação, não para o modelo {#\_meta-for-the-application-not-the-model}
|
||||
|
||||
`content` é a parte da resposta que o modelo lê. `structured_content` é a mesma resposta como dados tipados. `_meta` é o terceiro canal: dados que viajam junto com o resultado para a **aplicação cliente**, sem fazer parte da resposta de forma alguma.
|
||||
|
||||
Use para IDs de registro, IDs de trace, qualquer coisa de que a sua UI precisa e o seu prompt não:
|
||||
|
||||
```python title="server.py" hl_lines="37"
|
||||
--8<-- "docs_src/lowlevel/tutorial004.py"
|
||||
```
|
||||
|
||||
* Você o constrói como `_meta=`, o nome no fio. O cliente o lê de volta como `result.meta`.
|
||||
* Use namespace nas suas chaves (`bookshop/record_ids`). As chaves `io.modelcontextprotocol/*` são reservadas pelo protocolo.
|
||||
|
||||
!!! warning
|
||||
`_meta` é uma convenção entre você e a aplicação cliente, não uma garantia sobre o que chega
|
||||
ao modelo. O host decide o que renderiza. Nunca coloque um segredo em nenhuma parte de um resultado de ferramenta.
|
||||
|
||||
## As capacidades seguem os seus handlers {#capabilities-follow-your-handlers}
|
||||
|
||||
Um `Server` anuncia exatamente as famílias de métodos para as quais você deu handlers. O `Bookshop` acima passa `on_list_tools` e `on_call_tool` e nada mais, então um cliente que se conecta a ele vê:
|
||||
|
||||
```json
|
||||
{"tools": {"listChanged": false}}
|
||||
```
|
||||
|
||||
Sem `resources`, sem `prompts`: não há nada que os sustente. Passe `on_list_prompts` e `prompts` aparece; passe `on_completion` e `completions` aparece.
|
||||
|
||||
O `MCPServer` sempre anuncia ferramentas, recursos e prompts, tenha você registrado algum ou não, porque os seus managers sempre existem. Aqui embaixo a declaração *é* a chamada ao construtor.
|
||||
|
||||
## O genérico do lifespan {#the-lifespan-generic}
|
||||
|
||||
O `Server` é genérico no tipo que o seu lifespan produz. Anote uma vez e o objeto fica tipado em todo lugar onde aparece:
|
||||
|
||||
```python title="server.py" hl_lines="24-26 44-45 50"
|
||||
--8<-- "docs_src/lowlevel/tutorial005.py"
|
||||
```
|
||||
|
||||
* O lifespan é um `Callable[[Server[Catalog]], AbstractAsyncContextManager[Catalog]]`; `@asynccontextmanager` em um gerador `async` dá exatamente isso.
|
||||
* O que quer que ele produza com `yield` vira `ctx.lifespan_context`, e como os handlers são anotados com `ServerRequestContext[Catalog]`, `.search(...)` tem autocompletar e passa na checagem de tipos.
|
||||
* Ele é aberto uma vez quando o servidor inicia e fechado uma vez quando para. Inicialização, encerramento e a versão do `MCPServer` da mesma ideia estão em **[Lifespan](../handlers/lifespan.md)**.
|
||||
|
||||
Sem um `lifespan=`, `ctx.lifespan_context` é um `dict` vazio.
|
||||
|
||||
## Um método só seu {#a-method-of-your-own}
|
||||
|
||||
O construtor cobre os métodos que o MCP define. `add_request_handler` cobre todo o resto:
|
||||
|
||||
```python title="server.py" hl_lines="35-36 39-40 43-44 48"
|
||||
--8<-- "docs_src/lowlevel/tutorial006.py"
|
||||
```
|
||||
|
||||
* O primeiro argumento é a string do método. Notificações têm um irmão gêmeo, `add_notification_handler`.
|
||||
* `params_type` é o modelo contra o qual os `params` recebidos são validados **antes** de o seu handler executar, então métodos personalizados *recebem* a validação que as ferramentas não recebem. Faça subclasse de `RequestParams` para que o campo `_meta` seja parseado como o de qualquer outro método.
|
||||
* O handler retorna um `BaseModel`, um `dict` ou `None`. O SDK serializa isso no resultado JSON-RPC.
|
||||
|
||||
Uma ressalva honesta: o `Client` de alto nível só tem verbos para os métodos que o MCP define, então não existe `client.reindex()`. Um método de fornecedor é para um par que já sabe que ele existe: um cliente que você também distribui, ou outro serviço seu falando JSON-RPC.
|
||||
|
||||
Um método que você não pode reivindicar:
|
||||
|
||||
```text
|
||||
ValueError: 'initialize' is handled by the server runner and cannot be overridden;
|
||||
use Server.middleware to observe or wrap initialization
|
||||
```
|
||||
|
||||
O handshake pertence ao runner. `server/discover`, `ping` e todos os outros embutidos são seus para substituir.
|
||||
|
||||
!!! tip
|
||||
`Server.middleware`, mencionado naquele erro, envolve **toda** mensagem de entrada, inclusive `initialize`. Se o que você quer é observar ou reescrever o tráfego em vez de responder a um método novo, comece por **[Middleware](middleware.md)**.
|
||||
|
||||
## Os outros handlers {#the-other-handlers}
|
||||
|
||||
Cada um destes é uma ideia para a qual você já tem o vocabulário; cada um tem sua própria página.
|
||||
|
||||
* `on_call_tool`, `on_get_prompt` e `on_read_resource` podem retornar um `InputRequiredResult` em vez do resultado normal para pausar a chamada e pedir entrada ao cliente; veja **[Requisições de múltiplas idas e voltas](../handlers/multi-round-trip.md)**. Fiel a este nível, nada é instalado para você: enquanto o `MCPServer` sela o `requestState` por padrão, aqui o `request_state` que você define atravessa o fio exatamente como foi escrito até você optar com `server.middleware.append(RequestStateBoundary(RequestStateSecurity(keys=[...]), default_audience=server.name))`: uma linha (os dois nomes são importados de `mcp.server.request_state`) para a mesma selagem e verificação que o `MCPServer` faz (**[Protegendo o `requestState`](../handlers/multi-round-trip.md#protecting-requeststate)**).
|
||||
* `on_list_resources`, `on_read_resource`, `on_list_prompts`, `on_get_prompt`, `on_completion` têm o mesmo formato `(ctx, params) -> result` para as outras primitivas.
|
||||
* `on_subscriptions_listen` serve o stream `subscriptions/listen` de 2026-07-28. Passe um `ListenHandler` construído sobre um `SubscriptionBus` e publique eventos no bus a partir dos seus outros handlers; veja **[Assinaturas](../handlers/subscriptions.md)** para a composição completa.
|
||||
* `server.streamable_http_app()` retorna o mesmo app Starlette que o do `MCPServer`; faça o deploy dele do jeito que **[Executando o seu servidor](../run/index.md)** faz o deploy de qualquer outro app ASGI. Não existe `server.run(transport=...)` aqui embaixo: `server.run(read_stream, write_stream, server.create_initialization_options())` conduz uma conexão sobre um par de streams, e essa única linha é a história completa.
|
||||
|
||||
## Recapitulando {#recap}
|
||||
|
||||
* O `Server` de baixo nível recebe os seus handlers como **parâmetros do construtor** `on_*`; todo handler é `async (ctx, params) -> result`.
|
||||
* Você escreve o dict `input_schema` e você monta o `CallToolResult`. Nada é derivado, encapsulado ou validado por você.
|
||||
* Uma exceção em um handler é um erro de protocolo `-32603`. Um erro de ferramenta que o modelo consegue ler é um `CallToolResult` com `is_error=True` que **você** retorna.
|
||||
* O `_meta` no resultado é endereçado à aplicação cliente, não ao modelo.
|
||||
* `Server[T]` é genérico no que o seu lifespan produz; `ctx.lifespan_context` é um `T` tipado.
|
||||
* `add_request_handler(method, params_type, handler)` serve qualquer método. `initialize` é reservado.
|
||||
* As capacidades que um `Server` anuncia são derivadas de quais handlers você registrou.
|
||||
|
||||
`Client(server)` tratou os dois servidores de forma idêntica porque eles *são* o mesmo protocolo, e essa é justamente a ideia. A próxima camada abaixo nem é uma classe: é **[Middleware](middleware.md)**.
|
||||
123
i18n/pt/pages/advanced/middleware.md
Normal file
123
i18n/pt/pages/advanced/middleware.md
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
---
|
||||
translation:
|
||||
sections: [6048b4f308edbb8c, 068bda0f21ee9c1b, c3e565b61acd75c5, c62422b159c6ed09, 47204fab253cc45c]
|
||||
tool: 1
|
||||
---
|
||||
# Middleware {#middleware}
|
||||
|
||||
Um **middleware** é uma única função async que envolve toda mensagem que o seu servidor recebe.
|
||||
|
||||
Você o escreve como `async (ctx, call_next)` e o adiciona ao fim de `server.middleware`. A API inteira é essa.
|
||||
|
||||
!!! warning
|
||||
A lista de middlewares está marcada como **provisória** no código-fonte: a assinatura e a
|
||||
semântica podem mudar em uma versão minor 2.x. Use-a para *observar* (tempo, logs, tracing) e
|
||||
para *recusar* mensagens; não faça dela o alicerce sobre o qual o seu servidor se apoia.
|
||||
|
||||
`MCPServer` recebe a lista na construção (`MCPServer(name, middleware=[...])`) e a expõe como
|
||||
`mcp.middleware`; o `Server` de baixo nível expõe a mesma lista como `server.middleware`. O exemplo
|
||||
abaixo usa o `Server` de baixo nível; se `Server(name, on_call_tool=...)` é novidade para você, leia
|
||||
**[O Server de baixo nível](low-level-server.md)** primeiro.
|
||||
|
||||
## Um middleware de medição de tempo {#a-timing-middleware}
|
||||
|
||||
Um servidor, uma ferramenta, um middleware que registra no log quanto tempo cada mensagem levou:
|
||||
|
||||
```python title="server.py" hl_lines="39-45 49"
|
||||
--8<-- "docs_src/middleware/tutorial001.py"
|
||||
```
|
||||
|
||||
* `ctx` é o mesmo `ServerRequestContext` que os seus handlers recebem. `ctx.method` é a string
|
||||
bruta do método; `ctx.params` são os params brutos, **antes** de qualquer validação.
|
||||
* `call_next(ctx)` executa o restante da cadeia: a validação, a busca do handler, o seu handler.
|
||||
Retorne o que ele retornou e a resposta fica intacta.
|
||||
* O `try`/`finally` é proposital: um handler que lança exceção ainda é cronometrado, porque a falha
|
||||
chega ao seu middleware como a exceção que sai de `call_next`.
|
||||
* `server.middleware.append(...)` faz o registro. A lista executa do mais externo para o mais
|
||||
interno, então `middleware[0]` é o que fica mais perto do fio.
|
||||
|
||||
### Experimente {#try-it}
|
||||
|
||||
Conecte um cliente, liste as ferramentas, chame uma. O seu log tem **três** linhas:
|
||||
|
||||
```text
|
||||
server/discover took 18.3 ms
|
||||
tools/list took 0.1 ms
|
||||
tools/call took 0.1 ms
|
||||
```
|
||||
|
||||
Você fez duas chamadas e recebeu três linhas. A primeira é `server/discover`: a requisição que o
|
||||
cliente enviou para estabelecer a conexão, antes de você pedir qualquer coisa.
|
||||
|
||||
É justamente esse o ponto. O middleware envolve **toda** mensagem de entrada:
|
||||
|
||||
* O estabelecimento da conexão: `server/discover`, ou `initialize` e `notifications/initialized`
|
||||
em uma sessão legada.
|
||||
* Toda requisição e toda notificação. Para uma notificação, `ctx.request_id is None`,
|
||||
`call_next(ctx)` retorna `None` e o que quer que você retorne é descartado.
|
||||
* Até um método para o qual o servidor não tem handler: `call_next` lança o
|
||||
`MCPError(-32601, "Method not found")` *através* do seu middleware a caminho do cliente.
|
||||
|
||||
## O que você pode fazer dentro de um {#what-you-can-do-inside-one}
|
||||
|
||||
Em ordem crescente do quanto você deveria hesitar:
|
||||
|
||||
* **Observar.** Cronometre, conte, registre no log. O exemplo acima.
|
||||
* **Recusar.** Lance um `MCPError` *em vez de* chamar `call_next(ctx)` e essa única mensagem é
|
||||
respondida com um erro JSON-RPC. A conexão continua de pé; a próxima mensagem passa. É assim
|
||||
que um servidor controla o acesso a `subscriptions/listen` por chamador:
|
||||
**[Decidindo quem pode observar](../handlers/subscriptions.md#deciding-who-may-watch)**, na
|
||||
página de Assinaturas, percorre o passo a passo.
|
||||
* **Reescrever.** `ctx` é uma dataclass: `await call_next(dataclasses.replace(ctx, params=...))`
|
||||
entrega ao restante da cadeia params diferentes dos que o cliente enviou. Nunca faça isso com
|
||||
`initialize`: o resultado que o cliente recebe de volta é construído a partir dos seus params
|
||||
reescritos, mas o servidor grava o estado da conexão a partir dos params originais do fio. Os
|
||||
dois lados podem terminar o handshake discordando sobre o que negociaram.
|
||||
* **Responder.** Retorne um resultado sem chamar `call_next(ctx)` e ele vai para o cliente como a
|
||||
sua resposta. `call_next` entrega a você a forma final do fio, e o pipeline nunca altera o que
|
||||
você retorna, então o envelope inteiro é seu: em uma conexão da era 2026 isso inclui o carimbo
|
||||
`_meta` de `serverInfo`, que o SDK adiciona aos resultados dos handlers, mas não aos seus.
|
||||
|
||||
!!! check
|
||||
`initialize` é uma das coisas que o middleware envolve, e é o *único* gancho que você tem
|
||||
para ele. Tente assumi-lo com `add_request_handler` e o SDK recusa:
|
||||
|
||||
```text
|
||||
ValueError: 'initialize' is handled by the server runner and cannot be overridden;
|
||||
use Server.middleware to observe or wrap initialization
|
||||
```
|
||||
|
||||
!!! warning
|
||||
`initialize` é tratado inline: o servidor não lê mais nenhuma mensagem de entrada até a sua
|
||||
cadeia de middlewares retornar. Aguardar uma requisição do servidor para o cliente
|
||||
(`ctx.session.send_request(...)`, uma elicitação (elicitation)) enquanto trata `initialize`,
|
||||
portanto, **trava a conexão em deadlock**: a resposta que você está esperando nunca poderá ser
|
||||
lida. Notificações do tipo fire-and-forget não têm problema.
|
||||
|
||||
## O único middleware que já vem ligado por padrão {#the-one-middleware-that-ships-on-by-default}
|
||||
|
||||
O SDK traz exatamente um middleware, e ele já está na lista do seu servidor: o que emite um span
|
||||
do OpenTelemetry para cada mensagem. Você não o adiciona, e na maior parte do tempo nem pensa
|
||||
nele. Ele é um no-op até você instalar um exportador, e tem a própria página:
|
||||
**[OpenTelemetry](../run/opentelemetry.md)**.
|
||||
|
||||
!!! info
|
||||
Se você já escreveu middleware ASGI, já conhece esse formato. O `(scope, receive, send)`
|
||||
do Starlette virou `(ctx, call_next)`, e ele executa *depois* do transporte, sobre a mensagem
|
||||
decodificada em vez da requisição HTTP bruta. Os dois se compõem: o middleware do Starlette
|
||||
em `streamable_http_app()` enxerga HTTP; este enxerga MCP.
|
||||
|
||||
## Recapitulando {#recap}
|
||||
|
||||
* Um middleware é `async (ctx, call_next) -> result`, passado como `MCPServer(middleware=[...])` (ou
|
||||
adicionado a `mcp.middleware`) e adicionado a `server.middleware` no `Server` de baixo nível.
|
||||
* Ele envolve **toda** mensagem de entrada (`server/discover`, `initialize`, requisições,
|
||||
notificações, métodos desconhecidos) e executa do mais externo para o mais interno.
|
||||
* `ctx.request_id is None` é como você distingue uma notificação de uma requisição.
|
||||
* Lance uma exceção em vez de chamar `call_next` para recusar uma mensagem; a conexão sobrevive.
|
||||
* O tracing do OpenTelemetry do próprio SDK também é um middleware, já na lista. Veja
|
||||
**[OpenTelemetry](../run/opentelemetry.md)**.
|
||||
* Toda essa superfície é provisória. Observe com ela; não construa em cima dela.
|
||||
|
||||
Isso é tudo o que envolve uma requisição. **[Autorização](../run/authorization.md)** é o que decide
|
||||
se a requisição chega a ser executada.
|
||||
85
i18n/pt/pages/advanced/pagination.md
Normal file
85
i18n/pt/pages/advanced/pagination.md
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
---
|
||||
translation:
|
||||
sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9]
|
||||
tool: 1
|
||||
---
|
||||
# Paginação {#pagination}
|
||||
|
||||
A maioria dos servidores nunca precisa disso.
|
||||
|
||||
O `MCPServer` responde a toda requisição `list_*` com tudo o que tem, em uma única página, `next_cursor=None`. Para algumas dezenas de ferramentas, recursos ou prompts, essa é a resposta certa e não há nada para configurar.
|
||||
|
||||
A paginação é para o servidor cuja lista de recursos é, na verdade, um banco de dados: milhares de linhas que ele se recusa a serializar em uma única resposta. A resposta do protocolo é um **cursor**: o servidor retorna uma página mais um token opaco, e o cliente envia esse token de volta para obter a próxima página.
|
||||
|
||||
O `@mcp.resource()` não tem nenhum gancho para isso. Para paginar, você mesmo escreve o handler de listagem, no **[Server de baixo nível](low-level-server.md)**.
|
||||
|
||||
## Um servidor que pagina {#a-server-that-pages}
|
||||
|
||||
```python title="server.py" hl_lines="12 15-16"
|
||||
--8<-- "docs_src/pagination/tutorial001.py"
|
||||
```
|
||||
|
||||
* Em um `Server` de baixo nível, os handlers são argumentos do construtor, não decoradores. O `on_list_resources` responde a toda requisição `resources/list`; a ligação inteira é essa.
|
||||
* Todo handler paginado é tipado como `params: PaginatedRequestParams | None`, e o exemplo aceita os dois. Em uma conexão, porém, o SDK nunca entrega `None` para você (uma requisição sem o membro `params` chega ao handler como o modelo com seus valores padrão), então o sinal que importa é `params.cursor is None`: **comece do início**.
|
||||
* Você decide o que um cursor *é*. Aqui é um offset representado como string. Um timestamp, uma chave primária, um blob em base64: qualquer coisa que você consiga gerar na saída e reconhecer na volta.
|
||||
* `next_cursor=None` é como você diz "essa foi a última página". Não há contagem, nem total, nem `has_more`. O `None` é o sinal inteiro.
|
||||
|
||||
!!! tip
|
||||
Um `PAGE_SIZE` de 10 deixa o exemplo legível. Escolha o seu por endpoint: uma lista de
|
||||
recursos de uma linha comporta uma página de 500; uma lista de templates de prompt pesados, não.
|
||||
O cliente não tem voz nisso, e é assim de propósito.
|
||||
|
||||
### Experimente {#try-it}
|
||||
|
||||
`Client(server)` se conecta a um `Server` de baixo nível em memória exatamente como se conecta a um `MCPServer`.
|
||||
|
||||
Chame `list_resources()` sem argumentos. Você recebe dez recursos, de `book-1` a `book-10`, e `next_cursor` é a string `"10"`.
|
||||
|
||||
Devolva-a com `list_resources(cursor="10")` e o primeiro recurso é `book-11`, o novo `next_cursor` é `"20"`.
|
||||
|
||||
A décima página volta com `next_cursor` definido como `None`. Pronto.
|
||||
|
||||
## O loop do cliente {#the-client-loop}
|
||||
|
||||
Todo método `list_*` do `Client` (`list_tools`, `list_resources`, `list_resource_templates`, `list_prompts`) aceita a palavra-chave `cursor=`. Esgotar uma lista paginada é um único `while True`:
|
||||
|
||||
```python title="client.py" hl_lines="26-32"
|
||||
--8<-- "docs_src/pagination/tutorial002.py"
|
||||
```
|
||||
|
||||
* `cursor` começa como `None`, então a primeira requisição não carrega nenhum cursor.
|
||||
* Acumule **antes** de olhar para `next_cursor`: a última página também tem recursos.
|
||||
* `next_cursor is None` é a saída. Qualquer outra coisa volta direto para `cursor=`, intocada.
|
||||
|
||||
Execute o `main()` dele e ele imprime `100 resources`: dez páginas de dez, costuradas por um loop que nunca soube que havia dez páginas.
|
||||
|
||||
Este é o mesmo loop que **[O cliente](../client/index.md)** mostra para todo verbo `list_*`, e ele não custa nada contra um servidor que não pagina: `next_cursor` é `None` na primeira resposta e o loop roda uma vez.
|
||||
|
||||
## As três regras {#the-three-rules}
|
||||
|
||||
**Cursores são opacos.** Um cliente nunca deve interpretar, construir ou adivinhar um. A única fonte legítima de um cursor é o `next_cursor` da página anterior, literalmente.
|
||||
|
||||
**O servidor escolhe o tamanho da página.** Não existe `limit=` no protocolo. Se você precisa de um tamanho de página diferente, altere o servidor.
|
||||
|
||||
**Um cliente que ignora a paginação continua funcionando.** Ele chama `list_resources()` uma vez, recebe os dez primeiros e nunca percebe o `next_cursor` que jogou fora. Nada quebra; ele só vê menos.
|
||||
|
||||
!!! check
|
||||
Opaco quer dizer opaco. Invente um cursor (`list_resources(cursor="page-2")`) e não há
|
||||
nada que o protocolo possa fazer por você. Este servidor tenta `int("page-2")`, o handler lança uma exceção,
|
||||
e o que volta para o cliente é:
|
||||
|
||||
```text
|
||||
MCPError(-32603, 'Internal server error', None)
|
||||
```
|
||||
|
||||
Um cursor que você não recebeu do servidor é um bug, não um pedido de funcionalidade.
|
||||
|
||||
## Recapitulando {#recap}
|
||||
|
||||
* O `MCPServer` retorna tudo em uma página. A paginação é opcional, e você opta por ela no `Server` de baixo nível.
|
||||
* `on_list_resources` (e `on_list_tools`, `on_list_prompts`, `on_list_resource_templates`) recebe `PaginatedRequestParams | None`; `params.cursor` é `None` na primeira página.
|
||||
* Você retorna uma página mais `next_cursor`: qualquer string que você vá reconhecer depois, ou `None` quando não sobrar nada.
|
||||
* O loop do cliente: passe `cursor=`, acumule, repita até `next_cursor is None`.
|
||||
* Cursores são opacos, o servidor é dono do tamanho da página, e um cliente que não pagina ainda recebe a primeira página.
|
||||
|
||||
O restante da API do `Server` escrita à mão (`on_call_tool`, dicts `input_schema`, `_meta`) está em **[O Server de baixo nível](low-level-server.md)**.
|
||||
122
i18n/pt/pages/client/caching.md
Normal file
122
i18n/pt/pages/client/caching.md
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
---
|
||||
translation:
|
||||
sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800]
|
||||
tool: 1
|
||||
---
|
||||
# Dicas de cache {#caching-hints}
|
||||
|
||||
Todo resultado que um servidor retorna para `tools/list`, `prompts/list`, `resources/list`, `resources/templates/list`, `resources/read` e `server/discover` carrega dois campos no protocolo 2026-07-28: `ttlMs`, por quantos milissegundos um cliente pode tratar o resultado como fresco, e `cacheScope`, se um resultado em cache pode ser compartilhado entre usuários (`"public"`) ou pertence a um único contexto de autorização (`"private"`).
|
||||
|
||||
O servidor não faz cache de nada. Os campos são uma *declaração*: "esta lista de ferramentas é a mesma para todo mundo e não vai mudar por um minuto." Um cliente (ou um gateway na sua frente) pode então pular a viagem de ida e volta. Respeitar as dicas é escolha do cliente; emiti-las é trabalho do servidor, e o SDK faz isso por você.
|
||||
|
||||
Por padrão, todo resultado diz `ttlMs: 0, cacheScope: "private"`: obsoleto imediatamente, nunca compartilhado. Isso é sempre seguro e sempre conforme. Se as suas listas realmente são estáveis e idênticas para todos os chamadores, diga isso na construção:
|
||||
|
||||
```python title="server.py" hl_lines="5-8"
|
||||
--8<-- "docs_src/caching/tutorial001.py"
|
||||
```
|
||||
|
||||
* O mapa é indexado pelo **nome do método**, e os seis métodos cacheáveis são as únicas chaves válidas. O parâmetro é tipado como `Mapping[CacheableMethod, CacheHint]`, então o seu editor autocompleta as chaves e aponta um erro de digitação antes de você executar; qualquer coisa que escape do verificador de tipos levanta exceção na construção.
|
||||
* Um método que você não menciona mantém os padrões. O mapa é um conjunto de sobrescritas, não um manifesto.
|
||||
* `CacheHint(ttl_ms=5_000)` deixou `scope` sem definir, então ele continua `"private"`: cinco segundos de frescor, por chamador. Escopo e TTL são decisões independentes.
|
||||
* `"server/discover"` também é uma chave válida, já que o resultado de descoberta é cacheável como qualquer lista.
|
||||
|
||||
!!! warning
|
||||
`cacheScope: "public"` significa que *qualquer um* pode receber a sua resposta em cache. Um
|
||||
gateway compartilhado vai entregar sem hesitar o resultado de um usuário a outro, mesmo quando a
|
||||
requisição foi autenticada. Marque um resultado como `"public"` apenas quando ele é idêntico para
|
||||
todo chamador, e nunca use `cacheScope` como controle de acesso: é um rótulo, não um cadeado.
|
||||
|
||||
## Sobrescrita por handler {#per-handler-override}
|
||||
|
||||
No `Server` de baixo nível, os handlers montam seus resultados à mão, e `ttl_ms` / `cache_scope` são apenas campos nos modelos de resultado. Um handler que os define explicitamente sempre vence o mapa do construtor, campo a campo:
|
||||
|
||||
```python title="server.py" hl_lines="10 16"
|
||||
--8<-- "docs_src/caching/tutorial002.py"
|
||||
```
|
||||
|
||||
O handler disse `ttl_ms=1_000` e nada sobre escopo. No fio: `ttlMs: 1000` (o do handler, não o `60_000` do mapa) e `cacheScope: "public"` (o do mapa, porque o handler o deixou sem definir). Explícito vence configurado, e configurado vence o padrão. Isso vale por campo, então um handler pode fixar um campo e deixar o outro para a política do servidor inteiro.
|
||||
|
||||
Essa também é a saída de emergência para dinâmicas que o construtor não tem como conhecer: um handler que filtra `resources/read` por usuário pode retornar `cache_scope="private"` para uma URI de um servidor que, de resto, é público.
|
||||
|
||||
Uma ressalva sobre listas paginadas: o protocolo exige o **mesmo `cacheScope` em todas as páginas** de uma lista. O mapa do construtor satisfaz isso por construção, já que é indexado por método, não por página. Mas um handler que sobrescreve o escopo assume ele mesmo essa consistência: sobrescreva em *todas* as páginas, nunca apenas quando há um cursor presente, ou a página um e a página dois vão discordar.
|
||||
|
||||
## O que o cliente vê {#what-the-client-sees}
|
||||
|
||||
Numa sessão 2026-07-28, o `Client` respeita as dicas por você: ele tem um cache de respostas embutido, ligado por padrão. Um resultado que chega carregando um `ttlMs` é armazenado, e uma chamada idêntica dentro desse TTL é servida do cache sem viagem de ida e volta. Um resultado que não carrega *nenhuma* dica não é armazenado: resultados sem dica recebem `CacheConfig.default_ttl_ms`, cujo padrão é `0` (obsoleto imediatamente), então um servidor que não declara nada vê exatamente o mesmo tráfego chamada a chamada de sempre.
|
||||
|
||||
```python title="client.py" hl_lines="33 35 38"
|
||||
--8<-- "docs_src/caching/tutorial003.py"
|
||||
```
|
||||
|
||||
Quatro chamadas, três buscas. A segunda chamada encontrou uma entrada fresca e nunca chegou ao servidor; avançar o relógio (injetado) além do TTL fez a terceira buscar de novo; a quarta disse `cache_mode="refresh"`. Esse argumento nomeado existe nos cinco verbos com cache (`list_tools`, `list_prompts`, `list_resources`, `list_resource_templates`, `read_resource`):
|
||||
|
||||
* `"use"` (o padrão) serve uma entrada fresca se houver uma, e armazena a busca se não houver.
|
||||
* `"refresh"` nunca serve: busca e armazena o resultado, substituindo o que quer que estivesse em cache.
|
||||
* `"bypass"` faz a viagem de ida e volta sem tocar no cache: sem leitura, sem escrita.
|
||||
|
||||
Uma regra fica acima de `"use"`: **chamadas que carregam `meta` sempre chegam ao servidor.** Uma requisição com `meta` definido (um token de progresso, campos de rastreamento) espera uma requisição no fio, então sob `cache_mode="use"` ela é tratada como `"refresh"`: a leitura do cache é pulada, e o resultado buscado ainda substitui a entrada em cache. `"bypass"` e um `"refresh"` explícito se comportam como sempre.
|
||||
|
||||
Para desligar o cache por completo, construa com `Client(server, cache=None)`: toda chamada volta a ser uma viagem de ida e volta, e `cache_mode`, embora ainda aceito, não faz nada.
|
||||
|
||||
O escopo também é respeitado automaticamente: entradas `"private"` são indexadas pela *partição* do cache (abaixo), enquanto as `"public"` podem optar por um compartilhamento mais amplo. E **notificações vencem o TTL** para as entradas exatas que nomeiam: uma notificação `list_changed` remove a listagem em cache correspondente, e `resources/updated` remove a leitura em cache armazenada sob exatamente a sua URI, por mais frescas que estivessem. Numa conexão 2026-07-28 essas notificações chegam num stream `subscriptions/listen` que você abre com `client.listen(...)`, e a remoção se completa antes de o seu observador ver o evento; **[Assinaturas](subscriptions.md)** é essa página.
|
||||
|
||||
Uma ressalva sobre `resources/updated`: a remoção é apenas por URI exata. O contrato do store não tem operação de enumeração nem de varredura (igual à implementação de referência em TypeScript), então uma notificação carregando a URI de um *sub*-recurso não remove a leitura em cache do seu pai. Se o seu servidor sinaliza sub-recursos dessa forma, busque o pai de novo com `cache_mode="refresh"`.
|
||||
|
||||
### Configurando: `CacheConfig` {#configuring-it-cacheconfig}
|
||||
|
||||
```python
|
||||
from mcp.client import CacheConfig
|
||||
|
||||
client = Client("https://api.example.com/mcp", cache=CacheConfig(default_ttl_ms=5_000))
|
||||
```
|
||||
|
||||
* `store`: onde as entradas vivem. O padrão é um store em memória novo por cliente; passe a sua própria implementação de `ResponseCacheStore` (apoiada em Redis, digamos) para compartilhar um cache entre clientes ou processos. Os tipos do contrato (`ResponseCacheStore`, `CacheKey`, `CacheEntry` e o padrão `InMemoryResponseCacheStore`) são importáveis de `mcp.client`. Uma consulta pode emitir até dois `get`s sequenciais ao store (o braço privado, depois o público), então dimensione as expectativas de latência de um store remoto de acordo. Um store personalizado **exige** uma `partition` explícita.
|
||||
* `partition`: o rótulo de contexto de autorização que impede que as entradas `"private"` de um principal sejam servidas a outro dentro de um store compartilhado.
|
||||
* `target_id`: identidade explícita do servidor, para transportes personalizados e servidores no mesmo processo (abaixo).
|
||||
* `default_ttl_ms`: TTL aplicado a resultados que não carregam dica `ttlMs`. O padrão `0` deixa resultados sem dica fora do cache.
|
||||
* `share_public`: servir entradas marcadas como `"public"` pelo servidor entre partições (abaixo). Desligado por padrão.
|
||||
* `clock`: a fonte de relógio de parede, em segundos da época. Injete uma, como o exemplo acima faz, e testes de expiração não precisam dormir.
|
||||
|
||||
!!! warning "Partição = principal verificado"
|
||||
Derive `partition` de uma **credencial verificada**, como o sujeito de um token validado. Nunca a derive de dados fornecidos pela requisição, e nunca da URL do servidor (a identidade do servidor é um eixo de chave separado). O SDK é uma biblioteca sem autenticação própria: a âncora de confiança é quem constrói o `CacheConfig`, que é o deploy, não o tenant. Um gateway multi-tenant emite um `CacheConfig` por principal autenticado.
|
||||
|
||||
A partição também é fixa pelo tempo de vida do `Client`. Se o contexto de autorização da conexão mudar no meio da sessão (uma reautenticação como um principal diferente, digamos), o cache não acompanha; construa um novo `Client` para o novo principal.
|
||||
|
||||
As chaves de cache também carregam a **identidade do servidor**: a string de URL que você discou, com qualquer userinfo `user:pass@` removido e, de resto, exata byte a byte. Sem normalização de maiúsculas, sem reordenação de query, sem limpeza de barra final. Normalizar de menos só custa compartilhamento, enquanto normalizar demais poderia fundir dois tenants (`?tenant=a` versus `?tenant=b`), então URLs superficialmente diferentes simplesmente não compartilham entradas. Quando não há URL (um servidor no mesmo processo, ou uma instância de `Transport`), o cliente recebe uma identidade aleatória por instância; defina `CacheConfig.target_id` para nomear o servidor (com um store personalizado isso é obrigatório, e a construção avisa). A identidade passa por hash sha256 antes de entrar no material da chave, então uma URL carregando segredos na query string nunca aparece nas chaves do store. Também não registre em log a forma pré-hash por conta própria.
|
||||
|
||||
!!! warning "`share_public` confia no servidor, para a frota inteira"
|
||||
Por padrão, até entradas `"public"` ficam dentro da sua partição. `share_public=True` serve entradas que o servidor marcou como `cacheScope: "public"` a **todas** as partições que usam o store, confiando na classificação do servidor em nome de todas elas. Um servidor que carimba `"public"` em dados por tenant (por bug ou por malícia) então vaza a resposta de um tenant para os outros. A flag é deliberadamente apenas de nível de construtor: o `cache_mode` por chamada pode restringir o cache, mas nada por chamada pode ampliar o compartilhamento.
|
||||
|
||||
### O que o cache nunca faz {#what-the-cache-never-does}
|
||||
|
||||
* **Chamadas da camada de sessão o contornam.** `client.session.list_tools()` e companhia sempre fazem a viagem de ida e volta; o cache vive nos verbos do `Client`.
|
||||
* **`server/discover` fica de fora.** O resultado de descoberta é entregue uma vez, na conexão, e nunca entra no cache de respostas, mesmo quando carrega um `ttlMs`. Se você persiste um por conta própria para pular a sondagem de reconexão ([`prior_discover`](../protocol-versions.md#reconnecting-with-prior_discover)), o frescor dele é contabilidade sua: `DiscoverResult` carrega `ttl_ms` e `cache_scope`, já parseados, exatamente para isso.
|
||||
* **Páginas de continuação nunca são armazenadas.** Apenas chamadas sem cursor participam. Uma página de continuação rejeitada por cursor expirado *remove*, sim, a listagem em cache, porque a listagem mudou por baixo dela.
|
||||
* **Leituras de múltiplas viagens nunca são armazenadas.** Um `read_resource` semeado com `input_responses`/`request_state`, ou um que se resolve por rodadas de entrada, nunca entra no cache (um MUST da especificação).
|
||||
* **Remoção por notificação precisa de notificações.** A remoção é tão boa quanto a entrega do transporte, e o caminho moderno no mesmo processo (`Client(server)` com o padrão `mode="auto"`) hoje não entrega notificações avulsas.
|
||||
* **A remoção é eventual, não instantânea.** Notificações pelo fio são despachadas a partir de tarefas iniciadas em paralelo, então uma chamada correndo contra a chegada de uma notificação pode receber a entrada pré-remoção mais uma vez; a janela é limitada pela latência de despacho, e a remoção ainda acontece.
|
||||
* **Sem stale-if-error.** Uma entrada expirada nunca é servida porque a nova busca falhou; o erro se propaga.
|
||||
* **Sem busca antecipada.** Uma entrada armazenada é servida até o TTL expirar, e a próxima chamada depois disso paga a viagem de ida e volta; nada se atualiza em segundo plano.
|
||||
* **Sem coalescência.** Duas chamadas idênticas concorrentes são duas buscas.
|
||||
* **Sem TTL acima de 24 horas.** Um `ttlMs` maior, seja enviado pelo servidor ou configurado, é reduzido ao armazenar (`mcp.client.caching.MAX_TTL_MS`), limitando por quanto tempo qualquer entrada, por mais generosa que seja a dica, pode ser servida.
|
||||
* Num **store compartilhado**, os clientes correm uns contra os outros. Cada cliente descarta a própria escrita quando uma remoção ultrapassou a busca em andamento, mas um cliente *co-tenant* ainda pode escrever de volta uma entrada que uma remoção que ele nunca viu havia removido; e essa contabilidade de corrida é ela própria limitada: acima de 4096 chaves rastreadas, a guarda da chave mais antiga é descartada primeiro. Ambas as janelas são aceitas, e fechadas pelo limite de TTL acima.
|
||||
* **Sem servir entre eras do protocolo.** As entradas têm escopo na versão de protocolo negociada: num store persistente compartilhado, uma sessão nunca serve uma entrada escrita sob uma versão negociada diferente (a mesma listagem difere de verdade por era, já que o SDK remove os campos de 2026 para sessões mais antigas). A remoção igualmente só toca as entradas da era atual; as entradas de outra era simplesmente envelhecem pelo TTL.
|
||||
|
||||
### Lendo as dicas por conta própria {#reading-the-hints-yourself}
|
||||
|
||||
As dicas também são campos simples em todo resultado cacheável (`result.ttl_ms` e `result.cache_scope`, já parseados), caso você queira acrescentar a sua própria contabilidade em cima do cache embutido (ou no lugar dele).
|
||||
|
||||
Contra um **servidor mais antigo** (protocolo pré-2026), os campos simplesmente não existem no fio, e os modelos mostram seus padrões conservadores: `ttl_ms == 0` e `cache_scope == "private"`, obsoleto e não compartilhado, a suposição certa para um servidor que não declarou nada. O cache trata uma sessão legada do mesmo jeito: as dicas nunca são consultadas ali (quaisquer que sejam as chaves que apareçam no fio), só `default_ttl_ms` se aplica, e seu padrão de `0` não armazena nada, então uma conexão pré-2026 se comporta exatamente como antes de o cache existir. Se você precisa distinguir "o servidor disse 0" de "o servidor não disse nada", verifique `"ttl_ms" in result.model_fields_set`: só é definido quando o campo realmente chegou.
|
||||
|
||||
## Clientes mais antigos {#older-clients}
|
||||
|
||||
Clientes em versões de protocolo pré-2026 nunca veem nenhum dos dois campos; o SDK os remove na serialização para essas conexões. Configure as suas dicas uma vez; não há nada específico de versão para escrever.
|
||||
|
||||
## Recapitulando {#recap}
|
||||
|
||||
* Seis métodos carregam `ttlMs`/`cacheScope`; o SDK os define por padrão como `0`/`"private"`, obsoleto e não compartilhado, sempre seguro.
|
||||
* `cache_hints={method: CacheHint(...)}` na construção (tanto `MCPServer` quanto `Server`) define valores do servidor inteiro por método.
|
||||
* Um handler que define os campos no seu resultado sobrescreve o mapa, por campo.
|
||||
* `"public"` é uma promessa de que o resultado é idêntico para todo chamador. Não é controle de acesso.
|
||||
* O `Client` respeita as dicas automaticamente: seu cache de respostas fica ligado por padrão, serve entradas frescas em vez de buscar de novo, e não armazena nada para servidores (ou sessões) que não fornecem dicas.
|
||||
* Por chamada, `cache_mode="refresh"` busca de novo e `"bypass"` pula o cache; `cache=None` na construção o desliga por completo.
|
||||
154
i18n/pt/pages/client/callbacks.md
Normal file
154
i18n/pt/pages/client/callbacks.md
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
---
|
||||
translation:
|
||||
sections: [adf3c545b5be46b6, 916cd3ab1c03f461, e9be7a8d0eb0a456, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 90c6043be435fcb0]
|
||||
tool: 1
|
||||
---
|
||||
# Callbacks do cliente {#client-callbacks}
|
||||
|
||||
Quase toda requisição no MCP vai em um só sentido: do cliente para o servidor.
|
||||
|
||||
Um servidor também pode pedir coisas ao **cliente**: fazer uma pergunta ao usuário, amostrar o modelo do usuário, listar as pastas do workspace do usuário. Você responde a essas requisições passando **callbacks** para `Client(...)`.
|
||||
|
||||
## Um servidor que pergunta {#a-server-that-asks}
|
||||
|
||||
Aqui está um servidor cuja ferramenta não consegue terminar sozinha:
|
||||
|
||||
```python title="server.py" hl_lines="16"
|
||||
--8<-- "docs_src/client_callbacks/tutorial001.py"
|
||||
```
|
||||
|
||||
* `ctx.elicit(...)` envia uma requisição `elicitation/create` **para o cliente** e espera.
|
||||
* A ferramenta não retorna até que alguém (uma pessoa em um formulário, ou o seu código) forneça um `name`.
|
||||
|
||||
Essa é a metade do servidor, e a página **[Elicitação](../handlers/elicitation.md)** cuida dela. Esta página é a outra ponta do fio.
|
||||
|
||||
## O callback de elicitação {#the-elicitation-callback}
|
||||
|
||||
```python title="client.py" hl_lines="6-10 16-17"
|
||||
--8<-- "docs_src/client_callbacks/tutorial002.py"
|
||||
```
|
||||
|
||||
* Um callback de elicitação (elicitation) é `async (context, params) -> ElicitResult`.
|
||||
* `params.message` é a pergunta. `params.requested_schema` é o JSON Schema da resposta que o servidor quer. Um cliente de verdade renderiza um formulário a partir dele; este aqui preenche automaticamente.
|
||||
* Você retorna `ElicitResult(action="accept", content={...})`, ou `action="decline"`, ou `action="cancel"`. A única outra opção é `ErrorData(...)`, que recusa a requisição e faz a chamada inteira falhar.
|
||||
* `context` é um `ClientRequestContext`: a `session` ativa, o `request_id` do servidor e qualquer `meta` que ele tenha anexado.
|
||||
|
||||
!!! tip
|
||||
`params` é uma união dos dois modos de elicitação. Aqui `params.mode` é `"form"`; uma requisição `"url"`
|
||||
traz `params.url` em vez de um schema. Um único callback trata os dois; ramifique em `params.mode`.
|
||||
**[Elicitação](../handlers/elicitation.md)** mostra o padrão completo.
|
||||
|
||||
### Experimente {#try-it}
|
||||
|
||||
Chame `issue_card` e observe as duas pontas.
|
||||
|
||||
Seu callback recebe a pergunta do servidor, já analisada:
|
||||
|
||||
```python
|
||||
params.mode # 'form'
|
||||
params.message # 'What name should go on the card?'
|
||||
params.requested_schema # {'properties': {'name': {'title': 'Name', 'type': 'string'}},
|
||||
# 'required': ['name'], 'title': 'CardHolder', 'type': 'object'}
|
||||
```
|
||||
|
||||
Ele responde, `ctx.elicit(...)` retoma dentro da ferramenta, e a ferramenta termina:
|
||||
|
||||
```python
|
||||
result.content # [TextContent(type='text', text='Card issued to Ada Lovelace.')]
|
||||
```
|
||||
|
||||
Um `tools/call` seu, um `elicitation/create` de volta do servidor, respondido pela sua função, tudo dentro de uma única chamada de ferramenta.
|
||||
|
||||
!!! info
|
||||
O `mode="legacy"` na chamada `Client(...)` está fazendo trabalho de verdade. Por padrão, `Client(...)` negocia o caminho
|
||||
moderno do protocolo, e esse caminho não tem canal de retorno (back-channel) para requisições do servidor ao cliente: `ctx.elicit`
|
||||
falha antes mesmo de o seu callback rodar. Não é o transporte que decide isso; é o protocolo
|
||||
negociado, tanto em memória quanto por uma URL. Fixe `mode="legacy"` sempre que o seu cliente tiver
|
||||
que responder a uma; todos os testes por trás desta página fazem isso. **[Versões do protocolo](../protocol-versions.md)** tem a história completa.
|
||||
|
||||
Em uma sessão 2026-07-28 o callback não está morto, ele é alimentado de outro jeito: quando uma ferramenta retorna um
|
||||
`InputRequiredResult` carregando um `ElicitRequest`, o `Client` despacha essa entrada para o mesmo
|
||||
`elicitation_callback` e refaz a chamada para você. Esse fluxo está em **[Requisições de múltiplas idas e voltas](../handlers/multi-round-trip.md)**.
|
||||
|
||||
## Um callback é uma capacidade {#a-callback-is-a-capability}
|
||||
|
||||
Você nunca disse ao servidor que o seu cliente consegue responder a requisições de elicitação. O SDK disse.
|
||||
|
||||
Quando um cliente se conecta, ele declara suas `capabilities`, a imagem espelhada das do servidor. Você não escreve esse objeto. **Registrar um callback é a declaração.**
|
||||
|
||||
| você passa | o cliente declara |
|
||||
| --- | --- |
|
||||
| `elicitation_callback=` | `"elicitation": {"form": {}, "url": {}}` |
|
||||
| `sampling_callback=` | `"sampling": {}` |
|
||||
| `list_roots_callback=` | `"roots": {"listChanged": true}` |
|
||||
| nenhum deles | `{}` |
|
||||
|
||||
As subcapacidades de amostragem (sampling) são o único refinamento: passe `sampling_capabilities=SamplingCapability(tools=SamplingToolsCapability())` junto com `sampling_callback` quando o seu amostrador trata os parâmetros `tools` / `tool_choice`. Os servidores precisam ver `sampling.tools` declarado antes de poderem enviá-los.
|
||||
|
||||
`logging_callback` e `message_handler` não estão na tabela. Eles tratam notificações, e notificações não precisam de capacidade.
|
||||
|
||||
O servidor lê a declaração de volta com `ctx.session.check_client_capability(...)`. Adicione uma ferramenta que faça isso:
|
||||
|
||||
```python title="server.py" hl_lines="23-31"
|
||||
--8<-- "docs_src/client_callbacks/tutorial003.py"
|
||||
```
|
||||
|
||||
Conecte com apenas `elicitation_callback` e chame-a:
|
||||
|
||||
```python
|
||||
result.structured_content # {'result': ['elicitation']}
|
||||
```
|
||||
|
||||
Passe os três callbacks e você recebe `['elicitation', 'sampling', 'roots']`. Não passe nenhum e você recebe `[]`.
|
||||
|
||||
!!! check
|
||||
Agora faça a coisa errada: conecte **sem** `elicitation_callback` e chame `issue_card` mesmo assim.
|
||||
|
||||
A requisição `elicitation/create` do servidor ainda chega ao seu cliente, e o SDK a responde por
|
||||
você, com um erro, porque você nunca disse que conseguiria tratá-la. Esse erro afunda a chamada inteira.
|
||||
`call_tool` não retorna um resultado `is_error`; ele levanta uma exceção:
|
||||
|
||||
```text
|
||||
MCPError: Elicitation not supported
|
||||
```
|
||||
|
||||
Isso é um erro de protocolo (`-32600`, *invalid request*), não um erro de ferramenta: não há nada para
|
||||
o modelo ler e tentar de novo. É por isso que vale a pena ter `client_features`: um servidor bem-comportado
|
||||
verifica antes de perguntar.
|
||||
|
||||
## O par descontinuado {#the-deprecated-pair}
|
||||
|
||||
`sampling_callback` responde a `sampling/createMessage`: o servidor pedindo ao *seu* modelo que complete algo. `list_roots_callback` responde a `roots/list`: o servidor perguntando em quais diretórios ele pode trabalhar.
|
||||
|
||||
Os dois funcionam. Os dois seguem a regra acima. E os dois atendem RPCs que a **spec 2026-07-28 remove**: um servidor moderno não chama de volta o seu cliente no meio de uma requisição, ele devolve a requisição para você como parte do resultado da ferramenta (**[Requisições de múltiplas idas e voltas](../handlers/multi-round-trip.md)**). Os callbacks em si não estão mortos. Quando um `InputRequiredResult` carrega um `CreateMessageRequest` ou um `ListRootsRequest`, o loop automático do `Client` o despacha para o mesmo `sampling_callback` ou `list_roots_callback` que você registrou aqui. A lista inteira está em **[Funcionalidades descontinuadas](../deprecated.md)**.
|
||||
|
||||
Você ainda precisa dos callbacks para falar com servidores que não migraram. As assinaturas:
|
||||
|
||||
```python title="client.py"
|
||||
--8<-- "docs_src/client_callbacks/tutorial004.py"
|
||||
```
|
||||
|
||||
* Um callback de amostragem recebe o `CreateMessageRequestParams` completo (`messages`, `model_preferences`, `max_tokens`) e retorna um `CreateMessageResult`. *Você* executa o modelo, do jeito que quiser; o SDK só transporta a requisição.
|
||||
* Um callback de roots não recebe parâmetro nenhum e retorna um `ListRootsResult`.
|
||||
* Qualquer um dos dois pode retornar `ErrorData(...)` no lugar, para recusar.
|
||||
|
||||
Passe-os para `Client(...)` exatamente como `elicitation_callback`.
|
||||
|
||||
## Os callbacks de notificação {#the-notification-callbacks}
|
||||
|
||||
Mais dois. Nenhum deles declara nada.
|
||||
|
||||
`logging_callback` recebe as `notifications/message` que um servidor envia, como `LoggingMessageNotificationParams` (`level`, `logger`, `data`). O logging de protocolo em si foi descontinuado pela spec 2026-07-28 (**[Logging](../handlers/logging.md)** diz o que fazer no lugar), então esse callback existe para os servidores que ainda o emitem. Em uma conexão da era 2026, o callback sozinho não te dá nada, porque servidores 2026 enviam mensagens de log apenas para requisições que optam por recebê-las: passe `log_level="info"` (ou outro nível) para `Client(...)` para carimbar essa opção em toda requisição e receber esse nível e acima. Servidores pré-2026 o ignoram e mantêm o comportamento de `logging/setLevel`.
|
||||
|
||||
`message_handler` é o pega-tudo: toda notificação do servidor que a sessão expõe chega até ele (além do callback específico dela), e em um transporte baseado em stream toda `Exception` no nível do transporte também. Duas nunca chegam: `notifications/cancelled` é aplicada pelo SDK em vez de exposta, e a confirmação de assinatura de um stream `listen()` ativo é consumida por esse stream. Anote o parâmetro com `IncomingMessage` (`ServerNotification | Exception`, exportado de `mcp.client`). O único padrão que vale conhecer é `if isinstance(message, Exception): raise message`, para que uma conexão quebrada falhe em alto e bom som em vez de sumir.
|
||||
|
||||
## Recapitulando {#recap}
|
||||
|
||||
* Um servidor pode enviar requisições ao cliente. Você as responde com callbacks passados para `Client(...)`.
|
||||
* O callback de elicitação é o atual: `async (context, params) -> ElicitResult`, uma função para os modos formulário e URL.
|
||||
* **Registrar um callback é declarar a capacidade.** Sem ele, o SDK recusa a requisição do servidor em seu nome e a chamada inteira falha com `MCPError`.
|
||||
* Um servidor descobre antes de perguntar com `ctx.session.check_client_capability(...)`.
|
||||
* `sampling_callback` e `list_roots_callback` funcionam do mesmo jeito, mas atendem funcionalidades descontinuadas; servidores modernos usam requisições de múltiplas idas e voltas no lugar.
|
||||
* `logging_callback` e `message_handler` recebem notificações. Eles não declaram nada.
|
||||
|
||||
O primeiro argumento de `Client(...)` é um objeto de transporte. **[Transportes do cliente](transports.md)** cobre todos os tipos.
|
||||
152
i18n/pt/pages/client/identity-assertion.md
Normal file
152
i18n/pt/pages/client/identity-assertion.md
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
---
|
||||
translation:
|
||||
sections: [a91322c46111d16d, 8e6fd6d6f59bb568, e7828fd2729b2c9d, a03ec26bfc678b65, 1034c653c0bcf1b0]
|
||||
tool: 1
|
||||
---
|
||||
# Asserção de identidade {#identity-assertion}
|
||||
|
||||
Um provider OAuth comum (**[Clientes OAuth](oauth-clients.md)**) começa fazendo uma pergunta ao servidor MCP: *em qual servidor de autorização você confia?* Ele segue a resposta para onde quer que ela aponte e, depois, ou uma pessoa faz login ou um segredo pré-compartilhado faz esse papel.
|
||||
|
||||
Uma empresa não quer nenhuma das duas coisas decidida servidor por servidor. Ela já opera um provedor de identidade (Okta, Microsoft Entra ID, o seu próprio); o usuário já fez login nele hoje de manhã; e esse é o único lugar onde o time de segurança quer decidir quem pode acessar o quê. A [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990), a extensão **Enterprise-Managed Authorization**, leva a decisão para lá. O IdP assina um JWT de curta duração, um **Identity Assertion JWT Authorization Grant**, o **ID-JAG**: uma declaração de que *este usuário*, por meio *deste cliente*, pode acessar *este servidor MCP*. O cliente o troca por um token de acesso comum. Sem navegador, sem tela de consentimento, sem registro dinâmico.
|
||||
|
||||
Esta página cobre as duas pontas dessa troca. O servidor MCP em si não muda nada: continua sendo o servidor de recursos de **[Autorização](../run/authorization.md)**, verificando qualquer token que apareça.
|
||||
|
||||
## Duas requisições de token {#two-token-requests}
|
||||
|
||||
Duas autoridades diferentes estão em jogo, e saber distingui-las pelo nome é quase tudo o que você precisa para entender esta página. O **IdP corporativo** é o provedor de identidade da sua organização: ele sabe quem é o funcionário, é onde as políticas vivem e é quem emite o ID-JAG. O SDK nunca fala com ele. O **servidor de autorização MCP** é a mesma parte que era em **[Autorização](../run/authorization.md)**: o issuer nomeado nos metadados do servidor MCP, aquilo que emite os tokens que esse servidor MCP aceita. Em um fluxo OAuth comum, esses dois papéis costumam ser uma caixa só. Aqui são duas, e o grant inteiro é a segunda concordando em confiar na primeira.
|
||||
|
||||
O cliente faz uma requisição de token a cada uma.
|
||||
|
||||
1. **Ao IdP corporativo.** O cliente troca o login do usuário (o ID token OpenID Connect dele) pelo ID-JAG. É um token exchange da [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693), é inteiramente a API do seu IdP, e **o SDK não o faz**. Você faz, dentro de um único callback assíncrono. É também onde a decisão de política acontece: um IdP que diz não nunca emite o ID-JAG, e não há nada a apresentar.
|
||||
2. **Ao servidor de autorização MCP.** O cliente apresenta o ID-JAG sob o grant `jwt-bearer` da [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523) (`grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer`, o ID-JAG como `assertion`) e recebe o token de acesso. **Esta é a requisição que o SDK faz**, e aceitá-la é a única coisa que esta página acrescenta a um servidor de autorização.
|
||||
|
||||
Tudo abaixo é a segunda requisição: o cliente que a envia e o servidor de autorização que a responde.
|
||||
|
||||
## O cliente {#the-client}
|
||||
|
||||
**`IdentityAssertionOAuthProvider`** fica em `mcp.client.auth.extensions.identity_assertion`. Como todo provider em **[Clientes OAuth](oauth-clients.md)**, ele é um `httpx2.Auth`: construa um, passe em `auth=`, entregue o `httpx2.AsyncClient` ao transporte.
|
||||
|
||||
```python title="client.py" hl_lines="49-50 53-61"
|
||||
--8<-- "docs_src/identity_assertion/tutorial001.py"
|
||||
```
|
||||
|
||||
Leia de baixo para cima.
|
||||
|
||||
* `main()` é o `main()` padrão de cliente OAuth (**[Clientes OAuth](oauth-clients.md)**), sem mudar uma linha sequer. Esse é o ponto: uma vez que o provider existe, nada adiante sabe qual grant produziu o token.
|
||||
* O provider recebe aquilo que os outros providers não conseguem descobrir: um `client_id` e um `client_secret` que alguém **pré-registrou** no servidor de autorização, o `issuer` desse servidor de autorização e `assertion_provider`, um callback assíncrono que retorna um ID-JAG novo sob demanda.
|
||||
* `storage` é o mesmo protocolo `TokenStorage`. Só os dois métodos de token são chamados; não há registro dinâmico aqui, então não há `client_info` para lembrar.
|
||||
|
||||
### O provedor de asserção {#the-assertion-provider}
|
||||
|
||||
`fetch_id_jag(audience, resource)` é o único código que você escreve. Ele é aguardado com await uma vez por troca de token, nunca na construção, e só *depois* que os metadados do servidor de autorização foram buscados e validados, de modo que um issuer mal configurado nunca vaza uma asserção. Seus dois argumentos são duas das claims com que o ID-JAG precisa ser emitido: `audience` é o issuer do servidor de autorização (o `aud` do ID-JAG) e `resource` é o identificador canônico do servidor MCP (o `resource` do ID-JAG). A terceira você já tem em mãos: a claim `client_id` do ID-JAG precisa nomear o `client_id` que você deu ao provider, ou o servidor de autorização recusa a troca.
|
||||
|
||||
`idp_issue_id_jag`, logo acima, **não é código seu**. Ele faz o papel do provedor de identidade, assinando a asserção no próprio processo para que o arquivo fique completo e você possa ler cada claim que um ID-JAG carrega. Um `fetch_id_jag` de verdade faz, em vez disso, a primeira requisição de token da seção anterior: um token exchange da [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) contra o seu IdP, definido pelo draft Identity Assertion JWT Authorization Grant do qual a [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) é um perfil. O ID token do usuário logado entra como `subject_token`, o `requested_token_type` é a URN própria do ID-JAG (`urn:ietf:params:oauth:token-type:id-jag`), `audience` e `resource` passam direto, e a resposta traz o ID-JAG. Essa troca, com esses nomes, é o que procurar na documentação do seu IdP.
|
||||
|
||||
!!! tip
|
||||
Um ID-JAG novo é solicitado a cada troca, e esse é o ponto: é um grant de uso único, que vive
|
||||
minutos, e o servidor de autorização desta página se recusa a aceitar o mesmo duas vezes. Não
|
||||
faça cache dele. O que é reutilizado é o token de acesso que ele compra para você.
|
||||
|
||||
### O issuer é configuração {#the-issuer-is-configuration}
|
||||
|
||||
Aqui está a inversão. `OAuthClientProvider` pergunta ao servidor de recursos qual servidor de autorização usar e segue a resposta para onde quer que ela aponte. Este provider se recusa a fazer isso: `issuer` é obrigatório, os metadados da [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) são buscados no caminho well-known do próprio issuer, o endpoint de token precisa estar na origem desse issuer, e nada é perguntado ao servidor de recursos.
|
||||
|
||||
A extensão não exige isso; é uma escolha deliberadamente mais rígida. Este cliente carrega duas coisas que valem a pena roubar, um segredo pré-registrado e uma asserção vinculada a uma audience, e um cliente que deixasse um servidor MCP comprometido conduzi-lo até o servidor de autorização de um atacante postaria as duas lá. Fixar o issuer na construção elimina essa conversa.
|
||||
|
||||
!!! warning
|
||||
O `issuer` configurado é comparado com o campo `issuer` do documento de metadados pela
|
||||
comparação simples de strings da RFC 8414 §3.3: caractere por caractere, barra final incluída,
|
||||
sem normalização. Não chute. Busque `/.well-known/oauth-authorization-server` no seu servidor
|
||||
de autorização e copie o valor de `issuer` que ele retorna. Para o servidor de autorização desta
|
||||
página, é `https://auth.example.com/`, com a barra, porque seu issuer foi construído a partir de
|
||||
um objeto URL do pydantic. Uma divergência para o fluxo em `OAuthFlowError: Authorization server metadata issuer
|
||||
mismatch` antes de qualquer credencial ou asserção ser enviada.
|
||||
|
||||
### Um cliente confidencial {#a-confidential-client}
|
||||
|
||||
`client_secret` é obrigatório; o construtor levanta `ValueError` sem ele. O perfil do IETF por baixo da [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) reserva este grant para clientes confidenciais, a SEP-990 exige que o cliente se autentique, e este SDK impõe as duas coisas insistindo em um segredo compartilhado. `token_endpoint_auth_method` escolhe por onde ele viaja: `client_secret_post` (o padrão, no corpo do formulário) ou `client_secret_basic` (um cabeçalho HTTP Basic). O perfil também permite `private_key_jwt`; este provider não oferece suporte a ele.
|
||||
|
||||
!!! tip
|
||||
Leia `client_secret` do ambiente ou de um gerenciador de segredos, nunca do controle de versão.
|
||||
|
||||
### O que o provider faz por você {#what-the-provider-does-for-you}
|
||||
|
||||
A primeira requisição sai sem autenticação, e o `401` do servidor inicia o fluxo.
|
||||
|
||||
1. **Descoberta.** Ele busca os metadados do servidor de autorização no caminho well-known da [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) do issuer configurado, verifica que o `issuer` do documento confere e verifica que o endpoint de token está na origem do issuer.
|
||||
2. **A asserção.** Ele aguarda com await o seu `assertion_provider`.
|
||||
3. **Troca.** Ele faz POST do grant `jwt-bearer` no endpoint de token, armazena o `OAuthToken` e reenvia sua requisição original com `Authorization: Bearer ...`.
|
||||
|
||||
Um `403` cujo `WWW-Authenticate` nomeia `insufficient_scope` executa os passos 2 e 3 de novo com a união do seu `scope` com o do desafio. (`scope` nunca passa de uma solicitação; o servidor de autorização desta página concede o que o ID-JAG diz e nada mais.) Não há refresh token em lugar nenhum disto: quando o token de acesso expira, o próximo `401` emite um ID-JAG novo e troca de novo, e *essa* é a alavanca que o IdP tem nas mãos. As falhas são as mesmas duas exceções do resto de **[Clientes OAuth](oauth-clients.md)**: `OAuthFlowError` para descoberta e validação, sua subclasse `OAuthTokenError` quando o endpoint de token diz não.
|
||||
|
||||
## O servidor de autorização {#the-authorization-server}
|
||||
|
||||
Na maioria das vezes você para aqui. O servidor de autorização MCP é produto de outra pessoa, aceitar ID-JAGs é uma configuração dele a ser ligada, e a metade da [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) que cabe ao SDK é o cliente acima.
|
||||
|
||||
O SDK também pode *ser* o servidor de autorização: `create_auth_routes` retorna as rotas do servidor de autorização como uma lista que qualquer app Starlette pode montar, e é assim que `examples/servers/simple-auth/` no repositório executa um. A SEP-990 acrescenta uma flag e um método a essa superfície:
|
||||
|
||||
```python title="auth_server.py" hl_lines="48-50 105-107"
|
||||
--8<-- "docs_src/identity_assertion/tutorial002.py"
|
||||
```
|
||||
|
||||
* `identity_assertion_enabled=True` controla tudo. Desligada, que é o padrão, `/token` responde a este grant com `unsupported_grant_type` mesmo que você tenha implementado o hook, e os metadados não o mencionam. Ligada, os metadados ganham o grant type `jwt-bearer` e listam `urn:ietf:params:oauth:grant-profile:id-jag` em `authorization_grant_profiles_supported`, o campo que a extensão usa para anunciar suporte. (O cliente deste SDK nunca o lê: ele é provisionado para um único issuer e simplesmente pede.)
|
||||
* **`exchange_identity_assertion`** é o hook. Antes de ele rodar, o SDK já autenticou o cliente, recusou clientes públicos e recusou clientes cujo registro não lista o grant. Você recebe um `IdentityAssertionParams` (a `assertion` crua, os `scopes` e o `resource` solicitados) e retorna um `OAuthToken` simples.
|
||||
* O registro dinâmico de clientes recusa este grant incondicionalmente, então `get_client` aqui serve um cliente provisionado à mão. Um cliente ID-JAG não consegue passar a existir registrando a si mesmo.
|
||||
* Metade da classe são recusas. `OAuthAuthorizationServerProvider` é o servidor de autorização *inteiro*, então também pede o fluxo authorization code; um servidor que também faz login de usuários implementa esses métodos de verdade, e este aqui tem exatamente uma porta.
|
||||
|
||||
!!! warning
|
||||
O SDK nunca decodifica a asserção: só o seu deploy sabe em qual IdP confia e quais chaves esse
|
||||
IdP publica, então tudo dentro de `exchange_identity_assertion` é o que sustenta a segurança.
|
||||
Verifique a assinatura contra as chaves publicadas pelo IdP (o JWKS dele; o segredo
|
||||
compartilhado aqui é o da demo), e também `iss` e `exp`, conforme a [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523) §3. Exija que o
|
||||
`typ` do cabeçalho do JWT seja `oauth-id-jag+jwt`, a proteção do perfil contra algum outro JWT
|
||||
ser reapresentado como grant. Exija que `aud` seja o seu próprio issuer. Exija que a claim
|
||||
`client_id` do ID-JAG seja igual ao cliente que o handler autenticou, e que a claim `resource`
|
||||
nomeie um recurso que você de fato serve. Rastreie o `jti` até o `exp` da asserção para que ela
|
||||
seja aceita uma vez só. E tire os escopos concedidos e, acima de tudo, o `resource` do token
|
||||
emitido do ID-JAG validado, nunca da requisição: `params.resource` é o que quer que o cliente
|
||||
tenha digitado. As regras completas de processamento estão na
|
||||
[especificação Enterprise-Managed Authorization](https://modelcontextprotocol.io/extensions/auth/enterprise-managed-authorization).
|
||||
|
||||
Rejeite uma asserção ruim com `TokenError("invalid_grant", ...)`. O outro código de erro neste fluxo é `invalid_target`: um ID-JAG que nomeia um recurso que você não serve é recusado com ele, e é isso que impede este servidor de emitir tokens para o recurso de outra pessoa. E os escopos concedidos vêm da claim `scope` do ID-JAG (uma asserção sem ela também é recusada); o seu talvez mapeie os grupos do usuário em vez disso.
|
||||
|
||||
E repare no que o `OAuthToken` retornado não carrega: um refresh token. O IdP decide por quanto tempo este usuário mantém o acesso ao decidir se emite o próximo ID-JAG. Um refresh token emitido aqui devolveria essa decisão sem alarde.
|
||||
|
||||
!!! info
|
||||
Um servidor que ainda embute seu servidor de autorização com `auth_server_provider=` chega ao
|
||||
mesmo código por meio de `AuthSettings(identity_assertion_enabled=True)`. **[Autorização](../run/authorization.md)** explica
|
||||
por que servidores novos não deveriam começar por aí.
|
||||
|
||||
!!! check
|
||||
Conecte os dois arquivos desta página e o grant inteiro é um único `POST /token`:
|
||||
|
||||
```text
|
||||
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
|
||||
assertion=eyJhbGciOiJIUzI1NiIsInR5cCI6Im9hdXRoLWlkLWphZytqd3QifQ...
|
||||
client_id=finance-agent
|
||||
resource=http://localhost:8001/mcp
|
||||
scope=notes:read
|
||||
client_secret=finance-agent-secret
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
{"access_token": "mcp_...", "token_type": "Bearer", "expires_in": 300, "scope": "notes:read"}
|
||||
```
|
||||
|
||||
Sem `/authorize`, sem `/register`, sem busca de protected-resource metadata. As únicas
|
||||
requisições na rede são a que provocou o `401`, a busca do well-known, esta troca e, depois,
|
||||
tráfego MCP comum com o bearer anexado. E o `sub` que o seu validador leu do ID-JAG é
|
||||
exatamente o que `get_access_token().subject` informa dentro de uma ferramenta.
|
||||
|
||||
### Experimente {#try-it}
|
||||
|
||||
`examples/stories/identity_assertion/` no repositório do SDK é esta página rodando de verdade: o mesmo validador `exchange_identity_assertion`, um servidor MCP protegido pelos tokens dele, um IdP substituto e o cliente, em um único programa que se autoverifica. `uv run python -m stories.identity_assertion.client --http` executa a troca inteira e confirma com assert que o usuário que o IdP nomeou é o usuário que a ferramenta vê.
|
||||
|
||||
## Recapitulando {#recap}
|
||||
|
||||
* A [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) deixa o provedor de identidade corporativo, e não o usuário final, decidir quais servidores MCP um cliente pode acessar. O IdP assina essa decisão em um **ID-JAG**.
|
||||
* Obter o ID-JAG é um token exchange da [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) contra *o seu IdP*, e o SDK não o faz. Apresentá-lo ao servidor de autorização MCP é o grant `jwt-bearer` da [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523), e o SDK faz os dois lados disso.
|
||||
* `IdentityAssertionOAuthProvider` é mais um `httpx2.Auth`: um cliente confidencial pré-registrado, um `issuer` fixado e um callback `assertion_provider(audience, resource)`. Sem navegador, sem registro, sem refresh token.
|
||||
* O servidor de autorização nunca é descoberto a partir do servidor de recursos. Configure `issuer` com exatamente a string que o documento de metadados dele serve; a comparação é caractere por caractere.
|
||||
* Do lado do servidor, `identity_assertion_enabled=True` mais `exchange_identity_assertion`. O SDK autentica o cliente e controla o acesso ao grant; validar o ID-JAG é inteiramente com você, e o token emitido fica vinculado ao `resource` do ID-JAG, não ao da requisição.
|
||||
|
||||
A única parte que esta página nunca tocou é o servidor MCP. O que ele faz com o token que você acabou de emitir, ele já fazia em **[Autorização](../run/authorization.md)**.
|
||||
217
i18n/pt/pages/client/index.md
Normal file
217
i18n/pt/pages/client/index.md
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
---
|
||||
translation:
|
||||
sections: [ebef1e7a0df854f4, a4c687d3d627d516, 8e79141fc2985342, b345dd05b9c3c7ab, 80ce41579825a6fa, 5f0fa90494de8f65, 83d10514eaa62fa5, 9190555aa39a5d28, 84a4c9d8bf14dddb, 927d71cf40b58c30]
|
||||
tool: 1
|
||||
---
|
||||
# O cliente {#the-client}
|
||||
|
||||
Um **`Client`** é como um programa Python conversa com um servidor MCP.
|
||||
|
||||
É um objeto com um ciclo de vida: construa, entre no `async with`, chame os métodos. Cada verbo do protocolo (listar as ferramentas, chamar uma, ler um recurso, renderizar um prompt) é um método `async` nele que retorna um resultado tipado.
|
||||
|
||||
## Seu primeiro cliente {#your-first-client}
|
||||
|
||||
```python title="client.py" hl_lines="14-18"
|
||||
--8<-- "docs_src/client/tutorial001.py"
|
||||
```
|
||||
|
||||
O servidor no topo só está ali para você ter algo a que se conectar. O cliente são as cinco linhas destacadas.
|
||||
|
||||
* `Client(mcp)` recebe o **próprio objeto servidor**. Esse é o transporte em memória: sem subprocesso, sem porta, sem HTTP. É assim que todo exemplo nesta página, e todo teste que você escrever, se conecta.
|
||||
* `async with` é o **ciclo de vida**. Entrar nele conecta e negocia; sair dele desconecta. Não há um par `connect()` / `close()`, e um `Client` não pode ser reutilizado depois que o bloco termina.
|
||||
* Dentro do bloco, os fatos da conexão já estão ali como propriedades comuns.
|
||||
|
||||
### O que você pode passar para `Client` {#what-you-can-pass-to-client}
|
||||
|
||||
`Client` recebe um argumento posicional e resolve o transporte a partir do tipo dele:
|
||||
|
||||
* Uma instância de `MCPServer` (ou do `Server` de baixo nível): conectada **no mesmo processo**.
|
||||
* Uma string de URL (`Client("http://localhost:8000/mcp")`): Streamable HTTP, o caminho de produção.
|
||||
* Um **transporte**: qualquer coisa com que você possa fazer `async with ... as (read, write)`, como `stdio_client(...)` encapsulando um subprocesso.
|
||||
|
||||
Todo o resto desta página é idêntico entre os três. Cabeçalhos, subprocessos, timeouts e o protocolo `Transport` têm sua própria página: **[Transportes do cliente](transports.md)**.
|
||||
|
||||
### O que há em um cliente conectado {#whats-on-a-connected-client}
|
||||
|
||||
Quatro propriedades somente leitura, preenchidas no instante em que você entra no bloco:
|
||||
|
||||
* `client.server_info`: a identidade do servidor, ou `None` para um servidor da era 2026 que não informa uma (servidores do python-sdk informam por padrão). `server_info.name` aqui é `"Bookshop"`, `server_info.version` é o que o servidor informar.
|
||||
* `client.server_capabilities`: o que o servidor sabe fazer (`tools`, `resources`, `prompts`, `completions`, ...). Uma capacidade que o servidor não tem é `None`.
|
||||
* `client.protocol_version`: a versão do protocolo em que os dois lados concordaram. Aqui é `"2026-07-28"`.
|
||||
* `client.instructions`: a string `instructions=` do servidor, ou `None` se ele não definiu uma.
|
||||
|
||||
Você nunca escolheu uma versão do protocolo. Por padrão, o `Client` sonda o servidor e recorre ao handshake clássico nos mais antigos, então um único cliente funciona contra servidores de qualquer era. Quando você precisar controlar isso, **[Versões do protocolo](../protocol-versions.md)** tem a história completa.
|
||||
|
||||
!!! tip
|
||||
`client.session` é a `ClientSession` subjacente, a saída de emergência de baixo nível.
|
||||
Você não vai precisar dela para nada nesta página.
|
||||
|
||||
## Listando ferramentas {#listing-tools}
|
||||
|
||||
```python title="client.py" hl_lines="15-20"
|
||||
--8<-- "docs_src/client/tutorial002.py"
|
||||
```
|
||||
|
||||
`list_tools()` retorna um `ListToolsResult`; as ferramentas estão em `.tools`. Cada uma é a definição completa que um host entregaria a um modelo:
|
||||
|
||||
```python
|
||||
tool.name # 'search_books'
|
||||
tool.title # 'Search the catalog'
|
||||
tool.description # 'Search the catalog by title or author.'
|
||||
```
|
||||
|
||||
e `tool.input_schema` é o JSON Schema que o servidor derivou das anotações de tipo da função:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"title": "Query", "type": "string"},
|
||||
"limit": {"default": 10, "title": "Limit", "type": "integer"}
|
||||
},
|
||||
"required": ["query"],
|
||||
"title": "search_booksArguments"
|
||||
}
|
||||
```
|
||||
|
||||
Esse schema é tudo o que uma UI precisa para renderizar um formulário de argumentos, e tudo o que um modelo precisa para produzir argumentos válidos.
|
||||
|
||||
!!! tip
|
||||
`title` é opcional, então uma UI que mostra ferramentas a um humano tem que escolher: o `title` se houver um,
|
||||
o `name` se não. `from mcp.shared.metadata_utils import get_display_name` faz exatamente isso,
|
||||
para ferramentas, recursos, templates de recurso e prompts.
|
||||
|
||||
## Chamando uma ferramenta {#calling-a-tool}
|
||||
|
||||
`call_tool(name, arguments)` executa a ferramenta e devolve um `CallToolResult`.
|
||||
|
||||
```python title="client.py" hl_lines="26-33"
|
||||
--8<-- "docs_src/client/tutorial003.py"
|
||||
```
|
||||
|
||||
O `lookup_book` do servidor retorna um `Book` do Pydantic. Eis o que o cliente vê:
|
||||
|
||||
```python
|
||||
result.content # [TextContent(type='text', text='{\n "title": "Dune",\n "author": "Frank Herbert",\n "year": 1965\n}')]
|
||||
result.structured_content # {'title': 'Dune', 'author': 'Frank Herbert', 'year': 1965}
|
||||
result.is_error # False
|
||||
```
|
||||
|
||||
Um valor de retorno, três coisas para ler. Cada uma tem um consumidor diferente.
|
||||
|
||||
### `content`: o que o modelo lê {#content-what-the-model-reads}
|
||||
|
||||
`content` é uma `list` de **blocos de conteúdo**, e um bloco de conteúdo é uma união: `TextContent`, `ImageContent`, `AudioContent`, `ResourceLink` ou `EmbeddedResource`. Uma ferramenta pode retornar vários, de tipos diferentes.
|
||||
|
||||
É por isso que `main` faz o narrowing com `isinstance(block, TextContent)` antes de tocar em `block.text`. Repare que não há `.text` fora do `isinstance`: o verificador de tipos não permite, porque `ImageContent` tem `.data`, não `.text`. A união é honesta sobre o que uma ferramenta pode enviar a você; seu código também deve ser.
|
||||
|
||||
### `structured_content`: o que sua aplicação lê {#structured_content-what-your-application-reads}
|
||||
|
||||
`structured_content` é o valor de retorno da ferramenta como JSON, correspondendo ao `output_schema` declarado pela ferramenta. Sem parsing de strings, sem adivinhação.
|
||||
|
||||
Quando ambos estão presentes, eles dizem a mesma coisa duas vezes de propósito: `content` é para um modelo, `structured_content` é para código. De onde vem a metade estruturada, e como controlá-la, é a página **[Saída estruturada](../servers/structured-output.md)**.
|
||||
|
||||
### `is_error`: se a ferramenta falhou {#is_error-whether-the-tool-failed}
|
||||
|
||||
Uma ferramenta que lança uma exceção **não** lança no seu cliente. Ela volta como um resultado comum com `is_error=True`.
|
||||
|
||||
!!! check
|
||||
Peça `"Solaris"` ao `lookup_book` (um título que não está no catálogo) e a função lança
|
||||
`ValueError`. A chamada ainda retorna normalmente:
|
||||
|
||||
```python
|
||||
result.is_error # True
|
||||
result.content # [TextContent(type='text', text="Error executing tool lookup_book: No book titled 'Solaris' in the catalog.")]
|
||||
result.structured_content # None
|
||||
```
|
||||
|
||||
A mensagem da exceção foi parar em `content`, onde o **modelo** pode lê-la e tentar de novo. Isso
|
||||
é proposital: um erro de ferramenta faz parte da conversa, não é um crash. Sempre olhe `is_error`
|
||||
antes de confiar em `structured_content`.
|
||||
|
||||
!!! warning
|
||||
`is_error=True` cobre mais do que o seu próprio `raise`. Peça uma ferramenta que o servidor nem tem
|
||||
(`call_tool("does_not_exist", {})`) e nada lança exceção. Você recebe o mesmo formato de volta,
|
||||
`is_error=True` com `Unknown tool: does_not_exist` em `content`. Um método de `Client` lança
|
||||
`MCPError` apenas quando o servidor responde com um **erro** JSON-RPC em vez de um resultado, e
|
||||
**[Tratando erros](../servers/handling-errors.md)** cobre quando um servidor produz cada um.
|
||||
|
||||
## Recursos {#resources}
|
||||
|
||||
Os verbos de recurso vêm em pares: duas formas de listar, uma forma de ler.
|
||||
|
||||
```python title="client.py" hl_lines="22-31"
|
||||
--8<-- "docs_src/client/tutorial004.py"
|
||||
```
|
||||
|
||||
* `list_resources()` retorna os recursos **concretos**, os que têm uma URI fixa. Aqui: `['catalog://genres']`.
|
||||
* `list_resource_templates()` retorna os **parametrizados**. Aqui: `['catalog://genres/{genre}']`. São duas listas diferentes porque um template não pode ser lido até você preenchê-lo.
|
||||
* `read_resource(uri)` recebe uma URI `str` comum e funciona com ambos: passe `"catalog://genres/poetry"` e o servidor a casa com o template.
|
||||
|
||||
`read_resource` retorna `contents`, uma lista de `TextResourceContents` ou `BlobResourceContents`. Mesma ideia do conteúdo de ferramenta: faça o narrowing com `isinstance`, depois leia `.text` (ou `.blob`).
|
||||
|
||||
Um cliente também pode ser avisado quando um recurso muda. Em conexões da era 2025 isso é `subscribe_resource(uri)` / `unsubscribe_resource(uri)` - um par de métodos que o `MCPServer` não implementa, então no protocolo 2026-07-28 (onde esses verbos não existem mais) a requisição responde `-32601`, *Method not found*. O substituto de 2026 é um stream `subscriptions/listen`, que o `MCPServer` *serve* sim - `server_capabilities.resources.subscribe` é `True` ali - e consumi-lo com `client.listen(...)` é a página **[Assinaturas](subscriptions.md)** desta seção.
|
||||
|
||||
## Prompts {#prompts}
|
||||
|
||||
```python title="client.py" hl_lines="15-20"
|
||||
--8<-- "docs_src/client/tutorial005.py"
|
||||
```
|
||||
|
||||
`list_prompts()` diz o que o servidor oferece e do que cada prompt precisa:
|
||||
|
||||
```python
|
||||
prompt.name # 'recommend'
|
||||
prompt.title # 'Recommend a book'
|
||||
prompt.arguments # [PromptArgument(name='genre', required=True)]
|
||||
```
|
||||
|
||||
`get_prompt(name, arguments)` o renderiza. O dict de argumentos é `str -> str`: argumentos de prompt são sempre strings. O resultado é `messages`, uma lista de `PromptMessage`, cada uma com um `role` e um bloco `content`:
|
||||
|
||||
```python
|
||||
message.role # 'user'
|
||||
message.content # TextContent(type='text', text='Recommend one poetry book from the catalog and say why.')
|
||||
```
|
||||
|
||||
Um host entrega essas mensagens direto ao modelo. A funcionalidade inteira é essa.
|
||||
|
||||
## Completions {#completions}
|
||||
|
||||
Um servidor com um handler de completion pode autocompletar argumentos de prompts e de templates de recurso enquanto o usuário digita.
|
||||
|
||||
```python title="client.py" hl_lines="27-31"
|
||||
--8<-- "docs_src/client/tutorial006.py"
|
||||
```
|
||||
|
||||
* `ref` diz *qual* prompt ou template você está preenchendo: uma `PromptReference` ou uma `ResourceTemplateReference`.
|
||||
* `argument` é `{"name": ..., "value": ...}`: o argumento e o que o usuário digitou até agora.
|
||||
|
||||
A resposta está em `result.completion.values`. Digite `"p"` e o servidor volta com `['poetry']`. O lado do servidor, e como um handler usa os *outros* argumentos já preenchidos para refinar as sugestões, é a página **[Completions](../servers/completions.md)**.
|
||||
|
||||
## Paginação {#pagination}
|
||||
|
||||
Todo método `list_*` aceita um argumento nomeado `cursor=` e todo resultado carrega um `next_cursor`. Quando `next_cursor` é `None`, você tem tudo.
|
||||
|
||||
```python title="client.py" hl_lines="22-30"
|
||||
--8<-- "docs_src/client/tutorial007.py"
|
||||
```
|
||||
|
||||
Esse loop está correto contra qualquer servidor. O `MCPServer` retorna tudo em uma página só, então `next_cursor` é `None` e o loop roda uma vez, e é por isso que a maioria do código nunca o escreve. Servidores que paginam de verdade, e as regras que os cursores obedecem, estão em **[Paginação](../advanced/pagination.md)**.
|
||||
|
||||
## Em testes {#in-tests}
|
||||
|
||||
`Client(mcp)`, sem processo e sem porta, já é um harness de teste para o seu servidor.
|
||||
|
||||
Existe uma flag do construtor feita para isso: `Client(mcp, raise_exceptions=True)`. Ela só tem efeito em conexões em memória, e **[Testes](../get-started/testing.md)** é a página que a explica e constrói todo o padrão em torno dela.
|
||||
|
||||
## Recapitulando {#recap}
|
||||
|
||||
* `Client(x)` conecta em memória a um objeto servidor, via Streamable HTTP a uma string de URL, e por qualquer outra coisa via um transporte.
|
||||
* `async with` é o ciclo de vida inteiro. Dentro dele, `server_capabilities` e `protocol_version` já estão preenchidos; `server_info` e `instructions` também, quando o servidor os fornece.
|
||||
* `list_tools()` dá a você o `name`, `title`, `description` e `input_schema` de cada ferramenta.
|
||||
* `call_tool()` retorna `content` para o modelo, `structured_content` para o seu código e `is_error`. Uma ferramenta que lança exceção é um resultado, não uma exceção.
|
||||
* `content` é uma união de tipos de bloco; faça o narrowing com `isinstance` antes de ler.
|
||||
* `list_resources` / `list_resource_templates` / `read_resource`, `list_prompts` / `get_prompt` e `complete` completam os verbos.
|
||||
* Todo `list_*` aceita `cursor=`; itere até `next_cursor` ser `None`.
|
||||
|
||||
As coisas que um servidor pode pedir ao *cliente*, e como você as responde, são os **[Callbacks do cliente](callbacks.md)**.
|
||||
152
i18n/pt/pages/client/oauth-clients.md
Normal file
152
i18n/pt/pages/client/oauth-clients.md
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
---
|
||||
translation:
|
||||
sections: [c6899d3892bd9fa0, 79372cff3cc48a88, 63878d29e87c3e73, 13175843d3588af4, e7e2b9fd516f77de, 758f06399b513c1f, a05d7278487d610b]
|
||||
tool: 1
|
||||
---
|
||||
# Clientes OAuth {#oauth-clients}
|
||||
|
||||
Alguns servidores MCP são protegidos. Envie a eles uma requisição sem token e a resposta é `401 Unauthorized`.
|
||||
|
||||
**`OAuthClientProvider`** é como você consegue o token. Ele não é um objeto MCP. É um `httpx2.Auth`, o hook padrão do httpx2 para "fazer algo em toda requisição". Você o anexa a um `httpx2.AsyncClient`, entrega esse cliente ao transporte Streamable HTTP e para de pensar no assunto.
|
||||
|
||||
Esta página é o lado do cliente. Fazer o seu próprio servidor exigir um token está em **[Autorização](../run/authorization.md)**.
|
||||
|
||||
## O provider {#the-provider}
|
||||
|
||||
```python title="client.py" hl_lines="44-54"
|
||||
--8<-- "docs_src/oauth_clients/tutorial001.py"
|
||||
```
|
||||
|
||||
Você entrega quatro coisas a ele:
|
||||
|
||||
* `server_url`: o endpoint MCP ao qual você está se conectando. O provider descobre todo o resto a partir dele.
|
||||
* `client_metadata`: o que você digitaria no formulário "registrar uma aplicação" de um servidor de autorização.
|
||||
* `storage`: onde os tokens ficam entre uma execução e outra.
|
||||
* `redirect_handler` e `callback_handler`: os dois momentos em que um humano participa.
|
||||
|
||||
Nada mais no arquivo menciona OAuth. `main()` nunca vê um token.
|
||||
|
||||
### Metadados do cliente {#client-metadata}
|
||||
|
||||
`OAuthClientMetadata` é o documento de registro real da [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591), na forma de um modelo Pydantic.
|
||||
|
||||
Você define três campos. Os valores padrão preenchem o resto: `grant_types` já é `["authorization_code", "refresh_token"]` e `response_types` já é `["code"]`, que é exatamente o fluxo que este provider executa.
|
||||
|
||||
!!! check
|
||||
Por ser um modelo Pydantic, ele valida **antes de um único byte trafegar pela rede**.
|
||||
Deixe `redirect_uris` de fora e a construção falha na hora com um `ValidationError` que
|
||||
nomeia o campo:
|
||||
|
||||
```text
|
||||
redirect_uris
|
||||
Field required [type=missing, input_value={'client_name': 'Bookshop Agent'}, input_type=dict]
|
||||
```
|
||||
|
||||
Nenhum navegador aberto, nenhum registro pela metade deixado para trás no servidor de autorização.
|
||||
|
||||
### Armazenamento de tokens {#token-storage}
|
||||
|
||||
**`TokenStorage`** é um `Protocol` com quatro métodos async. Você não herda de nada; escreva os métodos e qualquer classe vira um armazenamento de tokens:
|
||||
|
||||
* `get_tokens` / `set_tokens` guardam o `OAuthToken`: token de acesso, refresh token, expiração, escopo.
|
||||
* `get_client_info` / `set_client_info` guardam o `OAuthClientInformationFull` que o servidor de autorização emitiu quando o provider registrou você, incluindo o seu `client_id`.
|
||||
|
||||
A versão em memória acima funciona. Ela também esquece tudo quando o processo termina, então a próxima execução refaz a dança inteira. Persista em um arquivo ou no keyring da sua plataforma e a próxima execução fica silenciosa.
|
||||
|
||||
!!! tip
|
||||
Armazene `client_info`, não só os tokens. O provider faz o registro dinâmico na primeira vez em que
|
||||
não encontra um `client_info` armazenado. Jogue-o fora e você cria um registro novo a cada execução.
|
||||
|
||||
### Os dois handlers {#the-two-handlers}
|
||||
|
||||
O fluxo de authorization code precisa de um humano exatamente uma vez: alguém tem que fazer login e clicar em "permitir".
|
||||
|
||||
* **`redirect_handler`** recebe um await com a URL de autorização já montada. O `client_id`, a `redirect_uri`, o `state` e o desafio PKCE já estão nela. Seu único trabalho é levar um navegador até lá. Um app desktop chama `webbrowser.open`; este arquivo imprime a URL.
|
||||
* **`callback_handler`** recebe o await em seguida. Ele espera até o usuário voltar para a sua `redirect_uri` e retorna os parâmetros de query desse redirecionamento como um `AuthorizationCodeResult`.
|
||||
|
||||
Um cliente real executa um pequeno servidor HTTP local na URI de redirecionamento em vez de chamar `input()`. O formato é idêntico: receber o redirecionamento, devolver `code`, `state` e `iss`.
|
||||
|
||||
!!! warning
|
||||
Repasse `state` e `iss` exatamente como chegaram. O provider compara `state` com o que
|
||||
ele gerou e `iss` com o issuer que descobriu, e recusa qualquer divergência. Eles são as defesas
|
||||
contra CSRF e contra confusão de servidor (server mix-up).
|
||||
|
||||
### Para dentro do `Client` {#into-the-client}
|
||||
|
||||
Veja `main()`. O provider vai no **cliente httpx2**, o cliente httpx2 vai em `streamable_http_client(url, http_client=...)`, e esse transporte vai em `Client`.
|
||||
|
||||
`streamable_http_client` não tem o parâmetro nomeado `auth=`. Tudo que é de nível HTTP (auth, cabeçalhos, timeouts, proxies) pertence ao `httpx2.AsyncClient` que você traz. Essa divisão em camadas está em **[Transportes do cliente](transports.md)**.
|
||||
|
||||
## O que o provider faz por você {#what-the-provider-does-for-you}
|
||||
|
||||
Na primeira vez que `Client` envia uma requisição, o servidor responde `401`. O provider assume:
|
||||
|
||||
1. **Descoberta.** Ele lê o cabeçalho `WWW-Authenticate`, busca os Protected Resource Metadata do servidor em `/.well-known/oauth-protected-resource`, descobre qual servidor de autorização protege este recurso e busca os metadados *desse* servidor.
|
||||
2. **Registro.** Nada no armazenamento? Ele registra você dinamicamente com o seu `OAuthClientMetadata` e armazena o resultado.
|
||||
3. **Autorização.** Ele gera o par PKCE e um `state`, monta a URL de autorização, faz await no seu `redirect_handler` e depois faz await no seu `callback_handler` para obter o code.
|
||||
4. **Troca.** Ele troca o code por um `OAuthToken`, armazena e reenvia a sua requisição original com `Authorization: Bearer ...`.
|
||||
|
||||
Depois disso ele fica quieto. Os tokens saem do armazenamento, um token de acesso expirado é renovado com o refresh token, e só quando nada disso funciona ele executa o fluxo de novo.
|
||||
|
||||
Você não escreveu nada disso. Restam dois argumentos nomeados (`client_metadata_url` e `validate_resource_url`), e este arquivo não precisa de nenhum dos dois. `client_metadata_url` é o que vale a pena conhecer; ele ganha uma seção própria abaixo.
|
||||
|
||||
### Experimente {#try-it}
|
||||
|
||||
A maioria dos exemplos nesta documentação você consegue conferir com um `Client(server)` em memória. Este não: o ponto central do fluxo é um `401` HTTP, e não há HTTP entre um cliente em memória e o seu servidor.
|
||||
|
||||
O repositório traz a versão ao vivo. `examples/servers/simple-auth/` executa um servidor de autorização independente e um servidor MCP protegido; `examples/clients/simple-auth-client/` é o cliente desta página crescido até virar uma pequena CLI. O README dele tem os dois comandos: inicie os servidores, execute o cliente contra eles e veja as quatro etapas passarem.
|
||||
|
||||
## Client ID Metadata Documents {#client-id-metadata-documents}
|
||||
|
||||
A revisão 2026-07-28 da especificação torna obsoleto o registro dinâmico de clientes em favor dos **Client ID Metadata Documents** (CIMD). Em vez de fazer POST de um registro novo em cada servidor de autorização que encontra, o seu cliente publica um único documento JSON sobre si mesmo em uma URL HTTPS estável, e essa URL *é* o `client_id` dele. O servidor de autorização busca o documento; o provider nunca toca nele.
|
||||
|
||||
O SDK já fala isso: passe a URL como `client_metadata_url=` ao construir o provider. Quando os metadados do servidor de autorização anunciam `client_id_metadata_document_supported: true`, o provider pula completamente a requisição a `/register`: a URL entra no fluxo como `client_id`, e não há `client_secret`. Quando o servidor não anuncia isso (a maioria ainda não anuncia), ou você nunca passa uma URL, o provider recorre ao registro dinâmico **silenciosamente**, e tudo acima funciona exatamente como descrito. Um `client_info` armazenado ainda prevalece sobre ambos.
|
||||
|
||||
A URL precisa ser HTTPS com um caminho que não seja a raiz; qualquer outra coisa é um `ValueError` na construção, antes de qualquer tráfego de rede. O `examples/clients/simple-auth-client/` do repositório recebe a URL pela variável de ambiente `MCP_CLIENT_METADATA_URL`.
|
||||
|
||||
## Máquina para máquina {#machine-to-machine}
|
||||
|
||||
Um job noturno, uma etapa de CI, outro serviço. Não há navegador nem ninguém para clicar em "permitir". Esse é o grant **client credentials**: você já possui um `client_id` e um `client_secret`, e o endpoint de token é o fluxo inteiro.
|
||||
|
||||
`ClientCredentialsOAuthProvider` é o mesmo `httpx2.Auth`, sem o humano:
|
||||
|
||||
```python title="client.py" hl_lines="4 27-33"
|
||||
--8<-- "docs_src/oauth_clients/tutorial002.py"
|
||||
```
|
||||
|
||||
O que mudou:
|
||||
|
||||
* Sem `OAuthClientMetadata`, sem handlers. Você passa `client_id` e `client_secret`; o provider monta um registro `client_credentials` mínimo em torno deles e pula o registro dinâmico por completo.
|
||||
* `scope` é uma string separada por espaços, o formato OAuth usado na comunicação.
|
||||
* Tudo a partir daí é idêntico: o mesmo `TokenStorage`, o mesmo `httpx2.AsyncClient(auth=...)`, o mesmo `streamable_http_client`.
|
||||
|
||||
Por padrão, o secret viaja como HTTP Basic auth na requisição de token (`client_secret_basic`). Passe `token_endpoint_auth_method="client_secret_post"` para colocá-lo no corpo do formulário. Alguns servidores de autorização só aceitam um dos dois.
|
||||
|
||||
!!! tip
|
||||
Leia `client_secret` do ambiente ou de um gerenciador de segredos, nunca do controle de versão.
|
||||
|
||||
!!! info
|
||||
Mais um provider mora em `mcp.client.auth.extensions.client_credentials`:
|
||||
**`PrivateKeyJWTOAuthProvider`**, para clientes que se autenticam com um JWT em vez de um
|
||||
segredo compartilhado (`private_key_jwt`, a variante de par de chaves e workload identity). Ele segue
|
||||
o mesmo padrão: construa um, coloque em `auth=`. O mesmo módulo traz
|
||||
`SignedJWTParameters` e `static_assertion_provider`, dois helpers que montam a assertion dele.
|
||||
|
||||
Há mais uma situação sem humano: o cliente pertence a uma empresa cujo provedor de identidade, e não o usuário, decide quais servidores MCP ele pode alcançar. Esse é um grant diferente, com seu próprio modelo de confiança e sua própria página, **[Asserção de identidade](identity-assertion.md)**.
|
||||
|
||||
## Quando falha {#when-it-fails}
|
||||
|
||||
Quando o fluxo OAuth dá errado, o provider levanta um `OAuthFlowError` de `mcp.client.auth`. Ele tem duas subclasses. `OAuthRegistrationError` significa que o registro não rendeu um cliente que você possa usar: o servidor de autorização se recusou a registrar você, ou até registrou, mas com credenciais que este fluxo não consegue usar (por exemplo, um método de autenticação que ele não implementa). `OAuthTokenError` significa que não foi possível obter um token: o endpoint de token disse não, ou um registro de cliente armazenado carrega um método de autenticação que este cliente não consegue aplicar, o que é reportado durante a montagem da requisição de token em vez de ser enviado. Um único `except OAuthFlowError:` cobre descoberta, registro, autorização e troca.
|
||||
|
||||
Nem tudo é erro de fluxo. A rede ainda pode falhar; essas são exceções comuns do `httpx2` e passam intactas.
|
||||
|
||||
## Recapitulando {#recap}
|
||||
|
||||
* `OAuthClientProvider` é um `httpx2.Auth`. Coloque-o em um `httpx2.AsyncClient`, passe esse cliente para `streamable_http_client(url, http_client=...)`, e `Client` nunca fica sabendo que houve OAuth.
|
||||
* Você fornece quatro coisas: a URL do servidor, um `OAuthClientMetadata`, um `TokenStorage` e o par de handlers redirect/callback.
|
||||
* `TokenStorage` é um `Protocol`: quatro métodos async, sem classe base. Persista `client_info` além dos tokens.
|
||||
* Descoberta, registro (dinâmico ou via um **Client ID Metadata Document**), PKCE, as verificações de `state` e `iss` e a renovação de tokens são trabalho do provider, não seu.
|
||||
* `ClientCredentialsOAuthProvider` é a versão sem humano: `client_id` + `client_secret`, sem handlers, sem navegador.
|
||||
* Toda falha OAuth é um `OAuthFlowError`; `OAuthRegistrationError` e `OAuthTokenError` são suas subclasses.
|
||||
|
||||
A outra metade desse handshake, fazer o seu *servidor* exigir o token, está em **[Autorização](../run/authorization.md)**.
|
||||
87
i18n/pt/pages/client/session-groups.md
Normal file
87
i18n/pt/pages/client/session-groups.md
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
---
|
||||
translation:
|
||||
sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, d18adbdbb835ea73]
|
||||
tool: 1
|
||||
---
|
||||
# Grupos de sessões {#session-groups}
|
||||
|
||||
Um `Client` se conecta a um servidor. Aplicações reais frequentemente querem vários (um servidor de busca, um servidor de banco de dados, uma API interna) e acabam fazendo malabarismo com uma conexão e uma lista de ferramentas (tools) para cada um.
|
||||
|
||||
**`ClientSessionGroup`** é um único objeto que mantém várias conexões e reúne tudo o que elas expõem em uma única visão.
|
||||
|
||||
## Dois servidores {#two-servers}
|
||||
|
||||
Comece com dois servidores comuns. Eles não têm nada a ver um com o outro, então ambos naturalmente chamaram sua ferramenta de `search`:
|
||||
|
||||
```python title="library_server.py" hl_lines="7"
|
||||
--8<-- "docs_src/session_groups/tutorial001.py"
|
||||
```
|
||||
|
||||
```python title="web_server.py" hl_lines="7"
|
||||
--8<-- "docs_src/session_groups/tutorial002.py"
|
||||
```
|
||||
|
||||
## Um grupo {#one-group}
|
||||
|
||||
Crie um `ClientSessionGroup` e chame **`connect_to_server`** uma vez por servidor:
|
||||
|
||||
```python title="client.py" hl_lines="10-12"
|
||||
--8<-- "docs_src/session_groups/tutorial003.py"
|
||||
```
|
||||
|
||||
* `connect_to_server` recebe parâmetros de transporte, não um objeto de servidor: `StdioServerParameters` (de `mcp`) para iniciar um subprocesso, ou `StreamableHttpParameters` / `SseServerParameters` (de `mcp.client.session_group`) para um servidor que já está escutando em uma URL.
|
||||
* `group.tools` é um `dict[str, Tool]` com as ferramentas de todos os servidores conectados. `group.resources` e `group.prompts` têm o mesmo formato.
|
||||
* `group.call_tool(name, arguments)` procura o nome, encontra a sessão dona dele e encaminha a chamada. Você nunca diz qual servidor.
|
||||
|
||||
!!! check
|
||||
Coloque `client.py` ao lado dos dois servidores e execute. O segundo `connect_to_server` recusa:
|
||||
|
||||
```text
|
||||
mcp.shared.exceptions.MCPError: {'search'} already exist in group tools.
|
||||
```
|
||||
|
||||
Isso é um `MCPError`, lançado antes que qualquer coisa do segundo servidor seja registrada. Um nome precisa
|
||||
ser único no grupo **inteiro**, e dois servidores que você não controla vão colidir mais cedo ou mais tarde.
|
||||
|
||||
## `component_name_hook` {#component_name_hook}
|
||||
|
||||
Você resolve isso no grupo, não nos servidores. Passe uma função de `(name, server_info)` e o grupo a executa em cada nome que registra:
|
||||
|
||||
```python title="client.py" hl_lines="7-8 15"
|
||||
--8<-- "docs_src/session_groups/tutorial004.py"
|
||||
```
|
||||
|
||||
Execute de novo. `print(sorted(group.tools))` agora mostra as duas:
|
||||
|
||||
```text
|
||||
['Library.search', 'Web.search']
|
||||
```
|
||||
|
||||
* A **chave** é sua. `by_server` a montou a partir de `server_info.name`, o nome com que cada `MCPServer(...)` foi construído.
|
||||
* O `Tool` dentro fica intacto: `group.tools["Web.search"].name` ainda é `"search"`, e esse é o nome que `call_tool` coloca na rede. O prefixo nunca sai do seu processo.
|
||||
* Não são só ferramentas. O recurso `hours` da biblioteca é registrado como `Library.hours`.
|
||||
|
||||
!!! tip
|
||||
O hook é executado em **cada** nome de **cada** servidor, não só nos conflitos: não existe um
|
||||
modo de prefixar apenas em caso de colisão. Escolha um esquema e deixe que ele valha em todo lugar.
|
||||
|
||||
## Adicionando e removendo servidores {#adding-and-removing-servers}
|
||||
|
||||
`connect_to_server` retorna a `ClientSession` que abriu. Guarde-a se algum dia quiser tirar aquele servidor: `await group.disconnect_from_server(session)` remove do grupo as ferramentas, recursos e prompts dele.
|
||||
|
||||
Se você já tem em mãos uma `ClientSession` conectada (`Client.session` é uma), entregue-a a `await group.connect_with_session(server_info, session)` em vez de abrir um novo transporte. Ela é agregada da mesma forma. O grupo nunca fecha uma sessão que não abriu. `server_info` nomeia o servidor para os prefixos dos componentes; em uma conexão da era 2026, `client.server_info` pode ser `None` (a identidade é opcional), então nesse caso passe sua própria `Implementation(name=..., version=...)`.
|
||||
|
||||
## O handshake clássico {#the-classic-handshake}
|
||||
|
||||
`ClientSessionGroup` é construído sobre `ClientSession`, não sobre `Client`. Cada `connect_to_server` executa o handshake clássico `initialize`. Ele nunca envia a sondagem `server/discover` descrita em **[Versões do protocolo](../protocol-versions.md)**. Todo servidor MCP entende esse handshake, então isso não custa compatibilidade com nada; significa apenas que um grupo segue o caminho mais antigo e mais lento até um servidor que poderia fazer melhor.
|
||||
|
||||
## Recapitulando {#recap}
|
||||
|
||||
* `ClientSessionGroup` mantém várias conexões de servidor e reúne as ferramentas, recursos e prompts delas em um `dict` para cada tipo.
|
||||
* `connect_to_server(params)` por servidor. Ele recebe parâmetros de transporte, nunca o objeto de servidor ou a URL que um `Client` recebe.
|
||||
* `group.call_tool(name, arguments)` roteia para o servidor dono por você.
|
||||
* Os nomes precisam ser únicos no grupo inteiro; dois servidores com uma ferramenta `search` não conseguem coexistir por conta própria.
|
||||
* `component_name_hook=` reescreve cada nome registrado. A chave do dict muda, o nome na rede não.
|
||||
* `connect_with_session` adiciona uma sessão que você já tem; `disconnect_from_server` remove uma.
|
||||
|
||||
O handshake que um grupo fala (e o mais rápido que um `Client` prefere) é o assunto de **[Versões do protocolo](../protocol-versions.md)**.
|
||||
91
i18n/pt/pages/client/subscriptions.md
Normal file
91
i18n/pt/pages/client/subscriptions.md
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
---
|
||||
translation:
|
||||
sections: [8f9558e57f29eee1, a88c587739e0465c, 46ebfd5b325ed041, 4d10b00b57ce4bd9, 2cdb0edd1f59b3e2]
|
||||
tool: 1
|
||||
---
|
||||
# Assinaturas {#subscriptions}
|
||||
|
||||
O catálogo de um servidor não é fixo. Ferramentas (tools) aparecem em tempo de execução, e o conteúdo por trás da URI de um recurso muda. Um cliente fica sabendo disso por meio de `client.listen(...)`: uma única requisição `subscriptions/listen` cuja resposta *é* o stream. Ele fica aberto e carrega as notificações de mudança que o cliente pediu.
|
||||
|
||||
Esta página é a ponta do cliente: abrir o stream, observá-lo ao lado do seu fluxo principal e lidar com seus encerramentos. Publicar mudanças, filtrar e servir o método são o lado do servidor dessa história, contado em **[Assinaturas](../handlers/subscriptions.md)**, em *Dentro do seu handler*. Os exemplos aqui conversam com o servidor de quadro de sprint construído lá.
|
||||
|
||||
## Observando o stream {#watching-the-stream}
|
||||
|
||||
Uma assinatura é um único gerenciador de contexto. Entrar nele envia a requisição, com seus argumentos nomeados como filtro da assinatura, e espera a confirmação do servidor, então o stream já está ativo quando o bloco começa.
|
||||
|
||||
```python title="client.py" hl_lines="15 18 28"
|
||||
--8<-- "docs_src/subscriptions/tutorial003.py"
|
||||
```
|
||||
|
||||
A iteração produz quatro eventos tipados: `ToolsListChanged`, `PromptsListChanged`, `ResourcesListChanged` e `ResourceUpdated(uri=...)`.
|
||||
|
||||
Um evento diz *o que* mudou, nunca *como*. É por isso que `follow_board` chama `read_resource` e `list_tools`: o evento é uma deixa para buscar de novo. Leia `event.uri` em vez de presumir qual recurso mudou: um filtro pode nomear várias URIs, e um servidor pode reportar uma mudança em um sub-recurso de uma delas.
|
||||
|
||||
Eventos duplicados esperando para serem consumidos se fundem em um só, e buscar de novo ainda traz o estado atual para você. Só eventos idênticos se fundem: dois `ResourceUpdated` para URIs diferentes são dois eventos.
|
||||
|
||||
Mais duas propriedades do handle:
|
||||
|
||||
* `sub.honored` é o filtro que o servidor confirmou: um `SubscriptionFilter` com os campos que você passou, lidos como atributos (`sub.honored.prompts_list_changed`). O `MCPServer` honra todo tipo que você pede, então ele devolve sua requisição como eco. Um servidor que suporta menos tipos confirma menos, e um tipo honrado ainda pode nunca disparar. Um servidor também pode recusar a requisição inteira em vez de confirmá-la (veja [Decidindo quem pode observar](../handlers/subscriptions.md#deciding-who-may-watch) na página do servidor), o que aparece como o erro da requisição.
|
||||
* `sub.subscription_id` é o id da requisição listen, aquele carimbado em cada frame deste stream. Várias assinaturas podem estar abertas ao mesmo tempo, cada uma demultiplexada pelo seu próprio id.
|
||||
|
||||
## Observando sem bloquear {#watching-without-blocking}
|
||||
|
||||
`follow_board` roda até o servidor fechar o stream, o que pode ser nunca, então sozinha ela toma conta do seu programa. Clientes reais querem o observador *ao lado* do fluxo principal: um agente chama ferramentas enquanto um observador mantém um cache ou uma UI atualizados.
|
||||
|
||||
Abra a assinatura primeiro, depois inicie o observador e siga com o seu trabalho.
|
||||
|
||||
=== "asyncio"
|
||||
|
||||
```python title="app.py" hl_lines="18 20"
|
||||
--8<-- "docs_src/subscriptions/tutorial004_asyncio.py"
|
||||
```
|
||||
|
||||
=== "trio"
|
||||
|
||||
```python title="app.py" hl_lines="18 21"
|
||||
--8<-- "docs_src/subscriptions/tutorial004_trio.py"
|
||||
```
|
||||
|
||||
=== "anyio"
|
||||
|
||||
```python title="app.py" hl_lines="18 21"
|
||||
--8<-- "docs_src/subscriptions/tutorial004_anyio.py"
|
||||
```
|
||||
|
||||
!!! note
|
||||
`app.py` importa `BOARD` e `read_board` do primeiro exemplo, que este repositório guarda como
|
||||
`tutorial003.py`. Se você salvar os arquivos renderizados lado a lado como `client.py` e `app.py`,
|
||||
escreva `from client import BOARD, read_board` no lugar. O exemplo `watch.py` mais abaixo
|
||||
importa `read_board` do mesmo jeito.
|
||||
|
||||
A ordem é o ponto. Nada é reenviado, então um evento publicado antes de o seu stream existir se perde. Entrar em `client.listen(...)` espera a confirmação, então toda mudança daquele momento em diante chega ao seu observador, e o snapshot que você tira dentro do bloco não tem como perder nenhuma.
|
||||
|
||||
Requisições rodam livremente ao lado de um stream aberto, a partir da tarefa do observador ou de qualquer outra, no mesmo cliente. Como eventos *duplicados* não consumidos se fundem, um fluxo principal movimentado pode produzir uma nova busca em vez de três. Eventos diferentes não se fundem: um filtro que nomeia muitas URIs enfileira um evento pendente por URI.
|
||||
|
||||
Para parar de observar, saia do bloco: não existe chamada `unsubscribe`. Cancelar a tarefa que é dona do bloco faz isso por você, e o SDK cancela a requisição listen do jeito que o transporte espera: sobre Streamable HTTP, fechando o stream daquela requisição. Um observador que roda durante toda a vida do seu app nunca retorna sozinho, então cancele-o, ou o escopo do seu task group, no encerramento.
|
||||
|
||||
## Streams terminam {#streams-end}
|
||||
|
||||
Um stream termina de uma de duas maneiras, ambas fluxo de controle comum. Um fechamento gracioso do servidor encerra o `async for`; uma queda abrupta levanta `SubscriptionLost`.
|
||||
|
||||
A diferença é de diagnóstico, não uma diferença no que fazer a seguir: o stream se foi, nada foi reenviado, e um observador que ainda se importa escuta de novo e busca de novo.
|
||||
|
||||
```python title="watch.py" hl_lines="16 20"
|
||||
--8<-- "docs_src/subscriptions/tutorial005.py"
|
||||
```
|
||||
|
||||
Servidores fecham streams graciosamente por razões próprias, inclusive para se livrar de um assinante cujo backlog cresceu demais, então um fim limpo não é sinal para parar de observar. Espere um pouco (back off) antes de escutar de novo.
|
||||
|
||||
`SubscriptionLost` também tem uma causa local. O cliente guarda no máximo 1024 eventos não consumidos, e um consumidor que fica tão para trás assim perde a assinatura em vez de crescer sem limite. Mantenha o corpo do `async for` curto e faça o trabalho lento em outro lugar.
|
||||
|
||||
`keep_following` captura apenas `SubscriptionLost`. Entrar em `listen()` também pode levantar `MCPError` (a conexão falhou, ou o servidor não serve o método), `TimeoutError` (nenhuma confirmação chegou) e `ListenNotSupportedError` (uma conexão pré-2026). Decida quais desses o seu observador deve tentar de novo: o último nunca se resolve.
|
||||
|
||||
## Recapitulando {#recap}
|
||||
|
||||
* Entre em `async with client.listen(...)`; a entrada espera a confirmação, então nada publicado depois dela se perde.
|
||||
* Itere com `async for event in sub`. Eventos são deixas para buscar de novo, nunca payloads.
|
||||
* Abra a assinatura, depois rode o observador como uma tarefa, e as chamadas de ferramentas continuam fluindo ao lado dele.
|
||||
* Um fim limpo para o loop; uma queda levanta `SubscriptionLost`. De qualquer forma: escute de novo, busque de novo, espere um pouco antes.
|
||||
* Sair do bloco é o unsubscribe.
|
||||
|
||||
Publicar esses eventos, estreitar o filtro e escalar além de um processo são a história do servidor: **[Assinaturas](../handlers/subscriptions.md)**. Esses mesmos eventos também mantêm um cache do lado do cliente honesto, e **[Cache](caching.md)** é a próxima página.
|
||||
128
i18n/pt/pages/client/transports.md
Normal file
128
i18n/pt/pages/client/transports.md
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
---
|
||||
translation:
|
||||
sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, ff7401df479af877, 3d0832f39b0d7059, d4bf7e4479637768, 05e20c0a798860e7]
|
||||
tool: 1
|
||||
---
|
||||
# Transportes do cliente {#client-transports}
|
||||
|
||||
Todo `Client` conversa com seu servidor por meio de um **transporte**: aquilo que de fato carrega as mensagens.
|
||||
|
||||
Você nunca configura um transporte separadamente. `Client` recebe um único argumento posicional e deduz o transporte a partir do tipo dele.
|
||||
|
||||
O lado do *servidor* de cada um (o que `mcp.run()` faz e o que você coloca no deploy) está em **[Executando seu servidor](../run/index.md)**.
|
||||
|
||||
## Em memória {#in-memory}
|
||||
|
||||
Passe o próprio objeto do servidor:
|
||||
|
||||
```python title="client.py" hl_lines="14"
|
||||
--8<-- "docs_src/client_transports/tutorial001.py"
|
||||
```
|
||||
|
||||
Sem subprocesso, sem porta, sem bytes trafegando na rede. O cliente e o servidor são dois objetos no mesmo processo, e a chamada ainda passa pela camada real do protocolo: `search_books` é listada, validada e invocada exatamente como seria sobre HTTP.
|
||||
|
||||
Isso faz dele duas coisas ao mesmo tempo:
|
||||
|
||||
* **Uma estrutura de testes.** Todo exemplo desta documentação é exercitado dessa forma, e a página **[Testes](../get-started/testing.md)** constrói o padrão inteiro em torno disso.
|
||||
* **Uma API de embutimento.** Uma aplicação que constrói o servidor não precisa de um salto pela rede para chamar as ferramentas dele.
|
||||
|
||||
## Streamable HTTP {#streamable-http}
|
||||
|
||||
Passe uma string de URL e você tem **Streamable HTTP**, o transporte atrás do qual você faz o deploy:
|
||||
|
||||
```python title="client.py" hl_lines="5"
|
||||
--8<-- "docs_src/client_transports/tutorial002.py"
|
||||
```
|
||||
|
||||
Esse é o cliente de produção inteiro. `Client` envolve a URL em `streamable_http_client(...)` para você, sobre um `httpx2.AsyncClient` configurado do jeito que o MCP precisa: `follow_redirects=True`, um timeout de 30 segundos para connect/write/pool e um timeout de leitura de 300 segundos, porque o servidor pode manter um stream de resposta aberto.
|
||||
|
||||
!!! check
|
||||
Um `Client` que você construiu **não** está conectado. A construção só escolhe o transporte;
|
||||
é o `async with` que o abre. Tente usar a conexão antes de entrar e o SDK avisa:
|
||||
|
||||
```text
|
||||
RuntimeError: Client must be used within an async context manager
|
||||
```
|
||||
|
||||
Nada foi resolvido, buscado ou iniciado quando você escreveu `Client("http://...")`. Essa linha não custa nada.
|
||||
|
||||
### Traga seu próprio `httpx2.AsyncClient` {#bring-your-own-httpx2asyncclient}
|
||||
|
||||
No momento em que você precisar de um header `Authorization`, um cookie, um proxy, mTLS ou um timeout diferente, construa o `httpx2.AsyncClient` você mesmo e entregue-o a `streamable_http_client`:
|
||||
|
||||
```python title="client.py" hl_lines="8-14"
|
||||
--8<-- "docs_src/client_transports/tutorial003.py"
|
||||
```
|
||||
|
||||
Duas coisas para notar:
|
||||
|
||||
* Você é o dono do `httpx2.AsyncClient`, então é **você** quem entra e sai dele. O SDK nunca fecha um cliente que não criou.
|
||||
* `streamable_http_client(url, http_client=...)` retorna um transporte, e `Client(transport)` o aceita como qualquer outra coisa.
|
||||
|
||||
Uma observação sobre TLS: `httpx2` verifica certificados contra o repositório de confiança do sistema operacional (via
|
||||
[`truststore`](https://pypi.org/project/truststore/)), não contra uma lista de CAs embutida. Em um ambiente
|
||||
sem um repositório de CAs do sistema utilizável (alguns contêineres mínimos), defina as variáveis de ambiente padrão
|
||||
`SSL_CERT_FILE`/`SSL_CERT_DIR` ou passe um `verify=ssl_context` explícito ao seu `httpx2.AsyncClient`
|
||||
(contexto em
|
||||
[`httpx` e `httpx-sse` substituídos por `httpx2`](../migration.md#httpx-and-httpx-sse-replaced-by-httpx2)).
|
||||
|
||||
!!! warning
|
||||
`streamable_http_client` costumava aceitar `headers=` e `timeout=` diretamente. Não aceita mais:
|
||||
seus únicos parâmetros são `url`, `http_client` e `terminate_on_close`. Use `headers=` por
|
||||
hábito e você recebe:
|
||||
|
||||
```text
|
||||
TypeError: streamable_http_client() got an unexpected keyword argument 'headers'
|
||||
```
|
||||
|
||||
Tudo que tem cara de HTTP agora vive no único `httpx2.AsyncClient` que você passa.
|
||||
|
||||
!!! info
|
||||
`httpx2` mantém a API conhecida do `httpx`, então se você conhece `httpx` já sabe como fazer auth,
|
||||
proxies, event hooks, retentativas e limites de conexão aqui. O SDK não acrescenta nada por cima nem
|
||||
tira nada. É também onde o OAuth se encaixa:
|
||||
`httpx2.AsyncClient(auth=OAuthClientProvider(...))`. Esse fluxo inteiro está em **[Clientes OAuth](oauth-clients.md)**.
|
||||
|
||||
## stdio {#stdio}
|
||||
|
||||
Um servidor **stdio** é um subprocesso. O cliente o inicia, escreve JSON-RPC no stdin dele e lê JSON-RPC do stdout dele. É assim que um host de desktop executa um servidor na sua máquina: um host *é* este código mais uma interface, e **[Conecte a um host real](../get-started/real-host.md)** é a mesma relação vista do lado do host, como um arquivo de configuração.
|
||||
|
||||
Descreva o processo com `StdioServerParameters`, transforme-o em um transporte com `stdio_client` e entregue *isso* ao `Client`:
|
||||
|
||||
```python title="client.py" hl_lines="4-8 12"
|
||||
--8<-- "docs_src/client_transports/tutorial004.py"
|
||||
```
|
||||
|
||||
`Client` não aceita o objeto de parâmetros sozinho. `StdioServerParameters` é configuração; `stdio_client(server)` é o transporte que sabe como iniciar um processo a partir dela. Sempre envolva.
|
||||
|
||||
Sair do bloco `async with` também encerra o subprocesso: fecha o stdin, espera e mata o processo se ele demorar. Você nunca limpa isso por conta própria.
|
||||
|
||||
!!! warning
|
||||
O processo filho **não** herda o seu ambiente. Ele recebe uma allow-list mínima (`HOME`, `LOGNAME`,
|
||||
`PATH`, `SHELL`, `TERM` e `USER` no POSIX), para que nada sensível vaze para um processo que talvez
|
||||
não tenha sido escrito por você.
|
||||
|
||||
Um servidor que precise de uma chave de API não vai encontrá-la ali. Passe-a explicitamente com `env=`; essas
|
||||
variáveis são mescladas por cima da allow-list. É isso que `BOOKSHOP_API_KEY` está fazendo acima.
|
||||
|
||||
## SSE {#sse}
|
||||
|
||||
`sse_client(url)`, de `mcp.client.sse`, é o transporte HTTP que o Streamable HTTP substituiu. Envolva-o da mesma forma, `Client(sse_client("http://localhost:8000/sse"))`, para conversar com um servidor que ainda o fala, e não construa nada novo em cima dele.
|
||||
|
||||
## O protocolo `Transport` {#the-transport-protocol}
|
||||
|
||||
Para o `Client`, tudo acima é a mesma coisa.
|
||||
|
||||
Um **transporte** é qualquer gerenciador de contexto assíncrono que produz um par `(read, write)` de streams de mensagens: formalmente, o protocolo `Transport` em `mcp.client`. `Client` resolve seu argumento pelo tipo: um objeto de servidor conecta no próprio processo, uma `str` vira `streamable_http_client(url)` e qualquer outra coisa é aberta diretamente como transporte. É por causa dessa última regra que `stdio_client(...)`, `streamable_http_client(...)` e `sse_client(...)` se encaixam todos no mesmo lugar, e que você pode escrever o seu próprio.
|
||||
|
||||
## Recapitulando {#recap}
|
||||
|
||||
* `Client(mcp)` (o objeto do servidor) conecta em memória. Use para testes e para embutir.
|
||||
* `Client("http://.../mcp")` (uma URL) conecta por Streamable HTTP, o transporte de produção.
|
||||
* Headers, auth, proxies e timeouts pertencem a um `httpx2.AsyncClient` que você passa a `streamable_http_client(url, http_client=...)`. Não existe o argumento `headers=`.
|
||||
* stdio é `Client(stdio_client(StdioServerParameters(...)))`, nunca o objeto de parâmetros sozinho.
|
||||
* O subprocesso recebe um ambiente em allow-list, não o seu; `env=` acrescenta a ele.
|
||||
* Um transporte é qualquer coisa com que você possa fazer `async with x as (read, write)`. `Client` entrega direto a esse protocolo tudo que não for um objeto de servidor ou uma URL.
|
||||
* Construir um `Client` escolhe o transporte. `async with` o abre.
|
||||
|
||||
Depois que o transporte está aberto, os dois lados precisam concordar sobre uma versão do protocolo. Normalmente você nunca pensa nisso; quando pensar, **[Versões do protocolo](../protocol-versions.md)** é a página.
|
||||
96
i18n/pt/pages/deprecated.md
Normal file
96
i18n/pt/pages/deprecated.md
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
---
|
||||
translation:
|
||||
sections: [20541a40dbdd5980, 01262a123ad9501d, 429db5b574a2ac08, 56b2d49da412cb28, 6a1717123fe4513c]
|
||||
tool: 1
|
||||
---
|
||||
# Funcionalidades descontinuadas {#deprecated-features}
|
||||
|
||||
A especificação 2026-07-28 aposenta cinco coisas. O SDK ainda implementa cada uma delas, e cada uma agora carrega um **aviso de descontinuação**.
|
||||
|
||||
A tabela abaixo nomeia cada funcionalidade descontinuada, o motivo de ela estar saindo e o substituto sobre o qual construir.
|
||||
|
||||
## O que está descontinuado {#what-is-deprecated}
|
||||
|
||||
| Descontinuado | Por quê | O que fazer no lugar |
|
||||
|---|---|---|
|
||||
| **Roots**: `ctx.session.list_roots()`, `client.send_roots_list_changed()`, o `list_roots_callback=` que você passa para `Client(...)` | A [SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577) aposenta a capacidade. | Receba os caminhos como argumentos comuns de ferramenta ou URIs de recurso, ou embuta um `ListRootsRequest` em um `InputRequiredResult` (veja **[Requisições de múltiplas idas e voltas](handlers/multi-round-trip.md)**). |
|
||||
| **Amostragem (sampling) iniciada pelo servidor**: `ctx.session.create_message()`, o `sampling_callback=` que você passa para `Client(...)` | A SEP-2577 aposenta a capacidade. | Retorne `InputRequiredResult` e deixe o cliente repetir a chamada (veja **[Requisições de múltiplas idas e voltas](handlers/multi-round-trip.md)**). |
|
||||
| **Logging de protocolo**: `ctx.log()`, `ctx.debug()`, `ctx.info()`, `ctx.warning()`, `ctx.error()`, `ctx.session.send_log_message()`, `client.set_logging_level()` | A SEP-2577 aposenta a capacidade. Nada dentro do protocolo a substitui. | O `import logging` comum para stderr (veja **[Logging](handlers/logging.md)**). |
|
||||
| **`ping`**: `client.send_ping()` | **Removido** do protocolo, não apenas descontinuado. Não existe método `ping` em 2026-07-28. | Nada. Só funciona em uma conexão `mode="legacy"`. |
|
||||
| **Progresso cliente->servidor**: `client.send_progress_notification()` | A 2026-07-28 torna o progresso exclusivamente servidor->cliente. | Nada a enviar. O seu *servidor* informa progresso com `ctx.report_progress()` (veja **[Progresso](handlers/progress.md)**). |
|
||||
|
||||
Três coisas saem dessa tabela:
|
||||
|
||||
* Roots, amostragem e logging andam juntos. Uma única proposta, a **SEP-2577**, descontinua as três capacidades de uma vez.
|
||||
* Amostragem e roots compartilham um problema mais profundo: são pontos em que um **servidor** envia uma **requisição** ao **cliente**. Essa direção inteira é o que a 2026-07-28 substitui por **[Requisições de múltiplas idas e voltas](handlers/multi-round-trip.md)**. O que desaparece são os métodos RPC independentes (`sampling/createMessage`, `roots/list` e o `elicitation/create` no estilo push); os tipos de payload `CreateMessageRequest` / `ListRootsRequest` / `ElicitRequest` sobrevivem, embutidos em `InputRequiredResult.input_requests`, e no cliente chegam aos mesmos callbacks.
|
||||
* `ping` é o diferente do grupo. O protocolo não o descontinua, ele o remove. O método do SDK ainda emite o aviso (a mensagem diz *removed*, não *deprecated*) e chamá-lo em uma conexão moderna responde com *"Method not found"*.
|
||||
|
||||
## Descontinuado é consultivo {#deprecated-is-advisory}
|
||||
|
||||
Nada quebra hoje.
|
||||
|
||||
Cada método acima continua funcionando em qualquer sessão que tenha negociado **2025-11-25 ou anterior**. Fixe `mode="legacy"` no cliente e você obtém exatamente o comportamento pré-2026. Não há mudanças no protocolo de transmissão e a negociação de capacidades segue igual.
|
||||
|
||||
O que muda é que você recebe um aviso visível na primeira vez que cada um é executado:
|
||||
|
||||
```text
|
||||
MCPDeprecationWarning: The logging capability is deprecated as of 2026-07-28 (SEP-2577).
|
||||
```
|
||||
|
||||
`MCPDeprecationWarning` é subclasse de `UserWarning`, **não** de `DeprecationWarning`. Isso é proposital: o filtro padrão do Python só mostra `DeprecationWarning` em código executado diretamente como `__main__`, e é assim que bibliotecas descontinuam coisas sem ninguém perceber por dois anos. Este aparece em todo lugar, sem nenhuma flag `-W`.
|
||||
|
||||
!!! warning
|
||||
"Consultivo" termina no nível do protocolo de transmissão. Amostragem e roots são
|
||||
*requisições* do servidor para o cliente, e uma sessão 2026-07-28 não tem canal para
|
||||
carregar uma. Chame `ctx.session.create_message()` dentro de uma ferramenta em uma
|
||||
conexão moderna e o aviso ainda dispara, e então o envio falha com um erro:
|
||||
|
||||
```text
|
||||
Cannot send 'sampling/createMessage': this transport context has no back-channel
|
||||
for server-initiated requests.
|
||||
```
|
||||
|
||||
Dois sinais, nessa ordem. O `MCPDeprecationWarning` dispara no momento em que você
|
||||
chama o método, em qualquer conexão. O erro é o que volta quando o SDK tenta enviar
|
||||
em seguida. Esses dois só funcionam de ponta a ponta em uma conexão `mode="legacy"`
|
||||
cujo cliente registrou o callback correspondente.
|
||||
|
||||
## Silenciando o aviso {#silencing-the-warning}
|
||||
|
||||
Não faça isso, em código novo.
|
||||
|
||||
Mas um servidor que você mantém e que de fato atende clientes pré-2026 tem todo o direito a um log silencioso. Filtre a categoria antes que a primeira chamada descontinuada seja executada:
|
||||
|
||||
```python
|
||||
import warnings
|
||||
|
||||
from mcp import MCPDeprecationWarning
|
||||
|
||||
warnings.filterwarnings("ignore", category=MCPDeprecationWarning)
|
||||
```
|
||||
|
||||
A API inteira é essa. Não há uma chave por método, e você não quer uma: o sentido de ter uma única categoria é que uma linha a silencia e uma linha a traz de volta.
|
||||
|
||||
!!! check
|
||||
Aplique o filtro no sentido contrário e você ganha um teste de regressão de graça.
|
||||
Adicione `"error::mcp.MCPDeprecationWarning"` à configuração `filterwarnings` do seu
|
||||
pytest e a chamada descontinuada **lança uma exceção** em vez de avisar. Uma ferramenta
|
||||
chamada `old_log` que ainda chama `ctx.info()` para de passar e começa a reportar:
|
||||
|
||||
```text
|
||||
Error executing tool old_log: The logging capability is deprecated as of 2026-07-28 (SEP-2577).
|
||||
```
|
||||
|
||||
Uma linha de configuração do pytest, e uma chamada descontinuada nunca mais consegue
|
||||
voltar sorrateiramente ao seu código sem quebrar um teste.
|
||||
|
||||
## Recapitulando {#recap}
|
||||
|
||||
* A especificação 2026-07-28 descontinua **roots**, a **amostragem** iniciada pelo servidor e o **logging** de protocolo (todos pela [SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577)), restringe o **progresso** ao sentido servidor para cliente e remove o **`ping`**.
|
||||
* A coluna de substitutos indica o próximo passo: **[Requisições de múltiplas idas e voltas](handlers/multi-round-trip.md)** para amostragem e roots, **[Logging](handlers/logging.md)** para logging, **[Progresso](handlers/progress.md)** para progresso. `ping` não precisa de nada.
|
||||
* Descontinuado é consultivo: sem mudanças no protocolo de transmissão, tudo continua funcionando em sessões pré-2026, e você recebe um `MCPDeprecationWarning` visível (um `UserWarning`, então está ligado por padrão).
|
||||
* Amostragem e roots precisam, além disso, de um canal de retorno (back-channel) que uma sessão 2026-07-28 não tem. Em uma conexão moderna elas avisam e depois lançam uma exceção.
|
||||
* `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` silencia a categoria inteira; `"error::mcp.MCPDeprecationWarning"` no pytest a transforma em falha de teste.
|
||||
* Código novo não deve ser construído sobre nenhuma delas.
|
||||
|
||||
Todas as outras páginas desta documentação ensinam a API atual.
|
||||
144
i18n/pt/pages/get-started/first-steps.md
Normal file
144
i18n/pt/pages/get-started/first-steps.md
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
---
|
||||
translation:
|
||||
sections: [0d6c05bcbf836bf3, 59a7b14eeefc68c1, 7114d8d6daba203f, e8bbb56a98ba7bc9, 5138010f6159901c, f78da7c7c363d4c6, 220a939cab348686]
|
||||
tool: 1
|
||||
---
|
||||
# Primeiros passos {#first-steps}
|
||||
|
||||
A **[página inicial](../index.md)** anda rápido: escrever um servidor, executá-lo, chamar uma ferramenta.
|
||||
|
||||
Esta página vai com calma, passando pelas três coisas que um servidor pode expor e dando nome a tudo pelo caminho.
|
||||
|
||||
## Host, cliente e servidor {#host-client-and-server}
|
||||
|
||||
Três palavras que você vai ver em todas as páginas daqui em diante:
|
||||
|
||||
* Um **host** é a aplicação de LLM: o Claude, uma IDE, um runtime de agentes. É com ele que o usuário conversa.
|
||||
* Um **cliente** vive dentro do host e fala MCP. O host executa um cliente para cada servidor ao qual está conectado.
|
||||
* Um **servidor** é o que você constrói com este SDK. Ele expõe coisas aos clientes. Nunca fala diretamente com o modelo.
|
||||
|
||||
Você escreve o servidor. Os hosts são produto de terceiros. O SDK também traz um `Client`. Você vai usá-lo para testar seus servidores, e ele aparece mais adiante nesta página.
|
||||
|
||||
## As três primitivas {#the-three-primitives}
|
||||
|
||||
Um servidor expõe exatamente três tipos de coisa. O que as distingue é **quem decide usá-las**:
|
||||
|
||||
| Primitiva | Quem controla | O que é | Exemplo |
|
||||
|-----------------|-----------------|-----------------------------------------------------------------|----------------------------------------------------|
|
||||
| **Ferramentas** | O modelo | Uma função que o modelo chama para executar uma ação | Uma chamada de API, uma escrita no banco de dados |
|
||||
| **Recursos** | A aplicação | Dados que o host carrega no contexto do modelo | O conteúdo de um arquivo, uma resposta de API |
|
||||
| **Prompts** | O usuário | Um template de mensagem reutilizável que o usuário invoca pelo nome | Um comando de barra, um item de menu |
|
||||
|
||||
"Quem controla" é justamente o sentido da divisão. Uma ferramenta roda porque o **modelo** decidiu chamá-la. Um recurso é anexado porque a **aplicação** decidiu que o modelo precisava dele. Um prompt roda porque o **usuário** o escolheu.
|
||||
|
||||
!!! info
|
||||
Se você já construiu uma API web, já tem quase toda a intuição: um **recurso** é um `GET`
|
||||
(carrega dados e não altera nada) e uma **ferramenta** é um `POST` (realiza trabalho e pode ter
|
||||
efeitos colaterais). Um **prompt** não tem equivalente em HTTP; está mais para uma consulta salva
|
||||
que o usuário executa pelo nome.
|
||||
|
||||
## Um servidor, as três {#one-server-all-three}
|
||||
|
||||
```python title="server.py" hl_lines="6 12 18"
|
||||
--8<-- "docs_src/first_steps/tutorial001.py"
|
||||
```
|
||||
|
||||
Três funções simples, três decoradores. Cada decorador já é o registro completo:
|
||||
|
||||
* `@mcp.tool()` transforma `add` em uma **ferramenta**.
|
||||
* `@mcp.resource("greeting://{name}")` transforma `greeting` em um **template de recurso**: o `{name}` na URI é o parâmetro da função.
|
||||
* `@mcp.prompt()` transforma `summarize` em um **prompt**. A string que ela retorna vira uma mensagem de usuário.
|
||||
|
||||
Todo o resto (o nome, a descrição, o schema dos argumentos) o SDK lê da própria função: o nome dela, a docstring, as anotações de tipo. Você nunca declarou nada disso separadamente.
|
||||
|
||||
!!! tip
|
||||
As duas metades do SDK têm dois caminhos de importação: `from mcp import Client` e
|
||||
`from mcp.server import MCPServer`. Não existe `from mcp import MCPServer`.
|
||||
|
||||
### Experimente {#try-it}
|
||||
|
||||
Execute com o MCP Inspector:
|
||||
|
||||
```console
|
||||
uv run mcp dev server.py
|
||||
```
|
||||
|
||||
Abra a URL que ele imprime. O Inspector tem uma aba por primitiva; passe por elas na ordem.
|
||||
|
||||
**Ferramentas.** Uma entrada: `add`, descrita como *Add two numbers.* O formulário tem um campo inteiro obrigatório para `a` e outro para `b`. Preencha, chame, e o resultado é `3`. O Inspector montou esse formulário a partir de `a: int, b: int`. Qualquer outro cliente faz o mesmo.
|
||||
|
||||
**Recursos.** A lista *Resources* está vazia. `greeting` fica em **Resource Templates**, porque `greeting://{name}` tem um parâmetro: não existe um recurso concreto para listar até alguém fornecer um `name`. Passe `World` e leia:
|
||||
|
||||
```text
|
||||
Hello, World!
|
||||
```
|
||||
|
||||
**Prompts.** Uma entrada: `summarize`, com um único argumento obrigatório, `text`. Obtenha-o com algum texto e você recebe uma mensagem com `role: user` e sua string renderizada como conteúdo. Um prompt é só isso: uma função que monta mensagens.
|
||||
|
||||
O Inspector executou seu servidor via **stdio**, um dos transportes que um servidor MCP sabe falar. Por enquanto você não escolhe um; **[Executando seu servidor](../run/index.md)** é a página para isso.
|
||||
|
||||
## Capacidades {#capabilities}
|
||||
|
||||
Você viu três abas no Inspector. Como ele sabia que eram três?
|
||||
|
||||
Quando um cliente se conecta, o servidor declara suas **capacidades**: quais famílias de requisições ele vai responder. O cliente usa essa declaração para decidir o que faz sentido pedir. Você nunca escreveu isso; o `MCPServer` declara por você.
|
||||
|
||||
Veja você mesmo. O `Client` do SDK aceita o objeto do servidor diretamente e se conecta a ele **em memória** (sem subprocesso, sem porta):
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
from mcp import Client
|
||||
|
||||
from server import mcp
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(mcp) as client:
|
||||
print(client.server_capabilities.model_dump(exclude_none=True))
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
```text
|
||||
{'prompts': {'list_changed': True}, 'resources': {'subscribe': True, 'list_changed': True}, 'tools': {'list_changed': True}}
|
||||
```
|
||||
|
||||
Esse dicionário é a declaração de **capacidades** do seu servidor. É a primeira coisa que todo cliente aprende ao se conectar:
|
||||
|
||||
| Capacidade | O cliente agora pode chamar |
|
||||
|-------------|---------------------------------------------------------------|
|
||||
| `tools` | `tools/list`, `tools/call` |
|
||||
| `resources` | `resources/list`, `resources/templates/list`, `resources/read` |
|
||||
| `prompts` | `prompts/list`, `prompts/get` |
|
||||
|
||||
O `MCPServer` serve as três primitivas, então as três são sempre declaradas.
|
||||
|
||||
Repare no que não aparece ali. `completions` (autocompletar de argumentos para templates de recurso e prompts) precisa de um handler escrito por você; este servidor não tem nenhum, então a capacidade fica de fora e um cliente bem-comportado nem pede. Essa é a regra para tudo que é opcional: registre a coisa e a capacidade aparece; **[Completions](../servers/completions.md)** comprova isso.
|
||||
|
||||
!!! info
|
||||
`Client(mcp)` é o mesmo cliente em memória com que todos os exemplos desta documentação são
|
||||
testados, e é assim que você vai testar os seus. Ele ganha uma página inteira: **[Testes](testing.md)**.
|
||||
|
||||
## O que você não escreveu {#what-you-did-not-write}
|
||||
|
||||
Olhe de novo esta página. Você escreveu três funções Python pequenas. Você **não** escreveu:
|
||||
|
||||
* Um JSON Schema. `a: int, b: int` *é* o schema de `add`.
|
||||
* Um handler de requisição. `tools/list`, `resources/read`, `prompts/get`: o SDK atende todos por você.
|
||||
* Uma declaração de capacidades. O `MCPServer` fez isso por você.
|
||||
* Uma linha de protocolo. A negociação de versão, o enquadramento JSON-RPC, a troca de capacidades: tudo isso aconteceu dentro de `mcp dev` e `Client(mcp)`, e você nunca viu.
|
||||
|
||||
Essa proporção é a razão de ser do SDK.
|
||||
|
||||
## Recapitulando {#recap}
|
||||
|
||||
* Um **host** é o app de LLM, um **cliente** é a metade dele que fala MCP, um **servidor** é o que você constrói.
|
||||
* Ferramentas são controladas pelo **modelo**, recursos pela **aplicação**, prompts pelo **usuário**.
|
||||
* Um decorador por primitiva: `@mcp.tool()`, `@mcp.resource(uri)`, `@mcp.prompt()`. Nome, descrição e schema vêm da função.
|
||||
* Uma URI com um `{param}` cria um **template** de recurso, listado separadamente dos recursos concretos.
|
||||
* As **capacidades** do servidor já vêm declaradas para você, e um cliente só pede o que o servidor declara.
|
||||
* `Client(mcp)` se conecta ao objeto do servidor em memória: seu ambiente de testes desde o primeiro dia.
|
||||
|
||||
A seguir vem **[Conecte a um host real](real-host.md)**: este servidor dentro do Claude Desktop ou de uma IDE, de verdade. Depois, **[Testes](testing.md)**: uma página, um cliente em memória, e você nunca mais fica adivinhando se funciona. Depois disso, cada primitiva ganha sua própria página, começando pela que o modelo comanda: **[Ferramentas](../servers/tools.md)**.
|
||||
57
i18n/pt/pages/get-started/index.md
Normal file
57
i18n/pt/pages/get-started/index.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
---
|
||||
translation:
|
||||
sections: [ed4a756b4c53c585, 97e2fb315b7fe398, 4d04f1c6f4bf6c1d, 577d73078fc62baf]
|
||||
tool: 1
|
||||
---
|
||||
# Comece por aqui {#get-started}
|
||||
|
||||
Novo no MCP, ou novo neste SDK? Comece aqui. Estas páginas levam você do zero a um
|
||||
servidor funcional e testado: [instale o SDK](installation.md), construa seu
|
||||
[primeiro servidor](first-steps.md), [conecte-o a um host real](real-host.md) e
|
||||
[teste-o](testing.md) com um cliente em memória.
|
||||
|
||||
## Execute o código {#run-the-code}
|
||||
|
||||
Todos os blocos de código podem ser copiados e usados diretamente: são arquivos completos e funcionais.
|
||||
|
||||
Para acompanhar, cole um bloco em um `server.py` e abra-o no MCP Inspector:
|
||||
|
||||
```console
|
||||
uv run mcp dev server.py
|
||||
```
|
||||
|
||||
É **ALTAMENTE recomendado** que você escreva (ou copie) o código, edite-o e execute-o localmente. Usá-lo no seu próprio editor é o que mostra de verdade qual é a ideia: o pouco que você escreve, o autocompletar, a checagem de tipos pegando erros antes de executar qualquer coisa.
|
||||
|
||||
## Você não vai precisar adivinhar {#you-will-not-be-guessing}
|
||||
|
||||
Cada exemplo nesta documentação é um arquivo completo em [`docs_src/`](https://github.com/modelcontextprotocol/python-sdk/tree/main/docs_src) no próprio repositório do SDK, e a suíte de testes do SDK exercita cada um deles por meio de um **cliente em memória**:
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from mcp import Client
|
||||
|
||||
from server import mcp
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_add() -> None:
|
||||
async with Client(mcp) as client:
|
||||
result = await client.call_tool("add", {"a": 1, "b": 2})
|
||||
assert result.structured_content == {"result": 3}
|
||||
```
|
||||
|
||||
Sem subprocesso, sem porta, sem transporte. `Client(mcp)` se conecta diretamente ao objeto do servidor.
|
||||
|
||||
Se uma mudança no SDK quebrar um exemplo de uma destas páginas, o CI fica vermelho antes que a página quebre. O código que você lê aqui é o código que roda.
|
||||
|
||||
Você mesmo vai usar isso em [Testes](testing.md); é assim que você testa seus próprios servidores também.
|
||||
|
||||
## Para onde ir agora {#where-to-go-next}
|
||||
|
||||
Depois que você tiver um servidor rodando, o resto desta documentação é uma referência, não um curso.
|
||||
Cada página se sustenta sozinha, então pule direto para o que você precisa:
|
||||
|
||||
* O que um servidor expõe (ferramentas, recursos, prompts) está em **[Servidores](../servers/index.md)**.
|
||||
* O que está disponível dentro das funções que você registra está em **[Dentro do seu handler](../handlers/index.md)**.
|
||||
* Levar o servidor até os clientes (stdio, HTTP, o app FastAPI que você já tem) está em **[Executando seu servidor](../run/index.md)**.
|
||||
* Construir o outro lado, uma aplicação que *usa* servidores MCP, está em **[Clientes](../client/index.md)**.
|
||||
47
i18n/pt/pages/get-started/installation.md
Normal file
47
i18n/pt/pages/get-started/installation.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
---
|
||||
translation:
|
||||
sections: [6e2f9bab94d5ed36, 8cf653388f69e28b, 6fd9ea2f65de0df6]
|
||||
tool: 1
|
||||
---
|
||||
# Instalação {#installation}
|
||||
|
||||
O SDK Python está no PyPI como [`mcp`](https://pypi.org/project/mcp/). Ele requer **Python 3.10+**.
|
||||
|
||||
Esta documentação descreve a **v2**, a linha de versões estável atual:
|
||||
|
||||
=== "uv"
|
||||
|
||||
```bash
|
||||
uv add "mcp[cli]"
|
||||
```
|
||||
|
||||
=== "pip"
|
||||
|
||||
```bash
|
||||
pip install "mcp[cli]"
|
||||
```
|
||||
|
||||
!!! note "Vindo da v1?"
|
||||
A v2 é uma versão major com quebras de compatibilidade; o **[Guia de migração](../migration.md)**
|
||||
cobre todas elas. Se o seu *pacote* depende de `mcp` e ainda não está pronto para migrar, mantenha um
|
||||
limite superior `<2` (por exemplo `mcp>=1.28,<2`) para que uma resolução sem versão fixada continue na linha 1.x.
|
||||
|
||||
## O que é instalado {#what-gets-installed}
|
||||
|
||||
Você não precisa saber nada disso para usar o SDK, mas, se quiser saber para que serve cada dependência:
|
||||
|
||||
* `mcp-types`: todos os tipos do protocolo (requisições, resultados, blocos de conteúdo) em um pacote próprio, versionado em sincronia com o SDK. O código que depende de `mcp` o importa pelo alias `mcp.types` (todo `from mcp.types import ...` nesta documentação); importe `mcp_types` diretamente apenas em um projeto que instala `mcp-types` sem o SDK.
|
||||
* [`anyio`](https://anyio.readthedocs.io/): o runtime assíncrono. O SDK inteiro é escrito sobre o anyio, então roda tanto com `asyncio` quanto com `trio`.
|
||||
* [`pydantic`](https://docs.pydantic.dev/): a base de todos os modelos de `mcp.types`, além de toda a geração e validação de schemas.
|
||||
* [`httpx2`](https://pypi.org/project/httpx2/): o cliente HTTP por trás dos transportes de *cliente* Streamable HTTP e SSE, com suporte embutido a server-sent events.
|
||||
* [`starlette`](https://www.starlette.io/), [`uvicorn`](https://www.uvicorn.org/), [`sse-starlette`](https://pypi.org/project/sse-starlette/) e [`python-multipart`](https://pypi.org/project/python-multipart/): os transportes HTTP de *servidor*.
|
||||
* [`jsonschema`](https://pypi.org/project/jsonschema/): valida a saída estruturada de uma ferramenta (tool) contra o schema de saída declarado por ela.
|
||||
* [`pyjwt[crypto]`](https://pyjwt.readthedocs.io/): tratamento de tokens OAuth para autorização.
|
||||
* [`opentelemetry-api`](https://opentelemetry-python.readthedocs.io/): apenas a API leve, então o middleware de tracing do SDK não custa nada, a menos que você mesmo instale um SDK e um exporter do OpenTelemetry.
|
||||
* [`typing-extensions`](https://typing-extensions.readthedocs.io/) e [`typing-inspection`](https://pypi.org/project/typing-inspection/): funcionalidades modernas de tipagem no Python 3.10.
|
||||
* [`pywin32`](https://pypi.org/project/pywin32/): somente no Windows, usado para o gerenciamento de subprocessos `stdio`.
|
||||
|
||||
## Extras opcionais {#optional-extras}
|
||||
|
||||
* `mcp[cli]` adiciona [`typer`](https://typer.tiangolo.com/) e [`python-dotenv`](https://pypi.org/project/python-dotenv/) para a ferramenta de linha de comando `mcp` (`mcp dev`, `mcp run`, `mcp install`). Você vai querer isso durante o desenvolvimento; talvez não precise dele em um servidor depois do deploy.
|
||||
* `mcp[rich]` adiciona [`rich`](https://rich.readthedocs.io/) para logs de servidor mais bonitos.
|
||||
185
i18n/pt/pages/get-started/real-host.md
Normal file
185
i18n/pt/pages/get-started/real-host.md
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
---
|
||||
translation:
|
||||
sections: [3c4f2f06b4e978b6, 22520eecae3d1961, f4e1709db18d635a, 2eb57992049671d9, 1ba83e9af37cc1b4, 4822586344b08d9e, 1c93afef72478992, b6b448f9eddd51dc, fe55370fd931815b]
|
||||
tool: 1
|
||||
---
|
||||
# Conecte-se a um host de verdade {#connect-to-a-real-host}
|
||||
|
||||
Um **host** é a aplicação dentro da qual seu servidor acaba rodando: Claude Desktop, Claude Code, uma IDE. O host é aquilo com que o usuário conversa. Dentro dele, um **cliente** MCP inicia seu servidor como um processo filho e fala com ele pelo stdin e stdout desse processo.
|
||||
|
||||
Ou seja, conectar a um host é um ato só: você informa a ele **o comando que inicia seu servidor**. Tudo nesta página (dois comandos de CLI, três arquivos JSON) é um lugar diferente para colocar esse mesmo comando.
|
||||
|
||||
## Um servidor, todos os hosts {#one-server-every-host}
|
||||
|
||||
```python title="server.py" hl_lines="3 33-34"
|
||||
--8<-- "docs_src/real_host/tutorial001.py"
|
||||
```
|
||||
|
||||
Duas ferramentas (tools) e um recurso, um arquivo só. Três coisas sobre esse arquivo importam para todos os hosts abaixo:
|
||||
|
||||
* `mcp.run()` sem argumentos inicia um servidor **stdio**: ele bloqueia, lê mensagens do protocolo no stdin e as escreve no stdout. Esse é o transporte que todos os hosts desta página falam. O host inicia seu arquivo como um processo filho e é dono desses dois pipes, e é por isso que conectar nunca passa de "aqui está o comando". Você nunca escolhe uma porta, e nada fica escutando em uma.
|
||||
* `run()` fica dentro de `if __name__ == "__main__":`. Tudo abaixo **importa** este arquivo em vez de executá-lo, então um `run()` sem essa proteção iniciaria um servidor no instante em que qualquer coisa carregasse o módulo.
|
||||
* O objeto do servidor é uma global de nível de módulo chamada `mcp`. É esse o nome que `mcp run` procura (`server` e `app` também funcionam). Dê outro nome a ele e você precisa informá-lo explicitamente: `mcp run server.py:bookshop`.
|
||||
|
||||
Essa é a última linha de Python nesta página. Daqui para baixo é tudo configuração de host.
|
||||
|
||||
## O comando de inicialização {#the-launch-command}
|
||||
|
||||
Todos os hosts abaixo recebem o mesmo comando:
|
||||
|
||||
```bash
|
||||
uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py
|
||||
```
|
||||
|
||||
Um comando só para todos eles porque `uv run --with` resolve o SDK em um ambiente novo na hora: funciona a partir de qualquer diretório e não precisa de projeto nem de ambiente virtual para ativar. Isso importa aqui mais do que em qualquer outro lugar, porque um host inicia seu servidor a partir do diretório de trabalho *dele*, com um ambiente quase vazio, e não a partir do seu shell.
|
||||
|
||||
É também o comando que `mcp install` grava na configuração do Claude Desktop para você (abaixo), então o que você digita à mão e o que o utilitário gera coincidem, fora o pin exato de versão que o utilitário adiciona.
|
||||
|
||||
!!! tip "Se um host não encontrar o `uv`"
|
||||
Um host inicia seu servidor com um `PATH` mínimo, e o `uv` pode não estar nele. Troque o
|
||||
`uv` sozinho pelo caminho absoluto que `which uv` (macOS/Linux) ou `where uv` (Windows)
|
||||
retorna. É exatamente isso que `mcp install` grava.
|
||||
|
||||
!!! note "Esta página é o cenário local"
|
||||
Tudo aqui executa seu servidor na máquina em que o host está: o host inicia seu
|
||||
arquivo, via stdio. Isso é exatamente o certo para uma ferramenta pessoal ou de uma máquina
|
||||
só. Para entregar um servidor a pessoas que *não* têm seu arquivo, você distribui uma
|
||||
**URL**, não um comando: o mesmo objeto `mcp` servido via Streamable HTTP.
|
||||
**[Executando seu servidor](../run/index.md)** é essa decisão em uma tabela só, e
|
||||
**[Deploy e escala](../run/deploy.md)** é o caminho dali até um hostname de verdade.
|
||||
|
||||
E um host nada mais é que uma aplicação com um cliente MCP dentro, então seu próprio
|
||||
código Python pode fazer o papel do host: **[Transportes do cliente](../client/transports.md)**
|
||||
inicia este mesmo arquivo como subprocesso com `stdio_client(...)`, e **[Testes](testing.md)**
|
||||
se conecta a ele em memória, sem processo nenhum.
|
||||
|
||||
## Claude Desktop {#claude-desktop}
|
||||
|
||||
O único host que o SDK consegue configurar para você:
|
||||
|
||||
```bash
|
||||
uv run mcp install server.py
|
||||
```
|
||||
|
||||
É só isso. `mcp install` importa o arquivo para ler o nome do servidor, encontra o arquivo de configuração do Claude Desktop e grava o comando de inicialização nele. De passagem, já converte o caminho para absoluto, então você não precisa fazer isso.
|
||||
|
||||
Não há mistério nenhum. Esta é a entrada que ele grava:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"Bookshop": {
|
||||
"command": "/absolute/path/to/uv",
|
||||
"args": [
|
||||
"run",
|
||||
"--frozen",
|
||||
"--with",
|
||||
"mcp[cli]==2.0.0",
|
||||
"mcp",
|
||||
"run",
|
||||
"/absolute/path/to/server.py"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
É o comando de inicialização da seção acima com três acréscimos: o caminho absoluto do `uv`, `--frozen` para que o `uv` nunca reescreva um lockfile que por acaso esteja por perto, e um pin exato na versão do `mcp` que você tem instalada. Ele vai parar em `claude_desktop_config.json`, que fica em:
|
||||
|
||||
* **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||
* **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
||||
|
||||
Você pode escrever esse arquivo à mão. `mcp install` existe para você não cometer o erro clássico (um caminho relativo) ao fazer isso.
|
||||
|
||||
Encerre o Claude Desktop por completo (não só a janela) e abra-o de novo.
|
||||
|
||||
!!! warning
|
||||
`mcp install` falha com `Claude app not found` se o *diretório* de configuração do Claude
|
||||
Desktop ainda não existir. Instale o Claude Desktop e execute-o uma vez: é isso que cria o
|
||||
diretório.
|
||||
|
||||
!!! tip
|
||||
O Claude Desktop inicia seu servidor em um processo próprio, então as variáveis de ambiente do
|
||||
seu shell não estão lá. `uv run mcp install server.py -v API_KEY=abc123` (ou `-f .env`) as
|
||||
registra no campo `env` da entrada. `--name` sobrescreve o nome da entrada; o padrão é o
|
||||
`name` do servidor.
|
||||
|
||||
## Claude Code {#claude-code}
|
||||
|
||||
Não há arquivo para editar. Registre o servidor com a CLI `claude`; tudo depois de `--` é o comando de inicialização.
|
||||
|
||||
```bash
|
||||
claude mcp add bookshop -- uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py
|
||||
```
|
||||
|
||||
Execute `/mcp` dentro de uma sessão do Claude Code para confirmar que `bookshop` está conectado e que suas ferramentas aparecem listadas.
|
||||
|
||||
## Cursor {#cursor}
|
||||
|
||||
Crie `.cursor/mcp.json` na raiz do seu projeto.
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"bookshop": {
|
||||
"command": "uv",
|
||||
"args": ["run", "--with", "mcp[cli]", "mcp", "run", "/absolute/path/to/server.py"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Os mesmos `command` e `args`, sob a mesma chave `mcpServers` que o Claude Desktop usa. O servidor aparece nas configurações de MCP do Cursor com as duas ferramentas listadas.
|
||||
|
||||
## VS Code {#vs-code}
|
||||
|
||||
Crie `.vscode/mcp.json` na raiz do seu projeto.
|
||||
|
||||
```json
|
||||
{
|
||||
"servers": {
|
||||
"bookshop": {
|
||||
"type": "stdio",
|
||||
"command": "uv",
|
||||
"args": ["run", "--with", "mcp[cli]", "mcp", "run", "/absolute/path/to/server.py"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Duas diferenças em relação ao arquivo do Cursor, e são as únicas duas: a chave externa é `servers`, não `mcpServers`, e cada entrada declara seu `type`. Confirme o diálogo de confiança e, em seguida, **MCP: List Servers** na paleta de comandos mostra `bookshop` rodando.
|
||||
|
||||
!!! note
|
||||
Você precisa do VS Code 1.99 ou posterior, com login feito na extensão **GitHub Copilot**
|
||||
(o Copilot Free basta), e o Copilot Chat precisa estar no modo **Agent**, porque nenhum
|
||||
outro modo chama ferramentas.
|
||||
|
||||
## Não aparece {#it-doesnt-show-up}
|
||||
|
||||
Antes de mexer em qualquer configuração de host, execute você mesmo o comando de inicialização:
|
||||
|
||||
```bash
|
||||
uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py
|
||||
```
|
||||
|
||||
Nada é impresso, e ele não retorna. Esse silêncio está certo: um servidor stdio está esperando um host falar primeiro no stdin (`Ctrl-C` para pará-lo). Um traceback ou uma saída imediata é o bug de verdade, e agora você consegue lê-lo em vez de tentar adivinhá-lo através de um host.
|
||||
|
||||
Uma vez que esse comando fica parado esperando, o que sobra é quase sempre uma de três coisas:
|
||||
|
||||
* **Um caminho relativo.** O host inicia seu servidor a partir do diretório de trabalho *dele*, não daquele de onde você fez o registro. `server.py` onde é preciso `/absolute/path/to/server.py` é, de longe, a falha mais comum. Se o host também não encontrar o `uv`, esse caminho precisa ser absoluto também.
|
||||
* **O host ainda está rodando a configuração antiga.** Os hosts leem a configuração ao iniciar. O Claude Desktop, em particular, precisa ser *encerrado por completo* (não basta fechar a janela) e reaberto para que uma edição em `claude_desktop_config.json` tenha efeito.
|
||||
* **Algo chegou ao stdout fora do intervalo de desvio.** No stdio, o stdout *é* o protocolo. O SDK desvia para o stderr a saída avulsa descarregada (com flush) enquanto está servindo, mas uma saída descarregada no stdout antes disso (um script wrapper que ecoa algo, um `print()` em tempo de importação em um processo sem buffer), ou um `print()` em buffer que só é descarregado quando o interpretador encerra, entrega ao host uma mensagem corrompida, e ele derruba a conexão. Registre os logs usando a configuração padrão do `logging`, cujo handler de stderr faz flush de cada registro; handlers personalizados também precisam evitar o stdout. **[Logging](../handlers/logging.md)** tem a história completa.
|
||||
|
||||
O Claude Desktop mantém um log por servidor: `mcp-server-<NAME>.log` é o stderr do seu servidor, ao lado de `mcp.log` para as conexões, em `~/Library/Logs/Claude` no macOS e `%APPDATA%\Claude\logs` no Windows.
|
||||
|
||||
Para qualquer coisa além dessas três, a página é **[Solução de problemas](../troubleshooting.md)**.
|
||||
|
||||
## Recapitulando {#recap}
|
||||
|
||||
* Um **host** (Claude Desktop, uma IDE) executa um cliente MCP que inicia seu servidor como processo filho via stdio. Conectar significa dar a ele um comando de inicialização.
|
||||
* Esse comando é `uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py`: nenhum venv para ativar, funciona a partir de qualquer diretório.
|
||||
* **Claude Desktop** é o único host que `mcp install` configura para você. Ele grava esse mesmo comando (mais o caminho absoluto do `uv`, `--frozen` e um pin exato na versão que você tem instalada) em `claude_desktop_config.json`, para você nunca precisar fazer isso.
|
||||
* **Claude Code** é `claude mcp add bookshop -- <launch command>`. **Cursor** é `.cursor/mcp.json` sob `mcpServers`. **VS Code** é `.vscode/mcp.json` sob `servers`, cada entrada com um `type`.
|
||||
* Caminhos absolutos em todo lugar, reinicie o host depois de editar a configuração dele, e nunca deixe nada além do SDK escrever no stdout.
|
||||
|
||||
Todos os hosts desta página se conectaram ao mesmo arquivo, com o mesmo comando. O que esse arquivo pode *expor* é o resto desta documentação: **[Ferramentas](../servers/tools.md)**, **[Recursos](../servers/resources.md)** e todos os transportes além do stdio em **[Executando seu servidor](../run/index.md)**.
|
||||
115
i18n/pt/pages/get-started/testing.md
Normal file
115
i18n/pt/pages/get-started/testing.md
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
---
|
||||
translation:
|
||||
sections: ['4926721070127497', c52a1de2b6b32f40, 2e410b412c25f314, 627195f7159e24ef]
|
||||
tool: 1
|
||||
---
|
||||
# Testes {#testing}
|
||||
|
||||
O SDK Python traz uma classe `Client` com um **transporte em memória**: passe a ela o objeto do seu servidor e ela se conecta diretamente a ele.
|
||||
|
||||
Sem subprocesso. Sem porta. Sem transporte nenhum. É a mesma ideia do `TestClient` do FastAPI.
|
||||
|
||||
## Uso básico {#basic-usage}
|
||||
|
||||
Vamos supor que você tenha um servidor simples com uma única ferramenta (tool):
|
||||
|
||||
```python title="server.py"
|
||||
--8<-- "docs_src/testing/tutorial001.py"
|
||||
```
|
||||
|
||||
Para executar o teste abaixo, você vai precisar de duas dependências extras (de desenvolvimento):
|
||||
|
||||
=== "uv"
|
||||
|
||||
```bash
|
||||
uv add --dev pytest inline-snapshot
|
||||
```
|
||||
|
||||
=== "pip"
|
||||
|
||||
```bash
|
||||
pip install pytest inline-snapshot
|
||||
```
|
||||
|
||||
!!! info
|
||||
Esta documentação pressupõe que você já conhece o [`pytest`](https://docs.pytest.org/en/stable/).
|
||||
|
||||
O [`inline-snapshot`](https://15r10nk.github.io/inline-snapshot/latest/) é o que o teste abaixo
|
||||
usa para fazer a asserção sobre o objeto de resultado inteiro em uma única linha. Ele grava a saída
|
||||
de um teste como o literal `snapshot(...)` que você vê. Se preferir não usá-lo, remova o import e
|
||||
faça as asserções sobre os campos que interessam (`result.content[0].text == "3"`), como em
|
||||
qualquer outro teste.
|
||||
|
||||
Agora o teste:
|
||||
|
||||
```python title="test_server.py"
|
||||
import pytest
|
||||
from inline_snapshot import snapshot
|
||||
from mcp import Client
|
||||
from mcp.types import CallToolResult, TextContent
|
||||
|
||||
from server import mcp
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def anyio_backend(): # (1)!
|
||||
return "asyncio"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def client(): # (2)!
|
||||
async with Client(mcp, raise_exceptions=True) as c:
|
||||
yield c
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_call_add_tool(client: Client):
|
||||
result = await client.call_tool("add", {"a": 1, "b": 2})
|
||||
# Drop the server identity stamp in `_meta`; it is not what this test is about.
|
||||
result.meta = None
|
||||
assert result == snapshot(
|
||||
CallToolResult(
|
||||
content=[TextContent(type="text", text="3")],
|
||||
structured_content={"result": 3},
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
1. Se você estiver usando `trio`, retorne `"trio"` no lugar. Veja a [documentação do anyio](https://anyio.readthedocs.io/en/stable/testing.html#specifying-the-backends-to-run-on) para os detalhes.
|
||||
2. A fixture entrega um cliente já conectado. Todo teste que recebe `client` ganha uma conexão em memória nova com o mesmo servidor.
|
||||
|
||||
Pronto! Agora você pode estender seus testes para cobrir mais cenários.
|
||||
|
||||
## Por que `raise_exceptions=True`? {#why-raise_exceptionstrue}
|
||||
|
||||
Duas coisas diferentes podem dar errado, e essa flag só mexe em uma delas.
|
||||
|
||||
Uma exceção dentro de uma das **suas ferramentas** não é uma falha de protocolo. Ela vira um resultado normal com
|
||||
`is_error=True`, e o modelo lê a mensagem. `raise_exceptions` não muda isso: com ou
|
||||
sem ela, `call_tool` retorna o mesmo resultado com `is_error=True`. Há uma página inteira sobre isso:
|
||||
**[Tratamento de erros](../servers/handling-errors.md)**.
|
||||
|
||||
Uma falha **fora** do corpo de uma ferramenta é diferente. Na conexão que `Client(mcp)` entrega, o
|
||||
servidor a sanitiza em um genérico `"Internal server error"` antes que o cliente a veja. Você nunca
|
||||
deve vazar os detalhes de um crash inesperado para um chamador remoto. Em um teste, isso é exatamente o que
|
||||
você *não* quer, e é isso que `raise_exceptions=True` muda: seu teste vê a mensagem real
|
||||
em vez da sanitizada.
|
||||
|
||||
Deixe-a ligada nos testes. Em código de produção, ela não significa nada.
|
||||
|
||||
## No mesmo processo por padrão {#in-process-by-default}
|
||||
|
||||
!!! note
|
||||
`Client(mcp)` se conecta no mesmo processo e é **neutro quanto à era** por padrão: ele sonda o servidor e
|
||||
escolhe o caminho de protocolo adequado. Fixe `mode="legacy"` se o seu teste exercita semântica
|
||||
específica do modo legado (push de amostragem (sampling) ou de elicitação (elicitation), `message_handler`), e remova `raise_exceptions=True`
|
||||
nesse caso: uma conexão legada nem sequer sanitiza, e a flag relança a
|
||||
falha dentro da task do servidor em vez de no seu teste.
|
||||
|
||||
Essa única linha é também o motivo pelo qual esta documentação pode prometer que os exemplos funcionam: cada
|
||||
arquivo de exemplo é exercitado pela própria suíte de testes do SDK, quase todos exatamente por meio deste
|
||||
cliente. Você está usando a mesma ferramenta que o SDK usa em si mesmo.
|
||||
|
||||
Você tem um servidor funcionando e testado. Colocá-lo dentro de uma aplicação real (Claude Desktop, uma
|
||||
IDE) é o tema de **[Conecte-se a um host real](real-host.md)**; todas as outras formas de servi-lo estão em
|
||||
**[Executando seu servidor](../run/index.md)**.
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue