docs: refresh translations, and translate pages in parallel (#3458)
This commit is contained in:
parent
08a3bc8eaf
commit
fd66270926
6 changed files with 4821 additions and 2277 deletions
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue