FastMCP: La guía completa para crear servidores y clientes de MCP

FastMCP: La guía completa para crear servidores y clientes de MCP

Un completo tutorial sobre FastMCP 2.0, la forma rápida y "pythónica" de construir servidores y clientes basados en el Protocolo de Contexto del Modelo (MCP). Aprende a crear herramientas, recursos e instrucciones para tus aplicaciones LLM.

Lección 3 2025-07-03 06:58

Creando Herramientas y Recursos Potentes

Creación de Herramientas y Recursos Potentes

Comprensión de los Componentes MCP

Los servidores FastMCP exponen tres tipos principales de componentes a los clientes LLM:

  1. Herramientas (Tools): Funciones que realizan acciones o cálculos.
  2. Recursos (Resources): Fuentes de datos que los clientes pueden leer.
  3. Prompts (Indicaciones): Plantillas de mensajes reutilizables para interacciones con LLM.

Exploremos cómo construir cada uno de ellos de forma eficaz.

Creación de Herramientas

Las herramientas son el motor de los servidores MCP. Permiten a los LLM realizar acciones, hacer cálculos, llamar a APIs e interactuar con sistemas externos.

Herramientas Básicas

Las herramientas más sencillas son funciones decoradas:

from fastmcp import FastMCP

mcp = FastMCP("Utility Server")

@mcp.tool
def calculate_area(length: float, width: float) -> float:
    """Calcula el área de un rectángulo."""
    return length * width

@mcp.tool
def generate_password(length: int = 12, include_symbols: bool = True) -> str:
    """Genera una contraseña aleatoria."""
    import random
    import string

    chars = string.ascii_letters + string.digits
    if include_symbols:
        chars += "!@#$%^&*"

    return ''.join(random.choice(chars) for _ in range(length))

Herramientas Avanzadas con Contexto

Las herramientas pueden acceder al contexto MCP para realizar operaciones avanzadas:

from fastmcp import FastMCP, Context
import aiohttp
import json

mcp = FastMCP("API Tools")

@mcp.tool
async def fetch_weather(city: str, ctx: Context) -> dict:
    """Obtiene el clima actual para una ciudad."""
    await ctx.info(f"Obteniendo clima para {city}...")

    # Realiza una solicitud HTTP usando el contexto
    response = await ctx.http_request(
        "GET", 
        f"https://api.openweathermap.org/data/2.5/weather",
        params={"q": city, "appid": "your_api_key"}
    )

    weather_data = response.json()

    return {
        "city": city,
        "temperature": weather_data["main"]["temp"],
        "description": weather_data["weather"][0]["description"],
        "humidity": weather_data["main"]["humidity"]
    }

@mcp.tool
async def analyze_text(text: str, ctx: Context) -> str:
    """Analiza texto usando el LLM del cliente."""
    prompt = f"""
    Por favor, analiza el siguiente texto y proporciona información sobre:
    1. Sentimiento
    2. Temas clave
    3. Estilo de escritura

    Texto: {text}
    """

    # Usa el LLM del cliente para el análisis
    response = await ctx.sample(prompt)
    return response.text

Herramientas con Tipos de Retorno Complejos

Las herramientas pueden devolver varios tipos de datos, incluyendo medios:

from fastmcp import FastMCP
from fastmcp.types import Image, BlobResourceContents
import matplotlib.pyplot as plt
import io
import base64

mcp = FastMCP("Data Visualization")

@mcp.tool
def create_chart(data: list[float], chart_type: str = "line") -> Image:
    """Crea un gráfico a partir de datos."""
    plt.figure(figsize=(10, 6))

    if chart_type == "line":
        plt.plot(data)
    elif chart_type == "bar":
        plt.bar(range(len(data)), data)

    plt.title(f"Gráfico de {chart_type.title()}")
    plt.xlabel("Índice")
    plt.ylabel("Valor")

    # Guarda en bytes
    buffer = io.BytesIO()
    plt.savefig(buffer, format='png')
    buffer.seek(0)

    # Convierte a base64
    image_base64 = base64.b64encode(buffer.read()).decode()

    return Image(
        data=image_base64,
        mimeType="image/png"
    )

@mcp.tool
def process_data(numbers: list[float]) -> dict:
    """Procesa una lista de números y devuelve estadísticas."""
    import statistics

    if not numbers:
        return {"error": "No se proporcionaron datos"}

    return {
        "count": len(numbers),
        "sum": sum(numbers),
        "mean": statistics.mean(numbers),
        "median": statistics.median(numbers),
        "std_dev": statistics.stdev(numbers) if len(numbers) > 1 else 0,
        "min": min(numbers),
        "max": max(numbers)
    }

Creación de Recursos

Los recursos exponen datos que los LLM pueden leer e incorporar a su contexto. Son perfectos para datos de configuración, documentación o cualquier información de solo lectura.

Recursos Estáticos

Fuentes de datos simples con URIs fijas:

@mcp.resource("config://database")
def get_database_config() -> dict:
    """Configuración de la base de datos."""
    return {
        "host": "localhost",
        "port": 5432,
        "database": "myapp",
        "ssl_enabled": True
    }

@mcp.resource("docs://api-guide")
def get_api_documentation() -> str:
    """Documentación de uso de la API."""
    return """
    # Guía de Uso de la API

    ## Autenticación
    Todas las solicitudes requieren una clave de API en el encabezado de Autorización.

    ## Límites de Tasa
    - 1000 solicitudes por hora para el nivel básico
    - 10000 solicitudes por hora para el nivel premium

    ## Puntos Finales Disponibles
    - GET /users - Lista todos los usuarios
    - POST /users - Crea un nuevo usuario
    - GET /users/{id} - Obtiene detalles del usuario
    """

Plantillas de Recursos Dinámicos

Las plantillas permiten a los clientes solicitar datos específicos utilizando parámetros:

@mcp.resource("users://{user_id}/profile")
def get_user_profile(user_id: int) -> dict:
    """Obtiene la información del perfil de un usuario."""
    # En una aplicación real, esto consultaría una base de datos
    users_db = {
        1: {"name": "Alice Johnson", "role": "Admin", "email": "[email protected]"},
        2: {"name": "Bob Smith", "role": "User", "email": "[email protected]"},
        3: {"name": "Carol Davis", "role": "Manager", "email": "[email protected]"}
    }

    user = users_db.get(user_id)
    if not user:
        raise ValueError(f"Usuario {user_id} no encontrado")

    return {
        "id": user_id,
        **user,
        "last_login": "2024-01-15T10:30:00Z",
        "status": "active"
    }

@mcp.resource("data://{dataset}/stats")
def get_dataset_stats(dataset: str) -> dict:
    """Obtiene estadísticas para un conjunto de datos específico."""
    # Simula diferentes conjuntos de datos
    datasets = {
        "sales": {"records": 10000, "last_updated": "2024-01-15", "size_mb": 45.2},
        "users": {"records": 5000, "last_updated": "2024-01-14", "size_mb": 12.8},
        "products": {"records": 2500, "last_updated": "2024-01-13", "size_mb": 8.1}
    }

    if dataset not in datasets:
        raise ValueError(f"Conjunto de datos '{dataset}' no encontrado")

    return {
        "dataset": dataset,
        **datasets[dataset],
        "access_level": "public"
    }

Recursos Basados en Archivos

Los recursos también pueden servir contenido de archivos:

import os
from pathlib import Path

@mcp.resource("files://{file_path}")
def read_file_content(file_path: str) -> str:
    """Lee el contenido de un archivo en el directorio del proyecto."""
    # Seguridad: restringe al directorio del proyecto
    project_root = Path(__file__).parent
    full_path = project_root / file_path

    # Asegúrate de que la ruta esté dentro del directorio del proyecto
    if not str(full_path.resolve()).startswith(str(project_root.resolve())):
        raise ValueError("Acceso denegado: ruta fuera del directorio del proyecto")

    if not full_path.exists():
        raise FileNotFoundError(f"Archivo no encontrado: {file_path}")

    return full_path.read_text(encoding='utf-8')

@mcp.resource("logs://{date}")
def get_daily_logs(date: str) -> str:
    """Obtiene los registros de la aplicación para una fecha específica."""
    import datetime

    try:
        # Valida el formato de la fecha
        datetime.datetime.strptime(date, "%Y-%m-%d")
    except ValueError:
        raise ValueError("La fecha debe estar en formato YYYY-MM-DD")

    log_file = f"logs/{date}.log"

    if not os.path.exists(log_file):
        return f"No se encontraron registros para {date}"

    with open(log_file, 'r') as f:
        return f.read()

Creación de Prompts (Indicaciones)

Los prompts proporcionan plantillas reutilizables para las interacciones con LLM:

@mcp.prompt
def code_review_prompt(code: str, language: str = "python") -> str:
    """Genera una indicación para la revisión de código."""
    return f"""
    Por favor, revisa el siguiente código de {language} y proporciona comentarios sobre:

    1. Calidad y legibilidad del código
    2. Consideraciones de rendimiento
    3. Problemas de seguridad
    4. Adherencia a las mejores prácticas
    5. Sugerencias de mejora

    Código:
    ```{language}
    {code}
    ```

    Por favor, proporciona comentarios constructivos con ejemplos específicos.
    """

@mcp.prompt
def data_analysis_prompt(data_description: str, questions: list[str]) -> str:
    """Genera una indicación para el análisis de datos."""
    questions_text = "\\n".join(f"- {q}" for q in questions)

    return f"""
    Necesito ayuda para analizar datos con las siguientes características:
    {data_description}

    Preguntas específicas que quiero explorar:
    {questions_text}

    Por favor, proporciona un enfoque de análisis estructurado y sugiere
    métodos estadísticos o visualizaciones apropiadas.
    """

Manejo de Errores y Validación

Las herramientas robustas incluyen un manejo de errores adecuado:

@mcp.tool
def divide_numbers(a: float, b: float) -> float:
    """Divide dos números con manejo de errores."""
    if b == 0:
        raise ValueError("No se puede dividir por cero")

    if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
        raise TypeError("Ambos argumentos deben ser números")

    return a / b

@mcp.tool
def process_email(email: str) -> dict:
    """Valida y procesa una dirección de correo electrónico."""
    import re

    # Validación básica de correo electrónico
    email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$'

    if not re.match(email_pattern, email):
        raise ValueError("Formato de correo electrónico no válido")

    username, domain = email.split('@')

    return {
        "email": email,
        "username": username,
        "domain": domain,
        "is_valid": True
    }

Prueba de tus Componentes

Siempre prueba tus herramientas y recursos:

import asyncio
from fastmcp import Client

async def test_server():
    async with Client(mcp) as client:
        # Prueba de herramientas
        result = await client.call_tool("calculate_area", {"length": 5, "width": 3})
        print(f"Área: {result.text}")

        # Prueba de recursos
        config = await client.read_resource("config://database")
        print(f"Configuración: {config.content}")

        # Prueba de plantillas de recursos
        user = await client.read_resource("users://1/profile")
        print(f"Usuario: {user.content}")

if __name__ == "__main__":
    asyncio.run(test_server())

Mejores Prácticas

  1. Documentación Clara: Siempre incluye docstrings descriptivos.
  2. Sugerencias de Tipos: Utiliza anotaciones de tipos adecuadas para la generación automática de esquemas.
  3. Manejo de Errores: Proporciona mensajes de error significativos.
  4. Seguridad: Valida las entradas y restringe el acceso a archivos de manera apropiada.
  5. Rendimiento: Considera operaciones asíncronas para tareas ligadas a E/S.
  6. Pruebas: Prueba exhaustivamente todos los componentes.

En la siguiente sección, exploraremos cómo integrar tu servidor FastMCP con Claude Code para flujos de trabajo de desarrollo sin interrupciones.