Implémenter un client MCP
Sur cette page
Notre serveur MCP fonctionne et l’inspecteur nous l’a confirmé. Il est maintenant temps de construire l’autre moitié du dispositif : le côté client, qui permet à notre code applicatif de communiquer avec le serveur MCP et d’accéder à ses fonctionnalités. C’est la pièce qui manquait pour que notre CLI puisse enfin parler à Claude en connaissant les outils disponibles. À la fin de cette leçon, vous saurez ouvrir et refermer proprement une session MCP, lister les outils et en exécuter un, puis faire tourner la chaîne complète avec Claude.
Deux objets, deux rôles
Un mot d’abord sur ce que vous êtes en train de faire. Dans la plupart des projets réels, vous implémenterez soit un client MCP, soit un serveur MCP — rarement les deux. Nous construisons ici les deux côtés parce que c’est le meilleur moyen de voir ce qui circule entre eux, mais ne prenez pas cette symétrie pour une obligation.
Le client MCP se compose de deux éléments. Le premier, que nous appellerons MCP Client, est une classe personnalisée que nous créons nous-mêmes pour faciliter l’utilisation de la session. Le second, la Client Session, est la connexion réelle au serveur, fournie par le SDK MCP Python. Pourquoi ne pas utiliser directement la session du SDK ? Parce qu’elle nécessite une gestion soigneuse des ressources : une connexion ouverte doit être refermée proprement, y compris quand une exception survient au milieu d’un appel. En l’encapsulant dans notre propre classe, nous obtenons un objet qui prend ce nettoyage en charge automatiquement, et le reste du code applicatif n’a plus à y penser.
Le squelette du client
Voici le fichier mcp_client.py complet, avant d’y ajouter les méthodes de cette leçon et des suivantes. Il lance le serveur comme un sous-processus (transport stdio), ouvre la session du SDK et la referme proprement à la fin :
import asyncio
from contextlib import AsyncExitStack
from typing import Any
from mcp import ClientSession, StdioServerParameters, stdio_client, types
class MCPClient:
def __init__(self, command: str, args: list[str]):
self._params = StdioServerParameters(command=command, args=args)
self._session: ClientSession | None = None
self._stack = AsyncExitStack()
async def connect(self) -> None:
read, write = await self._stack.enter_async_context(stdio_client(self._params))
self._session = await self._stack.enter_async_context(ClientSession(read, write))
await self._session.initialize()
def session(self) -> ClientSession:
if self._session is None:
raise ConnectionError("Client non connecté : appelez connect() d'abord.")
return self._session
async def cleanup(self) -> None:
await self._stack.aclose()
self._session = None
async def __aenter__(self) -> "MCPClient":
await self.connect()
return self
async def __aexit__(self, *exc) -> None:
await self.cleanup()
# Les méthodes list_tools(), call_tool(), read_resource()... viennent ici.
async def main() -> None:
async with MCPClient(command="uv", args=["run", "mcp_server.py"]) as client:
tools = await client.list_tools()
print([tool.name for tool in tools])
if __name__ == "__main__":
asyncio.run(main())
AsyncExitStack garantit que le sous-processus et la session sont fermés dans le bon ordre, même si une erreur survient. Le SDK v2 propose aussi un client de plus haut niveau, Client, qui fait tout cela en une ligne : async with Client(StdioServerParameters(command="uv", args=["run", "mcp_server.py"])) as client:. Nous gardons ici la session explicite, plus proche de ce qui circule réellement entre les deux programmes.
Lister les outils disponibles
Deux fonctions suffisent pour couvrir le cycle de vie d’un outil, et elles reprennent exactement les deux paires de messages vues aux leçons 65 et 66. La première, list_tools(), récupère tous les outils que le serveur MCP expose :
async def list_tools(self) -> list[types.Tool]:
result = await self.session().list_tools()
return result.tools
Le corps est volontairement minimal : on accède à la session, c’est-à-dire à la connexion au serveur, on appelle la méthode list_tools() intégrée au SDK, et on retourne le champ tools du résultat. Toute la mécanique du protocole — sérialisation du message, envoi sur stdio, attente de la réponse — se déroule à l’intérieur de cet unique await.
Exécuter un outil
La seconde fonction déclenche l’exécution d’un outil précis sur le serveur :
async def call_tool(
self, tool_name: str, tool_input: dict
) -> types.CallToolResult | None:
return await self.session().call_tool(tool_name, tool_input)
On transmet le nom de l’outil et ses paramètres d’entrée, puis on retourne le résultat. Le point important est l’origine de ces deux arguments : ils ne viennent pas de vous, ils viennent de Claude. Le modèle a choisi l’outil et rempli ses paramètres, votre code se contente de transporter cette décision jusqu’au serveur.
Vérifier que la connexion tient
Le fichier client inclut un harnais de test simple, que vous pouvez lancer directement sans passer par l’application complète :
uv run mcp_client.py
Cette commande connecte votre serveur MCP et affiche les outils disponibles. Vous devriez y retrouver les définitions écrites à la leçon précédente, avec leurs descriptions et leurs schémas d’entrée (tool.input_schema en v2), la preuve que les annotations Pydantic ont bien été traduites en schéma exploitable par le modèle. Sur notre poste, avec le SDK 2.3.0, [tool.name for tool in tools] affiche ['read_doc_contents', 'edit_document'], et un appel call_tool("read_doc_contents", {"doc_id": "plan.md"}) renvoie un CallToolResult dont is_error vaut False et dont content[0].text contient le texte du document.
Faire tourner la chaîne complète
Les deux fonctions étant en place, testez le flux de bout en bout :
uv run main.py
Posez une question comme « Quel est le contenu du document rapport.pdf ? » et observez la réponse. En coulisses, la séquence se déroule ainsi :
1. Votre application utilise le client pour obtenir les outils disponibles
2. Ces outils sont envoyés à Claude avec votre question
3. Claude décide d'utiliser l'outil read_doc_contents
4. Votre application utilise le client pour exécuter cet outil
5. Le résultat est retourné à Claude, qui vous répond
Retenez la répartition des rôles que cette séquence met en évidence : le client agit comme pont entre votre logique applicative et les fonctionnalités du serveur MCP. Il rend facile l’intégration d’outils puissants dans vos workflows IA, sans que vous ayez à gérer les détails du protocole de communication. Dans les prochaines leçons, nous enrichirons cette architecture avec les deux primitives restantes : les ressources, puis les prompts.
À vous de jouer
- Écrivez
mcp_client.pyavec le squelette ci-dessus et les deux méthodes, puis lancezuv run mcp_client.py: la liste des deux outils doit s’afficher. - Dans le harnais de test, ajoutez un appel
call_tool("read_doc_contents", {"doc_id": "plan.md"})et affichezresult.is_erroretresult.content[0].text; puis le même appel avec un identifiant inconnu. - Remplacez temporairement la session explicite par le client de haut niveau
Clientdu SDK v2 et comparez la longueur du code. - Lancez
uv run main.pyet posez une question qui demande un document ; dans la réponse, repérez le moment où Claude a décidé d’appeler l’outil.
Sources officielles : Implementing a client (Claude Academy) · What’s new in v2 : le client · Guide de migration
Points clés à retenir
- Dans un projet réel, on écrit un client ou un serveur, rarement les deux ; ici les deux, pour voir ce qui circule.
- Le client encapsule la
ClientSessiondu SDK dans une classe qui ouvre et referme proprement le sous-processus stdio (AsyncExitStack). list_tools()renvoie les outils avec leurinput_schema;call_tool(name, args)transporte la décision de Claude jusqu’au serveur et rend unCallToolResult(is_error,content).- Le SDK v2 propose aussi un client de haut niveau,
Client, qui fait tout cela en une ligne.