Serveur MCP avec Python, uv et FastMCP
Traduction automatique
Cet article a été traduit automatiquement depuis la version originale en anglais.
Ce tutoriel construit un petit serveur MCP de feature store avec FastMCP, le lance avec uv et le connecte à Claude Desktop. Il s’adresse aux développeurs Python qui veulent exposer un workflow de base de données locale à un hôte MCP. À la fin, vous aurez un environnement épinglé sur le SDK v1, un serveur avec trois outils et une ressource, et une vérification locale à lancer avant de connecter Claude Desktop.
Qu’est-ce qu’un serveur MCP ?
Le Model Context Protocol (MCP) est un protocole ouvert pour connecter des applications d’AI à des systèmes externes. Il remplace une intégration personnalisée distincte pour chaque outil par un seul ensemble de conventions.
Dans ce tutoriel, nous construisons un serveur MCP FeatureStoreLite. Il se place entre un LLM et un feature store, c’est-à-dire une base de données de features ML précalculées. Le serveur expose des outils pour interroger et écrire des vecteurs de features indexés par utilisateur, produit ou document.
Pourquoi le construire ?
Déboguer un pipeline de features oblige généralement à ouvrir du SQL ou à écrire un script jetable pour vérifier une valeur. Avec le serveur en marche, vous posez la question à Claude : « Quel est le vecteur de features de user_123 ? » ou « Montre-moi les métadonnées de product_abc. »
Pourquoi utiliser uv ?
Nous utilisons uv pour installer les paquets, 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 utilise ainsi la dépendance mcp[cli] déclarée ici et les versions enregistrées dans uv.lock.
Outils, ressources et prompts
Un serveur MCP peut proposer trois types d’éléments. La spécification MCP les distingue selon qui décide de les utiliser :
| Primitive | Qui la déclenche | Dans ce serveur |
|---|---|---|
| Outil | Le modèle décide de l’appeler | get_feature, store_feature, list_features |
| Ressource | L’hôte décide comment l’inclure dans le contexte | schema://main |
| Prompt | L’utilisateur le choisit, comme un modèle de message | Non utilisé ici |
Les outils permettent au modèle de demander une action. Les ressources fournissent du contexte par l’intermédiaire de l’hôte ; la section 4.4 montre pourquoi exposer une ressource ne garantit pas qu’une réponse l’utilise.
Vue d’ensemble de l’architecture
Le schéma montre quelles parties tournent sur votre machine et quelles données vont au modèle :
- L’utilisateur pose une question en langage naturel.
- Claude Desktop est l’hôte MCP. Il envoie la conversation au modèle Claude, crée un client MCP pour ce serveur et gère la connexion.
- Notre serveur
FastMCPexpose trois outils (get_feature,store_feature,list_features) et une ressource (schema://main). - SQLite est le stockage sous-jacent des vecteurs de features.
L’hôte transmet la liste des outils à Claude. Claude peut demander un tool call, mais c’est le client MCP du serveur concerné qui envoie les messages du protocole. Le serveur et SQLite tournent en local, mais les tool results arrivent au modèle dans la conversation. N’exposez que des données que vous colleriez aussi dans le chat.
1. Configuration et installation
1.1. Installer uv
Si uv n’est pas encore installé, installez-le. Le reste du tutoriel suppose qu’il est 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 un pyproject.toml pour vous.
# 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 du SDK Python MCP v1. La documentation officielle du SDK v1 demande aux utilisateurs de la v1 d’épingler mcp>=1.28,<2 ; gardez donc la borne supérieure <2 jusqu’à la migration du code. uv.lock enregistre l’environnement complet résolu après cette commande.
Le code utilise le FastMCP fourni avec le SDK officiel (mcp.server.fastmcp). Le paquet autonome fastmcp est un projet distinct avec une API différente : ne mélangez pas sa documentation avec ce code.
2. Construire le serveur
Deux fichiers, séparés par responsabilité :
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 y insérons deux lignes d’exemple pour que le serveur ait quelque chose à renvoyer dès 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 fait l’essentiel du travail. Décorez une fonction Python ordinaire et elle est enregistrée comme outil ou ressource MCP. FastMCP construit le schéma d’entrée de chaque outil à partir des annotations de type et utilise la docstring comme description de l’outil. Le modèle voit les deux : écrivez donc la docstring pour le modèle, en indiquant ce que fait l’outil et à quoi ressemble chaque argument.
Créez 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()
Quatre détails de ce code déterminent ce que le modèle envoie et ce qu’il reçoit :
vectortypé. Le tool schema demande un tableau non vide de nombres. Avec le SDK 1.30, Pydantic convertit aussi les booléens et les chaînes numériques en floats :[true, false]devient[1.0, 0.0], et["1.5", "2"]devient[1.5, 2.0]. Il vérifie ensuite que la liste n’est pas vide et que chaque valeur est finie avant l’exécution de la fonction. Si un modèle envoie le vecteur sous forme de chaîne JSON comme"[1, NaN]", FastMCP analyse d’abord la chaîne.allow_inf_nan=Falserejette ensuiteNaNet les infinis, que l’analyseur JSON de Python accepte par défaut.metadatatypé. Le schéma accepte un objet JSON,nullou une valeur omise.allow_nan=Falseempêche aussiNaNd’entrer dans les métadonnées stockées.- Erreurs levées.
get_featurelèveValueErrorpour une clé absente. Une exception levée atteint le client comme une erreur d’outil avecisError: true; une chaîne renvoyée ressemble à un résultat réussi. - Écrasement.
store_featureutiliseINSERT OR REPLACEet remplace donc une clé existante. La docstring le dit, de sorte que le modèle le sait avant d’appeler l’outil.
3. Tester avec MCP Inspector
Avant de le brancher à Claude, vérifiez le serveur avec le MCP Inspector. C’est une petite interface web qui se connecte au serveur comme le fait un hôte. Vous pouvez y parcourir les ressources du serveur, appeler chaque outil avec vos propres arguments et voir chaque requête et chaque réponse.
Le SDK lance l’Inspector via npx. Cette étape nécessite donc aussi Node.js et npm, avec npx dans votre PATH. Inspector 2.10.1 nécessite Node.js 22.19.0 ou une version ultérieure. Vérifiez les commandes installées, puis lancez l’Inspector :
node --version
npx --version
uv run mcp dev featurestore_server.py
La commande démarre le serveur sous le MCP Inspector via stdio. Utilisez l’URL de navigateur affichée par la commande. Le port de l’interface de l’Inspector dépend de l’implémentation : ne supposez pas un port fixe.
Appelez get_feature avec key="user_123". S’il renvoie le JSON de la ligne d’exemple, le serveur fonctionne. Appelez-le ensuite avec une clé qui n’existe pas : le résultat est une erreur d’outil avec isError: true. Cela vérifie le chemin local du protocole, l’enregistrement des outils, la requête en base et le chemin d’erreur. Cela ne teste pas la façon dont Claude choisit ses outils ; la section 4 s’en charge.
L’Inspector lit le schéma SQL depuis la ressource schema://main :
4. Connecter Claude Desktop
Une fois que l’Inspector confirme que le serveur fonctionne, enregistrez-le dans 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 pour le répertoire du projet et pour featurestore_server.py. Claude Desktop démarre le serveur comme un processus séparé. uv run découvre un projet depuis son répertoire de travail pour une commande comme mcp ; --directory sélectionne donc le pyproject.toml et le uv.lock de ce projet, et --locked échoue au lieu de modifier ce fichier de verrouillage. La commande uv add de l’étape 1.2 a déjà déclaré mcp[cli] dans le projet.
Le SDK propose aussi un raccourci : uv run mcp install featurestore_server.py écrit l’entrée Claude Desktop pour vous. Dans mcp 1.30, cette entrée exécute uv run --frozen --with mcp[cli] mcp run /ABSOLUTE/PATH/featurestore_server.py. Elle n’a pas de --directory, donc elle n’utilise pas le uv.lock de ce projet, et --with mcp[cli] n’a pas de borne <2. Utilisez-la pour un essai rapide ; la configuration manuelle ci-dessus est celle qui est reproductible.
4.2. Comment fonctionne l’interaction
Voici ce qui s’exécute de bout en bout quand Claude a besoin d’une recherche de feature. Les étapes 1 et 2 ont lieu une fois, au démarrage du serveur par Claude Desktop. Les étapes 3 à 6 ont lieu pour chaque question qui nécessite des données de features.
- 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 quand la question nécessite des données du feature store.
- Le client MCP envoie cette requête 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 hôte-client-serveur suit la spécification d’architecture MCP.
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 insérées au départ :
user_123etproduct_abc. -
“Récupère le vecteur de features de user_123.” La réponse contient le vecteur et les métadonnées
premiumdedatabase.py. -
“Stocke une nouvelle feature pour
new_itemavec le vecteur[0.5, 0.5]et les métadonnées{"type": "test"}. Récupère ensuitenew_itemet affiche le vecteur et les métadonnées stockés.” Après conversion, le vecteur validé doit être non vide et ne contenir que des floats finis. Les métadonnées peuvent être un objet JSON,nullou être omises. La lecture qui suit vérifie l’aller-retour complet écriture/lecture. -
“Récupère le vecteur de features de user_999.”
get_featurelève une erreur pour la clé absente. Claude devrait dire que la clé n’existe pas, et il peut appelerlist_featurespour afficher les clés valides.
4.4. Lire les réponses de Claude
Le serveur renvoie des données d’outils et de ressources. Claude choisit l’outil et rédige l’explication autour du résultat.
Cette capture montre une exécution de juin 2025 avec une version antérieure de l’exemple. J’ai demandé à Claude d’afficher le schéma de la base de données :
Claude a décrit le système comme un stockage NoSQL ou de documents et a fourni un schéma conceptuel, mais database.py crée une table SQLite relationnelle. La réponse affichée n’est pas l’instruction CREATE TABLE renvoyée par schema://main. La capture n’indique pas si Claude Desktop a lu cette ressource ni quel contexte Claude a reçu. Elle ne permet donc pas d’établir la cause de l’erreur.
Les ressources sont pilotées par l’application : l’hôte décide comment les inclure dans le contexte du modèle, par sélection de l’utilisateur ou inclusion automatique. MCP n’oblige pas l’hôte à transmettre chaque ressource au modèle. Si le modèle doit demander le schéma par un tool call, exposez-le aussi comme outil.
Une explication plausible du modèle n’est pas ce que l’outil a renvoyé. Quand le détail compte, vérifiez le tool result ou le code source.
4.5. Autres hôtes MCP
Le serveur ne dépend pas de Claude Desktop. Tout hôte qui démarre des serveurs stdio sur la même machine peut exécuter la même commande uv. Claude Desktop utilise le JSON de la section 4.1 ; les deux autres sont :
- Claude Code :
claude mcp add featurestore -- uv run --directory /ABSOLUTE/PATH/TO/mcp-featurestore --locked mcp run /ABSOLUTE/PATH/TO/mcp-featurestore/featurestore_server.py. Tout ce qui suit--est la commande qui démarre le serveur. - Cursor lit le même format JSON
mcpServersdepuis~/.cursor/mcp.json, ou.cursor/mcp.jsondans un projet.
Un hôte sur une autre machine ne peut pas démarrer un processus stdio sur la vôtre. Pour un serveur partagé par plusieurs personnes, exécutez-le en Streamable HTTP avec mcp.run(transport="streamable-http") et placez une authentification devant.
5. Dépannage
Trouvez quelle partie a échoué avant de modifier le code : les erreurs de démarrage sont dans les logs, les erreurs d’outil se reproduisent dans l’Inspector sans modèle, et une mauvaise réponse demande de comparer le tool result avec le texte de Claude.
-
« Server connection failed » :
- Consultez les logs dans
~/Library/Logs/Claude/sous macOS ou%APPDATA%\Claude\logssous Windows.mcp.logenregistre les événements de connexion.mcp-server-featurestore.logdans le même dossier contient le stderr du serveur lui-même : une trace de démarrage s’y trouve donc (logs de Claude Desktop). - Vérifiez que la configuration utilise un chemin absolu, pas un chemin relatif.
- Vérifiez que
uvest dans lePATHde Claude Desktop. Sinon, indiquez le chemin complet du binaire (which uvvous dira où il se trouve). - Avec stdio, stdout transporte les messages du protocole. Écrivez les logs sur stderr ; un
print()dans le serveur corrompt ce flux et peut casser la connexion.
- Consultez les logs dans
-
« Tool execution error » :
- Reproduisez l’erreur dans l’Inspector avec
uv run mcp dev featurestore_server.py. L’Inspector affiche le message d’erreur et les arguments qui l’ont provoquée. - Vérifiez que
features.dbest bien créé à côté dedatabase.py. Le chemin vient deget_db_path(), qui le résout par rapport au script : un répertoire de travail différent ne devrait donc pas déplacer le fichier.
- Reproduisez l’erreur dans l’Inspector avec
-
L’appel réussit, mais la réponse est fausse :
- Ouvrez le tool result dans le chat et comparez-le avec le texte de Claude. La section 4.4 montre une réponse du modèle qui décrit mal la base de données.
6. Avant qu’un agent l’utilise pour de bon
store_featureécrase n’importe quelle clé, y compris les lignes d’exemple. Gardez l’approbation à chaque appel pour les outils d’écriture dans l’hôte, ou déplacez-les vers un serveur séparé que vous n’activez qu’en cas de besoin.- Les tool results parviennent au fournisseur du modèle dans la conversation. Ne renvoyez pas de données qui doivent rester sur la machine.
- Un serveur partagé par plusieurs personnes nécessite Streamable HTTP et une authentification plutôt que stdio (section 4.5).
7. Conclusion
Voilà l’ensemble : un serveur FastMCP, un stockage SQLite et une configuration Claude Desktop qui pointe vers une commande uv run. La même structure convient à la plupart de ce qu’on peut envelopper dans une fonction Python. Remplacez les appels SQLite par un vrai feature store, une API interne ou un registre de modèles, et le serveur reste petit.

