Votre premier serveur MCP STDIO en Python
FastMCP (SDK officiel Anthropic) réduit la création d’un serveur MCP à quelques décorateurs Python. En moins d’une heure, vous construisez mcp-travel — deux outils simulés (get_weather et get_currency) testables dans Claude Desktop, Claude Code CLI et OpenCode sans modifier une ligne de code serveur. Cet article pose les fondations et le prochain (partie 6) vous proposera de câbler les vraies APIs.
- Série : MCP-101 — Partie 5 / 12
- Niveau : Python intermédiaire — classes, décorateurs, async
- Stack : uv · Python 3.12 · FastMCP (SDK officiel) · Claude Desktop
De la sécurité à la pratique
L’article précédent de cette série (la partie 4) a cartographié les risques : tool poisoning, exfiltration silencieuse, rug pull. La meilleure façon de comprendre ces vecteurs d’attaque, c’est d’écrire soi-même un serveur MCP — et de voir exactement ce qu’il peut faire.
Ce cinquième article marque le passage de la théorie à la pratique. Nous allons créer mcp-travel, un serveur MCP STDIO Python avec deux outils : get_weather pour la météo d’une destination, et get_currency pour convertir des devises. Les valeurs retournées seront codées en dur, sans appel réseau externe pour cette première partie. L’objectif est de maîtriser la mécanique du protocole et de FastMCP avant d’introduire la complexité des APIs réelles, qui fait l’objet de la partie 6.
La thématique “voyage” n’est pas anodine : regrouper des outils par domaine métier (météo + devises = contexte voyage) plutôt que par technologie est un réflexe de design MCP qu’on retrouvera tout au long de cette série. Un serveur MCP bien nommé et bien découpé se comprend, se teste et se maintient plus facilement.
Durée estimée : 30 à 45 minutes pour un développeur Python à l’aise avec les décorateurs.
Pourquoi FastMCP — et lequel ?
Avant d’écrire la première ligne, une clarification est nécessaire : il existe deux packages qui s’appellent FastMCP, et la confusion est fréquente.
Le SDK officiel Anthropic
from mcp.server.fastmcp import FastMCP
Ce FastMCP est intégré dans le package mcp maintenu par Anthropic (David Soria Parra, Anthropic PBC). Version actuelle : mcp 1.28.1, publiée le 26 juin 2026. Il est disponible dès que vous installez mcp — pas de dépendance supplémentaire. Stable, “officiel”, adapté pour débuter.
C’est ce que nous utilisons dans cette série.
Le package standalone jlowin
from fastmcp import FastMCP
Ce FastMCP est un package indépendant maintenu par Jerrod Linderman (jlowin), le créateur d’origine. Version actuelle : v3.4.2. Il ajoute des fonctionnalités absentes du SDK officiel : composition de serveurs, proxying, intégration OpenAPI/FastAPI. Environ 70% des serveurs MCP en production l’utilisent.
Si vous construisez un serveur de production avec des besoins avancés, le standalone mérite l’exploration. Pour apprendre le protocole, le SDK officiel suffit largement et évite les questions de provenance.
Pourquoi pas le bas niveau ?
Le SDK Python MCP expose aussi une classe Server de bas niveau, qui demande de gérer manuellement les handlers de méthodes, la sérialisation JSON-RPC et la boucle stdin/stdout. FastMCP enveloppe tout ça derrière des décorateurs. Là où le bas niveau prend une cinquantaine de lignes pour exposer un seul outil, FastMCP le fait en cinq.
Architecture de mcp-travel
Avant de coder, voici ce qui se passe lorsqu’un hôte MCP appelle un outil :
Claude Desktop (hôte MCP)
└─ MCP Client
└─ STDIO (stdin/stdout) ──> server.py [processus Python]
├─ @mcp.tool get_weather(city)
└─ @mcp.tool get_currency(from_currency,
to_currency, amount)
STDIO signifie que le serveur Python tourne en arrière-plan comme un processus ordinaire. L’hôte MCP (Claude Desktop, Claude Code CLI, OpenCode…) le démarre au lancement, lui envoie des messages via stdin, lit les réponses sur stdout. Pas de port réseau, pas d’exposition externe, pas de serveur HTTP à gérer. L’hôte gère entièrement le cycle de vie du processus.
Cette architecture a une conséquence importante pour le code : tout ce qu’on écrit sur stdout est intercepté par le protocole MCP. Un simple print("debug") corrompt les messages JSON-RPC et fait planter la connexion. On y revient dans la section code.
mcp-travel expose deux outils dans ce même processus :
get_weather(city)— retourne les conditions météo d’une ville (température, description, humidité)get_currency(from_currency, to_currency, amount)— convertit un montant entre deux devises
Dans cet article, les deux retournent des données codées en dur. La logique métier “voyage” justifie de les grouper dans un même serveur : un assistant de voyage qui aide à préparer un séjour a naturellement besoin des deux.
Mise en place avec uv
Deux prérequis : Python 3.10 ou supérieur (3.12 recommandé), et uv installé.
# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# Windows (PowerShell)
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
Redémarrez votre terminal après l’installation pour que la commande uv soit disponible.
Créer le projet
uv init mcp-travel --python 3.12
cd mcp-travel
uv add "mcp[cli]"
Le flag --python 3.12 est essentiel : il fixe à la fois le fichier .python-version et le champ requires-python = ">=3.12" dans pyproject.toml. Sans lui, uv init génère requires-python = ">=3.9" et la résolution de mcp[cli] (qui exige Python ≥ 3.10) échoue. Si Python 3.12 n’est pas encore installé sur votre machine, uv le télécharge automatiquement.
Après uv init, la structure est minimale :
mcp-travel/
main.py ← généré par uv (on va le remplacer par server.py)
pyproject.toml
README.md
.gitignore
.python-version ← contient "3.12"
Le premier uv add ou uv run crée automatiquement .venv/ et uv.lock. Pas de python -m venv, pas d’activation manuelle.
L’extra [cli] installe deux choses utiles au-delà du SDK de base : mcp dev, l’inspecteur interactif pour tester votre serveur sans Claude Desktop ou un autre hôte MCP, et les outils de diagnostic du protocole.
Renommer le fichier principal
uv init génère main.py. Renommez-le (ou créez directement) server.py :
# macOS / Linux
mv main.py server.py
# Windows
ren main.py server.py
Vérifier le pyproject.toml
[project]
name = "mcp-travel"
version = "0.1.0"
description = "Serveur MCP de voyage — météo et devises"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"mcp[cli]>=1.2.0",
]
Structure finale attendue :
mcp-travel/
server.py ← votre code
pyproject.toml
uv.lock
.venv/
README.md
Écrire les deux outils — get_weather et get_currency
Ouvrez server.py et remplacez son contenu par ce qui suit, section par section.
Import et instanciation
import json
import sys
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("mcp-travel")
Le nom passé à FastMCP("mcp-travel") apparaît dans l’interface de l’hôte MCP pour identifier votre serveur. Choisissez un nom lisible.
Pourquoi json et sys ?
jsonsert à sérialiser les réponses des outils en chaînes JSON — convention MCP pour les retours structurés.sysest nécessaire pour écrire surstderrsi vous avez besoin de logger quoi que ce soit :print("debug", file=sys.stderr). Toute écriture surstdoutcorrompt le protocole.
get_weather
@mcp.tool()
def get_weather(city: str) -> str:
"""Retourne les conditions météo actuelles pour une ville donnée.
Args:
city: Nom de la ville (ex: 'Paris', 'Tokyo', 'New York')
"""
# Données simulées — Part 6 appellera une vraie API météo
weather_data = {
"Paris": {"temperature_c": 18, "conditions": "Nuageux", "humidity_pct": 72},
"Tokyo": {"temperature_c": 28, "conditions": "Ensoleillé", "humidity_pct": 65},
"New York": {"temperature_c": 22, "conditions": "Partiellement nuageux", "humidity_pct": 58},
"Berlin": {"temperature_c": 15, "conditions": "Pluvieux", "humidity_pct": 85},
"Reykjavik": {"temperature_c": 4, "conditions": "Venteux", "humidity_pct": 80},
}
data = weather_data.get(city, {
"temperature_c": 20,
"conditions": "Données non disponibles pour cette ville",
"humidity_pct": 60,
})
return json.dumps({"city": city, **data}, ensure_ascii=False)
Trois points à retenir :
Les annotations de type sont obligatoires. FastMCP lit city: str pour générer le JSON Schema du tool. Sans annotation, le schema est incomplet et Claude ne peut pas construire les arguments — l’outil sera ignoré ou produira une erreur.
La docstring est l’interface du LLM. Claude lit la description de l’outil et la docstring de chaque paramètre pour décider quand appeler get_weather et quoi lui passer. Une docstring floue = un outil mal utilisé.
On retourne une chaîne JSON, pas un dict. Les outils MCP retournent du texte. json.dumps() sérialise proprement la réponse et Claude peut l’interpréter comme structure de données dans sa réponse naturelle.
get_currency
@mcp.tool()
def get_currency(from_currency: str, to_currency: str, amount: float = 1.0) -> str:
"""Convertit un montant d'une devise vers une autre.
Args:
from_currency: Code ISO de la devise source (ex: 'EUR', 'USD', 'GBP', 'JPY')
to_currency: Code ISO de la devise cible
amount: Montant à convertir (défaut : 1.0)
"""
# Taux simulés réalistes (juillet 2026 approximatif)
rates = {
("EUR", "USD"): 1.08,
("EUR", "GBP"): 0.84,
("EUR", "JPY"): 163.0,
("USD", "EUR"): 0.93,
("USD", "GBP"): 0.78,
("USD", "JPY"): 151.0,
("GBP", "EUR"): 1.19,
("GBP", "USD"): 1.28,
("GBP", "JPY"): 194.0,
("JPY", "EUR"): 0.0061,
("JPY", "USD"): 0.0066,
("JPY", "GBP"): 0.0052,
}
if from_currency == to_currency:
return json.dumps({
"from": from_currency,
"to": to_currency,
"amount": amount,
"converted": amount,
"rate": 1.0,
})
rate = rates.get((from_currency, to_currency))
if rate is None:
return json.dumps({"error": f"Taux {from_currency}/{to_currency} non disponible"})
converted = round(amount * rate, 2)
return json.dumps({
"from": from_currency,
"to": to_currency,
"amount": amount,
"converted": converted,
"rate": rate,
})
Le paramètre amount: float = 1.0 illustre la valeur par défaut FastMCP : Claude peut appeler get_currency("EUR", "JPY") sans préciser de montant, et obtient le taux unitaire.
Point d’entrée
if __name__ == "__main__":
mcp.run(transport="stdio")
mcp.run(transport="stdio") démarre la boucle de lecture/écriture sur stdin/stdout. transport="stdio" est explicite ici — FastMCP peut aussi gérer HTTP streamable (partie 11), on évite l’ambiguïté.
Fichier complet
Voici server.py dans son intégralité :
import json
import sys # pour print(..., file=sys.stderr) dans les logs de débogage
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("mcp-travel")
@mcp.tool()
def get_weather(city: str) -> str:
"""Retourne les conditions météo actuelles pour une ville donnée.
Args:
city: Nom de la ville (ex: 'Paris', 'Tokyo', 'New York')
"""
weather_data = {
"Paris": {"temperature_c": 18, "conditions": "Nuageux", "humidity_pct": 72},
"Tokyo": {"temperature_c": 28, "conditions": "Ensoleillé", "humidity_pct": 65},
"New York": {"temperature_c": 22, "conditions": "Partiellement nuageux", "humidity_pct": 58},
"Berlin": {"temperature_c": 15, "conditions": "Pluvieux", "humidity_pct": 85},
"Reykjavik": {"temperature_c": 4, "conditions": "Venteux", "humidity_pct": 80},
}
data = weather_data.get(city, {
"temperature_c": 20,
"conditions": "Données non disponibles pour cette ville",
"humidity_pct": 60,
})
return json.dumps({"city": city, **data}, ensure_ascii=False)
@mcp.tool()
def get_currency(from_currency: str, to_currency: str, amount: float = 1.0) -> str:
"""Convertit un montant d'une devise vers une autre.
Args:
from_currency: Code ISO de la devise source (ex: 'EUR', 'USD', 'GBP', 'JPY')
to_currency: Code ISO de la devise cible
amount: Montant à convertir (défaut : 1.0)
"""
rates = {
("EUR", "USD"): 1.08,
("EUR", "GBP"): 0.84,
("EUR", "JPY"): 163.0,
("USD", "EUR"): 0.93,
("USD", "GBP"): 0.78,
("USD", "JPY"): 151.0,
("GBP", "EUR"): 1.19,
("GBP", "USD"): 1.28,
("GBP", "JPY"): 194.0,
("JPY", "EUR"): 0.0061,
("JPY", "USD"): 0.0066,
("JPY", "GBP"): 0.0052,
}
if from_currency == to_currency:
return json.dumps({
"from": from_currency,
"to": to_currency,
"amount": amount,
"converted": amount,
"rate": 1.0,
})
rate = rates.get((from_currency, to_currency))
if rate is None:
return json.dumps({"error": f"Taux {from_currency}/{to_currency} non disponible"})
converted = round(amount * rate, 2)
return json.dumps({
"from": from_currency,
"to": to_currency,
"amount": amount,
"converted": converted,
"rate": rate,
})
if __name__ == "__main__":
mcp.run(transport="stdio")
Vérifiez que le serveur démarre sans erreur :
uv run python server.py
Le processus reste en attente (en lecture sur stdin). Utilisez Ctrl+C pour l’arrêter.
Tester avec l’inspecteur mcp dev (optionnel — nécessite Node.js)
mcp dev lance le MCP Inspector, une interface web qui affiche les outils disponibles et permet de les appeler manuellement. Il requiert Node.js (et donc npx).
# Installer Node.js si absent (macOS)
brew install node
# Lancer l'inspecteur
uv run mcp dev server.py
# → ouvre http://localhost:5173 dans le navigateur
Si Node.js n’est pas installé, ignorez cette étape : Claude Desktop fait le même travail de validation et la section suivante suffit pour confirmer que le serveur fonctionne.
Configurer Claude Desktop et tester
Localiser le fichier de configuration
Claude Desktop lit sa configuration MCP dans un fichier JSON :
| OS | Emplacement |
|---|---|
| macOS | ~/Library/Application Support/Claude/claude_desktop_config.json |
| Windows | %APPDATA%\Claude\claude_desktop_config.json |
Créez le fichier s’il n’existe pas encore. Ouvrez-le et ajoutez :
{
"mcpServers": {
"mcp-travel": {
"command": "uv",
"args": [
"--directory",
"/chemin/absolu/vers/mcp-travel",
"run",
"server.py"
]
}
}
}
Points critiques :
- Le chemin doit être absolu (pas de
~, pas de chemin relatif). Sur macOS/Linux :pwddans le répertoire du projet. - L’ordre dans
argsest--directory <chemin> run <fichier>—--directoryvient avantrun. - Le bloc
mcpServersdoit être à la racine du fichier JSON — pas imbriqué dans une clé"preferences"ou autre. - Sur certains systèmes,
uvn’est pas dans le PATH utilisé par Claude Desktop. Si le serveur ne se connecte pas, remplacez"command": "uv"par le chemin complet :which uv(macOS/Linux) ouwhere uv(Windows).
Redémarrer et vérifier
Quittez complètement Claude Desktop et relancez-le. Si la configuration est correcte, une icône MCP apparaît dans l’interface.

Assurez-vous que le serveur MCP mcp-travel soit présent et activé.
Tester avec un prompt réaliste
Je pars à Tokyo la semaine prochaine. Quelle météo prévoir,
et combien valent 500 EUR en JPY ?
Claude devrait appeler get_weather("Tokyo") et get_currency("EUR", "JPY", 500) dans la même réponse, puis formuler une réponse naturelle à partir des deux résultats. Par défaut, l’appel aux outils déclenche une demande d’autorisation.

Exemple de réponse ci-dessous. Et notez, à droite, dans la section contexte, l’affichage du détail des appels MCP :

Utiliser l’inspecteur MCP
L’inspecteur MCP fait partie du SDK et vous permet de tester vos serveurs MCP interactivement :
cd mcp-travel
uv run mcp dev server.py
L’inspecteur ouvre une interface web (généralement http://localhost:5173) qui liste vos outils et permet de les appeler manuellement en JSON.

Ci-dessous, un exemple d’invocation de tool MCP depuis l’interface de l’inspecteur :

Problèmes courants
| Symptôme | Cause probable | Solution |
|---|---|---|
| Serveur absent dans Claude Desktop | Chemin non absolu ou uv absent du PATH | Utiliser chemin complet which uv |
| Outil visible mais erreur à l’appel | Print sur stdout dans le code | Remplacer par print(..., file=sys.stderr) |
| Serveur lent à démarrer | Premier uv run crée le venv | Normal, 5-10 sec au premier lancement |
| L’outil MCP n’est pas appelé par votre hôte | Docstring trop vague | Préciser la description et les Args, forcer l’appel dans le prompt |
mcp-travel dans d’autres hôtes MCP
Avec MCP, un même serveur fonctionne dans n’importe quel hôte compatible, sans modifier une ligne de code. Voici comment connecter mcp-travel à deux autres environnements.
Claude Code CLI
Depuis un terminal où Claude Code est installé :
claude mcp add mcp-travel -- uv --directory /chemin/absolu/vers/mcp-travel run server.py
Vérifiez la configuration :
claude mcp list
Dans une session Claude Code (claude dans le terminal), tapez /mcp pour lister les serveurs disponibles et leurs outils. Vous devez voir mcp-travel avec get_weather et get_currency.

Testez directement dans le terminal :
using mcp-travel, provide weather information for Reykjavik
Exemple de demande d’approbation des appels dans Claude Code :

Après autorisation, Claude appelle l’outil et intègre les résultats dans sa réponse — même comportement que dans Claude Desktop, même code serveur.
OpenCode
Pour OpenCode, référez-vous à la documentation de votre version pour l’emplacement du fichier de configuration. Sur macOS, sur ma machine et ma version d’OpenCode, le fichier est ~/.config/opencode/config.json. Voici le fragment correspondant à la configuration MCP :
{
"mcp": {
"mcp-travel": {
"type": "local",
"command": ["/usr/local/bin/uv", "--directory", "/chemin/absolu/vers/mcp-travel", "run", "server.py"],
"enabled": true,
"args": []
}
}
}
Après redémarrage, OpenCode affiche les serveurs MCP chargés et vous pouvez tester l’invocation des outils :
Using mcp-travel, provide weather information for Tokyo

Notre server.py n’a pas changé : pas d’adaptation de code nécessaire, le protocole MCP assure l’interopérabilité. Seul le fichier de config JSON de chaque hôte diffère.
Et ci-dessous, le même prompt en japonais, que Qwen 3.6 supporte :

FAQ
Pourquoi commencer par STDIO et pas par HTTP ?
STDIO est le transport le plus simple : le serveur tourne sur la même machine que l’hôte, pas de port réseau à exposer, pas de TLS, pas de gestion de sessions. L’hôte démarre et arrête le processus automatiquement. Pour un premier serveur, c’est le chemin le plus rapide vers un résultat fonctionnel. Le transport HTTP streamable (partie 11) permet d’exposer un serveur MCP à distance, accessible depuis plusieurs clients simultanément — et le code des outils ne change pas entre les deux transports.
Comment voir les messages JSON échangés entre Claude et mon serveur ?
Trois approches : mcp dev server.py affiche en temps réel tous les messages MCP dans une interface web ; depuis le code serveur, tout ce que vous écrivez sur stderr apparaît dans les logs de l’hôte (print("reçu city=", city, file=sys.stderr)) ; Claude Desktop logge les échanges MCP dans ses fichiers de log (macOS : ~/Library/Logs/Claude/).
Les annotations de type sont-elles vraiment obligatoires ?
Oui, pour FastMCP. Sans city: str, FastMCP génère un JSON Schema incomplet — le champ city manquera ou n’aura pas de type. Claude ne peut pas construire les arguments et l’outil sera soit ignoré, soit appelé avec des erreurs. Avec mcp dev, vous pouvez voir le schema généré : un tool sans annotations produit "properties": {}, un schema vide qui aboutit à un outil inutilisable.
Mocks validés, place aux vraies APIs
Vous avez construit un serveur MCP STDIO fonctionnel, intégré dans trois hôtes différents sans modifier une ligne de code serveur. Dans cette partie, nous avons abordé :
- La mécanique FastMCP : décorateurs, annotations de type, docstrings comme interface du LLM
- Le protocole STDIO : processus Python en arrière-plan, stdin/stdout, stderr pour les logs
- Le design par domaine : regrouper
get_weatheretget_currencydansmcp-travelparce qu’ils servent le même cas d’usage - La configuration hôte-par-hôte : même
argsJSON, même commandeuv, quelle que soit la cible
La partie 6 reprend les outils de ce premier serveur — même nom, mêmes signatures de fonctions, mêmes noms de tools. On va simplement modifier l’implémentation : get_weather appellera wttr.in, et get_currency interrogera une API de taux de change publique. On ajoutera la gestion des erreurs réseau, les timeouts et les éventuelles clés API via variables d’environnement.
La séparation interface/implémentation dans MCP n’est pas juste une bonne pratique Python. C’est ce qui permet à Claude Desktop, Claude Code CLI et OpenCode de continuer à utiliser mcp-travel sans reconfiguration quand on branchera les vraies données en partie 6.
Article suivant : MCP-101 Partie 6 — Tools MCP avec accès internet — météo et devises (à venir)
