Sur cette page
- Un serveur MCP expose des outils ; l'assistant, client MCP, les découvre et les appelle. La révision actuelle du protocole date du 28 juillet 2026.
- Le SDK Python officiel, version 2, appelle MCPServer ce qui s'appelait FastMCP ; l'ancien chemin d'import n'existe plus. Un outil, c'est une fonction décorée, décrite par sa docstring. mcp 2.3.0 au 2 octobre 2026.
- En local, l'assistant lance le serveur et lui parle par stdio : rien ne doit sortir sur la sortie standard hors du protocole ; un print suffit à tout casser.
- La sécurité est dans le serveur : la spécification demande de valider les entrées, contrôler l'accès, limiter les appels et nettoyer les sorties. Ici, lecture seule.
- Tout se teste sans modèle : un client du SDK lance le serveur et appelle ses outils, comme le ferait l'assistant, sans rien payer.
Objectif
Écrire un serveur MCP en Python, avec deux outils en lecture seule : chercher_client, qui donne la fiche d'un client, et factures_en_retard, qui liste les factures non payées dont l'échéance est passée. Le tester avec un client qui le lance comme le ferait un assistant, puis le brancher sur Claude Code ou Claude Desktop. Compter quarante-cinq minutes, après l'agent à deux outils.
Données fournies
Deux fichiers fabriqués, séparés par des points-virgules ; les adresses en « .example » ne reçoivent aucun courriel.
clients.csvclient;ville;contact;encours_eur Société A;Lyon;compta@societe-a.example;1250.00 Société B;Nantes;factures@societe-b.example;6300.00 Société C;Lille;admin@societe-c.example;0.00
factures.csvnumero;client;montant_eur;echeance;payee F-2026-097;Société A;1250.00;2026-09-15;non F-2026-101;Société B;4320.00;2026-08-30;non F-2026-104;Société C;860.40;2026-09-30;oui F-2026-115;Société B;1980.00;2026-11-20;non
Qui parle à qui
- L'assistantClaude Desktop, Claude Code, un éditeur : le client MCP, qui lance le serveur
- stdiodes messages JSON-RPC par l'entrée et la sortie standard, sur le même poste
- serveur.pyvos deux outils, en lecture seule ; les journaux sur la sortie d'erreur
- Les fichiersclients.csv et factures.csv ; demain, votre logiciel
Consignes
Préparer le projet
uv init suivi-clients
uv add "mcp[cli]"
Ce qu'on fait : Python 3.10 ou plus récent ; le SDK officiel, sous licence MIT.
Écrire serveur.py
Ce qu'on fait : un MCPServer, deux fonctions décorées par @mcp.tool(), une docstring qui dit à quoi sert chaque outil et ce qu'il attend ; des types, pour que le résultat soit structuré.
Ne rien écrire sur la sortie standard
Ce qu'on fait : pas de print : les journaux passent par logging, sur la sortie d'erreur.
Vérifier les entrées
Ce qu'on fait : un client inconnu ou une date mal écrite renvoient un message clair, avec ToolError, au lieu d'une erreur générique.
Tester sans modèle
uv run essai_client.py
Ce qu'on fait : le client lance le serveur sur stdio, liste ses outils et les appelle.
Le brancher sur un assistant
Claude Code : claude mcp add suivi-clients -- uv --directory /chemin/absolu/suivi-clients run serveur.py
Claude Desktop : la déclaration plus bas, dans sa configuration
Ce qu'on fait : le serveur lancé par l'assistant à la demande.
Poser la question
Ce qu'on fait : « Quelles factures sont en retard, et qui faut-il relancer ? » : l'assistant appelle factures_en_retard, puis chercher_client.
Critères de réussite
Indications
Le client se bloque ou reçoit des messages illisibles
CauseUn print écrit sur la sortie standard, qui est réservée au protocole.
À faireRemplacer chaque print par logging ; vérifier les bibliothèques qui écrivent sur la sortie standard.
ImportError sur mcp.server.fastmcp
CauseUn exemple écrit pour la version 1 : en version 2, FastMCP s'appelle MCPServer et l'ancien chemin a disparu.
À fairefrom mcp.server import MCPServer ; puis MCPServer("Suivi clients").
Une erreur d'outil n'arrive que sous la forme « Error executing tool »
CauseUne exception ordinaire est traitée comme une panne : son texte reste sur le serveur.
À faireLever ToolError pour une erreur prévue : son message part au client, sans trace.
L'assistant ne voit pas le serveur
CauseLe chemin n'est pas absolu, ou l'assistant n'a pas été relancé.
À faireUn chemin absolu dans la commande ; relancer l'assistant ; dans Claude Code, un serveur déclaré dans .mcp.json attend d'être approuvé.
Le serveur doit servir toute l'équipe, à distance
CauseEn Streamable HTTP, le serveur doit vérifier l'en-tête Origin, et l'autorisation suit OAuth 2.1 ; un token reçu ne se transmet jamais à une autre API.
À faireCommencer en local ; pour le reste, la sécurité d'une application d'IA.
Correction commentée
serveur.py"""Un serveur MCP en lecture seule sur deux fichiers de l'entreprise : les clients et les factures.
Données fabriquées. Rien ne s'écrit ; rien ne sort sur stdout, réservé au protocole."""
import csv
import logging
from datetime import date
from pathlib import Path
from typing import TypedDict
from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
ICI = Path(__file__).parent
logging.basicConfig(level=logging.INFO) # les journaux vont sur stderr
mcp = MCPServer("Suivi clients")
class Fiche(TypedDict):
client: str
ville: str
contact: str
encours_eur: str
def _lire(nom: str) -> list[dict]:
with open(ICI / nom, encoding="utf-8", newline="") as f:
return list(csv.DictReader(f, delimiter=";"))
@mcp.tool()
def chercher_client(nom: str) -> Fiche:
"""Donne la fiche d'un client : ville, contact, encours.
Args:
nom: Le nom exact du client, par exemple Société A.
"""
for ligne in _lire("clients.csv"):
if ligne["client"] == nom:
return ligne
raise ToolError(f"Aucun client nommé {nom}.") # le message part au modèle, sans trace
@mcp.tool()
def factures_en_retard(au: str = "") -> list[dict]:
"""Liste les factures non payées dont l'échéance est passée.
Args:
au: La date de référence, au format AAAA-MM-JJ ; par défaut, aujourd'hui.
"""
try:
jour = date.fromisoformat(au) if au else date.today()
except ValueError:
raise ToolError("Date attendue au format AAAA-MM-JJ.")
return [f for f in _lire("factures.csv")
if f["payee"] == "non" and date.fromisoformat(f["echeance"]) < jour]
if __name__ == "__main__":
mcp.run(transport="stdio")essai_client.py"""Lance le serveur comme le ferait un assistant, sur stdio, et appelle ses deux outils. Aucun modèle."""
import asyncio
import sys
from mcp import Client, StdioServerParameters
async def main() -> None:
serveur = StdioServerParameters(command=sys.executable, args=["serveur.py"])
async with Client(serveur) as client:
outils = await client.list_tools()
print("Outils :", ", ".join(o.name for o in outils.tools))
fiche = await client.call_tool("chercher_client", {"nom": "Société B"})
f = fiche.structured_content
print(f"Société B : {f['ville']}, {f['contact']}, encours {f['encours_eur']} €")
inconnu = await client.call_tool("chercher_client", {"nom": "Société Z"})
print("Société Z :", inconnu.content[0].text if inconnu.is_error else "trouvée")
retards = await client.call_tool("factures_en_retard", {"au": "2026-10-09"})
print("En retard au 9 octobre 2026 :")
for r in retards.structured_content["result"]:
print(f" {r['numero']}, {r['client']}, {r['montant_eur']} €, échue le {r['echeance']}")
asyncio.run(main())uv run essai_client.pyOutils : chercher_client, factures_en_retardSociété B : Nantes, factures@societe-b.example, encours 6300.00 €Société Z : Error executing tool chercher_client: Aucun client nommé Société Z.En retard au 9 octobre 2026 : F-2026-097, Société A, 1250.00 €, échue le 2026-09-15 F-2026-101, Société B, 4320.00 €, échue le 2026-08-30
Essayé le 9 octobre 2026 avec Python 3.12.7 et mcp 2.3.0, sans modèle.
Le brancher sur Claude Desktop : la déclaration, sur le modèle du tutoriel officiel, avec un chemin absolu.
claude_desktop_config.json{
"mcpServers": {
"suivi-clients": {
"command": "uv",
"args": [
"--directory",
"/CHEMIN/ABSOLU/suivi-clients",
"run",
"serveur.py"
]
}
}
}Claude Desktop : « Quelles factures sont en retard ? »Pas encore exécuté par Meridal. Ce qu'on attend : l'assistant appelle factures_en_retard, puis cite F-2026-097 et F-2026-101.
Pourquoi en lecture seule. Un assistant qui lit des contenus venus de l'extérieur ne doit pas pouvoir écrire ou envoyer sans une personne ; un outil d'écriture viendra plus tard, avec une validation.
Extensions
- Le servir en Streamable HTTP :
uv run mcp run serveur.py --transport streamable-http; puis l'en-tête Origin, l'écoute sur localhost en local, et l'autorisation OAuth 2.1 pour un accès à distance. - L'inspecteur MCP :
uv run mcp dev serveur.pyouvre une interface pour essayer chaque outil à la main. - Votre vrai logiciel à la place des fichiers : un outil de lecture par question utile, jamais un accès général à la base.
- Les serveurs déjà publiés par les éditeurs : les serveurs MCP existants ; le projet complet : un serveur MCP sur un logiciel de suivi des clients.
Lectures suivantes
Un serveur MCP
Rien avant : c'est un point de départ.
Les pages qui renvoient ici
Limites et sources
Les fichiers sont fabriqués ; le serveur et son client ont tourné sur un poste de Meridal, sans modèle.
La question posée à l'assistant n'a pas été exécutée : elle appelle un modèle payant.
Le SDK et le protocole changent vite ; versions au 2 octobre et au 28 juillet 2026.
- SDK Python officiel ; nouveautés de la version 2 ; tutoriel officiel ; les outils dans la spécification ; bonnes pratiques de sécurité ; Claude Code et MCP.
- Le relevé :
apprendre/recherches/2026-10-08-dev-avance.json; le code :apprendre/pages/agents/serveur-mcp/code/.
Écrit par Meridal, avec l'aide de l'IA ; chaque fait vérifié à sa source et relu avant publication. Texte et schémas sous licence CC BY 4.0 : reprenez-les en citant Meridal. Le code sous licence MIT.
Ne plus recopier d'un logiciel à l'autre
1 100 € à 3 400 €, puis sur devis