Tutoriel sur les serveurs MCP : développement avec Python, uv et FastMCP
Traduction automatique Cet article a été traduit automatiquement depuis la version originale en anglais.
Les développeurs Python peuvent transformer un workflow local de feature store en serveur FastMCP versionné pour Claude Desktop, en utilisant uv pour créer l’environnement, exposer des outils et une ressource, puis vérifier le serveur localement avant d’établir la connexion avec l’application desktop.
En bref : un petit serveur FastMCP suffit pour exposer un outil local utile à un agent. Utilisez uv pour obtenir une configuration reproductible, limitez la frontière du serveur, validez la forme du vecteur à la frontière de l’outil et testez l’outil MCP avant de le connecter à un modèle.
Qu’est-ce qu’un serveur MCP ?
Le Model Context Protocol (MCP) est un protocole ouvert permettant de connecter des applications AI à des systèmes externes. Il remplace une intégration personnalisée distincte pour chaque outil par un ensemble unique de conventions.
Dans ce tutoriel, nous allons créer un serveur MCP FeatureStoreLite. Il se place entre un LLM et un feature store, c’est-à-dire une base de données contenant des features ML précalculées. Le serveur expose des outils permettant d’interroger et d’écrire des vecteurs de features indexés par utilisateur, produit ou document.
Pourquoi le construire ?
Le debugging d’un pipeline de features consiste généralement à passer par SQL ou à écrire un script temporaire pour vérifier une valeur. Une fois le serveur lancé, vous pouvez plutôt demander à Claude : « Quel est le vecteur de features de user_123 ? » ou « Affiche-moi les métadonnées de product_abc. »
Pourquoi utiliser uv ?
Nous utiliserons uv pour installer les packages, résoudre les dépendances et gérer l’environnement virtuel. La configuration de Claude Desktop lancera ce projet depuis son propre répertoire avec --locked. Elle utilisera donc la dépendance mcp[cli] déclarée ici ainsi que les versions enregistrées dans uv.lock.
Vue d’ensemble de l’architecture
Voici les quatre composants et leur articulation :
- L’utilisateur pose une question en langage naturel.
- Claude Desktop est l’hôte MCP. Il crée un client MCP pour ce serveur et gère la connexion.
- Notre serveur
FastMCPexposeget_featureetstore_featurecomme outils MCP. - SQLite est le stockage sous-jacent des vecteurs de features.
L’hôte peut mettre les outils découverts à la disposition de Claude. Claude peut demander un tool call, mais c’est le client MCP dédié au serveur qui envoie les messages du protocole.
1. Configuration et installation
1.1. Installer uv
Si vous n’avez pas encore uv, installez-le. La suite du tutoriel suppose qu’il se trouve dans votre PATH.
# macOS/Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# Or via Homebrew
brew install uv
1.2. Initialiser le projet
Créez un nouveau répertoire et initialisez un projet Python. uv init crée automatiquement un pyproject.toml.
# 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"
Ce tutoriel utilise l’API v1 du SDK MCP Python. La documentation officielle du SDK v1 indique aux utilisateurs de la v1 de verrouiller mcp>=1.28,<2. Conservez donc la borne supérieure de <2 jusqu’à la migration du code. uv.lock enregistre l’environnement complet résolu après cette commande.
Le code inline de cet article fait foi. Le repository associé indiqué dans les références correspond à une version historique. Il ne reproduit ni le verrouillage actuel des dépendances ni la validation actuelle des vecteurs.
2. Construire le serveur
Deux fichiers, séparés selon leurs responsabilités :
database.pygère les opérations SQLite.featurestore_server.pydéfinit le serveur MCP.
2.1. La couche base de données (database.py)
Ce module gère la connexion SQLite et quelques fonctions utilitaires. Nous l’initialisons avec deux lignes d’exemple afin que le serveur ait des données à renvoyer lors de la première requête.
Créez 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!")
Initialisez la base de données :
uv run python database.py
2.2. Le serveur MCP (featurestore_server.py)
FastMCP effectue l’essentiel du travail. Annotez une fonction Python ordinaire et elle sera enregistrée comme outil ou ressource MCP. La docstring devient la description visible par le LLM. Rédigez-la en pensant à ce lecteur.
Créez 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()
L’analyseur JSON de Python accepte par défaut NaN et les infinis, bien qu’ils soient hors de la grammaire des nombres JSON. Le callback parse_constant rejette ces écritures, tandis que math.isfinite vérifie chaque élément du vecteur avant que SQLite ne reçoive la ligne. Les métadonnées suivent un contrat unique : elles doivent être une chaîne représentant un objet JSON, puis le serveur les analyse et les normalise avant l’insertion. Consultez les notes de Python sur l’interopérabilité JSON.
3. Tester avec MCP Inspector
Avant de connecter le serveur à Claude, effectuez une vérification rapide avec MCP Inspector. Il s’agit d’une petite interface web permettant d’appeler directement les outils et de lire les ressources.
uv run mcp dev featurestore_server.py
Cette commande démarre le serveur sous MCP Inspector via stdio. Utilisez l’URL du navigateur affichée par la commande. Le port de l’interface Inspector dépend de l’implémentation : ne supposez donc pas qu’il soit fixe.
Voici à quoi ressemblait mon exécution originale de juin 2025 avec MCP Inspector v0.14.0. La capture enregistre la configuration stdio d’Inspector et une connexion réussie. Son argument --with mcp non verrouillé est historique ; utilisez la commande v1 verrouillée de cet article si vous reproduisez le tutoriel aujourd’hui.

Appelez get_feature avec key="user_123". Si la commande renvoie le JSON de la ligne initiale, le serveur fonctionne.
Cela vérifie le chemin de protocole local, l’enregistrement des outils, la recherche dans la base de données et la sortie JSON. Cela ne valide pas la qualité de la recherche vectorielle. Pour un système de retrieval en production, ajoutez des tests avec des requêtes représentatives, des résultats de plus proches voisins attendus, des seuils de distance, des filtres de métadonnées et un jeu d’évaluation correspondant à votre workload.
La même exécution a exposé les trois outils ainsi que la ressource schema://main. Ces deux captures constituent de bonnes vérifications de la discovery : Inspector a trouvé les fonctions et lu le schéma SQL renvoyé par le serveur.


La capture de l’outil contient quatre lignes : user_123, product_abc, doc_guide_001 et recommendation_engine. Il s’agissait de ma base de données locale plus riche du 10 juin 2025. Son implémentation de list_features renvoyait des objets contenant les champs key et created_at. La fonction actuelle renvoie uniquement un tableau JSON de chaînes de clés et n’initialise que les deux premières lignes. Ne comparez pas ligne par ligne la sortie historique avec la fixture actuelle.
4. Connecter le serveur à Claude Desktop
Une fois qu’Inspector a confirmé que le serveur fonctionne, enregistrez-le auprès de Claude Desktop.
4.1. Configurer Claude
Modifiez le fichier de configuration de Claude Desktop :
- macOS :
~/Library/Application Support/Claude/claude_desktop_config.json - Windows :
%APPDATA%/Claude/claude_desktop_config.json
Ajoutez votre serveur à l’objet mcpServers :
{
"mcpServers": {
"featurestore": {
"command": "uv",
"args": [
"run",
"--directory",
"/ABSOLUTE/PATH/TO/mcp-featurestore",
"--locked",
"mcp",
"run",
"/ABSOLUTE/PATH/TO/mcp-featurestore/featurestore_server.py"
]
}
}
}
Important : utilisez des chemins absolus à la fois pour le répertoire du projet et pour
featurestore_server.py. Claude Desktop démarre le serveur comme un processus distinct.uv rundétecte un projet depuis son répertoire de travail pour une commande telle quemcp. Ainsi,--directorysélectionne lepyproject.tomlet leuv.lockde ce projet ;--lockedéchoue ensuite au lieu de modifier ce lockfile. La commandeuv addde l’étape 1.2 a déjà déclarémcp[cli]dans le projet.
4.2. Fonctionnement de l’interaction
Voici ce qui s’exécute de bout en bout lorsque Claude doit effectuer une recherche de feature :
- Claude Desktop démarre un client MCP dédié au serveur et découvre ses outils.
- L’hôte met les descriptions des outils à la disposition de Claude.
- Claude peut demander un tool call lorsque la question nécessite des données du feature store.
- Le client MCP envoie cette demande au serveur.
- Le serveur exécute la fonction Python et renvoie le résultat à l’hôte.
- L’hôte peut fournir le résultat à Claude pour la réponse finale.
Cette séparation entre hôte, client et serveur suit la spécification de l’architecture MCP. Les ressources fournissent également du contexte à l’application hôte. MCP n’oblige pas l’hôte à transmettre chaque ressource au modèle.
4.3. Exemples de requêtes
Redémarrez Claude Desktop et essayez quelques prompts :
-
« Liste toutes les features disponibles. » Le résultat déterministe contient les deux clés initialisées :
user_123etproduct_abc. -
« Récupère le vecteur de features de user_123. » La réponse contient le vecteur ainsi que les métadonnées
premiumprovenant dedatabase.py. -
« Stocke une nouvelle feature pour
new_itemavec la chaîne JSON de vecteur[0.5, 0.5]et la chaîne JSON de métadonnées{"type": "test"}. Récupère ensuitenew_itemet affiche le vecteur et les métadonnées stockés. » Les deux arguments doivent contenir du JSON valide. L’écriture ne réussit qu’après validation du vecteur et des métadonnées par le serveur. La lecture suivante vérifie l’aller-retour complet écriture/lecture.
4.4. Ce qu’a montré mon exécution avec Claude Desktop
Les captures suivantes proviennent de la même exécution de juin 2025, avant que je réduise la fixture de l’article à deux lignes. Elles montrent ce que Claude Desktop affichait après sa connexion à mon serveur. Il s’agit d’observations de cette exécution, et non d’une sortie MCP garantie. Le serveur renvoie les données des outils et des ressources ; Claude choisit un outil et rédige l’explication autour du résultat.
J’ai d’abord demandé à Claude d’afficher le schéma de la base de données :

Claude s’est trompé sur un point important. Il a qualifié le système de base NoSQL ou de document store, alors que database.py utilise SQLite et crée une table relationnelle. La colonne metadata contient du texte JSON, mais cela ne change pas le moteur de base de données. Cette capture rappelle utilement qu’une explication plausible du modèle ne constitue pas le contrat de l’outil. Vérifiez l’instruction CREATE TABLE renvoyée ou le code source lorsque cette distinction est importante.
J’ai également demandé à Claude de lister les features disponibles :

Les quatre clés correspondent à la base de données locale plus riche affichée dans Inspector. Les libellés tels que « user embedding » et « model embedding » sont des interprétations de Claude fondées sur les noms et les métadonnées. list_features garantit uniquement les lignes renvoyées par son implémentation.
Enfin, j’ai récupéré product_abc :

Ici, le vecteur et les métadonnées provenaient du résultat de l’outil. Le texte sur la similarité, les recommandations et le clustering provenait de Claude. Il s’agit d’usages possibles d’un embedding, mais le serveur de ce tutoriel se contente de stocker et de récupérer des vecteurs. Il n’implémente pas de recherche des plus proches voisins.
5. Dépannage
Voici quelques modes de défaillance à connaître :
-
« Échec de la connexion au serveur » :
- Vérifiez les logs à l’emplacement
~/Library/Logs/Claude/mcp.logsur macOS. - Vérifiez que la configuration utilise un chemin absolu et non un chemin relatif.
- Vérifiez que
uvse trouve dans lePATHde Claude Desktop. Si ce n’est pas le cas, indiquez le chemin complet vers le binaire (which uvvous indiquera où il se trouve).
- Vérifiez les logs à l’emplacement
-
« Erreur d’exécution de l’outil » :
- Reproduisez l’erreur dans Inspector avec
uv run mcp dev featurestore_server.py. Inspector affiche l’erreur brute, que Claude Desktop masque généralement. - Vérifiez que
features.dbest créé à côté dedatabase.py. Le chemin provient deget_db_path(), qui le résout relativement au script ; un changement de répertoire de travail ne devrait donc pas déplacer le fichier.
- Reproduisez l’erreur dans Inspector avec
6. Conclusion
C’est tout : un serveur FastMCP, un stockage SQLite et une configuration Claude Desktop qui pointe vers une commande uv run. Cette structure fonctionne pour la plupart des systèmes que vous pouvez encapsuler dans une fonction Python. Remplacez les appels SQLite par un véritable feature store, une API interne ou un registre de modèles, et le serveur reste compact.
Points clés
- MCP définit une frontière d’outils ; ce n’est pas une raison pour exposer toutes les fonctions internes.
- Gardez des outils serveur petits, typés et faciles à tester sans LLM.
- Utilisez uv afin de pouvoir reconstruire l’environnement du tutoriel depuis zéro.
- Traitez le serveur MCP comme du code de production dès qu’un agent peut l’appeler.
Références
- Repository associé historique (ne reproduit pas le code inline actuel)
- Introduction à MCP
- SDK MCP Python
- Claude Desktop
- uv