Tutorial de un servidor MCP: construirlo 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.
Los desarrolladores de Python pueden convertir un workflow local de feature store en un servidor FastMCP con versiones fijadas para Claude Desktop. Para ello, usan uv para crear el entorno, exponer herramientas y un recurso, y verificar el servidor localmente antes de establecer la conexión con el escritorio.
En resumen: Un servidor FastMCP pequeño basta para exponer una herramienta local útil a un agente. Usa uv para conseguir una configuración reproducible, mantén estrecho el límite del servidor, valida la forma del vector en el límite de la herramienta y prueba la herramienta MCP antes de conectarla a un modelo.
¿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 personalizada independiente para cada herramienta por un conjunto de convenciones.
En este tutorial construiremos un servidor MCP 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 identificados por usuario, producto o documento.
¿Por qué construirlo?
Depurar un pipeline de features suele implicar recurrir a SQL o escribir un script desechable para comprobar un valor. Con el servidor en ejecución, en su lugar puedes preguntar a Claude: «¿Cuál es el vector de features de user_123?» o «Enséñame 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, por lo que usará la dependencia mcp[cli] declarada aquí y las versiones registradas en uv.lock.
Descripción general de la arquitectura
Estas son las cuatro piezas y cómo encajan:
- El usuario formula una pregunta en lenguaje natural.
- Claude Desktop es el host MCP. Crea un cliente MCP para este servidor y gestiona la conexión.
- Nuestro servidor
FastMCPexponeget_featureystore_featurecomo herramientas MCP. - SQLite es el almacén subyacente de los vectores de features.
El host puede poner las herramientas descubiertas a disposición de Claude. Claude puede solicitar un tool call, pero el cliente MCP por servidor envía los mensajes del protocolo.
1. Configuración e instalación
1.1. Instalar uv
Si aún no tienes uv, instálalo. El resto del tutorial presupone 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 MCP para Python. La documentación oficial del SDK v1 indica a los usuarios de v1 que fijen mcp>=1.28,<2, así que mantén el límite superior de <2 hasta migrar el código. uv.lock registra el entorno completo resuelto después de este comando.
El código inline de este artículo es el canónico. El repositorio complementario enlazado en las referencias corresponde a una versión histórica. No reproduce la fijación de dependencias actual ni la validación de vectores actual.
2. Construir el servidor
Dos archivos, separados por responsabilidad:
database.pygestiona las operaciones de SQLite.featurestore_server.pydefine el servidor MCP.
2.1. La capa de base de datos (database.py)
Este módulo es propietario de la conexión a SQLite y de un par de funciones auxiliares. Lo inicializamos 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. Decora una función de Python normal y esta queda registrada como herramienta o recurso MCP. El docstring se convierte en la descripción que ve el LLM. Redáctalo pensando en ese lector.
Crea featurestore_server.py:
# featurestore_server.py
import json
import math
from mcp.server.fastmcp import FastMCP
from database import get_db_connection, init_db
# Initialize the MCP Server
mcp = FastMCP("FeatureStoreLite")
# Ensure DB is ready when server starts
init_db()
def reject_non_finite_json_number(value: str) -> None:
"""Reject NaN and infinities, which are not valid JSON numbers."""
raise ValueError(f"Non-finite JSON number: {value}")
@mcp.resource("schema://main")
def get_schema() -> str:
"""
Resource: Provide the database schema.
Resources provide context to the host application.
The host decides whether to pass that context to the model.
"""
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: str, metadata: str | None = None) -> str:
"""
Tool: Store a feature vector.
Tools are executable functions that LLMs can call to perform actions.
"""
try:
parsed_vector = json.loads(
vector, parse_constant=reject_non_finite_json_number
)
except (json.JSONDecodeError, ValueError):
return "Error: Vector must be valid JSON with finite numbers"
try:
valid_vector = (
isinstance(parsed_vector, list)
and bool(parsed_vector)
and all(
isinstance(value, (int, float))
and not isinstance(value, bool)
and math.isfinite(value)
for value in parsed_vector
)
)
except OverflowError:
valid_vector = False
if not valid_vector:
return "Error: Vector must be a non-empty JSON array of finite numbers (e.g., '[0.1, 0.2]')"
metadata_json = None
if metadata is not None:
try:
parsed_metadata = json.loads(
metadata, parse_constant=reject_non_finite_json_number
)
except (json.JSONDecodeError, ValueError):
return "Error: Metadata must be valid JSON with finite numbers"
if not isinstance(parsed_metadata, dict):
return "Error: Metadata must be a JSON object (e.g., '{\"type\": \"test\"}')"
metadata_json = json.dumps(parsed_metadata, allow_nan=False)
conn = get_db_connection()
try:
conn.execute(
"INSERT OR REPLACE INTO features (key, vector, metadata) VALUES (?, ?, ?)",
(key, json.dumps(parsed_vector, allow_nan=False), metadata_json),
)
conn.commit()
return f"Successfully stored feature '{key}'"
except Exception as e:
return f"Error: {str(e)}"
finally:
conn.close()
@mcp.tool()
def get_feature(key: str) -> str:
"""
Tool: Retrieve a feature vector by key.
"""
conn = get_db_connection()
try:
row = conn.execute(
"SELECT vector, metadata FROM features WHERE key = ?", (key,)
).fetchone()
if row:
return json.dumps(
{
"key": key,
"vector": json.loads(row[0]),
"metadata": json.loads(row[1]) if row[1] else None,
},
indent=2,
)
return f"Feature '{key}' not found."
finally:
conn.close()
@mcp.tool()
def list_features() -> str:
"""
Tool: List all available feature keys.
"""
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()
El parser JSON de Python acepta NaN y los infinitos de forma predeterminada, aunque estén fuera de la gramática de números de JSON. El callback parse_constant rechaza esas representaciones, y math.isfinite comprueba cada elemento del vector antes de que SQLite reciba la fila. Los metadatos siguen un único contrato: deben ser una cadena con un objeto JSON, y el servidor los parsea y normaliza antes de insertarlos. Consulta las notas de interoperabilidad de JSON en Python.
3. Probar con MCP Inspector
Antes de conectarlo a Claude, comprueba el servidor con MCP Inspector. Es una pequeña interfaz web para invocar herramientas y leer recursos directamente.
uv run mcp dev featurestore_server.py
El comando inicia el servidor bajo MCP Inspector mediante 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 que sea fijo.
Así fue mi ejecución original de junio de 2025 en MCP Inspector v0.14.0. La captura registra la configuración stdio de Inspector y una conexión correcta. Su argumento --with mcp, sin fijar, es histórico; cuando reproduzcas ahora el tutorial, usa el comando v1 fijado de este artículo.

Invoca get_feature con key="user_123". Si devuelve el JSON de la fila inicial, el servidor funciona.
Esto comprueba la ruta de protocolo local, el registro de herramientas, la consulta a la base de datos y la salida JSON. No valida la calidad de la búsqueda vectorial. Para retrieval en producción, añade pruebas con consultas representativas, resultados de nearest neighbors esperados, umbrales de distancia, filtros de metadatos y un conjunto de evaluación que corresponda a tu workload.
La misma ejecución expuso las tres herramientas y el recurso schema://main. Estas dos capturas son comprobaciones útiles del discovery: Inspector encontró las funciones y leyó el esquema SQL que devolvió el servidor.


La captura de la herramienta contiene cuatro filas: user_123, product_abc, doc_guide_001 y recommendation_engine. Esa era mi base de datos local más completa del 10 de junio de 2025. Su implementación de list_features devolvía objetos con los campos key y created_at. La función actual devuelve únicamente un array JSON de cadenas de claves y solo inicializa las dos primeras filas. No compares fila por fila la salida histórica con el fixture actual.
4. Conectar con Claude Desktop
Una vez que 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 independiente.uv rundescubre un proyecto a partir de su directorio de trabajo para un comando comomcp, por lo que--directoryselecciona elpyproject.tomly eluv.lockde este proyecto;--lockedfalla en lugar de modificar ese lockfile. El comandouv adddel paso 1.2 ya ha declaradomcp[cli]en el proyecto.
4.2. Cómo funciona la interacción
Esto es lo que ocurre de extremo a extremo cuando Claude necesita consultar una feature:
- Claude Desktop inicia un cliente MCP dedicado para el servidor y descubre sus herramientas.
- El host pone las descripciones de las herramientas a disposición de Claude.
- Claude puede solicitar un tool call cuando la pregunta requiere datos del feature store.
- El cliente MCP envía esa solicitud al servidor.
- El servidor ejecuta la función de Python y devuelve el resultado al host.
- El host puede proporcionar el resultado a Claude para la respuesta final.
Esta separación entre host, cliente y servidor sigue la especificación de arquitectura de MCP. Los recursos también proporcionan contexto a la aplicación host. MCP no obliga al host a pasar todos los recursos al modelo.
4.3. Ejemplos de consultas
Reinicia Claude Desktop y prueba algunos prompts:
-
«Enumera todas las features disponibles». El resultado determinista contiene las dos claves iniciales:
user_123yproduct_abc. -
«Obtén el vector de features de user_123». La respuesta contiene el vector y los metadatos
premiumdedatabase.py. -
«Almacena una feature nueva para
new_itemcon la cadena JSON del vector[0.5, 0.5]y la cadena JSON de metadatos{"type": "test"}. Después recuperanew_itemy muestra el vector y los metadatos almacenados». Ambos argumentos deben contener JSON válido. La escritura solo se realiza después de que el servidor valide el vector y los metadatos. La lectura posterior comprueba el ciclo completo de escritura y lectura.
4.4. Qué mostró mi ejecución de Claude Desktop
Las capturas siguientes corresponden a la misma ejecución de junio de 2025, antes de que redujera a dos filas el fixture del artículo. Muestran lo que Claude Desktop enseñó después de conectarse a mi servidor. Son observaciones de esa ejecución, no una salida MCP garantizada. El servidor devuelve datos de herramientas y recursos; Claude elige una herramienta y redacta la explicación alrededor del resultado.
Primero pedí a Claude que mostrara el esquema de la base de datos:

Claude se equivocó en un detalle importante. Describió el sistema como un almacén NoSQL o documental, aunque database.py usa SQLite y crea una tabla relacional. La columna metadata contiene texto JSON, pero eso no cambia el motor de base de datos. La captura recuerda que una explicación verosímil del modelo no es el contrato de la herramienta. Comprueba la sentencia CREATE TABLE devuelta o el código fuente cuando la distinción sea importante.
También pedí a Claude que enumerara las features disponibles:

Las cuatro claves coinciden con la base de datos local más completa mostrada en Inspector. Etiquetas como «user embedding» y «model embedding» son interpretaciones de Claude basadas en los nombres y los metadatos. list_features solo garantiza las filas que devuelve su implementación.
Por último, recuperé product_abc:

En este caso, el vector y los metadatos procedían del resultado de la herramienta. El texto sobre similitud, recomendaciones y clustering lo generó Claude. Son posibles usos de un embedding, pero el servidor de este tutorial solo almacena y recupera vectores. No implementa una búsqueda de nearest neighbors.
5. Solución de problemas
Conviene conocer algunos modos de fallo:
-
«Server connection failed»:
- Comprueba los logs en
~/Library/Logs/Claude/mcp.logen macOS. - Confirma que la configuración usa una ruta absoluta, no una relativa.
- Confirma que
uvestá en elPATHde Claude Desktop. Si no lo está, indica la ruta completa al binario (which uvte dirá dónde se encuentra).
- Comprueba los logs en
-
«Tool execution error»:
- Reprodúcelo en Inspector con
uv run mcp dev featurestore_server.py. Inspector muestra el error sin procesar, que Claude Desktop normalmente oculta. - Comprueba que
features.dbse crea junto adatabase.py. La ruta procede deget_db_path(), que la resuelve relativa al script, por lo que un directorio de trabajo cambiante no debería mover el archivo.
- Reprodúcelo en Inspector con
6. 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. El mismo patrón funciona para casi cualquier cosa que puedas envolver en una función de Python. Sustituye las llamadas a SQLite por un feature store real, una API interna o un registro de modelos, y el servidor seguirá siendo pequeño.
Puntos clave
- MCP es un límite de herramientas, no una razón para exponer todas las funciones internas.
- Mantén las herramientas del servidor pequeñas, tipadas y fáciles de probar sin un LLM.
- Usa uv para que el entorno del tutorial pueda reconstruirse desde cero.
- Trata el servidor MCP como código de producción en cuanto un agente pueda invocarlo.
Referencias
- Repositorio complementario histórico (no reproduce el código inline actual)
- Introducción a MCP
- SDK de MCP para Python
- Claude Desktop
- uv