docs: refresh translations, and translate pages in parallel (#3458)
This commit is contained in:
parent
08a3bc8eaf
commit
fd66270926
5 changed files with 4821 additions and 2277 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [3d1663c18edc824c, d4fd37009a13f03d, af9f398a5a8b679a, 470c2dd144294d69, 8e45827e6d24e8c8, 91dfd0ce98ebb03c]
|
||||
sections: [3d1663c18edc824c, 90956965ae6a1ca1, af9f398a5a8b679a, 5ce83b1f9d88da62, 0d9b5d13fffc94e5, 8e45827e6d24e8c8, 91dfd0ce98ebb03c]
|
||||
tool: 1
|
||||
---
|
||||
# Atender clientes heredados {#serving-legacy-clients}
|
||||
|
|
@ -21,15 +21,25 @@ Así que un cliente heredado (legacy) no es algo *para* lo que construyes. Es al
|
|||
|
||||
## Un handler, ambas generaciones {#one-handler-both-eras}
|
||||
|
||||
Aquí tienes una herramienta que necesita preguntarle algo al usuario, y clientes de ambas generaciones que la llaman:
|
||||
Aquí tienes una herramienta que necesita preguntarle algo al usuario:
|
||||
|
||||
```python title="server.py" hl_lines="24 37-38"
|
||||
```python title="server.py" hl_lines="21"
|
||||
--8<-- "docs_src/legacy_clients/tutorial001.py"
|
||||
```
|
||||
|
||||
`reserve` necesita una cosa que el modelo no proporcionó: cuántos ejemplares. `Annotated[..., Resolve(ask_quantity)]` es la forma en que una herramienta lo declara (**[Dependencias](../handlers/dependencies.md)** tiene todos los detalles). Nada en `reserve` nombra una versión, comprueba una capacidad ni se bifurca.
|
||||
|
||||
Los dos clientes están abiertos **al mismo tiempo**, sobre el mismo objeto `mcp`. `mode="legacy"` ejecuta el handshake `initialize`: exactamente la conexión que abre un cliente anterior a 2026. El otro toma el valor por defecto y queda en `2026-07-28`.
|
||||
Sírvela por HTTP, y aquí tienes clientes de ambas generaciones que la llaman:
|
||||
|
||||
```console
|
||||
uv run mcp run server.py --transport streamable-http
|
||||
```
|
||||
|
||||
```python title="client.py" hl_lines="14-15"
|
||||
--8<-- "docs_src/legacy_clients/tutorial001_client.py"
|
||||
```
|
||||
|
||||
Los dos clientes están abiertos **al mismo tiempo**, contra el mismo servidor en ejecución. `mode="legacy"` ejecuta el handshake `initialize`: exactamente la conexión que abre un cliente anterior a 2026. El otro toma el valor por defecto y queda en `2026-07-28`. Ejecuta `python client.py` desde una segunda terminal:
|
||||
|
||||
```text
|
||||
2025-11-25 {'result': "Reserved 2 of 'Dune'."}
|
||||
|
|
@ -62,6 +72,41 @@ Con un solo worker eso no se nota. Con dos, es todo el problema: una solicitud q
|
|||
perdidos a un cliente que se reconecta a la *misma* sesión), no un almacén de sesiones. Nunca
|
||||
hace que una sesión sea alcanzable desde otro proceso.
|
||||
|
||||
## Duración y límites de la sesión {#session-lifetime-and-limits}
|
||||
|
||||
Una sesión heredada no vive para siempre, y un proceso no mantiene un número ilimitado de
|
||||
ellas. Dos ajustes controlan esto. Ambos son argumentos nombrados de `run()`, `streamable_http_app()`
|
||||
y `Server.streamable_http_app()`. Las conexiones modernas (`2026-07-28`) y `stateless_http=True`
|
||||
no tienen sesiones, así que ninguno de los dos ajustes se les aplica.
|
||||
|
||||
| Ajuste | Por defecto | Qué hace | Qué ve el cliente | Desactivarlo |
|
||||
|---|---|---|---|---|
|
||||
| `session_idle_timeout` | `1800` (30 min) | Cierra una sesión que no ha tenido nada en curso durante ese tiempo. | `404 Session not found`. Tiene que volver a hacer `initialize`. | `None` |
|
||||
| `max_sessions` | `10_000` | Se niega a abrir una sesión más allá de esa cantidad. Las sesiones existentes no se tocan y no se expulsa ninguna. | `503 Too many open sessions` con el código JSON-RPC `-32603`. | `None` |
|
||||
|
||||
Qué cuenta como "en curso":
|
||||
|
||||
* Un stream `GET` abierto. Los clientes del SDK mantienen uno abierto, así que la sesión de un
|
||||
cliente conectado nunca expira.
|
||||
* Una solicitud que todavía se está respondiendo. Una llamada a herramienta que dura más que el
|
||||
timeout no se interrumpe, y el plazo solo empieza a contar cuando termina.
|
||||
* Nada más. Entre solicitudes el reloj corre. Cualquier solicitud en la sesión lo reinicia,
|
||||
`ping` incluido. Una vez que una sesión ha expirado, nada la revive.
|
||||
|
||||
Un cliente que termina su sesión con `DELETE` la libera de inmediato. También uno cuya
|
||||
solicitud de apertura fue rechazada.
|
||||
|
||||
```python
|
||||
mcp.run(transport="streamable-http", session_idle_timeout=None, max_sessions=50_000)
|
||||
```
|
||||
|
||||
Ambos eventos aparecen en el log del servidor. Una expiración es `Session <id> idle timeout` en
|
||||
nivel `INFO`. Una apertura rechazada es `Refusing to open a new session: <n> sessions are already open`
|
||||
en nivel `WARNING`.
|
||||
|
||||
Los límites son por proceso. Con cuatro workers el techo es cuatro veces `max_sessions`, y cada
|
||||
worker expira sus propias sesiones.
|
||||
|
||||
## El único ajuste: `stateless_http` {#the-one-knob-stateless_http}
|
||||
|
||||
Si la afinidad es un costo que te niegas a pagar, hay exactamente una cosa que puedes cambiar.
|
||||
|
|
@ -87,7 +132,7 @@ Dos cosas sobre él importan más que lo que hace.
|
|||
|
||||
!!! check
|
||||
Haz lo incorrecto. `reserve` es exactamente la herramienta que acaba de atender a ambos clientes.
|
||||
Despliégala con `stateless_http=True`, conecta los mismos dos clientes por HTTP y llámala desde
|
||||
Despliégala con `stateless_http=True`, conecta los mismos dos clientes y llámala desde
|
||||
cada uno.
|
||||
|
||||
El cliente moderno sigue recibiendo `Reserved 2 of 'Dune'.` El tramo moderno no cambió.
|
||||
|
|
|
|||
Loading…
Reference in a new issue