docs: refresh translations, and translate pages in parallel (#3458)

This commit is contained in:
Max 2026-09-07 13:35:53 +01:00 • committed by GitHub
commit fd66270926
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 4821 additions and 2277 deletions

View file

@ -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.

View file

@ -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)**.

View file

@ -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.

View file

@ -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

View file

@ -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ó.