
Vous savez ce qu'est le Model Context Protocol. Place à la pratique : ce guide construit un serveur MCP complet en Python avec FastMCP, le framework officiel. Le serveur d'exemple — blog-helper — expose un dossier de notes Markdown via trois tools et une resource. Vous le testerez avec un client MCP Python, puis verrez comment le brancher sur un hôte comme Claude Desktop. Tout le code de ce guide a été exécuté et validé en lab. Public visé : développeur Python à l'aise avec les fonctions et les décorateurs.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Créer un serveur MCP avec FastMCP en quelques dizaines de lignes.
- Exposer des tools (fonctions appelables) et une resource (donnée lisible).
- Tester votre serveur avec un client MCP Python, sans hôte.
- Brancher le serveur sur un hôte MCP.
- Sécuriser le serveur contre les accès hors de son périmètre.
Prérequis
Section intitulée « Prérequis »Ce guide suppose acquis le vocabulaire de MCP — hôte, client, serveur, tools, resources. Si ce n'est pas le cas, lisez d'abord Comprendre le Model Context Protocol. Il vous faut également :
- Python 3.10 ou plus récent.
- Des bases en Python : fonctions, décorateurs,
pathlib.
Aucune clé d'API ni service externe : tout tourne en local.
FastMCP en deux mots
Section intitulée « FastMCP en deux mots »Le SDK officiel mcp pour Python embarque FastMCP, une couche qui réduit l'écriture d'un serveur à l'essentiel. Vous ne manipulez ni JSON-RPC, ni le cycle de vie du protocole : vous écrivez des fonctions Python normales et vous les décorez. FastMCP génère le schéma, gère la négociation et le transport.
Le principe tient en une phrase : un tool, c'est une fonction décorée @mcp.tool(). Sa signature devient le schéma d'appel, sa docstring devient la description lue par le modèle. Soigner la docstring n'est donc pas cosmétique — c'est elle qui dit au LLM quand et comment utiliser l'outil.
Construire le serveur
Section intitulée « Construire le serveur »Le serveur blog-helper expose un dossier notes/ contenant des fichiers Markdown. Construisons-le par étapes.
-
Installer le SDK MCP.
Travaillez dans un environnement virtuel dédié. L'option
--only-binaryn'accepte que des paquets pré-compilés, sans exécution de script d'installation.Fenêtre de terminal mkdir blog-helper && cd blog-helperpython3 -m venv .venv./.venv/bin/pip install --only-binary=:all: mcp -
Créer le squelette du serveur.
Dans
server.py, on instancie un serveur FastMCP et on fixe le dossier de notes.mcp.run()démarre le serveur en transport stdio — le mode local par défaut.from pathlib import Pathfrom mcp.server.fastmcp import FastMCPNOTES_DIR = Path(__file__).parent / "notes"mcp = FastMCP("blog-helper")if __name__ == "__main__":mcp.run() -
Exposer un premier tool.
Un tool est une fonction décorée. Celui-ci liste les notes disponibles. Le type de retour (
list[str]) et la docstring sont repris automatiquement par FastMCP.@mcp.tool()def list_notes() -> list[str]:"""Liste les notes Markdown disponibles."""return sorted(p.name for p in NOTES_DIR.glob("*.md")) -
Ajouter la lecture et la recherche.
Deux tools de plus : lire une note par son nom, et chercher un terme dans toutes les notes. La fonction
_safe_path— détaillée dans la section Sécurité — empêche de sortir du dossiernotes/.@mcp.tool()def read_note(name: str) -> str:"""Lit le contenu d'une note Markdown par son nom de fichier."""path = _safe_path(name)if not path.is_file():raise FileNotFoundError(f"Note introuvable : {name}")return path.read_text(encoding="utf-8")@mcp.tool()def search_notes(query: str) -> list[str]:"""Renvoie les notes dont le contenu contient le terme recherché."""return sorted(p.name for p in NOTES_DIR.glob("*.md")if query.lower() in p.read_text(encoding="utf-8").lower()) -
Exposer une resource.
Une resource fournit une donnée, identifiée par une URI à motif. Ici,
note://kubernetes.mdrenverra le contenu de cette note. La resource ne fait rien — elle donne du contexte que l'hôte peut injecter.@mcp.resource("note://{name}")def note_resource(name: str) -> str:"""Expose une note comme resource lisible par l'hôte."""return read_note(name)
Tester le serveur avec un client Python
Section intitulée « Tester le serveur avec un client Python »Inutile d'attendre un hôte complet pour valider le serveur. Le SDK mcp fournit aussi un client : on lance le serveur, on ouvre une session, on liste les capacités et on appelle les tools. C'est le test de fumée idéal.
import asyncioimport sysfrom mcp import ClientSession, StdioServerParametersfrom mcp.client.stdio import stdio_client
async def main() -> None: params = StdioServerParameters(command=sys.executable, args=["server.py"]) async with stdio_client(params) as (read, write): async with ClientSession(read, write) as session: await session.initialize() tools = await session.list_tools() print("Tools :", [t.name for t in tools.tools]) res = await session.call_tool("search_notes", {"query": "primitives"}) print("search_notes ->", [c.text for c in res.content])
asyncio.run(main())L'appel à session.initialize() déclenche la négociation : le client et le serveur s'accordent sur leurs capacités. Vient ensuite la découverte (list_tools), puis l'invocation (call_tool). Sur le serveur blog-helper, la sortie attendue est :
Tools : ['list_notes', 'read_note', 'search_notes']search_notes -> ['mcp.md']Brancher le serveur sur un hôte
Section intitulée « Brancher le serveur sur un hôte »Une fois le serveur validé, on le déclare auprès d'un hôte MCP. La configuration est déclarative : on indique la commande qui lance le serveur.
Ajoutez le serveur au fichier claude_desktop_config.json (sous ~/.config/Claude/ sur Linux, ~/Library/Application Support/Claude/ sur macOS).
{ "mcpServers": { "blog-helper": { "command": "/chemin/vers/blog-helper/.venv/bin/python", "args": ["/chemin/vers/blog-helper/server.py"] } }}Redémarrez l'application : les tools du serveur apparaissent dans l'interface.
En ligne de commande, la sous-commande mcp enregistre le serveur :
claude mcp add blog-helper \ /chemin/vers/blog-helper/.venv/bin/python /chemin/vers/blog-helper/server.pyLe serveur est alors disponible dans les sessions Claude Code.
Dans les deux cas, on pointe le Python du venv — celui où le SDK mcp est installé — et le chemin absolu du server.py.
Sécurité
Section intitulée « Sécurité »Un serveur MCP exécute du code à la demande d'un modèle. La règle d'or : ne jamais faire confiance aux arguments reçus. Le tool read_note accepte un nom de fichier — sans précaution, un appel avec ../../etc/passwd lirait un fichier hors du périmètre prévu. C'est une attaque par traversée de chemin.
La parade tient en une fonction : résoudre le chemin, puis vérifier qu'il reste dans le dossier autorisé.
def _safe_path(name: str) -> Path: """Résout un nom de note en interdisant toute sortie du dossier notes/.""" path = (NOTES_DIR / name).resolve() if not path.is_relative_to(NOTES_DIR.resolve()): raise ValueError(f"Chemin hors du dossier notes/ : {name}") return pathTrois principes guident un serveur MCP sain. Restreindre le périmètre : un serveur n'expose que ce qui est strictement nécessaire. Valider chaque entrée : tout argument est potentiellement hostile. Limiter les droits : le serveur tourne avec un compte aux permissions minimales, jamais en root.
Dépannage
Section intitulée « Dépannage »| Symptôme | Cause probable | Solution |
|---|---|---|
ModuleNotFoundError: mcp | Serveur lancé avec le mauvais Python | Pointer le Python du venv où mcp est installé |
Le client se bloque à initialize() | Le serveur a planté au démarrage | Lancer python server.py seul pour voir l'erreur |
list_tools renvoie une liste vide | Décorateurs @mcp.tool() absents ou mal placés | Vérifier que chaque tool est bien décoré |
| Un tool ne renvoie qu'un résultat partiel | Lecture de content[0] au lieu de tout content | Parcourir l'ensemble des blocs de contenu |
ValueError: Chemin hors du dossier | Argument tentant une traversée de chemin | Comportement attendu : la validation a fait son travail |
À retenir
Section intitulée « À retenir »- FastMCP réduit un serveur MCP à des fonctions Python décorées — pas de JSON-RPC à écrire.
- Un tool se déclare avec
@mcp.tool(); sa docstring est le mode d'emploi lu par le modèle. - Une resource (
@mcp.resource) fournit du contexte, identifiée par une URI à motif. - Le client MCP Python teste un serveur sans hôte :
initialize,list_tools,call_tool. - Une réponse de tool est une liste de blocs — les parcourir tous.
- Sécurité non négociable : valider chaque argument, restreindre le périmètre, droits minimaux.