docs: refresh translations, and translate pages in parallel (#3458)
This commit is contained in:
parent
08a3bc8eaf
commit
fd66270926
23 changed files with 4821 additions and 2277 deletions
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [0355618e5f4d5fe4, 1821eaf50f2d0b64, 82e0b28ebd3abf5a, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487]
|
||||
sections: [0355618e5f4d5fe4, 0fefa31fb7c7b585, 5c53e7487c9c70cc, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487]
|
||||
tool: 1
|
||||
---
|
||||
# MCP Apps {#mcp-apps}
|
||||
|
|
@ -25,7 +25,7 @@ y luego vuelve.
|
|||
|
||||
## Un reloj con cara visible {#a-clock-with-a-face}
|
||||
|
||||
```python title="server.py" hl_lines="19 22 30 32"
|
||||
```python title="server.py" hl_lines="17 20 28 30"
|
||||
--8<-- "docs_src/apps/tutorial001.py"
|
||||
```
|
||||
|
||||
|
|
@ -50,21 +50,42 @@ dentro de tu HTML. Te da `ontoolresult`, `callServerTool`,
|
|||
|
||||
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.
|
||||
> Las herramientas **DEBEN** devolver un array `content` significativo incluso cuando hay una UI disponible.
|
||||
|
||||
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"
|
||||
```python title="server.py" hl_lines="21-25"
|
||||
--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.
|
||||
no cuenta. Esta es la mitad cliente de la negociación:
|
||||
|
||||
```python title="client.py" hl_lines="8 12"
|
||||
--8<-- "docs_src/apps/tutorial001_client.py"
|
||||
```
|
||||
|
||||
Sirve `server.py` por HTTP y luego ejecuta el cliente desde una segunda terminal:
|
||||
|
||||
```console
|
||||
uv run mcp run server.py --transport streamable-http
|
||||
```
|
||||
|
||||
```console
|
||||
python client.py
|
||||
```
|
||||
|
||||
```text
|
||||
2026-06-26T12:00:00Z
|
||||
```
|
||||
|
||||
Volvió la respuesta enriquecida. Quita `extensions=[APPS_SUPPORT]` de la llamada a `Client`
|
||||
y el mismo programa imprime `The time is 2026-06-26T12:00:00Z.` en su lugar, que es
|
||||
todo lo que un cliente solo de texto llega a ver.
|
||||
|
||||
!!! warning
|
||||
Nunca devuelvas un marcador de posición como `"[Rendered UI]"` como único contenido. Si el
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff]
|
||||
sections: [05891e7cc1938a13, b3c01a6af28c51ee, 6eba6ce094f4417e, 125f28588c85dd7b, ddd8969b698ca11c, ed6af2df4b656dff]
|
||||
tool: 1
|
||||
---
|
||||
# Extensiones {#extensions}
|
||||
|
|
@ -62,7 +62,7 @@ especificadas por el propio proyecto MCP.
|
|||
|
||||
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"
|
||||
```python title="server.py" hl_lines="16 18-19 21-22 25"
|
||||
--8<-- "docs_src/extensions/tutorial003.py"
|
||||
```
|
||||
|
||||
|
|
@ -74,18 +74,25 @@ La extensión útil más pequeña es una herramienta y un mapa de 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`:
|
||||
Sírvela por HTTP, y un cliente es la prueba:
|
||||
|
||||
```python title="server.py" hl_lines="29-34"
|
||||
--8<-- "docs_src/extensions/tutorial003.py"
|
||||
```console
|
||||
uv run mcp run server.py --transport streamable-http
|
||||
```
|
||||
|
||||
```python title="client.py" hl_lines="7-11"
|
||||
--8<-- "docs_src/extensions/tutorial003_client.py"
|
||||
```
|
||||
|
||||
Cada `server.py` de esta página se sirve con ese comando, y cada `client.py`
|
||||
se ejecuta a su lado con `python client.py` desde una segunda terminal.
|
||||
|
||||
### 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"
|
||||
```python title="server.py" hl_lines="14-20 24 33-41"
|
||||
--8<-- "docs_src/extensions/tutorial004.py"
|
||||
```
|
||||
|
||||
|
|
@ -112,10 +119,10 @@ tiempo de ejecución:
|
|||
|
||||
### El lado del cliente {#the-client-side}
|
||||
|
||||
El `main()` del mismo archivo es toda la historia del cliente, sus dos mitades:
|
||||
El cliente es su propio programa, y lleva las dos mitades de la historia del cliente:
|
||||
|
||||
```python title="server.py" hl_lines="54-58"
|
||||
--8<-- "docs_src/extensions/tutorial004.py"
|
||||
```python title="client.py" hl_lines="21-23 27-30"
|
||||
--8<-- "docs_src/extensions/tutorial004_client.py"
|
||||
```
|
||||
|
||||
* `Client(..., extensions=[advertise(EXTENSION_ID)])` declara la extensión. Las
|
||||
|
|
@ -127,6 +134,9 @@ El `main()` del mismo archivo es toda la historia del cliente, sus dos mitades:
|
|||
* 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.
|
||||
* `SearchRequest` y los dos modelos que lleva son el contrato de la extensión en el canal,
|
||||
así que el cliente los declara por su cuenta. Una extensión publicada los distribuiría en
|
||||
un paquete que ambos lados importan.
|
||||
|
||||
### Interceptar `tools/call` {#intercepting-toolscall}
|
||||
|
||||
|
|
@ -159,13 +169,20 @@ El hook envuelve `tools/call` y nada más. Para lo que afecta a cada mensaje, us
|
|||
## 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:
|
||||
comportamiento del lado del cliente detrás de un único identificador. Aquí el servidor responde a `buy`
|
||||
con un recibo para canjear en lugar de la mercancía, y solo para un cliente que declaró la
|
||||
extensión:
|
||||
|
||||
```python title="client.py" hl_lines="66-68"
|
||||
```python title="server.py" hl_lines="22-25"
|
||||
--8<-- "docs_src/extensions/tutorial006.py"
|
||||
```
|
||||
|
||||
En el cliente, pasa las instancias a `Client(extensions=[...])` y llama a las herramientas con normalidad:
|
||||
|
||||
```python title="client.py" hl_lines="33-35"
|
||||
--8<-- "docs_src/extensions/tutorial006_client.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
|
||||
|
|
@ -184,7 +201,7 @@ según la capacidad, el cliente no hace nada, como en el cliente de búsqueda de
|
|||
```python
|
||||
from mcp.client import advertise
|
||||
|
||||
client = Client(mcp, extensions=[advertise("com.example/search")])
|
||||
client = Client("http://localhost:8000/mcp", extensions=[advertise("com.example/search")])
|
||||
```
|
||||
|
||||
## Escribir una extensión de cliente {#writing-a-client-extension}
|
||||
|
|
@ -192,8 +209,8 @@ client = Client(mcp, extensions=[advertise("com.example/search")])
|
|||
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"
|
||||
```python title="client.py" hl_lines="16-17 25-26 28-29"
|
||||
--8<-- "docs_src/extensions/tutorial006_client.py"
|
||||
```
|
||||
|
||||
* El identificador sigue la misma gramática que el del servidor y se valida cuando se
|
||||
|
|
@ -231,14 +248,21 @@ forma reclamada que llega a un llamador del nivel de sesión lanza `UnexpectedCl
|
|||
|
||||
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`:
|
||||
como en [Servir tus propios métodos](#serving-your-own-methods). Toma un servidor cuya
|
||||
extensión sirve un único verbo sobre un trabajo con nombre:
|
||||
|
||||
```python title="client.py" hl_lines="22-25 46-47"
|
||||
```python title="server.py" hl_lines="12-13 30"
|
||||
--8<-- "docs_src/extensions/tutorial007.py"
|
||||
```
|
||||
|
||||
Un añadido en el cliente: 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="20-23 28-29"
|
||||
--8<-- "docs_src/extensions/tutorial007_client.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.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [2c79b6338e09b7ac, 7edc43b3fae11314, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, cd0e9c933350390e]
|
||||
sections: [2c79b6338e09b7ac, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, 0fde3bcea081ba3a]
|
||||
tool: 1
|
||||
---
|
||||
# El Server de bajo nivel {#the-low-level-server}
|
||||
|
|
@ -36,18 +36,22 @@ Cambiaron tres cosas, y son toda la API de bajo nivel:
|
|||
|
||||
### 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`:
|
||||
`mcp dev` y `mcp run` solo aceptan un `MCPServer`, así que este lo sirves por tu cuenta. La última línea de `server.py` construye una app ASGI común a partir de él, y uvicorn la ejecuta:
|
||||
|
||||
```python title="main.py"
|
||||
```console
|
||||
uvicorn server:app --port 8000
|
||||
```
|
||||
|
||||
Apunta el Inspector, o cualquier cliente, a `http://localhost:8000/mcp`:
|
||||
|
||||
```python title="client.py"
|
||||
import asyncio
|
||||
|
||||
from mcp import Client
|
||||
|
||||
from server import server
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(server) as client:
|
||||
async with Client("http://localhost:8000/mcp") as client:
|
||||
result = await client.call_tool("search_books", {"query": "dune", "limit": 5})
|
||||
print(result.content)
|
||||
|
||||
|
|
@ -64,6 +68,8 @@ El mismo texto que produjo la versión con `@mcp.tool()`. Dos diferencias honest
|
|||
* `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í.
|
||||
|
||||
En una prueba te ahorras uvicorn y el puerto: `Client(server)` acepta un `Server` de bajo nivel en el mismo proceso exactamente igual que acepta un `MCPServer`, y **[Pruebas](../get-started/testing.md)** es ese patrón.
|
||||
|
||||
## 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)**).
|
||||
|
|
@ -215,4 +221,4 @@ Cada uno de estos es una idea para la que ya tienes el vocabulario; cada uno tie
|
|||
* `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)**.
|
||||
El cliente 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)**.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9]
|
||||
sections: [a9aba7a026c7bd85, 83e2a08b9d46a398, 9fd8a0aa384b3257, 22a0129ee78b3c63, d875373c06d8d2f9]
|
||||
tool: 1
|
||||
---
|
||||
# Paginación {#pagination}
|
||||
|
|
@ -31,9 +31,13 @@ La paginación es para el servidor cuya lista de recursos es en realidad una bas
|
|||
|
||||
### Pruébalo {#try-it}
|
||||
|
||||
`Client(server)` se conecta a un `Server` de bajo nivel en memoria exactamente igual que se conecta a un `MCPServer`.
|
||||
`mcp run` solo acepta un `MCPServer`, así que este lo sirves tú mismo. La última línea de `server.py` construye una app ASGI normal a partir del `Server`, y uvicorn la ejecuta:
|
||||
|
||||
Llama a `list_resources()` sin argumentos. Obtienes diez recursos, de `book-1` a `book-10`, y `next_cursor` es la cadena `"10"`.
|
||||
```console
|
||||
uvicorn server:app --port 8000
|
||||
```
|
||||
|
||||
Apunta cualquier cliente (**[El cliente](../client/index.md)**, o el Inspector) a `http://localhost:8000/mcp` y 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"`.
|
||||
|
||||
|
|
@ -43,7 +47,7 @@ La décima página vuelve con `next_cursor` en `None`. Listo.
|
|||
|
||||
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"
|
||||
```python title="client.py" hl_lines="9-15"
|
||||
--8<-- "docs_src/pagination/tutorial002.py"
|
||||
```
|
||||
|
||||
|
|
@ -51,7 +55,7 @@ Cada método `list_*` de `Client` (`list_tools`, `list_resources`, `list_resourc
|
|||
* 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.
|
||||
Con uvicorn todavía sirviendo `server.py`, ejecuta `python client.py` en una segunda terminal. 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.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800]
|
||||
sections: [9e7b9a1710e5aeba, 66a2e9acc9101d54, d3d25aa802f5145a, 04db67a886b7271c, 857690fb8f876800]
|
||||
tool: 1
|
||||
---
|
||||
# Sugerencias de caché {#caching-hints}
|
||||
|
|
@ -31,7 +31,7 @@ Por defecto, cada resultado dice `ttlMs: 0, cacheScope: "private"`: caducado de
|
|||
|
||||
En el `Server` de bajo nivel, los handlers construyen sus resultados a mano, y `ttl_ms` / `cache_scope` son simplemente campos de los modelos de resultado. Un handler que los define explícitamente siempre gana al mapa del constructor, campo por campo:
|
||||
|
||||
```python title="server.py" hl_lines="10 16"
|
||||
```python title="server.py" hl_lines="11 17"
|
||||
--8<-- "docs_src/caching/tutorial002.py"
|
||||
```
|
||||
|
||||
|
|
@ -45,10 +45,24 @@ Una salvedad sobre las listas paginadas: el protocolo exige el **mismo `cacheSco
|
|||
|
||||
En una sesión 2026-07-28, `Client` respeta las sugerencias por ti: tiene una caché de respuestas integrada, activada por defecto. Un resultado que llega con un `ttlMs` se almacena, y una llamada idéntica dentro de ese TTL se sirve desde la caché sin ida y vuelta. Un resultado que llega *sin* sugerencia no se guarda en caché: los resultados sin sugerencia reciben `CacheConfig.default_ttl_ms`, que es `0` por defecto (caducado de inmediato), así que un servidor que no declara nada ve exactamente el mismo tráfico llamada por llamada de siempre.
|
||||
|
||||
```python title="client.py" hl_lines="33 35 38"
|
||||
Para verlo en acción, sirve el `server.py` de la sección anterior con uvicorn (su última línea construye la app ASGI). El handler imprime una línea cada vez que se ejecuta de verdad:
|
||||
|
||||
```console
|
||||
uvicorn server:app --port 8000
|
||||
```
|
||||
|
||||
```python title="client.py" hl_lines="20 23 28"
|
||||
--8<-- "docs_src/caching/tutorial003.py"
|
||||
```
|
||||
|
||||
Ejecuta `python client.py` desde una segunda terminal. Imprime las sugerencias que llevaba el primer resultado: el `ttlMs` del handler junto al `cacheScope` del mapa:
|
||||
|
||||
```text
|
||||
1000 public
|
||||
```
|
||||
|
||||
La terminal del servidor cuenta el resto: entre los logs de solicitudes de uvicorn, `tools/list served` aparece tres veces.
|
||||
|
||||
Cuatro llamadas, tres consultas al servidor. La segunda llamada encontró una entrada vigente y nunca llegó al servidor; adelantar el reloj (inyectado) más allá del TTL hizo que la tercera volviera a consultar; la cuarta dijo `cache_mode="refresh"`. Ese argumento nombrado existe en los cinco verbos con caché (`list_tools`, `list_prompts`, `list_resources`, `list_resource_templates`, `read_resource`):
|
||||
|
||||
* `"use"` (el valor por defecto) sirve una entrada vigente si la hay, y almacena lo consultado si no.
|
||||
|
|
@ -57,7 +71,7 @@ Cuatro llamadas, tres consultas al servidor. La segunda llamada encontró una en
|
|||
|
||||
Hay una regla por encima de `"use"`: **las llamadas que llevan `meta` siempre llegan al servidor.** Una solicitud con `meta` definido (un token de progreso, campos de trazado) espera una solicitud real por el canal, así que con `cache_mode="use"` se trata como `"refresh"`: se omite la lectura de la caché, y el resultado obtenido sigue reemplazando la entrada en caché. `"bypass"` y un `"refresh"` explícito se comportan como siempre.
|
||||
|
||||
Para desactivar la caché por completo, construye con `Client(server, cache=None)`: cada llamada vuelve a ser una ida y vuelta, y `cache_mode`, aunque se sigue aceptando, no hace nada.
|
||||
Para desactivar la caché por completo, pasa `cache=None` al construir el `Client`: cada llamada vuelve a ser una ida y vuelta, y `cache_mode`, aunque se sigue aceptando, no hace nada.
|
||||
|
||||
El alcance también se respeta automáticamente: las entradas `"private"` se asocian a la *partición* de la caché (más abajo), mientras que las `"public"` pueden optar por compartirse más ampliamente. Y **las notificaciones ganan al TTL** para las entradas exactas que nombran: una notificación `list_changed` desaloja el listado en caché correspondiente, y `resources/updated` desaloja la lectura en caché almacenada exactamente bajo su URI, por muy vigentes que estuvieran. En una conexión 2026-07-28 esas notificaciones llegan por un stream `subscriptions/listen` que abres con `client.listen(...)`, y el desalojo se completa antes de que tu observador vea el evento; **[Suscripciones](subscriptions.md)** es esa página.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [adf3c545b5be46b6, 916cd3ab1c03f461, e9be7a8d0eb0a456, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 90c6043be435fcb0]
|
||||
sections: [adf3c545b5be46b6, 916cd3ab1c03f461, 32ef568335dd95a7, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 0abc5ea5cb7ff6b3]
|
||||
tool: 1
|
||||
---
|
||||
# Callbacks del cliente {#client-callbacks}
|
||||
|
|
@ -63,7 +63,7 @@ Un `tools/call` tuyo, un `elicitation/create` de vuelta desde el servidor, respo
|
|||
`mode="legacy"` en la llamada a `Client(...)` hace trabajo real. Por defecto, `Client(...)` negocia la ruta
|
||||
moderna del protocolo, y esa ruta no tiene canal de retorno (back-channel) para las solicitudes del servidor al cliente: `ctx.elicit`
|
||||
falla antes de que tu callback llegue a ejecutarse. No lo decide el transporte; lo decide el protocolo
|
||||
negociado, tanto en memoria como a través de una URL. Fija `mode="legacy"` siempre que tu cliente tenga
|
||||
negociado. Fija `mode="legacy"` siempre que tu cliente tenga
|
||||
que responder a una; todas las pruebas detrás de esta página lo hacen. **[Versiones del protocolo](../protocol-versions.md)** tiene todos los detalles.
|
||||
|
||||
En una sesión 2026-07-28 el callback no está muerto, se alimenta de otra forma: cuando una herramienta devuelve un
|
||||
|
|
@ -151,4 +151,4 @@ Dos más. Ninguno declara nada.
|
|||
* `sampling_callback` y `list_roots_callback` funcionan igual pero atienden funcionalidades obsoletas; los servidores modernos usan solicitudes de varias idas y vueltas en su lugar.
|
||||
* `logging_callback` y `message_handler` reciben notificaciones. No declaran nada.
|
||||
|
||||
El primer argumento de `Client(...)` es un objeto de transporte. **[Transportes del cliente](transports.md)** cubre todos los tipos.
|
||||
El primer argumento de `Client(...)` elige el transporte. **[Transportes del cliente](transports.md)** cubre todos los tipos.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [ebef1e7a0df854f4, 8355cfaf1f76c9d5, 8e79141fc2985342, 46bdb07c7537e8a5, 80ce41579825a6fa, 5f0fa90494de8f65, 83d10514eaa62fa5, 9190555aa39a5d28, 84a4c9d8bf14dddb, 927d71cf40b58c30]
|
||||
sections: [ebef1e7a0df854f4, 7e16449f66e7dfd6, eeb0682f7d2a1079, 5713f0196a34e6e7, 0e844597859e4248, 3a97d9195ddcc92e, 1da08c483e59c141, 84702cc6e0a1fd42, 8dee7a31c86ffc5c, 83a5bce168ef23d7]
|
||||
tool: 1
|
||||
---
|
||||
# El cliente {#the-client}
|
||||
|
|
@ -11,13 +11,23 @@ Es un solo objeto con un solo ciclo de vida: lo construyes, entras en `async wit
|
|||
|
||||
## Tu primer cliente {#your-first-client}
|
||||
|
||||
```python title="client.py" hl_lines="14-18"
|
||||
Un cliente necesita un servidor con el que hablar. Este Bookshop es al que se conectan todos los fragmentos de esta página. Guárdalo como `server.py` y déjalo ejecutándose por HTTP:
|
||||
|
||||
```python title="server.py"
|
||||
--8<-- "docs_src/client/tutorial001.py"
|
||||
```
|
||||
|
||||
El servidor del principio solo está ahí para que tengas algo a lo que conectarte. El cliente son las cinco líneas resaltadas.
|
||||
```console
|
||||
uv run mcp run server.py --transport streamable-http
|
||||
```
|
||||
|
||||
* `Client(mcp)` recibe **el propio objeto servidor**. Ese es el transporte en memoria: sin subproceso, sin puerto, sin HTTP. Así se conectan todos los ejemplos de esta página y todas las pruebas que escribas.
|
||||
Con eso queda disponible en `http://localhost:8000/mcp`. El cliente es un programa aparte. Guárdalo como `client.py` y ejecuta `python client.py` en una segunda terminal:
|
||||
|
||||
```python title="client.py" hl_lines="7-11"
|
||||
--8<-- "docs_src/client/tutorial001_client.py"
|
||||
```
|
||||
|
||||
* `Client("http://localhost:8000/mcp")` recibe una **URL**, así que se conecta por Streamable HTTP al servidor que acabas de iniciar.
|
||||
* `async with` es el **ciclo de vida**. Al entrar se conecta y negocia; al salir se desconecta. No hay un par `connect()` / `close()`, y un `Client` no se puede reutilizar una vez que termina el bloque.
|
||||
* Dentro del bloque, los datos de la conexión ya están ahí como propiedades simples.
|
||||
|
||||
|
|
@ -25,10 +35,10 @@ El servidor del principio solo está ahí para que tengas algo a lo que conectar
|
|||
|
||||
`Client` recibe un solo argumento posicional y resuelve el transporte a partir de su tipo:
|
||||
|
||||
* Una instancia de `MCPServer` (o del `Server` de bajo nivel): se conecta **en el mismo proceso**.
|
||||
* Una cadena con una URL (`Client("http://localhost:8000/mcp")`): Streamable HTTP, el camino de producción.
|
||||
* Un `StdioServerParameters`: el comando que se lanza como **subproceso**, con el que se habla a través de su stdin y su stdout.
|
||||
* Una cadena con una URL (`Client("http://localhost:8000/mcp")`): Streamable HTTP, el transporte con el que despliegas.
|
||||
* Un `StdioServerParameters`: el comando que se lanza como **subproceso** local, 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.
|
||||
* Una instancia de `MCPServer` (o del `Server` de bajo nivel): se conecta **en el mismo proceso**, sin subproceso y sin puerto. Ese caso es para las pruebas, y **[Pruebas](../get-started/testing.md)** se construye sobre él.
|
||||
|
||||
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)**.
|
||||
|
||||
|
|
@ -49,11 +59,11 @@ Nunca elegiste una versión del protocolo. Por defecto, el `Client` sondea el se
|
|||
|
||||
## Listar herramientas {#listing-tools}
|
||||
|
||||
```python title="client.py" hl_lines="15-20"
|
||||
```python title="client.py" hl_lines="8-13"
|
||||
--8<-- "docs_src/client/tutorial002.py"
|
||||
```
|
||||
|
||||
`list_tools()` devuelve un `ListToolsResult`; las herramientas están en `.tools`. Cada una es la definición completa que un host le entregaría a un modelo:
|
||||
`list_tools()` devuelve un `ListToolsResult`; las herramientas están en `.tools`. Cada una es la definición completa que un host le entregaría a un modelo. Esta es la primera:
|
||||
|
||||
```python
|
||||
tool.name # 'search_books'
|
||||
|
|
@ -77,6 +87,8 @@ y `tool.input_schema` es el JSON Schema que el servidor derivó de las anotacion
|
|||
|
||||
Ese esquema es todo lo que una UI necesita para renderizar un formulario de argumentos, y todo lo que un modelo necesita para producir argumentos válidos.
|
||||
|
||||
La segunda herramienta, `lookup_book`, se registró sin `title=`, así que su `tool.title` es `None`.
|
||||
|
||||
!!! tip
|
||||
`title` es opcional, así que una UI que muestra herramientas a una persona tiene que elegir: el `title` si lo hay,
|
||||
el `name` si no. `from mcp.shared.metadata_utils import get_display_name` hace exactamente eso,
|
||||
|
|
@ -86,7 +98,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="27-34"
|
||||
```python title="client.py" hl_lines="9-16"
|
||||
--8<-- "docs_src/client/tutorial003.py"
|
||||
```
|
||||
|
||||
|
|
@ -142,7 +154,7 @@ Una herramienta que lanza una excepción **no** la lanza en tu cliente. Vuelve c
|
|||
|
||||
Los verbos de recursos vienen en pares: dos formas de listar, una de leer.
|
||||
|
||||
```python title="client.py" hl_lines="22-31"
|
||||
```python title="client.py" hl_lines="9-18"
|
||||
--8<-- "docs_src/client/tutorial004.py"
|
||||
```
|
||||
|
||||
|
|
@ -156,7 +168,7 @@ A un cliente también se le puede avisar cuando cambia un recurso. En conexiones
|
|||
|
||||
## Prompts {#prompts}
|
||||
|
||||
```python title="client.py" hl_lines="15-20"
|
||||
```python title="client.py" hl_lines="8-13"
|
||||
--8<-- "docs_src/client/tutorial005.py"
|
||||
```
|
||||
|
||||
|
|
@ -181,7 +193,7 @@ Un host le entrega esos mensajes directamente al modelo. Esa es toda la funciona
|
|||
|
||||
Un servidor con un handler de autocompletado puede autocompletar argumentos de prompts y de plantillas de recursos mientras el usuario escribe.
|
||||
|
||||
```python title="client.py" hl_lines="27-31"
|
||||
```python title="client.py" hl_lines="9-13"
|
||||
--8<-- "docs_src/client/tutorial006.py"
|
||||
```
|
||||
|
||||
|
|
@ -194,21 +206,21 @@ La respuesta está en `result.completion.values`. Escribe `"p"` y el servidor de
|
|||
|
||||
Cada método `list_*` acepta un argumento nombrado `cursor=` y cada resultado trae un `next_cursor`. Cuando `next_cursor` es `None`, ya lo tienes todo.
|
||||
|
||||
```python title="client.py" hl_lines="22-30"
|
||||
```python title="client.py" hl_lines="7-15"
|
||||
--8<-- "docs_src/client/tutorial007.py"
|
||||
```
|
||||
|
||||
Este bucle es correcto contra cualquier servidor. `MCPServer` devuelve todo en una sola página, así que `next_cursor` es `None` y el bucle se ejecuta una vez; por eso la mayoría del código nunca lo escribe. Los servidores que realmente paginan, y las reglas que siguen los cursores, están en **[Paginación](../advanced/pagination.md)**.
|
||||
`list_all_tools` es correcta contra cualquier servidor. `MCPServer` devuelve todo en una sola página, así que `next_cursor` es `None` y el bucle se ejecuta una vez; por eso la mayoría del código nunca lo escribe. Los servidores que realmente paginan, y las reglas que siguen los cursores, están en **[Paginación](../advanced/pagination.md)**.
|
||||
|
||||
## En las pruebas {#in-tests}
|
||||
|
||||
`Client(mcp)`, sin proceso y sin puerto, ya es un banco de pruebas para tu servidor.
|
||||
Cada `client.py` de esta página llegó a `server.py` por HTTP. En una prueba te saltas la red y le pasas a `Client` el propio objeto servidor: `from server import mcp` y luego `Client(mcp)`. Sin proceso, sin puerto, y todos los métodos anteriores funcionan igual.
|
||||
|
||||
Hay una opción del constructor pensada para eso: `Client(mcp, raise_exceptions=True)`. Solo tiene efecto en conexiones en memoria, y **[Pruebas](../get-started/testing.md)** es la página que la explica y construye todo el patrón a su alrededor.
|
||||
Hay una opción del constructor pensada para eso: `Client(mcp, raise_exceptions=True)`. Solo tiene efecto en conexiones en el mismo proceso, y **[Pruebas](../get-started/testing.md)** es la página que la explica y construye todo el patrón a su alrededor.
|
||||
|
||||
## Resumen {#recap}
|
||||
|
||||
* `Client(x)` se conecta en memoria a un objeto servidor, por Streamable HTTP a una cadena con una URL, y por cualquier otra cosa mediante un transporte.
|
||||
* `Client(x)` se conecta por Streamable HTTP a una cadena con una URL, lanza un subproceso para un `StdioServerParameters`, entra directamente en un transporte y, en las pruebas, recibe el propio objeto servidor.
|
||||
* `async with` es todo el ciclo de vida. Dentro, `server_capabilities` y `protocol_version` ya están rellenas; `server_info` e `instructions` también, cuando el servidor las proporciona.
|
||||
* `list_tools()` te da el `name`, `title`, `description` e `input_schema` de cada herramienta.
|
||||
* `call_tool()` devuelve `content` para el modelo, `structured_content` para tu código, e `is_error`. Una herramienta que lanza una excepción es un resultado, no una excepción.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [c6899d3892bd9fa0, 79372cff3cc48a88, 63878d29e87c3e73, 13175843d3588af4, e7e2b9fd516f77de, 758f06399b513c1f, a05d7278487d610b]
|
||||
sections: [c6899d3892bd9fa0, 79372cff3cc48a88, c2dae1ebe2ebd543, 13175843d3588af4, df06056fb16b3846, 758f06399b513c1f, a05d7278487d610b]
|
||||
tool: 1
|
||||
---
|
||||
# Clientes OAuth {#oauth-clients}
|
||||
|
|
@ -81,18 +81,20 @@ Mira `main()`. El proveedor va en el **cliente httpx2**, el cliente httpx2 va en
|
|||
|
||||
La primera vez que `Client` envía una solicitud, el servidor responde `401`. El proveedor toma el control:
|
||||
|
||||
1. **Descubrimiento.** Lee la cabecera `WWW-Authenticate`, obtiene los Protected Resource Metadata del servidor desde `/.well-known/oauth-protected-resource`, averigua qué servidor de autorización protege este recurso y obtiene los metadatos de *ese* servidor.
|
||||
1. **Descubrimiento.** Lee la cabecera `WWW-Authenticate`, obtiene los Protected Resource Metadata del servidor desde `/.well-known/oauth-protected-resource`, averigua qué servidor de autorización protege este recurso y obtiene los metadatos de *ese* servidor. (A un servidor más antiguo que no publica metadatos del recurso se le piden, en su lugar, los metadatos del servidor de autorización en su propio origen.) En cualquier caso, los metadatos deben nombrar como `issuer` al servidor para el que se obtuvieron; cualquier otra cosa se rechaza.
|
||||
2. **Registro.** ¿No hay nada en el almacenamiento? Te registra dinámicamente con tu `OAuthClientMetadata` y guarda el resultado.
|
||||
3. **Autorización.** Genera el par PKCE y un `state`, construye la URL de autorización, espera tu `redirect_handler` y luego espera tu `callback_handler` para obtener el código.
|
||||
4. **Intercambio.** Cambia el código por un `OAuthToken`, lo guarda y repite tu solicitud original con `Authorization: Bearer ...`.
|
||||
|
||||
Después de eso, se queda callado. Los tokens salen del almacenamiento, un token de acceso caducado se renueva con el token de actualización y solo cuando nada de eso funciona vuelve a ejecutar el flujo.
|
||||
|
||||
Una regla de transporte se aplica a todas estas solicitudes: igual que la solicitud MCP dentro de la que se ejecutan, siguen una redirección solo cuando permanece en el mismo origen y conserva el método (un 307/308 por una barra final, por ejemplo), y tratan cualquier otra redirección como si esa URL no respondiera.
|
||||
|
||||
No escribiste nada de eso. Quedan dos argumentos nombrados (`client_metadata_url` y `validate_resource_url`), y este archivo no necesita ninguno. `client_metadata_url` es el que vale la pena conocer; tiene su propia sección más abajo.
|
||||
|
||||
### Pruébalo {#try-it}
|
||||
|
||||
La mayoría de los ejemplos de esta documentación puedes comprobarlos con un `Client(server)` en memoria. Este no: todo el sentido del flujo es un `401` HTTP, y no hay HTTP entre un cliente en memoria y su servidor.
|
||||
El `Client(server)` en memoria que usan tus pruebas no sirve aquí: todo el sentido del flujo es un `401` HTTP, y no hay HTTP entre un cliente en memoria y su servidor.
|
||||
|
||||
El repositorio incluye la versión real. `examples/servers/simple-auth/` ejecuta un servidor de autorización independiente y un servidor MCP protegido; `examples/clients/simple-auth-client/` es el cliente de esta página convertido en una pequeña CLI. Su README tiene los dos comandos: inicia los servidores, ejecuta el cliente contra ellos y verás pasar los cuatro pasos.
|
||||
|
||||
|
|
@ -110,13 +112,14 @@ Un trabajo nocturno, un paso de CI, otro servicio. No hay navegador ni nadie que
|
|||
|
||||
`ClientCredentialsOAuthProvider` es el mismo `httpx2.Auth`, sin el humano:
|
||||
|
||||
```python title="client.py" hl_lines="4 27-33"
|
||||
```python title="client.py" hl_lines="4 27-34"
|
||||
--8<-- "docs_src/oauth_clients/tutorial002.py"
|
||||
```
|
||||
|
||||
Qué cambió:
|
||||
|
||||
* Sin `OAuthClientMetadata`, sin handlers. Pasas `client_id` y `client_secret`; el proveedor construye un registro `client_credentials` mínimo en torno a ellos y se salta el registro dinámico por completo.
|
||||
* `issuer` nombra el servidor de autorización que emitió esas credenciales; usa el valor `issuer` que devuelve su documento `/.well-known/oauth-authorization-server`. El descubrimiento sigue ejecutándose como arriba, pero las solicitudes de token solo se construyen a partir de los metadatos de *ese* emisor; si el servidor MCP apunta a cualquier otro sitio, el flujo se detiene con un `OAuthFlowError`. Omitirlo está obsoleto y pasa a ser obligatorio en 3.0 (consulta **[Funcionalidades obsoletas](../deprecated.md#deprecated-sdk-helpers)**); hasta entonces, el proveedor emite un aviso y usa el servidor de autorización que encuentre el descubrimiento.
|
||||
* `scope` es una cadena separada por espacios, el formato que OAuth usa en lo que se transmite.
|
||||
* Todo lo que viene después es idéntico: el mismo `TokenStorage`, el mismo `httpx2.AsyncClient(auth=...)`, el mismo `streamable_http_client`.
|
||||
|
||||
|
|
@ -129,7 +132,7 @@ Por defecto, el secreto viaja como autenticación HTTP Basic en la solicitud de
|
|||
Hay un proveedor más en `mcp.client.auth.extensions.client_credentials`:
|
||||
**`PrivateKeyJWTOAuthProvider`**, para clientes que se autentican con un JWT en lugar de un
|
||||
secreto compartido (`private_key_jwt`, la variante de par de claves e identidad de carga de trabajo). Sigue
|
||||
el mismo patrón: construye uno y ponlo en `auth=`. El mismo módulo incluye
|
||||
el mismo patrón: construye uno (acepta el mismo `issuer` opcional) y ponlo en `auth=`. El mismo módulo incluye
|
||||
`SignedJWTParameters` y `static_assertion_provider`, dos utilidades que construyen su aserción.
|
||||
|
||||
Hay una situación más sin humanos: el cliente pertenece a una empresa cuyo proveedor de identidad, y no el usuario, decide a qué servidores MCP puede acceder. Ese es un grant distinto, con su propio modelo de confianza y su propia página, **[Aserción de identidad](identity-assertion.md)**.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, d18adbdbb835ea73]
|
||||
sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, ed4d17e894864056]
|
||||
tool: 1
|
||||
---
|
||||
# Grupos de sesiones {#session-groups}
|
||||
|
|
@ -78,7 +78,7 @@ Si ya tienes una `ClientSession` conectada (`Client.session` lo es), pásala a `
|
|||
## Resumen {#recap}
|
||||
|
||||
* `ClientSessionGroup` mantiene muchas conexiones a servidores y reúne sus herramientas, recursos y prompts en un `dict` para cada tipo.
|
||||
* `connect_to_server(params)` por servidor. Recibe parámetros de transporte, nunca el objeto servidor ni la URL que recibe un `Client`.
|
||||
* `connect_to_server(params)` por servidor. Recibe parámetros de transporte, nunca la URL ni el `Transport` que recibe un `Client`.
|
||||
* `group.call_tool(name, arguments)` enruta por ti al servidor al que pertenece.
|
||||
* Los nombres deben ser únicos en todo el grupo; dos servidores con una herramienta `search` no pueden coexistir por sí solos.
|
||||
* `component_name_hook=` reescribe cada nombre registrado. La clave del dict cambia; el nombre que se transmite por el canal, no.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, 40b4916d82eaf1d4, 3d0832f39b0d7059, dfa4446556badef0, 5bd93be2ab2ecb9c]
|
||||
sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302]
|
||||
tool: 1
|
||||
---
|
||||
# Transportes del cliente {#client-transports}
|
||||
|
|
@ -11,30 +11,15 @@ Nunca configuras uno por separado. `Client` recibe un único argumento posiciona
|
|||
|
||||
El lado del *servidor* de cada uno (lo que hace `mcp.run()` y lo que despliegas) está en **[Ejecutar el servidor](../run/index.md)**.
|
||||
|
||||
## En memoria {#in-memory}
|
||||
|
||||
Pasa el propio objeto del servidor:
|
||||
|
||||
```python title="client.py" hl_lines="14"
|
||||
--8<-- "docs_src/client_transports/tutorial001.py"
|
||||
```
|
||||
|
||||
Sin subproceso, sin puerto, sin bytes por ningún canal. El cliente y el servidor son dos objetos en el mismo proceso, y aun así la llamada pasa por la capa real del protocolo: `search_books` se lista, se valida y se invoca exactamente igual que por HTTP.
|
||||
|
||||
Eso lo convierte en dos cosas a la vez:
|
||||
|
||||
* **Un arnés de pruebas.** Todos los ejemplos de esta documentación se ejercitan así, y la página **[Pruebas](../get-started/testing.md)** construye todo el patrón en torno a ello.
|
||||
* **Una API de integración.** Una aplicación que construye el servidor no necesita un salto de red para llamar a sus herramientas.
|
||||
|
||||
## Streamable HTTP {#streamable-http}
|
||||
|
||||
Pasa una cadena con una URL y obtienes **Streamable HTTP**, el transporte con el que despliegas:
|
||||
Pasa una cadena con una URL y obtienes **Streamable HTTP**, el transporte detrás del que despliegas y el primero al que deberías recurrir:
|
||||
|
||||
```python title="client.py" hl_lines="5"
|
||||
--8<-- "docs_src/client_transports/tutorial002.py"
|
||||
```
|
||||
|
||||
Ese es todo el cliente de producción. `Client` envuelve la URL en `streamable_http_client(...)` por ti, encima de un `httpx2.AsyncClient` configurado como MCP necesita: `follow_redirects=True`, un timeout de 30 segundos para connect/write/pool y un timeout de lectura de 300 segundos, porque el servidor puede mantener abierto un flujo de respuesta.
|
||||
Ese es todo el cliente de producción. `Client` envuelve la URL en `streamable_http_client(...)` por ti, encima de un `httpx2.AsyncClient` configurado como MCP necesita: un timeout de 30 segundos para connect/write/pool y un timeout de lectura de 300 segundos, porque el servidor puede mantener abierto un flujo de respuesta.
|
||||
|
||||
!!! check
|
||||
Un `Client` que has construido **no** está conectado. La construcción solo elige el transporte;
|
||||
|
|
@ -50,7 +35,7 @@ Ese es todo el cliente de producción. `Client` envuelve la URL en `streamable_h
|
|||
|
||||
En cuanto necesites un encabezado `Authorization`, una cookie, un proxy, mTLS o un timeout distinto, construye tú mismo el `httpx2.AsyncClient` y entrégaselo a `streamable_http_client`:
|
||||
|
||||
```python title="client.py" hl_lines="8-14"
|
||||
```python title="client.py" hl_lines="8-13"
|
||||
--8<-- "docs_src/client_transports/tutorial003.py"
|
||||
```
|
||||
|
||||
|
|
@ -80,9 +65,30 @@ estándar `SSL_CERT_FILE`/`SSL_CERT_DIR` o pasa un `verify=ssl_context` explíci
|
|||
!!! info
|
||||
`httpx2` conserva la API conocida de `httpx`, así que si conoces `httpx` ya sabes cómo hacer la autenticación,
|
||||
los proxies, los event hooks, los reintentos y los límites de conexión aquí. El SDK no añade nada encima ni quita
|
||||
nada. También es donde se conecta OAuth:
|
||||
nada, salvo el [manejo de redirecciones](#redirects). También es donde se conecta OAuth:
|
||||
`httpx2.AsyncClient(auth=OAuthClientProvider(...))`. Todo ese flujo está en **[Clientes OAuth](oauth-clients.md)**.
|
||||
|
||||
### Redirecciones {#redirects}
|
||||
|
||||
El transporte se conecta a la URL que le diste, y solo a ese origen.
|
||||
|
||||
* Una redirección `307`/`308` que se queda en el mismo esquema, host y puerto se sigue, y también `http://` → `https://` en el mismo host. Eso cubre la redirección habitual de barra final `/mcp` → `/mcp/`.
|
||||
* Una redirección a cualquier otro sitio **no** se sigue. La llamada falla con:
|
||||
|
||||
```text
|
||||
MCPError: Redirect to https://other.example.com/mcp not followed; use that URL as the endpoint if it is the intended server
|
||||
```
|
||||
|
||||
Si esa URL es el servidor que querías, ponla en tu configuración. Si no, el servidor o un proxy delante de él está mal configurado.
|
||||
|
||||
Esto vale para cualquier `httpx2.AsyncClient` que pases: su ajuste `follow_redirects` no se consulta para las solicitudes MCP, en ningún sentido. Los proveedores OAuth del SDK aplican la misma regla a sus propias solicitudes.
|
||||
|
||||
!!! tip
|
||||
`Redirect to http://… not followed: it would downgrade this HTTPS endpoint to plain HTTP` significa que el
|
||||
servidor está detrás de un proxy que termina TLS del que no sabe nada y está emitiendo redirecciones `http://`.
|
||||
Eso se arregla en el servidor (**[Desplegar y escalar](../run/deploy.md#behind-a-tls-terminating-proxy)**),
|
||||
o usando la URL `https://…/` exacta que sugiere el mensaje.
|
||||
|
||||
## stdio {#stdio}
|
||||
|
||||
Un servidor **stdio** es un subproceso. El cliente lo lanza, escribe JSON-RPC en su stdin y lee JSON-RPC de su stdout. Así es como un host de escritorio ejecuta un servidor en tu máquina: un host *es* este código más una interfaz de usuario, y **[Conectar a un host real](../get-started/real-host.md)** es la misma relación vista desde el lado del host, como archivo de configuración.
|
||||
|
|
@ -105,6 +111,18 @@ El stderr del proceso hijo va al tuyo. Para enviarlo a otro sitio, construye tú
|
|||
Un servidor que necesita una clave de API no la encontrará ahí. Pásala explícitamente con `env=`; esas
|
||||
variables se fusionan encima de la lista de permitidos. Eso es lo que hace `BOOKSHOP_API_KEY` arriba.
|
||||
|
||||
## En memoria {#in-memory}
|
||||
|
||||
En una prueba no hay nada que desplegar ni nada que lanzar. Pasa el propio objeto del servidor:
|
||||
|
||||
```python hl_lines="14"
|
||||
--8<-- "docs_src/client_transports/tutorial001.py"
|
||||
```
|
||||
|
||||
Sin subproceso, sin puerto, sin bytes por ningún canal. El cliente y el servidor son dos objetos en el mismo proceso, y aun así la llamada pasa por la capa real del protocolo: `search_books` se lista, se valida y se invoca exactamente igual que por HTTP. **[Pruebas](../get-started/testing.md)** construye todo el patrón en torno a ello.
|
||||
|
||||
La misma forma sirve además como API de integración: una aplicación que construye ella misma el servidor puede llamar a sus herramientas sin un salto de red.
|
||||
|
||||
## SSE {#sse}
|
||||
|
||||
`sse_client(url)`, de `mcp.client.sse`, es el transporte HTTP al que reemplazó Streamable HTTP. Envuélvelo igual, `Client(sse_client("http://localhost:8000/sse"))`, para hablar con un servidor que todavía lo usa, y no construyas nada nuevo sobre él.
|
||||
|
|
@ -113,15 +131,16 @@ El stderr del proceso hijo va al tuyo. Para enviarlo a otro sitio, construye tú
|
|||
|
||||
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)`, 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.
|
||||
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 `str` se convierte en `streamable_http_client(url)`, un `StdioServerParameters` se convierte en `stdio_client(params)`, un objeto de servidor se conecta dentro del proceso 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=`.
|
||||
* Las redirecciones se siguen solo dentro del propio origen de la URL (un `307`/`308` de barra final), más `http`→`https` en el mismo host. Cualquier otra cosa falla con `Redirect to … not followed`; configura la URL final.
|
||||
* 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.
|
||||
* `Client(mcp)` (el objeto del servidor) se conecta en memoria. Úsalo en pruebas, o para integrar un servidor en la aplicación que lo construyó.
|
||||
* 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.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
---
|
||||
translation:
|
||||
sections: [490237e61c3a7a44, 01262a123ad9501d, 429db5b574a2ac08, e2d0d273fbd2d74b, 64ab0331e868f3d4, 6c8878ce2d1f6d56, 4068f23e371bf0b3, eaef75b8725bc931]
|
||||
sections: [4c1dea378b2b1bf7, 01262a123ad9501d, 429db5b574a2ac08, e2d0d273fbd2d74b, 64ab0331e868f3d4, 6c8878ce2d1f6d56, c3d1099701156881, e185a1b8e53669f6]
|
||||
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**. Una función auxiliar del SDK queda obsoleta por su cuenta y aparece [al final](#deprecated-sdk-helpers).
|
||||
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**. Algunas partes del SDK quedan obsoletas por su cuenta y aparecen [al final](#deprecated-sdk-helpers).
|
||||
|
||||
La tabla siguiente nombra cada funcionalidad obsoleta, explica por qué desaparece e indica el reemplazo sobre el que construir.
|
||||
|
||||
|
|
@ -137,11 +137,13 @@ Esa es toda la API. No hay un interruptor por método, y tampoco lo quieres: la
|
|||
|
||||
## 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.
|
||||
No son cambios de la especificación, solo usos del SDK que tienen un reemplazo mejor. Avisan con el mismo `MCPDeprecationWarning`, y 3.0 elimina la forma antigua.
|
||||
|
||||
| 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). |
|
||||
| `AuthSettings(resource_server_url=...)` sin `validate_token_resource=` | Defínelo: `True` hace que el servidor rechace los tokens bearer que tu verificador no declara como emitidos para `resource_server_url`; `False` indica que tu verificador comprueba por sí mismo la audiencia del token (consulta **[Autorización](run/authorization.md#a-token-verifier)**). Sin definir se comporta como `False`; 3.0 hace que `True` sea el valor por defecto siempre que `resource_server_url` esté definido. |
|
||||
| `ClientCredentialsOAuthProvider(...)` o `PrivateKeyJWTOAuthProvider(...)` sin `issuer=` | Pasa `issuer=` con el servidor de autorización que emitió las credenciales (consulta **[Escribir clientes OAuth](client/oauth-clients.md#machine-to-machine)**). Sin él, el servidor MCP decide qué servidor de autorización las recibe; 3.0 hace obligatorio el argumento nombrado. |
|
||||
|
||||
## Resumen {#recap}
|
||||
|
||||
|
|
@ -150,7 +152,7 @@ No son cambios de la especificación, solo detalles internos del SDK con un reem
|
|||
* 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 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.
|
||||
* Las [partes del SDK obsoletas](#deprecated-sdk-helpers) siguen la misma regla: avisan ahora, y 3.0 elimina la forma antigua.
|
||||
* 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: [0d6c05bcbf836bf3, 59a7b14eeefc68c1, 7114d8d6daba203f, e8bbb56a98ba7bc9, 5138010f6159901c, f78da7c7c363d4c6, 220a939cab348686]
|
||||
sections: [0d6c05bcbf836bf3, 9a78b5f6b44b18ab, 7114d8d6daba203f, e8bbb56a98ba7bc9, bfd2fd1153e71dac, 1615a994ef071fdd, 65c599fae991f245]
|
||||
tool: 1
|
||||
---
|
||||
# Primeros pasos {#first-steps}
|
||||
|
|
@ -17,7 +17,7 @@ Tres palabras que verás en cada página a partir de aquí:
|
|||
* Un **cliente** vive dentro del host y habla MCP. El host ejecuta un cliente por cada servidor al que está conectado.
|
||||
* Un **servidor** es lo que construyes con este SDK. Expone cosas a los clientes. Nunca habla directamente con el modelo.
|
||||
|
||||
Tú escribes el servidor. Los hosts son el producto de otra persona. El SDK también te da un `Client`. Lo usarás para probar tus servidores, y aparece más adelante en esta página.
|
||||
Tú escribes el servidor. Los hosts son el producto de otra persona. El SDK también te da un `Client`, la misma clase que usaría un host para llegar a un servidor por URL o lanzarlo como subproceso. Aparece más adelante en esta página, y es también la forma en que probarás tus servidores.
|
||||
|
||||
## Las tres primitivas {#the-three-primitives}
|
||||
|
||||
|
|
@ -83,22 +83,20 @@ Viste tres pestañas en el Inspector. ¿Cómo supo que había tres?
|
|||
|
||||
Cuando un cliente se conecta, el servidor declara sus **capacidades**: qué familias de solicitudes va a responder. El cliente usa esa declaración para decidir qué vale la pena pedir siquiera. Nunca la escribiste; `MCPServer` la declara por ti.
|
||||
|
||||
Míralo tú mismo. El `Client` del SDK acepta el objeto servidor directamente y se conecta a él **en memoria** (sin subproceso, sin puerto):
|
||||
Míralo tú mismo. Deja `server.py` ejecutándose sobre HTTP en una terminal:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
```console
|
||||
uv run mcp run server.py --transport streamable-http
|
||||
```
|
||||
|
||||
from mcp import Client
|
||||
y apunta un cliente hacia él desde otra:
|
||||
|
||||
from server import mcp
|
||||
```python title="client.py" hl_lines="7-8"
|
||||
--8<-- "docs_src/first_steps/tutorial001_client.py"
|
||||
```
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(mcp) as client:
|
||||
print(client.server_capabilities.model_dump(exclude_none=True))
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
```console
|
||||
python client.py
|
||||
```
|
||||
|
||||
```text
|
||||
|
|
@ -118,17 +116,18 @@ Ese diccionario son las **capacidades** declaradas de tu servidor. Es lo primero
|
|||
Fíjate en lo que no está. `completions` (autocompletado de argumentos para plantillas de recurso y prompts) necesita un handler que escribes tú, este servidor no tiene uno, así que la capacidad está ausente y un cliente bien hecho no la pedirá. Esa es la regla para todo lo opcional: registra la cosa y la capacidad aparece; **[Autocompletado](../servers/completions.md)** lo demuestra.
|
||||
|
||||
!!! info
|
||||
`Client(mcp)` es el mismo cliente en memoria con el que se prueba cada ejemplo de esta
|
||||
documentación, y es como probarás los tuyos. Tiene una página entera: **[Pruebas](testing.md)**.
|
||||
Ese `client.py` es un cliente MCP completo, y **[El cliente](../client/index.md)** es su página.
|
||||
En una prueba te saltas la terminal y el puerto y le pasas a `Client` el propio objeto servidor,
|
||||
`Client(mcp)`. Eso también tiene una página entera: **[Pruebas](testing.md)**.
|
||||
|
||||
## Lo que no escribiste {#what-you-did-not-write}
|
||||
|
||||
Repasa esta página. Escribiste tres pequeñas funciones de Python. **No** escribiste:
|
||||
|
||||
* Un JSON Schema. `a: int, b: int` *es* el esquema de `add`.
|
||||
* Un handler de solicitudes. `tools/list`, `resources/read`, `prompts/get`: todos servidos por ti.
|
||||
* Un handler de solicitudes. `tools/list`, `resources/read`, `prompts/get`: el SDK los atiende todos por ti.
|
||||
* Una declaración de capacidades. `MCPServer` la hizo por ti.
|
||||
* Una línea de protocolo. La negociación de versión, el encuadre JSON-RPC, el intercambio de capacidades: todo ocurrió dentro de `mcp dev` y `Client(mcp)`, y nunca lo viste.
|
||||
* Una línea de protocolo. La negociación de versión, el encuadre JSON-RPC, el intercambio de capacidades: todo ocurrió dentro de `mcp dev` y `client.py`, y nunca lo viste.
|
||||
|
||||
Esa proporción es la razón de ser del SDK.
|
||||
|
||||
|
|
@ -139,6 +138,6 @@ Esa proporción es la razón de ser del SDK.
|
|||
* Un decorador por primitiva: `@mcp.tool()`, `@mcp.resource(uri)`, `@mcp.prompt()`. Nombre, descripción y esquema salen de la función.
|
||||
* Una URI con un `{param}` crea una **plantilla** de recurso, que se lista aparte de los recursos concretos.
|
||||
* Las **capacidades** del servidor se declaran por ti, y un cliente solo pide lo que un servidor declara.
|
||||
* `Client(mcp)` se conecta al objeto servidor en memoria: tu entorno de pruebas desde el primer día.
|
||||
* `Client("http://localhost:8000/mcp")` habla con tu servidor en ejecución. Pásale en su lugar el objeto servidor, `Client(mcp)`, y es tu entorno de pruebas desde el primer día.
|
||||
|
||||
Lo siguiente es **[Conectar a un host real](real-host.md)**: este servidor dentro de Claude Desktop o un IDE, de verdad. Después, **[Pruebas](testing.md)**: una página, un cliente en memoria, y nunca más adivinas si funciona. Tras eso, cada primitiva tiene su propia página, empezando por la que maneja el modelo: **[Herramientas](../servers/tools.md)**.
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
---
|
||||
translation:
|
||||
sections: ['4926721070127497', c52a1de2b6b32f40, 8e792bf8c7489ec6, 627195f7159e24ef]
|
||||
sections: [5d13c2f0ba42c0d2, c52a1de2b6b32f40, 8e792bf8c7489ec6, 38552ea228b0a04f]
|
||||
tool: 1
|
||||
---
|
||||
# Pruebas {#testing}
|
||||
|
||||
El SDK de Python incluye una clase `Client` con un **transporte en memoria**: le pasas tu objeto servidor y se conecta a él directamente.
|
||||
La clase `Client` del SDK, la misma que se conecta a una URL o lanza un subproceso, también se conecta **en memoria**: le pasas tu objeto servidor y habla con él directamente.
|
||||
|
||||
Sin subproceso. Sin puerto. Sin transporte alguno. Es la misma idea que el `TestClient` de FastAPI.
|
||||
Sin subproceso. Sin puerto. Nada que se transmita por ningún canal. Es la misma idea que el `TestClient` de FastAPI.
|
||||
|
||||
## Uso básico {#basic-usage}
|
||||
|
||||
|
|
@ -97,11 +97,11 @@ remoto. En una prueba eso es exactamente lo que *no* quieres, y es lo que cambia
|
|||
|
||||
Déjalo activado en las pruebas. No tiene ningún sentido en código de producción.
|
||||
|
||||
## En proceso por defecto {#in-process-by-default}
|
||||
## Neutral respecto a la generación por defecto {#era-neutral-by-default}
|
||||
|
||||
!!! note
|
||||
`Client(mcp)` se conecta en proceso y es **neutral respecto a la generación** por defecto: sondea
|
||||
el servidor y elige la ruta de protocolo adecuada. Fija `mode="legacy"` si tu prueba ejercita
|
||||
el servidor y elige la ruta de protocolo adecuada. Fija `mode="legacy"` si tu prueba comprueba
|
||||
comportamientos específicos de las conexiones heredadas (envío de muestreo (sampling) o
|
||||
elicitación (elicitation), `message_handler`), y quita `raise_exceptions=True` en ese caso: una
|
||||
conexión heredada nunca depura los errores en primer lugar, y el indicador relanza el fallo
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, 8dd027377d46448b, f81491125dcbfe8b]
|
||||
sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, b47667184ca5b516, f81491125dcbfe8b]
|
||||
tool: 1
|
||||
---
|
||||
# Solicitudes de varias idas y vueltas {#multi-round-trip-requests}
|
||||
|
|
@ -164,7 +164,7 @@ El `Server` de bajo nivel es el nivel sin pilas incluidas: a diferencia de `MCPS
|
|||
|
||||
## Un resultado de 2026-07-28 {#a-2026-07-28-result}
|
||||
|
||||
`InputRequiredResult` solo existe en la versión del protocolo **2026-07-28**. El `Client(server)` en memoria la negocia por ti; a través del canal, `mode="auto"` la descubre. Tras conectar, `client.protocol_version` te dice qué obtuviste.
|
||||
`InputRequiredResult` solo existe en la versión del protocolo **2026-07-28**. El `mode="auto"` por defecto de `Client` la descubre en cualquier conexión. Tras conectar, `client.protocol_version` te dice qué obtuviste.
|
||||
|
||||
!!! warning
|
||||
Una sesión anterior a 2026 no tiene dónde poner un `InputRequiredResult`. Devuelve uno desde tu
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 0284b215e85366c4, 8534d8dbb4053a70, 2966fac6fe697007]
|
||||
sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007]
|
||||
tool: 1
|
||||
---
|
||||
# Progreso {#progress}
|
||||
|
|
@ -29,19 +29,17 @@ Tres argumentos, y tú decides qué significan:
|
|||
|
||||
El cliente lo activa **por llamada**, pasando `progress_callback=` a `call_tool`:
|
||||
|
||||
```python title="client.py" hl_lines="7 16"
|
||||
```python title="client.py" hl_lines="5 14"
|
||||
import anyio
|
||||
from mcp import Client
|
||||
|
||||
from server import mcp
|
||||
|
||||
|
||||
async def show(progress: float, total: float | None, message: str | None) -> None:
|
||||
print(f"{message} ({progress}/{total})")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(mcp) as client:
|
||||
async with Client("http://localhost:8000/mcp") as client:
|
||||
result = await client.call_tool(
|
||||
"import_catalog",
|
||||
{"urls": ["https://example.com/a.json", "https://example.com/b.json"]},
|
||||
|
|
@ -56,28 +54,31 @@ anyio.run(main)
|
|||
El callback es una función `async` que recibe exactamente lo que reportó el servidor: `progress`, `total`, `message`.
|
||||
|
||||
!!! info
|
||||
`Client(mcp)` se conecta directamente al objeto servidor, en memoria; es el mismo cliente sobre el que se construye la página **[Pruebas](../get-started/testing.md)**.
|
||||
`progress_callback` es el mismo parámetro sea cual sea el transporte que use el `Client`;
|
||||
los *tiempos* que vas a ver son los de la conexión en memoria. Ejecuta tu callback
|
||||
de forma directa, así que cada reporte llega antes de que `call_tool` devuelva. Con un transporte real,
|
||||
las notificaciones compiten con el resultado, y un callback lento puede seguir ejecutándose después de que `call_tool`
|
||||
haya devuelto.
|
||||
`progress_callback` es el mismo parámetro le pases lo que le pases a `Client`: una URL como aquí, un
|
||||
`StdioServerParameters` o el objeto servidor en una prueba. Eso sí, ten en cuenta los tiempos con un
|
||||
transporte real. Cada notificación se entrega por su cuenta, al margen de la respuesta, así que un callback
|
||||
lento puede seguir ejecutándose después de que `call_tool` haya devuelto. Solo la conexión de prueba en el
|
||||
mismo proceso ejecuta el callback de forma directa y garantiza que cada reporte llegue antes.
|
||||
|
||||
### Pruébalo {#try-it}
|
||||
|
||||
Pon `client.py` junto a `server.py` y ejecútalo:
|
||||
Sirve `server.py` por HTTP y luego ejecuta el cliente desde una segunda terminal:
|
||||
|
||||
```console
|
||||
uv run mcp run server.py --transport streamable-http
|
||||
```
|
||||
|
||||
```console
|
||||
python client.py
|
||||
```
|
||||
|
||||
```text
|
||||
Imported https://example.com/a.json (1/2)
|
||||
Imported https://example.com/b.json (2/2)
|
||||
Imported https://example.com/a.json (1.0/2.0)
|
||||
Imported https://example.com/b.json (2.0/2.0)
|
||||
{'result': 'Imported 2 records.'}
|
||||
```
|
||||
|
||||
Cada `await ctx.report_progress(...)` en el servidor se convirtió en una llamada a `show` en el cliente, en orden, y ambas líneas se imprimieron **antes** de que `call_tool` devolviera. El progreso no va empaquetado en el resultado; se transmite mientras la herramienta sigue trabajando.
|
||||
Cada `await ctx.report_progress(...)` en el servidor se convirtió en una llamada a `show` en el cliente, en orden. El progreso no va empaquetado en el resultado. Se transmite mientras la herramienta sigue trabajando.
|
||||
|
||||
!!! warning
|
||||
`progress_callback` pertenece a la **llamada**, no al `Client`. No hay un argumento del constructor
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [478fd619e5f90ef8, aef094a00e44e248, bab8cbf3449fa7e9, df1809b15a58335b, 5f9d8c2336ed0239, f54974398e43ddef, b24443dd78584870]
|
||||
sections: [d98e337bf28845e0, dd642acbba36f615, f547b3e76da19c94, 149513378588da5c, 4e149ed992046709, f54974398e43ddef, b24443dd78584870]
|
||||
tool: 1
|
||||
---
|
||||
# Versiones del protocolo {#protocol-versions}
|
||||
|
|
@ -11,9 +11,17 @@ Los servidores publicados antes de 2026-07-28 abren cada conexión con el **hand
|
|||
|
||||
Casi nunca tienes que preocuparte por esto, porque `Client` negocia por ti. Esta página trata del único argumento del constructor que lo controla, `mode=`, y de las tres ocasiones en que lo cambias.
|
||||
|
||||
Cada fragmento de esta página es un `client.py` que habla con el `server.py` de Bookshop de **[El cliente](client/index.md)**. Inicia ese servidor en una terminal:
|
||||
|
||||
```console
|
||||
uv run mcp run server.py --transport streamable-http
|
||||
```
|
||||
|
||||
Luego ejecuta cada fragmento en una segunda terminal con `python client.py`.
|
||||
|
||||
## `mode="auto"` {#modeauto}
|
||||
|
||||
```python title="client.py" hl_lines="14-15"
|
||||
```python title="client.py" hl_lines="7-8"
|
||||
--8<-- "docs_src/protocol_versions/tutorial001.py"
|
||||
```
|
||||
|
||||
|
|
@ -31,13 +39,14 @@ En cualquier caso terminas conectado, y `client.protocol_version` te dice cuál
|
|||
Esa es toda la funcionalidad. Un solo `Client`, servidores de cualquier generación, sin ramificaciones en tu código.
|
||||
|
||||
!!! info
|
||||
`MCPServer` responde a `server/discover` en todos los transportes (en memoria, stdio, Streamable
|
||||
HTTP), así que contra tu propio servidor `auto` siempre llega a `2026-07-28`. El mecanismo de
|
||||
respaldo solo se activa contra un servidor real anterior a 2026, que es exactamente cuando quieres que lo haga.
|
||||
`MCPServer` responde a `server/discover` en todos los transportes (Streamable HTTP, stdio y la
|
||||
conexión en proceso que usan tus pruebas), así que contra tu propio servidor `auto` siempre llega
|
||||
a `2026-07-28`. El mecanismo de respaldo solo se activa contra un servidor real anterior a 2026,
|
||||
que es exactamente cuando quieres que lo haga.
|
||||
|
||||
## `mode="legacy"` {#modelegacy}
|
||||
|
||||
```python title="client.py" hl_lines="14"
|
||||
```python title="client.py" hl_lines="7"
|
||||
--8<-- "docs_src/protocol_versions/tutorial002.py"
|
||||
```
|
||||
|
||||
|
|
@ -61,7 +70,7 @@ En 2026-07-28 ya no existe. El servidor *devuelve* sus preguntas y tú repites l
|
|||
|
||||
`mode` también acepta una cadena con una versión moderna del protocolo. Hoy ese conjunto es exactamente `["2026-07-28"]`.
|
||||
|
||||
```python title="client.py" hl_lines="14"
|
||||
```python title="client.py" hl_lines="7"
|
||||
--8<-- "docs_src/protocol_versions/tutorial003.py"
|
||||
```
|
||||
|
||||
|
|
@ -94,7 +103,7 @@ El sondeo es barato, pero sigue siendo una ida y vuelta que pagas en cada recone
|
|||
|
||||
Así que guárdala. Tras una conexión `auto`, `client.session.discover_result` contiene el `DiscoverResult` exacto que envió el servidor: sus `supported_versions`, sus `capabilities`, sus `instructions` y la identidad que el servidor grabó en el `_meta` del resultado. Devuélveselo como `prior_discover=` la próxima vez:
|
||||
|
||||
```python title="client.py" hl_lines="15 17"
|
||||
```python title="client.py" hl_lines="8 10"
|
||||
--8<-- "docs_src/protocol_versions/tutorial004.py"
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 70b9ece244ca1b0c, cba78e052898c3f6, f06bdb541cb0b469, fb82d526320b7cc3]
|
||||
sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 10394c6f16601638, cba78e052898c3f6, f06bdb541cb0b469, 6dc898ccc5a903f9]
|
||||
tool: 1
|
||||
---
|
||||
# Añadir a una app existente {#add-to-an-existing-app}
|
||||
|
|
@ -100,7 +100,7 @@ Ese `/mcp` final es `streamable_http_path`. Ponlo en `"/"` y el prefijo de monta
|
|||
--8<-- "docs_src/asgi/tutorial004.py"
|
||||
```
|
||||
|
||||
Ahora los clientes se conectan a `/notes`, no a `/notes/mcp`.
|
||||
Ahora los clientes se conectan a `/notes/`, no a `/notes/mcp`.
|
||||
|
||||
## CORS para clientes de navegador {#cors-for-browser-clients}
|
||||
|
||||
|
|
@ -143,4 +143,4 @@ Un cliente basado en navegador necesita dos permisos de tu parte: **enviar** sus
|
|||
* Los clientes de navegador necesitan CORS: `allow_headers` para los encabezados de solicitud `Mcp-*`, `expose_headers=["Mcp-Session-Id"]` para la respuesta.
|
||||
* `@mcp.custom_route()` añade endpoints HTTP simples, sin autenticación, junto a `/mcp`.
|
||||
|
||||
Una vez que el servidor es accesible en una URL real, **[El cliente](../client/index.md)** se conecta a él con esa URL en lugar de con un objeto servidor.
|
||||
Una vez que el servidor es accesible en una URL real, **[El cliente](../client/index.md)** se conecta a él con esa URL.
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
---
|
||||
translation:
|
||||
sections: [d62c13457fc4a534, 80e73abaca6e0652, d1dc4c54cd00ec9c, 14ad3bc7904036bb, 5225f127bc1b9c77, fe1626fdd5aad1da, 4556cb7ea1a04a31]
|
||||
sections: [d62c13457fc4a534, 80e73abaca6e0652, 128a492d18295f64, 14ad3bc7904036bb, 54a6697833fcc1d9, fe1626fdd5aad1da, 811d083c1da8bcf6]
|
||||
tool: 1
|
||||
---
|
||||
# Autorización {#authorization}
|
||||
|
||||
Sobre Streamable HTTP, tu servidor MCP es un servicio web común y corriente, y lo proteges igual que proteges cualquier servicio web: con tokens bearer de OAuth 2.1.
|
||||
Sobre Streamable HTTP tu servidor MCP es un servicio web común y corriente, y lo proteges igual que proteges cualquier servicio web: con tokens bearer de OAuth 2.1.
|
||||
|
||||
En términos de OAuth, el servidor es un **servidor de recursos**. Nunca inicia la sesión de nadie y nunca emite un token. Hace una sola cosa: mirar el header `Authorization` de cada solicitud y decidir si el token que trae es válido.
|
||||
En términos de OAuth, el servidor es un **servidor de recursos**. Nunca inicia la sesión de nadie y nunca emite un token. Hace una sola cosa: mirar el header `Authorization` de cada solicitud y decidir si el token que trae es bueno.
|
||||
|
||||
Esta página es el lado del servidor. Un cliente que descubre tu servidor de autorización y obtiene el token está en **[Clientes OAuth](../client/oauth-clients.md)**.
|
||||
|
||||
|
|
@ -21,21 +21,25 @@ Ese es todo el triángulo. Todo lo que hay en esta página es el punto del medio
|
|||
|
||||
## Un verificador de tokens {#a-token-verifier}
|
||||
|
||||
El SDK no opina sobre cómo debe ser un token válido. Se lo dices tú, implementando **`TokenVerifier`**:
|
||||
El SDK no tiene opinión sobre cómo es un token válido. Se lo dices tú, implementando **`TokenVerifier`**:
|
||||
|
||||
```python title="server.py" hl_lines="12-14 19-24"
|
||||
```python title="server.py" hl_lines="14-16 21-27"
|
||||
--8<-- "docs_src/authorization/tutorial001.py"
|
||||
```
|
||||
|
||||
* `TokenVerifier` es un protocolo con un solo método asíncrono. `verify_token` recibe el token en bruto del header `Authorization` y devuelve un **`AccessToken`** si es válido, `None` si no lo es. No hay nada más que implementar.
|
||||
* Este busca el token en una tabla. Uno real verifica la firma de un JWT o llama al endpoint de introspección de tokens del servidor de autorización. Ese código es tuyo; el SDK solo lo llama.
|
||||
* `token_verifier=` y `auth=` siempre van juntos. Pasa uno sin el otro y `MCPServer(...)` lanza un `ValueError` antes de atender ninguna solicitud.
|
||||
* `TokenVerifier` es un protocolo con un único método asíncrono. `verify_token` recibe el token en bruto del header `Authorization` y devuelve un **`AccessToken`** si es válido, `None` si no lo es. No hay nada más que implementar.
|
||||
* Este busca el token en una tabla; cada entrada registra el recurso para el que se emitió. Uno real verifica la firma de un JWT o llama al endpoint de introspección de tokens del servidor de autorización, e informa para quién se emitió el token (su `aud`) en `AccessToken.resource`. Ese código es tuyo; el SDK solo lo llama.
|
||||
* `token_verifier=` y `auth=` siempre van juntos. Pasa uno sin el otro y `MCPServer(...)` lanza un `ValueError` antes de atender una sola solicitud.
|
||||
|
||||
`AuthSettings` es la cara pública de tu servidor de recursos:
|
||||
|
||||
* `issuer_url`: el servidor de autorización que emite tus tokens.
|
||||
* `resource_server_url`: la URL pública de este endpoint MCP. Indica *para qué* recurso es un token y es donde vive el documento de descubrimiento.
|
||||
* `resource_server_url`: la URL pública de este endpoint MCP. Indica para *qué* recurso es un token, y es donde vive el documento de descubrimiento.
|
||||
* `required_scopes`: todo token debe traerlos todos.
|
||||
* `validate_token_resource`: rechaza cualquier token cuyo `AccessToken.resource` no sea `resource_server_url`. Dejarlo sin definir mientras `resource_server_url` está definido emite un aviso (`MCPDeprecationWarning`) y se comporta como `False`; la versión 3.0 hace que `True` sea el valor por defecto para los servidores de recursos.
|
||||
* Actívalo cuando tu servidor de autorización vincula los tokens al `resource` que pidió el cliente, que los clientes MCP siempre envían. Mantén `resource_server_url` como la URL exacta a la que se conectan los clientes.
|
||||
* Déjalo desactivado cuando tu servidor de autorización usa sus propios identificadores de audiencia (un identificador de API de Auth0, un ID de aplicación de Entra) y comprueba `aud` en tu verificador, devolviendo `None` para un token que no sea para este servidor.
|
||||
* Si `aud` es una lista, pon en `resource` la entrada que sea igual a `resource_server_url`.
|
||||
|
||||
!!! tip
|
||||
`examples/servers/simple-auth/` en el repositorio del SDK tiene un `IntrospectionTokenVerifier` que llama
|
||||
|
|
@ -43,7 +47,7 @@ El SDK no opina sobre cómo debe ser un token válido. Se lo dices tú, implemen
|
|||
|
||||
## Lo que obtienes sobre HTTP {#what-you-get-over-http}
|
||||
|
||||
La autorización vive en los headers HTTP, así que solo existe en los transportes HTTP. Ejecútala en el que despliegues: `mcp.run(transport="streamable-http")` la pone en `http://127.0.0.1:8000/mcp`, y **[Ejecutar el servidor](index.md)** tiene el resto. La app ahora tiene dos rutas:
|
||||
La autorización vive en los headers HTTP, así que solo existe en los transportes HTTP. Ejecútala en el que despliegues: `mcp.run(transport="streamable-http")` la pone en `http://127.0.0.1:8000/mcp`, y **[Ejecutar tu servidor](index.md)** tiene el resto. La app ahora tiene dos rutas:
|
||||
|
||||
```text
|
||||
/mcp
|
||||
|
|
@ -65,7 +69,7 @@ Haz un `GET` a esa ruta well-known y obtienes los **Protected Resource Metadata
|
|||
}
|
||||
```
|
||||
|
||||
Este documento es la forma en que un cliente que nunca ha oído hablar de tu servidor encuentra la entrada: lee `authorization_servers` y va ahí a buscar un token. No escribiste nada de él.
|
||||
Este documento es la forma en que un cliente que nunca ha oído hablar de tu servidor encuentra el camino de entrada: lee `authorization_servers` y va allí a buscar un token. No escribiste nada de él.
|
||||
|
||||
!!! check
|
||||
Llama a `/mcp` sin token (o con uno para el que tu verificador devolvió `None`) y la solicitud
|
||||
|
|
@ -78,25 +82,25 @@ Este documento es la forma en que un cliente que nunca ha oído hablar de tu ser
|
|||
{"error": "invalid_token", "error_description": "Authentication required"}
|
||||
```
|
||||
|
||||
No se analizó nada ni se ejecutó ninguna herramienta. Y ese puntero `resource_metadata` en `WWW-Authenticate` es
|
||||
No se analizó nada y no se ejecutó ninguna herramienta. Y ese puntero `resource_metadata` en `WWW-Authenticate` es
|
||||
lo que hace automático el descubrimiento: 401 -> documento de metadatos -> servidor de autorización -> token -> reintento.
|
||||
|
||||
!!! warning
|
||||
Nada de esto protege a `stdio`. Una tubería no tiene header `Authorization`, así que ahí nunca se
|
||||
consulta `token_verifier`. La frontera de seguridad de un servidor `stdio` es el proceso que lo lanzó. Lo mismo
|
||||
vale para el `Client(mcp)` en memoria que usas en las pruebas: se conecta directamente al objeto servidor
|
||||
Nada de esto protege `stdio`. Una tubería no tiene header `Authorization`, así que `token_verifier` nunca
|
||||
se consulta ahí. El límite de seguridad de un servidor `stdio` es el proceso que lo lanzó. Lo mismo
|
||||
vale para el `Client(mcp)` en memoria que usas en las pruebas: se conecta directamente al objeto del servidor
|
||||
y se salta la capa HTTP, autorización incluida.
|
||||
|
||||
## La identidad de quien llama {#the-callers-identity}
|
||||
|
||||
Dentro de cualquier handler, **`get_access_token()`** es el `AccessToken` que tu verificador devolvió para la solicitud actual:
|
||||
|
||||
```python title="server.py" hl_lines="4 32-35"
|
||||
```python title="server.py" hl_lines="4 35-38"
|
||||
--8<-- "docs_src/authorization/tutorial002.py"
|
||||
```
|
||||
|
||||
* Funciona en herramientas, recursos y prompts, y no hay nada que pasar de un lado a otro: el middleware de autenticación lo guarda en una variable de contexto por solicitud.
|
||||
* Recibes el **mismo objeto que construyó tu verificador**: `client_id`, `scopes`, `subject`, `expires_at` y cualquier `claims` extra que hayas añadido. Ese es el punto de enganche para reglas por herramienta: lee los scopes y rechaza.
|
||||
* Recibes el **mismo objeto que construyó tu verificador**: `client_id`, `scopes`, `subject`, `expires_at` y cualquier `claims` extra que hayas adjuntado. Ese es el gancho para reglas por herramienta: lee los scopes y rechaza.
|
||||
* Fuera de una solicitud HTTP autenticada devuelve `None`. En memoria y sobre `stdio` siempre es `None`.
|
||||
|
||||
Llama a `whoami` con `Authorization: Bearer alice-token` y el modelo lee:
|
||||
|
|
@ -109,14 +113,14 @@ alice (scopes: notes:read)
|
|||
|
||||
El SDK te da la mitad del servidor de recursos: verificar, anunciar, rechazar. No te da una página de inicio de sesión, una pantalla de consentimiento ni un token.
|
||||
|
||||
Para ver a las tres partes en movimiento, ejecuta `examples/servers/simple-auth/` del repositorio del SDK (un pequeño servidor de autorización y un servidor de recursos configurado exactamente como en esta página) y luego apunta `examples/clients/simple-auth-client/` hacia él para ver el recorrido completo de descubrimiento y token.
|
||||
Para ver moverse a las tres partes, ejecuta `examples/servers/simple-auth/` del repositorio del SDK (un pequeño servidor de autorización y un servidor de recursos configurados exactamente como en esta página) y luego apunta `examples/clients/simple-auth-client/` hacia él para ver la coreografía completa de descubrimiento y token.
|
||||
|
||||
!!! info
|
||||
Hay un segundo argumento del constructor, `auth_server_provider=`, que incrusta un servidor de autorización
|
||||
completo dentro de tu servidor MCP. Es anterior a la separación AS/RS sobre la que se construye la especificación
|
||||
de autorización de MCP. Los servidores nuevos no deberían recurrir a él.
|
||||
completo dentro de tu servidor MCP. Es anterior a la separación AS/RS alrededor de la cual está construida
|
||||
la especificación de autorización de MCP. Los servidores nuevos no deberían recurrir a él.
|
||||
|
||||
Un servidor de autorización también puede aceptar la aserción firmada de un proveedor de identidad empresarial en lugar de que un usuario haga clic en una pantalla de consentimiento, y el SDK admite los dos lados de ese intercambio. El grant, y el cliente que lo presenta, están en **[Aserción de identidad](../client/identity-assertion.md)**.
|
||||
Un servidor de autorización también puede aceptar la aserción firmada de un proveedor de identidad empresarial en lugar de que un usuario pase por una pantalla de consentimiento, y el SDK admite ambos lados de ese intercambio. El grant, y el cliente que lo presenta, está en **[Aserción de identidad](../client/identity-assertion.md)**.
|
||||
|
||||
## Resumen {#recap}
|
||||
|
||||
|
|
@ -124,7 +128,7 @@ Un servidor de autorización también puede aceptar la aserción firmada de un p
|
|||
* `TokenVerifier` es toda la superficie de integración: un método asíncrono, entra un token, sale `AccessToken | None`.
|
||||
* `token_verifier=` y `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` siempre van juntos.
|
||||
* El SDK publica los Protected Resource Metadata de [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) en `/.well-known/oauth-protected-resource/...` y responde a las solicitudes no autenticadas con un 401 cuyo header `WWW-Authenticate` apunta a ellos. Ese es todo el mecanismo de descubrimiento.
|
||||
* `get_access_token()` en cualquier handler te dice quién llama.
|
||||
* La autorización es un asunto de HTTP. `stdio` y el cliente en memoria nunca la ven.
|
||||
* `get_access_token()` en cualquier handler es quién está llamando.
|
||||
* La autorización es asunto de HTTP. `stdio` y el cliente de prueba en memoria nunca la ven.
|
||||
|
||||
La mitad del cliente (descubrir tu servidor de autorización y obtener el token por ti) está en **[Clientes OAuth](../client/oauth-clients.md)**. Y un cliente que *afirma* una identidad en lugar de pedírsela a un usuario está en **[Aserción de identidad](../client/identity-assertion.md)**.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [28221886b198784f, f88ea1f1614f3a1d, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, e758745df6fb7b0a]
|
||||
sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, 697b01d95080880d]
|
||||
tool: 1
|
||||
---
|
||||
# Desplegar y escalar {#deploy-scale}
|
||||
|
|
@ -47,6 +47,22 @@ Desplegada detrás de un nombre de host real, esa misma configuración por defec
|
|||
desplegado que rechaza todas las conexiones es una lista de hosts permitidos hasta que se demuestre lo contrario.
|
||||
**[Solución de problemas](../troubleshooting.md)** también empieza por aquí.
|
||||
|
||||
## Detrás de un proxy que termina TLS {#behind-a-tls-terminating-proxy}
|
||||
|
||||
Si TLS termina en un proxy (un ingress, un balanceador de carga, Caddy, nginx) y uvicorn sirve HTTP plano detrás de él, dile a uvicorn que confíe en los encabezados `X-Forwarded-*` del proxy:
|
||||
|
||||
```console
|
||||
uvicorn server:app --proxy-headers --forwarded-allow-ips='<proxy address>'
|
||||
```
|
||||
|
||||
Sin eso, la app cree que se está sirviendo por `http://`, y cualquier redirección que emita (la habitual es `/mcp` → `/mcp/`) apunta a `http://…`. El cliente de Python se niega a seguir un endpoint HTTPS hacia HTTP plano y lo dice:
|
||||
|
||||
```text
|
||||
MCPError: Redirect to http://mcp.example.com/mcp/ not followed: it would downgrade this HTTPS endpoint to plain HTTP.
|
||||
```
|
||||
|
||||
El parche del lado del cliente es configurar la URL exacta que sirve el servidor (`https://mcp.example.com/mcp/`, barra incluida) para que no haya ninguna redirección. La solución es el flag de arriba. `FORWARDED_ALLOW_IPS` es la forma en variable de entorno; `*` confía en todos los saltos, lo que solo es correcto cuando nada salvo el proxy puede llegar a uvicorn.
|
||||
|
||||
## Workers, y quién tiene que ser sticky {#workers-and-who-has-to-be-sticky}
|
||||
|
||||
Una vez que el nombre de host responde, pon más de un worker detrás. No hay ningún ajuste del SDK para eso; una app Starlette se escala como cualquier app ASGI, entregando el objeto a algo que sepa hacer fork:
|
||||
|
|
@ -170,6 +186,7 @@ Un `MCPServer` es una implementación del protocolo, no un servidor de aplicacio
|
|||
## Resumen {#recap}
|
||||
|
||||
* Por defecto, la app responde solo a las solicitudes dirigidas a localhost. `transport_security=TransportSecuritySettings(allowed_hosts=[...], allowed_origins=[...])` es la puerta de salida a producción: hasta que lo pases, cada solicitud detrás de un nombre de host real es un `421` y el motivo solo está en el log del servidor.
|
||||
* Detrás de un proxy que termina TLS, ejecuta uvicorn con `--proxy-headers --forwarded-allow-ips=...`, o sus redirecciones apuntarán a `http://` y el cliente las rechazará.
|
||||
* En 2026-07-28 no hay sesión ni nada sobre lo que un balanceador de carga pueda ser sticky. `stateless_http=True` es un ajuste solo para lo heredado porque una solicitud moderna se enruta y se responde antes de que ese flag llegue a leerse.
|
||||
* La clave por defecto de `requestState` es `os.urandom(32)`, acuñada por proceso. Un reintento de varias idas y vueltas que llega a otro worker falla con `-32602` *"Invalid or expired requestState"*.
|
||||
* La solución es `RequestStateSecurity(keys=[...])` **y** el mismo nombre de servidor en cada instancia. El nombre es el claim de audiencia por defecto del token. Las mismas claves, el mismo nombre.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 2fd7cf825e6d2b2c, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0]
|
||||
sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 9fd2357154a5b7e7, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0]
|
||||
tool: 1
|
||||
---
|
||||
# Ejecutar el servidor {#running-your-server}
|
||||
|
|
@ -75,6 +75,11 @@ Cada transporte tiene sus propios argumentos nombrados, todos en `run()`:
|
|||
* `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.
|
||||
* `session_idle_timeout`: segundos que una sesión heredada puede pasar sin nada en curso antes de que el
|
||||
servidor la cierre. Por defecto 1800. `None` lo desactiva. Consulta
|
||||
[Duración de la sesión y límites](legacy-clients.md#session-lifetime-and-limits).
|
||||
* `max_sessions`: cuántas sesiones heredadas mantiene un proceso a la vez. Por defecto 10 000. `None`
|
||||
quita el límite. Se trata en la misma sección.
|
||||
* `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`.
|
||||
|
||||
!!! warning
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue