Servidor MCP en Python con FastMCP: tools, resources y prompts

Tutorial con código probado: crea un servidor MCP en Python con FastMCP, con herramientas, recursos y prompts, pruébalo sin LLM y conéctalo a Claude.

Servidor MCP en Python con FastMCP: tools, resources y prompts

Ya tienes una API, una base de datos o un script interno que resuelve un problema real. Ahora quieres que un asistente de IA, como Claude, lo use sin copiar y pegar respuestas en el chat. El camino estándar hoy es exponer ese sistema como un servidor MCP. En Python, la forma más corta de hacerlo es FastMCP: escribes funciones normales, añades un decorador y el protocolo se encarga del resto.

En este tutorial vas a crear un servidor MCP con FastMCP desde cero, con las tres piezas que ofrece el protocolo: herramientas, recursos y prompts. Todo el código se ejecutó con FastMCP 4.1 y Python 3.11, y la salida real aparece más abajo.

Qué vas a construir

Un servidor para el catálogo de una biblioteca, lo bastante pequeño para caber en el artículo y lo bastante completo para mostrar cada concepto. El código mantiene los nombres en portugués (livros es "libros", reservar es igual):

  • Herramientas (tools): acciones que el modelo puede llamar. Aquí, buscar_livros y reservar_livro.
  • Recursos (resources): datos que el cliente puede leer en una dirección, como un archivo. Aquí, las reglas de préstamo y la ficha de cada libro.
  • Prompts: plantillas de mensaje listas que el usuario elige en el cliente. Aquí, un prompt que pide una recomendación de lectura.

La diferencia importa: una herramienta la decide el modelo, un recurso lo elige la aplicación y un prompt lo elige el usuario.

Instalar FastMCP

Crea un entorno e instala el paquete:

uv venv && source .venv/bin/activate
uv pip install fastmcp

Con pip funciona igual: pip install fastmcp.

El servidor completo

Guárdalo como server.py:

"""Servidor MCP de exemplo: catálogo de livros de uma biblioteca."""
from typing import Annotated

from fastmcp import FastMCP
from pydantic import Field

mcp = FastMCP("biblioteca")

LIVROS = {
    "978-8575228050": {"titulo": "Python Fluente", "autor": "Luciano Ramalho", "disponivel": True},
    "978-8550804606": {"titulo": "Código Limpo", "autor": "Robert C. Martin", "disponivel": False},
}
RESERVAS: set[str] = set()


@mcp.tool(annotations={"readOnlyHint": True})
def buscar_livros(
    termo: Annotated[str, Field(description="Parte do título ou do autor", min_length=2)],
    so_disponiveis: bool = False,
) -> list[dict]:
    """Busca livros do catálogo pelo título ou pelo autor."""
    termo = termo.casefold()
    return [
        {"isbn": isbn, **livro}
        for isbn, livro in LIVROS.items()
        if termo in (livro["titulo"] + " " + livro["autor"]).casefold()
        and (livro["disponivel"] or not so_disponiveis)
    ]


@mcp.tool(annotations={"destructiveHint": False, "idempotentHint": True})
def reservar_livro(isbn: str) -> dict:
    """Reserva um livro disponível. Reservar de novo o mesmo livro não muda nada."""
    livro = LIVROS.get(isbn)
    if livro is None:
        raise ValueError(f"ISBN {isbn} não existe no catálogo")
    if isbn in RESERVAS:
        return {"isbn": isbn, "status": "reservado"}
    if not livro["disponivel"]:
        raise ValueError(f"'{livro['titulo']}' já está emprestado")
    livro["disponivel"] = False
    RESERVAS.add(isbn)
    return {"isbn": isbn, "status": "reservado"}


@mcp.resource("biblioteca://regras")
def regras() -> str:
    """Regras de empréstimo da biblioteca."""
    return "Prazo de 14 dias. Até 3 livros por pessoa. Renovação online uma vez."


@mcp.resource("biblioteca://livros/{isbn}")
def ficha_do_livro(isbn: str) -> dict:
    """Ficha completa de um livro pelo ISBN."""
    return {"isbn": isbn, **LIVROS[isbn]}


@mcp.prompt
def recomendar(tema: str, nivel: str = "iniciante") -> str:
    """Pede ao modelo uma recomendação de leitura usando o catálogo."""
    return (
        f"Use a ferramenta buscar_livros para encontrar livros sobre {tema} "
        f"para um leitor {nivel}. Recomende no máximo 3 e explique o porquê."
    )


if __name__ == "__main__":
    mcp.run()

Son menos de 70 líneas, y ninguna habla de JSON-RPC, de mensajes ni de transporte. Veamos qué hace cada parte.

Herramientas: la firma se convierte en el contrato

FastMCP lee la firma y el docstring de la función y genera, por sí solo, lo que el modelo va a recibir: el nombre, la descripción y un JSON Schema de los argumentos. Para buscar_livros, el esquema generado es este:

{
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "termo": {"description": "Parte do título ou do autor", "minLength": 2, "type": "string"},
    "so_disponiveis": {"default": false, "type": "boolean"}
  },
  "required": ["termo"]
}

Tres detalles de este ejemplo valen para cualquier servidor:

  • El docstring es el manual del modelo. El modelo decide si llama a la herramienta leyendo esa frase. Escribe qué hace y cuándo usarla, no cómo está implementada.
  • Valida en la frontera. Field(min_length=2) se convierte en minLength en el esquema, y FastMCP rechaza una llamada que no respeta el esquema antes de que llegue a tu función.
  • Las anotaciones dicen el riesgo. readOnlyHint avisa al cliente de que la herramienta solo lee; idempotentHint avisa de que repetir la llamada es seguro. Los clientes lo usan para decidir cuándo pedir confirmación al usuario. Por eso reservar_livro devuelve el mismo resultado en la segunda llamada en lugar de fallar: la anotación lo promete y el código tiene que cumplirlo.

Cuando algo sale mal, lanza una excepción con un mensaje útil. FastMCP la convierte en un resultado con isError: true y el texto del mensaje, que el modelo lee y puede usar para corregir el siguiente intento. Llamar a reservar_livro con un ISBN inexistente devuelve exactamente ISBN 000 não existe no catálogo ("el ISBN 000 no está en el catálogo").

Recursos: datos con dirección

Un recurso se lee, no se ejecuta. @mcp.resource("biblioteca://regras") publica un texto fijo; biblioteca://livros/{isbn} es una plantilla de recurso: la parte entre llaves se convierte en el argumento de la función. Usa recursos para el contexto que la aplicación adjunta a la conversación, como documentación, configuración o un registro concreto, y deja las herramientas para acciones y búsquedas.

Prompts: atajos para el usuario

@mcp.prompt publica una plantilla de mensaje con argumentos. En los clientes que admiten prompts, aparece como un comando que el usuario elige, y el texto generado ya orienta al modelo hacia las herramientas correctas. Es una buena forma de distribuir "la forma correcta de pedir" junto con el servidor.

Probar sin un asistente

Salida real de fastmcp list y del cliente Python llamando a las herramientas, el recurso y el prompt del servidor MCP de la biblioteca
Salida real de fastmcp list y del cliente Python llamando a las herramientas, el recurso y el prompt del servidor MCP de la biblioteca

No necesitas un LLM para probar el servidor. FastMCP incluye un cliente en Python. Guárdalo como cliente.py:

import asyncio

from fastmcp import Client


async def main():
    async with Client("server.py") as client:
        tools = await client.list_tools()
        print("ferramentas:", [t.name for t in tools])
        resultado = await client.call_tool("buscar_livros", {"termo": "python"})
        print("buscar_livros:", resultado.data)
        regras = await client.read_resource("biblioteca://regras")
        print("regras:", regras[0].text)
        prompt = await client.get_prompt("recomendar", {"tema": "testes"})
        print("prompt:", prompt.messages[0].content.text)


asyncio.run(main())

Pasar la ruta server.py hace que el cliente arranque el servidor como subproceso y hable con él por stdio. La línea de comandos de FastMCP también ayuda: fastmcp list server.py muestra las herramientas y fastmcp call server.py buscar_livros termo=código llama a una de ellas. Esta es la salida real de los dos comandos.

Para explorar visualmente, fastmcp dev inspector server.py abre el MCP Inspector en el navegador. Estas mismas pruebas caben en tu pytest: el cliente en memoria te permite probar el servidor como pruebas cualquier otra función.

Ejecutar por stdio o por HTTP

mcp.run() sin argumentos usa stdio: el cliente arranca el proceso en la máquina del usuario. Es el modo adecuado para herramientas locales, como el acceso a archivos o a un repositorio.

Para un servicio compartido, usa HTTP:

mcp.run(transport="http", host="0.0.0.0", port=8000)

El endpoint queda en http://<host>:8000/mcp y el mismo Client se conecta con Client("http://localhost:8000/mcp"). En producción, pon autenticación delante (un token por usuario u OAuth) y trata cada herramienta como un endpoint público: valida todo, registra las llamadas y da a cada token solo los permisos que necesita.

Conectarlo a Claude

En Claude Code, un comando registra el servidor:

# servidor local, por stdio
claude mcp add biblioteca -- python /ruta/a/server.py

# servidor remoto, por HTTP
claude mcp add --transport http biblioteca https://mcp.ejemplo.com/mcp

A partir de ahí, pedir "¿hay algún libro de Python disponible? Si lo hay, resérvalo" hace que Claude llame a buscar_livros, lea el resultado y, si tiene sentido, llame a reservar_livro.

Buenas prácticas para diseñar la API de las herramientas

  • Pocas herramientas, bien nombradas. Prefiere buscar_livros y reservar_livro a un ejecutar_accion(tipo, payload). El modelo elige mejor cuando el nombre ya dice la intención.
  • Argumentos explícitos y tipados. Enums, límites y descripciones en el esquema evitan llamadas erróneas antes de que ocurran.
  • Respuestas pequeñas y estructuradas. Devuelve lo que el modelo necesita para el siguiente paso, no la tabla entera. Pagina las listas largas.
  • Errores que enseñan. "El ISBN no existe" ayuda al modelo a corregirse; un stack trace no.
  • Escrituras idempotentes y anotadas. Si una herramienta cambia datos, márcala con las anotaciones y haz que repetirla sea seguro.
  • Separa lectura y escritura. Las herramientas de solo lectura pueden ejecutarse sin confirmación; las de escritura merecen la atención del usuario.

Próximos pasos

Cambia el diccionario LIVROS por tu API o tu base de datos, mantén las mismas reglas de diseño y tendrás un servidor MCP útil en una tarde. El protocolo es el mismo para cualquier cliente compatible, así que el trabajo sirve para Claude, para los IDE y para los agentes que escribas tú mismo.

Te gusto el articulo?

Compartelo con tus amigos y ayuda a difundir conocimiento!