Tutorial de servidor MCP: crea uno con Python, uv y FastMCP

Traducción automática

Este artículo se tradujo automáticamente a partir de la versión original en inglés.

Este tutorial de servidor MCP construye un pequeño servidor de feature store con FastMCP, lo ejecuta con uv y lo conecta a Claude Desktop. Está pensado para desarrolladores de Python que quieren exponer a un host MCP un flujo de trabajo con una base de datos local. Al terminar tendrás un entorno con el SDK v1 fijado, un servidor con tres herramientas y un recurso, y una comprobación local que ejecutar antes de conectar Claude Desktop.


¿Qué es un servidor MCP?

El Model Context Protocol (MCP) es un protocolo abierto para conectar aplicaciones de AI con sistemas externos. Sustituye una integración a medida para cada herramienta por un único conjunto de convenciones.

En este tutorial construiremos un servidor MCP llamado FeatureStoreLite. Se sitúa entre un LLM y un feature store, es decir, una base de datos de features de ML precalculadas. El servidor expone herramientas para consultar y escribir vectores de features indexados por usuario, producto o documento.

¿Por qué construirlo?

Depurar un pipeline de features suele significar abrir SQL o escribir un script desechable para comprobar un valor. Con el servidor en marcha, se lo preguntas a Claude: “¿Cuál es el vector de features de user_123?” o “Muéstrame los metadatos de product_abc.”

¿Por qué usar uv?

Usaremos uv para instalar paquetes, resolver dependencias y gestionar el entorno virtual. La configuración de Claude Desktop ejecutará este proyecto desde su propio directorio con --locked, de modo que use la dependencia mcp[cli] declarada aquí y las versiones registradas en uv.lock.

Herramientas, recursos y prompts

Un servidor MCP puede ofrecer tres tipos de elementos. La especificación de MCP los distingue según quién decide usar cada uno:

PrimitivaQuién la activaEn este servidor
ToolEl modelo decide llamarlaget_feature, store_feature, list_features
ResourceLa aplicación host o el usuario la adjuntaschema://main
PromptEl usuario la elige, como una plantilla de mensaje guardadaNo se usa aquí

Por su cuenta, el modelo solo llama a herramientas; la sección 4.4 muestra qué ocurre cuando esperas que lea un recurso.

Visión general de la arquitectura

El diagrama muestra qué partes se ejecutan en tu máquina y qué datos van al modelo:

Arquitectura de FeatureStoreLite: una petición del usuario llega a Claude Desktop, que habla con el modelo Claude fuera de la máquina e inicia un cliente MCP por servidor que llega al servidor FastMCP y al almacén SQLiteArquitectura de FeatureStoreLite: una petición del usuario llega a Claude Desktop, que habla con el modelo Claude fuera de la máquina e inicia un cliente MCP por servidor que llega al servidor FastMCP y al almacén SQLite

  1. El usuario hace una pregunta en lenguaje natural.
  2. Claude Desktop es el host MCP. Envía la conversación al modelo Claude, crea un cliente MCP para este servidor y gestiona la conexión.
  3. Nuestro servidor FastMCP expone tres herramientas (get_feature, store_feature, list_features) y un recurso (schema://main).
  4. SQLite es el almacén de respaldo de los vectores de features.

El host pasa la lista de herramientas a Claude. Claude puede solicitar una llamada a una herramienta, pero es el cliente MCP de cada servidor quien envía los mensajes del protocolo. El servidor y SQLite se ejecutan en local, pero los resultados de las herramientas llegan al modelo como parte de la conversación. Expón solo datos que también pegarías en el chat.


1. Preparación e instalación

1.1. Instalar uv

Si aún no tienes uv, instálalo. El resto del tutorial da por hecho que está en tu PATH.

# macOS/Linux
curl -LsSf https://astral.sh/uv/install.sh | sh

# Or via Homebrew
brew install uv

1.2. Inicializar el proyecto

Crea un directorio nuevo e inicializa un proyecto de Python. uv init crea un pyproject.toml por ti.

# Create project directory
mkdir mcp-featurestore
cd mcp-featurestore

# Initialize Python project
uv init

# Add the MCP SDK with CLI tools
uv add "mcp[cli]>=1.28,<2"

Este tutorial usa la API v1 del SDK de Python de MCP. La documentación oficial del SDK v1 indica a quienes usan la v1 que fijen mcp>=1.28,<2, así que mantén el límite superior <2 hasta que migres el código. uv.lock registra el entorno completo resuelto tras este comando.

El código usa el FastMCP que se incluye en el SDK oficial (mcp.server.fastmcp). El paquete independiente fastmcp es otro proyecto con una API distinta, así que no mezcles su documentación con este código.


2. Construir el servidor

Dos archivos, separados por responsabilidad:

  1. database.py se encarga de las operaciones con SQLite.
  2. featurestore_server.py define el servidor MCP.

2.1. La capa de base de datos (database.py)

Este módulo gestiona la conexión con SQLite y un par de funciones auxiliares. Lo poblamos con dos filas de ejemplo para que el servidor tenga algo que devolver en la primera consulta.

Crea database.py:

# database.py
import json
import os
import sqlite3

def get_db_path() -> str:
    """Get the database path - always in the script's directory"""
    script_dir = os.path.dirname(os.path.abspath(__file__))
    return os.path.join(script_dir, "features.db")

def init_db() -> None:
    """Initialize the feature store database with table and sample data"""
    conn = sqlite3.connect(get_db_path())
    conn.execute("""
        CREATE TABLE IF NOT EXISTS features (
            key TEXT PRIMARY KEY,
            vector TEXT NOT NULL,
            metadata TEXT,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    """)

    # Sample data for experimentation
    example_features = [
        (
            "user_123",
            "[0.1, 0.2, -0.5, 0.8, 0.3, -0.1, 0.9, -0.4]",
            json.dumps({"type": "user", "id": 123, "segment": "premium"}),
        ),
        (
            "product_abc",
            "[0.7, -0.3, 0.4, 0.1, -0.8, 0.6, 0.2, -0.5]",
            json.dumps({"type": "product", "id": "abc", "category": "electronics"}),
        ),
    ]

    # Insert if not exists
    for key, vector, metadata in example_features:
        try:
            conn.execute(
                "INSERT INTO features (key, vector, metadata) VALUES (?, ?, ?)",
                (key, vector, metadata),
            )
        except sqlite3.IntegrityError:
            pass  # Already exists

    conn.commit()
    conn.close()

def get_db_connection() -> sqlite3.Connection:
    """Get a database connection"""
    return sqlite3.connect(get_db_path())

if __name__ == "__main__":
    init_db()
    print("✅ Database initialized successfully!")

Inicializa la base de datos:

uv run python database.py

2.2. El servidor MCP (featurestore_server.py)

FastMCP hace la mayor parte del trabajo. Decoras una función normal de Python y se registra como herramienta o recurso de MCP. FastMCP genera el esquema de entrada de cada herramienta a partir de las anotaciones de tipo y usa el docstring como descripción de la herramienta. El modelo ve ambos, así que escribe el docstring para el modelo: indica qué hace la herramienta y qué forma tiene cada argumento.

Crea featurestore_server.py:

# featurestore_server.py
import json
from typing import Annotated, Any

from mcp.server.fastmcp import FastMCP
from pydantic import Field

from database import get_db_connection, init_db

# Initialize the MCP Server
mcp = FastMCP("FeatureStoreLite")

# Ensure DB is ready when server starts
init_db()

# A float that rejects NaN and infinities
FiniteFloat = Annotated[float, Field(allow_inf_nan=False)]

@mcp.resource("schema://main")
def get_schema() -> str:
    """The CREATE TABLE statements of the feature store database."""
    conn = get_db_connection()
    try:
        schema = conn.execute(
            "SELECT sql FROM sqlite_master WHERE type='table'"
        ).fetchall()
        return "\n".join(sql[0] for sql in schema if sql[0]) or "No tables found."
    finally:
        conn.close()

@mcp.tool()
def store_feature(
    key: str,
    vector: Annotated[list[FiniteFloat], Field(min_length=1)],
    metadata: dict[str, Any] | None = None,
) -> str:
    """Store a feature vector under key. Overwrites an existing key.

    vector: non-empty list of finite numbers, e.g. [0.1, 0.2].
    metadata: optional JSON object, e.g. {"type": "user"}.
    """
    metadata_json = None if metadata is None else json.dumps(metadata, allow_nan=False)
    conn = get_db_connection()
    try:
        conn.execute(
            "INSERT OR REPLACE INTO features (key, vector, metadata) VALUES (?, ?, ?)",
            (key, json.dumps(vector), metadata_json),
        )
        conn.commit()
    finally:
        conn.close()
    return f"Stored feature '{key}'"

@mcp.tool()
def get_feature(key: str) -> str:
    """Return the vector and metadata stored under key, as JSON.

    Call list_features first if you do not know the key.
    """
    conn = get_db_connection()
    try:
        row = conn.execute(
            "SELECT vector, metadata FROM features WHERE key = ?", (key,)
        ).fetchone()
    finally:
        conn.close()

    if row is None:
        raise ValueError(f"Feature '{key}' not found. Call list_features for valid keys.")
    return json.dumps(
        {
            "key": key,
            "vector": json.loads(row[0]),
            "metadata": json.loads(row[1]) if row[1] else None,
        },
        indent=2,
    )

@mcp.tool()
def list_features() -> str:
    """Return a JSON array of every stored feature key."""
    conn = get_db_connection()
    try:
        rows = conn.execute("SELECT key FROM features").fetchall()
        return json.dumps([row[0] for row in rows])
    finally:
        conn.close()

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

Cuatro detalles de este código deciden qué envía el modelo y qué recibe:

  • vector tipado. El esquema de la herramienta le dice al modelo que envíe un array no vacío de números, y el SDK rechaza cualquier otra cosa antes de que se ejecute la función. Si un modelo envía el vector como una cadena JSON como "[1, NaN]", FastMCP analiza primero la cadena. Después allow_inf_nan=False rechaza NaN y los infinitos, que el analizador JSON de Python acepta por defecto.
  • metadata tipado. El esquema pide un objeto JSON. allow_nan=False evita también que NaN llegue a los metadatos almacenados.
  • Errores lanzados. get_feature lanza ValueError si falta la clave. Una excepción lanzada llega al cliente como un error de herramienta con isError: true; una cadena devuelta parece un resultado correcto.
  • Sobrescrituras. store_feature usa INSERT OR REPLACE, así que reemplaza una clave existente. El docstring lo indica, de modo que el modelo lo sabe antes de llamar a la herramienta.

3. Pruebas con MCP Inspector

Antes de conectarlo a Claude, comprueba el servidor con MCP Inspector. Es una pequeña interfaz web que se conecta al servidor igual que lo hace un host. En ella puedes explorar los recursos del servidor, llamar a cada herramienta con tus propios argumentos y ver cada petición y respuesta.

uv run mcp dev featurestore_server.py

El comando inicia el servidor bajo MCP Inspector por stdio. Usa la URL del navegador que imprime el comando. El puerto de la interfaz de Inspector depende de la implementación, así que no des por hecho un puerto fijo.

Llama a get_feature con key="user_123". Si devuelve el JSON de la fila inicial, el servidor funciona. Después llámala con una clave que no exista: el resultado es un error de herramienta con isError: true. Esto comprueba la ruta local del protocolo, el registro de herramientas, la consulta a la base de datos y la ruta de error. No comprueba cómo elige Claude las herramientas; eso lo comprueba la sección 4.

Inspector lee el esquema SQL del recurso schema://main:

MCP Inspector mostrando el recurso get_schema y la sentencia CREATE TABLE de SQLite devuelta


4. Conectar con Claude Desktop

Cuando Inspector confirme que el servidor funciona, regístralo en Claude Desktop.

4.1. Configurar Claude

Edita el archivo de configuración de Claude Desktop:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%/Claude/claude_desktop_config.json

Añade tu servidor al objeto mcpServers:

{
    "mcpServers": {
        "featurestore": {
            "command": "uv",
            "args": [
                "run",
                "--directory",
                "/ABSOLUTE/PATH/TO/mcp-featurestore",
                "--locked",
                "mcp",
                "run",
                "/ABSOLUTE/PATH/TO/mcp-featurestore/featurestore_server.py"
            ]
        }
    }
}

Importante: usa rutas absolutas tanto para el directorio del proyecto como para featurestore_server.py. Claude Desktop inicia el servidor como un proceso aparte. Para un comando como mcp, uv run descubre el proyecto a partir de su directorio de trabajo, así que --directory selecciona el pyproject.toml y el uv.lock de este proyecto; --locked falla en lugar de modificar ese archivo de bloqueo. El comando uv add del paso 1.2 ya ha declarado mcp[cli] en el proyecto.

El SDK también tiene un atajo: uv run mcp install featurestore_server.py escribe por ti la entrada de Claude Desktop. En mcp 1.30 esa entrada ejecuta uv run --frozen --with mcp[cli] mcp run /ABSOLUTE/PATH/featurestore_server.py. No incluye --directory, así que no usa el uv.lock de este proyecto, y --with mcp[cli] no tiene el límite <2. Úsalo para una prueba rápida; la configuración manual de arriba es la reproducible.

4.2. Cómo funciona la interacción

Esto es lo que se ejecuta de principio a fin cuando Claude necesita consultar una feature. Los pasos 1 y 2 se ejecutan una vez, cuando Claude Desktop inicia el servidor. Los pasos 3 a 6 se ejecutan con cada pregunta que necesita datos de features.

Consulta de FeatureStoreLite en seis pasos: descubrimiento de herramientas y exposición del esquema al inicio y, para cada pregunta, una petición get_feature, un mensaje tools/call, la lectura de SQLite y la respuestaConsulta de FeatureStoreLite en seis pasos: descubrimiento de herramientas y exposición del esquema al inicio y, para cada pregunta, una petición get_feature, un mensaje tools/call, la lectura de SQLite y la respuesta

  1. Claude Desktop inicia un cliente MCP dedicado para el servidor y descubre sus herramientas.
  2. El host pone las descripciones de las herramientas a disposición de Claude.
  3. Claude puede solicitar una llamada a una herramienta cuando la pregunta necesita datos del feature store.
  4. El cliente MCP envía esa petición al servidor.
  5. El servidor ejecuta la función de Python y devuelve el resultado al host.
  6. El host puede entregar el resultado a Claude para la respuesta final.

Esta separación entre host, cliente y servidor sigue la especificación de la arquitectura de MCP.

4.3. Consultas de ejemplo

Reinicia Claude Desktop y prueba algunos prompts:

  1. “List all available features.” El resultado determinista contiene las dos claves iniciales: user_123 y product_abc.

  2. “Get the feature vector for user_123.” La respuesta contiene el vector y los metadatos premium de database.py.

  3. “Store a new feature for new_item with the vector [0.5, 0.5] and the metadata {"type": "test"}. Then retrieve new_item and show the stored vector and metadata.” La escritura solo tiene éxito si el vector es una lista no vacía de números finitos y los metadatos son un objeto. La lectura posterior comprueba el ciclo completo de escritura y lectura.

  4. “Get the feature vector for user_999.” get_feature lanza un error por la clave inexistente. Claude debería decir que la clave no existe y puede llamar a list_features para mostrar las claves válidas.

4.4. Cómo leer las respuestas de Claude

El servidor devuelve datos de herramientas y recursos. Claude elige la herramienta y redacta la explicación en torno al resultado.

Pide a Claude que muestre el esquema de la base de datos:

Claude Desktop respondiendo a una petición para mostrar el esquema de la base de datos de FeatureStoreLite

Claude describió el sistema como un almacén NoSQL o de documentos, pero database.py crea una tabla relacional de SQLite. Claude no leyó schema://main. No tiene ninguna herramienta para el esquema, así que lo supuso a partir de las descripciones de las herramientas. MCP no exige que el host pase todos los recursos al modelo: los recursos los controla la aplicación, y Claude Desktop solo pasa uno al modelo cuando lo adjuntas a tu mensaje. Si quieres que el modelo obtenga el esquema por sí mismo, exponlo también como herramienta.

Una explicación verosímil del modelo no es lo mismo que lo que devolvió la herramienta. Cuando el detalle importe, comprueba el resultado de la herramienta o el código fuente.

4.5. Otros hosts MCP

El servidor no depende de Claude Desktop. Cualquier host que inicie servidores stdio en la misma máquina puede ejecutar el mismo comando uv. Claude Desktop usa el JSON de la sección 4.1; los otros dos son:

Un host en otra máquina no puede iniciar un proceso stdio en la tuya. Para un servidor que comparten varias personas, ejecútalo sobre Streamable HTTP con mcp.run(transport="streamable-http") y añade autenticación delante.


5. Solución de problemas

Averigua qué parte ha fallado antes de cambiar el código: los errores de arranque están en los logs, los errores de las herramientas se repiten en Inspector sin un modelo, y una respuesta incorrecta exige comparar el resultado de la herramienta con el texto de Claude.

  • “Server connection failed”:

    • Revisa los logs en ~/Library/Logs/Claude/ en macOS o en %APPDATA%\Claude\logs en Windows. mcp.log registra los eventos de conexión. mcp-server-featurestore.log, en la misma carpeta, contiene el stderr del propio servidor, así que un traceback de arranque aparece ahí (logs de Claude Desktop).
    • Confirma que la configuración usa una ruta absoluta, no una relativa.
    • Confirma que uv está en el PATH de Claude Desktop. Si no lo está, indica la ruta completa del binario (which uv te dirá dónde está).
    • En stdio, stdout transporta los mensajes del protocolo. Escribe los logs en stderr; un print() en el servidor corrompe ese flujo y puede romper la conexión.
  • “Tool execution error”:

    • Reprodúcelo en Inspector con uv run mcp dev featurestore_server.py. Inspector muestra el mensaje de error y los argumentos que lo causaron.
    • Comprueba que features.db se crea junto a database.py. La ruta sale de get_db_path(), que la resuelve respecto al script, así que un directorio de trabajo cambiante no debería mover el archivo.
  • La llamada funciona, pero la respuesta es incorrecta:

    • Abre el resultado de la herramienta en el chat y compáralo con el texto de Claude. La sección 4.4 muestra un caso en el que el modelo supuso en lugar de leer los datos.

6. Antes de que un agente lo use de verdad

  • store_feature sobrescribe cualquier clave, incluidas las filas iniciales. Mantén activa la aprobación por llamada para las herramientas de escritura en el host, o muévelas a un servidor aparte que actives solo cuando lo necesites.
  • Los resultados de las herramientas llegan al proveedor del modelo como parte de la conversación. No devuelvas datos que deban quedarse en la máquina.
  • Un servidor que comparten varias personas necesita Streamable HTTP y autenticación en lugar de stdio (sección 4.5).

7. Conclusión

Eso es todo: un servidor FastMCP, un almacén SQLite y una configuración de Claude Desktop que apunta a un comando uv run. La misma estructura sirve para casi cualquier cosa que puedas envolver en una función de Python. Cambia las llamadas a SQLite por un feature store real, una API interna o un registro de modelos, y el servidor sigue siendo pequeño.

Referencias