Lesson 7 — Mémoire, état et MCP

Brancher des outils avec MCP

6 min read

This content is not translated yet. Showing the original version.

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
Next