docs: publish translated docs in twelve languages and the tool that maintains them (#3280)

This commit is contained in:
Max 2026-08-14 17:07:34 +01:00 • committed by GitHub
commit 52ad0a8876
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 89737 additions and 54 deletions

307
i18n/es/glossary.json Normal file
View file

@ -0,0 +1,307 @@
{
"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": "herramienta",
"note": "MCP protocol noun (a server exposes tools), feminine: la herramienta / las herramientas. Standard rendering. Wire identifiers such as `tools/call` and the `@mcp.tool()` decorator are code and stay untouched; the Inspector's **Tools** tab is a UI label and stays English."
},
{
"source": "resource",
"target": "recurso",
"note": "MCP protocol noun (data a server exposes for reading) and the general noun alike, masculine: el recurso / los recursos. Standard rendering. `resources/read` and `@mcp.resource()` are code."
},
{
"source": "prompt",
"target": "prompt",
"note": "The MCP feature (a reusable prompt template a server exposes) and the everyday AI sense; kept in English as Spanish AI writing does, masculine: el prompt / los prompts. Provisional pending native review: indicación / instrucción are not used for the MCP noun. `prompts/get` and `@mcp.prompt()` are code."
},
{
"source": "sampling",
"target": "muestreo",
"note": "The (deprecated) client feature that lets a server borrow the client's model for a completion, masculine: el muestreo. Provisional pending native review; first mention in a page body reads \"muestreo (sampling)\" so the reader can map it to `sampling/createMessage`, which is code. Keeping the English word is the open alternative."
},
{
"source": "roots",
"target": "roots",
"note": "The (deprecated) client feature listing the workspace directories a client exposes; kept in English because the reader meets it as `roots/list`, masculine plural: los roots. Provisional pending native review; may take the gloss \"roots (directorios raíz)\" on first mention. raíces is the open alternative; a `Root` object in code font stays Latin."
},
{
"source": "elicitation",
"target": "elicitación",
"note": "OPEN QUESTION for native review: the server asking the user a question mid-request through the client. Provisionally the software-engineering loan elicitación (as in elicitación de requisitos), feminine, glossed on first mention per page — elicitación (elicitation). Alternatives a reviewer may prefer: consulta al usuario, petición de datos al usuario. `elicitation/create` and `ctx.elicit()` are code."
},
{
"source": "capability",
"target": "capacidad",
"note": "What client and server declare during initialization (\"capability negotiation\" → negociación de capacidades), feminine. Not habilidad; and funcionalidad / característica mean a feature, which is a different thing. The `capabilities` field and keys such as `sampling.tools` are code."
},
{
"source": "transport",
"target": "transporte",
"note": "The connection mechanism (\"every standard transport\" → todos los transportes estándar), masculine. The transport names stdio, Streamable HTTP and SSE are on the keep list: el transporte stdio."
},
{
"source": "session",
"target": "sesión",
"note": "An MCP session (the negotiated connection state), feminine: la sesión / las sesiones, ID de sesión. `session` objects, `ClientSession` and `ServerSession` are code."
},
{
"source": "handler",
"target": "handler",
"note": "The tool, resource or prompt function you register; kept in English as Spanish-speaking developers say it, masculine: el handler / los handlers (nav section \"Inside your handler\" → Dentro de tu handler). Provisional pending native review: manejador is the open alternative; controlador is not used because it also means controller and driver."
},
{
"source": "dependency",
"target": "dependencia",
"note": "Both package dependencies and the SDK's parameter-injection feature (the \"Dependencies\" page → Dependencias; \"dependency injection\" → inyección de dependencias), feminine. The `Resolve` marker class is code."
},
{
"source": "resolver",
"target": "resolutor",
"note": "The plain function attached with `Resolve(...)` that computes or asks for a parameter's value before the tool runs. Rendered as the noun resolutor (masculine) because resolver is a Spanish verb and \"el resolver\" misreads. OPEN QUESTION for native review: resolvedor and función de resolución are the alternatives. The `Resolve` class stays Latin."
},
{
"source": "client",
"target": "cliente",
"note": "An MCP client and the client side of a connection, masculine: el cliente. The `Client` class and the `mcp.client` module are code and stay in English."
},
{
"source": "server",
"target": "servidor",
"note": "An MCP server (the program you build), masculine: el servidor. The `MCPServer`, `Server` and `ServerSession` classes are code."
},
{
"source": "host",
"target": "host",
"note": "The MCP host — the application the user talks to (Claude Desktop, an IDE, an agent runtime) — and a network host alike; kept in English as Spanish networking usage does, masculine: el host / los hosts. Provisional pending native review: anfitrión is not used for the MCP role."
},
{
"source": "context",
"target": "contexto",
"note": "The generic lower-case word (\"provide context to LLMs\" → proporcionar contexto a los LLM), masculine. The capitalised `Context` is the SDK object injected as `ctx`; it is on the keep list and stays Latin (\"The Context\" → El Context, la clase `Context`)."
},
{
"source": "request",
"target": "solicitud",
"note": "A JSON-RPC or HTTP request (\"the initialize request\" → la solicitud de inicialización), feminine. Provisional pending native review: petición is equally understood but use solicitud throughout rather than alternating; the raw English \"request\" is developer speech, not used in prose here. `Request` types in code font stay Latin."
},
{
"source": "response",
"target": "respuesta",
"note": "A JSON-RPC or HTTP response, feminine: la respuesta. `Response` types in code font stay Latin."
},
{
"source": "callback",
"target": "callback",
"note": "Client callbacks and OAuth redirect callbacks alike; kept in English, masculine: el callback / los callbacks. Provisional pending native review: retrollamada (the Python documentation's term) and devolución de llamada (vendor style) are not used. Parameter names such as `sampling_callback` are code."
},
{
"source": "decorator",
"target": "decorador",
"note": "The Python decorators the SDK is built on, masculine: el decorador. Standard rendering in the Spanish Python documentation. `@mcp.tool()` and its siblings are code and stay untouched."
},
{
"source": "type hint",
"target": "anotación de tipo",
"note": "Python type hints (\"from your type hints\" → a partir de tus anotaciones de tipo), feminine; the plural goes on anotación, tipo stays singular. Provisional pending native review: the Spanish Python documentation also uses indicador de tipo; pin anotación de tipo here and do not leave \"type hints\" in English prose. `type hints` inside code font is code."
},
{
"source": "notification",
"target": "notificación",
"note": "A JSON-RPC notification (a message with no response) and the change notifications a server publishes, feminine: la notificación / las notificaciones. `notifications/...` method strings are code."
},
{
"source": "round trip",
"target": "ida y vuelta",
"note": "One request-and-response exchange (\"zero negotiation round trips\" → cero idas y vueltas de negociación; \"round-trip time\" → tiempo de ida y vuelta), feminine. Provisional pending native review."
},
{
"source": "multi-round-trip",
"target": "de varias idas y vueltas",
"note": "The 2026-07-28 request pattern (\"Multi-round-trip requests\" → Solicitudes de varias idas y vueltas). Provisional coinage pending native review: gloss the English on first mention per page — solicitudes de varias idas y vueltas (multi-round-trip). The abbreviation MRTR stays Latin."
},
{
"source": "lifespan",
"target": "lifespan",
"note": "The server's startup/shutdown scope and its `lifespan=` parameter; kept in English so the prose matches the parameter name, masculine: el lifespan. Provisional pending native review; may take the gloss \"lifespan (ciclo de vida del servidor)\" on first mention. Not vida útil; and esperanza de vida (life expectancy) is never the sense. The neighbouring word \"lifetime\" (\"for the lifetime of the app\") may render as durante toda la vida de la app.",
"avoid": ["esperanza de vida"]
},
{
"source": "back-channel",
"target": "canal de retorno",
"note": "This documentation's term for the server calling back into the client during a request, which exists only on legacy connections; masculine. Provisional pending native review: gloss the English on first mention per page — canal de retorno (back-channel) — so the reader can connect it to `NoBackChannelError`, which is code."
},
{
"source": "deprecated",
"target": "obsoleto",
"note": "Advisory status: still works, scheduled for removal later — obsoleto / obsoleta (agreeing), as the Spanish Python documentation says (\"Deprecated features\" → Funcionalidades obsoletas; \"deprecation warning\" → aviso de obsolescencia); \"removed\" is eliminado. Provisional pending native review: the calque deprecado is widespread in speech but not used here; en desuso is understood but pin obsoleto. `MCPDeprecationWarning` is code."
},
{
"source": "legacy",
"target": "heredado",
"note": "\"A legacy connection / client\" = one negotiated at spec version 2025-11-25 or earlier → una conexión heredada, un cliente heredado (\"Serving legacy clients\" → Atender clientes heredados). Standard vendor rendering, understood everywhere; do not leave \"legacy\" in English prose. Provisional pending native review."
},
{
"source": "era",
"target": "generación",
"note": "\"Protocol era\" (\"a 2025-era client\", \"whatever era the client speaks\") → la generación del protocolo, un cliente de la generación 2025. Provisional pending native review; not the literal era or época."
},
{
"source": "handshake",
"target": "handshake",
"note": "The initialization handshake (\"the classic handshake\" → el handshake clásico), kept in English as Spanish networking usage mostly does, masculine. Provisional pending native review: negociación inicial is the descriptive alternative; the literal apretón de manos appears in some vendor texts but is not used here."
},
{
"source": "wire",
"target": "canal",
"note": "The corpus's light metaphor for the byte stream between client and server. Render the meaning, not the picture: \"stdout is the wire\" → stdout es el canal; \"invisible on the wire\" → no aparece en lo que se transmite; \"the JSON on the wire\" → el JSON que realmente se transmite. Provisional pending native review. Never the literal alambre; cable only if a reviewer prefers it.",
"avoid": ["alambre"]
},
{
"source": "token",
"target": "token",
"note": "Kept in English for OAuth tokens (token de acceso, token de actualización) and LLM tokens alike, masculine: el token / los tokens. Standard."
},
{
"source": "schema",
"target": "esquema",
"note": "A JSON schema describing tool input or output (\"the input schema\" → el esquema de entrada), masculine. The proper name JSON Schema stays English; `inputSchema` / `outputSchema` are code."
},
{
"source": "default",
"target": "por defecto",
"note": "\"by default\" → por defecto; \"the default value\" → el valor por defecto; \"defaults to X\" → es X por defecto. Provisional pending native review: predeterminado is equally correct but pin one rendering; never leave \"default\" in English prose. `default=` in code font is code."
},
{
"source": "deploy",
"target": "desplegar",
"note": "Verb desplegar, noun despliegue (masculine): \"Deploy & scale\" → Desplegar y escalar; \"after deployment\" → tras el despliegue. Standard pan-Hispanic rendering; never the verbified deployar."
},
{
"source": "subscription",
"target": "suscripción",
"note": "Resource and list-change subscriptions (the \"Subscriptions\" pages → Suscripciones; \"subscribe\" → suscribirse), feminine. Spelled without the b (suscripción, not subscripción). `subscriptions/listen` and `resources/subscribe` are code."
},
{
"source": "completion",
"target": "autocompletado",
"note": "The MCP feature that suggests values for prompt and resource-template arguments (the \"Completions\" page → Autocompletado), masculine. It is argument autocompletion, not LLM text completion; when a page means the model's generated text (sampling), say la respuesta del modelo. `completion/complete` is code. Provisional pending native review."
},
{
"source": "library",
"target": "biblioteca",
"note": "A code library, feminine: la biblioteca (\"the standard library\" → la biblioteca estándar). The false friend librería means a bookshop and is never right for this sense. It is not on the banned list only because the corpus's running example server is a bookshop (\"Bookshop\"), for which librería would be the correct word if the English prose ever names it."
},
{
"source": "file",
"target": "archivo",
"note": "Masculine: el archivo. fichero is Spain-only usage and this single global variant never uses it; archivo is understood everywhere, Spain included.",
"avoid": ["fichero"]
},
{
"source": "computer",
"target": "computadora",
"note": "Rare in this corpus, which mostly says \"machine\" (→ la máquina: \"on your machine\" → en tu máquina) or can say el equipo. Where \"computer\" itself must be rendered, computadora is the pan-American form and is understood in Spain; ordenador is Spain-only and this global variant never uses it. Provisional pending native review.",
"avoid": ["ordenador"]
},
{
"source": "string",
"target": "cadena",
"note": "A text string (\"returns a string\" → devuelve una cadena), feminine, as the Spanish Python documentation writes it; cadena de texto where cadena alone could be ambiguous. `str` is code. Provisional pending native review: the raw \"string\" is common in speech but not used in prose here."
},
{
"source": "return",
"target": "devolver",
"note": "What a function or tool gives back (\"returns `3`\" → devuelve `3`; \"the return value\" → el valor devuelto / el valor de retorno). Provisional pending native review: retornar is understood everywhere and the Python documentation uses it too, but pin devolver so pages do not alternate. The `return` keyword is code."
},
{
"source": "raise",
"target": "lanzar",
"note": "Raising an exception (\"raises `ToolError`\" → lanza `ToolError`; \"an exception is raised\" → se lanza una excepción), as the Spanish Python documentation says. generar una excepción is understood but pin lanzar. The `raise` keyword is code."
},
{
"source": "async",
"target": "asíncrono",
"note": "The prose adjective (\"the async runtime\" → el entorno de ejecución asíncrono; \"an async callback\" → un callback asíncrono). Provisional pending native review: asincrónico is equally valid and more common in parts of the Americas; pin asíncrono for consistency. The `async` and `await` keywords in code font stay Latin."
},
{
"source": "Get started",
"target": "Empieza aquí",
"note": "The nav section that opens the guide, and the title of its index page. \"First steps\" is a separate page inside that section (Primeros pasos), so the two need distinct renderings or the sidebar shows the same title twice. Provisional pending native review; Cómo empezar and Introducción are the alternatives for the section."
},
{
"source": "First steps",
"target": "Primeros pasos",
"note": "The tutorial page inside the \"Get started\" section; never reuse this rendering for the section itself (see that entry). Provisional pending native review."
},
{
"source": "Recap",
"target": "Resumen",
"note": "Recurring section heading that closes most pages; one rendering everywhere, not Resumen on some pages and Recapitulación or En resumen on others. Provisional pending native review."
},
{
"source": "Try it",
"target": "Pruébalo",
"note": "Recurring section heading above a runnable example; one rendering everywhere (tú imperative with the clitic, accent kept). Provisional pending native review."
}
]
}

170
i18n/es/instructions.md Normal file
View file

@ -0,0 +1,170 @@
# Spanish (es) — translation instructions
Target language: Spanish (español), one global variant for Latin America
and Spain alike, directory and URL code `es`, page language tag `es`. This
file is sent verbatim with every translation request for this language, on
top of the shared rules in `../general-prompt.md`. The termbase in
`glossary.json` is sent alongside it and wins any terminology conflict.
## 1. Register
Write the relaxed, direct register of modern open-source documentation in
Spanish: professional, plain-spoken, addressed to a colleague.
- Address the reader as **tú**, always, with matching verb forms and
pronouns (puedes, tu servidor, te devuelve). Never usted (ejecute, su, le),
never vosotros or its forms (ejecutáis, vuestro, os), never voseo (podés,
tenés). Where the English "you" is plainly plural ("you and your team"),
the plural is **ustedes**, never vosotros.
- Steps are bare tú imperatives: "Install the SDK, then run the server" →
Instala el SDK y luego ejecuta el servidor — not Instale (usted), not Debes
instalar, no por favor before every step; prohibitions are no + subjunctive
(no llames, no uses). The authorial "we" is nosotros.
- Spanish drops the subject pronoun. Let the verb carry the person; write tú
only for contrast — two or three explicit tú on a page is a lot. "Your
server" is usually el servidor; tu servidor when ownership is the point.
Impersonal se constructions (se instala con uv) are welcome for describing
behaviour and keep a page from becoming a wall of imperatives, but a
third-person verb aimed at the reader is an usted form and is wrong.
- One page, one register: a page that drifts between tú and usted, or slips
in one vosotros or vos form, is wrong even when each sentence is fine.
- One global Spanish. Where regions differ, use the form understood
everywhere and pinned in the glossary (archivo, not fichero; computadora,
not ordenador); avoid words that are everyday in one region and odd in
another (vale, coger; accesar, checar). ejecutar, not correr, a program;
"enter a value" → escribe or indica (ingresar and introducir each read as
regional); "click **Tools**" → haz clic en **Tools**.
## 2. Voice
The English is warm, direct and confident: short sentences, second person,
the occasional one-line payoff ("That's the whole API."). Developer Spanish
carries that voice naturally; keep it, and keep the payoff lines short — Esa
es toda la API.
- Follow Spanish rhythm: split a long English sentence in two rather than
chaining clauses with commas; use plain connectives (así que, es decir,
por eso) where they help. Prefer concrete verbs to noun stacks (realizar la
ejecución de → ejecutar) and the active voice or pasiva refleja to a
calqued passive: "The tool is called by the model" → el modelo llama a la
herramienta, not la herramienta es llamada por el modelo.
- Keep the directness ("don't" is no uses, not quizá convenga evitar), skip
the hype, and avoid officialese: el presente documento, dicho / dicha
everywhere, el mismo as a pronoun, a nivel de, en base a, cabe destacar.
- Verbs are Spanish even when the noun is borrowed: never deployar, setear,
loguear, debuggear, testear, parsear, pushear — write desplegar, configurar,
registrar, depurar, probar, analizar, enviar; hacer un commit, hacer push.
- False friends and calques: librería for a code library (→ biblioteca),
eventualmente for "eventually" (→ con el tiempo), actual for "actual"
(→ real), soportar for "support" (→ admite / es compatible con), remover
(→ quitar / eliminar), asumir (→ suponer), the gerundio de posterioridad
("…, generando un error"), and "under the hood" → internamente, not bajo
el capó. retornar is understood everywhere, but this corpus pins devolver.
Example — English: "You don't construct it and you don't configure it. You
ask for it."
- Not this (usted, pronoun in every clause): Usted no lo construye y usted
no lo configura. Usted lo solicita.
- Not this either (voseo, slang, added emphasis): No lo construís ni lo
configurás. Lo pedís y listo, ¡facilísimo!
- This: No lo construyes ni lo configuras. Lo pides.
## 3. Humour and idioms
- The English is friendly and dry rather than jokey, and conversational
Spanish absorbs that easily; what needs work is idiom. Never translate a
pun, idiom or aside word for word: say what it means in a short natural
sentence in the same register. A widely understood turn of phrase (y
listo, sin más) is fine, a regional one is not; culture-bound references
take the plain meaning. An aside that carries no information may go —
never a technical caveat that happens to be phrased lightly.
- Recurring English tags get fixed renderings: "**[X](…)** has the whole
story" → **[X](…)** tiene todos los detalles; "The whole story is in
**[X](…)**" → Todos los detalles están en **[X](…)**; "That's the whole
API." / "That is the whole API." → Esa es toda la API.; "That's the whole
protocol." → Ese es todo el protocolo.; "That's it. It's just Python." →
Eso es todo. Es simplemente Python.; "You get `3` back. ✨" → Te devuelve
`3`. ✨; "Out of the box the app answers **only** requests addressed to
localhost." → Por defecto, la app responde **solo** a las solicitudes
dirigidas a localhost. — never recién sacada de la caja.
- Exclamation marks: English exports far more than Spanish prose wants. Keep
one only where the source is genuinely emphatic, always paired ¡…!, never
doubled, never in a heading, never after a warning or error description.
- Emoji: keep the source's rare, deliberately placed emoji exactly where they
are (two payoff lines end in ✨); never add one, never in a heading. "Give
a parameter a default value and it stops being required. That's it. It's
just Python." → Dale un valor por defecto a un parámetro y deja de ser
obligatorio. Eso es todo. Es simplemente Python. — not ¡Eso es todo, es
solo Python! ✨ (merged sentences, added exclamation and emoji).
## 4. Typography
- Questions and exclamations always open with the inverted mark — ¿Dónde va
esto?, ¡Listo! — placed where the question starts: Si falla, ¿qué ves?
- Sentence case for headings, admonition titles, tab labels and table
headers: first word and proper nouns only (Configurar el transporte). An
English -ing heading becomes an infinitive or a noun phrase, never a
gerund: "Handling errors" → Manejo de errores, not Manejando errores.
Language names and months are lower-case (en inglés, en julio); capitals
keep their accents; solo and este / ese / aquel never take one.
- Punctuation characters stay as the source has them: straight double quotes
"…" (not «…», not curly), ASCII apostrophes, parentheses and colons. No
space before : ; ! ? and lower case after a colon unless a proper noun or
code follows. An English em-dash aside becomes commas, parentheses, a
colon or a second sentence; if a dash pair truly reads best, use the raya
pegada —así—, never an English " — " floating between spaces.
- Digits stay ASCII and identifiers are copied byte for byte: protocol
revisions such as `2026-07-28`, versions, ports, status and error codes,
RFC and SEP numbers — never 28/07/2026 or 28 de julio de 2026. Ordinary
quantities keep the source's form, decimal point included (2.5 segundos),
since decimal conventions differ across the Spanish-speaking world. A
space between number and unit (100 MB, 30 s); % as in the source (100%).
- e.g. → por ejemplo, i.e. → es decir, vs → frente a, etc. stays; & → y,
and y → e / o → u also before English words (clientes e IDE).
- Loanwords kept in English are plain type — no italics, no quotes — with a
Spanish article: el token, la API. Emphasis lands where the source puts
it; a bolded negation stays bold ("does **not** raise" → **no** lanza).
## 5. Terminology pointer
The termbase `glossary.json` is injected separately and overrides anything
written here. This section fixes the conventions its renderings assume:
- Terms in the glossary's `keep` list and every identifier — class, function,
method, parameter, module and package names, protocol method strings
(`tools/call`, `notifications/...`), header names, environment variables,
anything in code font — are copied byte for byte. Acronyms and product
names take no plural s in Spanish: "the SDKs" → los SDK, "both APIs" →
ambas API; the article carries the number. You may name the kind of thing
in front: la clase `Context`, el parámetro `lifespan=`. A glossary term
used as a code-font identifier stays English although its prose noun is
translated: "the `sampling` capability" → la capacidad `sampling`.
- Text quoted from what the example code prints or displays — an output
line, a log message, an error string, a UI label such as the Inspector's
**Tools** and **Resources** tabs — stays exactly as the code emits it
(usually English), with no Spanish reading added in brackets.
- English nouns kept in English keep their spelling, take a fixed gender and
pluralise with -s (los tokens, los callbacks). Masculine by default — el
token, el handler, el callback, el host, el prompt, el endpoint, el log,
el middleware, el payload, el timeout; feminine where settled — la API, la
URL, la URI, la CLI, la web, la caché, la terminal (el terminal is Spain
usage). A gender given in a glossary note wins.
- 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 in the page body — muestreo (sampling) — and appears alone
after that, never glossed in a heading. Each glossary note says which
terms take the gloss.
- Python vocabulary follows the established Spanish of the Python
documentation: devolver un valor, lanzar una excepción, argumento
nombrado, decorador, cadena, entorno virtual, tiempo de ejecución, hilo,
de terceros, asíncrono. One rendering per term per page: the glossary
target, every time — also where a note marks the choice as provisional.
## 6. Provisional note
Every decision above, and every entry in `glossary.json`, is provisional
pending review by native Spanish-speaking readers from more than one region.
To propose a change — a rendering that reads as regional, a rule that yields
stiff Spanish, a term to pin — edit this file or `glossary.json` in a pull
request; the generated pages under `pages/` are never edited by hand.

20
i18n/es/notices.md Normal file
View file

@ -0,0 +1,20 @@
---
translation:
sections: [aff1b3e872b7876a, 4d80558ad052d586, 0bb81f1e62062d26, d5c35dcec50156bc]
tool: 1
---
# Avisos de traducción {#translation-notices}
Una de estas notas aparece al principio de cada página de un sitio de documentación traducido.
## Traducción automática {#translated}
Esta página se tradujo automáticamente a partir de la documentación en inglés, y la [página en inglés](ENGLISH_PAGE) es la versión de referencia. Si algo no se lee bien, [Traducciones](TRANSLATIONS_PAGE) explica cómo avisarnos.
## Traducción desactualizada respecto a la página en inglés {#outdated}
La página en inglés cambió después de que se hizo esta traducción, así que algunas partes pueden estar desactualizadas. En caso de duda, lee la [página en inglés](ENGLISH_PAGE); [Traducciones](TRANSLATIONS_PAGE) explica cómo funciona la documentación traducida.
## Se muestra en inglés {#english}
No hay una traducción vigente de esta página, así que la estás leyendo en inglés. [Traducciones](TRANSLATIONS_PAGE) explica cómo funciona la documentación traducida.

View file

@ -0,0 +1,165 @@
---
translation:
sections: [0355618e5f4d5fe4, 1821eaf50f2d0b64, 82e0b28ebd3abf5a, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487]
tool: 1
---
# MCP Apps {#mcp-apps}
Una **MCP App** es una herramienta con cara visible: además de sus datos, la herramienta apunta a un
documento HTML que el host muestra como una superficie interactiva.
Dos partes, siempre dos partes:
1. **Una herramienta** que hace el trabajo y devuelve datos, como cualquier otra herramienta.
2. **Un recurso `ui://`** que contiene el HTML que el host muestra para ella.
La herramienta lleva una referencia `_meta.ui.resourceUri` al recurso. El host lo obtiene
con `resources/read`, lo muestra en un **iframe aislado (sandboxed)** y envía el resultado de la
herramienta a ese iframe mediante `postMessage`. El servidor nunca envía ni recibe
mensajes `ui/*`: ese tráfico ocurre entre el host y el iframe. Tú sirves una herramienta
y un documento HTML; el host monta el espectáculo.
El SDK incluye esto como la extensión integrada `Apps` (`io.modelcontextprotocol/ui`).
Si las [extensiones](extensions.md) son nuevas para ti, échale un vistazo primero a esa página. Un minuto,
y luego vuelve.
## Un reloj con cara visible {#a-clock-with-a-face}
```python title="server.py" hl_lines="19 22 30 32"
--8<-- "docs_src/apps/tutorial001.py"
```
Cuatro pasos:
* `Apps()`: una sola instancia contiene tus herramientas vinculadas a una UI y sus recursos.
* `@apps.tool(resource_uri="ui://clock/app.html")`: una herramienta normal, más la
marca `_meta.ui.resourceUri`. Todo lo que acepta `@mcp.tool()` (name, title,
description, ...) se pasa tal cual.
* `apps.add_html_resource("ui://clock/app.html", CLOCK_HTML)`: el recurso
correspondiente, servido como `text/html;profile=mcp-app`. Ese tipo MIME exacto es lo que
le dice a un host "esto es una app, muéstrala".
* `MCPServer("clock", extensions=[apps])`: la activación. El servidor ahora anuncia
`io.modelcontextprotocol/ui` bajo `capabilities.extensions`.
El HTML en sí escucha el `postMessage` del host y muestra el resultado. Para apps
reales, usa el SDK oficial de navegador [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps)
dentro de tu HTML. Te da `ontoolresult`, `callServerTool`,
`getHostContext` y `onhostcontextchanged` en lugar de eventos de mensaje sin procesar.
## Degradación elegante {#graceful-degradation}
No todos los clientes muestran apps. La especificación es tajante sobre lo que eso significa para ti:
> Tools **MUST** return a meaningful `content` array even when UI is available.
El modelo lee `content`; el iframe es para humanos. Un host capaz de mostrar UI sigue entregando
el resultado en texto al modelo, y un cliente solo de texto recibe *solo* eso. Así que el
patrón canónico es una herramienta, dos respuestas. Mira `get_time` de nuevo:
```python title="server.py" hl_lines="23-27"
--8<-- "docs_src/apps/tutorial001.py"
```
`client_supports_apps(ctx)` es `True` solo cuando el cliente declaró la
extensión `io.modelcontextprotocol/ui` **y** incluyó `text/html;profile=mcp-app`
en su configuración `mimeTypes`. El campo es obligatorio, así que un cliente que lo omite
no cuenta. Eso es exactamente lo que declara `main()` en el mismo archivo: la
mitad cliente de la negociación, y vuelve la respuesta enriquecida.
!!! warning
Nunca devuelvas un marcador de posición como `"[Rendered UI]"` como único contenido. Si el
texto alternativo es inútil, la herramienta es inútil para todos los clientes solo de texto y para
el propio modelo. Escribe la frase.
## Blindar el iframe {#locking-the-iframe-down}
El lado del recurso lleva los metadatos de seguridad: qué puede cargar el iframe, qué
permisos del navegador quiere, cómo le gustaría que lo enmarcaran:
```python title="server.py" hl_lines="9 19-22"
--8<-- "docs_src/apps/tutorial002.py"
```
`csp` y `permissions` son **solicitudes al host**, no comportamiento del servidor. El host
construye las políticas Content-Security-Policy y Permissions-Policy del iframe a partir de ellas, y
puede negarse. Detecta las funcionalidades en tu JS en lugar de suponer que se concedieron.
`ResourceCsp`, campo por campo (nombre en Python, clave en el canal, qué hace el host con ella):
| Python | Canal (`_meta.ui.csp`) | Controla |
|---|---|---|
| `connect_domains` | `connectDomains` | `connect-src`: adónde pueden ir `fetch`/XHR |
| `resource_domains` | `resourceDomains` | `img-src`, `style-src`, ...: recursos estáticos |
| `frame_domains` | `frameDomains` | `frame-src`: iframes anidados |
| `base_uri_domains` | `baseUriDomains` | `base-uri`: a qué puede apuntar `<base>` |
`ResourcePermissions`: cada campo solicita un permiso del navegador para el iframe.
| Python | Canal (`_meta.ui.permissions`) |
|---|---|
| `camera` | `camera` |
| `microphone` | `microphone` |
| `geolocation` | `geolocation` |
| `clipboard_write` | `clipboardWrite` |
!!! note
La CSP y los permisos viven en el **recurso**, nunca en la herramienta. Los metadatos de herramienta
de la especificación no tienen hueco para ellos, y los hosts los ignoran ahí. El SDK hace que el
error sea imposible de representar: `@apps.tool()` simplemente no tiene parámetro `csp`.
### Visibilidad {#visibility}
`visibility=["app"]` en una herramienta dice "esto existe para el iframe, no para el modelo":
* `"model"`: el modelo puede llamarla.
* `"app"`: el iframe puede llamarla (mediante `callServerTool`).
* Omitido: ambos, que es el valor por defecto.
Filtrar es tarea del **host**. El servidor lista las herramientas exclusivas de app en `tools/list`
como cualquier otra; el host las oculta al modelo. No filtres en el servidor.
## Las reglas que el SDK hace cumplir {#the-rules-the-sdk-enforces}
Todas estas fallan al arrancar, no en producción:
* Un `resource_uri` o una URI de recurso que no sea `ui://...` es un `ValueError` en el
momento de decorar o registrar.
* Una herramienta vinculada a una URI **sin un recurso registrado que corresponda** es un `ValueError`
cuando `MCPServer(extensions=[apps])` consume la extensión. Una herramienta que anuncia
un HTML que responde 404 en `resources/read` es un error de configuración, así que se niega a
construirse.
* `meta={"ui": ...}` en `@apps.tool()` es un `ValueError`. El decorador es dueño de
`_meta["ui"]`; exprésalo con `resource_uri=` y `visibility=`. Otras claves de `meta=`
se combinan sin problema al lado.
Ni el SDK ext-apps de TypeScript ni FastMCP detectan hoy ninguno de estos casos; preferimos
que te enteres antes de que lo haga un host.
## Más allá del HTML en línea {#beyond-inline-html}
`add_html_resource` cubre el caso común: una cadena de HTML. Para cualquier otra cosa,
HTML en disco o contenido generado, construye el recurso tú mismo y entrégalo:
```python title="server.py" hl_lines="12 18"
--8<-- "docs_src/apps/tutorial003.py"
```
`add_resource` rellena el tipo MIME `text/html;profile=mcp-app` cuando el recurso
no fija uno explícitamente, y rechaza una discrepancia explícita: un recurso `ui://`
con cualquier otro tipo MIME es uno que ningún host va a mostrar.
!!! tip
¿Apuntas a un host previo a la disponibilidad general que todavía lee la clave plana
obsoleta `_meta["ui/resourceUri"]`? Combínala tú mismo:
`@apps.tool(resource_uri="ui://x", meta={"ui/resourceUri": "ui://x"})`.
El objeto `ui` anidado es la forma de la especificación; la clave plana está de salida.
## Verlo en marcha {#see-it-run}
La historia `apps` en `examples/stories/` es esta página en forma de pareja ejecutable: un servidor
con una herramienta de reloj vinculada a una UI y un cliente que negocia Apps, lee el
`_meta.ui.resourceUri` de la herramienta, obtiene el HTML y llama a la herramienta.
```bash
uv run python -m stories.apps.client
```

View file

@ -0,0 +1,260 @@
---
translation:
sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff]
tool: 1
---
# Extensiones {#extensions}
Una **extensión** es un paquete opcional de comportamiento MCP detrás de un único identificador.
En un servidor puede aportar herramientas, recursos y nuevos métodos de solicitud, y puede envolver
`tools/call`. En un cliente puede reclamar formas de resultado adicionales de `tools/call` y observar
notificaciones de proveedor. Cada lado se anuncia bajo su propio `capabilities.extensions`, y nada
cambia para quien no lo haya pedido. Ese es el contrato ([SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)), y
tiene una regla de oro: **las extensiones están desactivadas por defecto**.
## Usar una extensión {#using-an-extension}
Pasa las instancias al construir:
```python title="server.py"
--8<-- "docs_src/extensions/tutorial001.py"
```
Listo. El servidor ahora anuncia `io.modelcontextprotocol/ui` bajo
`capabilities.extensions` y sirve todo lo que aporta la extensión.
`Apps` es la extensión de referencia incorporada y tiene su propia página: **[MCP Apps](apps.md)**.
!!! note
Las extensiones se fijan al construir. No hay un `add_extension` que llamar después:
el mapa de capacidades de un servidor no debería cambiar mientras haya clientes conectados a él.
El mapa de capacidades viaja en `server/discover`, que es una ruta de **2026-07-28**. Un
handshake `initialize` heredado no tiene dónde ponerlo, así que un cliente heredado simplemente
no ve la extensión. Diseña pensando en eso: una extensión *amplía* un servidor, no debe ser la
única forma de usarlo.
## Escribir la tuya {#writing-your-own}
Crea una subclase de `Extension` y sobrescribe solo lo que necesites. Cada método tiene un valor por defecto.
### El identificador {#the-identifier}
```python
--8<-- "docs_src/extensions/tutorial002.py"
```
El identificador es una cadena `vendor-prefix/name` que sigue la gramática de claves `_meta`
de la especificación: etiquetas separadas por puntos (cada una empieza con una letra y termina
con una letra o un dígito), una barra y luego el nombre. Se valida **cuando se define la clase**,
así que un error tipográfico no espera a que arranque un servidor:
```text
TypeError: Stamps.identifier must be a `vendor-prefix/name` string
(reverse-DNS prefix required), got 'stamps'
```
Usa como prefijo un dominio que controles. `io.modelcontextprotocol/*` es para extensiones
especificadas por el propio proyecto MCP.
### Aportar herramientas {#contributing-tools}
La extensión útil más pequeña es una herramienta y un mapa de ajustes:
```python title="server.py" hl_lines="17 19-20 22-23 26"
--8<-- "docs_src/extensions/tutorial003.py"
```
* `tools()` devuelve objetos `ToolBinding`. El servidor registra cada uno exactamente como si
hubieras llamado tú a `mcp.add_tool(...)`: la misma generación de esquema, la misma inyección
de `Context`, todo igual.
* `settings()` es el valor anunciado en `capabilities.extensions["com.example/stamps"]`.
Devuelve `{}` (el valor por defecto) para anunciar la extensión sin ajustes.
* La extensión nunca recibe el servidor. Declara sus aportaciones como datos;
`MCPServer` las consume. No hay un `self.server` que mutar.
Y `main()` es la prueba, un cliente en memoria directamente contra `mcp`:
```python title="server.py" hl_lines="29-34"
--8<-- "docs_src/extensions/tutorial003.py"
```
### Servir tus propios métodos {#serving-your-own-methods}
Una extensión puede registrar **nuevos métodos de solicitud**: sus propios verbos, servidos junto a los
de la especificación:
```python title="server.py" hl_lines="16-22 31 40-48"
--8<-- "docs_src/extensions/tutorial004.py"
```
* `SearchParams` es una subclase de `RequestParams`, así que el sobre `_meta` de 2026 se analiza
de forma uniforme y tu handler recibe parámetros validados, nunca un diccionario en bruto. Acota lo
que controla el cliente: `Field(ge=1, le=100)` rechaza un `limit` absurdo antes de que
tu código reserve nada para él.
* `require_client_extension(ctx, EXTENSION_ID)` es el filtro: un cliente que no declaró
la extensión recibe el error `-32021` (falta una capacidad de cliente requerida),
con el payload legible por máquina `requiredCapabilities` que pide la especificación.
* `protocol_versions=frozenset({"2026-07-28"})` fija el método a una única versión del protocolo.
En cualquier otra versión el cliente recibe `METHOD_NOT_FOUND`, exactamente como si el método
no existiera ahí. Para ese cliente, no existe.
Los métodos son **estrictamente aditivos**. El SDK lo hace cumplir al construir, no en
tiempo de ejecución:
* Un `MethodBinding` para un método definido por la especificación (`tools/list`, `completion/complete`, ...)
lanza `ValueError` cuando se construye el binding. Los verbos principales pertenecen al servidor.
* Dos extensiones que vinculan el mismo método lanzan una excepción cuando se registra la segunda.
Que gane la última escritura es como los plugins se corrompen entre sí; aquí no hacemos eso.
* Un conjunto `protocol_versions` vacío también lanza una excepción: un método que nunca puede
servirse es un bug, no una configuración.
### El lado del cliente {#the-client-side}
El `main()` del mismo archivo es toda la historia del cliente, sus dos mitades:
```python title="server.py" hl_lines="54-58"
--8<-- "docs_src/extensions/tutorial004.py"
```
* `Client(..., extensions=[advertise(EXTENSION_ID)])` declara la extensión. Las
declaraciones se convierten en `ClientCapabilities.extensions`: en una conexión 2026-07-28
el mapa viaja en el sobre `_meta` de cada solicitud, así que el servidor lo ve en
**cada** solicitud; en una conexión heredada viaja en el handshake `initialize`.
Al código del servidor le da igual cuál: `require_client_extension(ctx, ...)` y
`ctx.session.check_client_capability(...)` leen la fuente correcta en ambas rutas.
* Los métodos de proveedor bajan una capa hasta `client.session.send_request(...)`; `Client`
solo incorpora métodos de primera clase para los verbos de la especificación. `send_request`
acepta cualquier subclase de `Request`, así que la solicitud de proveedor pasa tal cual.
### Interceptar `tools/call` {#intercepting-toolscall}
El único hook que intercepta. Sobrescribe `intercept_tool_call` para observar, cortocircuitar
o vetar una llamada a herramienta:
```python title="server.py" hl_lines="17-24"
--8<-- "docs_src/extensions/tutorial005.py"
```
* `params` es el `CallToolRequestParams` validado: obtienes `params.name` y
`params.arguments` sin tocar JSON en bruto. También es lo que decide qué llamada a
herramienta se ejecuta: pasar un contexto reescrito a través de `call_next` cambia lo que
el handler observa en `ctx`, no la invocación de la herramienta. Reescribir solicitudes a
nivel del canal es cosa de [Middleware](middleware.md).
* `call_next(ctx)` ejecuta el resto de la cadena y devuelve el resultado del handler.
Devuélvelo sin cambios (observar), devuelve otra cosa (reemplazar) o lanza un
`MCPError` (rechazar). Lo que devuelvas se serializa como cualquier resultado de
handler, incluido el sello de identidad `serverInfo` de la generación 2026, así que un
interceptor que cortocircuita nunca produce una respuesta anónima o fuera de esquema.
* Con varias extensiones, los interceptores se anidan en orden de registro: la primera
extensión en `extensions=[...]` es la más externa.
* La implementación por defecto deja pasar todo, y un servidor cuyas extensiones nunca
sobrescriben este hook mantiene intacto el handler `tools/call` sin más. No
pagas por lo que no usas.
El hook envuelve `tools/call` y nada más. Para lo que afecta a cada mensaje, usa
[Middleware](middleware.md). Para eso está.
## Usar una extensión de cliente {#using-a-client-extension}
Una **extensión de cliente** es el mismo contrato desde el lado que consume: un paquete de
comportamiento del lado del cliente detrás de un único identificador. Pasa las instancias a
`Client(extensions=[...])` y llama a las herramientas con normalidad:
```python title="client.py" hl_lines="66-68"
--8<-- "docs_src/extensions/tutorial006.py"
```
`call_tool("buy", ...)` devuelve un `CallToolResult` normal, como cualquier otra llamada. Lo que
cambió la extensión: el servidor ahora puede responder a `buy` con una **forma de resultado**
`receipt` en lugar de un resultado final, y `Receipts` la termina (aquí canjeando el
recibo con una llamada de seguimiento) antes de que `call_tool` devuelva. Nada del punto
de llamada se mueve.
Quita la extensión y nada de esto existe: el filtro del servidor rechaza a un cliente
que no la declaró (error -32021), y una forma reclamada procedente de un servidor que
se salta el filtro falla la validación, exactamente como exige la especificación para un
`resultType` no reconocido. Desactivada por defecto, en ambos extremos del canal.
Para anunciar un identificador **sin** comportamiento del lado del cliente (el servidor filtra
según la capacidad, el cliente no hace nada, como en el cliente de búsqueda de arriba), usa
`advertise()`:
```python
from mcp.client import advertise
client = Client(mcp, extensions=[advertise("com.example/search")])
```
## Escribir una extensión de cliente {#writing-a-client-extension}
Crea una subclase de `ClientExtension` y sobrescribe solo lo que necesites. Tres tipos de
aportación, cada uno con un valor por defecto: `settings()`, `claims()` y `notifications()`.
```python title="client.py" hl_lines="17-18 43-44 46-47"
--8<-- "docs_src/extensions/tutorial006.py"
```
* El identificador sigue la misma gramática que el del servidor y se valida cuando se
define la clase.
* `claims()` devuelve objetos `ResultClaim`: una etiqueta del canal, el modelo que la analiza y el
resolutor que la termina. El modelo debe fijar la etiqueta con
`result_type: Literal["receipt"]` y no debe ser subclase de los tipos de resultado principales
del verbo; ambas cosas se hacen cumplir cuando se construye el claim. Los campos de proveedor como
`receipt_token` viajan por el canal tal cual: una forma sustituida llega al cliente
literalmente.
* El resolutor recibe el modelo analizado y un `ClaimContext`; `ctx.session` es el
mismo identificador público que `client.session`, así que los seguimientos son llamadas de
sesión normales. Devuelve el `CallToolResult` normal del verbo.
* `settings()` es el valor anunciado en `ClientCapabilities.extensions[identifier]`,
leído una vez al construir el `Client`.
`notifications()` declara las notificaciones de servidor de proveedor que se van a observar:
```python
def notifications(self) -> Sequence[NotificationBinding[Any]]:
return [NotificationBinding(method="notifications/receipts", params_type=ReceiptEvent, handler=self.on_receipt)]
```
El handler recibe los parámetros validados de uno en uno, en orden de despacho. Observa; no puede vetar
ni responder.
Dos reglas discretas. Los claims solo están activos en conexiones 2026-07-28, y el anuncio de
capacidades los sigue: en una conexión heredada los claims se disuelven y el identificador sale
del anuncio con ellos, así que el cliente nunca anuncia una extensión cuyas formas
rechazaría. Y cuando quieras la forma reclamada tú mismo en lugar del resolutor,
llama a `client.session.call_tool(..., allow_claimed=True)`; sin esa bandera, una
forma reclamada que llega a un llamador del nivel de sesión lanza `UnexpectedClaimedResult`.
### Verbos de extensión {#extension-verbs}
Los métodos de solicitud propios de una extensión no necesitan registro del lado del cliente. Un tipo
de solicitud de proveedor es una subclase de `mcp.types.Request` y pasa por `client.session.send_request`,
como en [Servir tus propios métodos](#serving-your-own-methods). Un añadido: cuando una
clave de los parámetros debe viajar en el header `Mcp-Name` (las especificaciones de extensión, como
tasks, lo exigen para sus verbos), el tipo de solicitud declara `name_param`:
```python title="client.py" hl_lines="22-25 46-47"
--8<-- "docs_src/extensions/tutorial007.py"
```
La sesión replica `params["jobId"]` en `Mcp-Name` en cada ruta de envío, y un
valor ausente falla de forma visible en lugar de omitir en silencio un header obligatorio.
## Lo que una extensión no puede hacer {#what-an-extension-cannot-do}
La superficie de aportación es **cerrada** a propósito. En el servidor: ajustes, herramientas,
recursos, métodos y un interceptor de `tools/call`. En el cliente: ajustes, claims de
resultado y bindings de notificación. Una extensión no puede:
* **Meterse en el host.** Declara datos; no guarda ninguna referencia al servidor ni al cliente.
* **Reemplazar el comportamiento principal.** Los métodos de la especificación y las etiquetas de
resultado principales se rechazan al construir (el runner reserva `initialize` directamente); un
binding de notificación eclipsado por el vocabulario principal se silencia con un aviso en su lugar.
* **Registrarse tarde.** Una vez que `MCPServer(...)` o `Client(...)` devuelven, el conjunto
de extensiones es el que es.
Si estás peleando contra estos muros, no estás escribiendo una extensión. Estás escribiendo
un fork. Los muros son la funcionalidad: quien lee `extensions=[Apps(), Stamps()]`
sabe *todo* lo que esas dos pueden haber tocado.

View file

@ -0,0 +1,34 @@
---
translation:
sections: [ca6988b7503cd2d3]
tool: 1
---
# Avanzado {#advanced}
Todo lo que necesita un servidor o un cliente normal tiene su sitio temático en las secciones anteriores.
Esta sección reúne las vías de escape a las que recurres cuando la capa de conveniencia de `MCPServer`
te estorba:
* **[El Server de bajo nivel](low-level-server.md)**: la clase sobre la que está construido `MCPServer`.
Esquemas escritos a mano, handlers `on_*`, nada se comprueba por ti, y métodos JSON-RPC
personalizados propios.
* **[Paginación](pagination.md)** y **[Middleware](middleware.md)**: dos cosas que
*solo* puedes hacer en el `Server` de bajo nivel.
* **[Extensiones](extensions.md)** y **[MCP Apps](apps.md)**: la superficie de
extensión del protocolo. Compón paquetes de extensión en un servidor o escribe los tuyos.
Algunas cosas que sería razonable buscar aquí viven, en cambio, donde realmente las
usarías:
* **Autorización** está en **[Ejecutar tu servidor](../run/index.md)**, porque un
servidor se protege donde se despliega.
* **OAuth**, la **aserción de identidad**, la conexión a **varios servidores** y la
**caché** de respuestas están en **[Clientes](../client/index.md)**.
* Las **solicitudes de varias idas y vueltas (multi-round-trip)** y las **suscripciones** están en
**[Dentro de tu handler](../handlers/index.md)**, porque ambas son cosas que un
handler *hace*.
* Las **plantillas de URI** están en **[Servidores](../servers/index.md)**, junto a Recursos.
* **[Versiones del protocolo](../protocol-versions.md)** y
**[Funcionalidades obsoletas](../deprecated.md)** tienen cada una su propia página de nivel superior.
Si no tienes claro si necesitas esta sección, no la necesitas.

View file

@ -0,0 +1,207 @@
---
translation:
sections: [2c79b6338e09b7ac, 7edc43b3fae11314, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, b3530fcf4d11fd56, ebc33704fbd74262, cd0e9c933350390e]
tool: 1
---
# El Server de bajo nivel {#the-low-level-server}
`@mcp.tool()` es una capa. Debajo hay una segunda clase de servidor, `Server`, que habla MCP en crudo: le pasas los objetos del protocolo y los transmite tal cual, sin tocarlos.
`MCPServer` está construido encima. Bajas de nivel cuando la capa de conveniencia estorba:
* Necesitas emitir un esquema **exacto** (cargado de un archivo, generado a partir de una base de datos), no uno derivado de una firma de Python.
* Necesitas control total del resultado: `_meta`, `is_error`, cada clave de `structured_content`.
* Necesitas atender un método que MCP no define.
Para todo lo demás, quédate en `MCPServer`.
## La misma herramienta, a mano {#the-same-tool-by-hand}
Esta es la herramienta `search_books` que **[Herramientas](../servers/tools.md)** escribe en nueve líneas de `@mcp.tool()`, sin el azúcar sintáctico:
```python title="server.py" hl_lines="22 26 32"
--8<-- "docs_src/lowlevel/tutorial001.py"
```
Cambiaron tres cosas, y son toda la API de bajo nivel:
* **Los handlers son parámetros del constructor.** `on_list_tools=` y `on_call_tool=` van en `Server(...)`. Aquí abajo no hay decoradores, y todos los handlers tienen la misma forma: `async (ctx, params) -> result`.
* **Tú escribes el esquema de entrada.** `Tool.input_schema` es un simple `dict` de JSON Schema. Nadie lo deriva de las anotaciones de tipo, porque no hay anotaciones de tipo de las que derivarlo.
* **Tú construyes el resultado.** `CallToolResult(content=[TextContent(...)])`, a mano. Nada se envuelve, se convierte ni se infiere de una anotación de retorno.
`params` es la solicitud ya analizada: `CallToolRequestParams` te da `.name` y `.arguments`. `ctx` es un `ServerRequestContext`: `ctx.session` para responder al cliente, `ctx.lifespan_context`, `ctx.request_id` y `ctx.meta`, el `_meta` entrante de la solicitud.
!!! info
Si has usado FastAPI, ya conoces esta relación. `MCPServer` es la capa de decoradores y anotaciones de tipo; `Server` es el Starlette de debajo. No son rivales: `MCPServer` construye un `Server` y registra en él handlers exactamente como estos.
### Pruébalo {#try-it}
Aquí no hay Inspector: `mcp dev` y `mcp run` solo aceptan un `MCPServer`. Al `Client` en memoria le da igual; acepta un `Server` de bajo nivel exactamente igual que acepta un `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)]
```
El mismo texto que produjo la versión con `@mcp.tool()`. Dos diferencias honestas:
* `result.structured_content` es `None`. El servidor de alto nivel envuelve un `-> str` en `{"result": ...}` por ti; aquí nadie construye lo que no construiste.
* `list_tools` devuelve el esquema que escribiste **tú**, carácter por carácter. La versión de alto nivel tenía `"title": "Query"` en cada propiedad y un `"title": "search_booksArguments"` en la raíz: artefactos de Pydantic. Aquí abajo, si se transmite, es porque lo pusiste ahí.
## Nada se comprueba por ti {#nothing-is-checked-for-you}
`MCPServer` rechaza un argumento incorrecto antes de que tu función llegue a ejecutarse, validando la llamada contra el esquema que generó (**[Herramientas](../servers/tools.md)**).
`Server` no hace eso. Tu `input_schema` se *anuncia* al cliente; nunca se *aplica* a `params.arguments`.
!!! check
Llama a `search_books` sin `limit` y tu `args["limit"]` lanza `KeyError`. El cliente ve:
```text
MCPError: Internal server error
```
Un error de JSON-RPC, código `-32603`, con un mensaje deliberadamente genérico: el SDK no filtra tu traceback a un llamador remoto. El modelo nunca se entera de qué hizo mal, así que no puede reintentar. (En una prueba, `raise_exceptions=True` muestra la excepción real en su lugar; consulta **[Pruebas](../get-started/testing.md)**.)
Eso se generaliza. Una excepción lanzada desde un handler de bajo nivel es **siempre** un error de protocolo, nunca un resultado de herramienta con `is_error=True`. Si quieres que el modelo lea el fallo y se recupere, valida `params.arguments` por tu cuenta y devuelve `CallToolResult(content=[TextContent(...)], is_error=True)`. Los dos tipos de fallo son el tema de **[Manejo de errores](../servers/handling-errors.md)**.
## Dos herramientas, un handler {#two-tools-one-handler}
`on_call_tool` es el único punto de entrada para todas las herramientas del servidor. Enrutas según `params.name`:
```python title="server.py" hl_lines="38-43"
--8<-- "docs_src/lowlevel/tutorial002.py"
```
* `list_tools` anuncia las dos. `call_tool` despacha según el nombre.
* La rama `else` importa: `Server` reenvía sin problema un `tools/call` para un nombre que nunca listaste directamente a tu handler. Lanzar una excepción ahí convierte la llamada en el mismo `-32603` de antes.
## Salida estructurada, a mano {#structured-output-by-hand}
Declara `output_schema` en el `Tool` y pon `structured_content` en el resultado. Ambos son cosa tuya:
```python title="server.py" hl_lines="19-23 36"
--8<-- "docs_src/lowlevel/tutorial003.py"
```
Llámala y el resultado lleva ambas representaciones:
```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"}}
}
```
El bloque `_meta` es el sello de identidad del servidor: el SDK lo añade a cada resultado de la generación 2026, con la `version` del constructor (un servidor que no establece ninguna informa una cadena vacía). Un servidor que no deba identificarse puede quitar la clave con un middleware, que es dueño de los resultados que devuelve.
El servidor nunca compara los dos campos. El `Client` de este SDK sí: devuelve un `structured_content` que no cumpla el `output_schema` que declaraste y `call_tool` lanza un `RuntimeError` que empieza por `Invalid structured content returned by tool search_books` y sigue citando el fallo de `jsonschema`. Prometer un esquema es barato; cumplirlo depende de ti. Toda la escalera de tipos de retorno y esquemas está en **[Salida estructurada](../servers/structured-output.md)**.
## `_meta`: para la aplicación, no para el modelo {#\_meta-for-the-application-not-the-model}
`content` es la parte de la respuesta que lee el modelo. `structured_content` es la misma respuesta como datos tipados. `_meta` es el tercer canal: datos que viajan con el resultado para la **aplicación cliente**, sin formar parte de la respuesta en absoluto.
Úsalo para ID de registros, ID de trazas, cualquier cosa que tu interfaz necesite y tu prompt no:
```python title="server.py" hl_lines="37"
--8<-- "docs_src/lowlevel/tutorial004.py"
```
* Lo construyes como `_meta=`, el nombre que se transmite. El cliente lo lee de vuelta como `result.meta`.
* Pon tus claves en un espacio de nombres (`bookshop/record_ids`). Las claves `io.modelcontextprotocol/*` están reservadas por el protocolo.
!!! warning
`_meta` es una convención entre tú y la aplicación cliente, no una garantía sobre lo que llega
al modelo. El host decide qué muestra. Nunca pongas un secreto en ninguna parte de un resultado de herramienta.
## Las capacidades siguen a tus handlers {#capabilities-follow-your-handlers}
Un `Server` anuncia exactamente las familias de métodos para las que le diste handlers. El `Bookshop` de arriba pasa `on_list_tools` y `on_call_tool` y nada más, así que un cliente que se conecta a él ve:
```json
{"tools": {"listChanged": false}}
```
Ni `resources` ni `prompts`: no hay nada que los respalde. Pasa `on_list_prompts` y aparece `prompts`; pasa `on_completion` y aparece `completions`.
`MCPServer` siempre anuncia herramientas, recursos y prompts, hayas registrado alguno o no, porque sus gestores siempre existen. Aquí abajo la declaración *es* la llamada al constructor.
## El genérico del lifespan {#the-lifespan-generic}
`Server` es genérico en el tipo que produce su lifespan. Anótalo una vez y el objeto queda tipado en todos los sitios donde aparece:
```python title="server.py" hl_lines="24-26 44-45 50"
--8<-- "docs_src/lowlevel/tutorial005.py"
```
* El lifespan es un `Callable[[Server[Catalog]], AbstractAsyncContextManager[Catalog]]`; `@asynccontextmanager` sobre un generador `async` te da exactamente eso.
* Lo que produzca con `yield` se convierte en `ctx.lifespan_context`, y como los handlers están anotados como `ServerRequestContext[Catalog]`, `.search(...)` se autocompleta y pasa la comprobación de tipos.
* Se entra en él una vez cuando el servidor arranca y se sale una vez cuando se detiene. El arranque, la finalización y la versión de `MCPServer` de la misma idea están en **[Lifespan](../handlers/lifespan.md)**.
Sin un `lifespan=`, `ctx.lifespan_context` es un `dict` vacío.
## Un método propio {#a-method-of-your-own}
El constructor cubre los métodos que MCP define. `add_request_handler` cubre todo lo demás:
```python title="server.py" hl_lines="35-36 39-40 43-44 48"
--8<-- "docs_src/lowlevel/tutorial006.py"
```
* El primer argumento es la cadena del método. Las notificaciones tienen un gemelo, `add_notification_handler`.
* `params_type` es el modelo contra el que se validan los `params` entrantes **antes** de que se ejecute tu handler, así que los métodos personalizados *sí* reciben la validación que las herramientas no. Hereda de `RequestParams` para que el campo `_meta` se analice como el de cualquier otro método.
* El handler devuelve un `BaseModel`, un `dict` o `None`. El SDK lo serializa en el resultado JSON-RPC.
Una advertencia honesta: el `Client` de alto nivel solo tiene verbos para los métodos que MCP define, así que no hay `client.reindex()`. Un método de proveedor es para un par que ya sabe que existe: un cliente que también distribuyes, u otro servicio tuyo que hable JSON-RPC.
Un método que no puedes reclamar:
```text
ValueError: 'initialize' is handled by the server runner and cannot be overridden;
use Server.middleware to observe or wrap initialization
```
El handshake pertenece al runner. `server/discover`, `ping` y todos los demás métodos integrados son tuyos para reemplazarlos.
!!! tip
`Server.middleware`, mencionado en ese error, envuelve **todos** los mensajes entrantes, incluido `initialize`. Si lo que quieres es observar o reescribir el tráfico en vez de responder a un método nuevo, empieza en **[Middleware](middleware.md)**.
## Los otros handlers {#the-other-handlers}
Cada uno de estos es una idea para la que ya tienes el vocabulario; cada uno tiene su propia página.
* `on_call_tool`, `on_get_prompt` y `on_read_resource` pueden devolver un `InputRequiredResult` en lugar de su resultado normal para pausar la llamada y pedir datos al cliente; consulta **[Solicitudes de varias idas y vueltas (multi-round-trip)](../handlers/multi-round-trip.md)**. Fiel a este nivel, nada se instala por ti: donde `MCPServer` sella `requestState` por defecto, aquí el `request_state` que estableces se transmite exactamente como lo escribiste hasta que optas por `server.middleware.append(RequestStateBoundary(RequestStateSecurity(keys=[...]), default_audience=server.name))`: una línea (ambos nombres se importan de `mcp.server.request_state`) para el mismo sellado y verificación que realiza `MCPServer` (**[Proteger `requestState`](../handlers/multi-round-trip.md#protecting-requeststate)**).
* `on_list_resources`, `on_read_resource`, `on_list_prompts`, `on_get_prompt` y `on_completion` tienen la misma forma `(ctx, params) -> result` para las demás primitivas.
* `on_subscriptions_listen` sirve el stream `subscriptions/listen` de 2026-07-28. Pasa un `ListenHandler` construido sobre un `SubscriptionBus` y publica eventos en el bus desde tus otros handlers; consulta **[Suscripciones](../handlers/subscriptions.md)** para la composición completa.
* `server.streamable_http_app()` devuelve la misma app de Starlette que la de `MCPServer`; despliégala como **[Ejecutar tu servidor](../run/index.md)** despliega cualquier otra app ASGI. Aquí abajo no hay `server.run(transport=...)`: `server.run(read_stream, write_stream, server.create_initialization_options())` conduce una conexión sobre un par de streams, y esa única línea es todo lo que hay.
## Resumen {#recap}
* El `Server` de bajo nivel recibe sus handlers como **parámetros del constructor** `on_*`; cada handler es `async (ctx, params) -> result`.
* Tú escribes el dict `input_schema` y tú construyes el `CallToolResult`. Nada se deriva, se envuelve ni se valida por ti.
* Una excepción en un handler es un error de protocolo `-32603`. Un error de herramienta que el modelo pueda leer es un `CallToolResult` con `is_error=True` que devuelves **tú**.
* El `_meta` del resultado va dirigido a la aplicación cliente, no al modelo.
* `Server[T]` es genérico en lo que produce su lifespan; `ctx.lifespan_context` es un `T` tipado.
* `add_request_handler(method, params_type, handler)` sirve cualquier método. `initialize` está reservado.
* Las capacidades que anuncia un `Server` se derivan de los handlers que registraste.
`Client(server)` trató a ambos servidores de forma idéntica porque *son* el mismo protocolo, que es justamente la idea. La siguiente capa hacia abajo no es una clase: es **[Middleware](middleware.md)**.

View file

@ -0,0 +1,126 @@
---
translation:
sections: [6048b4f308edbb8c, 068bda0f21ee9c1b, c3e565b61acd75c5, c62422b159c6ed09, 47204fab253cc45c]
tool: 1
---
# Middleware {#middleware}
Un **middleware** es una función asíncrona que envuelve cada mensaje que recibe el servidor.
Lo escribes como `async (ctx, call_next)` y lo añades a `server.middleware`. Esa es toda la API.
!!! warning
La lista de middleware está marcada como **provisional** en el código fuente: su firma y su
semántica pueden cambiar en una versión menor 2.x. Úsala para *observar* (medir tiempos,
registrar, trazar) y para *rechazar* mensajes; no la conviertas en los cimientos del servidor.
`MCPServer` recibe la lista en el constructor (`MCPServer(name, middleware=[...])`) y la expone como
`mcp.middleware`; el `Server` de bajo nivel expone la misma lista como `server.middleware`. El ejemplo
de abajo usa el `Server` de bajo nivel; si `Server(name, on_call_tool=...)` es nuevo para ti, lee
primero **[El Server de bajo nivel](low-level-server.md)**.
## Un middleware que mide tiempos {#a-timing-middleware}
Un servidor, una herramienta y un middleware que registra cuánto tardó cada mensaje:
```python title="server.py" hl_lines="39-45 49"
--8<-- "docs_src/middleware/tutorial001.py"
```
* `ctx` es el mismo `ServerRequestContext` que reciben tus handlers. `ctx.method` es la cadena
del método sin procesar; `ctx.params` son los parámetros sin procesar, **antes** de cualquier
validación.
* `call_next(ctx)` ejecuta el resto de la cadena: la validación, la búsqueda del handler y tu
handler. Devuelve lo que devolvió y la respuesta queda intacta.
* El `try`/`finally` es deliberado: un handler que lanza una excepción también se cronometra,
porque el fallo llega a tu middleware como la excepción que sale de `call_next`.
* `server.middleware.append(...)` lo registra. La lista se ejecuta de fuera hacia dentro, así que
`middleware[0]` es el más cercano al canal.
### Pruébalo {#try-it}
Conecta un cliente, lista las herramientas, llama a una. El log tiene **tres** líneas:
```text
server/discover took 18.3 ms
tools/list took 0.1 ms
tools/call took 0.1 ms
```
Hiciste dos llamadas y obtuviste tres líneas. La primera es `server/discover`: la solicitud que
envió el cliente para establecer la conexión, antes de que pidieras nada.
Ese es el punto. El middleware envuelve **cada** mensaje entrante:
* El establecimiento de la conexión: `server/discover`, o `initialize` y `notifications/initialized`
en una sesión heredada.
* Cada solicitud y cada notificación. Para una notificación, `ctx.request_id is None`,
`call_next(ctx)` devuelve `None` y lo que devuelvas se descarta.
* Incluso un método para el que el servidor no tiene handler: `call_next` lanza el
`MCPError(-32601, "Method not found")` *a través de* tu middleware de camino al cliente.
## Qué puedes hacer dentro de uno {#what-you-can-do-inside-one}
En orden creciente de cuánto deberías dudar:
* **Observar.** Cronométralo, cuéntalo, regístralo. El ejemplo de arriba.
* **Rechazar.** Lanza un `MCPError` *en lugar de* llamar a `call_next(ctx)` y ese único mensaje se
responde con un error JSON-RPC. La conexión sigue activa; el siguiente mensaje pasa. Así es
como un servidor restringe `subscriptions/listen` por llamante:
**[Decidir quién puede observar](../handlers/subscriptions.md#deciding-who-may-watch)** en la
página de Suscripciones lo recorre paso a paso.
* **Reescribir.** `ctx` es una dataclass: `await call_next(dataclasses.replace(ctx, params=...))`
entrega al resto de la cadena unos parámetros distintos de los que envió el cliente. Nunca hagas
esto con `initialize`: el resultado que recibe el cliente se construye a partir de tus parámetros
reescritos, pero el servidor fija el estado de la conexión a partir de los parámetros originales
que llegaron por el canal. Los dos lados pueden terminar el handshake en desacuerdo sobre lo que
negociaron.
* **Responder.** Devuelve un resultado sin llamar a `call_next(ctx)` y llega al cliente como tu
respuesta. `call_next` te entrega la forma final que se transmite, y la canalización nunca
retoca lo que devuelves, así que todo el sobre es tuyo: en una conexión de la generación 2026
eso incluye la marca `_meta` de `serverInfo`, que el SDK añade a los resultados de los handlers
pero no a los tuyos.
!!! check
`initialize` es una de las cosas que el middleware envuelve, y es el *único* punto de enganche
que tienes para ello. Intenta apropiártelo con `add_request_handler` y el SDK se niega:
```text
ValueError: 'initialize' is handled by the server runner and cannot be overridden;
use Server.middleware to observe or wrap initialization
```
!!! warning
`initialize` se maneja en línea: el servidor no lee más mensajes entrantes hasta que tu cadena
de middleware devuelve. Esperar con await una solicitud del servidor al cliente
(`ctx.session.send_request(...)`, una elicitación) mientras se maneja `initialize` **bloquea
la conexión por completo**: la respuesta que esperas nunca se podrá leer. Las notificaciones
que se envían sin esperar respuesta no dan problemas.
## El único middleware que viene activado por defecto {#the-one-middleware-that-ships-on-by-default}
El SDK incluye exactamente un middleware, y ya está en la lista del servidor: el que emite un
span de OpenTelemetry por cada mensaje. No lo añades y, la mayor parte del tiempo, ni piensas en
él. No hace nada hasta que instalas un exportador, y tiene su propia página:
**[OpenTelemetry](../run/opentelemetry.md)**.
!!! info
Si has escrito middleware ASGI, ya conoces esta forma. El `(scope, receive, send)` de
Starlette se convirtió en `(ctx, call_next)`, y se ejecuta *después* del transporte, sobre el
mensaje ya decodificado en lugar de la solicitud HTTP sin procesar. Los dos se combinan: el
middleware de Starlette sobre `streamable_http_app()` ve HTTP; este ve MCP.
## Resumen {#recap}
* Un middleware es `async (ctx, call_next) -> result`, se pasa como `MCPServer(middleware=[...])` (o
se añade a `mcp.middleware`), y se añade a `server.middleware` en el `Server` de bajo nivel.
* Envuelve **cada** mensaje entrante (`server/discover`, `initialize`, solicitudes, notificaciones,
métodos desconocidos) y se ejecuta de fuera hacia dentro.
* `ctx.request_id is None` es la forma de distinguir una notificación de una solicitud.
* Lanza una excepción en lugar de llamar a `call_next` para rechazar un mensaje; la conexión sobrevive.
* El trazado con OpenTelemetry del propio SDK también es un middleware, ya incluido en la lista. Consulta
**[OpenTelemetry](../run/opentelemetry.md)**.
* Toda la superficie es provisional. Observa con ella; no construyas sobre ella.
Eso es todo lo que envuelve una solicitud. **[Autorización](../run/authorization.md)** es lo que decide si la solicitud
llega a ejecutarse siquiera.

View file

@ -0,0 +1,85 @@
---
translation:
sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9]
tool: 1
---
# Paginación {#pagination}
La mayoría de los servidores nunca necesitan esto.
`MCPServer` responde a cada solicitud `list_*` con todo lo que tiene, en una sola página, `next_cursor=None`. Para unas cuantas docenas de herramientas, recursos o prompts esa es la respuesta correcta y no hay nada que configurar.
La paginación es para el servidor cuya lista de recursos es en realidad una base de datos: miles de filas que se niega a serializar en una sola respuesta. La respuesta del protocolo es un **cursor**: el servidor devuelve una página más un token opaco, y el cliente envía ese token de vuelta para obtener la siguiente página.
`@mcp.resource()` no tiene ningún punto de extensión para nada de eso. Para paginar, escribes el handler de listado tú mismo, sobre el **[Server de bajo nivel](low-level-server.md)**.
## Un servidor que pagina {#a-server-that-pages}
```python title="server.py" hl_lines="12 15-16"
--8<-- "docs_src/pagination/tutorial001.py"
```
* En un `Server` de bajo nivel, los handlers son argumentos del constructor, no decoradores. `on_list_resources` responde a cada solicitud `resources/list`; esa es toda la conexión necesaria.
* Todo handler paginado lleva el tipo `params: PaginatedRequestParams | None`, y el ejemplo acepta ambos. Sin embargo, a través de una conexión el SDK nunca te entrega `None` (una solicitud sin miembro `params` llega al handler como el modelo con sus valores por defecto), así que la señal que importa es `params.cursor is None`: **empieza desde el principio**.
* Tú decides qué *es* un cursor. Aquí es un desplazamiento representado como cadena. Una marca de tiempo, una clave primaria, un blob en base64: cualquier cosa que puedas generar de salida y reconocer cuando vuelva.
* `next_cursor=None` es la forma de decir "esa fue la última página". No hay recuento, ni total, ni `has_more`. `None` es toda la señal.
!!! tip
Un `PAGE_SIZE` de 10 hace legible el ejemplo. Elige el tuyo por endpoint: una lista de
recursos de una línea se puede permitir una página de 500; una lista de plantillas de prompt voluminosas, no.
El cliente no tiene voz en ello, y así está diseñado.
### Pruébalo {#try-it}
`Client(server)` se conecta a un `Server` de bajo nivel en memoria exactamente igual que se conecta a un `MCPServer`.
Llama a `list_resources()` sin argumentos. Obtienes diez recursos, de `book-1` a `book-10`, y `next_cursor` es la cadena `"10"`.
Devuélvelo con `list_resources(cursor="10")` y el primer recurso es `book-11`; el nuevo `next_cursor` es `"20"`.
La décima página vuelve con `next_cursor` en `None`. Listo.
## El bucle del cliente {#the-client-loop}
Cada método `list_*` de `Client` (`list_tools`, `list_resources`, `list_resource_templates`, `list_prompts`) acepta el argumento nombrado `cursor=`. Vaciar una lista paginada es un solo `while True`:
```python title="client.py" hl_lines="26-32"
--8<-- "docs_src/pagination/tutorial002.py"
```
* `cursor` empieza como `None`, así que la primera solicitud no lleva cursor.
* Extiende **antes** de mirar `next_cursor`: la última página también tiene recursos.
* `next_cursor is None` es la salida. Cualquier otra cosa vuelve directamente a `cursor=`, sin tocarla.
Ejecuta su `main()` e imprime `100 resources`: diez páginas de diez, unidas por un bucle que nunca supo que había diez páginas.
Es el mismo bucle que **[El cliente](../client/index.md)** muestra para cada verbo `list_*`, y no cuesta nada frente a un servidor que no pagina: `next_cursor` es `None` en la primera respuesta y el bucle se ejecuta una vez.
## Las tres reglas {#the-three-rules}
**Los cursores son opacos.** Un cliente nunca debe analizar, construir ni adivinar uno. La única fuente legítima de un cursor es el `next_cursor` de la página anterior, tal cual.
**El servidor elige el tamaño de página.** No hay `limit=` en el protocolo. Si necesitas un tamaño de página distinto, cambias el servidor.
**Un cliente que ignora la paginación sigue funcionando.** Llama a `list_resources()` una vez, obtiene los diez primeros y nunca se entera del `next_cursor` que descartó. Nada se rompe; simplemente ve menos.
!!! check
Opaco significa opaco. Inventa un cursor (`list_resources(cursor="page-2")`) y no hay
nada que el protocolo pueda hacer por ti. Este servidor intenta `int("page-2")`, el handler lanza una excepción,
y lo que le vuelve al cliente es:
```text
MCPError(-32603, 'Internal server error', None)
```
Un cursor que no obtuviste del servidor es un bug, no una petición de funcionalidad.
## Resumen {#recap}
* `MCPServer` devuelve todo en una página. La paginación es opcional, y la activas en el `Server` de bajo nivel.
* `on_list_resources` (y `on_list_tools`, `on_list_prompts`, `on_list_resource_templates`) recibe `PaginatedRequestParams | None`; `params.cursor` es `None` para la primera página.
* Devuelves una página más `next_cursor`: cualquier cadena que reconozcas después, o `None` cuando no queda nada.
* El bucle del cliente: pasa `cursor=`, acumula, repite hasta que `next_cursor is None`.
* Los cursores son opacos, el servidor es dueño del tamaño de página y un cliente que no pagina sigue recibiendo la primera página.
El resto de la API del `Server` escrito a mano (`on_call_tool`, diccionarios `input_schema`, `_meta`) está en **[El Server de bajo nivel](low-level-server.md)**.

View file

@ -0,0 +1,123 @@
---
translation:
sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800]
tool: 1
---
# Sugerencias de caché {#caching-hints}
En el protocolo 2026-07-28, cada resultado que un servidor devuelve para `tools/list`, `prompts/list`, `resources/list`, `resources/templates/list`, `resources/read` y `server/discover` lleva dos campos: `ttlMs`, cuántos milisegundos puede un cliente tratar el resultado como vigente, y `cacheScope`, si un resultado en caché puede compartirse entre usuarios (`"public"`) o pertenece a un único contexto de autorización (`"private"`).
El servidor no guarda nada en caché. Los campos son una *declaración*: "esta lista de herramientas es la misma para todos y no cambiará durante un minuto". Un cliente (o un gateway delante de ti) puede entonces saltarse la ida y vuelta. Respetar las sugerencias es decisión del cliente; emitirlas es trabajo del servidor, y el SDK lo hace por ti.
Por defecto, cada resultado dice `ttlMs: 0, cacheScope: "private"`: caducado de inmediato, nunca compartido. Eso siempre es seguro y siempre conforme. Si tus listas realmente son estables e idénticas para todos los que llaman, dilo en la construcción:
```python title="server.py" hl_lines="5-8"
--8<-- "docs_src/caching/tutorial001.py"
```
* El mapa usa como clave el **nombre del método**, y los seis métodos que admiten caché son las únicas claves válidas. El parámetro tiene el tipo `Mapping[CacheableMethod, CacheHint]`, así que tu editor autocompleta las claves y marca un error tipográfico antes de ejecutar; lo que se le escape al verificador de tipos lanza una excepción en la construcción.
* Un método que no mencionas conserva los valores por defecto. El mapa es un conjunto de sobrescrituras, no un manifiesto.
* `CacheHint(ttl_ms=5_000)` dejó `scope` sin definir, así que sigue siendo `"private"`: cinco segundos de vigencia, por cada llamador. El alcance y el TTL son decisiones independientes.
* `"server/discover"` también es una clave válida, ya que el resultado de descubrimiento admite caché como cualquier lista.
!!! warning
`cacheScope: "public"` significa que *cualquiera* puede recibir tu respuesta en caché. Un
gateway compartido entregará sin problema el resultado de un usuario a otro, incluso cuando
la solicitud estaba autenticada. Marca un resultado como `"public"` solo cuando sea idéntico
para todos los llamadores, y nunca uses `cacheScope` como control de acceso: es una etiqueta,
no un candado.
## Sobrescritura por handler {#per-handler-override}
En el `Server` de bajo nivel, los handlers construyen sus resultados a mano, y `ttl_ms` / `cache_scope` son simplemente campos de los modelos de resultado. Un handler que los define explícitamente siempre gana al mapa del constructor, campo por campo:
```python title="server.py" hl_lines="10 16"
--8<-- "docs_src/caching/tutorial002.py"
```
El handler dijo `ttl_ms=1_000` y nada sobre el alcance. En lo que se transmite: `ttlMs: 1000` (el del handler, no el `60_000` del mapa) y `cacheScope: "public"` (el del mapa, porque el handler lo dejó sin definir). Lo explícito gana a lo configurado, y lo configurado gana a lo por defecto. Esto vale por campo, así que un handler puede fijar un campo y dejar el otro a la política de todo el servidor.
Esta es también la vía de escape para dinámicas que el constructor no puede conocer: un handler que filtra `resources/read` por usuario puede devolver `cache_scope="private"` para una URI desde un servidor por lo demás público.
Una salvedad sobre las listas paginadas: el protocolo exige el **mismo `cacheScope` en cada página** de una misma lista. El mapa del constructor lo cumple por construcción, ya que usa como clave el método, no la página. Pero un handler que sobrescribe el alcance se hace responsable de esa coherencia: sobrescríbelo en *todas* las páginas, nunca solo cuando hay un cursor, o la página uno y la página dos no coincidirán.
## Lo que ve el cliente {#what-the-client-sees}
En una sesión 2026-07-28, `Client` respeta las sugerencias por ti: tiene una caché de respuestas integrada, activada por defecto. Un resultado que llega con un `ttlMs` se almacena, y una llamada idéntica dentro de ese TTL se sirve desde la caché sin ida y vuelta. Un resultado que llega *sin* sugerencia no se guarda en caché: los resultados sin sugerencia reciben `CacheConfig.default_ttl_ms`, que es `0` por defecto (caducado de inmediato), así que un servidor que no declara nada ve exactamente el mismo tráfico llamada por llamada de siempre.
```python title="client.py" hl_lines="33 35 38"
--8<-- "docs_src/caching/tutorial003.py"
```
Cuatro llamadas, tres consultas al servidor. La segunda llamada encontró una entrada vigente y nunca llegó al servidor; adelantar el reloj (inyectado) más allá del TTL hizo que la tercera volviera a consultar; la cuarta dijo `cache_mode="refresh"`. Ese argumento nombrado existe en los cinco verbos con caché (`list_tools`, `list_prompts`, `list_resources`, `list_resource_templates`, `read_resource`):
* `"use"` (el valor por defecto) sirve una entrada vigente si la hay, y almacena lo consultado si no.
* `"refresh"` nunca sirve desde la caché: consulta al servidor y almacena el resultado, reemplazando lo que hubiera en caché.
* `"bypass"` hace la ida y vuelta sin tocar la caché en absoluto: ni lectura ni escritura.
Hay una regla por encima de `"use"`: **las llamadas que llevan `meta` siempre llegan al servidor.** Una solicitud con `meta` definido (un token de progreso, campos de trazado) espera una solicitud real por el canal, así que con `cache_mode="use"` se trata como `"refresh"`: se omite la lectura de la caché, y el resultado obtenido sigue reemplazando la entrada en caché. `"bypass"` y un `"refresh"` explícito se comportan como siempre.
Para desactivar la caché por completo, construye con `Client(server, cache=None)`: cada llamada vuelve a ser una ida y vuelta, y `cache_mode`, aunque se sigue aceptando, no hace nada.
El alcance también se respeta automáticamente: las entradas `"private"` se asocian a la *partición* de la caché (más abajo), mientras que las `"public"` pueden optar por compartirse más ampliamente. Y **las notificaciones ganan al TTL** para las entradas exactas que nombran: una notificación `list_changed` desaloja el listado en caché correspondiente, y `resources/updated` desaloja la lectura en caché almacenada exactamente bajo su URI, por muy vigentes que estuvieran. En una conexión 2026-07-28 esas notificaciones llegan por un stream `subscriptions/listen` que abres con `client.listen(...)`, y el desalojo se completa antes de que tu observador vea el evento; **[Suscripciones](subscriptions.md)** es esa página.
Una salvedad sobre `resources/updated`: el desalojo es solo por URI exacta. El contrato del almacén no tiene operación de enumerar ni de recorrer (igual que la implementación de referencia en TypeScript), así que una notificación que lleva la URI de un *sub*recurso no desaloja una lectura en caché de su padre. Si tu servidor señala los subrecursos de esta forma, vuelve a consultar el padre con `cache_mode="refresh"`.
### Configurarla: `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`: dónde viven las entradas. Por defecto es un almacén en memoria nuevo por cliente; pasa tu propia implementación de `ResponseCacheStore` (respaldada por Redis, por ejemplo) para compartir una caché entre clientes o procesos. Los tipos del contrato (`ResponseCacheStore`, `CacheKey`, `CacheEntry` y el `InMemoryResponseCacheStore` por defecto) se pueden importar desde `mcp.client`. Una búsqueda puede emitir hasta dos `get` secuenciales al almacén (la rama privada, luego la pública), así que ajusta en consecuencia las expectativas de latencia de un almacén remoto. Un almacén personalizado **exige** una `partition` explícita.
* `partition`: la etiqueta de contexto de autorización que evita que las entradas `"private"` de un principal se sirvan a otro dentro de un almacén compartido.
* `target_id`: identidad explícita del servidor, para transportes personalizados y servidores en proceso (más abajo).
* `default_ttl_ms`: TTL aplicado a los resultados que no llevan sugerencia `ttlMs`. El `0` por defecto deja sin caché los resultados sin sugerencia.
* `share_public`: sirve entre particiones las entradas que el servidor afirma como `"public"` (más abajo). Desactivado por defecto.
* `clock`: la fuente de hora de reloj, en segundos desde la época Unix. Inyecta una, como hace el ejemplo de arriba, y las pruebas de caducidad no necesitan dormir.
!!! warning "Partición = principal verificado"
Deriva `partition` de una **credencial verificada**, como el subject de un token validado. Nunca la derives de datos proporcionados por la solicitud, y nunca de la URL del servidor (la identidad del servidor es un eje de clave aparte). El SDK es una biblioteca sin autenticación propia: el ancla de confianza es quien construye el `CacheConfig`, que es el despliegue, no el inquilino. Un gateway multiinquilino crea un `CacheConfig` por cada principal autenticado.
La partición también queda fija durante toda la vida del `Client`. Si el contexto de autorización de la conexión cambia a mitad de sesión (una reautenticación como un principal distinto, por ejemplo), la caché no lo sigue; construye un nuevo `Client` para el nuevo principal.
Las claves de caché también llevan la **identidad del servidor**: la cadena de URL a la que te conectaste, sin el userinfo `user:pass@` y, por lo demás, exacta byte por byte. Sin normalizar mayúsculas, sin reordenar la query, sin limpiar la barra final. Normalizar de menos solo cuesta compartición, mientras que normalizar de más podría fusionar dos inquilinos (`?tenant=a` frente a `?tenant=b`), así que las URL superficialmente distintas simplemente no comparten entradas. Cuando no hay URL (un servidor en proceso, o una instancia de `Transport`), el cliente recibe en su lugar una identidad aleatoria por instancia; define `CacheConfig.target_id` para nombrar el servidor (con un almacén personalizado es obligatorio, y la construcción lo dice). La identidad se pasa por un hash sha256 antes de entrar en el material de la clave, así que una URL con secretos en su cadena de consulta nunca aparece en las claves del almacén. Tampoco registres tú la forma previa al hash.
!!! warning "`share_public` confía en el servidor, para toda la flota"
Por defecto, incluso las entradas `"public"` permanecen dentro de su partición. `share_public=True` sirve las entradas que el servidor marcó `cacheScope: "public"` a **todas** las particiones que usan el almacén, confiando en la clasificación del servidor en nombre de todas ellas. Un servidor que pone `"public"` a datos por inquilino (por error o por malicia) filtra entonces la respuesta de un inquilino a los demás. La opción es deliberadamente solo de nivel constructor: el `cache_mode` por llamada puede restringir la caché, pero nada por llamada puede ampliar la compartición.
### Lo que la caché nunca hace {#what-the-cache-never-does}
* **Las llamadas del nivel de sesión la omiten.** `client.session.list_tools()` y compañía siempre hacen la ida y vuelta; la caché vive en los verbos de `Client`.
* **`server/discover` queda fuera.** El resultado de discover se entrega una vez, al conectar, y nunca entra en la caché de respuestas, incluso cuando lleva un `ttlMs`. Si persistes uno tú mismo para saltarte el sondeo de reconexión ([`prior_discover`](../protocol-versions.md#reconnecting-with-prior_discover)), su vigencia es tu responsabilidad: `DiscoverResult` lleva `ttl_ms` y `cache_scope`, ya analizados, exactamente para eso.
* **Las páginas de continuación nunca se guardan en caché.** Solo participan las llamadas sin cursor. Una página de continuación rechazada por un cursor caducado sí *desaloja* el listado en caché, porque el listado cambió por debajo.
* **Las lecturas de varias idas y vueltas (multi-round-trip) nunca se guardan en caché.** Un `read_resource` iniciado con `input_responses`/`request_state`, o uno que se resuelve a través de rondas de entrada, nunca entra en la caché (un MUST de la especificación).
* **El desalojo por notificación necesita notificaciones.** El desalojo es tan bueno como la entrega del transporte, y la ruta moderna en proceso (`Client(server)` con el `mode="auto"` por defecto) hoy no entrega notificaciones independientes.
* **El desalojo es diferido, no instantáneo.** Las notificaciones de la ruta de red se despachan desde tareas lanzadas aparte, así que una llamada que compite con la llegada de una notificación puede recibir una vez más la entrada previa al desalojo; la ventana está acotada por la latencia de despacho, y el desalojo igualmente se produce.
* **Sin stale-if-error.** Una entrada caducada nunca se sirve porque la nueva consulta falló; el error se propaga.
* **Sin reconsulta anticipada.** Una entrada almacenada se sirve hasta que caduca su TTL y la siguiente llamada después de eso paga la ida y vuelta; nada se refresca en segundo plano.
* **Sin coalescencia.** Dos llamadas idénticas concurrentes son dos consultas.
* **Ningún TTL de más de 24 horas.** Un `ttlMs` mayor, ya sea enviado por el servidor o configurado, se recorta al almacenar (`mcp.client.caching.MAX_TTL_MS`), lo que acota cuánto tiempo puede servirse cualquier entrada, por generosa que sea su sugerencia.
* En un **almacén compartido**, los clientes compiten entre sí. Cada cliente descarta su propia escritura cuando un desalojo adelantó a la consulta en curso, pero un cliente *coinquilino* aún puede volver a escribir una entrada que un desalojo que nunca vio había eliminado; y esa contabilidad de carreras está acotada a su vez: pasadas 4096 claves rastreadas, primero se descarta la guarda de la clave más antigua. Ambas ventanas se aceptan, y las cierra el límite de TTL de arriba.
* **Nada se sirve entre generaciones del protocolo.** Las entradas están acotadas a la versión de protocolo negociada: en un almacén persistente compartido, una sesión nunca sirve una entrada escrita bajo otra versión negociada (el mismo listado difiere de verdad según la generación, ya que el SDK quita los campos 2026 para las sesiones más antiguas). El desalojo, igualmente, solo toca las entradas de la generación actual; las entradas de otra generación simplemente caducan por TTL.
### Leer las sugerencias por tu cuenta {#reading-the-hints-yourself}
Las sugerencias también son campos normales en cada resultado que admite caché (`result.ttl_ms` y `result.cache_scope`, ya analizados), por si quieres añadir tu propia contabilidad encima de la caché integrada (o en lugar de ella).
Contra un **servidor más antiguo** (protocolo anterior a 2026), los campos simplemente no aparecen en lo que se transmite, y los modelos muestran sus valores por defecto conservadores: `ttl_ms == 0` y `cache_scope == "private"`, caducado y sin compartir, la suposición correcta para un servidor que no declaró nada. La caché trata una sesión heredada de la misma forma: allí las sugerencias nunca se consultan (sean cuales sean las claves que aparezcan en lo que se transmite), solo se aplica `default_ttl_ms`, y su valor por defecto de `0` no guarda nada en caché, así que una conexión anterior a 2026 se comporta exactamente como antes de que existiera la caché. Si necesitas distinguir "el servidor dijo 0" de "el servidor no dijo nada", comprueba `"ttl_ms" in result.model_fields_set`: solo está definido cuando el campo llegó de verdad.
## Clientes más antiguos {#older-clients}
Los clientes con versiones del protocolo anteriores a 2026 nunca ven ninguno de los dos campos; el SDK los quita en la serialización para esas conexiones. Configura tus sugerencias una vez; no hay nada específico de versión que escribir.
## Resumen {#recap}
* Seis métodos llevan `ttlMs`/`cacheScope`; el SDK los deja por defecto en `0`/`"private"`, caducado y sin compartir, siempre seguro.
* `cache_hints={method: CacheHint(...)}` en la construcción (tanto en `MCPServer` como en `Server`) fija valores para todo el servidor por método.
* Un handler que define los campos en su resultado sobrescribe el mapa, campo por campo.
* `"public"` es una promesa de que el resultado es idéntico para todos los llamadores. No es control de acceso.
* `Client` respeta las sugerencias automáticamente: su caché de respuestas está activada por defecto, sirve entradas vigentes en lugar de volver a consultar, y no guarda nada en caché para servidores (o sesiones) que no proporcionan sugerencias.
* Por llamada, `cache_mode="refresh"` vuelve a consultar y `"bypass"` se salta la caché; `cache=None` en la construcción la desactiva por completo.

View file

@ -0,0 +1,154 @@
---
translation:
sections: [adf3c545b5be46b6, 916cd3ab1c03f461, e9be7a8d0eb0a456, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 90c6043be435fcb0]
tool: 1
---
# Callbacks del cliente {#client-callbacks}
Casi todas las solicitudes en MCP van en una sola dirección: del cliente al servidor.
Un servidor también puede pedirle cosas al **cliente**: hacerle una pregunta al usuario, muestrear el modelo del usuario, listar las carpetas del espacio de trabajo del usuario. Respondes a esas solicitudes pasando **callbacks** a `Client(...)`.
## Un servidor que pregunta {#a-server-that-asks}
Aquí tienes un servidor cuya herramienta no puede terminar por sí sola:
```python title="server.py" hl_lines="16"
--8<-- "docs_src/client_callbacks/tutorial001.py"
```
* `ctx.elicit(...)` envía una solicitud `elicitation/create` **al cliente** y espera.
* La herramienta no devuelve nada hasta que alguien (una persona en un formulario, o tu código) proporciona un `name`.
Esa es la mitad del servidor, y la página **[Elicitación](../handlers/elicitation.md)** se ocupa de ella. Esta página es el otro extremo de la conexión.
## El callback de elicitación {#the-elicitation-callback}
```python title="client.py" hl_lines="6-10 16-17"
--8<-- "docs_src/client_callbacks/tutorial002.py"
```
* Un callback de elicitación (elicitation) es `async (context, params) -> ElicitResult`.
* `params.message` es la pregunta. `params.requested_schema` es el JSON Schema de la respuesta que quiere el servidor. Un cliente real genera un formulario a partir de él; este lo rellena automáticamente.
* Devuelves `ElicitResult(action="accept", content={...})`, o `action="decline"`, o `action="cancel"`. La única otra opción es `ErrorData(...)`, que rechaza la solicitud y hace fallar toda la llamada.
* `context` es un `ClientRequestContext`: la `session` activa, el `request_id` del servidor y cualquier `meta` que haya adjuntado.
!!! tip
`params` es una unión de los dos modos de elicitación. Aquí `params.mode` es `"form"`; una solicitud
`"url"` lleva `params.url` en lugar de un esquema. Un solo callback maneja ambos; bifurca según `params.mode`.
**[Elicitación](../handlers/elicitation.md)** muestra el patrón completo.
### Pruébalo {#try-it}
Llama a `issue_card` y observa ambos extremos.
Tu callback recibe la pregunta del servidor, ya analizada:
```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'}
```
Responde, `ctx.elicit(...)` se reanuda dentro de la herramienta y la herramienta termina:
```python
result.content # [TextContent(type='text', text='Card issued to Ada Lovelace.')]
```
Un `tools/call` tuyo, un `elicitation/create` de vuelta desde el servidor, respondido por tu función, todo dentro de una sola llamada a herramienta.
!!! info
`mode="legacy"` en la llamada a `Client(...)` hace trabajo real. Por defecto, `Client(...)` negocia la ruta
moderna del protocolo, y esa ruta no tiene canal de retorno (back-channel) para las solicitudes del servidor al cliente: `ctx.elicit`
falla antes de que tu callback llegue a ejecutarse. No lo decide el transporte; lo decide el protocolo
negociado, tanto en memoria como a través de una URL. Fija `mode="legacy"` siempre que tu cliente tenga
que responder a una; todas las pruebas detrás de esta página lo hacen. **[Versiones del protocolo](../protocol-versions.md)** tiene todos los detalles.
En una sesión 2026-07-28 el callback no está muerto, se alimenta de otra forma: cuando una herramienta devuelve un
`InputRequiredResult` que lleva un `ElicitRequest`, `Client` despacha esa entrada al mismo
`elicitation_callback` y reintenta la llamada por ti. Ese flujo está en **[Solicitudes de varias idas y vueltas](../handlers/multi-round-trip.md)**.
## Un callback es una capacidad {#a-callback-is-a-capability}
Nunca le dijiste al servidor que tu cliente puede responder solicitudes de elicitación. Lo hizo el SDK.
Cuando un cliente se conecta declara sus `capabilities`, la imagen especular de las del servidor. No escribes ese objeto. **Registrar un callback es la declaración.**
| lo que pasas | lo que declara el cliente |
| --- | --- |
| `elicitation_callback=` | `"elicitation": {"form": {}, "url": {}}` |
| `sampling_callback=` | `"sampling": {}` |
| `list_roots_callback=` | `"roots": {"listChanged": true}` |
| ninguno de ellos | `{}` |
Las subcapacidades de muestreo (sampling) son el único refinamiento: pasa `sampling_capabilities=SamplingCapability(tools=SamplingToolsCapability())` junto con `sampling_callback` cuando tu muestreador maneja los parámetros `tools` / `tool_choice`. Los servidores deben ver `sampling.tools` declarado antes de poder enviarlos.
`logging_callback` y `message_handler` no están en la tabla. Manejan notificaciones, y las notificaciones no necesitan ninguna capacidad.
El servidor lee la declaración con `ctx.session.check_client_capability(...)`. Añade una herramienta que lo haga:
```python title="server.py" hl_lines="23-31"
--8<-- "docs_src/client_callbacks/tutorial003.py"
```
Conéctate solo con `elicitation_callback` y llámala:
```python
result.structured_content # {'result': ['elicitation']}
```
Pasa los tres callbacks y obtienes `['elicitation', 'sampling', 'roots']`. No pases ninguno y obtienes `[]`.
!!! check
Ahora haz lo incorrecto: conéctate **sin** `elicitation_callback` y llama a `issue_card` de todos modos.
La solicitud `elicitation/create` del servidor sigue llegando a tu cliente, y el SDK la responde por
ti, con un error, porque nunca dijiste que pudieras manejarla. Ese error hunde toda la llamada.
`call_tool` no devuelve un resultado `is_error`; lanza:
```text
MCPError: Elicitation not supported
```
Eso es un error de protocolo (`-32600`, *invalid request*), no un error de herramienta: no hay nada que
el modelo pueda leer y reintentar. Por eso vale la pena tener `client_features`: un servidor bien educado
comprueba antes de preguntar.
## El par obsoleto {#the-deprecated-pair}
`sampling_callback` responde a `sampling/createMessage`: el servidor pidiéndole a *tu* modelo que complete algo. `list_roots_callback` responde a `roots/list`: el servidor preguntando en qué directorios puede trabajar.
Ambos funcionan. Ambos siguen la regla anterior. Y ambos atienden RPC que **la especificación 2026-07-28 elimina**: un servidor moderno no llama de vuelta a tu cliente a mitad de una solicitud, te devuelve la solicitud como parte del resultado de la herramienta (**[Solicitudes de varias idas y vueltas](../handlers/multi-round-trip.md)**). Los callbacks en sí no están muertos. Cuando un `InputRequiredResult` lleva un `CreateMessageRequest` o un `ListRootsRequest`, el bucle automático de `Client` lo despacha al mismo `sampling_callback` o `list_roots_callback` que registraste aquí. La lista completa está en **[Funcionalidades obsoletas](../deprecated.md)**.
Sigues necesitando los callbacks para hablar con servidores que no han migrado. Las firmas:
```python title="client.py"
--8<-- "docs_src/client_callbacks/tutorial004.py"
```
* Un callback de muestreo recibe los `CreateMessageRequestParams` completos (`messages`, `model_preferences`, `max_tokens`) y devuelve un `CreateMessageResult`. *Tú* ejecutas el modelo, como prefieras; el SDK solo transporta la solicitud.
* Un callback de roots no recibe ningún parámetro y devuelve un `ListRootsResult`.
* Cualquiera de los dos puede devolver `ErrorData(...)` en su lugar, para rechazar.
Pásalos a `Client(...)` exactamente igual que `elicitation_callback`.
## Los callbacks de notificaciones {#the-notification-callbacks}
Dos más. Ninguno declara nada.
`logging_callback` recibe el `notifications/message` que envía un servidor, como `LoggingMessageNotificationParams` (`level`, `logger`, `data`). El logging del protocolo está a su vez obsoleto según la especificación 2026-07-28 (**[Logging](../handlers/logging.md)** explica qué hacer en su lugar), así que este callback existe para los servidores que todavía lo emiten. En una conexión de la generación 2026 el callback por sí solo no te da nada, porque los servidores 2026 envían mensajes de log solo a las solicitudes que lo piden: pasa `log_level="info"` (u otro nivel) a `Client(...)` para marcar esa preferencia en cada solicitud y recibir ese nivel y los superiores. Los servidores anteriores a 2026 lo ignoran y mantienen su comportamiento de `logging/setLevel`.
`message_handler` es el comodín: toda notificación del servidor que la sesión expone le llega (además de a su callback específico), y en un transporte basado en flujos también toda `Exception` a nivel de transporte. Dos nunca llegan: `notifications/cancelled` la aplica el SDK en lugar de exponerla, y la confirmación de suscripción de un flujo `listen()` activo la consume ese flujo. Anota el parámetro con `IncomingMessage` (`ServerNotification | Exception`, exportado desde `mcp.client`). El único patrón que vale la pena conocer es `if isinstance(message, Exception): raise message`, para que una conexión rota falle de forma visible en lugar de desvanecerse.
## Resumen {#recap}
* Un servidor puede enviar solicitudes al cliente. Las respondes con callbacks pasados a `Client(...)`.
* El callback de elicitación es el vigente: `async (context, params) -> ElicitResult`, una sola función para los modos formulario y URL.
* **Registrar un callback es declarar la capacidad.** Sin él, el SDK rechaza la solicitud del servidor en tu nombre y toda la llamada falla con `MCPError`.
* Un servidor lo averigua antes de preguntar con `ctx.session.check_client_capability(...)`.
* `sampling_callback` y `list_roots_callback` funcionan igual pero atienden funcionalidades obsoletas; los servidores modernos usan solicitudes de varias idas y vueltas en su lugar.
* `logging_callback` y `message_handler` reciben notificaciones. No declaran nada.
El primer argumento de `Client(...)` es un objeto de transporte. **[Transportes del cliente](transports.md)** cubre todos los tipos.

View file

@ -0,0 +1,153 @@
---
translation:
sections: [a91322c46111d16d, 8e6fd6d6f59bb568, e7828fd2729b2c9d, a03ec26bfc678b65, 1034c653c0bcf1b0]
tool: 1
---
# Aserción de identidad {#identity-assertion}
Un proveedor OAuth ordinario (**[Clientes OAuth](oauth-clients.md)**) empieza por hacerle una pregunta al servidor MCP: *¿en qué servidor de autorización confías?* Sigue la respuesta adonde apunte y, a partir de ahí, o bien una persona inicia sesión o bien un secreto compartido de antemano ocupa su lugar.
Una empresa no quiere que ninguna de las dos cosas se decida servidor por servidor. Ya tiene un proveedor de identidad en marcha (Okta, Microsoft Entra ID, el tuyo propio); el usuario ya inició sesión en él esta mañana; y es el único lugar donde el equipo de seguridad quiere decidir quién puede acceder a qué. [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990), la extensión **Enterprise-Managed Authorization**, traslada la decisión allí. El IdP firma un JWT de corta duración, un **Identity Assertion JWT Authorization Grant**, el **ID-JAG**: una declaración de que *este usuario*, a través de *este cliente*, puede acceder a *este servidor MCP*. El cliente lo intercambia por un token de acceso ordinario. Sin navegador, sin pantalla de consentimiento, sin registro dinámico.
Esta página cubre los dos extremos de ese intercambio. El servidor MCP en sí nunca cambia: sigue siendo el servidor de recursos de **[Autorización](../run/authorization.md)**, que comprueba cualquier token que le llegue.
## Dos solicitudes de token {#two-token-requests}
Hay dos autoridades distintas en juego, y saber distinguirlas por su nombre es casi todo lo que hace falta para entender esta página. El **IdP de la empresa** es el proveedor de identidad de tu organización: sabe quién es el empleado, es donde vive la política y es quien emite el ID-JAG. El SDK nunca habla con él. El **servidor de autorización MCP** es la misma parte que era en **[Autorización](../run/authorization.md)**: el emisor nombrado en los metadatos del servidor MCP, lo que acuña los tokens que ese servidor MCP acepta. En un flujo OAuth ordinario, esos dos roles suelen ser una sola caja. Aquí son dos, y toda la concesión consiste en que el segundo acepte confiar en el primero.
El cliente hace una solicitud de token a cada uno.
1. **Al IdP de la empresa.** El cliente intercambia el inicio de sesión del usuario (su token de ID de OpenID Connect) por el ID-JAG. Es un intercambio de tokens de [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693), es por completo la API de tu IdP y **el SDK no lo hace**. Lo haces tú, dentro de un callback asíncrono. Es también donde ocurre la decisión de política: un IdP que dice que no nunca emite el ID-JAG, y no hay nada que presentar.
2. **Al servidor de autorización MCP.** El cliente presenta el ID-JAG bajo la concesión `jwt-bearer` de [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523) (`grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer`, con el ID-JAG como `assertion`) y recibe el token de acceso. **Esta es la solicitud que hace el SDK**, y aceptarla es lo único que esta página añade a un servidor de autorización.
Todo lo que sigue es la segunda solicitud: el cliente que la envía y el servidor de autorización que la responde.
## El cliente {#the-client}
**`IdentityAssertionOAuthProvider`** vive en `mcp.client.auth.extensions.identity_assertion`. Como todos los proveedores de **[Clientes OAuth](oauth-clients.md)**, es un `httpx2.Auth`: construyes uno, lo pones en `auth=` y le pasas el `httpx2.AsyncClient` al transporte.
```python title="client.py" hl_lines="49-50 53-61"
--8<-- "docs_src/identity_assertion/tutorial001.py"
```
Léelo desde abajo.
* `main()` es el `main()` estándar de un cliente OAuth (**[Clientes OAuth](oauth-clients.md)**), sin cambiar una sola línea. Esa es la idea: una vez que existe el proveedor, nada de lo que viene después sabe qué concesión produjo el token.
* El proveedor recibe lo que los demás proveedores no pueden descubrir: un `client_id` y un `client_secret` que alguien **registró de antemano** en el servidor de autorización, el `issuer` de ese servidor de autorización y `assertion_provider`, un callback asíncrono que devuelve un ID-JAG nuevo cuando se le pide.
* `storage` es el mismo protocolo `TokenStorage`. Solo se llama a los dos métodos de tokens; aquí no hay registro dinámico, así que no hay ningún `client_info` que recordar.
### El proveedor de aserciones {#the-assertion-provider}
`fetch_id_jag(audience, resource)` es el único código que escribes. Se espera una vez por intercambio de tokens, nunca en la construcción, y solo *después* de que los metadatos del servidor de autorización se hayan obtenido y validado, de modo que un emisor mal configurado nunca filtra una aserción. Sus dos argumentos son dos de los claims con los que debe acuñarse el ID-JAG: `audience` es el emisor del servidor de autorización (el `aud` del ID-JAG) y `resource` es el identificador canónico del servidor MCP (el `resource` del ID-JAG). El tercero ya lo tienes: el claim `client_id` del ID-JAG debe nombrar el `client_id` que le diste al proveedor, o el servidor de autorización rechaza el intercambio.
`idp_issue_id_jag`, justo encima, **no es tu código**. Hace las veces del proveedor de identidad y firma la aserción dentro del mismo proceso para que el archivo esté completo y puedas leer cada claim que lleva un ID-JAG. Un `fetch_id_jag` real hace, en cambio, la primera solicitud de token de la sección anterior: un intercambio de tokens de [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) contra tu IdP, definido por el borrador Identity Assertion JWT Authorization Grant que [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) perfila. El token de ID del usuario que inició sesión entra como `subject_token`, el `requested_token_type` es el URN propio del ID-JAG (`urn:ietf:params:oauth:token-type:id-jag`), `audience` y `resource` pasan tal cual, y la respuesta trae el ID-JAG. Ese intercambio, con esos nombres, es lo que debes buscar en la documentación de tu IdP.
!!! tip
Se solicita un ID-JAG nuevo en cada intercambio, y esa es la idea: es una concesión de un
solo uso que vive minutos, y el servidor de autorización de esta página se niega a aceptar el
mismo dos veces. No lo guardes en caché. Lo que se reutiliza es el token de acceso que te compra.
### El emisor es configuración {#the-issuer-is-configuration}
Aquí está la inversión. `OAuthClientProvider` le pregunta al servidor de recursos qué servidor de autorización usar y sigue la respuesta adonde apunte. Este proveedor se niega a hacerlo: `issuer` es obligatorio, los metadatos de [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) se obtienen de la ruta well-known propia de ese emisor, el endpoint de token debe estar en el origen de ese emisor y al servidor de recursos nunca se le pregunta nada.
La extensión no exige esto; es una elección deliberadamente más estricta. Este cliente lleva dos cosas que vale la pena robar, un secreto registrado de antemano y una aserción vinculada a una audiencia, y un cliente que dejara que un servidor MCP comprometido lo dirigiera al servidor de autorización de un atacante le enviaría ambas. Fijar el emisor en la construcción elimina esa conversación.
!!! warning
El `issuer` configurado se compara con el campo `issuer` del documento de metadatos mediante la
comparación simple de cadenas de RFC 8414 §3.3: carácter por carácter, barra final incluida,
sin normalización. No lo adivines. Obtén `/.well-known/oauth-authorization-server` de tu
servidor de autorización y copia el valor `issuer` que devuelve. Para el servidor de
autorización de esta página es `https://auth.example.com/`, con la barra, porque su emisor se
construyó a partir de un objeto URL de pydantic. Una discrepancia detiene el flujo en
`OAuthFlowError: Authorization server metadata issuer
mismatch` antes de que se envíe una sola credencial o aserción.
### Un cliente confidencial {#a-confidential-client}
`client_secret` es obligatorio; el constructor lanza `ValueError` si falta. El perfil del IETF que hay debajo de [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) reserva esta concesión para clientes confidenciales, SEP-990 exige que el cliente se autentique, y este SDK hace cumplir ambas cosas insistiendo en un secreto compartido. `token_endpoint_auth_method` elige por dónde viaja: `client_secret_post` (el valor por defecto, en el cuerpo del formulario) o `client_secret_basic` (una cabecera HTTP Basic). El perfil también permite `private_key_jwt`; este proveedor no lo admite.
!!! tip
Lee `client_secret` del entorno o de un gestor de secretos, nunca del control de versiones.
### Lo que el proveedor hace por ti {#what-the-provider-does-for-you}
La primera solicitud sale sin autenticar, y el `401` del servidor inicia el flujo.
1. **Descubrimiento.** Obtiene los metadatos del servidor de autorización de la ruta well-known de [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) del emisor configurado, comprueba que el `issuer` del documento coincide y comprueba que el endpoint de token está en el origen del emisor.
2. **La aserción.** Espera tu `assertion_provider`.
3. **Intercambio.** Envía con POST la concesión `jwt-bearer` al endpoint de token, guarda el `OAuthToken` y repite tu solicitud original con `Authorization: Bearer ...`.
Un `403` cuyo `WWW-Authenticate` indica `insufficient_scope` ejecuta los pasos 2 y 3 de nuevo con la unión de tu `scope` y el reclamado. (`scope` nunca es más que una petición; el servidor de autorización de esta página concede lo que dice el ID-JAG y nada más.) No hay token de actualización en ninguna parte de esto: cuando el token de acceso caduca, el siguiente `401` acuña un ID-JAG nuevo y vuelve a intercambiar, y *esa* es la palanca que tiene el IdP. Los fallos son las mismas dos excepciones que en el resto de **[Clientes OAuth](oauth-clients.md)**: `OAuthFlowError` para el descubrimiento y la validación, y su subclase `OAuthTokenError` cuando el endpoint de token dice que no.
## El servidor de autorización {#the-authorization-server}
La mayoría de las veces te detienes aquí. El servidor de autorización MCP es el producto de otra persona, aceptar ID-JAG es una configuración suya que hay que activar, y la mitad de [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) que le toca al SDK es el cliente de arriba.
El SDK también puede *ser* el servidor de autorización: `create_auth_routes` devuelve las rutas del servidor de autorización como una lista que cualquier app de Starlette puede montar, que es como `examples/servers/simple-auth/` en el repositorio ejecuta uno. SEP-990 añade una bandera y un método a esa superficie:
```python title="auth_server.py" hl_lines="48-50 105-107"
--8<-- "docs_src/identity_assertion/tutorial002.py"
```
* `identity_assertion_enabled=True` lo controla todo. Desactivado, que es el valor por defecto, `/token` responde a esta concesión con `unsupported_grant_type` aunque hayas implementado el hook, y los metadatos no la mencionan. Activado, los metadatos ganan el tipo de concesión `jwt-bearer` y listan `urn:ietf:params:oauth:grant-profile:id-jag` en `authorization_grant_profiles_supported`, el campo que la extensión usa para anunciar la compatibilidad. (El cliente de este SDK nunca lo lee: está aprovisionado para un solo emisor y simplemente pregunta.)
* **`exchange_identity_assertion`** es el hook. Antes de que se ejecute, el SDK ha autenticado al cliente, ha rechazado los clientes públicos y ha rechazado los clientes cuyo registro no lista la concesión. Recibes un `IdentityAssertionParams` (la `assertion` sin procesar, los `scopes` solicitados y el `resource`) y devuelves un `OAuthToken` simple.
* El registro dinámico de clientes rechaza esta concesión sin excepciones, así que `get_client` aquí sirve un cliente aprovisionado a mano. Un cliente ID-JAG no puede registrarse a sí mismo para existir.
* La mitad de la clase son rechazos. `OAuthAuthorizationServerProvider` es el servidor de autorización *completo*, así que también pide el flujo de código de autorización; un servidor que además inicia la sesión de los usuarios implementa esos de verdad, y este tiene exactamente una puerta.
!!! warning
El SDK nunca decodifica la aserción: solo tu despliegue sabe en qué IdP confía y qué claves
publica ese IdP, así que todo lo que hay dentro de `exchange_identity_assertion` es esencial.
Verifica la firma contra las claves publicadas del IdP (su JWKS; el secreto compartido de aquí
es el de la demo), así como `iss` y `exp`, según [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523) §3. Exige que el `typ` de la cabecera
del JWT sea `oauth-id-jag+jwt`, la protección del perfil contra que algún otro JWT se reenvíe
como concesión. Exige que `aud` sea tu propio emisor. Exige que el claim `client_id` del ID-JAG
sea igual al cliente que el handler autenticó, y que su claim `resource` nombre un recurso que
realmente sirves. Lleva registro de `jti` hasta el `exp` de la aserción para que se acepte una
sola vez. Y toma los scopes concedidos y, sobre todo, el `resource` del token emitido del ID-JAG
validado, nunca de la solicitud: `params.resource` es lo que sea que el cliente escribió. Las
reglas de procesamiento completas están en la
[especificación Enterprise-Managed Authorization](https://modelcontextprotocol.io/extensions/auth/enterprise-managed-authorization).
Rechaza una aserción inválida con `TokenError("invalid_grant", ...)`. El otro código de error de este flujo es `invalid_target`: un ID-JAG que nombra un recurso que no sirves se rechaza con él, que es lo que impide que este servidor acuñe tokens para el de otra persona. Y los scopes concedidos salen del claim `scope` del ID-JAG (una aserción sin él también se rechaza); el tuyo podría mapear los grupos del usuario en su lugar.
Y fíjate en lo que el `OAuthToken` devuelto no lleva: un token de actualización. El IdP decide cuánto tiempo conserva el acceso este usuario decidiendo si emite el siguiente ID-JAG. Un token de actualización acuñado aquí le devolvería en silencio esa decisión.
!!! info
Un servidor que todavía incrusta su servidor de autorización con `auth_server_provider=` llega al
mismo código a través de `AuthSettings(identity_assertion_enabled=True)`. **[Autorización](../run/authorization.md)** explica
por qué los servidores nuevos no deberían empezar por ahí.
!!! check
Conecta los dos archivos de esta página entre sí y toda la concesión es un solo `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"}
```
Sin `/authorize`, sin `/register`, sin obtener los metadatos del recurso protegido. Las únicas
solicitudes que se transmiten son la que provocó el `401`, la obtención del well-known, este
intercambio y luego tráfico MCP ordinario con el bearer adjunto. Y el `sub` que tu validador
leyó del ID-JAG es exactamente lo que `get_access_token().subject` informa dentro de una herramienta.
### Pruébalo {#try-it}
`examples/stories/identity_assertion/` en el repositorio del SDK es esta página funcionando de verdad: el mismo validador `exchange_identity_assertion`, un servidor MCP protegido por sus tokens, un IdP sustituto y el cliente, en un solo programa que se verifica a sí mismo. `uv run python -m stories.identity_assertion.client --http` ejecuta todo el intercambio y comprueba que el usuario que nombró el IdP es el usuario que ve la herramienta.
## Resumen {#recap}
* [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) permite que el proveedor de identidad de la empresa, y no el usuario final, decida a qué servidores MCP puede acceder un cliente. El IdP firma esa decisión en un **ID-JAG**.
* Obtener el ID-JAG es un intercambio de tokens de [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) contra *tu IdP*, y el SDK no lo hace. Presentarlo al servidor de autorización MCP es la concesión `jwt-bearer` de [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523), y el SDK cubre los dos lados de eso.
* `IdentityAssertionOAuthProvider` es otro `httpx2.Auth`: un cliente confidencial registrado de antemano, un `issuer` fijado y un callback `assertion_provider(audience, resource)`. Sin navegador, sin registro, sin token de actualización.
* El servidor de autorización nunca se descubre desde el servidor de recursos. Configura `issuer` con exactamente la cadena que sirve su documento de metadatos; la comparación es carácter por carácter.
* Del lado del servidor, `identity_assertion_enabled=True` más `exchange_identity_assertion`. El SDK autentica al cliente y controla la concesión; validar el ID-JAG es enteramente cosa tuya, y el token emitido queda vinculado al `resource` del ID-JAG, no al de la solicitud.
La única parte que esta página nunca tocó es el servidor MCP. Lo que hace con el token que acabas de acuñar ya lo hacía en **[Autorización](../run/authorization.md)**.

View file

@ -0,0 +1,217 @@
---
translation:
sections: [ebef1e7a0df854f4, a4c687d3d627d516, 8e79141fc2985342, b345dd05b9c3c7ab, 80ce41579825a6fa, 5f0fa90494de8f65, 83d10514eaa62fa5, 9190555aa39a5d28, 84a4c9d8bf14dddb, 927d71cf40b58c30]
tool: 1
---
# El cliente {#the-client}
Un **`Client`** es la forma en que un programa de Python se comunica con un servidor MCP.
Es un solo objeto con un solo ciclo de vida: lo construyes, entras en `async with`, llamas a sus métodos. Cada verbo del protocolo (listar las herramientas, llamar a una, leer un recurso, renderizar un prompt) es un método `async` del objeto que devuelve un resultado tipado.
## Tu primer cliente {#your-first-client}
```python title="client.py" hl_lines="14-18"
--8<-- "docs_src/client/tutorial001.py"
```
El servidor del principio solo está ahí para que tengas algo a lo que conectarte. El cliente son las cinco líneas resaltadas.
* `Client(mcp)` recibe **el propio objeto servidor**. Ese es el transporte en memoria: sin subproceso, sin puerto, sin HTTP. Así se conectan todos los ejemplos de esta página y todas las pruebas que escribas.
* `async with` es el **ciclo de vida**. Al entrar se conecta y negocia; al salir se desconecta. No hay un par `connect()` / `close()`, y un `Client` no se puede reutilizar una vez que termina el bloque.
* Dentro del bloque, los datos de la conexión ya están ahí como propiedades simples.
### Qué puedes pasarle a `Client` {#what-you-can-pass-to-client}
`Client` recibe un solo argumento posicional y resuelve el transporte a partir de su tipo:
* Una instancia de `MCPServer` (o del `Server` de bajo nivel): se conecta **en el mismo proceso**.
* Una cadena con una URL (`Client("http://localhost:8000/mcp")`): Streamable HTTP, el camino de producción.
* Un **transporte**: cualquier cosa que puedas usar con `async with ... as (read, write)`, como `stdio_client(...)` envolviendo un subproceso.
Todo lo demás en esta página es idéntico en los tres casos. Los encabezados, los subprocesos, los timeouts y el protocolo `Transport` tienen su propia página: **[Transportes del cliente](transports.md)**.
### Qué hay en un cliente conectado {#whats-on-a-connected-client}
Cuatro propiedades de solo lectura, que se rellenan en cuanto entras en el bloque:
* `client.server_info`: la identidad del servidor, o `None` para un servidor de la generación 2026 que no la informa (los servidores de python-sdk lo hacen por defecto). Aquí `server_info.name` es `"Bookshop"` y `server_info.version` es lo que el servidor informe.
* `client.server_capabilities`: lo que el servidor puede hacer (`tools`, `resources`, `prompts`, `completions`, ...). Una capacidad que el servidor no tiene es `None`.
* `client.protocol_version`: la versión del protocolo que acordaron las dos partes. Aquí es `"2026-07-28"`.
* `client.instructions`: la cadena `instructions=` del servidor, o `None` si no definió una.
Nunca elegiste una versión del protocolo. Por defecto, el `Client` sondea el servidor y recurre al handshake clásico con los más antiguos, así que un mismo cliente funciona contra servidores de cualquier generación. Cuando necesites controlar eso, **[Versiones del protocolo](../protocol-versions.md)** tiene todos los detalles.
!!! tip
`client.session` es la `ClientSession` subyacente, la vía de escape de bajo nivel.
No la necesitarás para nada de esta página.
## Listar herramientas {#listing-tools}
```python title="client.py" hl_lines="15-20"
--8<-- "docs_src/client/tutorial002.py"
```
`list_tools()` devuelve un `ListToolsResult`; las herramientas están en `.tools`. Cada una es la definición completa que un host le entregaría a un modelo:
```python
tool.name # 'search_books'
tool.title # 'Search the catalog'
tool.description # 'Search the catalog by title or author.'
```
y `tool.input_schema` es el JSON Schema que el servidor derivó de las anotaciones de tipo de la función:
```json
{
"type": "object",
"properties": {
"query": {"title": "Query", "type": "string"},
"limit": {"default": 10, "title": "Limit", "type": "integer"}
},
"required": ["query"],
"title": "search_booksArguments"
}
```
Ese esquema es todo lo que una UI necesita para renderizar un formulario de argumentos, y todo lo que un modelo necesita para producir argumentos válidos.
!!! tip
`title` es opcional, así que una UI que muestra herramientas a una persona tiene que elegir: el `title` si lo hay,
el `name` si no. `from mcp.shared.metadata_utils import get_display_name` hace exactamente eso,
para herramientas, recursos, plantillas de recursos y prompts.
## Llamar a una herramienta {#calling-a-tool}
`call_tool(name, arguments)` ejecuta la herramienta y te devuelve un `CallToolResult`.
```python title="client.py" hl_lines="26-33"
--8<-- "docs_src/client/tutorial003.py"
```
El `lookup_book` del servidor devuelve un `Book` de Pydantic. Esto es lo que ve el cliente:
```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
```
Un solo valor de retorno, tres cosas que leer. Cada una tiene un consumidor distinto.
### `content`: lo que lee el modelo {#content-what-the-model-reads}
`content` es una `list` de **bloques de contenido**, y un bloque de contenido es una unión: `TextContent`, `ImageContent`, `AudioContent`, `ResourceLink` o `EmbeddedResource`. Una herramienta puede devolver varios, de distintos tipos.
Por eso `main` acota el tipo con `isinstance(block, TextContent)` antes de tocar `block.text`. Fíjate en que no hay ningún `.text` fuera del `isinstance`: el verificador de tipos no lo permite, porque `ImageContent` tiene `.data`, no `.text`. La unión es honesta sobre lo que una herramienta puede enviarte; tu código también debería serlo.
### `structured_content`: lo que lee tu aplicación {#structured_content-what-your-application-reads}
`structured_content` es el valor de retorno de la herramienta en JSON, conforme al `output_schema` que declara la herramienta. Sin analizar cadenas, sin adivinar.
Cuando ambos están presentes dicen lo mismo dos veces a propósito: `content` es para un modelo, `structured_content` es para el código. De dónde sale la mitad estructurada, y cómo controlarla, está en la página **[Salida estructurada](../servers/structured-output.md)**.
### `is_error`: si la herramienta falló {#is_error-whether-the-tool-failed}
Una herramienta que lanza una excepción **no** la lanza en tu cliente. Vuelve como un resultado normal con `is_error=True`.
!!! check
Pídele `"Solaris"` a `lookup_book` (un título que no está en el catálogo) y la función lanza
`ValueError`. Aun así, la llamada devuelve un resultado normal:
```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
```
El mensaje de la excepción acabó en `content`, donde el **modelo** puede leerlo y volver a intentarlo. Es
deliberado: un error de herramienta es parte de la conversación, no un fallo fatal. Mira siempre `is_error`
antes de confiar en `structured_content`.
!!! warning
`is_error=True` cubre más que tu propio `raise`. Pide una herramienta que el servidor ni siquiera tiene
(`call_tool("does_not_exist", {})`) y no se lanza nada. Recibes la misma forma de vuelta,
`is_error=True` con `Unknown tool: does_not_exist` en `content`. Un método de `Client` lanza
`MCPError` solo cuando el servidor responde con un **error** JSON-RPC en lugar de un resultado, y
**[Manejo de errores](../servers/handling-errors.md)** explica cuándo un servidor produce cada cosa.
## Recursos {#resources}
Los verbos de recursos vienen en pares: dos formas de listar, una de leer.
```python title="client.py" hl_lines="22-31"
--8<-- "docs_src/client/tutorial004.py"
```
* `list_resources()` devuelve los recursos **concretos**, los que tienen una URI fija. Aquí: `['catalog://genres']`.
* `list_resource_templates()` devuelve los **parametrizados**. Aquí: `['catalog://genres/{genre}']`. Son dos listas distintas porque una plantilla no se puede leer hasta que la rellenas.
* `read_resource(uri)` recibe una URI como `str` simple y funciona con ambos: pasa `"catalog://genres/poetry"` y el servidor la hace coincidir con la plantilla.
`read_resource` devuelve `contents`, una lista de `TextResourceContents` o `BlobResourceContents`. La misma idea que con el contenido de las herramientas: acota con `isinstance` y luego lee `.text` (o `.blob`).
A un cliente también se le puede avisar cuando cambia un recurso. En conexiones de la generación 2025 eso es `subscribe_resource(uri)` / `unsubscribe_resource(uri)`, un par de métodos que `MCPServer` no implementa, así que con el protocolo 2026-07-28 (donde esos verbos ya no existen) la solicitud responde `-32601`, *Method not found*. El reemplazo de 2026 es un stream `subscriptions/listen`, que `MCPServer` *sí* sirve (allí `server_capabilities.resources.subscribe` es `True`), y cómo consumirlo con `client.listen(...)` es la página **[Suscripciones](subscriptions.md)** de esta sección.
## Prompts {#prompts}
```python title="client.py" hl_lines="15-20"
--8<-- "docs_src/client/tutorial005.py"
```
`list_prompts()` te dice qué ofrece el servidor y qué necesita cada prompt:
```python
prompt.name # 'recommend'
prompt.title # 'Recommend a book'
prompt.arguments # [PromptArgument(name='genre', required=True)]
```
`get_prompt(name, arguments)` lo renderiza. El diccionario de argumentos es `str -> str`: los argumentos de un prompt siempre son cadenas. El resultado es `messages`, una lista de `PromptMessage`, cada uno con un `role` y un bloque `content`:
```python
message.role # 'user'
message.content # TextContent(type='text', text='Recommend one poetry book from the catalog and say why.')
```
Un host le entrega esos mensajes directamente al modelo. Esa es toda la funcionalidad.
## Autocompletado {#completions}
Un servidor con un handler de autocompletado puede autocompletar argumentos de prompts y de plantillas de recursos mientras el usuario escribe.
```python title="client.py" hl_lines="27-31"
--8<-- "docs_src/client/tutorial006.py"
```
* `ref` dice *qué* prompt o plantilla estás rellenando: un `PromptReference` o un `ResourceTemplateReference`.
* `argument` es `{"name": ..., "value": ...}`: el argumento y lo que el usuario ha escrito hasta ahora.
La respuesta está en `result.completion.values`. Escribe `"p"` y el servidor devuelve `['poetry']`. El lado del servidor, y cómo un handler usa los *otros* argumentos ya rellenados para acotar sus sugerencias, es la página **[Autocompletado](../servers/completions.md)**.
## Paginación {#pagination}
Cada método `list_*` acepta un argumento nombrado `cursor=` y cada resultado trae un `next_cursor`. Cuando `next_cursor` es `None`, ya lo tienes todo.
```python title="client.py" hl_lines="22-30"
--8<-- "docs_src/client/tutorial007.py"
```
Este bucle es correcto contra cualquier servidor. `MCPServer` devuelve todo en una sola página, así que `next_cursor` es `None` y el bucle se ejecuta una vez; por eso la mayoría del código nunca lo escribe. Los servidores que realmente paginan, y las reglas que siguen los cursores, están en **[Paginación](../advanced/pagination.md)**.
## En las pruebas {#in-tests}
`Client(mcp)`, sin proceso y sin puerto, ya es un banco de pruebas para tu servidor.
Hay una opción del constructor pensada para eso: `Client(mcp, raise_exceptions=True)`. Solo tiene efecto en conexiones en memoria, y **[Pruebas](../get-started/testing.md)** es la página que la explica y construye todo el patrón a su alrededor.
## Resumen {#recap}
* `Client(x)` se conecta en memoria a un objeto servidor, por Streamable HTTP a una cadena con una URL, y por cualquier otra cosa mediante un transporte.
* `async with` es todo el ciclo de vida. Dentro, `server_capabilities` y `protocol_version` ya están rellenas; `server_info` e `instructions` también, cuando el servidor las proporciona.
* `list_tools()` te da el `name`, `title`, `description` e `input_schema` de cada herramienta.
* `call_tool()` devuelve `content` para el modelo, `structured_content` para tu código, e `is_error`. Una herramienta que lanza una excepción es un resultado, no una excepción.
* `content` es una unión de tipos de bloque; acota con `isinstance` antes de leer.
* `list_resources` / `list_resource_templates` / `read_resource`, `list_prompts` / `get_prompt` y `complete` completan los verbos.
* Cada `list_*` acepta `cursor=`; itera hasta que `next_cursor` sea `None`.
Lo que un servidor puede pedirle al *cliente*, y cómo le respondes, está en **[Callbacks del cliente](callbacks.md)**.

View file

@ -0,0 +1,152 @@
---
translation:
sections: [c6899d3892bd9fa0, 79372cff3cc48a88, 63878d29e87c3e73, 13175843d3588af4, e7e2b9fd516f77de, 758f06399b513c1f, a05d7278487d610b]
tool: 1
---
# Clientes OAuth {#oauth-clients}
Algunos servidores MCP están protegidos. Envíales una solicitud sin token y responden `401 Unauthorized`.
**`OAuthClientProvider`** es la forma de conseguir el token. No es un objeto de MCP en absoluto. Es un `httpx2.Auth`, el hook estándar de httpx2 para "hacer algo con cada solicitud". Lo asocias a un `httpx2.AsyncClient`, le pasas ese cliente al transporte Streamable HTTP y dejas de pensar en ello.
Esta página es el lado del cliente. Hacer que tu propio servidor exija un token es **[Autorización](../run/authorization.md)**.
## El proveedor {#the-provider}
```python title="client.py" hl_lines="44-54"
--8<-- "docs_src/oauth_clients/tutorial001.py"
```
Le das cuatro cosas:
* `server_url`: el endpoint MCP al que te conectas. El proveedor descubre todo lo demás a partir de él.
* `client_metadata`: lo que escribirías en el formulario de "registrar una aplicación" de un servidor de autorización.
* `storage`: dónde viven los tokens entre ejecuciones.
* `redirect_handler` y `callback_handler`: los dos momentos en los que interviene un humano.
Nada más en el archivo menciona OAuth. `main()` nunca ve un token.
### Metadatos del cliente {#client-metadata}
`OAuthClientMetadata` es el documento de registro real de [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591), como modelo de Pydantic.
Defines tres campos. Los valores por defecto completan el resto: `grant_types` ya es `["authorization_code", "refresh_token"]` y `response_types` ya es `["code"]`, que es exactamente el flujo que ejecuta este proveedor.
!!! check
Al ser un modelo de Pydantic, valida **antes de que un solo byte salga a la red**.
Omite `redirect_uris` y la construcción falla en el acto con un `ValidationError` que
nombra el campo:
```text
redirect_uris
Field required [type=missing, input_value={'client_name': 'Bookshop Agent'}, input_type=dict]
```
No se abre ningún navegador ni queda un registro a medias en el servidor de autorización.
### Almacenamiento de tokens {#token-storage}
**`TokenStorage`** es un `Protocol` con cuatro métodos asíncronos. No heredas de nada; escribe los métodos y cualquier clase es un almacén de tokens:
* `get_tokens` / `set_tokens` guardan el `OAuthToken`: token de acceso, token de actualización, caducidad, scope.
* `get_client_info` / `set_client_info` guardan el `OAuthClientInformationFull` que el servidor de autorización emitió cuando el proveedor te registró, incluido tu `client_id`.
La versión en memoria de arriba funciona. También olvida todo cuando el proceso termina, así que la siguiente ejecución repite todo el proceso. Persístelo en un archivo o en el llavero de tu plataforma y la siguiente ejecución transcurre en silencio.
!!! tip
Guarda `client_info`, no solo los tokens. El proveedor se registra dinámicamente la primera vez que
no encuentra un `client_info` almacenado. Si lo descartas, generas un registro nuevo en cada ejecución.
### Los dos handlers {#the-two-handlers}
El flujo de código de autorización necesita un humano exactamente una vez: alguien tiene que iniciar sesión y hacer clic en "permitir".
* **`redirect_handler`** se espera con la URL de autorización ya construida por completo. El `client_id`, el `redirect_uri`, el `state` y el desafío PKCE ya están en ella. Tu único trabajo es llevar un navegador hasta allí. Una app de escritorio llama a `webbrowser.open`; este archivo la imprime.
* **`callback_handler`** se espera a continuación. Aguarda hasta que el usuario vuelve a tu `redirect_uri` y devuelve los parámetros de consulta de esa redirección como un `AuthorizationCodeResult`.
Un cliente real ejecuta un pequeño servidor HTTP local en el URI de redirección en lugar de llamar a `input()`. La forma es idéntica: recibe la redirección y devuelve `code`, `state` e `iss`.
!!! warning
Pasa `state` e `iss` exactamente como llegaron. El proveedor compara `state` con el que
generó e `iss` con el emisor que descubrió, y rechaza cualquier discrepancia. Son las defensas
contra CSRF y contra la confusión de servidores.
### Dentro del `Client` {#into-the-client}
Mira `main()`. El proveedor va en el **cliente httpx2**, el cliente httpx2 va en `streamable_http_client(url, http_client=...)` y ese transporte va en `Client`.
`streamable_http_client` no tiene argumento nombrado `auth=`. Todo lo que es de nivel HTTP (autenticación, cabeceras, timeouts, proxies) pertenece al `httpx2.AsyncClient` que traes. Esa organización en capas está en **[Transportes del cliente](transports.md)**.
## Lo que el proveedor hace por ti {#what-the-provider-does-for-you}
La primera vez que `Client` envía una solicitud, el servidor responde `401`. El proveedor toma el control:
1. **Descubrimiento.** Lee la cabecera `WWW-Authenticate`, obtiene los Protected Resource Metadata del servidor desde `/.well-known/oauth-protected-resource`, averigua qué servidor de autorización protege este recurso y obtiene los metadatos de *ese* servidor.
2. **Registro.** ¿No hay nada en el almacenamiento? Te registra dinámicamente con tu `OAuthClientMetadata` y guarda el resultado.
3. **Autorización.** Genera el par PKCE y un `state`, construye la URL de autorización, espera tu `redirect_handler` y luego espera tu `callback_handler` para obtener el código.
4. **Intercambio.** Cambia el código por un `OAuthToken`, lo guarda y repite tu solicitud original con `Authorization: Bearer ...`.
Después de eso, se queda callado. Los tokens salen del almacenamiento, un token de acceso caducado se renueva con el token de actualización y solo cuando nada de eso funciona vuelve a ejecutar el flujo.
No escribiste nada de eso. Quedan dos argumentos nombrados (`client_metadata_url` y `validate_resource_url`), y este archivo no necesita ninguno. `client_metadata_url` es el que vale la pena conocer; tiene su propia sección más abajo.
### Pruébalo {#try-it}
La mayoría de los ejemplos de esta documentación puedes comprobarlos con un `Client(server)` en memoria. Este no: todo el sentido del flujo es un `401` HTTP, y no hay HTTP entre un cliente en memoria y su servidor.
El repositorio incluye la versión real. `examples/servers/simple-auth/` ejecuta un servidor de autorización independiente y un servidor MCP protegido; `examples/clients/simple-auth-client/` es el cliente de esta página convertido en una pequeña CLI. Su README tiene los dos comandos: inicia los servidores, ejecuta el cliente contra ellos y verás pasar los cuatro pasos.
## Client ID Metadata Documents {#client-id-metadata-documents}
La revisión 2026-07-28 de la especificación declara obsoleto el registro dinámico de clientes en favor de los **Client ID Metadata Documents** (CIMD). En lugar de enviar un POST con un registro nuevo a cada servidor de autorización que encuentra, tu cliente publica un único documento JSON sobre sí mismo en una URL HTTPS estable, y esa URL *es* su `client_id`. El servidor de autorización obtiene el documento; el proveedor nunca lo toca.
El SDK ya lo admite: pasa la URL como `client_metadata_url=` al construir el proveedor. Cuando los metadatos del servidor de autorización anuncian `client_id_metadata_document_supported: true`, el proveedor se salta por completo la solicitud a `/register`: la URL entra en el flujo como `client_id` y no hay `client_secret`. Cuando el servidor no lo anuncia (la mayoría aún no lo hace), o nunca pasas una URL, el proveedor recurre al registro dinámico **en silencio**, y todo lo anterior funciona exactamente como se describe. Un `client_info` almacenado sigue teniendo prioridad sobre ambos.
La URL debe ser HTTPS con una ruta que no sea la raíz; cualquier otra cosa es un `ValueError` en la construcción, antes de que ocurra nada en la red. El ejemplo incluido en `examples/clients/simple-auth-client/` la toma de la variable de entorno `MCP_CLIENT_METADATA_URL`.
## De máquina a máquina {#machine-to-machine}
Un trabajo nocturno, un paso de CI, otro servicio. No hay navegador ni nadie que haga clic en "permitir". Ese es el grant **client credentials**: ya tienes un `client_id` y un `client_secret`, y el endpoint de token es todo el flujo.
`ClientCredentialsOAuthProvider` es el mismo `httpx2.Auth`, sin el humano:
```python title="client.py" hl_lines="4 27-33"
--8<-- "docs_src/oauth_clients/tutorial002.py"
```
Qué cambió:
* Sin `OAuthClientMetadata`, sin handlers. Pasas `client_id` y `client_secret`; el proveedor construye un registro `client_credentials` mínimo en torno a ellos y se salta el registro dinámico por completo.
* `scope` es una cadena separada por espacios, el formato que OAuth usa en lo que se transmite.
* Todo lo que viene después es idéntico: el mismo `TokenStorage`, el mismo `httpx2.AsyncClient(auth=...)`, el mismo `streamable_http_client`.
Por defecto, el secreto viaja como autenticación HTTP Basic en la solicitud de token (`client_secret_basic`). Pasa `token_endpoint_auth_method="client_secret_post"` para ponerlo en el cuerpo del formulario en su lugar. Algunos servidores de autorización solo aceptan uno de los dos.
!!! tip
Lee `client_secret` del entorno o de un gestor de secretos, nunca del control de versiones.
!!! info
Hay un proveedor más en `mcp.client.auth.extensions.client_credentials`:
**`PrivateKeyJWTOAuthProvider`**, para clientes que se autentican con un JWT en lugar de un
secreto compartido (`private_key_jwt`, la variante de par de claves e identidad de carga de trabajo). Sigue
el mismo patrón: construye uno y ponlo en `auth=`. El mismo módulo incluye
`SignedJWTParameters` y `static_assertion_provider`, dos utilidades que construyen su aserción.
Hay una situación más sin humanos: el cliente pertenece a una empresa cuyo proveedor de identidad, y no el usuario, decide a qué servidores MCP puede acceder. Ese es un grant distinto, con su propio modelo de confianza y su propia página, **[Aserción de identidad](identity-assertion.md)**.
## Cuando falla {#when-it-fails}
Cuando el flujo OAuth sale mal, el proveedor lanza un `OAuthFlowError` de `mcp.client.auth`. Tiene dos subclases. `OAuthRegistrationError` significa que el registro no produjo un cliente que puedas usar: el servidor de autorización se negó a registrarte, o sí te registró pero con credenciales que este flujo no puede usar (por ejemplo, un método de autenticación que no implementa). `OAuthTokenError` significa que no se pudo obtener un token: el endpoint de token dijo que no, o un registro de cliente almacenado lleva un método de autenticación que este cliente no puede aplicar, lo cual se informa al construir la solicitud de token en lugar de enviarse. Un solo `except OAuthFlowError:` cubre descubrimiento, registro, autorización e intercambio.
No todo es un error de flujo. La red todavía puede fallar; esas son excepciones ordinarias de `httpx2` y pasan sin modificar.
## Resumen {#recap}
* `OAuthClientProvider` es un `httpx2.Auth`. Ponlo en un `httpx2.AsyncClient`, pásaselo a `streamable_http_client(url, http_client=...)` y `Client` nunca se entera de que hubo OAuth.
* Aportas cuatro cosas: la URL del servidor, un `OAuthClientMetadata`, un `TokenStorage` y el par de handlers de redirección y callback.
* `TokenStorage` es un `Protocol`: cuatro métodos asíncronos, sin clase base. Persiste `client_info` además de los tokens.
* El descubrimiento, el registro (dinámico o mediante un **Client ID Metadata Document**), PKCE, las comprobaciones de `state` e `iss` y la renovación de tokens son trabajo del proveedor, no tuyo.
* `ClientCredentialsOAuthProvider` es la versión sin humanos: `client_id` + `client_secret`, sin handlers, sin navegador.
* Todo fallo de OAuth es un `OAuthFlowError`; `OAuthRegistrationError` y `OAuthTokenError` son sus subclases.
La otra mitad de este handshake, hacer que tu *servidor* exija el token, es **[Autorización](../run/authorization.md)**.

View file

@ -0,0 +1,87 @@
---
translation:
sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, d18adbdbb835ea73]
tool: 1
---
# Grupos de sesiones {#session-groups}
Un `Client` se conecta a un servidor. Las aplicaciones reales suelen querer varios (un servidor de búsqueda, un servidor de base de datos, una API interna) y terminan haciendo malabares con una conexión y una lista de herramientas para cada uno.
**`ClientSessionGroup`** es un único objeto que mantiene muchas conexiones y reúne todo lo que exponen en una sola vista.
## Dos servidores {#two-servers}
Empieza con dos servidores normales. No tienen nada que ver entre sí, así que, como es natural, ambos llamaron `search` a su herramienta:
```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"
```
## Un grupo {#one-group}
Crea un `ClientSessionGroup` y llama a **`connect_to_server`** una vez por servidor:
```python title="client.py" hl_lines="10-12"
--8<-- "docs_src/session_groups/tutorial003.py"
```
* `connect_to_server` recibe parámetros de transporte, no un objeto servidor: `StdioServerParameters` (de `mcp`) para lanzar un subproceso, o `StreamableHttpParameters` / `SseServerParameters` (de `mcp.client.session_group`) para un servidor que ya está escuchando en una URL.
* `group.tools` es un `dict[str, Tool]` con las herramientas de todos los servidores conectados. `group.resources` y `group.prompts` tienen la misma forma.
* `group.call_tool(name, arguments)` busca el nombre, encuentra la sesión a la que pertenece y le reenvía la llamada. Nunca indicas qué servidor.
!!! check
Pon `client.py` junto a los dos servidores y ejecútalo. El segundo `connect_to_server` se niega:
```text
mcp.shared.exceptions.MCPError: {'search'} already exist in group tools.
```
Es un `MCPError`, lanzado antes de que se registre nada del segundo servidor. Un nombre debe
ser único en **todo** el grupo, y dos servidores que no controlas acabarán chocando tarde o temprano.
## `component_name_hook` {#component_name_hook}
Esto se arregla en el grupo, no en los servidores. Pasa una función de `(name, server_info)` y el grupo la ejecuta sobre cada nombre que registra:
```python title="client.py" hl_lines="7-8 15"
--8<-- "docs_src/session_groups/tutorial004.py"
```
Ejecútalo de nuevo. `print(sorted(group.tools))` ahora muestra ambos:
```text
['Library.search', 'Web.search']
```
* La **clave** es tuya. `by_server` la construyó a partir de `server_info.name`, el nombre con el que se creó cada `MCPServer(...)`.
* El `Tool` que contiene queda intacto: `group.tools["Web.search"].name` sigue siendo `"search"`, y ese es el nombre que `call_tool` transmite por el canal. El prefijo nunca sale de tu proceso.
* No son solo las herramientas. El recurso `hours` de la biblioteca se registra como `Library.hours`.
!!! tip
El hook se ejecuta sobre **cada** nombre de **cada** servidor, no solo en los conflictos: no hay un
modo de prefijo solo en caso de colisión. Elige un esquema y deja que se aplique en todas partes.
## Añadir y quitar servidores {#adding-and-removing-servers}
`connect_to_server` devuelve la `ClientSession` que abrió. Guárdala si alguna vez quieres deshacerte de ese servidor: `await group.disconnect_from_server(session)` quita del grupo sus herramientas, recursos y prompts.
Si ya tienes una `ClientSession` conectada (`Client.session` lo es), pásala a `await group.connect_with_session(server_info, session)` en lugar de abrir un transporte nuevo. La agrega de la misma manera. El grupo nunca cierra una sesión que no abrió. `server_info` da nombre al servidor para los prefijos de los componentes; en una conexión de la generación 2026, `client.server_info` puede ser `None` (la identidad es opcional), así que en ese caso pasa tu propio `Implementation(name=..., version=...)`.
## El handshake clásico {#the-classic-handshake}
`ClientSessionGroup` está construido sobre `ClientSession`, no sobre `Client`. Cada `connect_to_server` ejecuta el handshake clásico de `initialize`. Nunca envía el sondeo `server/discover` descrito en **[Versiones del protocolo](../protocol-versions.md)**. Todos los servidores MCP entienden ese handshake, así que esto no te cuesta compatibilidad con nada; solo significa que un grupo toma el camino más antiguo y lento hacia un servidor que podría hacerlo mejor.
## Resumen {#recap}
* `ClientSessionGroup` mantiene muchas conexiones a servidores y reúne sus herramientas, recursos y prompts en un `dict` para cada tipo.
* `connect_to_server(params)` por servidor. Recibe parámetros de transporte, nunca el objeto servidor ni la URL que recibe un `Client`.
* `group.call_tool(name, arguments)` enruta por ti al servidor al que pertenece.
* Los nombres deben ser únicos en todo el grupo; dos servidores con una herramienta `search` no pueden coexistir por sí solos.
* `component_name_hook=` reescribe cada nombre registrado. La clave del dict cambia; el nombre que se transmite por el canal, no.
* `connect_with_session` añade una sesión que ya tienes; `disconnect_from_server` quita una.
El handshake que habla un grupo (y el más rápido que prefiere un `Client`) es el tema de **[Versiones del protocolo](../protocol-versions.md)**.

View file

@ -0,0 +1,91 @@
---
translation:
sections: [8f9558e57f29eee1, a88c587739e0465c, 46ebfd5b325ed041, 4d10b00b57ce4bd9, 2cdb0edd1f59b3e2]
tool: 1
---
# Suscripciones {#subscriptions}
El catálogo de un servidor no es fijo. Las herramientas aparecen en tiempo de ejecución y el contenido detrás de la URI de un recurso cambia. Un cliente se entera a través de `client.listen(...)`: una sola solicitud `subscriptions/listen` cuya respuesta *es* el flujo. Permanece abierta y transporta las notificaciones de cambio que el cliente pidió.
Esta página es el extremo del cliente: abrir el flujo, observarlo junto a tu flujo principal y manejar sus finales. Publicar cambios, filtrar y atender el método son la parte del servidor, que se cuenta en **[Suscripciones](../handlers/subscriptions.md)**, dentro de *Dentro de tu handler*. Los ejemplos de aquí hablan con el servidor del tablero de sprint que se construye allí.
## Observar el flujo {#watching-the-stream}
Una suscripción es un gestor de contexto. Al entrar se envía la solicitud, con tus argumentos nombrados como filtro de la suscripción, y se espera la confirmación del servidor, así que el flujo ya está activo cuando empieza el bloque.
```python title="client.py" hl_lines="15 18 28"
--8<-- "docs_src/subscriptions/tutorial003.py"
```
La iteración produce cuatro eventos tipados: `ToolsListChanged`, `PromptsListChanged`, `ResourcesListChanged` y `ResourceUpdated(uri=...)`.
Un evento dice *qué* cambió, nunca *cómo*. Por eso `follow_board` llama a `read_resource` y a `list_tools`: el evento es una señal para volver a pedir los datos. Lee `event.uri` en lugar de suponer qué recurso se movió: un filtro puede nombrar varias URI, y un servidor puede informar de un cambio en un subrecurso de una de ellas.
Los eventos duplicados que esperan a ser consumidos se funden en uno, y al volver a pedir los datos obtienes igualmente el estado actual. Solo se funden los eventos idénticos: dos `ResourceUpdated` para URI distintas son dos eventos.
Dos propiedades más del objeto devuelto:
* `sub.honored` es el filtro que el servidor confirmó: un `SubscriptionFilter` con los campos que pasaste, que se leen como atributos (`sub.honored.prompts_list_changed`). `MCPServer` acepta todos los tipos que pides, así que te devuelve tu solicitud tal cual. Un servidor que admite menos tipos confirma menos, y un tipo aceptado puede no dispararse nunca. Un servidor también puede rechazar la solicitud entera en lugar de confirmarla (consulta [Decidir quién puede observar](../handlers/subscriptions.md#deciding-who-may-watch) en la página del servidor), lo que aparece como el error de la solicitud.
* `sub.subscription_id` es el id de la solicitud de escucha, el que va estampado en cada trama de este flujo. Puede haber varias suscripciones abiertas a la vez, cada una demultiplexada por su propio id.
## Observar sin bloquear {#watching-without-blocking}
`follow_board` se ejecuta hasta que el servidor cierra el flujo, lo que puede no ocurrir nunca, así que por sí solo se adueña de tu programa. Los clientes reales quieren el observador *junto* al flujo principal: un agente llama a herramientas mientras un observador mantiene al día una caché o una interfaz.
Abre primero la suscripción, luego inicia el observador y sigue con tu trabajo.
=== "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` y `read_board` del primer ejemplo, que este repositorio guarda como
`tutorial003.py`. Si guardas los archivos renderizados uno junto al otro como `client.py` y `app.py`,
escribe `from client import BOARD, read_board` en su lugar. El ejemplo `watch.py` de más abajo
importa `read_board` de la misma manera.
El orden es la clave. No se reenvía nada, así que un evento publicado antes de que existiera tu flujo se pierde. Entrar en `client.listen(...)` espera la confirmación, así que cada cambio a partir de ese momento llega a tu observador, y la instantánea que tomas dentro del bloque no puede perderse ninguno.
Las solicitudes se ejecutan libremente junto a un flujo abierto, desde la tarea del observador o desde cualquier otra, en el mismo cliente. Como los eventos *duplicados* sin consumir se funden, un flujo principal ocupado puede producir una sola recarga en lugar de tres. Los eventos distintos no se funden: un filtro que nombra muchas URI encola un evento pendiente por URI.
Para dejar de observar, sal del bloque: no hay ninguna llamada `unsubscribe`. Cancelar la tarea propietaria del bloque lo hace por ti, y el SDK cancela la solicitud de escucha como espera el transporte: en Streamable HTTP, cerrando el flujo de esa solicitud. Un observador que se ejecuta durante toda la vida de tu app nunca vuelve por sí solo, así que cancélalo, o cancela el alcance de su grupo de tareas, al apagar.
## Los flujos terminan {#streams-end}
Un flujo termina de una de dos maneras, y ambas son flujo de control normal. Un cierre ordenado del servidor termina el `async for`; una caída abrupta lanza `SubscriptionLost`.
La diferencia es de diagnóstico, no de qué hacer después: el flujo ya no está, no se reenvió nada, y un observador al que todavía le importa vuelve a escuchar y vuelve a pedir los datos.
```python title="watch.py" hl_lines="16 20"
--8<-- "docs_src/subscriptions/tutorial005.py"
```
Los servidores cierran flujos de forma ordenada por sus propias razones, entre ellas deshacerse de un suscriptor cuyo atraso creció demasiado, así que un final limpio no es una señal para dejar de observar. Espera un poco antes de volver a escuchar.
`SubscriptionLost` también tiene una causa local. El cliente retiene como máximo 1024 eventos sin consumir, y un consumidor que se atrasa tanto pierde la suscripción en lugar de crecer sin límite. Mantén corto el cuerpo del `async for` y haz el trabajo lento en otro sitio.
`keep_following` captura solo `SubscriptionLost`. Entrar en `listen()` también puede lanzar `MCPError` (la conexión falló o el servidor no atiende el método), `TimeoutError` (no llegó ninguna confirmación) y `ListenNotSupportedError` (una conexión anterior a 2026). Decide cuáles de ellas debe reintentar tu observador: la última nunca se recupera.
## Resumen {#recap}
* Entra con `async with client.listen(...)`; al entrar se espera la confirmación, así que no se pierde nada publicado después.
* Itera con `async for event in sub`. Los eventos son señales para volver a pedir los datos, nunca cargas de datos.
* Abre la suscripción, luego ejecuta el observador como tarea, y las llamadas a herramientas siguen fluyendo junto a él.
* Un final limpio detiene el bucle; una caída lanza `SubscriptionLost`. En ambos casos: vuelve a escuchar, vuelve a pedir los datos y, antes, espera un poco.
* Salir del bloque es darse de baja.
Publicar estos eventos, acotar el filtro y escalar más allá de un proceso son la parte del servidor: **[Suscripciones](../handlers/subscriptions.md)**. Estos mismos eventos también mantienen fiable una caché del lado del cliente, y **[Caché](caching.md)** es la página siguiente.

View file

@ -0,0 +1,128 @@
---
translation:
sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, ff7401df479af877, 3d0832f39b0d7059, d4bf7e4479637768, 05e20c0a798860e7]
tool: 1
---
# Transportes del cliente {#client-transports}
Cada `Client` habla con su servidor a través de un **transporte**: lo que realmente lleva los mensajes.
Nunca configuras uno por separado. `Client` recibe un único argumento posicional y deduce el transporte a partir de su tipo.
El lado del *servidor* de cada uno (lo que hace `mcp.run()` y lo que despliegas) está en **[Ejecutar el servidor](../run/index.md)**.
## En memoria {#in-memory}
Pasa el propio objeto del servidor:
```python title="client.py" hl_lines="14"
--8<-- "docs_src/client_transports/tutorial001.py"
```
Sin subproceso, sin puerto, sin bytes por ningún canal. El cliente y el servidor son dos objetos en el mismo proceso, y aun así la llamada pasa por la capa real del protocolo: `search_books` se lista, se valida y se invoca exactamente igual que por HTTP.
Eso lo convierte en dos cosas a la vez:
* **Un arnés de pruebas.** Todos los ejemplos de esta documentación se ejercitan así, y la página **[Pruebas](../get-started/testing.md)** construye todo el patrón en torno a ello.
* **Una API de integración.** Una aplicación que construye el servidor no necesita un salto de red para llamar a sus herramientas.
## Streamable HTTP {#streamable-http}
Pasa una cadena con una URL y obtienes **Streamable HTTP**, el transporte con el que despliegas:
```python title="client.py" hl_lines="5"
--8<-- "docs_src/client_transports/tutorial002.py"
```
Ese es todo el cliente de producción. `Client` envuelve la URL en `streamable_http_client(...)` por ti, encima de un `httpx2.AsyncClient` configurado como MCP necesita: `follow_redirects=True`, un timeout de 30 segundos para connect/write/pool y un timeout de lectura de 300 segundos, porque el servidor puede mantener abierto un flujo de respuesta.
!!! check
Un `Client` que has construido **no** está conectado. La construcción solo elige el transporte;
`async with` es lo que lo abre. Intenta usar la conexión antes de entrar y el SDK te lo dice:
```text
RuntimeError: Client must be used within an async context manager
```
No se resolvió, se obtuvo ni se lanzó nada cuando escribiste `Client("http://...")`. Esa línea es gratis.
### Trae tu propio `httpx2.AsyncClient` {#bring-your-own-httpx2asyncclient}
En cuanto necesites un encabezado `Authorization`, una cookie, un proxy, mTLS o un timeout distinto, construye tú mismo el `httpx2.AsyncClient` y entrégaselo a `streamable_http_client`:
```python title="client.py" hl_lines="8-14"
--8<-- "docs_src/client_transports/tutorial003.py"
```
Dos cosas que notar:
* El `httpx2.AsyncClient` es tuyo, así que **tú** entras y sales de él. El SDK nunca cierra un cliente que no creó.
* `streamable_http_client(url, http_client=...)` devuelve un transporte, y `Client(transport)` lo acepta como cualquier otra cosa.
Una nota sobre TLS: `httpx2` verifica los certificados contra el almacén de confianza del sistema operativo (mediante
[`truststore`](https://pypi.org/project/truststore/)), no contra una lista de CA incluida. En un entorno sin
un almacén de CA del sistema utilizable (algunos contenedores mínimos), configura las variables de entorno
estándar `SSL_CERT_FILE`/`SSL_CERT_DIR` o pasa un `verify=ssl_context` explícito a tu `httpx2.AsyncClient`
(el contexto está en
[`httpx` y `httpx-sse` sustituidos por `httpx2`](../migration.md#httpx-and-httpx-sse-replaced-by-httpx2)).
!!! warning
`streamable_http_client` antes aceptaba `headers=` y `timeout=` directamente. Ya no:
sus únicos parámetros son `url`, `http_client` y `terminate_on_close`. Usa `headers=` por
costumbre y obtienes:
```text
TypeError: streamable_http_client() got an unexpected keyword argument 'headers'
```
Todo lo que tiene forma de HTTP vive ahora en el único `httpx2.AsyncClient` que pasas.
!!! info
`httpx2` conserva la API conocida de `httpx`, así que si conoces `httpx` ya sabes cómo hacer la autenticación,
los proxies, los event hooks, los reintentos y los límites de conexión aquí. El SDK no añade nada encima ni quita
nada. También es donde se conecta OAuth:
`httpx2.AsyncClient(auth=OAuthClientProvider(...))`. Todo ese flujo está en **[Clientes OAuth](oauth-clients.md)**.
## stdio {#stdio}
Un servidor **stdio** es un subproceso. El cliente lo lanza, escribe JSON-RPC en su stdin y lee JSON-RPC de su stdout. Así es como un host de escritorio ejecuta un servidor en tu máquina: un host *es* este código más una interfaz de usuario, y **[Conectar a un host real](../get-started/real-host.md)** es la misma relación vista desde el lado del host, como archivo de configuración.
Describe el proceso con `StdioServerParameters`, conviértelo en un transporte con `stdio_client` y entrega *eso* a `Client`:
```python title="client.py" hl_lines="4-8 12"
--8<-- "docs_src/client_transports/tutorial004.py"
```
`Client` no acepta el objeto de parámetros por sí solo. `StdioServerParameters` es configuración; `stdio_client(server)` es el transporte que sabe lanzar un proceso a partir de ella. Envuélvelo siempre.
Salir del bloque `async with` también cierra el subproceso: cierra stdin, espera y lo mata si se queda. Nunca lo limpias tú.
!!! warning
El proceso hijo **no** hereda tu entorno. Recibe una lista de permitidos mínima (`HOME`, `LOGNAME`,
`PATH`, `SHELL`, `TERM` y `USER` en POSIX) para que nada sensible se filtre a un proceso que quizá
no hayas escrito tú.
Un servidor que necesita una clave de API no la encontrará ahí. Pásala explícitamente con `env=`; esas
variables se fusionan encima de la lista de permitidos. Eso es lo que hace `BOOKSHOP_API_KEY` arriba.
## SSE {#sse}
`sse_client(url)`, de `mcp.client.sse`, es el transporte HTTP al que reemplazó Streamable HTTP. Envuélvelo igual, `Client(sse_client("http://localhost:8000/sse"))`, para hablar con un servidor que todavía lo usa, y no construyas nada nuevo sobre él.
## El protocolo `Transport` {#the-transport-protocol}
Para `Client`, todo lo anterior es lo mismo.
Un **transporte** es cualquier gestor de contexto asíncrono que produce un par `(read, write)` de flujos de mensajes: formalmente, el protocolo `Transport` de `mcp.client`. `Client` resuelve su argumento por tipo: un objeto de servidor se conecta dentro del proceso, un `str` se convierte en `streamable_http_client(url)` y cualquier otra cosa se entra directamente como transporte. Esa última regla es la razón por la que `stdio_client(...)`, `streamable_http_client(...)` y `sse_client(...)` encajan todos en el mismo hueco, y por la que puedes escribir el tuyo.
## Resumen {#recap}
* `Client(mcp)` (el objeto del servidor) se conecta en memoria. Úsalo para pruebas y para integración.
* `Client("http://.../mcp")` (una URL) se conecta por Streamable HTTP, el transporte de producción.
* Los encabezados, la autenticación, los proxies y los timeouts van en un `httpx2.AsyncClient` que pasas a `streamable_http_client(url, http_client=...)`. No existe el argumento nombrado `headers=`.
* stdio es `Client(stdio_client(StdioServerParameters(...)))`, nunca el objeto de parámetros solo.
* El subproceso recibe un entorno con lista de permitidos, no el tuyo; `env=` se añade a él.
* Un transporte es cualquier cosa con la que puedas hacer `async with x as (read, write)`. `Client` entrega directamente a ese protocolo todo lo que no sea un objeto de servidor ni una URL.
* Construir un `Client` elige el transporte. `async with` lo abre.
Una vez abierto el transporte, los dos lados tienen que acordar una versión del protocolo. Normalmente nunca piensas en ello; cuando lo hagas, **[Versiones del protocolo](../protocol-versions.md)** es la página.

View file

@ -0,0 +1,96 @@
---
translation:
sections: [20541a40dbdd5980, 01262a123ad9501d, 429db5b574a2ac08, 56b2d49da412cb28, 6a1717123fe4513c]
tool: 1
---
# Funcionalidades obsoletas {#deprecated-features}
La especificación 2026-07-28 retira cinco cosas. El SDK sigue implementando todas y cada una, y todas llevan ahora un **aviso de obsolescencia**.
La tabla siguiente nombra cada funcionalidad obsoleta, explica por qué desaparece e indica el reemplazo sobre el que construir.
## Qué queda obsoleto {#what-is-deprecated}
| Obsoleto | Por qué | Qué hacer en su lugar |
|---|---|---|
| **Roots** (directorios raíz): `ctx.session.list_roots()`, `client.send_roots_list_changed()`, el `list_roots_callback=` que pasas a `Client(...)` | [SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577) retira la capacidad. | Recibe las rutas como argumentos de herramienta normales o URI de recurso, o incrusta una `ListRootsRequest` en un `InputRequiredResult` (consulta **[Solicitudes de varias idas y vueltas (multi-round-trip)](handlers/multi-round-trip.md)**). |
| **Muestreo (sampling) iniciado por el servidor**: `ctx.session.create_message()`, el `sampling_callback=` que pasas a `Client(...)` | SEP-2577 retira la capacidad. | Devuelve `InputRequiredResult` y deja que el cliente reintente la llamada (consulta **[Solicitudes de varias idas y vueltas](handlers/multi-round-trip.md)**). |
| **Registro de logs del protocolo**: `ctx.log()`, `ctx.debug()`, `ctx.info()`, `ctx.warning()`, `ctx.error()`, `ctx.session.send_log_message()`, `client.set_logging_level()` | SEP-2577 retira la capacidad. Nada dentro del protocolo la reemplaza. | El `import logging` de siempre hacia stderr (consulta **[Registro de logs](handlers/logging.md)**). |
| **`ping`**: `client.send_ping()` | **Eliminado** del protocolo, no solo obsoleto. No hay método `ping` en 2026-07-28. | Nada. Solo funciona contra una conexión `mode="legacy"`. |
| **Progreso de cliente a servidor**: `client.send_progress_notification()` | 2026-07-28 hace que el progreso sea solo de servidor a cliente. | Nada que enviar. Tu *servidor* informa del progreso con `ctx.report_progress()` (consulta **[Progreso](handlers/progress.md)**). |
De esa tabla se desprenden tres cosas:
* Roots, muestreo y registro de logs van juntos. Una sola propuesta, **SEP-2577**, deja obsoletas las tres capacidades a la vez.
* El muestreo y los roots comparten un problema más profundo: son lugares donde un **servidor** envía una **solicitud** al **cliente**. Esa dirección entera es lo que 2026-07-28 reemplaza con las **[Solicitudes de varias idas y vueltas](handlers/multi-round-trip.md)**. Lo que desaparece son los métodos RPC independientes (`sampling/createMessage`, `roots/list` y el `elicitation/create` de estilo push); los tipos de payload `CreateMessageRequest` / `ListRootsRequest` / `ElicitRequest` sobreviven, incrustados en `InputRequiredResult.input_requests`, y en el cliente llegan a los mismos callbacks.
* `ping` es la excepción. El protocolo no lo deja obsoleto: lo elimina. El método del SDK sigue avisando (su mensaje dice *removed*, no *deprecated*) y llamarlo en una conexión moderna responde con *"Method not found"*.
## Obsoleto es solo un aviso {#deprecated-is-advisory}
Hoy no se rompe nada.
Todos los métodos anteriores siguen funcionando contra cualquier sesión que haya negociado **2025-11-25 o anterior**. Fija `mode="legacy"` en el cliente y obtienes exactamente el comportamiento anterior a 2026. No hay cambios en lo que se transmite y la negociación de capacidades no cambia.
Lo que cambia es que recibes un aviso visible la primera vez que se ejecuta cada uno:
```text
MCPDeprecationWarning: The logging capability is deprecated as of 2026-07-28 (SEP-2577).
```
`MCPDeprecationWarning` hereda de `UserWarning`, **no** de `DeprecationWarning`. Es deliberado: el filtro por defecto de Python solo muestra `DeprecationWarning` en código que se ejecuta directamente como `__main__`, y así es como las bibliotecas dejan cosas obsoletas sin que nadie se entere durante dos años. Este aparece en todas partes, sin ninguna opción `-W`.
!!! warning
"Solo un aviso" deja de ser cierto en el canal. El muestreo y los roots son *solicitudes*
de servidor a cliente, y una sesión 2026-07-28 no tiene ningún canal que las transporte.
Llama a `ctx.session.create_message()` dentro de una herramienta en una conexión moderna y
el aviso se dispara igual, y después el envío falla con un error:
```text
Cannot send 'sampling/createMessage': this transport context has no back-channel
for server-initiated requests.
```
Dos señales, en ese orden. El `MCPDeprecationWarning` se dispara en el momento en que llamas
al método, en cualquier conexión. El error es lo que vuelve cuando a continuación el SDK
intenta enviar. Estas dos funcionalidades solo funcionan de extremo a extremo en una conexión
`mode="legacy"` cuyo cliente registró el callback correspondiente.
## Silenciar el aviso {#silencing-the-warning}
No lo hagas en código nuevo.
Pero un servidor que mantienes y que de verdad atiende a clientes anteriores a 2026 tiene todo el derecho a un log tranquilo. Filtra la categoría antes de que se ejecute la primera llamada obsoleta:
```python
import warnings
from mcp import MCPDeprecationWarning
warnings.filterwarnings("ignore", category=MCPDeprecationWarning)
```
Esa es toda la API. No hay un interruptor por método, y tampoco lo quieres: la gracia de tener una sola categoría es que una línea la silencia y una línea la trae de vuelta.
!!! check
Aplica el filtro al revés y obtienes una prueba de regresión gratis. Añade
`"error::mcp.MCPDeprecationWarning"` al ajuste `filterwarnings` de tu configuración de
pytest y la llamada obsoleta **lanza una excepción** en lugar de avisar. Una herramienta
llamada `old_log` que todavía llama a `ctx.info()` deja de pasar y empieza a informar:
```text
Error executing tool old_log: The logging capability is deprecated as of 2026-07-28 (SEP-2577).
```
Una línea de configuración de pytest, y una llamada obsoleta nunca podrá volver a colarse
en tu código sin que falle una prueba.
## Resumen {#recap}
* La especificación 2026-07-28 deja obsoletos los **roots**, el **muestreo** iniciado por el servidor y el **registro de logs** del protocolo (todo en [SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577)), restringe el **progreso** a la dirección servidor a cliente y elimina **`ping`**.
* La columna de reemplazos te indica el camino: **[Solicitudes de varias idas y vueltas](handlers/multi-round-trip.md)** para el muestreo y los roots, **[Registro de logs](handlers/logging.md)** para los logs, **[Progreso](handlers/progress.md)** para el progreso. `ping` no necesita nada en absoluto.
* Obsoleto es solo un aviso: no hay cambios en lo que se transmite, todo sigue funcionando contra sesiones anteriores a 2026 y recibes un `MCPDeprecationWarning` visible (un `UserWarning`, así que está activo por defecto).
* El muestreo y los roots necesitan además un canal de retorno (back-channel) que una sesión 2026-07-28 no tiene. En una conexión moderna avisan y después lanzan una excepción.
* `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` silencia toda la categoría; `"error::mcp.MCPDeprecationWarning"` en pytest la convierte en un fallo de prueba.
* El código nuevo no debería construirse sobre nada de esto.
Todas las demás páginas de esta documentación enseñan la API actual.

View file

@ -0,0 +1,144 @@
---
translation:
sections: [0d6c05bcbf836bf3, 59a7b14eeefc68c1, 7114d8d6daba203f, e8bbb56a98ba7bc9, 5138010f6159901c, f78da7c7c363d4c6, 220a939cab348686]
tool: 1
---
# Primeros pasos {#first-steps}
La **[página de inicio](../index.md)** va rápido: escribes un servidor, lo ejecutas, llamas a una herramienta.
Esta página va despacio, con las tres cosas que un servidor puede exponer y un nombre para cada pieza por el camino.
## Host, cliente y servidor {#host-client-and-server}
Tres palabras que verás en cada página a partir de aquí:
* Un **host** es la aplicación LLM: Claude, un IDE, un entorno de ejecución de agentes. Es aquello con lo que habla el usuario.
* Un **cliente** vive dentro del host y habla MCP. El host ejecuta un cliente por cada servidor al que está conectado.
* Un **servidor** es lo que construyes con este SDK. Expone cosas a los clientes. Nunca habla directamente con el modelo.
Tú escribes el servidor. Los hosts son el producto de otra persona. El SDK también te da un `Client`. Lo usarás para probar tus servidores, y aparece más adelante en esta página.
## Las tres primitivas {#the-three-primitives}
Un servidor expone exactamente tres tipos de cosas. Lo que las distingue es **quién decide usarlas**:
| Primitiva | Quién la controla | Qué es | Ejemplo |
|------------------|-------------------|------------------------------------------------------------------------|-------------------------------------------------|
| **Herramientas** | El modelo | Una función que el modelo llama para realizar una acción | Una llamada a una API, una escritura en base de datos |
| **Recursos** | La aplicación | Datos que el host carga en el contexto del modelo | El contenido de un archivo, una respuesta de una API |
| **Prompts** | El usuario | Una plantilla de mensajes reutilizable que el usuario invoca por nombre | Un comando de barra, una entrada de menú |
"Quién la controla" es precisamente la razón de la división. Una herramienta se ejecuta porque el **modelo** decidió llamarla. Un recurso se adjunta porque la **aplicación** decidió que el modelo lo necesitaba. Un prompt se ejecuta porque el **usuario** lo eligió.
!!! info
Si has construido una API web ya tienes casi toda la intuición: un **recurso** es un `GET`
(carga datos y no cambia nada) y una **herramienta** es un `POST` (hace trabajo y puede tener
efectos secundarios). Un **prompt** no tiene equivalente HTTP; se parece más a una consulta
guardada que el usuario ejecuta por nombre.
## Un servidor, las tres {#one-server-all-three}
```python title="server.py" hl_lines="6 12 18"
--8<-- "docs_src/first_steps/tutorial001.py"
```
Tres funciones normales, tres decoradores. Cada decorador es el registro completo:
* `@mcp.tool()` convierte `add` en una **herramienta**.
* `@mcp.resource("greeting://{name}")` convierte `greeting` en una **plantilla de recurso**: el `{name}` de la URI es el parámetro de la función.
* `@mcp.prompt()` convierte `summarize` en un **prompt**. La cadena que devuelve se convierte en un mensaje de usuario.
Todo lo demás (el nombre, la descripción, el esquema de argumentos) el SDK lo lee de la propia función: su nombre, su docstring, sus anotaciones de tipo. Nunca declaraste nada de eso por separado.
!!! tip
Las dos mitades del SDK tienen dos rutas de importación: `from mcp import Client` y
`from mcp.server import MCPServer`. No existe `from mcp import MCPServer`.
### Pruébalo {#try-it}
Ejecútalo con el MCP Inspector:
```console
uv run mcp dev server.py
```
Abre la URL que imprime. El Inspector tiene una pestaña por primitiva; recórrelas en orden.
**Tools.** Una entrada: `add`, descrita como *Add two numbers.* El formulario tiene un campo entero obligatorio para `a` y otro para `b`. Rellénalos, llámala, y el resultado es `3`. El Inspector construyó ese formulario a partir de `a: int, b: int`. Lo mismo hace cualquier otro cliente.
**Resources.** La lista *Resources* está vacía. `greeting` está en **Resource Templates**, porque `greeting://{name}` tiene un parámetro: no hay un recurso concreto que listar hasta que alguien indique un `name`. Dale `World` y léelo:
```text
Hello, World!
```
**Prompts.** Una entrada: `summarize`, con un único argumento obligatorio `text`. Obtenlo con algo de texto y recibes un mensaje con `role: user` y tu cadena ya generada como contenido. Eso es todo lo que es un prompt: una función que construye mensajes.
El Inspector ejecutó tu servidor sobre **stdio**, uno de los transportes que puede hablar un servidor MCP. Todavía no eliges uno; **[Ejecutar tu servidor](../run/index.md)** es la página para eso.
## Capacidades {#capabilities}
Viste tres pestañas en el Inspector. ¿Cómo supo que había tres?
Cuando un cliente se conecta, el servidor declara sus **capacidades**: qué familias de solicitudes va a responder. El cliente usa esa declaración para decidir qué vale la pena pedir siquiera. Nunca la escribiste; `MCPServer` la declara por ti.
Míralo tú mismo. El `Client` del SDK acepta el objeto servidor directamente y se conecta a él **en memoria** (sin subproceso, sin puerto):
```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}}
```
Ese diccionario son las **capacidades** declaradas de tu servidor. Es lo primero que aprende cada cliente que se conecta:
| Capacidad | El cliente ya puede llamar a |
|-------------|----------------------------------------------------------------|
| `tools` | `tools/list`, `tools/call` |
| `resources` | `resources/list`, `resources/templates/list`, `resources/read` |
| `prompts` | `prompts/list`, `prompts/get` |
`MCPServer` sirve las tres primitivas, así que las tres se declaran siempre.
Fíjate en lo que no está. `completions` (autocompletado de argumentos para plantillas de recurso y prompts) necesita un handler que escribes tú, este servidor no tiene uno, así que la capacidad está ausente y un cliente bien hecho no la pedirá. Esa es la regla para todo lo opcional: registra la cosa y la capacidad aparece; **[Autocompletado](../servers/completions.md)** lo demuestra.
!!! info
`Client(mcp)` es el mismo cliente en memoria con el que se prueba cada ejemplo de esta
documentación, y es como probarás los tuyos. Tiene una página entera: **[Pruebas](testing.md)**.
## Lo que no escribiste {#what-you-did-not-write}
Repasa esta página. Escribiste tres pequeñas funciones de Python. **No** escribiste:
* Un JSON Schema. `a: int, b: int` *es* el esquema de `add`.
* Un handler de solicitudes. `tools/list`, `resources/read`, `prompts/get`: todos servidos por ti.
* Una declaración de capacidades. `MCPServer` la hizo por ti.
* Una línea de protocolo. La negociación de versión, el encuadre JSON-RPC, el intercambio de capacidades: todo ocurrió dentro de `mcp dev` y `Client(mcp)`, y nunca lo viste.
Esa proporción es la razón de ser del SDK.
## Resumen {#recap}
* Un **host** es la app LLM, un **cliente** es su mitad que habla MCP, un **servidor** es lo que construyes.
* Las herramientas las controla el **modelo**, los recursos los controla la **aplicación**, los prompts los controla el **usuario**.
* Un decorador por primitiva: `@mcp.tool()`, `@mcp.resource(uri)`, `@mcp.prompt()`. Nombre, descripción y esquema salen de la función.
* Una URI con un `{param}` crea una **plantilla** de recurso, que se lista aparte de los recursos concretos.
* Las **capacidades** del servidor se declaran por ti, y un cliente solo pide lo que un servidor declara.
* `Client(mcp)` se conecta al objeto servidor en memoria: tu entorno de pruebas desde el primer día.
Lo siguiente es **[Conectar a un host real](real-host.md)**: este servidor dentro de Claude Desktop o un IDE, de verdad. Después, **[Pruebas](testing.md)**: una página, un cliente en memoria, y nunca más adivinas si funciona. Tras eso, cada primitiva tiene su propia página, empezando por la que maneja el modelo: **[Herramientas](../servers/tools.md)**.

View file

@ -0,0 +1,53 @@
---
translation:
sections: [ed4a756b4c53c585, 97e2fb315b7fe398, 4d04f1c6f4bf6c1d, 577d73078fc62baf]
tool: 1
---
# Empieza aquí {#get-started}
¿Eres nuevo en MCP o nuevo en este SDK? Empieza aquí. Estas páginas te llevan desde cero hasta un servidor funcional y probado: [instala el SDK](installation.md), construye tu [primer servidor](first-steps.md), [conéctalo a un host real](real-host.md) y [pruébalo](testing.md) con un cliente en memoria.
## Ejecuta el código {#run-the-code}
Todos los bloques de código se pueden copiar y usar directamente: son archivos completos que funcionan.
Para seguir los pasos, pega un bloque en un `server.py` y ábrelo en el MCP Inspector:
```console
uv run mcp dev server.py
```
Se **RECOMIENDA ENCARECIDAMENTE** que escribas (o copies) el código, lo edites y lo ejecutes localmente. Usarlo en tu propio editor es lo que de verdad te muestra la idea: lo poco que escribes, el autocompletado, las comprobaciones de tipos que detectan errores antes de ejecutar nada.
## No vas a adivinar {#you-will-not-be-guessing}
Cada ejemplo de esta documentación es un archivo completo dentro de [`docs_src/`](https://github.com/modelcontextprotocol/python-sdk/tree/main/docs_src) en el propio repositorio del SDK, y cada uno de ellos lo ejercita la suite de pruebas del SDK mediante un **cliente en memoria**:
```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}
```
Sin subproceso, sin puerto, sin transporte. `Client(mcp)` se conecta directamente al objeto del servidor.
Si un cambio en el SDK rompe un ejemplo de una de estas páginas, la CI se pone en rojo antes que la página. El código que lees aquí es el código que se ejecuta.
Lo usarás tú mismo en [Pruebas](testing.md); así es también como pruebas tus propios servidores.
## Adónde ir después {#where-to-go-next}
Una vez que tengas un servidor en marcha, el resto de esta documentación es una referencia, no un curso. Cada página es independiente, así que ve directo a lo que necesitas:
* Lo que expone un servidor (herramientas, recursos, prompts) está en **[Servidores](../servers/index.md)**.
* Lo que tienes disponible dentro de las funciones que registras está en **[Dentro de tu handler](../handlers/index.md)**.
* Ponerlo delante de los clientes (stdio, HTTP, tu app FastAPI existente) está en **[Ejecutar el servidor](../run/index.md)**.
* Construir el otro lado, una aplicación que *usa* servidores MCP, está en **[Clientes](../client/index.md)**.

Some files were not shown because too many files have changed in this diff Show more