docs: refresh translations for recent English changes (#3379)
This commit is contained in:
parent
d8b638304e
commit
c5d7d0b6d0
18 changed files with 3149 additions and 1047 deletions
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [2c79b6338e09b7ac, 7edc43b3fae11314, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, b3530fcf4d11fd56, ebc33704fbd74262, cd0e9c933350390e]
|
||||
sections: [2c79b6338e09b7ac, 7edc43b3fae11314, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, cd0e9c933350390e]
|
||||
tool: 1
|
||||
---
|
||||
# El Server de bajo nivel {#the-low-level-server}
|
||||
|
|
@ -116,6 +116,17 @@ El bloque `_meta` es el sello de identidad del servidor: el SDK lo añade a cada
|
|||
|
||||
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)**.
|
||||
|
||||
## El dialecto es JSON Schema 2020-12 {#the-dialect-is-json-schema-2020-12}
|
||||
|
||||
`input_schema` y `output_schema` son JSON Schema, y la [especificación de MCP](https://modelcontextprotocol.io/specification/latest/basic#json-schema-usage) fija el dialecto: un esquema sin clave `$schema` es **JSON Schema 2020-12**. Los esquemas que genera `MCPServer` se apoyan en ese valor por defecto (Pydantic escribe 2020-12 y omite la clave), y un dict escrito a mano también se rige por él, así que tienes disponible todo el vocabulario de 2020-12:
|
||||
|
||||
```python title="server.py" hl_lines="8 14-15"
|
||||
--8<-- "docs_src/lowlevel/tutorial007.py"
|
||||
```
|
||||
|
||||
* La raíz de `input_schema` debe ser `"type": "object"`. A su lado, `oneOf`, `additionalProperties`, `anyOf`, `if`/`then`/`else`, `prefixItems`, `$defs` con `$ref` locales y el resto de las palabras clave de 2020-12 llegan al cliente exactamente como las escribiste.
|
||||
* No hace falta ninguna clave `$schema`. Añade una solo para optar por un draft más antiguo: el `Client` de este SDK, que valida `structured_content` contra el `output_schema` de una herramienta, elige su validador según `$schema` y usa 2020-12 cuando no hay ninguna.
|
||||
|
||||
## `_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.
|
||||
|
|
@ -167,7 +178,7 @@ El constructor cubre los métodos que MCP define. `add_request_handler` cubre to
|
|||
--8<-- "docs_src/lowlevel/tutorial006.py"
|
||||
```
|
||||
|
||||
* El primer argumento es la cadena del método. Las notificaciones tienen un gemelo, `add_notification_handler`.
|
||||
* El primer argumento es la cadena del método. Las notificaciones tienen un gemelo, `add_notification_handler`. Sus handlers se disparan en stdio y en conexiones HTTP de la generación del handshake; en la ruta Streamable HTTP de `2026-07-28`, el POST de notificación de un cliente se confirma con un `202` y no se despacha, porque esa revisión no define notificaciones de cliente a servidor sobre HTTP.
|
||||
* `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.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [6048b4f308edbb8c, 068bda0f21ee9c1b, c3e565b61acd75c5, c62422b159c6ed09, 47204fab253cc45c]
|
||||
sections: [6048b4f308edbb8c, 46056f318ef205e4, c3e565b61acd75c5, c62422b159c6ed09, 420968f514138f43]
|
||||
tool: 1
|
||||
---
|
||||
# Middleware {#middleware}
|
||||
|
|
@ -54,8 +54,11 @@ 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.
|
||||
* Cada solicitud y cada notificación que llega al servidor. Para una notificación,
|
||||
`ctx.request_id is None`, `call_next(ctx)` devuelve `None` y lo que devuelvas se descarta.
|
||||
(En la ruta Streamable HTTP de `2026-07-28`, el POST de notificación de un cliente se confirma
|
||||
con `202` en el transporte y nunca se despacha, así que tampoco llega al middleware; esa
|
||||
revisión no define notificaciones del cliente al servidor sobre HTTP.)
|
||||
* 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.
|
||||
|
||||
|
|
@ -114,8 +117,8 @@ span de OpenTelemetry por cada mensaje. No lo añades y, la mayor parte del tiem
|
|||
|
||||
* 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.
|
||||
* Envuelve **cada** mensaje entrante que llega al servidor (`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
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [ebef1e7a0df854f4, a4c687d3d627d516, 8e79141fc2985342, b345dd05b9c3c7ab, 80ce41579825a6fa, 5f0fa90494de8f65, 83d10514eaa62fa5, 9190555aa39a5d28, 84a4c9d8bf14dddb, 927d71cf40b58c30]
|
||||
sections: [ebef1e7a0df854f4, 8355cfaf1f76c9d5, 8e79141fc2985342, 46bdb07c7537e8a5, 80ce41579825a6fa, 5f0fa90494de8f65, 83d10514eaa62fa5, 9190555aa39a5d28, 84a4c9d8bf14dddb, 927d71cf40b58c30]
|
||||
tool: 1
|
||||
---
|
||||
# El cliente {#the-client}
|
||||
|
|
@ -27,9 +27,10 @@ El servidor del principio solo está ahí para que tengas algo a lo que conectar
|
|||
|
||||
* 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.
|
||||
* Un `StdioServerParameters`: el comando que se lanza como **subproceso**, con el que se habla a través de su stdin y su stdout.
|
||||
* Un **transporte**: cualquier cosa que puedas usar con `async with ... as (read, write)`, como `streamable_http_client(url, http_client=...)` envolviendo tu propio cliente HTTP.
|
||||
|
||||
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)**.
|
||||
Todo lo demás en esta página es idéntico en los cuatro 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}
|
||||
|
||||
|
|
@ -85,7 +86,7 @@ Ese esquema es todo lo que una UI necesita para renderizar un formulario de argu
|
|||
|
||||
`call_tool(name, arguments)` ejecuta la herramienta y te devuelve un `CallToolResult`.
|
||||
|
||||
```python title="client.py" hl_lines="26-33"
|
||||
```python title="client.py" hl_lines="27-34"
|
||||
--8<-- "docs_src/client/tutorial003.py"
|
||||
```
|
||||
|
||||
|
|
@ -117,7 +118,7 @@ Una herramienta que lanza una excepción **no** la lanza en tu cliente. Vuelve c
|
|||
|
||||
!!! 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:
|
||||
`ToolError`. Aun así, la llamada devuelve un resultado normal:
|
||||
|
||||
```python
|
||||
result.is_error # True
|
||||
|
|
@ -125,9 +126,10 @@ Una herramienta que lanza una excepción **no** la lanza en tu cliente. Vuelve c
|
|||
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`.
|
||||
El mensaje del `ToolError` 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. (Si la herramienta hubiera
|
||||
fallado con alguna otra excepción, `content` diría solo `Error executing tool lookup_book`.) 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
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, ff7401df479af877, 3d0832f39b0d7059, d4bf7e4479637768, 05e20c0a798860e7]
|
||||
sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, 40b4916d82eaf1d4, 3d0832f39b0d7059, dfa4446556badef0, 5bd93be2ab2ecb9c]
|
||||
tool: 1
|
||||
---
|
||||
# Transportes del cliente {#client-transports}
|
||||
|
|
@ -87,15 +87,15 @@ estándar `SSL_CERT_FILE`/`SSL_CERT_DIR` o pasa un `verify=ssl_context` explíci
|
|||
|
||||
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`:
|
||||
Describe el proceso con `StdioServerParameters` y entrégaselo a `Client`:
|
||||
|
||||
```python title="client.py" hl_lines="4-8 12"
|
||||
```python title="client.py" hl_lines="3-7 11"
|
||||
--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.
|
||||
Entrar en el bloque lanza el proceso. Salir de él cierra el subproceso: cierra stdin, espera y lo mata si se queda. Nunca lo limpias tú.
|
||||
|
||||
Salir del bloque `async with` también cierra el subproceso: cierra stdin, espera y lo mata si se queda. Nunca lo limpias tú.
|
||||
El stderr del proceso hijo va al tuyo. Para enviarlo a otro sitio, construye tú mismo el transporte con `stdio_client` (de `mcp`) y pasa eso en su lugar: `Client(stdio_client(server, errlog=log_file))`.
|
||||
|
||||
!!! warning
|
||||
El proceso hijo **no** hereda tu entorno. Recibe una lista de permitidos mínima (`HOME`, `LOGNAME`,
|
||||
|
|
@ -113,16 +113,16 @@ Salir del bloque `async with` también cierra el subproceso: cierra stdin, esper
|
|||
|
||||
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.
|
||||
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)`, un `StdioServerParameters` se convierte en `stdio_client(params)` 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.
|
||||
* stdio es `Client(StdioServerParameters(...))`. Envuélvelo tú mismo en `stdio_client(...)` solo para redirigir el stderr del proceso hijo.
|
||||
* 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.
|
||||
* 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, una URL ni un `StdioServerParameters`.
|
||||
* 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.
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
---
|
||||
translation:
|
||||
sections: [20541a40dbdd5980, 01262a123ad9501d, 429db5b574a2ac08, 56b2d49da412cb28, 6a1717123fe4513c]
|
||||
sections: [490237e61c3a7a44, 01262a123ad9501d, 429db5b574a2ac08, e2d0d273fbd2d74b, 64ab0331e868f3d4, 6c8878ce2d1f6d56, 4068f23e371bf0b3, eaef75b8725bc931]
|
||||
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 especificación 2026-07-28 retira cinco cosas. El SDK sigue implementando todas y cada una, y todas llevan ahora un **aviso de obsolescencia**. Una función auxiliar del SDK queda obsoleta por su cuenta y aparece [al final](#deprecated-sdk-helpers).
|
||||
|
||||
La tabla siguiente nombra cada funcionalidad obsoleta, explica por qué desaparece e indica el reemplazo sobre el que construir.
|
||||
|
||||
|
|
@ -55,6 +55,55 @@ MCPDeprecationWarning: The logging capability is deprecated as of 2026-07-28 (SE
|
|||
intenta enviar. Estas dos funcionalidades solo funcionan de extremo a extremo en una conexión
|
||||
`mode="legacy"` cuyo cliente registró el callback correspondiente.
|
||||
|
||||
## `ping` en una sesión heredada {#ping-on-a-legacy-session}
|
||||
|
||||
Un **ping** es una solicitud vacía que cualquiera de los dos lados puede enviar para comprobar que el otro sigue respondiendo. La especificación 2026-07-28 lo elimina ([SEP-2575](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575)): cada solicitud que envía un cliente moderno ya demuestra que el servidor está ahí, y un servidor moderno no tiene canal por el que enviar uno. Ambos métodos del SDK siguen funcionando en una sesión de la generación del handshake. Desde el cliente:
|
||||
|
||||
```python
|
||||
async def main() -> None:
|
||||
async with Client("http://localhost:8000/mcp", mode="legacy") as client:
|
||||
await client.send_ping() # warns; returns an EmptyResult
|
||||
```
|
||||
|
||||
Y desde el servidor, dentro de cualquier handler:
|
||||
|
||||
```python
|
||||
@mcp.tool()
|
||||
async def check_client(ctx: Context) -> str:
|
||||
"""A tool that still pings the client mid-call."""
|
||||
await ctx.session.send_ping() # no warning; an EmptyResult while the client is connected
|
||||
return "client answered"
|
||||
```
|
||||
|
||||
* `client.send_ping()` avisa con `MCPDeprecationWarning` en cada llamada. En una conexión por defecto (`2026-07-28`), el servidor responde `MCPError: Method not found` en su lugar.
|
||||
* `ctx.session.send_ping()` no lleva ningún aviso. En una conexión moderna lanza el mismo error de falta de canal de retorno (back-channel) que cualquier otra solicitud iniciada por el servidor.
|
||||
* Ninguno de los dos lados registra nada para responder a un ping.
|
||||
|
||||
## Notificaciones de cambio de roots {#roots-change-notifications}
|
||||
|
||||
Un cliente de la generación 2025 que declaró la capacidad roots puede avisar al servidor de que sus carpetas del espacio de trabajo cambiaron enviando `notifications/roots/list_changed`; el servidor responde solicitando `roots/list` de nuevo. La especificación 2026-07-28 elimina la notificación junto con el resto del flujo de roots de estilo push. En el cliente, pasar `list_roots_callback=` (**[Callbacks del cliente](client/callbacks.md)**) es lo que declara `"roots": {"listChanged": true}`, y una llamada cumple esa promesa:
|
||||
|
||||
```python
|
||||
async def open_folder(client: Client, uri: str, name: str) -> None:
|
||||
"""The user opened another folder: expose it through the roots callback, then tell the server."""
|
||||
workspace.append(Root(uri=FileUrl(uri), name=name))
|
||||
await client.send_roots_list_changed()
|
||||
```
|
||||
|
||||
En el servidor, el `Server` de bajo nivel acepta el handler que la recibe:
|
||||
|
||||
```python
|
||||
async def roots_changed(ctx: ServerRequestContext, params: NotificationParams | None) -> None:
|
||||
"""The client's roots changed: ask for the new list."""
|
||||
roots = (await ctx.session.list_roots()).roots
|
||||
|
||||
|
||||
server = Server("Bookshop", on_roots_list_changed=roots_changed)
|
||||
```
|
||||
|
||||
* `workspace` es la lista que devuelve tu `list_roots_callback`. `client.send_roots_list_changed()` avisa, y necesita un cliente `mode="legacy"`: en una conexión moderna la notificación se descarta en silencio. Mantén la sesión abierta después, porque el `roots/list` posterior del servidor llega por ella.
|
||||
* `MCPServer` no tiene ningún hook para la notificación. En el `Server` de bajo nivel, `on_roots_list_changed=` registra el handler (también obsoleto, y avisa en la construcción). La notificación no lleva payload, así que el handler llama a `ctx.session.list_roots()` para obtener la lista nueva.
|
||||
|
||||
## Silenciar el aviso {#silencing-the-warning}
|
||||
|
||||
No lo hagas en código nuevo.
|
||||
|
|
@ -75,22 +124,33 @@ Esa es toda la API. No hay un interruptor por método, y tampoco lo quieres: la
|
|||
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:
|
||||
llamada `old_log` que todavía llama a `ctx.info()` deja de pasar: la llamada vuelve con
|
||||
`is_error=True` y `Error executing tool old_log`, y el log capturado del servidor señala
|
||||
al culpable:
|
||||
|
||||
```text
|
||||
Error executing tool old_log: The logging capability is deprecated as of 2026-07-28 (SEP-2577).
|
||||
mcp.shared.exceptions.MCPDeprecationWarning: 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.
|
||||
|
||||
## Funciones auxiliares del SDK obsoletas {#deprecated-sdk-helpers}
|
||||
|
||||
No son cambios de la especificación, solo detalles internos del SDK con un reemplazo mejor. Avisan con el mismo `MCPDeprecationWarning` y se eliminarán en 3.0.
|
||||
|
||||
| Obsoleto | Qué hacer en su lugar |
|
||||
|---|---|
|
||||
| `FuncMetadata.call_fn_with_arg_validation()` | `FuncMetadata.validate_arguments()` y después `FuncMetadata.call_fn()`. Solo lo llamaba el código que maneja `FuncMetadata` directamente (una subclase propia de `Tool`, por ejemplo). |
|
||||
|
||||
## 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.
|
||||
* El muestreo y los roots necesitan además un canal de retorno 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.
|
||||
* Una función auxiliar del SDK, `FuncMetadata.call_fn_with_arg_validation()`, queda obsoleta por separado y se eliminará en 3.0.
|
||||
* 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.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [3c4f2f06b4e978b6, 22520eecae3d1961, f4e1709db18d635a, 2eb57992049671d9, 1ba83e9af37cc1b4, 4822586344b08d9e, 1c93afef72478992, b6b448f9eddd51dc, fe55370fd931815b]
|
||||
sections: [3c4f2f06b4e978b6, 51ea5fbcb0e93563, 32d8808606ffdae0, 2eb57992049671d9, 1ba83e9af37cc1b4, 4822586344b08d9e, 1c93afef72478992, b6b448f9eddd51dc, fe55370fd931815b]
|
||||
tool: 1
|
||||
---
|
||||
# Conectarse a un host real {#connect-to-a-real-host}
|
||||
|
|
@ -11,7 +11,7 @@ Esto significa que conectarse a un host es un único acto: le indicas **el coman
|
|||
|
||||
## Un servidor, todos los hosts {#one-server-every-host}
|
||||
|
||||
```python title="server.py" hl_lines="3 33-34"
|
||||
```python title="server.py" hl_lines="4 34-35"
|
||||
--8<-- "docs_src/real_host/tutorial001.py"
|
||||
```
|
||||
|
||||
|
|
@ -50,7 +50,7 @@ También es el comando que `mcp install` escribe por ti en la configuración de
|
|||
|
||||
Y un host no es más que una aplicación con un cliente MCP dentro, así que tu propio código
|
||||
Python puede hacer el papel del host: **[Transportes del cliente](../client/transports.md)** lanza
|
||||
este mismo archivo como subproceso con `stdio_client(...)`, y **[Pruebas](testing.md)**
|
||||
este mismo archivo como subproceso con `Client(StdioServerParameters(...))`, y **[Pruebas](testing.md)**
|
||||
se conecta a él en memoria, sin ningún proceso.
|
||||
|
||||
## Claude Desktop {#claude-desktop}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: ['4926721070127497', c52a1de2b6b32f40, 2e410b412c25f314, 627195f7159e24ef]
|
||||
sections: ['4926721070127497', c52a1de2b6b32f40, 8e792bf8c7489ec6, 627195f7159e24ef]
|
||||
tool: 1
|
||||
---
|
||||
# Pruebas {#testing}
|
||||
|
|
@ -84,9 +84,10 @@ async def test_call_add_tool(client: Client):
|
|||
Pueden fallar dos cosas distintas, y este indicador solo afecta a una de ellas.
|
||||
|
||||
Una excepción dentro de una de **tus herramientas** no es un fallo del protocolo. Se convierte en un
|
||||
resultado normal con `is_error=True`, y el modelo lee el mensaje. `raise_exceptions` no cambia eso:
|
||||
con o sin él, `call_tool` devuelve el mismo resultado con `is_error=True`. Hay una página entera
|
||||
dedicada a esto: **[Manejo de errores](../servers/handling-errors.md)**.
|
||||
resultado normal con `is_error=True` (y si era un `ToolError`, el modelo lee tu mensaje).
|
||||
`raise_exceptions` no cambia eso: con o sin él, `call_tool` devuelve el mismo resultado con
|
||||
`is_error=True`. Hay una página entera dedicada a esto:
|
||||
**[Manejo de errores](../servers/handling-errors.md)**.
|
||||
|
||||
Un fallo **fuera** del cuerpo de una herramienta es otra cosa. En la conexión que te da
|
||||
`Client(mcp)`, el servidor lo depura y lo convierte en un genérico `"Internal server error"` antes de
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [335ca2a0b266f003, d1ad562d3fe87bc0, 0bb1396c86daeba4, d1cb1235bb9ee267, 833179c09d239c83, e5d6dec2d2e655e8]
|
||||
sections: [335ca2a0b266f003, d1ad562d3fe87bc0, 25b49f89c9e6f9d0, d1cb1235bb9ee267, 833179c09d239c83, e5d6dec2d2e655e8]
|
||||
tool: 1
|
||||
---
|
||||
# Elicitación {#elicitation}
|
||||
|
|
@ -90,7 +90,8 @@ Ese esquema es el formulario. `Field(description=...)` es la etiqueta; un valor
|
|||
Un esquema de elicitación no es tan expresivo como el esquema de entrada de una herramienta.
|
||||
Solo campos planos y primitivos: `str`, `int`, `float`, `bool` o un `Literal` de cadenas (se
|
||||
convierte en un `enum`). Pon un modelo dentro del modelo y `ctx.elicit` lanza una excepción
|
||||
antes de que se envíe nada al cliente:
|
||||
antes de que se envíe nada al cliente. La llamada a la herramienta falla con
|
||||
`Error executing tool <name>`, y el log de tu servidor tiene el motivo:
|
||||
|
||||
```text
|
||||
TypeError: Elicitation schema field 'address' rendered as {'$ref': '#/$defs/Address'}, which is not a valid PrimitiveSchemaDefinition
|
||||
|
|
@ -113,8 +114,8 @@ Una negativa no es un error. La herramienta decide qué significa rechazar (aqu
|
|||
|
||||
!!! tip
|
||||
La respuesta se valida contra tu modelo antes de que tu código la vea. Un cliente que envía
|
||||
`"maybe"` para un `bool` no corrompe tu reserva: la llamada falla con un error de
|
||||
discrepancia de esquema y tu `if` nunca se ejecuta.
|
||||
`"maybe"` para un `bool` no corrompe tu reserva: `ctx.elicit` lanza `ValueError`, la llamada
|
||||
falla y tu `if` nunca se ejecuta.
|
||||
|
||||
## Enviar al usuario a una URL {#send-the-user-to-a-url}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [c93a3e1aefd77955, 7851abd5ec54393b, f49d1ca2f330f9cd, c03764bd9dfeef7b, 4a0391691a674ae4, 2df5cd279eabf9f5]
|
||||
sections: [c93a3e1aefd77955, 7851abd5ec54393b, f49d1ca2f330f9cd, 4cc0a00347c3f534, 4a0391691a674ae4, 2df5cd279eabf9f5]
|
||||
tool: 1
|
||||
---
|
||||
# Registro de logs {#logging}
|
||||
|
|
@ -55,6 +55,8 @@ El valor por defecto es `"INFO"`.
|
|||
|
||||
`logging.basicConfig()` nunca reemplaza handlers que ya existen. Si configuras el logging tú mismo antes de crear el servidor, tu configuración gana.
|
||||
|
||||
Tampoco necesitas un `try`/`except` en cada handler solo para registrar los fallos. Cuando una función de herramienta o de recurso lanza una excepción, el SDK la registra por ti. **[Manejo de errores](../servers/handling-errors.md#any-other-exception)** explica qué se registra y con qué nivel.
|
||||
|
||||
## Pruébalo {#try-it}
|
||||
|
||||
Ejecuta el servidor con el MCP Inspector:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 7a53ead3e704a7f0, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0]
|
||||
sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 2fd7cf825e6d2b2c, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0]
|
||||
tool: 1
|
||||
---
|
||||
# Ejecutar el servidor {#running-your-server}
|
||||
|
|
@ -72,7 +72,7 @@ Cada transporte tiene sus propios argumentos nombrados, todos en `run()`:
|
|||
* `streamable_http_path`: dónde vive el endpoint MCP. Por defecto `/mcp`.
|
||||
* `json_response=True`: responde a cada POST con un único cuerpo JSON en lugar de un flujo SSE. Ese cuerpo tiene sitio para la respuesta y nada más, así que una herramienta que llama de vuelta al cliente a mitad de solicitud (`ctx.elicit()`, muestreo (sampling)) lanza `NoBackChannelError` en este tramo, y las notificaciones ligadas a la llamada en curso (el progreso de `ctx.report_progress()`, los mensajes de log por llamada) se descartan; el flujo `GET` independiente sigue llevando las que no están relacionadas.
|
||||
* `stateless_http=True`: un transporte nuevo por solicitud, sin seguimiento de sesión.
|
||||
* `max_request_body_size`: el cuerpo POST más grande que se acepta, en bytes. Es 4 MiB por defecto; las solicitudes mayores
|
||||
* `max_request_body_size`: el cuerpo de solicitud más grande que se acepta, en bytes. Es 4 MiB por defecto; las solicitudes mayores
|
||||
reciben HTTP 413 antes del análisis o de la creación de la sesión. Súbelo solo cuando los mensajes MCP legítimos
|
||||
superen ese tamaño.
|
||||
* `event_store`, `retry_interval`, `transport_security`: reanudabilidad y protección contra DNS rebinding. Pueden esperar hasta que despliegues en algún lugar que no sea localhost; **[Desplegar y escalar](deploy.md)** cubre `transport_security`.
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
---
|
||||
translation:
|
||||
sections: [e33d441f12d50535, 7099694c603e0f5f, c1df4cf9673433e6, c9cd294541422e6e, 6cec073617bfd037, efa92b8f99e908c8, 6a22a29e27fb4601]
|
||||
sections: [7be05607887e6853, e7375894888d9750, c36f73fc7e3af13b, 2fec2d7e129e62fe, 809b0e0a7c27295a, b4395a04d2a5d906, 1a436007f5f54779, c6b2078ed1e63ba5]
|
||||
tool: 1
|
||||
---
|
||||
# Manejo de errores {#handling-errors}
|
||||
|
||||
Una herramienta puede fallar de dos maneras, y el SDK las trata de forma muy distinta.
|
||||
Una herramienta puede fallar de tres maneras, y el SDK trata cada una de forma distinta.
|
||||
|
||||
Lanza una excepción ordinaria y la ve el **modelo**. Lanza `MCPError` y la ve el **protocolo**.
|
||||
Lanza `ToolError` y el **modelo** ve tu mensaje. Lanza `MCPError` y lo ve el **protocolo**. Lanza cualquier otra cosa y es un fallo inesperado: el modelo solo se entera de que la llamada falló, y el traceback va a tu log.
|
||||
|
||||
Esta página trata de cómo elegir.
|
||||
|
||||
|
|
@ -15,11 +15,11 @@ Esta página trata de cómo elegir.
|
|||
|
||||
Toma una herramienta que busca algo, y deja que la búsqueda falle:
|
||||
|
||||
```python title="server.py" hl_lines="11-12"
|
||||
```python title="server.py" hl_lines="2 12-13"
|
||||
--8<-- "docs_src/handling_errors/tutorial001.py"
|
||||
```
|
||||
|
||||
No hay nada de MCP en esas dos líneas. `get_author` lanza un `ValueError` común y corriente, como lo haría cualquier función de Python.
|
||||
`ToolError`, de `mcp.server.mcpserver.exceptions`, es la forma en que una herramienta le dice al modelo que algo salió mal.
|
||||
|
||||
Llámala con un título que no esté en el catálogo y observa el resultado:
|
||||
|
||||
|
|
@ -30,13 +30,15 @@ result.structured_content # None
|
|||
```
|
||||
|
||||
* La solicitud **tuvo éxito**. Hay un resultado; no se lanzó nada del lado de quien llama.
|
||||
* `is_error` es `True`, y el mensaje de tu excepción (con el nombre de la herramienta como prefijo) está en `content`, justo donde lee el modelo.
|
||||
* `is_error` es `True`, y tu mensaje (con el nombre de la herramienta como prefijo) está en `content`, justo donde lee el modelo.
|
||||
* `structured_content` es `None`. Una llamada fallida no tiene valor devuelto que estructurar.
|
||||
|
||||
Esto es un **error de herramienta**, y es el comportamiento por defecto para *cualquier* excepción que lance tu herramienta. Además, casi siempre es lo que quieres.
|
||||
Esto es un **error de herramienta**, y casi siempre es lo que quieres.
|
||||
|
||||
El modelo es quien llama a tu herramienta. Él eligió los argumentos. Así que un error de herramienta es un turno de la conversación: el modelo lee *"No book titled 'Nothing' in the catalog."*, se da cuenta de que adivinó mal el título y vuelve a llamar con uno mejor. Escribiste un `raise` y obtuviste un agente que se corrige solo.
|
||||
|
||||
En el servidor, un `ToolError` es una sola línea `INFO` en el log, sin traceback. Lo veías venir, así que no hay nada que investigar.
|
||||
|
||||
!!! tip
|
||||
Nunca devuelvas con `return` un mensaje de error desde una herramienta. Una cadena devuelta
|
||||
tiene `is_error=False`, así que para el modelo (y para toda interfaz de cliente) parece que la
|
||||
|
|
@ -44,7 +46,7 @@ El modelo es quien llama a tu herramienta. Él eligió los argumentos. Así que
|
|||
|
||||
## Un error que el modelo no puede corregir {#an-error-the-model-cannot-fix}
|
||||
|
||||
Ahora cambia `ValueError` por `MCPError`.
|
||||
Ahora cambia `ToolError` por `MCPError`.
|
||||
|
||||
```python title="server.py" hl_lines="1 3 14"
|
||||
--8<-- "docs_src/handling_errors/tutorial002.py"
|
||||
|
|
@ -77,10 +79,10 @@ Ahora cambia `ValueError` por `MCPError`.
|
|||
|
||||
Los dos caminos responden a dos preguntas distintas.
|
||||
|
||||
* **Lanza cualquier excepción** ante un fallo de *ejecución*: lo que tu herramienta intentó hacer no funcionó. El modelo eligió la llamada, así que el modelo debería ver la consecuencia y tener la oportunidad de recuperarse. Un título mal escrito, una API externa que agotó el tiempo de espera, una fila que no existe: todos son errores de herramienta.
|
||||
* **Lanza `ToolError`** ante un fallo de *ejecución*: lo que tu herramienta intentó hacer no funcionó. El modelo eligió la llamada, así que el modelo debería ver la consecuencia y tener la oportunidad de recuperarse. Un título mal escrito, una API externa que agotó el tiempo de espera, una fila que no existe: todos son errores de herramienta.
|
||||
* **Lanza `MCPError`** cuando debe rechazarse la *solicitud misma*: al cliente le falta una capacidad de la que depende tu herramienta, el servidor no está en condiciones de atender a nadie, quien llama se saltó un paso obligatorio. Ningún reintento del modelo arregla nada de eso, así que no se gana nada entregándole el mensaje.
|
||||
|
||||
Una sola pregunta lo decide: **¿podría haberlo evitado un modelo más inteligente?** Sí -> excepción ordinaria. No -> `MCPError`.
|
||||
Una sola pregunta lo decide: **¿podría haberlo evitado un modelo más inteligente?** Sí -> `ToolError`. No -> `MCPError`.
|
||||
|
||||
Según ese criterio, la segunda versión de `get_author` eligió mal: un título mejor lo arregla, así que el modelo merecía ver el mensaje. Está ahí para mostrarte el mecanismo, no para recomendarlo.
|
||||
|
||||
|
|
@ -89,6 +91,25 @@ Según ese criterio, la segunda versión de `get_author` eligió mal: un título
|
|||
opcional `data`. Lo que pongas en ellos es lo que recibe el cliente: el SDK reenvía un
|
||||
`MCPError` lanzado tal cual, en lugar de sanearlo.
|
||||
|
||||
## Cualquier otra excepción {#any-other-exception}
|
||||
|
||||
Ahora quita la comprobación y deja que la búsqueda en el diccionario falle por sí sola:
|
||||
|
||||
```python title="server.py" hl_lines="11"
|
||||
--8<-- "docs_src/handling_errors/tutorial004.py"
|
||||
```
|
||||
|
||||
`CATALOG[title]` lanza `KeyError`. No lo tenías previsto, así que el SDK lo trata como un fallo inesperado:
|
||||
|
||||
```python
|
||||
result.is_error # True
|
||||
result.content # [TextContent(text="Error executing tool get_author")]
|
||||
```
|
||||
|
||||
La llamada sigue devolviendo `is_error=True`, así que el modelo sabe que falló y puede seguir adelante. Lo que no recibe es el texto de la excepción: un `KeyError` de tu código, o un montón de SQL de un driver tres bibliotecas más abajo, puede describir el funcionamiento interno de tu servidor, así que nunca sale del servidor.
|
||||
|
||||
Lo recibes tú. El servidor registra el fallo inesperado en nivel `ERROR` con el traceback completo, como `Tool 'get_author' raised an unexpected exception`. Así, un log de producción en `WARNING` se mantiene en silencio ante cada `ToolError` y habla en cuanto algo está realmente roto.
|
||||
|
||||
## Un recurso que no existe {#a-resource-that-doesnt-exist}
|
||||
|
||||
Los recursos trazan la misma línea, e incluyen una excepción con nombre propio para el caso común.
|
||||
|
|
@ -109,7 +130,7 @@ Cuando no pueda, lanza `ResourceNotFoundError`. El SDK lo convierte en el error
|
|||
}
|
||||
```
|
||||
|
||||
Fíjate en que aquí no hay un resultado a medias con `is_error=True`. La lectura de un recurso devuelve contenido o falla: los recursos solo tienen el camino del protocolo. Las plantillas y todo lo demás sobre recursos están en **[Recursos](resources.md)**.
|
||||
Fíjate en que aquí no hay un resultado a medias con `is_error=True`. La lectura de un recurso devuelve contenido o falla: los recursos solo tienen el camino del protocolo. `ResourceError` es lo mismo para un fallo que no es "no encontrado" (`-32603`, tu mensaje), y ambos son una sola línea `INFO` en tu log. Cualquier otra excepción salvo `MCPError` es un fallo inesperado: el cliente recibe un `-32603` que solo nombra la URI, y el traceback va a tu log en nivel `ERROR`. Las plantillas y todo lo demás sobre recursos están en **[Recursos](resources.md)**.
|
||||
|
||||
## Errores que nunca lanzas {#errors-you-never-raise}
|
||||
|
||||
|
|
@ -120,20 +141,22 @@ Envíale a `get_author` un `title` que no sea una cadena y el SDK lo rechaza con
|
|||
Eso significa toda una clase de sentencias `raise` que no escribes: no vuelvas a validar tus propias anotaciones de tipo.
|
||||
|
||||
!!! info
|
||||
Todo lo de esta página es lo que ve un **cliente**, y el `Client` en memoria con el que
|
||||
escribirás pruebas ve exactamente lo mismo. Ni siquiera `raise_exceptions=True` convierte un
|
||||
error de herramienta de nuevo en un traceback: para cuando ese indicador podría actuar, tu
|
||||
excepción ya es el resultado con `is_error=True`. Haz las aserciones sobre el resultado.
|
||||
Todo lo que ve un **cliente** en esta página lo ve también el `Client` en memoria con el que
|
||||
escribirás pruebas. Ni siquiera `raise_exceptions=True` le devuelve a quien llama la excepción
|
||||
de una herramienta que falla: para cuando ese indicador podría actuar, tu excepción ya es el
|
||||
resultado con `is_error=True`. Haz las aserciones sobre el resultado. Si necesitas el traceback
|
||||
de un fallo inesperado, está en el log del servidor, y el `caplog` de pytest lo captura.
|
||||
**[Pruebas](../get-started/testing.md)** cubre el patrón.
|
||||
|
||||
## Resumen {#recap}
|
||||
|
||||
* Lanza **cualquier excepción** en una herramienta -> la llamada devuelve `is_error=True` con tu mensaje en `content`. El modelo lo lee y puede reintentar. Este es el comportamiento por defecto.
|
||||
* Lanza **`ToolError`** en una herramienta -> la llamada devuelve `is_error=True` con tu mensaje en `content`. El modelo lo lee y puede reintentar.
|
||||
* Lanza **`MCPError`** -> la llamada misma falla con un error JSON-RPC. El modelo no ve nada; el host se encarga. `code`, `message` y `data` sobreviven intactos.
|
||||
* La pregunta decisiva: *¿podría haberlo evitado un modelo más inteligente?* Sí -> excepción. No -> `MCPError`.
|
||||
* La pregunta decisiva: *¿podría haberlo evitado un modelo más inteligente?* Sí -> `ToolError`. No -> `MCPError`.
|
||||
* Cualquier **otra excepción** es un fallo inesperado -> `is_error=True` con solo `Error executing tool <name>` para el modelo, y un registro `ERROR` con el traceback para ti.
|
||||
* `ResourceNotFoundError` desde un handler de recurso -> el `-32602` del protocolo, con la URI en `data`.
|
||||
* Los argumentos incorrectos se rechazan contra el esquema antes de que se ejecute tu función; para esos no usas `raise`.
|
||||
* `from mcp import MCPError`; las constantes de códigos de error vienen de `mcp.types`.
|
||||
* Importaciones: `from mcp import MCPError`, `from mcp.server.mcpserver.exceptions import ToolError, ResourceError, ResourceNotFoundError`, y las constantes de códigos de error de `mcp.types`.
|
||||
|
||||
Errores resueltos. Eso es todo lo que un servidor *expone*. Lo que cada handler puede leer, y hacer de vuelta hacia el cliente mientras se ejecuta, es la siguiente sección: **[Dentro de tu handler](../handlers/index.md)**.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [496394d24d221bf1, 4ceb4591180dc6c3, 0fd63e4682d02e0c, 969ede0bd3686a16, 043f526230dd243d, 6ee3e9bcfd24047a]
|
||||
sections: [496394d24d221bf1, 4ceb4591180dc6c3, 0fd63e4682d02e0c, 969ede0bd3686a16, 864137b5e9c61e91, 043f526230dd243d, db1ef91db7d6b3f3]
|
||||
tool: 1
|
||||
---
|
||||
# Multimedia {#media}
|
||||
|
|
@ -86,6 +86,24 @@ Un sufijo que no reconoce recurre a `application/octet-stream`.
|
|||
`Audio` así a partir de bytes MP3 y al cliente se le dice `mime_type="audio/wav"`, y entonces
|
||||
falla fielmente al decodificarlo. Cuando pases `data=`, pasa `format=`.
|
||||
|
||||
## Incrustar un recurso {#embedding-a-resource}
|
||||
|
||||
Una herramienta también puede devolver un documento: un texto o unos bytes junto con la URI donde vive y un tipo MIME. Eso es un **`EmbeddedResource`**, otro tipo de bloque de contenido. A diferencia de un `str` simple, le dice al cliente qué es el contenido, así que el cliente puede mostrarlo como un adjunto o reconocer un recurso que ya conoce.
|
||||
|
||||
```python title="server.py" hl_lines="7 14 16-18"
|
||||
--8<-- "docs_src/media/tutorial005.py"
|
||||
```
|
||||
|
||||
* `brand://guidelines` es un recurso normal y corriente (**[Recursos](resources.md)** los cubre). La herramienta le entrega el mismo documento al modelo cuando lo pide, y llamar a `guidelines()` directamente mantiene una única fuente de verdad.
|
||||
* `EmbeddedResource` y `TextResourceContents` vienen de `mcp.types`. No hay una utilidad como la de las imágenes: el bloque que construyes entra en el resultado sin cambios, y no hay `structured_content`.
|
||||
* Usa la URI con la que está registrado el recurso, para que un cliente pueda saber que el adjunto y `brand://guidelines` son el mismo documento. Cualquier URI es válida, registrada o no.
|
||||
|
||||
```python
|
||||
result.content # [EmbeddedResource(type="resource", resource=TextResourceContents(uri="brand://guidelines", mime_type="text/markdown", text="# Brand guidelines\n\n..."))]
|
||||
```
|
||||
|
||||
Para contenido binario, usa `BlobResourceContents(uri=..., mime_type=..., blob=...)` con los bytes codificados en base64 en `blob`, en lugar de `TextResourceContents`. Para enviar solo un puntero que el cliente pueda leer más tarde con `resources/read`, devuelve en su lugar un `ResourceLink(name=..., uri=...)`; también es un bloque de contenido.
|
||||
|
||||
## Iconos {#icons}
|
||||
|
||||
Un `Icon` es metadatos, no contenido. No lleva la imagen; apunta a una con una URI, y el cliente puede descargarla y mostrarla junto al nombre de tu servidor, una herramienta, un recurso o un prompt.
|
||||
|
|
@ -115,6 +133,7 @@ Los iconos de una herramienta están en el objeto `Tool` de `tools/list`, los de
|
|||
|
||||
* Devuelve un `Image` o un `Audio` desde una herramienta y el cliente recibe un bloque `ImageContent` / `AudioContent`: tus bytes codificados en base64, con un tipo MIME.
|
||||
* Constrúyelo a partir de un `path=` y deja que el sufijo decida el tipo MIME, o a partir de `data=` en memoria más un `format=` explícito.
|
||||
* Devuelve un `EmbeddedResource` para poner un documento (texto o un blob en base64, con su URI y tipo MIME) en el resultado, o un `ResourceLink` para enviar solo el puntero.
|
||||
* Los resultados multimedia no llevan `structured_content` ni esquema de salida.
|
||||
* Un `Icon` es un puntero: una URI `src` más `mime_type`, `sizes` y `theme` opcionales.
|
||||
* `icons=[...]` funciona en el servidor, en herramientas, en recursos y en prompts, y los clientes los encuentran en los objetos correspondientes.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [d65c098f37f5b6c3, dd0c2724d6f2877e, 6835bb3570c6714c, ffe823cb0fedd488, f33651add1b59094]
|
||||
sections: [d65c098f37f5b6c3, dd0c2724d6f2877e, 6835bb3570c6714c, d30d3c20168b88b2, f5ef38dad59d6f76, 6e38a699ba57fbdf, 2b984a3bf37a0ddd]
|
||||
tool: 1
|
||||
---
|
||||
# Prompts {#prompts}
|
||||
|
|
@ -140,10 +140,55 @@ La entrada de `prompts/list` ahora lleva todo lo que un cliente necesita para di
|
|||
```
|
||||
|
||||
!!! info
|
||||
Si has leído **[Herramientas](tools.md)**, ya sabes todo lo de esta página. El mismo decorador, el mismo
|
||||
Si has leído **[Herramientas](tools.md)**, ya sabes todo lo visto hasta aquí. El mismo decorador, el mismo
|
||||
docstring como descripción, el mismo `Annotated`/`Field`. Lo único que cambia es quién
|
||||
lo dispara (el usuario) y adónde va el resultado (a la conversación).
|
||||
|
||||
## Más que texto {#more-than-text}
|
||||
|
||||
`UserMessage` y `AssistantMessage` también aceptan un bloque de contenido, o un helper `Image` / `Audio`, en cualquier lugar donde aceptan un `str`. En los prompts aparecen dos casos: adjuntar un documento y adjuntar una imagen.
|
||||
|
||||
### Incrustar un archivo {#embedding-a-file}
|
||||
|
||||
```python title="server.py" hl_lines="5 12 21 23"
|
||||
--8<-- "docs_src/prompts/tutorial004.py"
|
||||
```
|
||||
|
||||
* La guía de estilo es un recurso en `style://python` (**[Recursos](resources.md)** los cubre), leído de un `style-guide.md` junto a `server.py`. Pon ahí cualquier archivo Markdown.
|
||||
* `EmbeddedResource(resource=TextResourceContents(...))`, ambos de `mcp.types`, lleva el archivo con su URI y su tipo MIME como primer mensaje; la solicitud que se refiere a él va después como texto plano.
|
||||
* Incrustar la guía, en lugar de pegarla en el f-string, permite al cliente mostrarla como adjunto y volver a abrir `style://python` más tarde, y el modelo recibe el archivo tal cual. Para un archivo binario usa `BlobResourceContents` con un `blob` en base64.
|
||||
|
||||
Renderizado, el `content` del primer mensaje es un bloque `resource`:
|
||||
|
||||
```json
|
||||
{"type": "resource", "resource": {"uri": "style://python", "mimeType": "text/markdown", "text": "* Prefer early returns.\n..."}}
|
||||
```
|
||||
|
||||
### Adjuntar una imagen {#attaching-an-image}
|
||||
|
||||
```python title="server.py" hl_lines="4 15"
|
||||
--8<-- "docs_src/prompts/tutorial005.py"
|
||||
```
|
||||
|
||||
* `Image` es el helper de **[Imágenes, audio e iconos](media.md)**. `UserMessage` lo convierte en un bloque `ImageContent` (el archivo codificado en base64, el tipo MIME deducido de `.png`) cuando se renderiza el prompt; `Audio` se convierte en un `AudioContent` del mismo modo.
|
||||
* Pon cualquier PNG llamado `architecture.png` junto a `server.py`. Los argumentos de un prompt son cadenas, así que la imagen siempre viene del servidor; `component` solo aporta las palabras.
|
||||
|
||||
```json
|
||||
{"type": "image", "data": "iVBORw0KGgoAAAANSUhEUg...", "mimeType": "image/png"}
|
||||
```
|
||||
|
||||
## Cambiar la lista en tiempo de ejecución {#changing-the-list-at-runtime}
|
||||
|
||||
Se pueden añadir prompts mientras hay clientes conectados, por ejemplo para que un usuario guarde una instrucción como entrada de menú propia. Registra el prompt y luego notifica:
|
||||
|
||||
```python title="server.py" hl_lines="5 23-27"
|
||||
--8<-- "docs_src/prompts/tutorial006.py"
|
||||
```
|
||||
|
||||
* `mcp.add_prompt(Prompt.from_function(fn, name=..., description=...))` registra una función exactamente como lo haría `@mcp.prompt()`, y `mcp.remove_prompt(name)` es lo inverso. `add_prompt` conserva una entrada existente con el mismo nombre en lugar de sobrescribirla, así que la herramienta elimina primero cualquier entrada anterior para que guardar equivalga a reemplazar. `prompts/list` refleja el cambio de inmediato.
|
||||
* `await ctx.notify_prompts_changed()` envía `notifications/prompts/list_changed` a cada cliente `2026-07-28` que escucha en un stream `subscriptions/listen` (**[Suscripciones](../handlers/subscriptions.md)**). `await ctx.session.send_prompt_list_changed()` se lo envía al cliente que hace la llamada cuando ese cliente es anterior a 2026 (**[Atender clientes heredados](../run/legacy-clients.md)**). Llama a los dos; cada uno no hace nada cuando no hay nadie a quien avisar.
|
||||
* Un cliente que recibe la notificación vuelve a llamar a `prompts/list`. En el `Client` de Python eso es `async with client.listen(prompts_list_changed=True) as sub:`, que produce un evento `PromptsListChanged`.
|
||||
|
||||
## Resumen {#recap}
|
||||
|
||||
* `@mcp.prompt()` en una función la convierte en un prompt. El nombre sale de la función y la descripción del docstring.
|
||||
|
|
@ -152,5 +197,7 @@ La entrada de `prompts/list` ahora lleva todo lo que un cliente necesita para di
|
|||
* Devuelve un `str` y se convierte en un mensaje de usuario. Devuelve una lista de `UserMessage` / `AssistantMessage` para sembrar una conversación de varios turnos.
|
||||
* `title=` y `Field(description=...)` son lo que un cliente pone en su interfaz.
|
||||
* Un argumento obligatorio que falta hace fallar toda la solicitud. No hay un resultado de error por prompt.
|
||||
* Envuelve un `EmbeddedResource` o un `Image` en un `UserMessage` para adjuntar un documento o una imagen.
|
||||
* Añade o quita prompts en tiempo de ejecución con `mcp.add_prompt(...)` / `mcp.remove_prompt(...)`, y luego `await ctx.notify_prompts_changed()` y `await ctx.session.send_prompt_list_changed()`.
|
||||
|
||||
El autocompletado en el servidor de los argumentos de un prompt (o de una plantilla de recurso) está en **[Autocompletado](completions.md)**.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [a838d57f003aed44, 857d03886a0137ed, 42d9efcb9f542867, 2290ff08435b5573, e866c192e11d1c14, 6cdbad079f7b47f0, d4b607372fb28b51, 18dbf726ac45e0b7, c6f7d2a148aa49f4, c851964bb3301907, d715db6f8dccc9cc, ef86634aa70498a7]
|
||||
sections: [a838d57f003aed44, 857d03886a0137ed, 42d9efcb9f542867, 2290ff08435b5573, 91be9b73602abcf1, 6cdbad079f7b47f0, d4b607372fb28b51, 18dbf726ac45e0b7, c7eff2a5698225fa, c851964bb3301907, 8f296f1f09e4c400, d715db6f8dccc9cc, a0c344a48450dbe4]
|
||||
tool: 1
|
||||
---
|
||||
# Salida estructurada {#structured-output}
|
||||
|
|
@ -105,7 +105,7 @@ No todas las formas merecen una clase. Un `TypedDict` produce el mismo esquema:
|
|||
--8<-- "docs_src/structured_output/tutorial003.py"
|
||||
```
|
||||
|
||||
Un `TypedDict` es un `dict` normal en tiempo de ejecución, así que eso es lo que construyes y devuelves. El esquema, la validación y `structured_content` son idénticos a los de la versión con `BaseModel` (salvo las descripciones, para las que `TypedDict` no tiene sitio).
|
||||
Un `TypedDict` es un `dict` normal en tiempo de ejecución, así que eso es lo que construyes y devuelves. El esquema, la validación y `structured_content` siguen las mismas reglas que la versión con `BaseModel`: añade un docstring a la clase o `Annotated[..., Field(description=...)]` y se convierten en las descripciones, y una clave `NotRequired` que dejes fuera del dict se queda fuera de `structured_content`.
|
||||
|
||||
## Una dataclass {#a-dataclass}
|
||||
|
||||
|
|
@ -187,18 +187,19 @@ No lo notas mientras construyes el valor a mano: Pydantic ya se aseguró de que
|
|||
La anotación promete `WeatherData`. La respuesta del servicio externo dejó de enviar `humidity`.
|
||||
|
||||
!!! check
|
||||
Llama a `get_weather` y no le entrega al cliente en silencio un objeto medio vacío. La llamada falla,
|
||||
y las primeras líneas del error nombran el campo:
|
||||
Llama a `get_weather` y no le entrega al cliente en silencio un objeto medio vacío. La llamada falla:
|
||||
el cliente recibe `is_error=True` con `Error executing tool get_weather`, así que el modelo sabe que la
|
||||
llamada falló en lugar de leer con toda confianza un tiempo que no existe. El nombre del campo es para ti,
|
||||
en el log del servidor con nivel `ERROR`:
|
||||
|
||||
```text
|
||||
Error executing tool get_weather: 1 validation error for WeatherData
|
||||
Tool 'get_weather' raised an unexpected exception
|
||||
...
|
||||
pydantic_core._pydantic_core.ValidationError: 1 validation error for WeatherData
|
||||
humidity
|
||||
Field required [type=missing, input_value={'temperature': 16.2, 'conditions': 'Overcast'}, input_type=dict]
|
||||
```
|
||||
|
||||
Ese texto vuelve como resultado de la herramienta con `is_error=True`, así que el modelo sabe que la
|
||||
llamada falló en lugar de leer con toda confianza un tiempo que no existe.
|
||||
|
||||
Por cierto, devolver un `dict` normal desde una herramienta `-> WeatherData` está bien. Es exactamente lo que produjo `json.loads`. La validación se aplica al valor, no al tipo de Python.
|
||||
|
||||
## Desactivarlo {#opting-out}
|
||||
|
|
@ -213,6 +214,10 @@ Sin `output_schema`, sin envoltorio, sin validación. `structured_content` es `N
|
|||
|
||||
Lo contrario, `structured_output=True`, convierte la detección automática en un requisito: una herramienta cuyo tipo de retorno no pueda producir un esquema lanza una excepción al importar el módulo en lugar de recurrir al texto.
|
||||
|
||||
## Bloques de contenido y medios {#content-blocks-and-media}
|
||||
|
||||
Los bloques de contenido y los medios (`TextContent`, `EmbeddedResource`, `Image`, `Audio` y compañía, ya sea solos, como elementos de un `list`, `tuple` o `Sequence`, o como ramas de una unión) quedan excluidos sin que hagas nada: son para que los lea el modelo, así que la detección automática no deriva ningún esquema de ellos (**[Imágenes, audio e iconos](media.md)** se ocupa de `Image` y `Audio`). `structured_output=True` sigue forzando uno para las clases de bloques de contenido.
|
||||
|
||||
## Una clase sin anotaciones de tipo {#a-class-without-type-hints}
|
||||
|
||||
Hay una forma de acabar sin salida estructurada sin haberlo pedido: devolver una clase que **no tiene anotaciones en su cuerpo**.
|
||||
|
|
@ -245,6 +250,6 @@ Hay una forma de acabar sin salida estructurada sin haberlo pedido: devolver una
|
|||
* Los escalares, las listas, las tuplas y las uniones se envuelven en `{"result": ...}`. Los modelos, los `TypedDict`, las dataclasses, las clases con anotaciones y `dict[str, ...]` ya son objetos y se quedan como están.
|
||||
* Cada resultado lleva `content` (texto, para el modelo) **y** `structured_content` (datos, para la aplicación).
|
||||
* Lo que devuelves se valida contra el esquema. Una discrepancia es un error de herramienta, no un resultado corrupto.
|
||||
* `structured_output=False` excluye una herramienta. Una clase sin anotaciones de tipo queda excluida en silencio; vigílalo.
|
||||
* `structured_output=False` excluye una herramienta. Los bloques de contenido, `Image` y `Audio` quedan excluidos por defecto; una clase sin anotaciones de tipo queda excluida en silencio, así que vigílalo.
|
||||
|
||||
Ahora dominas todo lo que una herramienta puede responder. A continuación, la segunda primitiva: **[Recursos](resources.md)**.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [e4cc390d56573409, 8566e2b68594e9ad, 2c97b9f888398951, 048e5471dfa71aea, 3076b1e16ad95950, edbedf2a16e71311, 3d8ef8da89fa87c1, f6c0e02e6ea5a363]
|
||||
sections: [e4cc390d56573409, f30cf8103a6e918c, 2c97b9f888398951, 048e5471dfa71aea, 3076b1e16ad95950, edbedf2a16e71311, 3d8ef8da89fa87c1, f6c0e02e6ea5a363]
|
||||
tool: 1
|
||||
---
|
||||
# Herramientas {#tools}
|
||||
|
|
@ -39,6 +39,8 @@ A partir de esas anotaciones de tipo, el SDK genera un JSON Schema y lo envía a
|
|||
|
||||
Ambos argumentos están en `required` porque ninguno tiene valor por defecto. Lo arreglarás en un momento. (Las claves `title` son artefactos de Pydantic; las propiedades, sus tipos y `required` son el contrato.)
|
||||
|
||||
Tampoco hay una clave `$schema`: MCP trata un esquema que no la tiene como **JSON Schema 2020-12**, que es lo que genera Pydantic, así que no hay nada que elegir hasta que escribas esquemas a mano en el **[Server de bajo nivel](../advanced/low-level-server.md#the-dialect-is-json-schema-2020-12)**.
|
||||
|
||||
!!! tip
|
||||
Aquí las anotaciones de tipo no son documentación. Son **el contrato**. Si un cliente envía `"limit": "ten"`,
|
||||
el SDK lo rechaza antes de que tu función llegue a ejecutarse.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [4a7033e1ed8ad602, 55dcbfff0c6271bf, 101ef9d14bf4ec46, 4b6c4a845438abc7, f98b46bafbee4acd]
|
||||
sections: [4a7033e1ed8ad602, 55dcbfff0c6271bf, 317f4256a650cab6, 4b6c4a845438abc7, f98b46bafbee4acd]
|
||||
tool: 1
|
||||
---
|
||||
# Plantillas de URI y seguridad de rutas {#uri-templates-and-path-safety}
|
||||
|
|
@ -164,7 +164,7 @@ Las comprobaciones integradas detienen los casos comunes, pero no pueden conocer
|
|||
de tu entorno aislado. Para acceder al sistema de archivos, usa `safe_join` para resolver la
|
||||
ruta y verificar que se mantiene dentro de tu directorio base:
|
||||
|
||||
```python title="server.py" hl_lines="4 14"
|
||||
```python title="server.py" hl_lines="5 15"
|
||||
--8<-- "docs_src/uri_templates/tutorial002.py"
|
||||
```
|
||||
|
||||
|
|
@ -204,10 +204,10 @@ Estas comprobaciones son un prefiltro heurístico; para el acceso al sistema de
|
|||
`safe_join` sigue siendo el límite de contención.
|
||||
|
||||
!!! tip
|
||||
Si tu handler no puede satisfacer la solicitud (el archivo no existe,
|
||||
el id es desconocido), lanza una excepción. El SDK la convierte en una
|
||||
respuesta de error. Consulta **[Manejo de errores](handling-errors.md)** para ver la diferencia entre un
|
||||
error de protocolo y un error de herramienta.
|
||||
Si tu handler no puede satisfacer la solicitud (el archivo no existe, el id es desconocido), lanza
|
||||
`ResourceNotFoundError` como hace `read_manual` arriba. El cliente recibe `-32602` con tu mensaje
|
||||
y la URI. Una excepción inesperada se convierte, en cambio, en un `-32603` genérico. Consulta
|
||||
**[Manejo de errores](handling-errors.md#a-resource-that-doesnt-exist)**.
|
||||
|
||||
## Recursos en el Server de bajo nivel {#resources-on-the-low-level-server}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [2efaecdef109a5c5, fcacd3e66b8635a4, 25323d737dcf0261, 4835ed1772f1d113, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 323ef84f6b4bebde, 30fd31be74169d9a, 656943c6cb567218, c2dc3b1007d2e987, 7cf5386b997d04e9, 0b59feed8384456e, 0cba47bae78d04eb, 954dc21efdb532a3]
|
||||
sections: [2efaecdef109a5c5, fcacd3e66b8635a4, 25323d737dcf0261, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 323ef84f6b4bebde, 30fd31be74169d9a, 656943c6cb567218, c2dc3b1007d2e987, 7cf5386b997d04e9, 0b59feed8384456e, 0cba47bae78d04eb, e4355f4c7cf4fb2e]
|
||||
tool: 1
|
||||
---
|
||||
# Solución de problemas {#troubleshooting}
|
||||
|
|
@ -81,11 +81,11 @@ async def main() -> None:
|
|||
|
||||
`__aexit__` es la desconexión, y por eso no hay ningún `client.close()` que olvidar. **[Pruebas](get-started/testing.md)** se basa exactamente en este patrón.
|
||||
|
||||
## `Error executing tool <name>: <message>` y `Unknown tool: <name>` {#error-executing-tool-name-message-and-unknown-tool-name}
|
||||
## `Error executing tool <name>: <message>`, `Error executing tool <name>` y `Unknown tool: <name>` {#error-executing-tool-name-message-error-executing-tool-name-and-unknown-tool-name}
|
||||
|
||||
Estás leyendo un **resultado**, no una excepción. `call_tool` no lanzó nada, y nunca lo hará para una herramienta que falla.
|
||||
|
||||
Llama a `forecast` con una ciudad que el servidor no conoce y la excepción que lanza vuelve con la solicitud marcada como *correcta*:
|
||||
Llama a `forecast` con una ciudad que el servidor no conoce y el `ToolError` que lanza vuelve con la solicitud marcada como *correcta*:
|
||||
|
||||
```python
|
||||
result.is_error # True
|
||||
|
|
@ -97,6 +97,8 @@ result.structured_content # None
|
|||
|
||||
La solución está en tu cliente: **comprueba `result.is_error`**. Un `try/except` alrededor de `call_tool` no captura ninguno de estos casos, porque no hay nada que capturar. Es deliberado, y es lo más útil de esta página que puedes interiorizar: el *modelo* eligió la llamada, así que el modelo recibe el mensaje y la oportunidad de intentarlo de nuevo. **[Manejo de errores](servers/handling-errors.md)** tiene todos los detalles, incluida la vía de `MCPError` que *sí* lanza.
|
||||
|
||||
La forma escueta, `Error executing tool <name>` sin mensaje, significa que la herramienta **se cayó**: se le escapó una excepción que no había previsto (o su valor devuelto no pasó el esquema de salida), y el texto de esa excepción no se transmite. El traceback está en el **log del servidor** con nivel `ERROR`, como `Tool '<name>' raised an unexpected exception`.
|
||||
|
||||
## `TypeError: The @tool decorator was used incorrectly. Did you forget to call it? Use @tool() instead of @tool` {#typeerror-the-tool-decorator-was-used-incorrectly-did-you-forget-to-call-it-use-tool-instead-of-tool}
|
||||
|
||||
Escribiste `@mcp.tool` en lugar de `@mcp.tool()`. `tool()` es una *fábrica* de decoradores: sin los paréntesis, Python le pasa tu función a su parámetro `name=`.
|
||||
|
|
@ -409,7 +411,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key
|
|||
## Resumen {#recap}
|
||||
|
||||
* `ExceptionGroup: unhandled errors in a TaskGroup` nunca es el error. Lee la **última línea**; capturar `MCPError` *dentro* del bloque `async with Client(...)` evita el envoltorio por completo.
|
||||
* `call_tool` no lanza nada para una herramienta que falla. `Error executing tool ...` y `Unknown tool: ...` son resultados: comprueba `result.is_error`.
|
||||
* `call_tool` no lanza nada para una herramienta que falla. `Error executing tool ...` y `Unknown tool: ...` son resultados: comprueba `result.is_error`. Si no hay mensaje después del nombre de la herramienta, se cayó, y el traceback está en el log del servidor.
|
||||
* `Client must be used within an async context manager` -> usa `async with`. `Use @tool() instead of @tool` -> añade los paréntesis.
|
||||
* `Tool already exists:` en el log del servidor es la única señal de que dos herramientas con el mismo nombre se fundieron en una.
|
||||
* Un 421, tres formas de escribirlo: `Server returned an error response` (el `Client` de python), `421 Misdirected Request` / `Invalid Host header` (todo lo demás), `Invalid Host header: <host>` (el log del servidor). Solución: `transport_security=TransportSecuritySettings(allowed_hosts=[...])`.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [cfe01c0c5863dfa2, 11d93f1fa09eadf5, a7392996acf1ad8f, 875eb2889263424e]
|
||||
sections: [cfe01c0c5863dfa2, 1c58c5cfcc37d455, a7392996acf1ad8f, 875eb2889263424e]
|
||||
tool: 1
|
||||
---
|
||||
# Novedades de la v2 {#whats-new-in-v2}
|
||||
|
|
@ -47,9 +47,9 @@ La v1 te entregaba tres capas anidadas: un gestor de contexto de transporte que
|
|||
--8<-- "docs_src/client/tutorial001.py"
|
||||
```
|
||||
|
||||
`Client` acepta un objeto servidor (en memoria, sin transporte: la historia de las pruebas), una URL (Streamable HTTP) o cualquier gestor de contexto de transporte como `stdio_client(...)`. Entrar en `async with` conecta y negocia la versión del protocolo, sea cual sea la generación que hable el servidor; `client.server_capabilities` y `client.protocol_version` simplemente están ahí después, y `client.server_info` también cuando el servidor se identifica (ahora es `Implementation | None`, porque la identidad en la generación 2026 es opcional). Los callbacks de muestreo y elicitación que registraste en la v1 siguen funcionando (sus cuerpos ven el mismo renombramiento de atributos a snake_case que todo lo demás en esta página), ahora también responden a las solicitudes dentro de resultados al estilo 2026 (más abajo) y se ejecutan concurrentemente en lugar de una a una. `ClientSession` sigue debajo para quien quiera la superficie de bajo nivel, y `client.session` te la entrega; también cambió (corre sobre el nuevo motor de despacho, y algunas de sus propias firmas cambiaron), así que lee la **[Guía de migración](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)** antes de bajar a ese nivel.
|
||||
`Client` acepta un objeto servidor (en memoria, sin transporte: la historia de las pruebas), una URL (Streamable HTTP), un `StdioServerParameters` (un subproceso stdio) o cualquier otro gestor de contexto de transporte como `sse_client(...)`. Entrar en `async with` conecta y negocia la versión del protocolo, sea cual sea la generación que hable el servidor; `client.server_capabilities` y `client.protocol_version` simplemente están ahí después, y `client.server_info` también cuando el servidor se identifica (ahora es `Implementation | None`, porque la identidad en la generación 2026 es opcional). Los callbacks de muestreo y elicitación que registraste en la v1 siguen funcionando (sus cuerpos ven el mismo renombramiento de atributos a snake_case que todo lo demás en esta página), ahora también responden a las solicitudes dentro de resultados al estilo 2026 (más abajo) y se ejecutan concurrentemente en lugar de una a una. `ClientSession` sigue debajo para quien quiera la superficie de bajo nivel, y `client.session` te la entrega; también cambió (se ejecuta sobre el nuevo motor de despacho, y algunas de sus propias firmas cambiaron), así que lee la **[Guía de migración](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)** antes de bajar a ese nivel.
|
||||
|
||||
**[El Client](client/index.md)** lo presenta, **[Transportes del cliente](client/transports.md)** cubre las tres formas de conexión, **[Callbacks del cliente](client/callbacks.md)** cubre los callbacks en sí y **[Pruebas](get-started/testing.md)** muestra el patrón en memoria que sustituye al helper `create_connected_server_and_client_session()` de la v1.
|
||||
**[El Client](client/index.md)** lo presenta, **[Transportes del cliente](client/transports.md)** cubre las cuatro formas de conexión, **[Callbacks del cliente](client/callbacks.md)** cubre los callbacks en sí y **[Pruebas](get-started/testing.md)** muestra el patrón en memoria que sustituye al helper `create_connected_server_and_client_session()` de la v1.
|
||||
|
||||
### El `Server` de bajo nivel se reconstruyó, no se renombró {#the-low-level-server-was-rebuilt-not-renamed}
|
||||
|
||||
|
|
@ -135,7 +135,7 @@ En esos tipos, cada atributo de Python es ahora snake_case: `result.is_error`, `
|
|||
Los renombramientos se anuncian solos. Estos no:
|
||||
|
||||
* **Las funciones síncronas se ejecutan en un hilo de trabajo.** Una herramienta `def` (o recurso, prompt o resolutor) ya no bloquea el bucle de eventos; la contrapartida es que su cuerpo ya no se ejecuta *en* el hilo del bucle de eventos, lo que importa para código afín a un hilo. Los handlers `async def` no cambian. **[Guía de migración](migration.md#sync-handler-functions-now-run-on-a-worker-thread)**.
|
||||
* **`MCPError` (el `McpError` de la v1) lanzado dentro de una herramienta es ahora un error de protocolo.** El modelo nunca lo ve. Cualquier otra excepción sigue convirtiéndose en un resultado `is_error=True` que el modelo puede leer y al que puede reaccionar. **[Manejo de errores](servers/handling-errors.md)** explica la división.
|
||||
* **`MCPError` (el `McpError` de la v1) lanzado dentro de una herramienta es ahora un error de protocolo.** El modelo nunca lo ve. Cualquier otra excepción sigue convirtiéndose en un resultado `is_error=True`, pero solo el mensaje de un `ToolError` llega al modelo: cualquier otra excepción se lee ahora como `Error executing tool <name>`, con el traceback en el log de tu servidor. **[Manejo de errores](servers/handling-errors.md)** explica la división.
|
||||
* **Los resultados se validan antes de salir.** Un `Tool` construido a mano cuyo `input_schema` sea `{}` ahora falla en `tools/list` (la especificación exige `"type": "object"`). Los servidores construidos sobre `@mcp.tool()` nunca ven esto; el SDK escribe sus esquemas.
|
||||
* **Tu cliente valida lo que recibe.** `list_tools()` y `call_tool()` comprueban la respuesta del servidor contra la versión del protocolo negociada, así que un servidor no del todo válido que el análisis permisivo de la v1 toleraba ahora lanza `pydantic.ValidationError`. Si te conectas a servidores que no controlas, cuenta con ser tú quien los descubra; la **[Guía de migración](migration.md#client-validates-inbound-traffic-against-the-protocol-schema)** tiene los detalles.
|
||||
* **Las plantillas de URI son ahora RFC 6570 de verdad.** `{+path}`, `{?query}` y compañía funcionan, la coincidencia es exacta en lugar de laxa por regex, y el path traversal en los valores extraídos se rechaza por defecto. Las plantillas más estrictas fallan al decorar, no en la primera solicitud. **[Plantillas de URI](servers/uri-templates.md)**.
|
||||
|
|
|
|||
Loading…
Reference in a new issue