Aller au contenu
Développement medium

Créer son premier serveur MCP avec FastMCP

11 min de lecture

logo python

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.

  • 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.

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.

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.

Le serveur blog-helper expose un dossier notes/ contenant des fichiers Markdown. Construisons-le par étapes.

  1. Installer le SDK MCP.

    Travaillez dans un environnement virtuel dédié. L'option --only-binary n'accepte que des paquets pré-compilés, sans exécution de script d'installation.

    Fenêtre de terminal
    mkdir blog-helper && cd blog-helper
    python3 -m venv .venv
    ./.venv/bin/pip install --only-binary=:all: mcp
  2. 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 Path
    from mcp.server.fastmcp import FastMCP
    NOTES_DIR = Path(__file__).parent / "notes"
    mcp = FastMCP("blog-helper")
    if __name__ == "__main__":
    mcp.run()
  3. 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"))
  4. 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 dossier notes/.

    @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()
    )
  5. Exposer une resource.

    Une resource fournit une donnée, identifiée par une URI à motif. Ici, note://kubernetes.md renverra 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)

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 asyncio
import sys
from mcp import ClientSession, StdioServerParameters
from 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']

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.

Dans les deux cas, on pointe le Python du venv — celui où le SDK mcp est installé — et le chemin absolu du server.py.

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 path

Trois 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.

SymptômeCause probableSolution
ModuleNotFoundError: mcpServeur lancé avec le mauvais PythonPointer le Python du venv où mcp est installé
Le client se bloque à initialize()Le serveur a planté au démarrageLancer python server.py seul pour voir l'erreur
list_tools renvoie une liste videDécorateurs @mcp.tool() absents ou mal placésVérifier que chaque tool est bien décoré
Un tool ne renvoie qu'un résultat partielLecture de content[0] au lieu de tout contentParcourir l'ensemble des blocs de contenu
ValueError: Chemin hors du dossierArgument tentant une traversée de cheminComportement attendu : la validation a fait son travail
  • 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.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn