Lección 7 — Mémoire, état et MCP

Brancher des outils avec MCP

6 min de lectura

Este contenido aún no está traducido. Se muestra la versión original.

Brancher des outils avec MCP

Le problème que MCP résout

Dans agent.py, chaque outil existe deux fois : la fonction Python et son schéma JSON, écrits à la main, dans le même fichier que l'agent. Si une autre équipe veut réutiliser « consulter le stock » dans son propre agent (ou dans Claude Desktop, un IDE, Gemini CLI…), elle doit tout recopier.

Le Model Context Protocol (MCP) est un protocole ouvert qui standardise ce branchement : un serveur expose des outils, un client (votre agent) les découvre et les appelle. Un même serveur sert n'importe quel client compatible.

flowchart LR
  subgraph HOTE["Votre agent (client MCP)"]
    B["Boucle + call_llm"]
  end
  B <-->|"stdio ou HTTP"| S1["Serveur MCP catalogue"]
  B <-->|"stdio ou HTTP"| S2["Serveur MCP agenda"]
  S1 --> DB["Base produits"]
  S2 --> CAL["Calendrier"]
  AUTRE["Autre client : IDE, assistant de bureau..."] <--> S1
Notion MCPCe que c'estDans notre exemple
Toolune fonction appelable, avec un schémaconsulter_stock
Resourcedes données en lecture (fichier, enregistrement)la fiche d'un produit
Promptun modèle de prompt proposé par le serveur« rédiger un récapitulatif de commande »
Transport stdiole serveur est un sous-processus localdéveloppement, outils locaux
Transport HTTP (streamable)le serveur est un service réseauserveur partagé entre équipes

Le serveur : vos outils, sans les schémas

Le SDK Python officiel (pip install mcp) fournit FastMCP, qui génère le schéma à partir des annotations de type et de la docstring :

serveur_catalogue.py
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("catalogue")

CATALOGUE = {"P1": ("Café en grains 1 kg", 18.0), "P2": ("Thé vert 200 g", 9.5)}
STOCK = {"P1": 12, "P2": 0}


@mcp.tool()
def chercher_produit(texte: str) -> list[dict]:
    """Cherche des produits du catalogue par mot-clé."""
    t = texte.lower()
    return [{"ref": r, "nom": n, "prix": p}
            for r, (n, p) in CATALOGUE.items() if t in n.lower()]


@mcp.tool()
def consulter_stock(ref: str) -> dict:
    """Donne la quantité en stock d'une référence (ex. P1)."""
    if ref not in STOCK:
        raise ValueError(f"Référence inconnue : {ref}")
    return {"ref": ref, "quantite": STOCK[ref]}


if __name__ == "__main__":
    mcp.run()  # transport stdio par défaut

Avec FastMCP, d'où vient le schéma JSON des paramètres de consulter_stock ?

Le client : la même boucle, d'autres outils

Côté agent, on remplace TOOLS et TOOLS_PY par ce que le serveur annonce : initialize, puis tools/list (noms, descriptions, schémas), puis un tools/call par appel demandé par le LLM. Le SDK client est asynchrone, d'où async / await.

agent_mcp.py
import asyncio
import json
import sys

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

from agent import SYSTEM, call_llm


def vers_schema(t):
    """Outil MCP -> format chat completions."""
    return {"type": "function", "function": {
        "name": t.name, "description": t.description or "",
        "parameters": t.inputSchema}}


async def agent_mcp(question, max_steps=6, llm=call_llm):
    params = StdioServerParameters(command=sys.executable,
                                   args=["serveur_catalogue.py"])
    async with stdio_client(params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()
            tools = [vers_schema(t) for t in (await session.list_tools()).tools]
            messages = [{"role": "system", "content": SYSTEM},
                        {"role": "user", "content": question}]
            for _ in range(max_steps):
                msg = llm(messages, tools)
                messages.append(msg)
                calls = msg.get("tool_calls") or []
                if not calls:
                    return msg.get("content") or ""
                for call in calls:
                    args = json.loads(call["function"]["arguments"] or "{}")
                    res = await session.call_tool(call["function"]["name"], args)
                    texte = "\n".join(c.text for c in res.content if c.type == "text")
                    messages.append({"role": "tool", "tool_call_id": call["id"],
                                     "content": texte})
            return "Désolé, je n'ai pas abouti. Je transmets à un conseiller."


if __name__ == "__main__":
    print(asyncio.run(agent_mcp(" ".join(sys.argv[1:]) or "Du café en stock ?")))

La boucle est identique à celle du module 2. Seules changent la découverte des outils (list_tools) et leur exécution (call_tool).

Quels avantages apporte MCP par rapport aux outils codés dans agent.py ? (plusieurs réponses)

Exercice : ajouter un outil au serveur, sans toucher à l'agent

Ajoutez au serveur un outil prix_total(ref: str, quantite: int). Relancez agent_mcp.py "Combien pour 3 kg de café ?". Qu'avez-vous dû modifier dans agent_mcp.py ?

Voir la solution
serveur_catalogue.py (ajout)
@mcp.tool()
def prix_total(ref: str, quantite: int) -> dict:
    """Calcule le prix total en euros pour une référence et une quantité.
    Toujours l'utiliser plutôt que de calculer soi-même."""
    if ref not in CATALOGUE:
        raise ValueError(f"Référence inconnue : {ref}")
    if quantite < 1:
        raise ValueError("La quantité doit être au moins 1")
    return {"ref": ref, "quantite": quantite,
            "total": round(CATALOGUE[ref][1] * quantite, 2)}

Dans agent_mcp.py : rien. L'agent découvre le nouvel outil au prochain list_tools(). C'est tout l'intérêt du protocole : l'ajout d'une capacité ne demande aucun redéploiement de l'agent.

Je sais faire

  • Expliquer ce qu'apportent un serveur et un client MCP
  • Écrire un serveur MCP avec FastMCP et des docstrings utiles
  • Convertir les outils MCP au format attendu par call_llm
  • Citer un risque de sécurité propre aux serveurs MCP tiers
Siguiente