Module 3 — Explorer les tools MCP
Le serveur MCP tourne (Module 2), mais rien ne l’a encore interrogé. Ce module écrit un vrai client Python avec le SDK officiel mcp et l’utilise contre le serveur réel.
Un point de vigilance sur les versions du SDK
Section titled “Un point de vigilance sur les versions du SDK”En écrivant ce client, une différence de version du SDK mcp a été rencontrée : les exemples circulant en ligne pour d’anciennes versions importent streamablehttp_client (sans séparateur) et le font renvoyer un triplet (read, write, get_session_id). Avec la version installée ici (mcp==2.2.0), la fonction s’appelle streamable_http_client (avec un tiret bas), et son gestionnaire de contexte renvoie un couple (read, write). Vérification faite directement sur le paquet installé :
python -c "import mcp.client.streamable_http as mprint([n for n in dir(m) if 'stream' in n.lower()])"['streamable_http_client', ...]C’est un rappel utile : ne jamais recopier un exemple de SDK sans vérifier la version réellement installée (pip show mcp).
Le client réutilisable
Section titled “Le client réutilisable”src/mcp_odoo_toolkit/client.py encapsule l’ouverture de session dans un gestionnaire de contexte asynchrone :
from contextlib import asynccontextmanager
from mcp import ClientSessionfrom mcp.client.streamable_http import streamable_http_client
@asynccontextmanagerasync def mcp_session(url): """Ouvre une session MCP vers `url` (ex: http://localhost:8000/mcp), l'initialise, puis la ferme proprement en sortie de bloc `async with`.""" async with streamable_http_client(url) as (read, write): async with ClientSession(read, write) as session: await session.initialize() yield session
async def list_tool_names(session): """Retourne la liste des noms de tools exposes par le serveur MCP.""" result = await session.list_tools() return [tool.name for tool in result.tools]
async def call_tool(session, name, arguments): """Appelle un tool MCP et retourne son contenu textuel (premier bloc).""" result = await session.call_tool(name, arguments) if result.content: return result.content[0].text return NoneLe script de démonstration
Section titled “Le script de démonstration”examples/demo.py utilise ce client pour lister les tools disponibles, puis en appeler deux :
import asyncioimport sys
from mcp_odoo_toolkit import call_tool, list_tool_names, mcp_session
DEFAULT_URL = "http://localhost:8000/mcp"
# Certains tools renvoient du texte contenant des emojis (ex. avertissements# YOLO). La console Windows utilise par defaut un encodage (cp1252) qui ne# les supporte pas : on force stdout/stderr en UTF-8.sys.stdout.reconfigure(encoding="utf-8")sys.stderr.reconfigure(encoding="utf-8")
async def main(url: str) -> None: async with mcp_session(url) as session: tools = await list_tool_names(session) print(f"Tools MCP disponibles ({len(tools)}) :") for name in tools: print(f" - {name}")
print("\n--- list_models ---") result = await call_tool(session, "list_models", {}) print(result)
print("\n--- search_records: res.partner (limite a 5) ---") result = await call_tool( session, "search_records", {"model": "res.partner", "domain": [], "limit": 5} ) print(result)
if __name__ == "__main__": url = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_URL asyncio.run(main(url))Exécution réelle
Section titled “Exécution réelle”python examples/demo.pySortie réellement obtenue (tronquée pour la lisibilité — la liste complète des modèles et des contacts est bien plus longue) :
Tools MCP disponibles (11) : - search_records - get_record - get_fields - get_current_context - list_models - list_resource_templates - create_record - update_record - delete_record - post_message - aggregate_records
--- list_models ---{ "models": [ { "model": "res.partner", "name": "Contact", "operations": null }, { "model": "res.company", "name": "Companies", "operations": null }, { "model": "res.users", "name": "User", "operations": null }, ... ], "yolo_mode": { "enabled": true, "level": "true", "description": "FULL ACCESS", "warning": "All models accessible without MCP security!", "operations": { "read": true, "write": true, "create": true, "unlink": true } }, "total": 55, "total_available": 55}
--- search_records: res.partner (limite a 5) ---{ "records": [ { "id": 14, "name": "Azure Interior", "city": "Fremont", "email": "[email protected]", "is_company": true }, { "id": 26, "name": "Brandon Freeman", "city": "Fremont", "email": "[email protected]", "is_company": false }, { "id": 33, "name": "Colleen Diaz", "city": "Fremont", "email": "[email protected]", "is_company": false }, { "id": 27, "name": "Nicole Ford", "city": "Fremont", "email": "[email protected]", "is_company": false }, { "id": 10, "name": "Deco Addict", "city": "Pleasant Hill", "email": "[email protected]", "is_company": true } ], "total": 36, "limit": 5, "offset": 0, "model": "res.partner"}Deux observations importantes sur cette sortie réelle :
list_modelsrecense 55 modèles, sans distinction (res.users,ir.config_parameter,res.users.apikeys…) : c’est exactement la conséquence du mode YOLO annoncée au Module 1, pas une erreur de configuration.search_recordsretourne 36 contacts réels, chargés par les données de démonstration Odoo (demo=trueà la création de la base) — pas des valeurs inventées pour ce cours.
Aller plus loin : les autres tools
Section titled “Aller plus loin : les autres tools”Les mêmes principes s’appliquent aux autres tools exposés : get_record (lire un enregistrement précis par id), get_fields (introspecter les champs d’un modèle), create_record/update_record/delete_record (écrire), aggregate_records (agrégations façon read_group), post_message (poster un message sur le chatter d’un enregistrement), call_model_method (appeler une méthode Python arbitraire du modèle — à utiliser avec prudence, surtout en mode YOLO).
Prochaine étape
Section titled “Prochaine étape”Le mode YOLO a permis d’aller vite, mais son absence de contrôle d’accès n’est pas acceptable au-delà d’une démonstration. Le Module 4 détaille le module Odoo mcp_server, qui ajoute la couche de sécurité manquante.