Skip to content

Module 5 — Basculer en mode sécurisé

Ce module bascule la stack du Module 2 du mode YOLO vers le mode sécurisé décrit au Module 4. Toutes les commandes ont été réellement exécutées contre le module mcp_server obtenu depuis sa source officielle — jamais copié dans ce dépôt (voir l’avertissement de licence du Module 4).

1. Monter votre copie du module (local uniquement)

Section titled “1. Monter votre copie du module (local uniquement)”

Créez un docker-compose.override.yml (déjà dans .gitignore du projet) à partir de l’exemple fourni :

Terminal window
cp docker-compose.override.example.yml docker-compose.override.yml

Adaptez-y le chemin vers votre propre copie du module :

services:
odoo:
volumes:
- "/chemin/vers/votre/copie/mcp_server:/mnt/extra-addons/mcp_server:ro"

Puis recréez le conteneur Odoo pour qu’il prenne en compte ce nouveau chemin d’extension :

Terminal window
docker compose up -d odoo

L’image officielle odoo:16 place déjà /mnt/extra-addons dans addons_path par défaut. On installe le module directement en ligne de commande, dans la base créée au Module 2 :

Terminal window
docker exec mcp-odoo-toolkit-odoo-1 \
odoo --db_host=db --db_port=5432 --db_user=odoo --db_password=odoo \
-d mcp_demo -i mcp_server --stop-after-init

Sortie réelle (extrait) :

odoo.modules.loading: Loading module mcp_server (18/24)
odoo.modules.registry: module mcp_server: creating or updating database tables
odoo.modules.loading: loading mcp_server/security/security.xml
odoo.modules.loading: loading mcp_server/security/ir.model.access.csv
odoo.modules.loading: loading mcp_server/views/mcp_enabled_models_views.xml
odoo.modules.loading: loading mcp_server/views/res_config_settings_views.xml
odoo.modules.loading: Module mcp_server loaded in 3.62s, 391 queries (+391 other)
...
odoo.modules.loading: 24 modules loaded in 33.13s, 7683 queries (+7683 extra)

Aucune erreur : la seule dépendance Python externe du module (defusedxml) était déjà présente dans l’image officielle.

Deux réglages sont nécessaires : le commutateur global (mcp_server.enabled) et l’activation du modèle res.partner en lecture seule uniquement, via l’ORM (ici en ligne de commande, l’équivalent de ce que fait l’interface Paramètres > MCP Server) :

res = models.execute_kw(db, uid, password, "ir.config_parameter", "set_param",
["mcp_server.enabled", "True"])
partner_model_id = models.execute_kw(db, uid, password, "ir.model", "search",
[[["model", "=", "res.partner"]]])
models.execute_kw(db, uid, password, "mcp.enabled.model", "create", [{
"model_id": partner_model_id[0],
"allow_read": True,
"allow_create": False,
"allow_write": False,
"allow_unlink": False,
}])

Vérification via l’endpoint REST du module lui-même (/mcp/health et /mcp/system/info, sans dépendre encore du serveur MCP) :

Terminal window
curl -s http://localhost:8069/mcp/health
{"success": true, "data": {"status": "ok", "mcp_server_version": "16.0.1.2.0"}}

En interface graphique, cela se fait depuis Mon profil > Comptes développeur > Nouvelle clé API. Pour rester scriptable, on peut appeler directement la méthode native d’Odoo (res.users.apikeys._generate) via odoo shell, en se plaçant dans le contexte de l’utilisateur admin :

ApiKeys = env["res.users.apikeys"].with_user(2) # 2 = admin
key_value = ApiKeys._generate(None, "mcp-odoo-toolkit-test")
print(key_value)
env.cr.commit()
odoo.addons.base.models.res_users: Users API Keys generated: scope: <None> for 'admin' (#2) from n/a
022aa9fc2e714fabe5eeeac09da4470ef0efc0a7

Confirmation, toujours via l’endpoint REST du module, avec la clé fraîchement générée :

Terminal window
curl -s http://localhost:8069/mcp/system/info -H "X-API-Key: 022aa9fc2e714fabe5eeeac09da4470ef0efc0a7"
{"success": true, "data": {"db_name": "mcp_demo", "enabled_mcp_models": 1, "mcp_server_version": "16.0.1.2.0"}}

enabled_mcp_models: 1 confirme que seul res.partner est whitelisté.

Basculez .env du mode YOLO vers la clé d’API :

ODOO_DB=mcp_demo
ODOO_API_KEY=022aa9fc2e714fabe5eeeac09da4470ef0efc0a7
ODOO_YOLO=false
Terminal window
docker compose up -d mcp-server

Les logs du conteneur montrent un comportement radicalement différent du Module 2 :

odoo_connection - INFO - Authenticating in standard MCP mode for database 'mcp_demo'
odoo_connection - INFO - Attempting MCP API key authentication
odoo_connection - INFO - Successfully authenticated with MCP API key (UID: 2)
access_control - INFO - Initialized AccessController for http://odoo:8069 (API key auth)
odoo_connection - ERROR - XML-RPC fault during read on res.users: <Fault 403:
"Access denied by MCP for model 'res.users' method 'read'.">

Plus aucune mention de « YOLO » : le serveur s’authentifie désormais par clé d’API — et tente même, dès le démarrage, de lire res.users pour construire son contexte utilisateur, tentative que le module rejette immédiatement puisque res.users n’est pas whitelisté. C’est le contrôle d’accès en action, dès la première seconde.

Sans changer une ligne du client Python, on relance exactement le même script :

Terminal window
python examples/demo.py
list_models — avant : 55 modèles (Module 3) / après : 1 seul
{
"models": [
{ "model": "res.partner", "name": "Contact",
"operations": { "read": true, "write": false, "create": false, "unlink": false } }
],
"yolo_mode": null,
"total": 1,
"total_available": 1
}

search_records sur res.partner continue de fonctionner (lecture autorisée) et retourne les mêmes 36 contacts qu’au Module 3. En revanche, une tentative d’écriture est rejetée :

Terminal window
python examples/test_hardened_denial.py
Error executing tool create_record: Access denied: Operation 'create' not allowed on model 'res.partner'
logs = env["mcp.log"].sudo().search_read(
[], ["event_type", "model_name", "operation"], limit=5, order="id desc")
{'event_type': 'model_access', 'model_name': 'res.partner', 'operation': 'read'}
{'event_type': 'auth_success', 'model_name': False, 'operation': False}
{'event_type': 'model_access', 'model_name': 'res.partner', 'operation': 'fields_get'}
{'event_type': 'model_access', 'model_name': 'res.partner', 'operation': 'search_count'}
{'event_type': 'model_access', 'model_name': 'res.partner', 'operation': 'search'}

Chaque appel XML-RPC réellement transmis à Odoo est journalisé. Une nuance observée en testant : le refus de create_record vu à l’étape 6 est décidé côté serveur MCP lui-même (à partir de la liste des modèles et opérations autorisés qu’il a récupérée), avant même d’émettre un appel XML-RPC — il n’apparaît donc pas dans mcp.log, qui journalise les appels reçus côté Odoo. Le refus de lecture de res.users à l’étape 5, lui, provient bien d’un appel XML-RPC réellement reçu et rejeté par Odoo, et apparaît donc dans le journal avec l’événement permission_denied.

Ce que cette bascule a changé, concrètement

Section titled “Ce que cette bascule a changé, concrètement”
Mode YOLO (Module 3) Mode sécurisé (ce module)
Modèles visibles par list_models 55 1 (res.partner)
Opérations sur res.partner toutes lecture seule
Authentification mot de passe Odoo clé d’API dédiée
res.users accessible ? oui non — rejeté et journalisé
Trace des accès aucune mcp.log, par appel

Le Module 6 branche ce même serveur MCP (en mode sécurisé ou YOLO, au choix) sur une interface de chat complète : LibreChat.

👉 Module 6 : Brancher LibreChat