Lesson 3 — Construire pas à pas

Brancher les serveurs avec McpToolset et filtrer les outils

5 min read

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

Brancher les serveurs avec McpToolset et filtrer les outils

McpToolset : un ensemble d'outils venu d'un serveur

Un toolset ADK est un objet qui fournit une liste d'outils au lieu d'un seul. McpToolset se connecte à un serveur MCP, appelle tools/list et transforme chaque outil MCP en outil ADK. Dans ADK 2.10, son constructeur accepte notamment :

ParamètreRôle
connection_paramscomment joindre le serveur : StdioConnectionParams, SseConnectionParams ou StreamableHTTPConnectionParams
tool_filterliste de noms à garder, ou fonction (outil, contexte) -> bool
tool_name_prefixpréfixe ajouté aux noms vus par le modèle (docs donne docs_lire_document)
require_confirmationdemande une confirmation avant chaque appel (booléen ou fonction)
files/agent.py
def serveur(script: str, **env: str) -> StdioConnectionParams:
    """Paramètres pour lancer un serveur MCP Python comme sous-processus (même interpréteur)."""
    return StdioConnectionParams(
        # env : ajouté aux seules variables « sûres » (PATH, HOME...) ; la clé d'API n'est PAS transmise.
        server_params=StdioServerParameters(command=sys.executable, args=[str(ICI / script)], env=env),
        timeout=20,  # secondes pour démarrer le serveur et répondre
    )


commandes = McpToolset(
    connection_params=serveur("commandes_server.py",
                              COMMANDES_DB=os.environ.get("COMMANDES_DB", str(ICI / "commandes.db"))),
    tool_filter=["lister_tables", "executer_select"],  # annuler_commande n'est PAS exposé au modèle
)

fichiers = McpToolset(
    connection_params=serveur("fichiers_server.py", DOCS_ROOT=os.environ.get("DOCS_ROOT", str(ICI / "docs"))),
    tool_name_prefix="docs",  # les outils deviennent docs_lister_documents, docs_lire_document
)

sys.executable lance le serveur avec le même Python que l'agent, donc avec le même environnement virtuel : pas de surprise du type « module mcp introuvable ».

flowchart LR
  S1["commandes_server : 3 outils"] --> F1{"tool_filter"}
  F1 -->|"gardés"| O1["lister_tables, executer_select"]
  F1 -->|"écarté"| X["annuler_commande"]
  S2["fichiers_server : 2 outils"] --> P["tool_name_prefix = docs"]
  P --> O2["docs_lister_documents, docs_lire_document"]
  O1 --> M["Déclarations envoyées à Gemini"]
  O2 --> M

Liste de noms ou prédicat ?

Une liste est explicite et lisible en revue de code : un outil ajouté plus tard au serveur n'apparaîtra pas sans modification de l'agent. Un prédicat (une fonction qui renvoie vrai ou faux) s'adapte automatiquement, par exemple « tous les outils annotés en lecture seule » :

variante (non utilisée dans files/)
def lecture_seule(outil, contexte=None) -> bool:
    annotations = outil.raw_mcp_tool.annotations
    return bool(annotations and annotations.read_only_hint)

commandes = McpToolset(connection_params=..., tool_filter=lecture_seule)

Le serveur de commandes gagne demain un outil exporter_clients (lecture seule, annoté readOnlyHint). Que voit l'agent sans modification de agent.py ?

L'agent et son instruction

files/agent.py
root_agent = Agent(
    name="analyste_commandes",
    model=os.environ.get("MODEL", "gemini-3.5-flash"),
    instruction="""Tu es l'analyste des commandes d'une boutique de vélos. Réponds en français.
- Pour les chiffres : appelle d'abord lister_tables pour connaître le schéma, puis executer_select.
  N'invente jamais un chiffre : chaque nombre cité vient d'une requête.
- Pour les règles (retours, livraison, promotions) : docs_lister_documents puis docs_lire_document.
- Tu ne peux rien modifier. Si on te demande une action (annuler, rembourser), explique ce que
  disent les règles et indique qu'un humain doit la faire.
- Termine par une ligne « Sources : » listant les tables et documents utilisés.""",
    tools=[commandes, fichiers],
)

Fermer proprement

Les serveurs stdio sont des sous-processus : il faut les arrêter. runner.close() ferme les toolsets de l'agent, donc les sessions MCP :

files/main.py
runner = InMemoryRunner(agent=root_agent, app_name="mcp_lab")
try:
    for q in sys.argv[1:] or DEMO:
        ...
finally:
    await runner.close()  # ferme proprement les sous-processus MCP

Pourquoi le préfixe docs est-il utile, même si les noms ne se chevauchent pas aujourd'hui ?

Exercice : un troisième serveur

Vous voulez brancher le serveur de fichiers une seconde fois, sur un dossier archives/, avec uniquement l'outil de lecture et le préfixe archives. Écrivez le McpToolset correspondant. Quel nom d'outil faut-il mettre dans tool_filter : avec ou sans préfixe ?

Voir la solution
archives = McpToolset(
    connection_params=serveur("fichiers_server.py", DOCS_ROOT=str(ICI / "archives")),
    tool_filter=["lire_document"],
    tool_name_prefix="archives",
)
root_agent = Agent(..., tools=[commandes, fichiers, archives])

Le filtre s'applique aux noms d'origine du serveur (lire_document) : dans ADK 2.10, get_tools() filtre, puis get_tools_with_prefix() ajoute le préfixe. Le modèle verra archives_lire_document. C'est exactement ce que vérifie test_filtrage_des_outils_mcp pour docs.

Résultat obtenu

Test du 2026-10-15 qui démarre les vrais serveurs en sous-processus et liste ce que voit le modèle (sans clé d'API) :

test_mcp.py::test_filtrage_des_outils_mcp PASSED
test_mcp.py::test_appel_outil_mcp_de_bout_en_bout PASSED

Le premier vérifie ["executer_select", "lister_tables"] pour les commandes et ["docs_lire_document", "docs_lister_documents"] pour les fichiers. Pendant l'écriture du lab, une première version de ce test appelait get_tools() et obtenait lire_document sans préfixe : c'est ainsi que nous avons vérifié l'ordre « filtre puis préfixe ».

Je sais faire

  • Déclarer un McpToolset avec StdioConnectionParams
  • Filtrer les outils par liste de noms ou par prédicat, et savoir lequel choisir
  • Préfixer les outils d'un serveur
  • Fermer les sous-processus MCP avec runner.close()
Next