Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BAUeCFpDkRD6tU5wGsc1C
159 lines
11 KiB
Markdown
159 lines
11 KiB
Markdown
# 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 :
|
|
|
|
```ts
|
|
{
|
|
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
|
|
|
|
```sh
|
|
# 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
|
|
|
|
```sh
|
|
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.
|