Aller au contenu
Développement medium

Construire un client MCP en Python

8 min de lecture

logo python

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.

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

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.

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.

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 sys
from mcp import ClientSession, StdioServerParameters
from 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.

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.

  1. 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"]),
    }
  2. Ouvrir toutes les sessions dans une pile.

    AsyncExitStack enregistre 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
  3. 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 serveur
    for name, session in sessions.items():
    tools = await session.list_tools()
    for tool in tools.tools:
    registry[tool.name] = name
  4. Router les appels.

    Une fonction call consulte 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.

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.

  • Un client MCP consomme les serveurs ; vous l'écrivez vous-même dès que vous construisez un agent.
  • La séquence est fixe : transportClientSessioninitializelist_toolscall_tool.
  • AsyncExitStack gère proprement plusieurs sessions ouvertes en parallèle.
  • Un registre tool → serveur agrè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.

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