Ardoise MCP-101 écrite à la craie sur fond de bois, lumière naturelle

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 ?

  • json sert à sérialiser les réponses des outils en chaînes JSON — convention MCP pour les retours structurés.
  • sys est nécessaire pour écrire sur stderr si vous avez besoin de logger quoi que ce soit : print("debug", file=sys.stderr). Toute écriture sur stdout corrompt 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 :

OSEmplacement
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 : pwd dans le répertoire du projet.
  • L’ordre dans args est --directory <chemin> run <fichier>--directory vient avant run.
  • Le bloc mcpServers doit être à la racine du fichier JSON — pas imbriqué dans une clé "preferences" ou autre.
  • Sur certains systèmes, uv n’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) ou where 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.

Icône MCP visible dans la barre d'outils Claude Desktop après redémarrage avec mcp-travel connecté

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.

Prompt d'autorisation d'appels d'outils MCP dans Claude Desktop

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

Claude Desktop : réponse avec appels MCP get_weather et get_currency visibles dans le panneau contexte

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.

MCP Inspector interface web affichant les outils get_weather et get_currency du serveur mcp-travel

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

MCP Inspector : exemple d'invocation manuelle d'un outil MCP avec résultat JSON

Problèmes courants

SymptômeCause probableSolution
Serveur absent dans Claude DesktopChemin non absolu ou uv absent du PATHUtiliser chemin complet which uv
Outil visible mais erreur à l’appelPrint sur stdout dans le codeRemplacer par print(..., file=sys.stderr)
Serveur lent à démarrerPremier uv run crée le venvNormal, 5-10 sec au premier lancement
L’outil MCP n’est pas appelé par votre hôteDocstring trop vaguePré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.

Claude Code CLI listant les serveurs MCP disponibles avec mcp-travel et ses deux outils

Testez directement dans le terminal :

using mcp-travel, provide weather information for Reykjavik

Exemple de demande d’approbation des appels dans Claude Code :

Claude Code CLI : demande d'autorisation avant d'invoquer un outil MCP

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
OpenCode avec mcp-travel configuré : appel de get_weather pour Tokyo et réponse de l'assistant

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 :

OpenCode : même prompt en japonais démontrant l'interopérabilité de mcp-travel avec différentes langues

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_weather et get_currency dans mcp-travel parce qu’ils servent le même cas d’usage
  • La configuration hôte-par-hôte : même args JSON, même commande uv, 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)

Publications similaires

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *