docs: publish translated docs in twelve languages and the tool that maintains them (#3280)
This commit is contained in:
parent
14aa889992
commit
52ad0a8876
6 changed files with 89737 additions and 54 deletions
165
i18n/pt/pages/advanced/apps.md
Normal file
165
i18n/pt/pages/advanced/apps.md
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
---
|
||||
translation:
|
||||
sections: [0355618e5f4d5fe4, 1821eaf50f2d0b64, 82e0b28ebd3abf5a, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487]
|
||||
tool: 1
|
||||
---
|
||||
# MCP Apps {#mcp-apps}
|
||||
|
||||
Um **MCP App** é uma ferramenta (tool) com uma cara: junto com os dados, a ferramenta aponta para um
|
||||
documento HTML que o host renderiza como uma superfície interativa.
|
||||
|
||||
Duas partes, sempre duas partes:
|
||||
|
||||
1. **Uma ferramenta** que faz o trabalho e retorna dados, como qualquer outra ferramenta.
|
||||
2. **Um recurso `ui://`** contendo o HTML que o host mostra para ela.
|
||||
|
||||
A ferramenta carrega uma referência `_meta.ui.resourceUri` ao recurso. O host busca esse
|
||||
recurso com `resources/read`, renderiza em um **iframe em sandbox** e envia o resultado
|
||||
da ferramenta para dentro desse iframe via `postMessage`. Seu servidor nunca envia nem recebe
|
||||
nenhuma mensagem `ui/*`: esse tráfego fica entre o host e o iframe. Você serve uma ferramenta
|
||||
e um documento HTML; o host cuida do espetáculo.
|
||||
|
||||
O SDK entrega isso como a extensão embutida `Apps` (`io.modelcontextprotocol/ui`).
|
||||
Se [Extensões](extensions.md) são novidade para você, dê uma olhada naquela página primeiro. Um minuto,
|
||||
e depois volte aqui.
|
||||
|
||||
## Um relógio com uma cara {#a-clock-with-a-face}
|
||||
|
||||
```python title="server.py" hl_lines="19 22 30 32"
|
||||
--8<-- "docs_src/apps/tutorial001.py"
|
||||
```
|
||||
|
||||
Quatro movimentos:
|
||||
|
||||
* `Apps()`: uma única instância guarda suas ferramentas ligadas a UI e os recursos delas.
|
||||
* `@apps.tool(resource_uri="ui://clock/app.html")`: uma ferramenta comum, mais o
|
||||
carimbo `_meta.ui.resourceUri`. Tudo o que `@mcp.tool()` aceita (name, title,
|
||||
description, ...) passa direto.
|
||||
* `apps.add_html_resource("ui://clock/app.html", CLOCK_HTML)`: o recurso
|
||||
correspondente, servido como `text/html;profile=mcp-app`. É exatamente esse MIME type que
|
||||
diz ao host "isto é um app, renderize".
|
||||
* `MCPServer("clock", extensions=[apps])`: você opta por participar. O servidor agora anuncia
|
||||
`io.modelcontextprotocol/ui` em `capabilities.extensions`.
|
||||
|
||||
O HTML em si escuta o `postMessage` do host e mostra o resultado. Para apps
|
||||
de verdade, use o SDK de navegador oficial [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps)
|
||||
dentro do seu HTML. Ele dá a você `ontoolresult`, `callServerTool`,
|
||||
`getHostContext` e `onhostcontextchanged` em vez de eventos de mensagem crus.
|
||||
|
||||
## Degradação elegante {#graceful-degradation}
|
||||
|
||||
Nem todo cliente renderiza apps. A especificação é direta sobre o que isso significa para você:
|
||||
|
||||
> As ferramentas **DEVEM** retornar um array `content` significativo mesmo quando há UI disponível.
|
||||
|
||||
O modelo lê `content`; o iframe é para humanos. Um host com suporte a UI ainda passa
|
||||
o resultado em texto para o modelo, e um cliente só de texto recebe *apenas* isso. Então o
|
||||
padrão canônico é uma ferramenta, duas respostas. Olhe `get_time` de novo:
|
||||
|
||||
```python title="server.py" hl_lines="23-27"
|
||||
--8<-- "docs_src/apps/tutorial001.py"
|
||||
```
|
||||
|
||||
`client_supports_apps(ctx)` é `True` somente quando o cliente declarou a
|
||||
extensão `io.modelcontextprotocol/ui` **e** listou `text/html;profile=mcp-app`
|
||||
nas suas configurações `mimeTypes`. O campo é obrigatório, então um cliente que o omite
|
||||
não conta. É exatamente isso que `main()` no mesmo arquivo declara: a
|
||||
metade cliente da negociação, e a resposta rica volta.
|
||||
|
||||
!!! warning
|
||||
Nunca retorne um placeholder como `"[Rendered UI]"` como único conteúdo. Se o
|
||||
texto de fallback é inútil, a ferramenta é inútil para todo cliente só de texto e para
|
||||
o próprio modelo. Escreva a frase.
|
||||
|
||||
## Trancando o iframe {#locking-the-iframe-down}
|
||||
|
||||
O lado do recurso carrega os metadados de segurança: o que o iframe pode carregar, quais
|
||||
permissões do navegador ele quer, como gostaria de ser enquadrado:
|
||||
|
||||
```python title="server.py" hl_lines="9 19-22"
|
||||
--8<-- "docs_src/apps/tutorial002.py"
|
||||
```
|
||||
|
||||
`csp` e `permissions` são **pedidos ao host**, não comportamento do servidor. O host
|
||||
monta a Content-Security-Policy e a Permissions-Policy do iframe a partir deles, e
|
||||
pode recusar. Faça detecção de funcionalidade no seu JS em vez de presumir que foi concedido.
|
||||
|
||||
`ResourceCsp`, campo por campo (nome em Python, chave no protocolo, o que o host faz com ele):
|
||||
|
||||
| Python | Protocolo (`_meta.ui.csp`) | Controla |
|
||||
|---|---|---|
|
||||
| `connect_domains` | `connectDomains` | `connect-src`: para onde `fetch`/XHR podem ir |
|
||||
| `resource_domains` | `resourceDomains` | `img-src`, `style-src`, ...: assets estáticos |
|
||||
| `frame_domains` | `frameDomains` | `frame-src`: iframes aninhados |
|
||||
| `base_uri_domains` | `baseUriDomains` | `base-uri`: para onde `<base>` pode apontar |
|
||||
|
||||
`ResourcePermissions`: cada campo solicita uma permissão do navegador para o iframe.
|
||||
|
||||
| Python | Protocolo (`_meta.ui.permissions`) |
|
||||
|---|---|
|
||||
| `camera` | `camera` |
|
||||
| `microphone` | `microphone` |
|
||||
| `geolocation` | `geolocation` |
|
||||
| `clipboard_write` | `clipboardWrite` |
|
||||
|
||||
!!! note
|
||||
CSP e permissões vivem no **recurso**, nunca na ferramenta. Os metadados de ferramenta
|
||||
da especificação não têm lugar para eles, e os hosts os ignoram ali. O SDK torna o
|
||||
erro irrepresentável: `@apps.tool()` simplesmente não tem parâmetro `csp`.
|
||||
|
||||
### Visibilidade {#visibility}
|
||||
|
||||
`visibility=["app"]` em uma ferramenta diz "isto existe para o iframe, não para o modelo":
|
||||
|
||||
* `"model"`: o modelo pode chamá-la.
|
||||
* `"app"`: o iframe pode chamá-la (via `callServerTool`).
|
||||
* Omitido: ambos, que é o padrão.
|
||||
|
||||
Filtrar é trabalho do **host**. Seu servidor lista as ferramentas só de app em `tools/list`
|
||||
como qualquer outra; o host as esconde do modelo. Não filtre no lado do servidor.
|
||||
|
||||
## As regras que o SDK impõe {#the-rules-the-sdk-enforces}
|
||||
|
||||
Todas estas falham na inicialização, não em produção:
|
||||
|
||||
* Um `resource_uri` ou URI de recurso que não seja `ui://...` é um `ValueError` no
|
||||
momento da decoração/registro.
|
||||
* Uma ferramenta ligada a uma URI **sem recurso registrado correspondente** é um `ValueError`
|
||||
quando `MCPServer(extensions=[apps])` consome a extensão. Uma ferramenta que anuncia
|
||||
um HTML que dá 404 em `resources/read` é uma configuração errada, então o servidor se recusa
|
||||
a ser construído.
|
||||
* `meta={"ui": ...}` em `@apps.tool()` é um `ValueError`. O decorator é dono de
|
||||
`_meta["ui"]`; diga isso com `resource_uri=` e `visibility=`. Outras chaves em `meta=`
|
||||
são mescladas normalmente ao lado.
|
||||
|
||||
Nem o SDK ext-apps em TypeScript nem o FastMCP pegam nenhum desses casos hoje; preferimos
|
||||
que você descubra antes que um host descubra.
|
||||
|
||||
## Além do HTML inline {#beyond-inline-html}
|
||||
|
||||
`add_html_resource` cobre o caso comum: uma string de HTML. Para qualquer outra coisa,
|
||||
HTML em disco ou conteúdo gerado, construa o recurso você mesmo e entregue:
|
||||
|
||||
```python title="server.py" hl_lines="12 18"
|
||||
--8<-- "docs_src/apps/tutorial003.py"
|
||||
```
|
||||
|
||||
`add_resource` preenche o MIME type `text/html;profile=mcp-app` quando o recurso
|
||||
não define um explicitamente, e rejeita uma incompatibilidade explícita: um recurso `ui://`
|
||||
sob qualquer outro MIME type é um que nenhum host vai renderizar.
|
||||
|
||||
!!! tip
|
||||
Mirando um host pré-GA que ainda lê a chave plana depreciada
|
||||
`_meta["ui/resourceUri"]`? Mescle você mesmo:
|
||||
`@apps.tool(resource_uri="ui://x", meta={"ui/resourceUri": "ui://x"})`.
|
||||
O objeto `ui` aninhado é o formato da especificação; a chave plana está de saída.
|
||||
|
||||
## Veja rodando {#see-it-run}
|
||||
|
||||
A história `apps` em `examples/stories/` é esta página como um par executável: um servidor
|
||||
com uma ferramenta de relógio ligada a UI e um cliente que negocia Apps, lê o
|
||||
`_meta.ui.resourceUri` da ferramenta, busca o HTML e chama a ferramenta.
|
||||
|
||||
```bash
|
||||
uv run python -m stories.apps.client
|
||||
```
|
||||
172
i18n/pt/pages/advanced/extensions.md
Normal file
172
i18n/pt/pages/advanced/extensions.md
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
---
|
||||
translation:
|
||||
sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff]
|
||||
tool: 1
|
||||
---
|
||||
# Extensões {#extensions}
|
||||
|
||||
Uma **extensão** é um pacote opcional de comportamento MCP reunido sob um único identificador.
|
||||
|
||||
Em um servidor, ela pode contribuir com ferramentas (tools), recursos e novos métodos de requisição, e pode envolver `tools/call`. Em um cliente, ela pode reivindicar formatos extras de resultado de `tools/call` e observar notificações de fornecedores. Cada lado se anuncia no seu próprio `capabilities.extensions`, e nada muda para quem não pediu nada. Esse é o contrato ([SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)), e ele tem uma regra de ouro: **extensões vêm desligadas por padrão**.
|
||||
|
||||
## Usando uma extensão {#using-an-extension}
|
||||
|
||||
Passe as instâncias na construção:
|
||||
|
||||
```python title="server.py"
|
||||
--8<-- "docs_src/extensions/tutorial001.py"
|
||||
```
|
||||
|
||||
Pronto. O servidor agora anuncia `io.modelcontextprotocol/ui` em `capabilities.extensions` e serve tudo o que a extensão contribui.
|
||||
|
||||
`Apps` é a extensão de referência embutida, e ela tem uma página própria: **[MCP Apps](apps.md)**.
|
||||
|
||||
!!! note
|
||||
As extensões são fixadas na construção. Não existe um `add_extension` para chamar depois: o mapa de capacidades de um servidor não deve mudar enquanto há clientes conectados a ele.
|
||||
|
||||
O mapa de capacidades viaja em `server/discover`, que é um caminho da **2026-07-28**. Um handshake `initialize` legado não tem onde colocá-lo, então um cliente legado simplesmente não enxerga a extensão. Projete pensando nisso: uma extensão *amplia* um servidor, ela não pode ser a única forma de usá-lo.
|
||||
|
||||
## Escrevendo a sua {#writing-your-own}
|
||||
|
||||
Herde de `Extension` e sobrescreva apenas o que precisar. Todo método tem um padrão.
|
||||
|
||||
### O identificador {#the-identifier}
|
||||
|
||||
```python
|
||||
--8<-- "docs_src/extensions/tutorial002.py"
|
||||
```
|
||||
|
||||
O identificador é uma string `vendor-prefix/name` que segue a gramática de chaves `_meta` da especificação: rótulos separados por ponto (cada um começa com uma letra e termina com uma letra ou dígito), uma barra e então o nome. Ele é validado **quando a classe é definida**, então um erro de digitação não espera o servidor subir:
|
||||
|
||||
```text
|
||||
TypeError: Stamps.identifier must be a `vendor-prefix/name` string
|
||||
(reverse-DNS prefix required), got 'stamps'
|
||||
```
|
||||
|
||||
Use como prefixo um domínio que você controla. `io.modelcontextprotocol/*` é reservado para extensões especificadas pelo próprio projeto MCP.
|
||||
|
||||
### Contribuindo com ferramentas {#contributing-tools}
|
||||
|
||||
A menor extensão útil é uma ferramenta e um mapa de configurações:
|
||||
|
||||
```python title="server.py" hl_lines="17 19-20 22-23 26"
|
||||
--8<-- "docs_src/extensions/tutorial003.py"
|
||||
```
|
||||
|
||||
* `tools()` retorna `ToolBinding`s. O servidor registra cada uma exatamente como se você tivesse chamado `mcp.add_tool(...)` por conta própria: mesma geração de schema, mesma injeção de `Context`, tudo igual.
|
||||
* `settings()` é o valor anunciado em `capabilities.extensions["com.example/stamps"]`. Retorne `{}` (o padrão) para anunciar a extensão sem configurações.
|
||||
* A extensão nunca recebe o servidor. Ela declara contribuições como dados; o `MCPServer` as consome. Não existe um `self.server` para modificar.
|
||||
|
||||
E `main()` é a prova, um cliente em memória direto contra `mcp`:
|
||||
|
||||
```python title="server.py" hl_lines="29-34"
|
||||
--8<-- "docs_src/extensions/tutorial003.py"
|
||||
```
|
||||
|
||||
### Servindo seus próprios métodos {#serving-your-own-methods}
|
||||
|
||||
Uma extensão pode registrar **novos métodos de requisição**: seus próprios verbos, servidos ao lado dos da especificação:
|
||||
|
||||
```python title="server.py" hl_lines="16-22 31 40-48"
|
||||
--8<-- "docs_src/extensions/tutorial004.py"
|
||||
```
|
||||
|
||||
* `SearchParams` herda de `RequestParams`, então o envelope `_meta` de 2026 é analisado de forma uniforme e seu handler recebe parâmetros validados, nunca um dict cru. Limite o que o cliente controla: `Field(ge=1, le=100)` rejeita um `limit` absurdo antes que seu código aloque qualquer coisa para ele.
|
||||
* `require_client_extension(ctx, EXTENSION_ID)` é a barreira: um cliente que não declarou a extensão recebe o erro `-32021` (capacidade obrigatória do cliente ausente), com o payload `requiredCapabilities` legível por máquina que a especificação pede.
|
||||
* `protocol_versions=frozenset({"2026-07-28"})` fixa o método em uma única versão de protocolo. Em qualquer outra versão o cliente recebe `METHOD_NOT_FOUND`, exatamente como se o método não existisse ali. Para esse cliente, não existe.
|
||||
|
||||
Os métodos são **estritamente aditivos**. O SDK impõe isso na construção, não em tempo de execução:
|
||||
|
||||
* Um `MethodBinding` para um método definido pela especificação (`tools/list`, `completion/complete`, ...) lança `ValueError` quando o binding é construído. Os verbos centrais pertencem ao servidor.
|
||||
* Duas extensões vinculando o mesmo método lançam quando a segunda se registra. A última escrita vencer é como plugins corrompem uns aos outros; não fazemos isso.
|
||||
* Um conjunto `protocol_versions` vazio também lança: um método que nunca pode ser servido é um bug, não uma configuração.
|
||||
|
||||
### O lado do cliente {#the-client-side}
|
||||
|
||||
O `main()` do mesmo arquivo é a história inteira do cliente, com as duas metades:
|
||||
|
||||
```python title="server.py" hl_lines="54-58"
|
||||
--8<-- "docs_src/extensions/tutorial004.py"
|
||||
```
|
||||
|
||||
* `Client(..., extensions=[advertise(EXTENSION_ID)])` declara a extensão. As declarações viram `ClientCapabilities.extensions`: em uma conexão 2026-07-28 o mapa viaja no envelope `_meta` de cada requisição, então o servidor o vê em **toda** requisição; em uma conexão legada ele vai no handshake `initialize`. O código do servidor não se importa com qual: `require_client_extension(ctx, ...)` e `ctx.session.check_client_capability(...)` leem a fonte certa nos dois caminhos.
|
||||
* Métodos de fornecedor descem uma camada para `client.session.send_request(...)`; `Client` só ganha métodos de primeira classe para verbos da especificação. `send_request` aceita qualquer subclasse de `Request`, então a requisição do fornecedor passa como está.
|
||||
|
||||
### Interceptando `tools/call` {#intercepting-toolscall}
|
||||
|
||||
O único hook interceptador. Sobrescreva `intercept_tool_call` para observar, curto-circuitar ou vetar uma chamada de ferramenta:
|
||||
|
||||
```python title="server.py" hl_lines="17-24"
|
||||
--8<-- "docs_src/extensions/tutorial005.py"
|
||||
```
|
||||
|
||||
* `params` é o `CallToolRequestParams` validado: você recebe `params.name` e `params.arguments` sem tocar em JSON cru. É também o que decide qual chamada de ferramenta é executada: passar um contexto reescrito por `call_next` muda o que o handler observa em `ctx`, não a invocação da ferramenta. Reescrita de requisição no nível do protocolo pertence ao [Middleware](middleware.md).
|
||||
* `call_next(ctx)` executa o resto da cadeia e retorna o resultado do handler. Retorne-o sem alterações (observar), retorne outra coisa (substituir) ou lance um `MCPError` (recusar). O que você retornar é serializado como qualquer resultado de handler, incluindo o carimbo de identidade `serverInfo` da era 2026, então um interceptador que curto-circuita nunca produz uma resposta anônima ou fora do schema.
|
||||
* Com várias extensões, os interceptadores se aninham na ordem de registro: a primeira extensão em `extensions=[...]` é a mais externa.
|
||||
* A implementação padrão é um repasse direto, e um servidor cujas extensões nunca sobrescrevem esse hook mantém o handler puro de `tools/call` intocado. Você não paga pelo que não usa.
|
||||
|
||||
O hook envolve `tools/call` e nada mais. Para preocupações que valem para toda mensagem, use o [Middleware](middleware.md). É para isso que ele serve.
|
||||
|
||||
## Usando uma extensão de cliente {#using-a-client-extension}
|
||||
|
||||
Uma **extensão de cliente** é o mesmo contrato visto do lado consumidor: um pacote de comportamento do lado do cliente reunido sob um único identificador. Passe as instâncias para `Client(extensions=[...])` e chame as ferramentas normalmente:
|
||||
|
||||
```python title="client.py" hl_lines="66-68"
|
||||
--8<-- "docs_src/extensions/tutorial006.py"
|
||||
```
|
||||
|
||||
`call_tool("buy", ...)` retorna um `CallToolResult` comum, como toda outra chamada. O que a extensão mudou: o servidor agora pode responder a `buy` com um **formato de resultado** `receipt` em vez de um resultado final, e `Receipts` o finaliza (aqui, resgatando o recibo com uma chamada seguinte) antes de `call_tool` retornar. Nada muda no ponto da chamada.
|
||||
|
||||
Tire a extensão e nada disso existe: a barreira do servidor recusa um cliente que não a declarou (erro -32021), e um formato reivindicado vindo de um servidor que pula a barreira falha na validação, exatamente como a especificação exige para um `resultType` não reconhecido. Desligado por padrão, nas duas pontas da conexão.
|
||||
|
||||
Para anunciar um identificador **sem** nenhum comportamento do lado do cliente (o servidor faz a barreira pela capacidade, o cliente não faz nada, como no cliente de busca acima), use `advertise()`:
|
||||
|
||||
```python
|
||||
from mcp.client import advertise
|
||||
|
||||
client = Client(mcp, extensions=[advertise("com.example/search")])
|
||||
```
|
||||
|
||||
## Escrevendo uma extensão de cliente {#writing-a-client-extension}
|
||||
|
||||
Herde de `ClientExtension` e sobrescreva apenas o que precisar. Três tipos de contribuição, cada um com um padrão: `settings()`, `claims()` e `notifications()`.
|
||||
|
||||
```python title="client.py" hl_lines="17-18 43-44 46-47"
|
||||
--8<-- "docs_src/extensions/tutorial006.py"
|
||||
```
|
||||
|
||||
* O identificador segue a mesma gramática do servidor, validada quando a classe é definida.
|
||||
* `claims()` retorna `ResultClaim`s: uma tag de protocolo, o modelo que a analisa e o resolvedor que a finaliza. O modelo precisa fixar a tag com `result_type: Literal["receipt"]` e não pode herdar dos tipos de resultado centrais do verbo; as duas coisas são impostas quando a claim é construída. Campos de fornecedor como `receipt_token` viajam pela conexão como estão: um formato substituído chega ao cliente literalmente.
|
||||
* O resolvedor recebe o modelo analisado e um `ClaimContext`; `ctx.session` é o mesmo handle público que `client.session`, então as chamadas seguintes são chamadas comuns de sessão. Ele retorna o `CallToolResult` normal do verbo.
|
||||
* `settings()` é o valor anunciado em `ClientCapabilities.extensions[identifier]`, lido uma vez na construção do `Client`.
|
||||
|
||||
`notifications()` declara notificações de servidor de fornecedor a observar:
|
||||
|
||||
```python
|
||||
def notifications(self) -> Sequence[NotificationBinding[Any]]:
|
||||
return [NotificationBinding(method="notifications/receipts", params_type=ReceiptEvent, handler=self.on_receipt)]
|
||||
```
|
||||
|
||||
O handler recebe parâmetros validados um de cada vez, na ordem de despacho. Ele observa; não pode vetar nem responder.
|
||||
|
||||
Duas regras discretas. As claims ficam ativas apenas em conexões 2026-07-28, e o anúncio de capacidade as acompanha: em uma conexão legada as claims se dissolvem e o identificador sai do anúncio junto com elas, então o cliente nunca anuncia uma extensão cujos formatos ele rejeitaria. E quando você mesmo quer o formato reivindicado em vez do resolvedor, chame `client.session.call_tool(..., allow_claimed=True)`; sem essa flag, um formato reivindicado que chega a um chamador no nível da sessão lança `UnexpectedClaimedResult`.
|
||||
|
||||
### Verbos de extensão {#extension-verbs}
|
||||
|
||||
Os métodos de requisição próprios de uma extensão não precisam de registro no lado do cliente. Um tipo de requisição de fornecedor herda de `mcp.types.Request` e passa por `client.session.send_request`, como em [Servindo seus próprios métodos](#serving-your-own-methods). Um acréscimo: quando uma chave de params precisa viajar no header `Mcp-Name` (especificações de extensão como tasks exigem isso para seus verbos), o tipo de requisição declara `name_param`:
|
||||
|
||||
```python title="client.py" hl_lines="22-25 46-47"
|
||||
--8<-- "docs_src/extensions/tutorial007.py"
|
||||
```
|
||||
|
||||
A sessão espelha `params["jobId"]` em `Mcp-Name` em todo caminho de envio, e um valor ausente falha de forma explícita em vez de omitir silenciosamente um header obrigatório.
|
||||
|
||||
## O que uma extensão não pode fazer {#what-an-extension-cannot-do}
|
||||
|
||||
A superfície de contribuição é **fechada** de propósito. No servidor: configurações, ferramentas, recursos, métodos, um interceptador de `tools/call`. No cliente: configurações, claims de resultado, bindings de notificação. Uma extensão não pode:
|
||||
|
||||
* **Alcançar o host.** Ela declara dados; não guarda nenhuma referência ao servidor nem ao cliente.
|
||||
* **Substituir comportamento central.** Métodos da especificação e tags de resultado centrais são rejeitados na construção (`initialize` é reservado pelo runner sem exceção); já um binding de notificação encoberto pelo vocabulário central fica em silêncio com um aviso.
|
||||
* **Registrar-se depois.** Depois que `MCPServer(...)` ou `Client(...)` retorna, o conjunto de extensões é o que é.
|
||||
|
||||
Se você está brigando com essas paredes, não está escrevendo uma extensão. Está escrevendo um fork. As paredes são a funcionalidade: um usuário que lê `extensions=[Apps(), Stamps()]` sabe *tudo* o que essas duas podem ter tocado.
|
||||
34
i18n/pt/pages/advanced/index.md
Normal file
34
i18n/pt/pages/advanced/index.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
translation:
|
||||
sections: [ca6988b7503cd2d3]
|
||||
tool: 1
|
||||
---
|
||||
# Avançado {#advanced}
|
||||
|
||||
Tudo o que um servidor ou cliente comum precisa tem seu lugar por assunto nas seções acima.
|
||||
Esta seção reúne as saídas de emergência a que você recorre quando a camada de conveniência
|
||||
do `MCPServer` atrapalha:
|
||||
|
||||
* **[O Server de baixo nível](low-level-server.md)**: a classe sobre a qual o `MCPServer` é construído.
|
||||
Schemas escritos à mão, handlers `on_*`, nada verificado para você e métodos JSON-RPC
|
||||
personalizados criados por você.
|
||||
* **[Paginação](pagination.md)** e **[Middleware](middleware.md)**: duas coisas que você
|
||||
*só* consegue fazer no `Server` de baixo nível.
|
||||
* **[Extensões](extensions.md)** e **[MCP Apps](apps.md)**: a superfície de
|
||||
extensão do protocolo. Componha pacotes de extensão em um servidor ou escreva os seus.
|
||||
|
||||
Algumas coisas que você poderia, com razão, procurar aqui ficam onde você de fato
|
||||
as usaria:
|
||||
|
||||
* **Autorização** está em **[Executando seu servidor](../run/index.md)**, porque você
|
||||
protege um servidor onde faz o deploy dele.
|
||||
* **OAuth**, **asserção de identidade**, conexão a **vários servidores** e o
|
||||
**cache** de respostas estão todos em **[Clientes](../client/index.md)**.
|
||||
* **Requisições com várias idas e voltas** e **Assinaturas** estão em
|
||||
**[Dentro do seu handler](../handlers/index.md)**, porque ambas são coisas que um
|
||||
handler *faz*.
|
||||
* **Templates de URI** está em **[Servidores](../servers/index.md)**, ao lado de Recursos.
|
||||
* **[Versões do protocolo](../protocol-versions.md)** e
|
||||
**[Funcionalidades descontinuadas](../deprecated.md)** têm, cada uma, sua própria página de nível superior.
|
||||
|
||||
Se você não tem certeza de que precisa desta seção, não precisa.
|
||||
207
i18n/pt/pages/advanced/low-level-server.md
Normal file
207
i18n/pt/pages/advanced/low-level-server.md
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
---
|
||||
translation:
|
||||
sections: [2c79b6338e09b7ac, 7edc43b3fae11314, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, b3530fcf4d11fd56, ebc33704fbd74262, cd0e9c933350390e]
|
||||
tool: 1
|
||||
---
|
||||
# O Server de baixo nível {#the-low-level-server}
|
||||
|
||||
`@mcp.tool()` é uma camada. Por baixo dela existe uma segunda classe de servidor, `Server`, que fala MCP cru: você entrega os objetos do protocolo e ela os coloca no fio, sem alterar nada.
|
||||
|
||||
O `MCPServer` é construído em cima dela. Você desce um nível quando a camada de conveniência atrapalha:
|
||||
|
||||
* Você precisa emitir um schema **exato** (carregado de um arquivo, gerado a partir de um banco de dados), não um derivado de uma assinatura Python.
|
||||
* Você precisa de controle total do resultado: `_meta`, `is_error`, cada chave de `structured_content`.
|
||||
* Você precisa tratar um método que o MCP não define.
|
||||
|
||||
Para todo o resto, fique no `MCPServer`.
|
||||
|
||||
## A mesma ferramenta, à mão {#the-same-tool-by-hand}
|
||||
|
||||
Esta é a ferramenta (tool) `search_books` que **[Ferramentas](../servers/tools.md)** escreve em nove linhas de `@mcp.tool()`, com o açúcar removido:
|
||||
|
||||
```python title="server.py" hl_lines="22 26 32"
|
||||
--8<-- "docs_src/lowlevel/tutorial001.py"
|
||||
```
|
||||
|
||||
Três coisas mudaram, e elas são a API de baixo nível inteira:
|
||||
|
||||
* **Os handlers são parâmetros do construtor.** `on_list_tools=` e `on_call_tool=` entram em `Server(...)`. Não há decoradores aqui embaixo, e todo handler tem o mesmo formato: `async (ctx, params) -> result`.
|
||||
* **Você escreve o schema de entrada.** `Tool.input_schema` é um `dict` JSON Schema comum. Ninguém o deriva de anotações de tipo, porque não há anotações de tipo de onde derivar.
|
||||
* **Você monta o resultado.** `CallToolResult(content=[TextContent(...)])`, à mão. Nada é encapsulado, convertido ou inferido de uma anotação de retorno.
|
||||
|
||||
`params` é a requisição já parseada: `CallToolRequestParams` dá `.name` e `.arguments`. `ctx` é um `ServerRequestContext`: `ctx.session` para falar de volta com o cliente, `ctx.lifespan_context`, `ctx.request_id` e `ctx.meta`, o `_meta` de entrada da requisição.
|
||||
|
||||
!!! info
|
||||
Se você já usou FastAPI, já conhece essa relação. O `MCPServer` é a camada de decoradores e anotações de tipo; o `Server` é o Starlette por baixo. Eles não são rivais: o `MCPServer` constrói um `Server` e registra nele handlers exatamente como esses.
|
||||
|
||||
### Experimente {#try-it}
|
||||
|
||||
Não existe Inspector para este aqui: `mcp dev` e `mcp run` só aceitam um `MCPServer`. O `Client` em memória não se importa; ele recebe um `Server` de baixo nível exatamente como recebe um `MCPServer`:
|
||||
|
||||
```python title="main.py"
|
||||
import asyncio
|
||||
|
||||
from mcp import Client
|
||||
|
||||
from server import server
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(server) as client:
|
||||
result = await client.call_tool("search_books", {"query": "dune", "limit": 5})
|
||||
print(result.content)
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
```text
|
||||
[TextContent(type='text', text="Found 3 books matching 'dune' (showing up to 5).", annotations=None, meta=None)]
|
||||
```
|
||||
|
||||
O mesmo texto que a versão com `@mcp.tool()` produziu. Duas diferenças honestas:
|
||||
|
||||
* `result.structured_content` é `None`. O servidor de alto nível encapsula um `-> str` em `{"result": ...}` para você; aqui ninguém monta o que você não montou.
|
||||
* `list_tools` retorna o schema que **você** digitou, caractere por caractere. A versão de alto nível tinha `"title": "Query"` em cada propriedade e um `"title": "search_booksArguments"` na raiz: artefatos do Pydantic. Aqui embaixo, se está no fio, foi você quem colocou lá.
|
||||
|
||||
## Nada é verificado por você {#nothing-is-checked-for-you}
|
||||
|
||||
O `MCPServer` rejeita um argumento ruim antes mesmo de a sua função executar, validando a chamada contra o schema que ele gerou (**[Ferramentas](../servers/tools.md)**).
|
||||
|
||||
O `Server` não faz isso. O seu `input_schema` é *anunciado* ao cliente; ele nunca é *aplicado* a `params.arguments`.
|
||||
|
||||
!!! check
|
||||
Chame `search_books` sem `limit` e o seu `args["limit"]` levanta `KeyError`. O cliente vê:
|
||||
|
||||
```text
|
||||
MCPError: Internal server error
|
||||
```
|
||||
|
||||
Um erro JSON-RPC, código `-32603`, com uma mensagem deliberadamente genérica: o SDK não vaza o seu traceback para um chamador remoto. O modelo nunca descobre o que fez de errado, então não consegue tentar de novo. (Em um teste, `raise_exceptions=True` expõe a exceção real; veja **[Testes](../get-started/testing.md)**.)
|
||||
|
||||
Isso se generaliza. Uma exceção levantada de um handler de baixo nível é **sempre** um erro de protocolo, nunca um resultado de ferramenta com `is_error=True`. Se você quer que o modelo leia a falha e se recupere, valide `params.arguments` você mesmo e retorne `CallToolResult(content=[TextContent(...)], is_error=True)`. Os dois tipos de falha são o assunto de **[Tratando erros](../servers/handling-errors.md)**.
|
||||
|
||||
## Duas ferramentas, um handler {#two-tools-one-handler}
|
||||
|
||||
`on_call_tool` é o único ponto de entrada para todas as ferramentas do servidor. Você roteia por `params.name`:
|
||||
|
||||
```python title="server.py" hl_lines="38-43"
|
||||
--8<-- "docs_src/lowlevel/tutorial002.py"
|
||||
```
|
||||
|
||||
* `list_tools` anuncia as duas. `call_tool` despacha pelo nome.
|
||||
* O ramo `else` importa: o `Server` encaminha sem reclamar um `tools/call` para um nome que você nunca listou direto para o seu handler. Levantar uma exceção ali transforma a chamada no mesmo `-32603` de cima.
|
||||
|
||||
## Saída estruturada, à mão {#structured-output-by-hand}
|
||||
|
||||
Declare `output_schema` na `Tool` e coloque `structured_content` no resultado. Os dois são seus:
|
||||
|
||||
```python title="server.py" hl_lines="19-23 36"
|
||||
--8<-- "docs_src/lowlevel/tutorial003.py"
|
||||
```
|
||||
|
||||
Chame e o resultado carrega as duas representações:
|
||||
|
||||
```json
|
||||
{
|
||||
"content": [{"type": "text", "text": "Found 3 books matching 'dune'."}],
|
||||
"structuredContent": {"matches": 3, "query": "dune"},
|
||||
"isError": false,
|
||||
"resultType": "complete",
|
||||
"_meta": {"io.modelcontextprotocol/serverInfo": {"name": "Bookshop", "version": "2.0.0"}}
|
||||
}
|
||||
```
|
||||
|
||||
O bloco `_meta` é o carimbo de identidade do servidor: o SDK o adiciona a todo resultado da era 2026, com a `version` vinda do construtor (um servidor que não define nenhuma reporta uma string vazia). Um servidor que não deve se identificar pode remover a chave com um middleware, que é dono dos resultados que retorna.
|
||||
|
||||
O servidor nunca compara os dois campos. O `Client` deste SDK compara: retorne um `structured_content` que não satisfaz o `output_schema` que você declarou e `call_tool` levanta um `RuntimeError` que começa com `Invalid structured content returned by tool search_books` e segue citando a falha do `jsonschema`. Prometer um schema é barato; cumprir a promessa é com você. A escada inteira de tipos de retorno e schemas está em **[Saída estruturada](../servers/structured-output.md)**.
|
||||
|
||||
## `_meta`: para a aplicação, não para o modelo {#\_meta-for-the-application-not-the-model}
|
||||
|
||||
`content` é a parte da resposta que o modelo lê. `structured_content` é a mesma resposta como dados tipados. `_meta` é o terceiro canal: dados que viajam junto com o resultado para a **aplicação cliente**, sem fazer parte da resposta de forma alguma.
|
||||
|
||||
Use para IDs de registro, IDs de trace, qualquer coisa de que a sua UI precisa e o seu prompt não:
|
||||
|
||||
```python title="server.py" hl_lines="37"
|
||||
--8<-- "docs_src/lowlevel/tutorial004.py"
|
||||
```
|
||||
|
||||
* Você o constrói como `_meta=`, o nome no fio. O cliente o lê de volta como `result.meta`.
|
||||
* Use namespace nas suas chaves (`bookshop/record_ids`). As chaves `io.modelcontextprotocol/*` são reservadas pelo protocolo.
|
||||
|
||||
!!! warning
|
||||
`_meta` é uma convenção entre você e a aplicação cliente, não uma garantia sobre o que chega
|
||||
ao modelo. O host decide o que renderiza. Nunca coloque um segredo em nenhuma parte de um resultado de ferramenta.
|
||||
|
||||
## As capacidades seguem os seus handlers {#capabilities-follow-your-handlers}
|
||||
|
||||
Um `Server` anuncia exatamente as famílias de métodos para as quais você deu handlers. O `Bookshop` acima passa `on_list_tools` e `on_call_tool` e nada mais, então um cliente que se conecta a ele vê:
|
||||
|
||||
```json
|
||||
{"tools": {"listChanged": false}}
|
||||
```
|
||||
|
||||
Sem `resources`, sem `prompts`: não há nada que os sustente. Passe `on_list_prompts` e `prompts` aparece; passe `on_completion` e `completions` aparece.
|
||||
|
||||
O `MCPServer` sempre anuncia ferramentas, recursos e prompts, tenha você registrado algum ou não, porque os seus managers sempre existem. Aqui embaixo a declaração *é* a chamada ao construtor.
|
||||
|
||||
## O genérico do lifespan {#the-lifespan-generic}
|
||||
|
||||
O `Server` é genérico no tipo que o seu lifespan produz. Anote uma vez e o objeto fica tipado em todo lugar onde aparece:
|
||||
|
||||
```python title="server.py" hl_lines="24-26 44-45 50"
|
||||
--8<-- "docs_src/lowlevel/tutorial005.py"
|
||||
```
|
||||
|
||||
* O lifespan é um `Callable[[Server[Catalog]], AbstractAsyncContextManager[Catalog]]`; `@asynccontextmanager` em um gerador `async` dá exatamente isso.
|
||||
* O que quer que ele produza com `yield` vira `ctx.lifespan_context`, e como os handlers são anotados com `ServerRequestContext[Catalog]`, `.search(...)` tem autocompletar e passa na checagem de tipos.
|
||||
* Ele é aberto uma vez quando o servidor inicia e fechado uma vez quando para. Inicialização, encerramento e a versão do `MCPServer` da mesma ideia estão em **[Lifespan](../handlers/lifespan.md)**.
|
||||
|
||||
Sem um `lifespan=`, `ctx.lifespan_context` é um `dict` vazio.
|
||||
|
||||
## Um método só seu {#a-method-of-your-own}
|
||||
|
||||
O construtor cobre os métodos que o MCP define. `add_request_handler` cobre todo o resto:
|
||||
|
||||
```python title="server.py" hl_lines="35-36 39-40 43-44 48"
|
||||
--8<-- "docs_src/lowlevel/tutorial006.py"
|
||||
```
|
||||
|
||||
* O primeiro argumento é a string do método. Notificações têm um irmão gêmeo, `add_notification_handler`.
|
||||
* `params_type` é o modelo contra o qual os `params` recebidos são validados **antes** de o seu handler executar, então métodos personalizados *recebem* a validação que as ferramentas não recebem. Faça subclasse de `RequestParams` para que o campo `_meta` seja parseado como o de qualquer outro método.
|
||||
* O handler retorna um `BaseModel`, um `dict` ou `None`. O SDK serializa isso no resultado JSON-RPC.
|
||||
|
||||
Uma ressalva honesta: o `Client` de alto nível só tem verbos para os métodos que o MCP define, então não existe `client.reindex()`. Um método de fornecedor é para um par que já sabe que ele existe: um cliente que você também distribui, ou outro serviço seu falando JSON-RPC.
|
||||
|
||||
Um método que você não pode reivindicar:
|
||||
|
||||
```text
|
||||
ValueError: 'initialize' is handled by the server runner and cannot be overridden;
|
||||
use Server.middleware to observe or wrap initialization
|
||||
```
|
||||
|
||||
O handshake pertence ao runner. `server/discover`, `ping` e todos os outros embutidos são seus para substituir.
|
||||
|
||||
!!! tip
|
||||
`Server.middleware`, mencionado naquele erro, envolve **toda** mensagem de entrada, inclusive `initialize`. Se o que você quer é observar ou reescrever o tráfego em vez de responder a um método novo, comece por **[Middleware](middleware.md)**.
|
||||
|
||||
## Os outros handlers {#the-other-handlers}
|
||||
|
||||
Cada um destes é uma ideia para a qual você já tem o vocabulário; cada um tem sua própria página.
|
||||
|
||||
* `on_call_tool`, `on_get_prompt` e `on_read_resource` podem retornar um `InputRequiredResult` em vez do resultado normal para pausar a chamada e pedir entrada ao cliente; veja **[Requisições de múltiplas idas e voltas](../handlers/multi-round-trip.md)**. Fiel a este nível, nada é instalado para você: enquanto o `MCPServer` sela o `requestState` por padrão, aqui o `request_state` que você define atravessa o fio exatamente como foi escrito até você optar com `server.middleware.append(RequestStateBoundary(RequestStateSecurity(keys=[...]), default_audience=server.name))`: uma linha (os dois nomes são importados de `mcp.server.request_state`) para a mesma selagem e verificação que o `MCPServer` faz (**[Protegendo o `requestState`](../handlers/multi-round-trip.md#protecting-requeststate)**).
|
||||
* `on_list_resources`, `on_read_resource`, `on_list_prompts`, `on_get_prompt`, `on_completion` têm o mesmo formato `(ctx, params) -> result` para as outras primitivas.
|
||||
* `on_subscriptions_listen` serve o stream `subscriptions/listen` de 2026-07-28. Passe um `ListenHandler` construído sobre um `SubscriptionBus` e publique eventos no bus a partir dos seus outros handlers; veja **[Assinaturas](../handlers/subscriptions.md)** para a composição completa.
|
||||
* `server.streamable_http_app()` retorna o mesmo app Starlette que o do `MCPServer`; faça o deploy dele do jeito que **[Executando o seu servidor](../run/index.md)** faz o deploy de qualquer outro app ASGI. Não existe `server.run(transport=...)` aqui embaixo: `server.run(read_stream, write_stream, server.create_initialization_options())` conduz uma conexão sobre um par de streams, e essa única linha é a história completa.
|
||||
|
||||
## Recapitulando {#recap}
|
||||
|
||||
* O `Server` de baixo nível recebe os seus handlers como **parâmetros do construtor** `on_*`; todo handler é `async (ctx, params) -> result`.
|
||||
* Você escreve o dict `input_schema` e você monta o `CallToolResult`. Nada é derivado, encapsulado ou validado por você.
|
||||
* Uma exceção em um handler é um erro de protocolo `-32603`. Um erro de ferramenta que o modelo consegue ler é um `CallToolResult` com `is_error=True` que **você** retorna.
|
||||
* O `_meta` no resultado é endereçado à aplicação cliente, não ao modelo.
|
||||
* `Server[T]` é genérico no que o seu lifespan produz; `ctx.lifespan_context` é um `T` tipado.
|
||||
* `add_request_handler(method, params_type, handler)` serve qualquer método. `initialize` é reservado.
|
||||
* As capacidades que um `Server` anuncia são derivadas de quais handlers você registrou.
|
||||
|
||||
`Client(server)` tratou os dois servidores de forma idêntica porque eles *são* o mesmo protocolo, e essa é justamente a ideia. A próxima camada abaixo nem é uma classe: é **[Middleware](middleware.md)**.
|
||||
123
i18n/pt/pages/advanced/middleware.md
Normal file
123
i18n/pt/pages/advanced/middleware.md
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
---
|
||||
translation:
|
||||
sections: [6048b4f308edbb8c, 068bda0f21ee9c1b, c3e565b61acd75c5, c62422b159c6ed09, 47204fab253cc45c]
|
||||
tool: 1
|
||||
---
|
||||
# Middleware {#middleware}
|
||||
|
||||
Um **middleware** é uma única função async que envolve toda mensagem que o seu servidor recebe.
|
||||
|
||||
Você o escreve como `async (ctx, call_next)` e o adiciona ao fim de `server.middleware`. A API inteira é essa.
|
||||
|
||||
!!! warning
|
||||
A lista de middlewares está marcada como **provisória** no código-fonte: a assinatura e a
|
||||
semântica podem mudar em uma versão minor 2.x. Use-a para *observar* (tempo, logs, tracing) e
|
||||
para *recusar* mensagens; não faça dela o alicerce sobre o qual o seu servidor se apoia.
|
||||
|
||||
`MCPServer` recebe a lista na construção (`MCPServer(name, middleware=[...])`) e a expõe como
|
||||
`mcp.middleware`; o `Server` de baixo nível expõe a mesma lista como `server.middleware`. O exemplo
|
||||
abaixo usa o `Server` de baixo nível; se `Server(name, on_call_tool=...)` é novidade para você, leia
|
||||
**[O Server de baixo nível](low-level-server.md)** primeiro.
|
||||
|
||||
## Um middleware de medição de tempo {#a-timing-middleware}
|
||||
|
||||
Um servidor, uma ferramenta, um middleware que registra no log quanto tempo cada mensagem levou:
|
||||
|
||||
```python title="server.py" hl_lines="39-45 49"
|
||||
--8<-- "docs_src/middleware/tutorial001.py"
|
||||
```
|
||||
|
||||
* `ctx` é o mesmo `ServerRequestContext` que os seus handlers recebem. `ctx.method` é a string
|
||||
bruta do método; `ctx.params` são os params brutos, **antes** de qualquer validação.
|
||||
* `call_next(ctx)` executa o restante da cadeia: a validação, a busca do handler, o seu handler.
|
||||
Retorne o que ele retornou e a resposta fica intacta.
|
||||
* O `try`/`finally` é proposital: um handler que lança exceção ainda é cronometrado, porque a falha
|
||||
chega ao seu middleware como a exceção que sai de `call_next`.
|
||||
* `server.middleware.append(...)` faz o registro. A lista executa do mais externo para o mais
|
||||
interno, então `middleware[0]` é o que fica mais perto do fio.
|
||||
|
||||
### Experimente {#try-it}
|
||||
|
||||
Conecte um cliente, liste as ferramentas, chame uma. O seu log tem **três** linhas:
|
||||
|
||||
```text
|
||||
server/discover took 18.3 ms
|
||||
tools/list took 0.1 ms
|
||||
tools/call took 0.1 ms
|
||||
```
|
||||
|
||||
Você fez duas chamadas e recebeu três linhas. A primeira é `server/discover`: a requisição que o
|
||||
cliente enviou para estabelecer a conexão, antes de você pedir qualquer coisa.
|
||||
|
||||
É justamente esse o ponto. O middleware envolve **toda** mensagem de entrada:
|
||||
|
||||
* O estabelecimento da conexão: `server/discover`, ou `initialize` e `notifications/initialized`
|
||||
em uma sessão legada.
|
||||
* Toda requisição e toda notificação. Para uma notificação, `ctx.request_id is None`,
|
||||
`call_next(ctx)` retorna `None` e o que quer que você retorne é descartado.
|
||||
* Até um método para o qual o servidor não tem handler: `call_next` lança o
|
||||
`MCPError(-32601, "Method not found")` *através* do seu middleware a caminho do cliente.
|
||||
|
||||
## O que você pode fazer dentro de um {#what-you-can-do-inside-one}
|
||||
|
||||
Em ordem crescente do quanto você deveria hesitar:
|
||||
|
||||
* **Observar.** Cronometre, conte, registre no log. O exemplo acima.
|
||||
* **Recusar.** Lance um `MCPError` *em vez de* chamar `call_next(ctx)` e essa única mensagem é
|
||||
respondida com um erro JSON-RPC. A conexão continua de pé; a próxima mensagem passa. É assim
|
||||
que um servidor controla o acesso a `subscriptions/listen` por chamador:
|
||||
**[Decidindo quem pode observar](../handlers/subscriptions.md#deciding-who-may-watch)**, na
|
||||
página de Assinaturas, percorre o passo a passo.
|
||||
* **Reescrever.** `ctx` é uma dataclass: `await call_next(dataclasses.replace(ctx, params=...))`
|
||||
entrega ao restante da cadeia params diferentes dos que o cliente enviou. Nunca faça isso com
|
||||
`initialize`: o resultado que o cliente recebe de volta é construído a partir dos seus params
|
||||
reescritos, mas o servidor grava o estado da conexão a partir dos params originais do fio. Os
|
||||
dois lados podem terminar o handshake discordando sobre o que negociaram.
|
||||
* **Responder.** Retorne um resultado sem chamar `call_next(ctx)` e ele vai para o cliente como a
|
||||
sua resposta. `call_next` entrega a você a forma final do fio, e o pipeline nunca altera o que
|
||||
você retorna, então o envelope inteiro é seu: em uma conexão da era 2026 isso inclui o carimbo
|
||||
`_meta` de `serverInfo`, que o SDK adiciona aos resultados dos handlers, mas não aos seus.
|
||||
|
||||
!!! check
|
||||
`initialize` é uma das coisas que o middleware envolve, e é o *único* gancho que você tem
|
||||
para ele. Tente assumi-lo com `add_request_handler` e o SDK recusa:
|
||||
|
||||
```text
|
||||
ValueError: 'initialize' is handled by the server runner and cannot be overridden;
|
||||
use Server.middleware to observe or wrap initialization
|
||||
```
|
||||
|
||||
!!! warning
|
||||
`initialize` é tratado inline: o servidor não lê mais nenhuma mensagem de entrada até a sua
|
||||
cadeia de middlewares retornar. Aguardar uma requisição do servidor para o cliente
|
||||
(`ctx.session.send_request(...)`, uma elicitação (elicitation)) enquanto trata `initialize`,
|
||||
portanto, **trava a conexão em deadlock**: a resposta que você está esperando nunca poderá ser
|
||||
lida. Notificações do tipo fire-and-forget não têm problema.
|
||||
|
||||
## O único middleware que já vem ligado por padrão {#the-one-middleware-that-ships-on-by-default}
|
||||
|
||||
O SDK traz exatamente um middleware, e ele já está na lista do seu servidor: o que emite um span
|
||||
do OpenTelemetry para cada mensagem. Você não o adiciona, e na maior parte do tempo nem pensa
|
||||
nele. Ele é um no-op até você instalar um exportador, e tem a própria página:
|
||||
**[OpenTelemetry](../run/opentelemetry.md)**.
|
||||
|
||||
!!! info
|
||||
Se você já escreveu middleware ASGI, já conhece esse formato. O `(scope, receive, send)`
|
||||
do Starlette virou `(ctx, call_next)`, e ele executa *depois* do transporte, sobre a mensagem
|
||||
decodificada em vez da requisição HTTP bruta. Os dois se compõem: o middleware do Starlette
|
||||
em `streamable_http_app()` enxerga HTTP; este enxerga MCP.
|
||||
|
||||
## Recapitulando {#recap}
|
||||
|
||||
* Um middleware é `async (ctx, call_next) -> result`, passado como `MCPServer(middleware=[...])` (ou
|
||||
adicionado a `mcp.middleware`) e adicionado a `server.middleware` no `Server` de baixo nível.
|
||||
* Ele envolve **toda** mensagem de entrada (`server/discover`, `initialize`, requisições,
|
||||
notificações, métodos desconhecidos) e executa do mais externo para o mais interno.
|
||||
* `ctx.request_id is None` é como você distingue uma notificação de uma requisição.
|
||||
* Lance uma exceção em vez de chamar `call_next` para recusar uma mensagem; a conexão sobrevive.
|
||||
* O tracing do OpenTelemetry do próprio SDK também é um middleware, já na lista. Veja
|
||||
**[OpenTelemetry](../run/opentelemetry.md)**.
|
||||
* Toda essa superfície é provisória. Observe com ela; não construa em cima dela.
|
||||
|
||||
Isso é tudo o que envolve uma requisição. **[Autorização](../run/authorization.md)** é o que decide
|
||||
se a requisição chega a ser executada.
|
||||
85
i18n/pt/pages/advanced/pagination.md
Normal file
85
i18n/pt/pages/advanced/pagination.md
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
---
|
||||
translation:
|
||||
sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9]
|
||||
tool: 1
|
||||
---
|
||||
# Paginação {#pagination}
|
||||
|
||||
A maioria dos servidores nunca precisa disso.
|
||||
|
||||
O `MCPServer` responde a toda requisição `list_*` com tudo o que tem, em uma única página, `next_cursor=None`. Para algumas dezenas de ferramentas, recursos ou prompts, essa é a resposta certa e não há nada para configurar.
|
||||
|
||||
A paginação é para o servidor cuja lista de recursos é, na verdade, um banco de dados: milhares de linhas que ele se recusa a serializar em uma única resposta. A resposta do protocolo é um **cursor**: o servidor retorna uma página mais um token opaco, e o cliente envia esse token de volta para obter a próxima página.
|
||||
|
||||
O `@mcp.resource()` não tem nenhum gancho para isso. Para paginar, você mesmo escreve o handler de listagem, no **[Server de baixo nível](low-level-server.md)**.
|
||||
|
||||
## Um servidor que pagina {#a-server-that-pages}
|
||||
|
||||
```python title="server.py" hl_lines="12 15-16"
|
||||
--8<-- "docs_src/pagination/tutorial001.py"
|
||||
```
|
||||
|
||||
* Em um `Server` de baixo nível, os handlers são argumentos do construtor, não decoradores. O `on_list_resources` responde a toda requisição `resources/list`; a ligação inteira é essa.
|
||||
* Todo handler paginado é tipado como `params: PaginatedRequestParams | None`, e o exemplo aceita os dois. Em uma conexão, porém, o SDK nunca entrega `None` para você (uma requisição sem o membro `params` chega ao handler como o modelo com seus valores padrão), então o sinal que importa é `params.cursor is None`: **comece do início**.
|
||||
* Você decide o que um cursor *é*. Aqui é um offset representado como string. Um timestamp, uma chave primária, um blob em base64: qualquer coisa que você consiga gerar na saída e reconhecer na volta.
|
||||
* `next_cursor=None` é como você diz "essa foi a última página". Não há contagem, nem total, nem `has_more`. O `None` é o sinal inteiro.
|
||||
|
||||
!!! tip
|
||||
Um `PAGE_SIZE` de 10 deixa o exemplo legível. Escolha o seu por endpoint: uma lista de
|
||||
recursos de uma linha comporta uma página de 500; uma lista de templates de prompt pesados, não.
|
||||
O cliente não tem voz nisso, e é assim de propósito.
|
||||
|
||||
### Experimente {#try-it}
|
||||
|
||||
`Client(server)` se conecta a um `Server` de baixo nível em memória exatamente como se conecta a um `MCPServer`.
|
||||
|
||||
Chame `list_resources()` sem argumentos. Você recebe dez recursos, de `book-1` a `book-10`, e `next_cursor` é a string `"10"`.
|
||||
|
||||
Devolva-a com `list_resources(cursor="10")` e o primeiro recurso é `book-11`, o novo `next_cursor` é `"20"`.
|
||||
|
||||
A décima página volta com `next_cursor` definido como `None`. Pronto.
|
||||
|
||||
## O loop do cliente {#the-client-loop}
|
||||
|
||||
Todo método `list_*` do `Client` (`list_tools`, `list_resources`, `list_resource_templates`, `list_prompts`) aceita a palavra-chave `cursor=`. Esgotar uma lista paginada é um único `while True`:
|
||||
|
||||
```python title="client.py" hl_lines="26-32"
|
||||
--8<-- "docs_src/pagination/tutorial002.py"
|
||||
```
|
||||
|
||||
* `cursor` começa como `None`, então a primeira requisição não carrega nenhum cursor.
|
||||
* Acumule **antes** de olhar para `next_cursor`: a última página também tem recursos.
|
||||
* `next_cursor is None` é a saída. Qualquer outra coisa volta direto para `cursor=`, intocada.
|
||||
|
||||
Execute o `main()` dele e ele imprime `100 resources`: dez páginas de dez, costuradas por um loop que nunca soube que havia dez páginas.
|
||||
|
||||
Este é o mesmo loop que **[O cliente](../client/index.md)** mostra para todo verbo `list_*`, e ele não custa nada contra um servidor que não pagina: `next_cursor` é `None` na primeira resposta e o loop roda uma vez.
|
||||
|
||||
## As três regras {#the-three-rules}
|
||||
|
||||
**Cursores são opacos.** Um cliente nunca deve interpretar, construir ou adivinhar um. A única fonte legítima de um cursor é o `next_cursor` da página anterior, literalmente.
|
||||
|
||||
**O servidor escolhe o tamanho da página.** Não existe `limit=` no protocolo. Se você precisa de um tamanho de página diferente, altere o servidor.
|
||||
|
||||
**Um cliente que ignora a paginação continua funcionando.** Ele chama `list_resources()` uma vez, recebe os dez primeiros e nunca percebe o `next_cursor` que jogou fora. Nada quebra; ele só vê menos.
|
||||
|
||||
!!! check
|
||||
Opaco quer dizer opaco. Invente um cursor (`list_resources(cursor="page-2")`) e não há
|
||||
nada que o protocolo possa fazer por você. Este servidor tenta `int("page-2")`, o handler lança uma exceção,
|
||||
e o que volta para o cliente é:
|
||||
|
||||
```text
|
||||
MCPError(-32603, 'Internal server error', None)
|
||||
```
|
||||
|
||||
Um cursor que você não recebeu do servidor é um bug, não um pedido de funcionalidade.
|
||||
|
||||
## Recapitulando {#recap}
|
||||
|
||||
* O `MCPServer` retorna tudo em uma página. A paginação é opcional, e você opta por ela no `Server` de baixo nível.
|
||||
* `on_list_resources` (e `on_list_tools`, `on_list_prompts`, `on_list_resource_templates`) recebe `PaginatedRequestParams | None`; `params.cursor` é `None` na primeira página.
|
||||
* Você retorna uma página mais `next_cursor`: qualquer string que você vá reconhecer depois, ou `None` quando não sobrar nada.
|
||||
* O loop do cliente: passe `cursor=`, acumule, repita até `next_cursor is None`.
|
||||
* Cursores são opacos, o servidor é dono do tamanho da página, e um cliente que não pagina ainda recebe a primeira página.
|
||||
|
||||
O restante da API do `Server` escrita à mão (`on_call_tool`, dicts `input_schema`, `_meta`) está em **[O Server de baixo nível](low-level-server.md)**.
|
||||
Loading…
Reference in a new issue