xpeditis2.0/docs/features/mcp.md
2026-09-07 21:40:54 +02:00

11 KiB

Serveur MCP et registre de capacités

Pourquoi MCP et non une skill

La demande était : « gérer l'ensemble des fonctionnalités du site en fonction des permissions de la personne ». Cette condition tranche le choix à elle seule.

Skill MCP
Nature Des instructions (Markdown + scripts) chargées dans le contexte d'un agent Un protocole : un serveur qui expose des outils, appelés en JSON-RPC
Qui exécute L'agent, avec les accès qu'il a déjà Votre serveur, que vous contrôlez
Permissions Aucune. Une skill est du texte : elle peut demander de respecter des droits, le modèle peut l'ignorer Vérifiées à chaque appel, dans le processus, sur l'identité authentifiée
Auditabilité Aucune trace propre Chaque appel passe par vos services, vos logs, vos gardes
Public Un agent qui a déjà accès à vos systèmes N'importe quel client MCP, et votre propre backend

Une frontière de permissions ne peut pas être faite d'instructions. Une skill qui dirait « ne supprime une réservation que si l'utilisateur est ADMIN » est une consigne : un message bien tourné la contourne, et aucun contrôle serveur ne rattrape le coup. MCP place la vérification là où elle tient : dans NestJS, avant l'action.

Verdict : MCP, sans hésitation, pour la couche de capacités.

Ce qui aurait été un contresens : « MCP partout »

Il y a deux consommateurs, et ils n'ont pas les mêmes besoins :

  1. Un client externe (Claude Desktop, Claude Code, un automate) → a besoin de MCP, c'est précisément le protocole d'interopérabilité.
  2. L'assistant intégré (/dashboard/assistant) → tourne dans le même processus NestJS. Lui faire parler MCP à lui-même en HTTP ajouterait un aller-retour réseau, une seconde authentification et une traduction de protocole, pour zéro bénéfice. Il lui faut de l'appel de fonctions branché sur les mêmes définitions.

D'où l'architecture retenue : un registre, deux adaptateurs.

              ┌─────────────────────────────────────┐
              │      Registre de capacités          │
              │  nom · schéma · rôle · offre        │
              │  → délègue aux services existants   │
              └─────────────────────────────────────┘
                   ▲                         ▲
        adaptateur MCP              adaptateur function-calling
       (HTTP + clé API/JWT)            (assistant intégré)
     Claude Desktop, Claude Code       /dashboard/assistant

Ajouter une capacité = une entrée, disponible des deux côtés, avec les mêmes droits.

Et la skill ? Elle a une place, mais pas celle-là : plus tard, pour apprendre à un agent externe comment bien utiliser ces outils (vocabulaire du LCL, quand chercher un tarif plutôt que le wiki, déroulé d'une réservation). C'est de la documentation pour agent, posée au-dessus de MCP — jamais à la place.

Le registre

application/mcp/capability.registry.ts compose le catalogue. Chaque capacité déclare :

{
  policy: { name, scope: 'read' | 'write', roles?, feature? },
  description,   // une phrase, du point de vue de l'utilisateur
  inputSchema,   // JSON Schema — contrat annoncé ET règle de validation
  handler,       // délègue au service applicatif qui sert déjà l'interface
}

Le registre n'implémente aucune logique métier. delete_unpaid_booking appelle CsvBookingService.deleteBooking, qui applique la même règle que le menu de la liste. Il n'y a pas de seconde vérité pour les agents.

La politique d'accès

domain/services/capability-access.ts, pur et testé :

  1. Le rôle — comparaison insensible à la casse, comme RolesGuard.
  2. L'offre — via les mêmes PLAN_FEATURES que le reste du produit, sur l'offre effective (effectivePlan) : un compte ADMIN dispose de Platinium, exactement comme dans l'aperçu d'abonnement et dans l'assistant.

Deux comportements volontaires :

  • Une capacité hors droits n'apparaît pas dans tools/list. Un agent ne peut pas proposer, ni même mentionner, une action interdite.
  • Un refus de rôle répond « capacité inconnue », indistinguable d'une capacité inexistante — sinon il suffirait de deviner les noms. Un refus lié à l'offre, lui, se dit clairement : la fonction existe, elle s'achète.

La validation des entrées

Les arguments viennent d'un modèle de langage : plausibles, pas fiables. parseInput applique le schéma strictement — types, valeurs autorisées, bornes — accepte les chaînes numériques ("4.5" → 4.5), traite null et "" comme absents, et refuse tout paramètre inventé plutôt que de le transmettre au service.

Capacités exposées (première vague)

Capacité Portée Condition
whoami lecture —
get_subscription lecture ADMIN, MANAGER
search_documentation lecture —
search_rates lecture —
list_carriers lecture —
list_my_bookings lecture —
list_organization_bookings lecture ADMIN, MANAGER
get_booking lecture —
booking_statistics lecture —
cancel_booking écriture —
delete_unpaid_booking écriture —
admin_list_users lecture ADMIN
admin_list_organizations lecture ADMIN
admin_rate_grid_overview lecture ADMIN

search_documentation réutilise l'index RAG de l'assistant : un agent externe répond donc à partir du wiki Xpeditis, avec les liens des pages.

Les écritures sont limitées à ce qui est réversible ou inoffensif. Payer une commission, envoyer une demande à un transporteur ou téléverser un document engagent un tiers ou de l'argent : hors de portée d'un agent à ce stade, délibérément.

Le transport

POST /api/v1/mcp — JSON-RPC 2.0, sans session ni SSE. Un serveur qui n'expose que des outils n'a rien à diffuser entre deux appels, et l'absence d'état rend chaque requête authentifiable indépendamment : deux appels consécutifs peuvent venir de deux comptes différents.

Méthodes : initialize, tools/list, tools/call, ping, et les notifications (notifications/*, sans réponse). Non couvert : ressources, invites, négociation SSE, notifications serveur → client.

L'authentification n'est pas réimplémentée : la route passe par le garde global ApiKeyOrJwtGuard — clé API X-API-Key (offres Gold et Platinium) ou jeton JWT.

Une erreur d'outil est rendue dans le résultat (isError: true) et non comme erreur JSON-RPC : c'est ce que demande MCP, pour qu'un agent puisse corriger son appel au lieu d'interrompre l'échange.

Se connecter

# Claude Code, avec une clé API Xpeditis (Gold ou Platinium)
claude mcp add --transport http xpeditis https://api.xpeditis.com/api/v1/mcp \
  --header "X-API-Key: xped_live_..."

En local, l'URL est http://localhost:4000/api/v1/mcp.

Vérification

npm test -- capability-access.spec.ts capability.spec.ts capability.registry.spec.ts

42 tests couvrent la politique d'accès, la validation des entrées et le registre. Éprouvé en plus sur la pile réelle avec deux identités : un ADMIN voit 11 outils, un USER en voit 9 — get_subscription et list_organization_bookings n'apparaissent pas dans sa liste et répondent « inconnue » s'il les devine.

L'assistant intégré agit

/dashboard/assistant consomme le même registre, non pas en MCP mais par appel de fonctions — il tourne dans le même processus, un aller-retour HTTP vers lui-même n'apporterait rien.

Le service construit le catalogue filtré pour l'appelant et le passe à l'adaptateur avec un exécuteur déjà lié à son identité : les arguments du modèle décrivent quoi faire, jamais pour qui. Une identité ne peut pas être passée en paramètre, elle vient de la session.

La boucle est bornée à 4 allers-retours d'outils par question ; au dernier tour les outils sont retirés, le modèle doit conclure avec ce qu'il a. Un outil en échec repart vers le modèle comme un résultat — il peut corriger son appel ou l'expliquer — plutôt que de perdre la réponse. Les jetons des différents tours sont cumulés : le quota facture l'échange entier.

Les actions effectuées sont persistées (trade_messages.actions) et affichées au-dessus de la réponse, avec leur succès ou leur échec. Une action sur le compte ne doit pas reposer sur la seule parole du modèle.

Piège rencontré, à ne pas réintroduire. L'instruction système disait « tu ne disposes pas d'un accès aux dossiers clients ». Une fois les outils branchés, le modèle l'a suivie et a refusé d'agir, outils en main. Cette phrase n'est désormais dite que lorsqu'elle est vraie — NO_TOOL_RULES quand aucun outil n'est ouvert, TOOL_RULES sinon.

Console d'assistant de l'administration

/admin/assistant rend le même écran que l'espace produit (AssistantWorkspace), avec des amorces tournées vers le pilotage. Ce qu'un administrateur peut faire ne vient pas de cette page : le serveur ouvre les capacités admin_* sur la foi de son rôle, ici comme depuis un client MCP.

Trois capacités d'administration, en lecture seule : admin_list_users, admin_list_organizations, admin_rate_grid_overview. Elles franchissent la frontière de l'organisation — c'est ce qui les distingue du reste du catalogue. Modifier un utilisateur, valider un SIRET ou remplacer une grille touche des comptes clients et de l'argent : ces actions restent à la main d'une personne tant qu'un mécanisme de confirmation explicite n'existe pas côté agent.

Journal des appels d'agents

Chaque invocation est journalisée dans audit_logs sous l'action agent_capability_invoked, avec le nom de la capacité (resourceName) et, en métadonnées, la surface (mcp ou assistant), la portée et le rôle.

L'audit est posé dans le registre, pas dans chaque adaptateur : c'est le seul passage obligé des deux surfaces, donc le seul endroit où la trace ne peut pas être oubliée en ajoutant une capacité. Les refus sont journalisés autant que les succès — c'est ce qui révèle une tentative répétée. AuditService.log n'échoue jamais vers l'appelant : une panne du journal n'empêche pas une action déjà autorisée.

Seuils de pertinence du RAG, ré-étalonnés

Mesuré le 2026-09-06 sur le corpus réel : une question métier remonte des extraits entre 0,57 et 0,71, une question sans rapport plafonne à 0,40. Le seuil initial de 0,28 faisait citer des pages « Calcul du Fret » sous une réponse portant sur le nombre de réservations. La coupure est passée à 0,45, avec de la marge des deux côtés. Le repli lexical, qui n'est pas sur la même échelle (une proportion de mots couverts), garde son propre seuil.

Suites

  1. Écritures d'administration : possibles maintenant que le journal existe, mais elles demandent un mécanisme de confirmation explicite côté agent avant d'être ouvertes.
  2. Skill Xpeditis : le mode d'emploi métier des outils, pour les agents externes.
  3. Transport MCP : ressources, invites et flux SSE ne sont pas couverts.