docs: publish translated docs in twelve languages and the tool that maintains them (#3280)
This commit is contained in:
parent
14aa889992
commit
52ad0a8876
8 changed files with 89737 additions and 54 deletions
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.
|
||||
Loading…
Reference in a new issue