docs: publish translated docs in twelve languages and the tool that maintains them (#3280)

This commit is contained in:
Max 2026-08-14 17:07:34 +01:00 • committed by GitHub
commit 52ad0a8876
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 89737 additions and 54 deletions

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

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

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

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

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

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

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

View 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`.

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