
Jusqu'ici, vous avez construit des serveurs MCP. Ce guide passe de l'autre côté : écrire le client. Dans un hôte comme Claude Desktop, le client est intégré — mais dès que vous construisez votre propre agent ou un script d'orchestration, c'est à vous de l'écrire. Et un vrai client ne parle pas à un seul serveur : il en orchestre plusieurs, agrège leurs outils et route chaque appel vers le bon. Ce guide montre comment, avec un client testé qui pilote deux serveurs à la fois. Public visé : développeur Python à l'aise avec async/await.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Comprendre le rôle d'un client MCP et son anatomie.
- Ouvrir une session avec un serveur et appeler ses tools.
- Orchestrer plusieurs serveurs simultanément avec
AsyncExitStack. - Construire un registre de tools pour router les appels.
- Gérer les erreurs : tool absent, échec côté serveur.
Prérequis
Section intitulée « Prérequis »Ce guide réutilise les serveurs des guides précédents : blog-helper et le serveur hello du guide transports. Il faut Python 3.10+, le SDK mcp, et une aise minimale avec la programmation asynchrone Python.
L'anatomie d'un client MCP
Section intitulée « L'anatomie d'un client MCP »Un client MCP est le composant qui consomme un serveur. Son travail suit toujours la même séquence, héritée du cycle de vie du protocole : ouvrir un transport, créer une session, l'initialiser, découvrir les capacités, puis invoquer.
Le SDK mcp fournit deux briques. La fonction de transport (stdio_client, streamablehttp_client) ouvre le canal. La classe ClientSession porte la logique du protocole : initialize, list_tools, call_tool. Vous orchestrez ces deux briques ; le SDK gère le JSON-RPC.
Se connecter à un serveur
Section intitulée « Se connecter à un serveur »La connexion à un serveur unique tient en quelques lignes. Les deux async with garantissent que le transport et la session sont refermés proprement, même en cas d'erreur.
import sysfrom mcp import ClientSession, StdioServerParametersfrom mcp.client.stdio import stdio_client
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() result = await session.call_tool("hello", {"name": "client"})C'est suffisant pour un test. Mais un client réel a un besoin que ce schéma ne couvre pas : parler à plusieurs serveurs.
Orchestrer plusieurs serveurs
Section intitulée « Orchestrer plusieurs serveurs »Un agent utile combine des outils de sources différentes : un serveur pour les fichiers, un pour Git, un pour une API métier. Le client doit donc tenir plusieurs sessions ouvertes en même temps. Empiler les async with devient vite illisible — la bonne réponse est AsyncExitStack.
-
Déclarer les serveurs à joindre.
Un dictionnaire associe un nom à des paramètres de connexion.
SERVERS = {"blog-helper": StdioServerParameters(command=sys.executable, args=["../blog-helper/server.py"]),"hello": StdioServerParameters(command=sys.executable, args=["../transports/server.py", "stdio"]),} -
Ouvrir toutes les sessions dans une pile.
AsyncExitStackenregistre chaque contexte ouvert et les refermera tous, dans l'ordre inverse, à la sortie du bloc — quoi qu'il arrive.async with AsyncExitStack() as stack:sessions = {}for name, params in SERVERS.items():read, write = await stack.enter_async_context(stdio_client(params))session = await stack.enter_async_context(ClientSession(read, write))await session.initialize()sessions[name] = session -
Agréger les tools dans un registre.
Chaque serveur expose ses tools. Le client les rassemble dans un registre unique qui mémorise, pour chaque tool, quel serveur le fournit.
registry = {} # nom du tool -> nom du serveurfor name, session in sessions.items():tools = await session.list_tools()for tool in tools.tools:registry[tool.name] = name -
Router les appels.
Une fonction
callconsulte le registre et envoie l'appel à la bonne session — l'appelant n'a pas à savoir quel serveur répond.async def call(tool, args):server = registry.get(tool)if server is None:return f"tool inconnu : {tool}"res = await sessions[server].call_tool(tool, args)return [c.text for c in res.content if hasattr(c, "text")]
Exécuté sur les deux serveurs, le client produit un registre unifié et route chaque appel :
Registre des tools : {'list_notes': 'blog-helper', 'read_note': 'blog-helper', 'search_notes': 'blog-helper', 'hello': 'hello'}hello -> ['Bonjour client, ici un serveur MCP.']search_notes -> ['mcp.md']L'appelant demande hello ou search_notes sans se soucier de leur origine : le registre fait l'aiguillage. C'est exactement ce qu'un hôte fait en interne pour présenter au modèle un catalogue unique de tools.
Gérer les erreurs
Section intitulée « Gérer les erreurs »Un client robuste anticipe trois familles d'échecs. Les ignorer, c'est un agent qui se fige ou plante en production.
Le tool inconnu se traite en amont : avant tout appel, vérifier que le tool est dans le registre. C'est une simple consultation de dictionnaire, et elle évite une requête vouée à l'échec.
L'échec côté serveur se lit dans la réponse. Un call_tool qui aboutit peut quand même signaler une erreur métier — fichier introuvable, argument invalide. Le champ res.isError le dit ; il faut le tester.
res = await sessions[server].call_tool(tool, args)if res.isError: return f"erreur sur {tool}"Le serveur injoignable, enfin : un serveur qui ne démarre pas, ou une URL HTTP morte, lève une exception dès la connexion. Entourez l'ouverture de session d'un try/except pour qu'un serveur défaillant ne fasse pas tomber tout le client — les autres serveurs doivent rester utilisables.
À retenir
Section intitulée « À retenir »- Un client MCP consomme les serveurs ; vous l'écrivez vous-même dès que vous construisez un agent.
- La séquence est fixe : transport →
ClientSession→initialize→list_tools→call_tool. AsyncExitStackgère proprement plusieurs sessions ouvertes en parallèle.- Un registre
tool → serveuragrège les capacités et route les appels. - Trois erreurs à gérer : tool inconnu (registre), échec serveur (
isError), serveur injoignable (try/except). - Un serveur défaillant ne doit jamais faire tomber tout le client.