docs: publish translated docs in twelve languages and the tool that maintains them (#3280)
This commit is contained in:
parent
14aa889992
commit
52ad0a8876
6 changed files with 89737 additions and 54 deletions
165
i18n/es/pages/advanced/apps.md
Normal file
165
i18n/es/pages/advanced/apps.md
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
---
|
||||
translation:
|
||||
sections: [0355618e5f4d5fe4, 1821eaf50f2d0b64, 82e0b28ebd3abf5a, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487]
|
||||
tool: 1
|
||||
---
|
||||
# MCP Apps {#mcp-apps}
|
||||
|
||||
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
|
||||
```
|
||||
260
i18n/es/pages/advanced/extensions.md
Normal file
260
i18n/es/pages/advanced/extensions.md
Normal 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.
|
||||
34
i18n/es/pages/advanced/index.md
Normal file
34
i18n/es/pages/advanced/index.md
Normal 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.
|
||||
207
i18n/es/pages/advanced/low-level-server.md
Normal file
207
i18n/es/pages/advanced/low-level-server.md
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
---
|
||||
translation:
|
||||
sections: [2c79b6338e09b7ac, 7edc43b3fae11314, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, b3530fcf4d11fd56, ebc33704fbd74262, cd0e9c933350390e]
|
||||
tool: 1
|
||||
---
|
||||
# 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)**.
|
||||
126
i18n/es/pages/advanced/middleware.md
Normal file
126
i18n/es/pages/advanced/middleware.md
Normal 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.
|
||||
85
i18n/es/pages/advanced/pagination.md
Normal file
85
i18n/es/pages/advanced/pagination.md
Normal 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)**.
|
||||
Loading…
Reference in a new issue