Herramientas
Traducción automática
Esta página se tradujo automáticamente a partir de la documentación en inglés, y la página en inglés es la versión de referencia. Si algo no se lee bien, Traducciones explica cómo avisarnos.
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
from mcp.server import MCPServer
mcp = MCPServer("Bookshop")
@mcp.tool()
def search_books(query: str, limit: int) -> str:
"""Search the catalog by title or author."""
return f"Found 3 books matching {query!r} (showing up to {limit})."
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: strylimit: int.
El esquema de entrada
A partir de esas anotaciones de tipo, el SDK genera un JSON Schema y lo envía al cliente durante tools/list:
{
"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
Llama a la herramienta con {"query": "dune", "limit": 5} y el resultado tiene dos partes:
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 trata justamente de eso.
Pruébalo
Ejecuta el servidor con el MCP Inspector:
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
Dale un valor por defecto a un parámetro y deja de ser obligatorio. Eso es todo. Es simplemente Python.
from mcp.server import MCPServer
mcp = MCPServer("Bookshop")
@mcp.tool()
def search_books(query: str, limit: int = 10) -> str:
"""Search the catalog by title or author."""
return f"Found 3 books matching {query!r} (showing up to {limit})."
El esquema lo refleja:
{
"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
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:
from typing import Annotated, Literal
from pydantic import Field
from mcp.server import MCPServer
mcp = MCPServer("Bookshop")
@mcp.tool()
def search_books(
query: Annotated[str, Field(description="Title or author to search for.")],
limit: Annotated[int, Field(ge=1, le=50, description="Maximum number of results.")] = 10,
genre: Literal["fiction", "non-fiction", "poetry"] | None = None,
) -> str:
"""Search the catalog by title or author."""
where = f" in {genre}" if genre else ""
return f"Found 3 books matching {query!r}{where} (showing up to {limit})."
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:
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
Cuando una herramienta recibe más de un par de argumentos, agrúpalos en un modelo de Pydantic:
from pydantic import BaseModel, Field
from mcp.server import MCPServer
mcp = MCPServer("Bookshop")
class Book(BaseModel):
title: str
author: str
year: int = Field(ge=1450, description="Year of first publication.")
@mcp.tool()
def add_book(book: Book) -> str:
"""Add a book to the catalog."""
return f"Added {book.title!r} by {book.author} ({book.year})."
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
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
Todo lo que el SDK infiere, puedes sobrescribirlo en el decorador:
from mcp.server import MCPServer
from mcp.types import ToolAnnotations
mcp = MCPServer("Bookshop")
@mcp.tool(
title="Search the catalog",
annotations=ToolAnnotations(read_only_hint=True, open_world_hint=False),
)
def search_books(query: str) -> str:
"""Search the catalog by title or author."""
return f"Found 3 books matching {query!r}."
titlees un nombre legible para las interfaces de usuario. Los clientes muestran "Search the catalog" en lugar desearch_books.annotationsson 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_hinteidempotent_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 ensearch_booksno 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
@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;Literalañ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 defpara E/S,defnormal para todo lo demás.
Salida estructurada es lo que le ocurre al valor que devuelves con return.