docs: publish translated docs in twelve languages and the tool that maintains them (#3280)
This commit is contained in:
parent
14aa889992
commit
52ad0a8876
9 changed files with 89737 additions and 54 deletions
131
i18n/es/pages/servers/completions.md
Normal file
131
i18n/es/pages/servers/completions.md
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
---
|
||||
translation:
|
||||
sections: [72f9c964769076dd, 9a2c14e10935b515, 235299eb78ab12d7, 8aee1e78c8237fb8, 9bd86acd4112138f, 55343cb7f250dc7b]
|
||||
tool: 1
|
||||
---
|
||||
# Autocompletado {#completions}
|
||||
|
||||
Un cliente que construye una interfaz sobre tu servidor quiere autocompletar los valores de los argumentos mientras el usuario escribe: nombres de lenguajes, nombres de repositorios, rutas de archivos.
|
||||
|
||||
El **autocompletado** (completions) es la forma en que tu servidor proporciona esas sugerencias.
|
||||
|
||||
## Algo que valga la pena completar {#something-worth-completing}
|
||||
|
||||
El autocompletado se aplica exactamente a dos cosas: los argumentos de un **prompt** y los parámetros de una **plantilla de recurso**. Así que empieza con un servidor que tenga uno de cada:
|
||||
|
||||
```python title="server.py" hl_lines="6 12"
|
||||
--8<-- "docs_src/completions/tutorial001.py"
|
||||
```
|
||||
|
||||
Aquí todavía no hay nada de autocompletado.
|
||||
|
||||
* `review_code` recibe un `language`. Un usuario no debería tener que adivinar qué formas de escribirlo aceptas.
|
||||
* `github_repo` recibe un `owner` y un `repo`. Dos campos de texto libre hacen un mal formulario.
|
||||
|
||||
## El handler de autocompletado {#the-completion-handler}
|
||||
|
||||
Añade **una** función decorada con `@mcp.completion()`:
|
||||
|
||||
```python title="server.py" hl_lines="21-29"
|
||||
--8<-- "docs_src/completions/tutorial002.py"
|
||||
```
|
||||
|
||||
* Hay un handler por servidor. Todas las solicitudes de autocompletado llegan aquí, y tú decides qué hacer según lo que se esté completando.
|
||||
* Debe ser `async def`: el SDK lo espera con await.
|
||||
* Recibe tres argumentos:
|
||||
* `ref`: *qué* prompt o plantilla de recurso, como `PromptReference` o `ResourceTemplateReference`. Con `isinstance` los distingues.
|
||||
* `argument`: `argument.name` es el argumento que se está completando, `argument.value` es lo que el usuario ha escrito hasta ahora.
|
||||
* `context`: los argumentos ya resueltos. Ignóralo por ahora.
|
||||
* Devuelves un `Completion(values=[...])`, o `None` cuando no tienes nada que ofrecer.
|
||||
|
||||
!!! tip
|
||||
`argument.value` es el prefijo que el usuario ha escrito. El SDK **no** filtra por ti: lo que
|
||||
pongas en `values` es lo que muestra la interfaz. El `startswith` lo escribes tú.
|
||||
|
||||
### Pruébalo {#try-it}
|
||||
|
||||
Manéjalo con el `Client` en memoria de **[Pruebas](../get-started/testing.md)**. Llama a
|
||||
`client.complete()` con `ref=PromptReference(name="review_code")` y
|
||||
`argument={"name": "language", "value": "py"}`:
|
||||
|
||||
```python
|
||||
result.completion.values # ['python']
|
||||
```
|
||||
|
||||
* `ref` es el mismo tipo de referencia que recibe tu handler.
|
||||
* `argument` es un dict normal con exactamente dos claves, `name` y `value`.
|
||||
|
||||
Envía un `value` vacío y te devuelve la lista completa. `lang.startswith("")` es verdadero para todos los lenguajes:
|
||||
|
||||
```python
|
||||
result.completion.values # ['go', 'javascript', 'python', 'rust', 'typescript']
|
||||
```
|
||||
|
||||
Pregunta por `code` (un argumento que tu handler no reconoce) y devuelve `None`, que el SDK convierte en una lista vacía:
|
||||
|
||||
```python
|
||||
result.completion.values # []
|
||||
```
|
||||
|
||||
`None` significa *"sin sugerencias"*, nunca un error. La interfaz recurre a un campo de texto normal.
|
||||
|
||||
## Una capacidad que nunca declaraste {#a-capability-you-never-declared}
|
||||
|
||||
Registrar el handler es la declaración. Conecta un cliente y mira:
|
||||
|
||||
```python
|
||||
client.server_capabilities.completions # CompletionsCapability()
|
||||
```
|
||||
|
||||
No escribiste `completions` en ninguna parte. El SDK vio el handler y declaró la capacidad por ti. Todas las capacidades *opcionales* funcionan así: el handler es la declaración. (Las tres primitivas no son opcionales: `MCPServer` siempre las declara, haya handlers o no.)
|
||||
|
||||
!!! check
|
||||
Vuelve al primer `server.py` (el que no tiene handler) y pregúntale de todos modos. La llamada
|
||||
falla con un error JSON-RPC:
|
||||
|
||||
```text
|
||||
Method not found
|
||||
```
|
||||
|
||||
Y `client.server_capabilities.completions` es `None`. Ese es el sentido de la capacidad: un
|
||||
cliente bien hecho la comprueba y nunca envía la solicitud que no puedes responder.
|
||||
|
||||
## Argumentos dependientes {#dependent-arguments}
|
||||
|
||||
`github://repos/{owner}/{repo}` tiene dos parámetros, y los valores útiles para `repo` dependen de qué `owner` se eligió primero.
|
||||
|
||||
Para eso sirve `context`. Lleva los argumentos que el usuario **ya ha resuelto**:
|
||||
|
||||
```python title="server.py" hl_lines="8-11 34-38"
|
||||
--8<-- "docs_src/completions/tutorial003.py"
|
||||
```
|
||||
|
||||
* La nueva rama se activa para el parámetro `repo` de la plantilla.
|
||||
* `context.arguments` es un `dict[str, str] | None` con los valores elegidos hasta ahora (aquí, `owner`).
|
||||
* Si todavía no hay `owner`, no hay sugerencias sensatas, así que el handler devuelve `None`.
|
||||
|
||||
El cliente envía esos valores resueltos con `context_arguments=`. Esta vez `ref` es un
|
||||
`ResourceTemplateReference(uri="github://repos/{owner}/{repo}")`. Pide `repo` con un
|
||||
`value` vacío y pasa `context_arguments={"owner": "modelcontextprotocol"}`:
|
||||
|
||||
```python
|
||||
result.completion.values # ['python-sdk', 'typescript-sdk', 'inspector']
|
||||
```
|
||||
|
||||
Quita `context_arguments=` y la misma llamada devuelve `[]`. El handler no puede saber qué repositorios ofrecer hasta que conoce el propietario.
|
||||
|
||||
!!! info
|
||||
`Completion` también acepta `total=` y `has_more=`. Úsalos cuando `values` sea un fragmento de
|
||||
una lista más larga, para que la interfaz pueda mostrar *"y 200 más"*. La mayoría de los
|
||||
handlers nunca los necesitan.
|
||||
|
||||
## Resumen {#recap}
|
||||
|
||||
* El autocompletado son sugerencias para **argumentos de prompts** y **parámetros de plantillas de recurso**. Nada más.
|
||||
* `@mcp.completion()` registra el único handler. Es `async def (ref, argument, context) -> Completion | None`.
|
||||
* Decide según `isinstance(ref, ...)` y `argument.name`. Filtra por `argument.value` tú mismo.
|
||||
* `None` se convierte en una lista vacía. Nunca es un error.
|
||||
* `context.arguments` contiene los valores ya resueltos; el cliente los proporciona como `context_arguments=`.
|
||||
* La capacidad `completions` aparece en cuanto registras el handler. Sin él, la solicitud da `Method not found`.
|
||||
|
||||
Las sugerencias ayudan mientras el usuario todavía está *rellenando* un prompt o una plantilla; para hacerle una pregunta en *mitad* de una llamada a una herramienta, lo que quieres es **[Elicitación](../handlers/elicitation.md)**. Todo lo que una herramienta puede devolver además de texto está en **[Imágenes, audio e iconos](media.md)**.
|
||||
140
i18n/es/pages/servers/handling-errors.md
Normal file
140
i18n/es/pages/servers/handling-errors.md
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
---
|
||||
translation:
|
||||
sections: [e33d441f12d50535, 7099694c603e0f5f, c1df4cf9673433e6, c9cd294541422e6e, 6cec073617bfd037, efa92b8f99e908c8, 6a22a29e27fb4601]
|
||||
tool: 1
|
||||
---
|
||||
# Manejo de errores {#handling-errors}
|
||||
|
||||
Una herramienta puede fallar de dos maneras, y el SDK las trata de forma muy distinta.
|
||||
|
||||
Lanza una excepción ordinaria y la ve el **modelo**. Lanza `MCPError` y la ve el **protocolo**.
|
||||
|
||||
Esta página trata de cómo elegir.
|
||||
|
||||
## Un error que el modelo puede corregir {#an-error-the-model-can-fix}
|
||||
|
||||
Toma una herramienta que busca algo, y deja que la búsqueda falle:
|
||||
|
||||
```python title="server.py" hl_lines="11-12"
|
||||
--8<-- "docs_src/handling_errors/tutorial001.py"
|
||||
```
|
||||
|
||||
No hay nada de MCP en esas dos líneas. `get_author` lanza un `ValueError` común y corriente, como lo haría cualquier función de Python.
|
||||
|
||||
Llámala con un título que no esté en el catálogo y observa el resultado:
|
||||
|
||||
```python
|
||||
result.is_error # True
|
||||
result.content # [TextContent(text="Error executing tool get_author: No book titled 'Nothing' in the catalog.")]
|
||||
result.structured_content # None
|
||||
```
|
||||
|
||||
* La solicitud **tuvo éxito**. Hay un resultado; no se lanzó nada del lado de quien llama.
|
||||
* `is_error` es `True`, y el mensaje de tu excepción (con el nombre de la herramienta como prefijo) está en `content`, justo donde lee el modelo.
|
||||
* `structured_content` es `None`. Una llamada fallida no tiene valor devuelto que estructurar.
|
||||
|
||||
Esto es un **error de herramienta**, y es el comportamiento por defecto para *cualquier* excepción que lance tu herramienta. Además, casi siempre es lo que quieres.
|
||||
|
||||
El modelo es quien llama a tu herramienta. Él eligió los argumentos. Así que un error de herramienta es un turno de la conversación: el modelo lee *"No book titled 'Nothing' in the catalog."*, se da cuenta de que adivinó mal el título y vuelve a llamar con uno mejor. Escribiste un `raise` y obtuviste un agente que se corrige solo.
|
||||
|
||||
!!! tip
|
||||
Nunca devuelvas con `return` un mensaje de error desde una herramienta. Una cadena devuelta
|
||||
tiene `is_error=False`, así que para el modelo (y para toda interfaz de cliente) parece que la
|
||||
herramienta funcionó y que esa cadena era la respuesta. Usa `raise`. El indicador es la señal.
|
||||
|
||||
## Un error que el modelo no puede corregir {#an-error-the-model-cannot-fix}
|
||||
|
||||
Ahora cambia `ValueError` por `MCPError`.
|
||||
|
||||
```python title="server.py" hl_lines="1 3 14"
|
||||
--8<-- "docs_src/handling_errors/tutorial002.py"
|
||||
```
|
||||
|
||||
`MCPError` es el **error de protocolo** del SDK. Es la única excepción que el envoltorio de la herramienta *no* captura: se propaga, y toda la solicitud `tools/call` falla con un error JSON-RPC en lugar de un resultado.
|
||||
|
||||
```json
|
||||
{
|
||||
"code": -32602,
|
||||
"message": "No book titled 'Nothing' in the catalog."
|
||||
}
|
||||
```
|
||||
|
||||
* **No hay resultado**. No hay `content` ni `is_error`: nada que el modelo pueda leer.
|
||||
* En su lugar, el error lo recibe la aplicación **host**, igual que si la herramienta no existiera.
|
||||
* `code`, `message` y `data` llegan intactos. `INVALID_PARAMS` es `-32602`; `mcp.types` lo exporta, junto con los demás códigos de error JSON-RPC (`INVALID_REQUEST`, `INTERNAL_ERROR`, ...), como constantes para que nunca escribas un número mágico.
|
||||
|
||||
!!! check
|
||||
La misma búsqueda, el mismo fallo, pero ahora la llamada *lanza* una excepción del lado del cliente en lugar de devolver un resultado:
|
||||
|
||||
```text
|
||||
mcp.shared.exceptions.MCPError: No book titled 'Nothing' in the catalog.
|
||||
```
|
||||
|
||||
La primera versión le entregaba al modelo una frase a la que podía reaccionar. Esta no le
|
||||
entrega nada. Para `get_author` eso es estrictamente peor, y de eso trata la siguiente sección.
|
||||
|
||||
## Cuál lanzar {#which-one-to-raise}
|
||||
|
||||
Los dos caminos responden a dos preguntas distintas.
|
||||
|
||||
* **Lanza cualquier excepción** ante un fallo de *ejecución*: lo que tu herramienta intentó hacer no funcionó. El modelo eligió la llamada, así que el modelo debería ver la consecuencia y tener la oportunidad de recuperarse. Un título mal escrito, una API externa que agotó el tiempo de espera, una fila que no existe: todos son errores de herramienta.
|
||||
* **Lanza `MCPError`** cuando debe rechazarse la *solicitud misma*: al cliente le falta una capacidad de la que depende tu herramienta, el servidor no está en condiciones de atender a nadie, quien llama se saltó un paso obligatorio. Ningún reintento del modelo arregla nada de eso, así que no se gana nada entregándole el mensaje.
|
||||
|
||||
Una sola pregunta lo decide: **¿podría haberlo evitado un modelo más inteligente?** Sí -> excepción ordinaria. No -> `MCPError`.
|
||||
|
||||
Según ese criterio, la segunda versión de `get_author` eligió mal: un título mejor lo arregla, así que el modelo merecía ver el mensaje. Está ahí para mostrarte el mecanismo, no para recomendarlo.
|
||||
|
||||
!!! info
|
||||
`MCPError` se importa con `from mcp import MCPError` y recibe `code`, `message` y un payload
|
||||
opcional `data`. Lo que pongas en ellos es lo que recibe el cliente: el SDK reenvía un
|
||||
`MCPError` lanzado tal cual, en lugar de sanearlo.
|
||||
|
||||
## Un recurso que no existe {#a-resource-that-doesnt-exist}
|
||||
|
||||
Los recursos trazan la misma línea, e incluyen una excepción con nombre propio para el caso común.
|
||||
|
||||
```python title="server.py" hl_lines="2 13"
|
||||
--8<-- "docs_src/handling_errors/tutorial003.py"
|
||||
```
|
||||
|
||||
`books://{title}` es una **plantilla**. Coincide con *cualquier* título, así que "la URI está bien formada" y "el libro existe" son dos preguntas distintas, y solo tu función puede responder la segunda.
|
||||
|
||||
Cuando no pueda, lanza `ResourceNotFoundError`. El SDK lo convierte en el error de protocolo que la especificación asigna a un recurso que falta: `-32602` con la URI solicitada en `data`, para que el cliente sepa *cuál* lectura falló.
|
||||
|
||||
```json
|
||||
{
|
||||
"code": -32602,
|
||||
"message": "No book titled 'Nothing' in the catalog.",
|
||||
"data": {"uri": "books://Nothing"}
|
||||
}
|
||||
```
|
||||
|
||||
Fíjate en que aquí no hay un resultado a medias con `is_error=True`. La lectura de un recurso devuelve contenido o falla: los recursos solo tienen el camino del protocolo. Las plantillas y todo lo demás sobre recursos están en **[Recursos](resources.md)**.
|
||||
|
||||
## Errores que nunca lanzas {#errors-you-never-raise}
|
||||
|
||||
Un argumento incorrecto nunca llega a tu función.
|
||||
|
||||
Envíale a `get_author` un `title` que no sea una cadena y el SDK lo rechaza contra el esquema de entrada **antes** de llamarte, como el mismo tipo de error de herramienta con `is_error=True` que el modelo puede leer y corregir. **[Herramientas](tools.md)** muestra el mismo rechazo con una restricción `Field(le=50)`.
|
||||
|
||||
Eso significa toda una clase de sentencias `raise` que no escribes: no vuelvas a validar tus propias anotaciones de tipo.
|
||||
|
||||
!!! info
|
||||
Todo lo de esta página es lo que ve un **cliente**, y el `Client` en memoria con el que
|
||||
escribirás pruebas ve exactamente lo mismo. Ni siquiera `raise_exceptions=True` convierte un
|
||||
error de herramienta de nuevo en un traceback: para cuando ese indicador podría actuar, tu
|
||||
excepción ya es el resultado con `is_error=True`. Haz las aserciones sobre el resultado.
|
||||
**[Pruebas](../get-started/testing.md)** cubre el patrón.
|
||||
|
||||
## Resumen {#recap}
|
||||
|
||||
* Lanza **cualquier excepción** en una herramienta -> la llamada devuelve `is_error=True` con tu mensaje en `content`. El modelo lo lee y puede reintentar. Este es el comportamiento por defecto.
|
||||
* Lanza **`MCPError`** -> la llamada misma falla con un error JSON-RPC. El modelo no ve nada; el host se encarga. `code`, `message` y `data` sobreviven intactos.
|
||||
* La pregunta decisiva: *¿podría haberlo evitado un modelo más inteligente?* Sí -> excepción. No -> `MCPError`.
|
||||
* `ResourceNotFoundError` desde un handler de recurso -> el `-32602` del protocolo, con la URI en `data`.
|
||||
* Los argumentos incorrectos se rechazan contra el esquema antes de que se ejecute tu función; para esos no usas `raise`.
|
||||
* `from mcp import MCPError`; las constantes de códigos de error vienen de `mcp.types`.
|
||||
|
||||
Errores resueltos. Eso es todo lo que un servidor *expone*. Lo que cada handler puede leer, y hacer de vuelta hacia el cliente mientras se ejecuta, es la siguiente sección: **[Dentro de tu handler](../handlers/index.md)**.
|
||||
|
||||
El texto exacto de los errores del SDK que es más probable que encuentres, qué significa cada uno y la solución de un solo paso para cada uno están en **[Solución de problemas](../troubleshooting.md)**.
|
||||
35
i18n/es/pages/servers/index.md
Normal file
35
i18n/es/pages/servers/index.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
translation:
|
||||
sections: [09defc170a0da89d]
|
||||
tool: 1
|
||||
---
|
||||
# Servidores {#servers}
|
||||
|
||||
Un `MCPServer` expone tres primitivas a un cliente conectado. Se diferencian en quién
|
||||
decide usarlas:
|
||||
|
||||
* Una **[herramienta](tools.md)** es una acción que el *modelo* elige y llama. Esta es
|
||||
la página que la mayoría quiere leer primero, y
|
||||
**[Salida estructurada](structured-output.md)** es su referencia complementaria:
|
||||
todo sobre la forma de lo que devuelve una herramienta.
|
||||
* Un **[recurso](resources.md)** son datos de solo lectura que la *aplicación*
|
||||
decide leer. **[Plantillas de URI](uri-templates.md)** es su referencia
|
||||
complementaria: la sintaxis de direccionamiento completa y las reglas de seguridad de rutas.
|
||||
* Un **[prompt](prompts.md)** es una plantilla de mensaje que una *persona* invoca por
|
||||
nombre, desde un menú o un comando de barra.
|
||||
|
||||
En torno a las tres primitivas, el resto de lo que declara un servidor:
|
||||
|
||||
* **[Autocompletado](completions.md)** es el autocompletado del lado del servidor para los
|
||||
argumentos de prompts y de plantillas de recursos.
|
||||
* **[Imágenes, audio e iconos](media.md)** cubre todo lo que una herramienta puede
|
||||
devolver además de texto, y los iconos que un cliente muestra junto al servidor.
|
||||
* **[Manejo de errores](handling-errors.md)** explica la diferencia entre un
|
||||
error del que el modelo puede recuperarse y uno que nunca debe ver.
|
||||
|
||||
Cada página de esta sección se sostiene por sí sola; ve directamente a la que necesites. Si aún no
|
||||
has creado un servidor, empieza por **[Primeros pasos](../get-started/first-steps.md)**.
|
||||
|
||||
Lo que ocurre *dentro* de las funciones que registras (el `Context`, la inyección de dependencias,
|
||||
pedirle al usuario más información a mitad de la llamada) es la siguiente sección,
|
||||
**[Dentro de tu handler](../handlers/index.md)**.
|
||||
122
i18n/es/pages/servers/media.md
Normal file
122
i18n/es/pages/servers/media.md
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
---
|
||||
translation:
|
||||
sections: [496394d24d221bf1, 4ceb4591180dc6c3, 0fd63e4682d02e0c, 969ede0bd3686a16, 043f526230dd243d, 6ee3e9bcfd24047a]
|
||||
tool: 1
|
||||
---
|
||||
# Multimedia {#media}
|
||||
|
||||
El texto no es lo único que puede devolver una herramienta.
|
||||
|
||||
El SDK incluye dos utilidades para resultados binarios (**`Image`** y **`Audio`**) y un tipo **`Icon`** para darles a tu servidor, herramientas, recursos y prompts una cara visible en la interfaz del cliente.
|
||||
|
||||
## Devolver una imagen {#returning-an-image}
|
||||
|
||||
Anota el tipo de retorno como `Image`, apúntalo a un archivo y devuélvelo:
|
||||
|
||||
```python title="server.py" hl_lines="8 12 14"
|
||||
--8<-- "docs_src/media/tutorial001.py"
|
||||
```
|
||||
|
||||
* `Image` acepta exactamente uno de los dos: `path` (un archivo que leer) o `data` (bytes en bruto).
|
||||
* El tipo MIME que ve el cliente se deduce del sufijo: `logo.png` se anuncia como `image/png`.
|
||||
* No hay nada especial en que sea un logo. Cualquier PNG junto a `server.py` sirve: una gráfica que generó tu código, un diagrama, una foto.
|
||||
|
||||
`Image` es una comodidad del SDK, no un tipo del protocolo. En lo que se transmite, el valor devuelto se convierte en un bloque **`ImageContent`** (los bytes del archivo codificados en base64, más el tipo MIME):
|
||||
|
||||
```python
|
||||
result.content # [ImageContent(type="image", data="iVBORw0KGgoAAAANSUhEUg...", mime_type="image/png")]
|
||||
result.structured_content # None
|
||||
```
|
||||
|
||||
Dos cosas que notar:
|
||||
|
||||
* `data` está en base64. Nunca tocaste los bytes; el SDK leyó el archivo e hizo la codificación.
|
||||
* `structured_content` es `None`. Un `Image` es contenido para que lo mire el modelo, no datos para que los analice la aplicación: no hay esquema de salida. (Compara con **[Salida estructurada](structured-output.md)**, donde la anotación de retorno *es* el esquema.)
|
||||
|
||||
!!! info
|
||||
`ImageContent` y `AudioContent` viven en `mcp.types`, justo al lado del `TextContent`
|
||||
en el que se convierte un resultado `str` simple (**[Herramientas](tools.md)**). El resultado de una herramienta es una lista de bloques de contenido; `Image` y `Audio` son
|
||||
la forma más corta de producir los dos tipos binarios.
|
||||
|
||||
### Pruébalo {#try-it}
|
||||
|
||||
Coloca cualquier PNG junto a `server.py`, llámalo `logo.png` y ejecuta:
|
||||
|
||||
```console
|
||||
uv run mcp dev server.py
|
||||
```
|
||||
|
||||
Abre la pestaña **Tools** y llama a `logo`. El resultado no es una cadena: es un bloque de contenido `image`, y el Inspector muestra tu imagen. Todo lo que hay entre el archivo en disco y los píxeles en pantalla lo hizo el SDK.
|
||||
|
||||
## Devolver audio {#returning-audio}
|
||||
|
||||
`Audio` tiene la misma forma. Deja `logo.png` donde estaba y pon cualquier WAV a su lado como `chime.wav`:
|
||||
|
||||
```python title="server.py" hl_lines="18-21"
|
||||
--8<-- "docs_src/media/tutorial002.py"
|
||||
```
|
||||
|
||||
El resultado es un bloque **`AudioContent`**:
|
||||
|
||||
```python
|
||||
result.content # [AudioContent(type="audio", data="UklGR...", mime_type="audio/wav")]
|
||||
result.structured_content # None
|
||||
```
|
||||
|
||||
Lo mismo: entra un archivo en disco, salen base64 y un tipo MIME, sin esquema de salida.
|
||||
|
||||
## Bytes o un archivo {#bytes-or-a-file}
|
||||
|
||||
Ambas utilidades aceptan también `data=` (bytes en bruto) en lugar de `path=`. Ese es el modo para los bytes que nunca vinieron de un archivo propio: una columna de base de datos, una respuesta HTTP, algo que Pillow acaba de dibujar:
|
||||
|
||||
```python title="server.py" hl_lines="14 15"
|
||||
--8<-- "docs_src/media/tutorial003.py"
|
||||
```
|
||||
|
||||
Con `path=` no hay nada que declarar: el archivo se lee cuando se construye el resultado y el tipo MIME se deduce del sufijo:
|
||||
|
||||
* `Image`: `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`.
|
||||
* `Audio`: `.wav`, `.mp3`, `.ogg`, `.flac`, `.aac`, `.m4a`.
|
||||
|
||||
Un sufijo que no reconoce recurre a `application/octet-stream`.
|
||||
|
||||
!!! check
|
||||
Con `data=` no hay nombre de archivo, así que no hay nada de lo que deducir. Olvida `format=` y
|
||||
el SDK recurre a un valor por defecto: `image/png` para imágenes, `audio/wav` para audio. Construye un
|
||||
`Audio` así a partir de bytes MP3 y al cliente se le dice `mime_type="audio/wav"`, y entonces
|
||||
falla fielmente al decodificarlo. Cuando pases `data=`, pasa `format=`.
|
||||
|
||||
## Iconos {#icons}
|
||||
|
||||
Un `Icon` es metadatos, no contenido. No lleva la imagen; apunta a una con una URI, y el cliente puede descargarla y mostrarla junto al nombre de tu servidor, una herramienta, un recurso o un prompt.
|
||||
|
||||
```python title="server.py" hl_lines="4-5 7 10 16"
|
||||
--8<-- "docs_src/media/tutorial004.py"
|
||||
```
|
||||
|
||||
* `src` es una URI que el cliente puede resolver: `https:`, o una URI `data:` si quieres el icono incrustado sin una descarga extra.
|
||||
* `mime_type` y `sizes` (`"48x48"`, o `"any"` para un formato escalable) permiten al cliente elegir el adecuado cuando ofreces varios.
|
||||
* `theme="light"` o `theme="dark"` marca un icono para un esquema de color.
|
||||
|
||||
El mismo argumento nombrado `icons=[...]` lo aceptan `MCPServer(...)`, `@mcp.tool()`, `@mcp.resource()` y `@mcp.prompt()`.
|
||||
|
||||
### Dónde los ve un cliente {#where-a-client-sees-them}
|
||||
|
||||
Los iconos viajan con lo que decoran. Los del servidor llegan cuando el cliente se conecta, en `client.server_info` (opcional en conexiones de la generación 2026, así que acota el tipo primero):
|
||||
|
||||
```python
|
||||
assert client.server_info is not None # python-sdk servers identify themselves by default
|
||||
client.server_info.icons # [Icon(src="https://example.com/brand-kit.png", mime_type="image/png", sizes=["48x48"])]
|
||||
```
|
||||
|
||||
Los iconos de una herramienta están en el objeto `Tool` de `tools/list`, los de un recurso en el `Resource` de `resources/list`, los de un prompt en el `Prompt` de `prompts/list`. El campo siempre se llama `icons`.
|
||||
|
||||
## Resumen {#recap}
|
||||
|
||||
* Devuelve un `Image` o un `Audio` desde una herramienta y el cliente recibe un bloque `ImageContent` / `AudioContent`: tus bytes codificados en base64, con un tipo MIME.
|
||||
* Constrúyelo a partir de un `path=` y deja que el sufijo decida el tipo MIME, o a partir de `data=` en memoria más un `format=` explícito.
|
||||
* Los resultados multimedia no llevan `structured_content` ni esquema de salida.
|
||||
* Un `Icon` es un puntero: una URI `src` más `mime_type`, `sizes` y `theme` opcionales.
|
||||
* `icons=[...]` funciona en el servidor, en herramientas, en recursos y en prompts, y los clientes los encuentran en los objetos correspondientes.
|
||||
|
||||
Eso es todo lo que una herramienta puede poner *dentro* de un resultado. Lo que ocurre cuando una herramienta *falla* (y quién debería enterarse) está en **[Manejo de errores](handling-errors.md)**.
|
||||
156
i18n/es/pages/servers/prompts.md
Normal file
156
i18n/es/pages/servers/prompts.md
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
---
|
||||
translation:
|
||||
sections: [d65c098f37f5b6c3, dd0c2724d6f2877e, 6835bb3570c6714c, ffe823cb0fedd488, f33651add1b59094]
|
||||
tool: 1
|
||||
---
|
||||
# Prompts {#prompts}
|
||||
|
||||
Un **prompt** es una plantilla de mensajes que elige el usuario.
|
||||
|
||||
Las herramientas son para el modelo. Un prompt es lo contrario: el usuario elige uno en un menú de su cliente (un comando de barra, un botón), completa sus argumentos y los mensajes renderizados entran en la conversación como si los hubiera escrito él mismo.
|
||||
|
||||
Para declarar uno, pon `@mcp.prompt()` en una función que devuelva el texto.
|
||||
|
||||
## Tu primer prompt {#your-first-prompt}
|
||||
|
||||
```python title="server.py" hl_lines="6-9"
|
||||
--8<-- "docs_src/prompts/tutorial001.py"
|
||||
```
|
||||
|
||||
El SDK lee las mismas tres cosas que lee de una herramienta:
|
||||
|
||||
* El **nombre** es el nombre de la función: `review_code`.
|
||||
* La **descripción** que muestra el cliente es el docstring: `Review a piece of code.`
|
||||
* Los **argumentos** salen de los parámetros. `code` no tiene valor por defecto, así que es obligatorio.
|
||||
|
||||
Esto es lo que recibe un cliente de `prompts/list`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "review_code",
|
||||
"description": "Review a piece of code.",
|
||||
"arguments": [
|
||||
{"name": "code", "required": true}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Aquí no hay JSON Schema. Los argumentos de un prompt son una lista plana de **valores de cadena con nombre**: un formulario que rellena una persona, no un payload que construye un modelo.
|
||||
|
||||
### Renderizarlo {#rendering-it}
|
||||
|
||||
El cliente renderiza la plantilla con `prompts/get`, pasando los argumentos. Tu función se ejecuta y el `str` que devuelves se convierte en **un único mensaje de usuario**:
|
||||
|
||||
```json
|
||||
{
|
||||
"description": "Review a piece of code.",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": {
|
||||
"type": "text",
|
||||
"text": "Please review this code:\n\ndef add(a, b): return a + b"
|
||||
}
|
||||
}
|
||||
],
|
||||
"resultType": "complete"
|
||||
}
|
||||
```
|
||||
|
||||
Esa es toda la vida de un prompt: se lista por nombre, se renderiza a demanda y se coloca en el chat.
|
||||
|
||||
!!! check
|
||||
`required` se comprueba antes de que se ejecute tu función. Renderiza `review_code` sin `code` y la
|
||||
propia solicitud falla con un error JSON-RPC (código `-32603`):
|
||||
|
||||
```text
|
||||
mcp.shared.exceptions.MCPError: Internal server error
|
||||
```
|
||||
|
||||
No hay un resultado de error al estilo de las herramientas que devolver a un modelo, porque no hay
|
||||
ningún modelo en el circuito: la llamada lanza una excepción. El motivo
|
||||
(`Missing required arguments: {'code'}`) queda en el log del servidor.
|
||||
|
||||
### Pruébalo {#try-it}
|
||||
|
||||
Ejecuta el servidor con el MCP Inspector:
|
||||
|
||||
```console
|
||||
uv run mcp dev server.py
|
||||
```
|
||||
|
||||
Abre la pestaña **Prompts** y selecciona `review_code`. El Inspector dibuja un formulario con un campo obligatorio `code`. Rellénalo, renderízalo y te devuelve exactamente el mensaje de usuario de arriba.
|
||||
|
||||
## Más de un mensaje {#more-than-one-message}
|
||||
|
||||
Una revisión de código es un mensaje. Una sesión de depuración es una conversación, y un prompt puede sembrarla entera.
|
||||
|
||||
Devuelve una lista de mensajes en lugar de un `str`:
|
||||
|
||||
```python title="server.py" hl_lines="2 13-20"
|
||||
--8<-- "docs_src/prompts/tutorial002.py"
|
||||
```
|
||||
|
||||
* `UserMessage` y `AssistantMessage` vienen de `mcp.server.mcpserver.prompts.base`. Dales un `str` y lo envuelven en `TextContent` por ti. El rol es el nombre de la clase.
|
||||
* `Message` es su base común. Úsala como anotación de retorno.
|
||||
|
||||
Renderizar `debug_error` ahora produce tres mensajes, en orden:
|
||||
|
||||
```json
|
||||
{
|
||||
"description": "Start a debugging conversation.",
|
||||
"messages": [
|
||||
{"role": "user", "content": {"type": "text", "text": "I'm seeing this error:"}},
|
||||
{"role": "user", "content": {"type": "text", "text": "TypeError: 'int' object is not iterable"}},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": {"type": "text", "text": "I'll help debug that. What have you tried so far?"}
|
||||
}
|
||||
],
|
||||
"resultType": "complete"
|
||||
}
|
||||
```
|
||||
|
||||
Fíjate en el último. Rellenar de antemano un turno `assistant` es la forma de orientar la *siguiente* respuesta del modelo sin que el usuario tenga que escribir esa orientación.
|
||||
|
||||
## Títulos y descripciones de argumentos {#titles-and-argument-descriptions}
|
||||
|
||||
`review_code` es un nombre de función, no una etiqueta. Dale al cliente algo mejor que poner en el botón y describe cada argumento para que el formulario se explique solo:
|
||||
|
||||
```python title="server.py" hl_lines="10-13"
|
||||
--8<-- "docs_src/prompts/tutorial003.py"
|
||||
```
|
||||
|
||||
* `title="Code review"` es el nombre legible para personas, exactamente igual que el `title` de una herramienta.
|
||||
* `Annotated[str, Field(description=...)]` es el mismo patrón que usa **[Herramientas](tools.md)** para describir los parámetros de una herramienta. Aquí la descripción va al argumento en lugar de a un esquema.
|
||||
* `language` tiene valor por defecto, así que deja de ser obligatorio.
|
||||
|
||||
La entrada de `prompts/list` ahora lleva todo lo que un cliente necesita para dibujar un buen formulario:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "review_code",
|
||||
"title": "Code review",
|
||||
"description": "Review a piece of code.",
|
||||
"arguments": [
|
||||
{"name": "code", "description": "The code to review.", "required": true},
|
||||
{"name": "language", "description": "The language the code is written in.", "required": false}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
!!! info
|
||||
Si has leído **[Herramientas](tools.md)**, ya sabes todo lo de esta página. El mismo decorador, el mismo
|
||||
docstring como descripción, el mismo `Annotated`/`Field`. Lo único que cambia es quién
|
||||
lo dispara (el usuario) y adónde va el resultado (a la conversación).
|
||||
|
||||
## Resumen {#recap}
|
||||
|
||||
* `@mcp.prompt()` en una función la convierte en un prompt. El nombre sale de la función y la descripción del docstring.
|
||||
* Los prompts están **controlados por el usuario**: el cliente los lista, el usuario elige uno y completa los argumentos.
|
||||
* Los argumentos son una lista plana de cadenas con nombre (sin esquema). Un parámetro con valor por defecto es opcional.
|
||||
* Devuelve un `str` y se convierte en un mensaje de usuario. Devuelve una lista de `UserMessage` / `AssistantMessage` para sembrar una conversación de varios turnos.
|
||||
* `title=` y `Field(description=...)` son lo que un cliente pone en su interfaz.
|
||||
* Un argumento obligatorio que falta hace fallar toda la solicitud. No hay un resultado de error por prompt.
|
||||
|
||||
El autocompletado en el servidor de los argumentos de un prompt (o de una plantilla de recurso) está en **[Autocompletado](completions.md)**.
|
||||
146
i18n/es/pages/servers/resources.md
Normal file
146
i18n/es/pages/servers/resources.md
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
---
|
||||
translation:
|
||||
sections: [09df998c2a799f78, 0cf131146d16d4f9, 4e6b91e3f8025346, 8fe4eef576db17ed, 0d0d1ed43e3d0a53]
|
||||
tool: 1
|
||||
---
|
||||
# Recursos {#resources}
|
||||
|
||||
Un **recurso** es un dato que expones para que la aplicación lo lea.
|
||||
|
||||
Esa es la diferencia. Una herramienta es algo que el **modelo** decide llamar. Un recurso es algo que la **aplicación** decide cargar (un archivo de configuración, un registro, un documento) y poner delante del modelo como contexto.
|
||||
|
||||
Declaras uno poniendo `@mcp.resource(uri)` sobre una función normal de Python.
|
||||
|
||||
## Tu primer recurso {#your-first-resource}
|
||||
|
||||
```python title="server.py" hl_lines="6-8"
|
||||
--8<-- "docs_src/resources/tutorial001.py"
|
||||
```
|
||||
|
||||
Tiene la misma forma que una herramienta, con un añadido: el **URI**. A los recursos se accede por dirección, no por nombre. Un cliente pide `config://app`, nunca `get_config`.
|
||||
|
||||
El SDK sigue leyendo el resto de la función:
|
||||
|
||||
* El **nombre** es el nombre de la función: `get_config`.
|
||||
* La **descripción** que ve el cliente es el docstring.
|
||||
* El **contenido** es lo que devuelvas.
|
||||
|
||||
Durante `resources/list` el cliente recibe esto:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "get_config",
|
||||
"uri": "config://app",
|
||||
"description": "The active shop configuration.",
|
||||
"mimeType": "text/plain"
|
||||
}
|
||||
```
|
||||
|
||||
Y cuando lee `config://app`, tu función se ejecuta y el valor devuelto regresa como texto:
|
||||
|
||||
```python
|
||||
result.contents # [TextResourceContents(uri="config://app", mime_type="text/plain", text="theme=dark\nlanguage=en")]
|
||||
```
|
||||
|
||||
!!! tip
|
||||
Listar es barato. Tu función **no** se llama durante `resources/list`, solo durante
|
||||
`resources/read`, y solo para el URI que se pidió. Expón mil recursos
|
||||
y pagas por los que alguien abre.
|
||||
|
||||
### Pruébalo {#try-it}
|
||||
|
||||
Ejecuta el servidor con el MCP Inspector:
|
||||
|
||||
```console
|
||||
uv run mcp dev server.py
|
||||
```
|
||||
|
||||
Abre la URL que imprime y ve a la pestaña **Resources**. `config://app` está en la lista con su descripción. Haz clic en él y el Inspector lo lee: ahí están tus dos líneas de configuración.
|
||||
|
||||
## Plantillas de recurso {#resource-templates}
|
||||
|
||||
Un URI por registro no escala. Pon un **marcador de posición** en el URI y un parámetro correspondiente en la función:
|
||||
|
||||
```python title="server.py" hl_lines="12-13"
|
||||
--8<-- "docs_src/resources/tutorial002.py"
|
||||
```
|
||||
|
||||
`{user_id}` en el URI, `user_id: str` en la función. Ese es todo el contrato.
|
||||
|
||||
Ahora es una **plantilla de recurso**, y se muda: sale de `resources/list` y aparece en `resources/templates/list`, como un patrón en lugar de una dirección:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "get_user_profile",
|
||||
"uriTemplate": "users://{user_id}/profile",
|
||||
"description": "A customer's profile.",
|
||||
"mimeType": "text/plain"
|
||||
}
|
||||
```
|
||||
|
||||
El cliente rellena el marcador de posición y lee un URI concreto: `users://42/profile`, `users://ada/profile`. Una sola función responde a todos, con el valor coincidente pasado como `user_id`:
|
||||
|
||||
```python
|
||||
result.contents # [TextResourceContents(uri="users://42/profile", text="User 42: 12 orders since 2021.")]
|
||||
```
|
||||
|
||||
Fíjate en el `uri` del resultado. Es el URI **concreto** que pidió el cliente, no la plantilla.
|
||||
|
||||
!!! check
|
||||
Los marcadores de posición y los parámetros tienen que coincidir. Renombra el parámetro de la función a
|
||||
`user` mientras el URI sigue diciendo `{user_id}` y el decorador lo rechaza **en tiempo de importación**,
|
||||
antes de que ningún cliente se acerque:
|
||||
|
||||
```text
|
||||
ValueError: Mismatch between URI parameters {'user_id'} and function parameters {'user'}
|
||||
```
|
||||
|
||||
Una discrepancia así solo puede ser un bug, así que el SDK hace imposible arrancar el servidor con una.
|
||||
|
||||
La sintaxis de los marcadores de posición es [RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570): `{+path}` para valores de varios segmentos, `{?q,lang}` para parámetros de consulta opcionales, y más. Por defecto, el SDK también aplica comprobaciones de seguridad de rutas a los valores extraídos. Consulta **[Plantillas de URI y seguridad de rutas](uri-templates.md)** para la referencia completa.
|
||||
|
||||
`get_user_profile` también puede recibir un parámetro anotado como `Context`. El SDK lo inyecta sin tratarlo nunca como un parámetro del URI, y la página **[El Context](../handlers/context.md)** explica lo que te ofrece.
|
||||
|
||||
## Lo que devuelves {#what-you-return}
|
||||
|
||||
No estás limitado a `str`. Dale a cada recurso un `mime_type` y devuelve lo que encaje:
|
||||
|
||||
```python title="server.py" hl_lines="8-9 14-15 20-21"
|
||||
--8<-- "docs_src/resources/tutorial003.py"
|
||||
```
|
||||
|
||||
* `readme` devuelve un `str`, así que se envía tal cual. Es el caso habitual.
|
||||
* `catalog_stats` devuelve un `dict`, así que el SDK lo serializa a **texto JSON** por ti:
|
||||
|
||||
```json
|
||||
{
|
||||
"books": 1204,
|
||||
"authors": 391
|
||||
}
|
||||
```
|
||||
|
||||
* `placeholder_cover` devuelve `bytes`, así que el cliente recibe un `BlobResourceContents` en lugar de un `TextResourceContents`, con tus bytes codificados en base64 en su campo `blob`.
|
||||
|
||||
La misma regla vale para cualquier otra cosa serializable a JSON: una lista, un modelo de Pydantic, una dataclass. Si no es `str` ni `bytes`, se convierte en JSON.
|
||||
|
||||
El `mime_type` lo declaras tú, y es `text/plain` por defecto. El SDK nunca inspecciona lo que devuelves para adivinarlo, así que un recurso `dict` sin etiquetar se sigue anunciando como texto plano.
|
||||
|
||||
!!! tip
|
||||
`@mcp.resource()` también acepta `name=`, `title=` y `description=` cuando no quieres
|
||||
derivarlos de la función. Y cuando no hay ninguna función que escribir,
|
||||
`mcp.server.mcpserver.resources` tiene clases `Resource` listas para usar (`TextResource`,
|
||||
`BinaryResource`, `FileResource`, `HttpResource`, `DirectoryResource`) que registras
|
||||
con `mcp.add_resource(...)`.
|
||||
|
||||
Un cliente también puede **suscribirse** a un recurso y recibir una notificación cuando cambie; esa es la mitad de la historia que le toca al cliente y vive en **[El cliente](../client/index.md)**.
|
||||
|
||||
## Resumen {#recap}
|
||||
|
||||
* `@mcp.resource(uri)` sobre una función la convierte en un recurso. El URI es la dirección, el valor devuelto es el contenido, el docstring es la descripción.
|
||||
* Un `{placeholder}` en el URI la convierte en una **plantilla**: se lista en `resources/templates/list` y una sola función sirve todos los URI que coinciden.
|
||||
* Los nombres de los marcadores de posición deben ser iguales a los nombres de los parámetros de la función. Equivócate y lo descubres en tiempo de importación, no en producción.
|
||||
* Tu función se ejecuta cuando el recurso se **lee**, no cuando se lista.
|
||||
* `str` se convierte en texto, `bytes` en un blob en base64, cualquier otra cosa en texto JSON. Con `mime_type=` lo etiquetas.
|
||||
* Las herramientas son para que el modelo actúe. Los recursos son para que la aplicación lea.
|
||||
|
||||
La tercera primitiva, la que una persona elige de un menú, son los **[Prompts](prompts.md)**.
|
||||
250
i18n/es/pages/servers/structured-output.md
Normal file
250
i18n/es/pages/servers/structured-output.md
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
---
|
||||
translation:
|
||||
sections: [a838d57f003aed44, 857d03886a0137ed, 42d9efcb9f542867, 2290ff08435b5573, e866c192e11d1c14, 6cdbad079f7b47f0, d4b607372fb28b51, 18dbf726ac45e0b7, c6f7d2a148aa49f4, c851964bb3301907, d715db6f8dccc9cc, ef86634aa70498a7]
|
||||
tool: 1
|
||||
---
|
||||
# Salida estructurada {#structured-output}
|
||||
|
||||
Una herramienta que devuelve un simple `str` produce el resultado dos veces: como texto en `content` y como `{"result": "..."}` en `structured_content`.
|
||||
|
||||
Esta página trata de ese segundo canal: de dónde sale, todas las formas que puede tomar y cómo el SDK garantiza que sea fiel.
|
||||
|
||||
La versión corta: **la anotación del tipo de retorno es el esquema de salida**. Ya la escribiste.
|
||||
|
||||
## El esquema de salida {#the-output-schema}
|
||||
|
||||
```python title="server.py" hl_lines="9"
|
||||
--8<-- "docs_src/structured_output/tutorial001.py"
|
||||
```
|
||||
|
||||
La línea que importa es la firma: `-> int`.
|
||||
|
||||
Gracias a ella, la herramienta que el SDK envía durante `tools/list` lleva un `output_schema` junto al esquema de entrada que construye a partir de tus parámetros (de ese se ocupa **[Herramientas](tools.md)**):
|
||||
|
||||
```json
|
||||
{
|
||||
"properties": {
|
||||
"result": {"title": "Result", "type": "integer"}
|
||||
},
|
||||
"required": ["result"],
|
||||
"title": "get_temperatureOutput",
|
||||
"type": "object"
|
||||
}
|
||||
```
|
||||
|
||||
Un `int` suelto no es un objeto JSON, así que el SDK lo **envuelve** en `{"result": ...}`. Llama a la herramienta y se llenan los dos canales:
|
||||
|
||||
```python
|
||||
result.content # [TextContent(text="17")]
|
||||
result.structured_content # {"result": 17}
|
||||
```
|
||||
|
||||
Todos los escalares reciben el mismo envoltorio: `str`, `int`, `float`, `bool`, `bytes`, `None`.
|
||||
|
||||
## Dos canales {#two-channels}
|
||||
|
||||
¿Por qué enviar el mismo valor dos veces?
|
||||
|
||||
* `content` es para el **modelo**. Un modelo de lenguaje lee texto; es la única parte del resultado que ve.
|
||||
* `structured_content` es para la **aplicación** dentro de la que se ejecuta el modelo: código que quiere `17`, no una frase que contenga "17".
|
||||
* `output_schema` es el contrato entre ambos, publicado antes de que la herramienta se llame por primera vez.
|
||||
|
||||
Devuelves un único valor de Python. El SDK rellena los tres.
|
||||
|
||||
## Devolver un modelo {#return-a-model}
|
||||
|
||||
Declara la forma como un `BaseModel` de Pydantic y devuelve una instancia:
|
||||
|
||||
```python title="server.py" hl_lines="8-11 15"
|
||||
--8<-- "docs_src/structured_output/tutorial002.py"
|
||||
```
|
||||
|
||||
Ahora `WeatherData` **es** el esquema. Sin envoltorio, sin clave `result`:
|
||||
|
||||
```json
|
||||
{
|
||||
"properties": {
|
||||
"temperature": {"description": "Degrees Celsius.", "title": "Temperature", "type": "number"},
|
||||
"humidity": {"description": "Relative humidity, 0 to 1.", "title": "Humidity", "type": "number"},
|
||||
"conditions": {"title": "Conditions", "type": "string"}
|
||||
},
|
||||
"required": ["temperature", "humidity", "conditions"],
|
||||
"title": "WeatherData",
|
||||
"type": "object"
|
||||
}
|
||||
```
|
||||
|
||||
`structured_content` es el objeto, campo por campo:
|
||||
|
||||
```python
|
||||
result.structured_content # {"temperature": 16.2, "humidity": 0.83, "conditions": "Overcast"}
|
||||
```
|
||||
|
||||
Y el modelo no se queda fuera. El SDK serializa el mismo objeto como texto JSON para `content`:
|
||||
|
||||
```json
|
||||
{
|
||||
"temperature": 16.2,
|
||||
"humidity": 0.83,
|
||||
"conditions": "Overcast"
|
||||
}
|
||||
```
|
||||
|
||||
Fíjate en que el `Field(description=...)` de `temperature` y `humidity` acabó en el esquema. El mismo `Field` que describía tus **entradas** describe tus salidas.
|
||||
|
||||
!!! info
|
||||
Si has usado el `response_model` de FastAPI, esto ya lo conoces: un modelo de Pydantic como respuesta
|
||||
declarada, serializado y documentado por ti. La única diferencia es que aquí la anotación de retorno
|
||||
es toda la declaración.
|
||||
|
||||
## Un `TypedDict` {#a-typeddict}
|
||||
|
||||
No todas las formas merecen una clase. Un `TypedDict` produce el mismo esquema:
|
||||
|
||||
```python title="server.py" hl_lines="8"
|
||||
--8<-- "docs_src/structured_output/tutorial003.py"
|
||||
```
|
||||
|
||||
Un `TypedDict` es un `dict` normal en tiempo de ejecución, así que eso es lo que construyes y devuelves. El esquema, la validación y `structured_content` son idénticos a los de la versión con `BaseModel` (salvo las descripciones, para las que `TypedDict` no tiene sitio).
|
||||
|
||||
## Una dataclass {#a-dataclass}
|
||||
|
||||
Las dataclasses también funcionan, igual que cualquier clase normal cuyos atributos tengan anotaciones de tipo. El SDK construye internamente un modelo de Pydantic a partir de las anotaciones.
|
||||
|
||||
```python title="server.py" hl_lines="8-9"
|
||||
--8<-- "docs_src/structured_output/tutorial004.py"
|
||||
```
|
||||
|
||||
Tres formas de escribirlo, un solo esquema. Usa la que ya tenga tu código.
|
||||
|
||||
## Listas {#lists}
|
||||
|
||||
Un `list[...]` tampoco es un objeto JSON, así que recibe el envoltorio `{"result": ...}`, con tu tipo de elemento dentro como referencia en `$defs`:
|
||||
|
||||
```python title="server.py" hl_lines="15"
|
||||
--8<-- "docs_src/structured_output/tutorial005.py"
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"$defs": {
|
||||
"WeatherData": {
|
||||
"properties": {
|
||||
"temperature": {"title": "Temperature", "type": "number"},
|
||||
"humidity": {"title": "Humidity", "type": "number"},
|
||||
"conditions": {"title": "Conditions", "type": "string"}
|
||||
},
|
||||
"required": ["temperature", "humidity", "conditions"],
|
||||
"title": "WeatherData",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"result": {"items": {"$ref": "#/$defs/WeatherData"}, "title": "Result", "type": "array"}
|
||||
},
|
||||
"required": ["result"],
|
||||
"title": "get_forecastOutput",
|
||||
"type": "object"
|
||||
}
|
||||
```
|
||||
|
||||
Pide un pronóstico de dos días y `structured_content` es `{"result": [{...}, {...}]}`. `content` se convierte en **dos** bloques `TextContent`, uno por elemento: una lista se aplana para el modelo en lugar de volcarse como una sola cadena.
|
||||
|
||||
`tuple[...]`, las uniones y `Optional[...]` se envuelven de la misma manera.
|
||||
|
||||
## Diccionarios {#dictionaries}
|
||||
|
||||
`dict[str, ...]` es el único genérico que ya *es* un objeto JSON, así que no se envuelve:
|
||||
|
||||
```python title="server.py" hl_lines="9"
|
||||
--8<-- "docs_src/structured_output/tutorial006.py"
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"additionalProperties": {"type": "number"},
|
||||
"title": "get_temperaturesDictOutput",
|
||||
"type": "object"
|
||||
}
|
||||
```
|
||||
|
||||
```python
|
||||
result.structured_content # {"London": 16.2, "Reykjavik": 4.4}
|
||||
```
|
||||
|
||||
Las claves deben ser `str`. Un `dict[int, float]` no puede ser un objeto JSON, así que recurre al envoltorio `{"result": ...}`.
|
||||
|
||||
## Validación {#validation}
|
||||
|
||||
`output_schema` no es documentación. Lo que devuelva tu función **se valida contra él** antes de salir del servidor.
|
||||
|
||||
No lo notas mientras construyes el valor a mano: Pydantic ya se aseguró de que tu `WeatherData` fuera un `WeatherData`. Lo notas el día que los datos vienen de algún sitio que no controlas:
|
||||
|
||||
```python title="server.py" hl_lines="9 21"
|
||||
--8<-- "docs_src/structured_output/tutorial007.py"
|
||||
```
|
||||
|
||||
La anotación promete `WeatherData`. La respuesta del servicio externo dejó de enviar `humidity`.
|
||||
|
||||
!!! check
|
||||
Llama a `get_weather` y no le entrega al cliente en silencio un objeto medio vacío. La llamada falla,
|
||||
y las primeras líneas del error nombran el campo:
|
||||
|
||||
```text
|
||||
Error executing tool get_weather: 1 validation error for WeatherData
|
||||
humidity
|
||||
Field required [type=missing, input_value={'temperature': 16.2, 'conditions': 'Overcast'}, input_type=dict]
|
||||
```
|
||||
|
||||
Ese texto vuelve como resultado de la herramienta con `is_error=True`, así que el modelo sabe que la
|
||||
llamada falló en lugar de leer con toda confianza un tiempo que no existe.
|
||||
|
||||
Por cierto, devolver un `dict` normal desde una herramienta `-> WeatherData` está bien. Es exactamente lo que produjo `json.loads`. La validación se aplica al valor, no al tipo de Python.
|
||||
|
||||
## Desactivarlo {#opting-out}
|
||||
|
||||
A veces la anotación de retorno es para tu verificador de tipos, no para el protocolo. Pasa `structured_output=False` y la herramienta es solo texto:
|
||||
|
||||
```python title="server.py" hl_lines="6"
|
||||
--8<-- "docs_src/structured_output/tutorial008.py"
|
||||
```
|
||||
|
||||
Sin `output_schema`, sin envoltorio, sin validación. `structured_content` es `None` y `content` es la cadena que devolviste.
|
||||
|
||||
Lo contrario, `structured_output=True`, convierte la detección automática en un requisito: una herramienta cuyo tipo de retorno no pueda producir un esquema lanza una excepción al importar el módulo en lugar de recurrir al texto.
|
||||
|
||||
## Una clase sin anotaciones de tipo {#a-class-without-type-hints}
|
||||
|
||||
Hay una forma de acabar sin salida estructurada sin haberlo pedido: devolver una clase que **no tiene anotaciones en su cuerpo**.
|
||||
|
||||
```python title="server.py" hl_lines="6-9"
|
||||
--8<-- "docs_src/structured_output/tutorial009.py"
|
||||
```
|
||||
|
||||
`Station` asigna `name` y `online` dentro de `__init__`, pero la *clase* no declara nada. El SDK lee las anotaciones de la clase, no encuentra ninguna y desiste.
|
||||
|
||||
!!! warning
|
||||
Desiste **en silencio**. `output_schema` es `None`, `structured_content` es `None` y el texto
|
||||
que lee el modelo es el `repr` del objeto:
|
||||
|
||||
```text
|
||||
"<server.Station object at 0x7f539d75b230>"
|
||||
```
|
||||
|
||||
Ni error, ni aviso: una herramienta inútil. Mueve las anotaciones al cuerpo de la clase o pasa
|
||||
`structured_output=True`, que convierte esto en un error inmediato en cuanto se importa el módulo:
|
||||
`Function get_station: return type <class 'server.Station'> is not serializable for structured output`.
|
||||
|
||||
!!! tip
|
||||
¿Necesitas control total (construir el `CallToolResult` tú mismo o adjuntar un `_meta` que la
|
||||
aplicación pueda ver pero el modelo no)? Eso es **[El Server de bajo nivel](../advanced/low-level-server.md)**.
|
||||
|
||||
## Resumen {#recap}
|
||||
|
||||
* La **anotación del tipo de retorno** es el esquema de salida. Se publica en `tools/list` como `output_schema`.
|
||||
* Los escalares, las listas, las tuplas y las uniones se envuelven en `{"result": ...}`. Los modelos, los `TypedDict`, las dataclasses, las clases con anotaciones y `dict[str, ...]` ya son objetos y se quedan como están.
|
||||
* Cada resultado lleva `content` (texto, para el modelo) **y** `structured_content` (datos, para la aplicación).
|
||||
* Lo que devuelves se valida contra el esquema. Una discrepancia es un error de herramienta, no un resultado corrupto.
|
||||
* `structured_output=False` excluye una herramienta. Una clase sin anotaciones de tipo queda excluida en silencio; vigílalo.
|
||||
|
||||
Ahora dominas todo lo que una herramienta puede responder. A continuación, la segunda primitiva: **[Recursos](resources.md)**.
|
||||
177
i18n/es/pages/servers/tools.md
Normal file
177
i18n/es/pages/servers/tools.md
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
---
|
||||
translation:
|
||||
sections: [e4cc390d56573409, 8566e2b68594e9ad, 2c97b9f888398951, 048e5471dfa71aea, 3076b1e16ad95950, edbedf2a16e71311, 3d8ef8da89fa87c1, f6c0e02e6ea5a363]
|
||||
tool: 1
|
||||
---
|
||||
# Herramientas {#tools}
|
||||
|
||||
Una **herramienta** es una función a la que el modelo puede llamar.
|
||||
|
||||
Declaras una poniendo `@mcp.tool()` sobre una función de Python normal. Esa es toda la API.
|
||||
|
||||
## Tu primera herramienta {#your-first-tool}
|
||||
|
||||
```python title="server.py" hl_lines="6-8"
|
||||
--8<-- "docs_src/tools/tutorial001.py"
|
||||
```
|
||||
|
||||
Mira lo que escribiste. No hay esquemas, ni JSON, ni protocolo: solo una función. El SDK lee tres cosas de ella:
|
||||
|
||||
* El **nombre** de la herramienta es el nombre de la función: `search_books`.
|
||||
* La **descripción** que ve el modelo es el docstring: `Search the catalog by title or author.`
|
||||
* Los **argumentos** que el modelo puede pasar salen de las anotaciones de tipo: `query: str` y `limit: int`.
|
||||
|
||||
### El esquema de entrada {#the-input-schema}
|
||||
|
||||
A partir de esas anotaciones de tipo, el SDK genera un JSON Schema y lo envía al cliente durante `tools/list`:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"title": "Query", "type": "string"},
|
||||
"limit": {"title": "Limit", "type": "integer"}
|
||||
},
|
||||
"required": ["query", "limit"],
|
||||
"title": "search_booksArguments"
|
||||
}
|
||||
```
|
||||
|
||||
Ambos argumentos están en `required` porque ninguno tiene valor por defecto. Lo arreglarás en un momento. (Las claves `title` son artefactos de Pydantic; las propiedades, sus tipos y `required` son el contrato.)
|
||||
|
||||
!!! tip
|
||||
Aquí las anotaciones de tipo no son documentación. Son **el contrato**. Si un cliente envía `"limit": "ten"`,
|
||||
el SDK lo rechaza antes de que tu función llegue a ejecutarse.
|
||||
|
||||
### Lo que recibe el modelo {#what-the-model-gets-back}
|
||||
|
||||
Llama a la herramienta con `{"query": "dune", "limit": 5}` y el resultado tiene dos partes:
|
||||
|
||||
```python
|
||||
result.content # [TextContent(text="Found 3 books matching 'dune' (showing up to 5).")]
|
||||
result.structured_content # {'result': "Found 3 books matching 'dune' (showing up to 5)."}
|
||||
```
|
||||
|
||||
`content` es el texto que lee el **modelo**. `structured_content` son datos tipados para la **aplicación cliente**. Está ahí porque declaraste el tipo de retorno como `-> str`.
|
||||
|
||||
No te preocupes todavía por `structured_content`. Devuelve objetos reales de Python desde tus herramientas y ocurre lo correcto; la página **[Salida estructurada](structured-output.md)** trata justamente de eso.
|
||||
|
||||
### Pruébalo {#try-it}
|
||||
|
||||
Ejecuta el servidor con el MCP Inspector:
|
||||
|
||||
```console
|
||||
uv run mcp dev server.py
|
||||
```
|
||||
|
||||
Abre la URL que imprime, ve a la pestaña **Tools** y llama a `search_books`.
|
||||
|
||||
El Inspector muestra un formulario con un campo de texto obligatorio `query` y un campo numérico obligatorio `limit`. Construyó ese formulario a partir de tus anotaciones de tipo. Lo mismo hará cualquier otro cliente MCP.
|
||||
|
||||
## Argumentos opcionales {#optional-arguments}
|
||||
|
||||
Dale un valor por defecto a un parámetro y deja de ser obligatorio. Eso es todo. Es simplemente Python.
|
||||
|
||||
```python title="server.py" hl_lines="7"
|
||||
--8<-- "docs_src/tools/tutorial002.py"
|
||||
```
|
||||
|
||||
El esquema lo refleja:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"title": "Query", "type": "string"},
|
||||
"limit": {"default": 10, "title": "Limit", "type": "integer"}
|
||||
},
|
||||
"required": ["query"],
|
||||
"title": "search_booksArguments"
|
||||
}
|
||||
```
|
||||
|
||||
`limit` salió de `required` y ganó `"default": 10`. Un cliente que lo omite recibe `10`, exactamente como haría Python.
|
||||
|
||||
## Esquemas más ricos con `Field` {#richer-schemas-with-field}
|
||||
|
||||
Las anotaciones de tipo te llevan lejos, pero a veces quieres *describir* un argumento, o restringirlo.
|
||||
|
||||
Envuelve el tipo en `Annotated` y añade un `Field` de Pydantic:
|
||||
|
||||
```python title="server.py" hl_lines="12-14"
|
||||
--8<-- "docs_src/tools/tutorial003.py"
|
||||
```
|
||||
|
||||
Tres cosas nuevas, todas en los parámetros:
|
||||
|
||||
* `Field(description=...)`: una descripción por argumento que el modelo lee junto con el docstring.
|
||||
* `Field(ge=1, le=50)`: límites numéricos. Llegan al esquema como `"minimum": 1, "maximum": 50`.
|
||||
* `Literal["fiction", "non-fiction", "poetry"]`: una enumeración. El modelo solo puede elegir uno de esos valores.
|
||||
|
||||
!!! check
|
||||
Las restricciones no son decoración. Llama a la herramienta con `limit=999` y el SDK responde con un
|
||||
error de herramienta **antes de que tu función se ejecute**:
|
||||
|
||||
```text
|
||||
Input should be less than or equal to 50
|
||||
```
|
||||
|
||||
Ese error vuelve al modelo como resultado de la herramienta, y el modelo lo lee y reintenta con
|
||||
un valor válido. Escribiste `le=50` una vez y obtuviste agentes que se corrigen solos, gratis.
|
||||
|
||||
!!! info
|
||||
Si has usado FastAPI o Pydantic, ya sabes todo esto. Es el mismo `Field`,
|
||||
el mismo `Annotated`, la misma validación. No hay nada específico de MCP que aprender aquí.
|
||||
|
||||
## Un modelo como parámetro {#a-model-as-a-parameter}
|
||||
|
||||
Cuando una herramienta recibe más de un par de argumentos, agrúpalos en un modelo de Pydantic:
|
||||
|
||||
```python title="server.py" hl_lines="8-11 15"
|
||||
--8<-- "docs_src/tools/tutorial004.py"
|
||||
```
|
||||
|
||||
El esquema de `Book` queda anidado dentro del esquema de entrada de la herramienta (como referencia en `$defs`), el modelo lo rellena como un objeto JSON y tu función recibe una **instancia real de `Book`**, ya validada, con los atributos `.title`, `.author` y `.year`.
|
||||
|
||||
Puedes combinar a tu gusto: parámetros simples junto a parámetros de modelo, modelos anidados, listas de modelos. Es Pydantic hasta el fondo.
|
||||
|
||||
## `async def` {#async-def}
|
||||
|
||||
Si una herramienta hace E/S (llama a una API, lee un archivo, consulta una base de datos), declárala como `async def` y usa `await` dentro. El SDK se encarga de esperarla.
|
||||
|
||||
Una herramienta con `def` normal también funciona: el SDK la ejecuta en un hilo para que nunca bloquee el servidor.
|
||||
|
||||
No hay nada más que configurar.
|
||||
|
||||
## Nombres, títulos y anotaciones {#names-titles-and-annotations}
|
||||
|
||||
Todo lo que el SDK infiere, puedes sobrescribirlo en el decorador:
|
||||
|
||||
```python title="server.py" hl_lines="7-10"
|
||||
--8<-- "docs_src/tools/tutorial005.py"
|
||||
```
|
||||
|
||||
* `title` es un nombre legible para las interfaces de usuario. Los clientes muestran *"Search the catalog"* en lugar de `search_books`.
|
||||
* `annotations` son **pistas** de comportamiento para el cliente:
|
||||
* `read_only_hint=True`: esta herramienta no cambia nada.
|
||||
* `open_world_hint=False`: trabaja sobre un conjunto cerrado de cosas (este catálogo), no sobre la web abierta.
|
||||
* Las otras dos, `destructive_hint` e `idempotent_hint`, describen una herramienta que *escribe*: ¿puede
|
||||
borrar algo?, ¿y llamarla dos veces equivale a llamarla una? La especificación define ambas
|
||||
solo para herramientas que no son de solo lectura, así que en `search_books` no dirían nada.
|
||||
|
||||
Un cliente bien hecho las usa para decidir cosas como *"¿tengo que preguntarle al usuario antes de ejecutar esto?"*. Son pistas, no seguridad. Nunca des por hecho que un cliente las respetará.
|
||||
|
||||
!!! tip
|
||||
`@mcp.tool()` también acepta `name=` y `description=` si no quieres derivarlos
|
||||
del nombre de la función y del docstring. La mayoría de las veces sí quieres.
|
||||
|
||||
## Resumen {#recap}
|
||||
|
||||
* `@mcp.tool()` sobre una función la convierte en herramienta. El nombre sale de la función, la descripción del docstring.
|
||||
* Las anotaciones de tipo **son** el esquema de entrada. Los valores por defecto hacen opcionales los argumentos.
|
||||
* `Annotated[..., Field(...)]` añade descripciones y restricciones; `Literal` añade enumeraciones.
|
||||
* Un parámetro que es un modelo de Pydantic es la forma de recibir un "cuerpo" estructurado.
|
||||
* Los argumentos incorrectos se rechazan por ti, con un error que el modelo puede leer y del que puede recuperarse.
|
||||
* `async def` para E/S, `def` normal para todo lo demás.
|
||||
|
||||
**[Salida estructurada](structured-output.md)** es lo que le ocurre al valor que devuelves con `return`.
|
||||
274
i18n/es/pages/servers/uri-templates.md
Normal file
274
i18n/es/pages/servers/uri-templates.md
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
---
|
||||
translation:
|
||||
sections: [4a7033e1ed8ad602, 55dcbfff0c6271bf, 101ef9d14bf4ec46, 4b6c4a845438abc7, f98b46bafbee4acd]
|
||||
tool: 1
|
||||
---
|
||||
# Plantillas de URI y seguridad de rutas {#uri-templates-and-path-safety}
|
||||
|
||||
Esta es la referencia de la sintaxis de plantillas de URI que acepta
|
||||
[`@mcp.resource`](resources.md) y de la
|
||||
política de seguridad de rutas que el SDK aplica a los valores extraídos. Para una
|
||||
introducción a qué son los recursos y cuándo usarlos, empieza por
|
||||
**[Recursos](resources.md)**; esta página supone que ya te sientes cómodo declarando un
|
||||
recurso y quieres el conjunto completo de operadores, los ajustes de seguridad o la
|
||||
conexión con la capa de bajo nivel.
|
||||
|
||||
La sintaxis de plantillas es [RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570).
|
||||
El SDK admite un subconjunto elegido para hacer coincidir las URI entrantes de
|
||||
`resources/read`, más una capa de seguridad que rechaza los valores que se resolverían
|
||||
fuera del directorio que pretendes servir. Para los detalles a nivel de protocolo
|
||||
(formatos de mensaje, ciclo de vida, paginación) consulta la
|
||||
[especificación de recursos de MCP](https://modelcontextprotocol.io/specification/latest/server/resources).
|
||||
|
||||
## El conjunto completo de operadores {#the-full-operator-set}
|
||||
|
||||
El marcador simple, `{user_id}`, es el que presenta **[Recursos](resources.md)**. Hay cuatro
|
||||
formas de operador más; aquí están en un solo servidor para que puedas verlas una junto a
|
||||
otra:
|
||||
|
||||
```python title="server.py" hl_lines="16-17 22-23 28-29 34-35 40-41"
|
||||
--8<-- "docs_src/uri_templates/tutorial001.py"
|
||||
```
|
||||
|
||||
Cada decorador resaltado es una forma distinta de dividir la URI.
|
||||
Las secciones siguientes los recorren de arriba abajo.
|
||||
|
||||
### Expansión simple: `{name}` {#simple-expansion-name}
|
||||
|
||||
`books://{isbn}` es la forma simple, la de todos los días. El marcador se asigna al
|
||||
parámetro `isbn`, así que un cliente que lee `books://978-0441172719` llama a
|
||||
`get_book("978-0441172719")`.
|
||||
|
||||
Un `{name}` simple se detiene en la primera `/`. `books://978/extra` no
|
||||
coincide porque la barra después de `978` termina la captura y `/extra`
|
||||
sobra.
|
||||
|
||||
### Conversión de tipos {#type-conversion}
|
||||
|
||||
Los valores extraídos llegan como cadenas, pero puedes declarar un tipo más específico
|
||||
y el SDK los convierte. `orders://{order_id}` llega a una función
|
||||
cuyo parámetro es `order_id: int`, así que leer `orders://12345` llama a
|
||||
`get_order(12345)`, no a `get_order("12345")`. El handler hace
|
||||
aritmética con él (`order_id + 1`) sin conversión explícita.
|
||||
|
||||
### Rutas de varios segmentos: `{+name}` {#multi-segment-paths-name}
|
||||
|
||||
Para capturar un valor que contiene barras, usa `{+name}`. Con
|
||||
`manuals://{+path}`:
|
||||
|
||||
* `manuals://returns.md` da `path = "returns.md"`
|
||||
* `manuals://printing/setup.md` da `path = "printing/setup.md"`
|
||||
|
||||
Recurre a `{+name}` siempre que el valor sea jerárquico: rutas del sistema de
|
||||
archivos, claves de objetos anidados, rutas de URL que estés redirigiendo como proxy.
|
||||
|
||||
### Parámetros de consulta: `{?a,b,c}` {#query-parameters-abc}
|
||||
|
||||
`reviews://{isbn}{?limit,sort}` pone `limit` y `sort` después del `?`.
|
||||
La ruta identifica *qué* libro; la consulta ajusta *cómo* lo lees.
|
||||
|
||||
Los parámetros de consulta se comparan con flexibilidad: el orden no importa, los
|
||||
sobrantes se ignoran y los omitidos caen en los valores por defecto de tu función. Así que
|
||||
`reviews://978-0441172719` usa `limit=10, sort="newest"`, y
|
||||
`reviews://978-0441172719?sort=top` sobrescribe solo `sort`.
|
||||
|
||||
### Segmentos de ruta como lista: `{/name*}` {#path-segments-as-a-list-name}
|
||||
|
||||
Si quieres cada segmento de ruta como un elemento separado de una lista en lugar de una
|
||||
sola cadena con barras, usa `{/name*}`. Con `shelves://browse{/path*}`, un
|
||||
cliente que lee `shelves://browse/fiction/sci-fi` llama a
|
||||
`browse_shelf(["fiction", "sci-fi"])`.
|
||||
|
||||
### Referencia de plantillas {#template-reference}
|
||||
|
||||
Los patrones más comunes:
|
||||
|
||||
| Patrón | Entrada de ejemplo | Obtienes |
|
||||
|--------------|-----------------------|-------------------------|
|
||||
| `{name}` | `alice` | `"alice"` |
|
||||
| `{name}` | `docs/intro.md` | *no coincide* (se detiene en `/`) |
|
||||
| `{+path}` | `docs/intro.md` | `"docs/intro.md"` |
|
||||
| `{.ext}` | `.json` | `"json"` |
|
||||
| `{/segment}` | `/v2` | `"v2"` |
|
||||
| `{?key}` | `?key=value` | `"value"` |
|
||||
| `{?a,b}` | `?a=1&b=2` | `"1"`, `"2"` |
|
||||
| `{/path*}` | `/a/b/c` | `["a", "b", "c"]` |
|
||||
|
||||
### Lo que rechaza el analizador {#what-the-parser-rejects}
|
||||
|
||||
Algunas formas de plantilla se detectan desde el principio en lugar de fallar en la
|
||||
primera solicitud. `@mcp.resource` analiza la plantilla cuando se ejecuta el
|
||||
decorador, así que ninguna de estas llega nunca a un servidor en ejecución.
|
||||
|
||||
`UriTemplate.parse()` lanza `InvalidUriTemplate` en estos casos:
|
||||
|
||||
* **Dos variables sin nada entre ellas.** `manuals://{+path}{ext}`
|
||||
se rechaza: la comparación no puede saber dónde termina `path` y dónde empieza `ext`.
|
||||
Pon un literal entre ellas (`manuals://{+path}/{ext}`) o usa un
|
||||
operador que aporte su propio delimitador. `manuals://{+path}{.ext}`
|
||||
se acepta porque `{.ext}` aporta el `.` por sí mismo.
|
||||
* **Más de una variable de varios segmentos.** Como máximo una entre `{+var}`,
|
||||
`{#var}` o una variable expandida (`{/var*}`, `{.var*}`, `{;var*}`)
|
||||
por plantilla. Dos son intrínsecamente ambiguas: no hay una forma fundamentada
|
||||
de decidir cuál de ellas absorbe un segmento adicional.
|
||||
* **Los errores de sintaxis habituales**: una llave sin cerrar, un nombre de variable usado
|
||||
dos veces o una característica de RFC 6570 que el SDK no admite, como el
|
||||
modificador de prefijo `{var:3}` o la expansión de consulta `{?vars*}`.
|
||||
|
||||
Además de eso, `@mcp.resource` lanza `ValueError` cuando un parámetro del
|
||||
handler está vinculado a una variable de consulta en el tramo final
|
||||
`{?...}`/`{&...}` de la plantilla pero no tiene valor por defecto en Python. Esas variables se
|
||||
comparan con flexibilidad (un cliente puede omitir cualquiera de ellas), así que un parámetro
|
||||
sin valor por defecto solo aparecería como un error interno opaco en la
|
||||
primera solicitud que lo omita. `reviews://{isbn}{?limit,sort}` en el
|
||||
servidor de arriba es la versión bien formada: tanto `limit` como `sort` tienen
|
||||
valores por defecto.
|
||||
|
||||
## Seguridad {#security}
|
||||
|
||||
Los parámetros de plantilla vienen del cliente. Si llegan a operaciones del sistema de
|
||||
archivos o de base de datos sin comprobar, valores como `../../etc/passwd` pueden
|
||||
resolverse fuera del directorio que pretendías servir.
|
||||
|
||||
### Lo que el SDK comprueba por defecto {#what-the-sdk-checks-by-default}
|
||||
|
||||
Antes de que se ejecute tu handler, el SDK rechaza cualquier parámetro que:
|
||||
|
||||
* escaparía de su directorio de partida mediante componentes `..`
|
||||
* parezca una ruta absoluta (`/etc/passwd`, `C:\Windows`) o una
|
||||
ruta relativa a unidad de Windows (`C:foo`). Un valor relativo a unidad y un
|
||||
identificador con espacio de nombres como `x:y` son indistinguibles como cadenas,
|
||||
así que cualquier valor de una sola letra seguida de dos puntos se rechaza por defecto;
|
||||
exime el parámetro si recibe legítimamente ese tipo de valores
|
||||
* contenga un byte nulo (`\x00`)
|
||||
|
||||
La comprobación de `..` se basa en componentes, no en buscar subcadenas. Valores como
|
||||
`v1.0..v2.0` o `HEAD~3..HEAD` pasan porque ahí `..` no es un segmento de ruta
|
||||
independiente.
|
||||
|
||||
Estas comprobaciones se aplican al valor decodificado, así que detectan el recorrido de
|
||||
directorios sin importar cómo se codificó en la URI (`../etc`, `..%2Fetc`,
|
||||
`%2E%2E/etc`, `..%5Cetc`, `%00`: todos se detectan).
|
||||
|
||||
!!! check
|
||||
Lee `manuals://../etc/passwd` en el servidor de arriba y la solicitud
|
||||
se rechaza sin más: la comparación de plantillas se detiene en el primer fallo,
|
||||
así que no se prueba ninguna plantilla posterior (potencialmente más permisiva) como
|
||||
alternativa. El cliente ve el mismo error `-32602` "Unknown resource"
|
||||
que vería con una URI que no coincide con ninguna plantilla, y
|
||||
`read_manual` nunca se ejecuta.
|
||||
|
||||
### Handlers del sistema de archivos: usa safe_join {#filesystem-handlers-use-safe_join}
|
||||
|
||||
Las comprobaciones integradas detienen los casos comunes, pero no pueden conocer el límite
|
||||
de tu entorno aislado. Para acceder al sistema de archivos, usa `safe_join` para resolver la
|
||||
ruta y verificar que se mantiene dentro de tu directorio base:
|
||||
|
||||
```python title="server.py" hl_lines="4 14"
|
||||
--8<-- "docs_src/uri_templates/tutorial002.py"
|
||||
```
|
||||
|
||||
`safe_join` detecta escapes mediante enlaces simbólicos, secuencias `..` y trucos con rutas
|
||||
absolutas que una simple comprobación de cadenas pasaría por alto. Si la ruta resuelta
|
||||
escapa de `DOCS_ROOT`, lanza `PathEscapeError`, que le llega al
|
||||
cliente como un `ResourceError`.
|
||||
|
||||
### Cuando los valores por defecto estorban {#when-the-defaults-get-in-the-way}
|
||||
|
||||
A veces las comprobaciones bloquean valores legítimos. Una herramienta de importación de
|
||||
catálogos podría recibir intencionadamente una ruta absoluta, o un parámetro podría ser una
|
||||
referencia relativa como `../sibling` que tu handler interpreta con
|
||||
seguridad sin tocar el sistema de archivos. Exime ese parámetro o relaja
|
||||
la política para todo el servidor:
|
||||
|
||||
```python title="server.py" hl_lines="9 16-19"
|
||||
--8<-- "docs_src/uri_templates/tutorial003.py"
|
||||
```
|
||||
|
||||
* `security=ResourceSecurity(exempt_params={"source"})` en el decorador
|
||||
omite las comprobaciones para ese único parámetro en ese único recurso. El
|
||||
resto del servidor mantiene la política por defecto.
|
||||
* `resource_security=` en el constructor de `MCPServer` fija el valor por defecto
|
||||
para todos los recursos. Aquí `relaxed` desactiva por completo la comprobación de `..`.
|
||||
|
||||
Las comprobaciones configurables:
|
||||
|
||||
| Ajuste | Por defecto | Qué hace |
|
||||
|-------------------------|---------|-------------------------------------|
|
||||
| `reject_path_traversal` | `True` | Rechaza secuencias `..` que escapan del directorio de partida |
|
||||
| `reject_absolute_paths` | `True` | Rechaza `/foo`, `C:\foo`, rutas UNC y la ruta relativa a unidad `C:foo` (también detecta `x:y`) |
|
||||
| `reject_null_bytes` | `True` | Rechaza valores que contienen `\x00` |
|
||||
| `exempt_params` | vacío | Nombres de parámetros para los que se omiten las comprobaciones |
|
||||
|
||||
Estas comprobaciones son un prefiltro heurístico; para el acceso al sistema de archivos,
|
||||
`safe_join` sigue siendo el límite de contención.
|
||||
|
||||
!!! tip
|
||||
Si tu handler no puede satisfacer la solicitud (el archivo no existe,
|
||||
el id es desconocido), lanza una excepción. El SDK la convierte en una
|
||||
respuesta de error. Consulta **[Manejo de errores](handling-errors.md)** para ver la diferencia entre un
|
||||
error de protocolo y un error de herramienta.
|
||||
|
||||
## Recursos en el Server de bajo nivel {#resources-on-the-low-level-server}
|
||||
|
||||
Si construyes sobre el `Server` de bajo nivel (consulta **[El Server de bajo
|
||||
nivel](../advanced/low-level-server.md)**), registras directamente los handlers para los métodos de protocolo
|
||||
`resources/list` y `resources/read`. No hay decorador; devuelves
|
||||
tú mismo los tipos del protocolo.
|
||||
|
||||
### Recursos estáticos {#static-resources}
|
||||
|
||||
Para URI fijas, mantén un registro y despacha por coincidencia exacta:
|
||||
|
||||
```python title="server.py" hl_lines="17 21 27"
|
||||
--8<-- "docs_src/uri_templates/tutorial004.py"
|
||||
```
|
||||
|
||||
El handler de listado les dice a los clientes qué hay disponible; el handler de lectura
|
||||
sirve el contenido. Comprueba primero tu registro, pasa a las
|
||||
plantillas (más abajo) si tienes alguna y luego lanza una excepción para cualquier otra cosa.
|
||||
|
||||
### Plantillas {#templates}
|
||||
|
||||
El motor de plantillas que usa `MCPServer` vive en `mcp.shared.uri_template`
|
||||
y funciona por sí solo. Obtienes el mismo análisis y la misma comparación; el
|
||||
enrutamiento y la política de seguridad los conectas tú mismo.
|
||||
|
||||
```python title="server.py" hl_lines="13-16 22-25 29 33 45"
|
||||
--8<-- "docs_src/uri_templates/tutorial005.py"
|
||||
```
|
||||
|
||||
En las líneas resaltadas ocurren tres cosas:
|
||||
|
||||
* **Analiza una vez, compara en cada solicitud.** `UriTemplate.parse()` construye la
|
||||
plantilla; `template.match(uri)` devuelve las variables extraídas como un
|
||||
`dict`, o `None` si la URI no encaja. La decodificación de URL ocurre dentro de
|
||||
`match()`; los valores decodificados se devuelven tal cual, sin validación de
|
||||
seguridad de rutas. Los valores salen como cadenas: conviértelos tú mismo
|
||||
(`int(matched["id"])`, `Path(matched["path"])`).
|
||||
* **Aplica tú mismo las comprobaciones de seguridad.** Las comprobaciones de `..` y de rutas
|
||||
absolutas que `MCPServer` ejecuta por defecto viven en `mcp.shared.path_security`.
|
||||
`read_manual_safely` las llama antes de tocar `MANUALS`. Si un
|
||||
parámetro no es una ruta del sistema de archivos (un ISBN, una consulta de búsqueda), omite las
|
||||
comprobaciones para ese valor: controlas la política por handler en lugar de
|
||||
hacerlo mediante un objeto de configuración.
|
||||
* **Lista las plantillas desde la misma fuente.** Los clientes descubren
|
||||
las plantillas mediante `resources/templates/list`. `str(template)` devuelve
|
||||
la cadena original de la plantilla, así que el listado y el comparador
|
||||
comparten una única fuente de verdad.
|
||||
|
||||
## Resumen {#recap}
|
||||
|
||||
* `{name}` coincide con un segmento; `{+name}` conserva las barras; `{?a,b}`
|
||||
toma de la cadena de consulta; `{/name*}` divide los segmentos en una lista.
|
||||
* Dos variables sin nada entre ellas, o una segunda variable de varios
|
||||
segmentos, se rechazan al analizar. Un parámetro vinculado a una variable de consulta
|
||||
final `{?...}`/`{&...}` debe declarar un valor por defecto en Python.
|
||||
* Anota el parámetro (`order_id: int`) y el SDK convierte.
|
||||
* La política de seguridad por defecto rechaza `..`, rutas absolutas y bytes
|
||||
nulos antes de que se ejecute tu handler; sobrescríbela por recurso con
|
||||
`security=ResourceSecurity(...)` o para todo el servidor con
|
||||
`resource_security=`.
|
||||
* Para el acceso al sistema de archivos, `safe_join` es el límite de contención.
|
||||
* En el `Server` de bajo nivel, analiza con `UriTemplate.parse()`, compara
|
||||
con `.match()` y aplica `mcp.shared.path_security` tú mismo.
|
||||
Loading…
Reference in a new issue