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