feat(mcp) : add MCP resources, documentation, and .mcp.json config

- 3 MCP resources: schema, roles, stats
- docs/mcp/README.md with full user guide (config, tools catalogue, workflows)
- .mcp.json for Claude Code stdio transport
- Design spec and implementation plan

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-03-16 15:49:00 +01:00
parent 4340a0e13e
commit f965affc94
7 changed files with 2476 additions and 0 deletions

185
docs/mcp/README.md Normal file
View File

@@ -0,0 +1,185 @@
# MCP Server — Inventory
Serveur MCP (Model Context Protocol) pour l'application Inventory. Permet aux assistants IA (Claude, ChatGPT, Codex) de consulter et gérer l'inventaire industriel.
## Prérequis
- Un profil actif avec rôle suffisant (ROLE_VIEWER pour lecture, ROLE_GESTIONNAIRE pour écriture)
- Accès au tunnel pour les clients distants (Claude Desktop, ChatGPT Desktop)
- Docker Compose démarré (`make start`)
## Configuration par client
### Claude Code (local, stdio)
Le fichier `.mcp.json` à la racine du projet est déjà configuré. Remplacez les placeholders :
```json
{
"mcpServers": {
"inventory": {
"command": "docker",
"args": [
"exec", "-i",
"-e", "MCP_PROFILE_ID=VOTRE_PROFILE_ID",
"-e", "MCP_PROFILE_PASSWORD=VOTRE_PASSWORD",
"php-inventory-apache",
"php", "bin/console", "mcp:server"
]
}
}
}
```
### Claude Desktop (HTTP via tunnel)
Dans `claude_desktop_config.json` :
```json
{
"mcpServers": {
"inventory": {
"url": "https://inventory.company-tunnel.com/_mcp",
"headers": {
"X-Profile-Id": "VOTRE_PROFILE_ID",
"X-Profile-Password": "VOTRE_PASSWORD"
}
}
}
}
```
### ChatGPT Desktop / Codex
Meme principe HTTP avec l'URL du tunnel + headers d'auth.
## Catalogue des Tools
### Tools de haut niveau
| Tool | Description | Role |
|------|-------------|------|
| `search_inventory` | Recherche globale (machines, pieces, composants, produits, sites, constructeurs) | VIEWER |
| `get_machine_structure` | Hierarchie complete d'une machine | VIEWER |
| `clone_machine` | Clone une machine avec toute sa structure | GESTIONNAIRE |
| `get_dashboard_stats` | Statistiques globales | VIEWER |
| `get_entity_history` | Historique d'audit d'une entite | VIEWER |
| `get_activity_log` | Journal d'activite global | VIEWER |
### CRUD par entite
Pour chaque entite (Machine, Composant, Piece, Produit, Site, Constructeur) :
| Pattern | Exemple | Role |
|---------|---------|------|
| `list_{entite}s` | `list_machines` | VIEWER |
| `get_{entite}` | `get_machine` | VIEWER |
| `create_{entite}` | `create_machine` | GESTIONNAIRE |
| `update_{entite}` | `update_machine` | GESTIONNAIRE |
| `delete_{entite}` | `delete_machine` | GESTIONNAIRE |
### Slots
| Tool | Description | Role |
|------|-------------|------|
| `list_slots` | Lister les slots d'un composant ou piece | VIEWER |
| `update_slots` | Remplir/vider les slots | GESTIONNAIRE |
### Machine Links
| Tool | Description | Role |
|------|-------------|------|
| `list_machine_links` | Liens composant/piece/produit d'une machine | VIEWER |
| `add_machine_links` | Ajouter des liens | GESTIONNAIRE |
| `update_machine_link` | Modifier un lien | GESTIONNAIRE |
| `remove_machine_link` | Supprimer un lien | GESTIONNAIRE |
### Commentaires
| Tool | Description | Role |
|------|-------------|------|
| `list_comments` | Lister les commentaires d'une entite | VIEWER |
| `create_comment` | Creer un commentaire | VIEWER |
| `resolve_comment` | Resoudre un commentaire | GESTIONNAIRE |
| `get_unresolved_comments_count` | Nombre de commentaires non resolus | VIEWER |
### Custom Fields
| Tool | Description | Role |
|------|-------------|------|
| `list_custom_field_values` | Valeurs de champs perso d'une entite | VIEWER |
| `upsert_custom_field_values` | Creer/mettre a jour des valeurs | GESTIONNAIRE |
| `delete_custom_field_value` | Supprimer une valeur | GESTIONNAIRE |
### Documents
| Tool | Description | Role |
|------|-------------|------|
| `list_documents` | Lister les documents d'une entite | VIEWER |
| `delete_document` | Supprimer un document | GESTIONNAIRE |
> **Limitation :** L'upload de documents n'est pas supporte via MCP (protocole JSON uniquement). Utilisez l'API REST `/api/documents` (POST multipart).
### ModelTypes
| Tool | Description | Role |
|------|-------------|------|
| `list_model_types` | Lister par categorie | VIEWER |
| `get_model_type` | Detail avec skeleton requirements | VIEWER |
| `create_model_type` | Creer | GESTIONNAIRE |
| `update_model_type` | Modifier | GESTIONNAIRE |
| `delete_model_type` | Supprimer | GESTIONNAIRE |
| `sync_model_type` | Preview/sync skeleton | GESTIONNAIRE |
## Workflows guides
### Creer un composant complet
```
1. list_model_types(category: "composant") -> choisir le type
2. get_model_type(modelTypeId: "...") -> voir le skeleton
3. create_composant(name, reference, modelTypeId) -> cree + slots auto
4. search_inventory(query: "Roulement", types: "piece") -> trouver pieces
5. update_slots(slots: [{slotId, selectedPieceId}]) -> remplir
6. upsert_custom_field_values(entityType: "composant", entityId, fields: [...])
```
### Creer une machine complete (bottom-up)
```
1. Creer les produits necessaires
2. Creer les pieces (avec produits dans les slots)
3. Creer les composants (avec pieces dans les slots)
4. list_sites -> choisir le site
5. create_machine(name, siteId)
6. add_machine_links(machineId, links: [{type: "composant", entityId, quantity}])
7. upsert_custom_field_values(entityType: "machine", machineId, fields: [...])
```
## Resources MCP
| URI | Description |
|-----|-------------|
| `inventory://schema/entities` | Schema de toutes les entites |
| `inventory://roles` | Hierarchie des roles et permissions |
| `inventory://stats` | Statistiques globales |
## Roles & Permissions
```
ROLE_ADMIN > ROLE_GESTIONNAIRE > ROLE_VIEWER > ROLE_USER
```
- **VIEWER** : lecture, recherche, commentaires
- **GESTIONNAIRE** : ecriture (CRUD, slots, links, clone)
- **ADMIN** : gestion profils (via API REST uniquement)
## Troubleshooting
| Erreur | Cause | Solution |
|--------|-------|----------|
| `401 Unauthorized` | Credentials invalides | Verifier X-Profile-Id et X-Profile-Password |
| `Permission denied: ROLE_GESTIONNAIRE required` | Role insuffisant | Utiliser un profil avec le bon role |
| `Rate limited` | Trop de tentatives echouees | Attendre 1 minute |
| `Tool not found` | Tool non enregistre | Verifier que le cache est a jour (`cache:clear`) |
| `Error while executing tool` | Erreur interne | Verifier les logs et les parametres |

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,669 @@
# MCP Server — Inventory Project — Design Spec
**Date :** 2026-03-16
**Version projet :** 1.9.1
**Statut :** Draft (post-review v2)
---
## 1. Objectif
Exposer l'intégralité de l'API Inventory (machines, pièces, composants, produits, sites, constructeurs, custom fields, documents, commentaires, audit) via un serveur MCP (Model Context Protocol) intégré directement dans l'application Symfony.
Le serveur doit être compatible avec tous les clients MCP majeurs : Claude Code, Claude Desktop, ChatGPT Desktop, Codex, et tout client supportant le protocole MCP.
## 2. Contraintes
| Contrainte | Détail |
|---|---|
| **Réseau** | Machine hébergée sur un réseau fermé d'entreprise. Les clients distants (Claude Desktop, ChatGPT, Codex) accèdent via un tunnel chiffré (Cloudflare/WireGuard/SSH) |
| **Auth** | Pass-through : chaque client fournit ses propres credentials (profileId + password). Le serveur MCP charge le profil correspondant et applique ses rôles. Les actions sont traçables par utilisateur dans l'audit log |
| **Transport** | Dual : stdio pour usage local (Claude Code sur la même machine) + HTTP Streamable/SSE pour clients distants via tunnel |
| **Stack** | PHP / Symfony 8.0 — le serveur MCP vit dans l'application existante, pas de service séparé |
| **Scope** | Lecture + écriture complète — les outils couvrent tout le CRUD + les opérations métier |
## 3. Stack technique
| Composant | Choix |
|---|---|
| SDK MCP | `symfony/mcp-bundle` v0.6.0 + `mcp/sdk` ^0.4 (officiel Symfony + PHP Foundation + Anthropic) |
| Transport stdio | `bin/console mcp:server` (dans le container Docker) |
| Transport HTTP | Endpoint `/_mcp` sur le même port que l'API (8081) |
| Auth HTTP | Custom Symfony Authenticator (`McpHeaderAuthenticator`) intégré au firewall Symfony |
| Auth stdio | Token synthétique chargé depuis `$_ENV` au boot |
| Rate limiting | `symfony/rate-limiter` sur les tentatives d'auth échouées |
| Accès données | Repositories Doctrine directs (pas de hop HTTP interne) |
**Note :** Le bundle est expérimental et non couvert par la BC Promise de Symfony. L'implémentation inclut un spike/PoC initial (étape 1 du plan) pour valider la compatibilité de l'API réelle du bundle avec ce design.
## 4. Architecture
```
┌─────────────────────────────────────────────────────┐
│ Docker Compose (réseau fermé entreprise) │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ php-inventory-apache (Symfony 8) │ │
│ │ │ │
│ │ /api/* ← API REST existante │ │
│ │ /_mcp ← Endpoint MCP HTTP (SSE) │ │
│ │ bin/console mcp:server ← Transport stdio │ │
│ │ │ │
│ │ Firewall Symfony : │ │
│ │ ^/api → SessionProfileAuthenticator │ │
│ │ ^/_mcp → McpHeaderAuthenticator │ │
│ │ │ │
│ │ src/Mcp/Tool/ ← Tools MCP │ │
│ │ src/Mcp/Resource/ ← Resources MCP │ │
│ │ src/Mcp/Security/ ← Authenticator + Guard │ │
│ └──────────┬───────────────────────────────────┘ │
│ │ réseau Docker interne │
│ ┌──────────▼──────────┐ │
│ │ PostgreSQL 16 │ │
│ └─────────────────────┘ │
└──────────────────┬──────────────────────────────────┘
│ tunnel (chiffré)
┌──────────────▼──────────────────┐
│ Postes utilisateurs │
│ - Claude Desktop → HTTP/SSE │
│ - ChatGPT Desktop → HTTP/SSE │
│ - Codex → HTTP/SSE │
│ - Claude Code local → stdio │
└─────────────────────────────────┘
```
Le serveur MCP accède directement aux repositories Doctrine et aux services Symfony existants. Pas de double sérialisation — les tools appellent les mêmes repositories/services que les controllers REST.
## 5. Authentification pass-through
### 5.1 Firewall Symfony — intégration sécurité
Un firewall dédié pour `/_mcp` avec un authenticator custom. Cela garantit que `$security->getUser()` retourne le bon Profile, que la hiérarchie des rôles fonctionne via `is_granted()`, et que l'audit log trace le bon acteur.
```yaml
# config/packages/security.yaml (ajout)
security:
firewalls:
mcp:
pattern: ^/_mcp
stateless: true
custom_authenticators:
- App\Mcp\Security\McpHeaderAuthenticator
```
Le `McpHeaderAuthenticator` implémente `AuthenticatorInterface` :
1. Extrait `X-Profile-Id` et `X-Profile-Password` des headers
2. Charge le profil via `ProfileRepository`
3. Vérifie le password hash via `UserPasswordHasherInterface`
4. Retourne un `Passport` avec le Profile comme User
5. Symfony gère le reste (token, rôles, hiérarchie)
Cela permet à `AbstractAuditSubscriber.resolveActorProfileId()` de résoudre l'acteur via `$security->getUser()` sans aucune modification du code existant.
### 5.2 Transport stdio — token synthétique
Pour le transport stdio (pas de requête HTTP), un `EventSubscriber` sur `console.command` (quand la commande est `mcp:server`) :
1. Lit `MCP_PROFILE_ID` et `MCP_PROFILE_PASSWORD` depuis `$_ENV`
2. Valide les credentials
3. Injecte un `UsernamePasswordToken` synthétique dans le `TokenStorage` avec le Profile
### 5.3 Rate limiting — protection brute-force
```yaml
# config/packages/rate_limiter.yaml
framework:
rate_limiter:
mcp_auth:
policy: sliding_window
limit: 5
interval: '1 minute'
```
Le `McpHeaderAuthenticator` consomme le rate limiter sur chaque tentative échouée (clé = IP). Après 5 échecs en 1 minute, toute tentative est rejetée avec une erreur MCP `429 Too Many Requests`.
### 5.4 Vérification des rôles
Chaque tool déclare un rôle minimum. L'authenticator Symfony gère la hiérarchie :
| Rôle | Droits MCP |
|---|---|
| `ROLE_VIEWER` | Tous les tools de lecture (list, get, search, history) |
| `ROLE_GESTIONNAIRE` | Lecture + écriture (create, update, delete, slots, clone) |
| `ROLE_ADMIN` | Tout + gestion profils |
Les tools utilisent `$this->security->isGranted('ROLE_XXX')` pour vérifier, bénéficiant de la hiérarchie Symfony standard.
## 6. Catalogue des Tools MCP
### 6.1 Tools de haut niveau (métier)
| Tool | Description | Paramètres principaux | Rôle min |
|---|---|---|---|
| `search_inventory` | Recherche globale dans toutes les entités (machines, pièces, composants, produits, sites, constructeurs) | `query: string`, `types?: string[]`, `limit?: int` | VIEWER |
| `get_machine_structure` | Hiérarchie complète d'une machine : composants, pièces, produits, custom fields, slots | `machineId: string` | VIEWER |
| `clone_machine` | Clone une machine avec sa structure complète | `machineId: string`, `name: string`, `siteId: string`, `reference?: string` | GESTIONNAIRE |
| `get_entity_history` | Historique d'audit d'une entité | `entityType: string`, `entityId: string` | VIEWER |
| `get_activity_log` | Journal d'activité global | `page?: int`, `limit?: int`, `entityType?: string`, `action?: string` | VIEWER |
| `get_dashboard_stats` | Compteurs globaux (machines, pièces, composants, produits, commentaires ouverts) | aucun | VIEWER |
| `sync_model_type` | Preview ou exécution de la synchronisation skeleton d'un ModelType | `modelTypeId: string`, `action: "preview"\|"sync"`, `structure?: object` | GESTIONNAIRE |
### 6.2 Tools CRUD — Machines
| Tool | Description | Rôle min |
|---|---|---|
| `list_machines` | Lister les machines avec filtres (nom, référence, site) et pagination | VIEWER |
| `get_machine` | Détail d'une machine par ID | VIEWER |
| `create_machine` | Créer une machine (nom, référence, siteId, constructeurs) | GESTIONNAIRE |
| `update_machine` | Mise à jour partielle d'une machine | GESTIONNAIRE |
| `delete_machine` | Supprimer une machine | GESTIONNAIRE |
### 6.3 Tools CRUD — Composants
| Tool | Description | Rôle min |
|---|---|---|
| `list_composants` | Lister les composants avec filtres et pagination | VIEWER |
| `get_composant` | Détail d'un composant par ID (incluant ses slots) | VIEWER |
| `create_composant` | Créer un composant (nom, référence, modelTypeId, constructeurs). Retourne l'ID + les slots vides auto-générés | GESTIONNAIRE |
| `update_composant` | Mise à jour partielle | GESTIONNAIRE |
| `delete_composant` | Supprimer un composant | GESTIONNAIRE |
### 6.4 Tools CRUD — Pièces
| Tool | Description | Rôle min |
|---|---|---|
| `list_pieces` | Lister les pièces avec filtres et pagination | VIEWER |
| `get_piece` | Détail d'une pièce par ID (incluant ses product-slots) | VIEWER |
| `create_piece` | Créer une pièce (nom, référence, modelTypeId, constructeurs). Retourne l'ID + product-slots auto-générés | GESTIONNAIRE |
| `update_piece` | Mise à jour partielle | GESTIONNAIRE |
| `delete_piece` | Supprimer une pièce | GESTIONNAIRE |
### 6.5 Tools CRUD — Produits
| Tool | Description | Rôle min |
|---|---|---|
| `list_products` | Lister les produits avec filtres et pagination | VIEWER |
| `get_product` | Détail d'un produit par ID | VIEWER |
| `create_product` | Créer un produit (nom, référence, modelTypeId, prix (string), constructeurs) | GESTIONNAIRE |
| `update_product` | Mise à jour partielle | GESTIONNAIRE |
| `delete_product` | Supprimer un produit | GESTIONNAIRE |
### 6.6 Tools CRUD — Sites
| Tool | Description | Rôle min |
|---|---|---|
| `list_sites` | Lister les sites | VIEWER |
| `get_site` | Détail d'un site par ID | VIEWER |
| `create_site` | Créer un site | GESTIONNAIRE |
| `update_site` | Mise à jour partielle | GESTIONNAIRE |
| `delete_site` | Supprimer un site | GESTIONNAIRE |
### 6.7 Tools CRUD — Constructeurs
| Tool | Description | Rôle min |
|---|---|---|
| `list_constructeurs` | Lister les constructeurs/fournisseurs | VIEWER |
| `get_constructeur` | Détail d'un constructeur par ID | VIEWER |
| `create_constructeur` | Créer un constructeur | GESTIONNAIRE |
| `update_constructeur` | Mise à jour partielle | GESTIONNAIRE |
| `delete_constructeur` | Supprimer un constructeur | GESTIONNAIRE |
### 6.8 Tools — Commentaires (splittés)
| Tool | Description | Rôle min |
|---|---|---|
| `list_comments` | Lister les commentaires d'une entité | VIEWER |
| `create_comment` | Créer un commentaire sur une entité | VIEWER |
| `resolve_comment` | Marquer un commentaire comme résolu | GESTIONNAIRE |
| `get_unresolved_comments_count` | Nombre de commentaires non résolus | VIEWER |
### 6.9 Tools — Custom Fields (splittés)
| Tool | Description | Rôle min |
|---|---|---|
| `list_custom_field_values` | Lister les custom field values d'une entité | VIEWER |
| `upsert_custom_field_values` | Créer ou mettre à jour des custom field values | GESTIONNAIRE |
| `delete_custom_field_value` | Supprimer une custom field value | GESTIONNAIRE |
### 6.10 Tools — Documents (splittés)
| Tool | Description | Rôle min |
|---|---|---|
| `list_documents` | Lister les documents d'une entité | VIEWER |
| `delete_document` | Supprimer un document | GESTIONNAIRE |
> **Limitation connue :** L'upload de documents n'est pas supporté via MCP. Le protocole MCP échange du JSON — l'upload de fichiers binaires (multipart/form-data) n'est pas compatible. Les uploads doivent se faire via l'API REST `/api/documents` (POST multipart). Cette limitation pourra être réévaluée si le protocole MCP ajoute un support binaire.
### 6.11 Tools — Machine Links (splittés)
| Tool | Description | Rôle min |
|---|---|---|
| `list_machine_links` | Lister les liens composant/pièce/produit d'une machine | VIEWER |
| `add_machine_links` | Ajouter des liens machine↔composant/pièce/produit | GESTIONNAIRE |
| `update_machine_link` | Modifier un lien (quantité, overrides) | GESTIONNAIRE |
| `remove_machine_link` | Supprimer un lien | GESTIONNAIRE |
### 6.12 Tools — Slots
| Tool | Description | Rôle min |
|---|---|---|
| `list_slots` | Lister les slots d'un composant ou pièce avec état (rempli/vide, requirement). Paramètre `entityType: "composant"\|"piece"` + `entityId` | VIEWER |
| `update_slots` | Remplir un ou plusieurs slots. Paramètre `slots: [{slotId, selectedPieceId?\|selectedProductId?\|selectedComposantId?}]` | GESTIONNAIRE |
> **Note :** Un seul tool `list_slots` et un seul `update_slots` — ils acceptent un paramètre `entityType` pour dispatcher vers composant ou pièce. Un seul fichier d'implémentation par tool.
### 6.13 Tools — ModelTypes
| Tool | Description | Rôle min |
|---|---|---|
| `list_model_types` | Lister les ModelTypes par catégorie avec skeleton requirements | VIEWER |
| `get_model_type` | Détail complet d'un ModelType (requirements + custom fields) | VIEWER |
| `create_model_type` | Créer un ModelType | GESTIONNAIRE |
| `update_model_type` | Modifier un ModelType | GESTIONNAIRE |
| `delete_model_type` | Supprimer un ModelType | GESTIONNAIRE |
**Total : ~55 tools** (splittés pour des schémas JSON non-ambigus, meilleure compatibilité LLM)
> **Note :** Les tools d'administration des profils (`list_profiles`, `create_profile`, etc.) ne sont pas inclus — la gestion des profils reste exclusivement via l'API REST `/api/admin/profiles` (ROLE_ADMIN). Cela évite d'exposer la gestion des comptes/mots de passe via MCP.
## 7. Resources MCP
| URI | Description | Contenu |
|---|---|---|
| `inventory://schema/entities` | Schéma de toutes les entités | Nom, champs (nom, type, nullable, description) pour chaque entité |
| `inventory://model-types/{category}` | ModelTypes par catégorie | Liste des ModelTypes avec leurs skeleton requirements et custom fields |
| `inventory://roles` | Hiérarchie des rôles | Rôles et permissions associées pour guider le LLM |
| `inventory://stats` | Statistiques globales | Compteurs de chaque entité, commentaires ouverts |
## 8. Workflows de création guidés
### 8.1 Créer un Composant complet
```
1. list_model_types(category: "composant")
→ Choisir le type de composant
2. get_model_type(modelTypeId)
→ Voir les skeleton requirements : pièces, produits, sous-composants attendus
→ Voir les custom fields de chaque requirement
3. create_composant(name, reference, modelTypeId, constructeurs)
→ Reçoit: { id, slots: [{slotId, type, requirementName}, ...] }
4. search_inventory(query: "Roulement", types: ["piece"])
→ Trouver les pièces candidates pour chaque slot
5. update_slots([{slotId, selectedPieceId}, {slotId, selectedProductId}, ...])
→ Remplir les slots
6. upsert_custom_field_values(entityType: "composant", entityId,
fields: [{name: "Tension", value: "220V"}, ...])
→ Remplir les custom fields
```
### 8.2 Créer une Pièce complète
```
1. list_model_types(category: "piece")
2. get_model_type(modelTypeId)
3. create_piece(name, reference, modelTypeId, constructeurs)
→ Reçoit: { id, productSlots: [{slotId, requirementName}, ...] }
4. search_inventory(query: "...", types: ["product"])
5. update_slots([{slotId, selectedProductId}, ...])
6. upsert_custom_field_values(...)
```
### 8.3 Créer un Produit
```
1. list_model_types(category: "product")
2. create_product(name, reference, modelTypeId, prix, constructeurs)
3. upsert_custom_field_values(...)
```
### 8.4 Créer une Machine complète (de bas en haut)
```
1. Créer les produits nécessaires (§8.3)
2. Créer les pièces avec les produits dans les slots (§8.2)
3. Créer les composants avec les pièces dans les slots (§8.1)
4. list_sites → choisir le site
5. create_machine(name, reference, siteId, constructeurs)
6. add_machine_links(machineId, links: [
{type: "composant", entityId, quantity},
{type: "piece", entityId, quantity},
{type: "product", entityId}
])
7. upsert_custom_field_values(entityType: "machine", machineId, ...)
```
## 9. Pagination
Toutes les tools `list_*` utilisent un contrat de pagination uniforme :
### Paramètres d'entrée
| Paramètre | Type | Default | Description |
|---|---|---|---|
| `page` | int | 1 | Numéro de page (1-indexed) |
| `limit` | int | 30 | Nombre d'items par page (max 100) |
### Format de réponse
```json
{
"items": [...],
"total": 142,
"page": 1,
"limit": 30,
"pageCount": 5
}
```
## 10. Format des erreurs
Toutes les erreurs MCP suivent un format uniforme via `isError: true` dans la réponse tool :
```json
{
"isError": true,
"content": [{"type": "text", "text": "Permission denied: ROLE_GESTIONNAIRE required for create_machine"}]
}
```
### Catégories d'erreurs
| Code | Description | Exemple |
|---|---|---|
| `auth_error` | Credentials invalides ou manquants | "Authentication failed: invalid password" |
| `permission_denied` | Rôle insuffisant pour l'opération | "Permission denied: ROLE_GESTIONNAIRE required" |
| `not_found` | Entité introuvable | "Machine not found: cl4a8b..." |
| `validation_error` | Données invalides | "Validation failed: name is required" |
| `rate_limited` | Trop de tentatives d'auth échouées | "Rate limited: try again in 45 seconds" |
| `internal_error` | Erreur serveur inattendue | "Internal error: database connection failed" |
Le champ `text` inclut toujours la catégorie en préfixe pour que le LLM puisse adapter son comportement.
## 11. Configuration
### 11.1 Symfony — config/packages/mcp.yaml
```yaml
mcp:
app: 'inventory'
version: '%env(file:resolve:VERSION)%'
description: 'Inventory MCP Server - Gestion inventaire industriel (machines, pièces, composants, produits)'
instructions: |
Serveur MCP pour gérer un inventaire industriel.
Entités principales : Machine, Composant, Pièce, Produit, Site, Constructeur.
Utilisez search_inventory pour chercher dans toutes les entités.
Utilisez get_model_type pour comprendre la structure attendue avant de créer un composant ou une pièce.
Consultez la resource inventory://schema/entities pour voir le schéma complet.
Authentification requise : envoyez X-Profile-Id et X-Profile-Password dans les headers HTTP.
client_transports:
stdio: true
http: true
http:
path: /_mcp
session:
store: file
directory: '%kernel.cache_dir%/mcp-sessions'
ttl: 3600
```
### 11.2 Security — config/packages/security.yaml (ajout firewall)
```yaml
security:
firewalls:
# AVANT le firewall api existant
mcp:
pattern: ^/_mcp
stateless: true
custom_authenticators:
- App\Mcp\Security\McpHeaderAuthenticator
api:
pattern: ^/api
# ... existant ...
```
### 11.3 Rate Limiter — config/packages/rate_limiter.yaml
```yaml
framework:
rate_limiter:
mcp_auth:
policy: sliding_window
limit: 5
interval: '1 minute'
```
### 11.4 Routes — config/routes.yaml (ajout)
```yaml
mcp:
resource: .
type: mcp
```
### 11.5 Logging — config/packages/monolog.yaml (ajout)
```yaml
monolog:
channels: ['mcp']
handlers:
mcp:
type: rotating_file
path: '%kernel.logs_dir%/mcp.log'
level: info
channels: ['mcp']
max_files: 30
```
## 12. Configuration des clients
### 12.1 Claude Code (local, stdio via Docker)
Fichier `.mcp.json` à la racine du projet :
```json
{
"mcpServers": {
"inventory": {
"command": "docker",
"args": [
"exec", "-i",
"-e", "MCP_PROFILE_ID=<votre-profile-id>",
"-e", "MCP_PROFILE_PASSWORD=<votre-password>",
"php-inventory-apache",
"php", "bin/console", "mcp:server"
]
}
}
}
```
> **Note :** Les env vars sont passées via les flags `-e` de `docker exec` car le bloc `env` de `.mcp.json` ne les injecte pas dans le container Docker. Si PHP et les dépendances Composer sont disponibles directement sur l'hôte (hors Docker), on peut utiliser `"command": "php", "args": ["bin/console", "mcp:server"]` avec un bloc `env` standard.
### 12.2 Claude Desktop (distant, HTTP via tunnel)
Fichier `claude_desktop_config.json` :
```json
{
"mcpServers": {
"inventory": {
"url": "https://inventory.company-tunnel.com/_mcp",
"headers": {
"X-Profile-Id": "<votre-profile-id>",
"X-Profile-Password": "<votre-password>"
}
}
}
}
```
### 12.3 ChatGPT Desktop (HTTP via tunnel)
Même principe HTTP : URL du tunnel + headers d'auth. Format de config selon la doc ChatGPT MCP.
### 12.4 Codex (HTTP via tunnel)
Même config HTTP que Claude Desktop.
## 13. Structure des fichiers
```
src/
└── Mcp/
├── Tool/
│ ├── SearchInventoryTool.php # search_inventory
│ ├── DashboardStatsTool.php # get_dashboard_stats
│ ├── ActivityLogTool.php # get_activity_log
│ ├── EntityHistoryTool.php # get_entity_history
│ ├── Machine/
│ │ ├── ListMachinesTool.php # list_machines
│ │ ├── GetMachineTool.php # get_machine
│ │ ├── CreateMachineTool.php # create_machine
│ │ ├── UpdateMachineTool.php # update_machine
│ │ ├── DeleteMachineTool.php # delete_machine
│ │ ├── MachineStructureTool.php # get_machine_structure
│ │ ├── CloneMachineTool.php # clone_machine
│ │ ├── ListMachineLinksTool.php # list_machine_links
│ │ ├── AddMachineLinksTool.php # add_machine_links
│ │ ├── UpdateMachineLinkTool.php # update_machine_link
│ │ └── RemoveMachineLinkTool.php # remove_machine_link
│ ├── Composant/
│ │ ├── ListComposantsTool.php # list_composants
│ │ ├── GetComposantTool.php # get_composant
│ │ ├── CreateComposantTool.php # create_composant
│ │ ├── UpdateComposantTool.php # update_composant
│ │ └── DeleteComposantTool.php # delete_composant
│ ├── Piece/
│ │ ├── ListPiecesTool.php # list_pieces
│ │ ├── GetPieceTool.php # get_piece
│ │ ├── CreatePieceTool.php # create_piece
│ │ ├── UpdatePieceTool.php # update_piece
│ │ └── DeletePieceTool.php # delete_piece
│ ├── Slot/
│ │ ├── ListSlotsTool.php # list_slots (dispatche par entityType)
│ │ └── UpdateSlotsTool.php # update_slots
│ ├── Product/
│ │ ├── ListProductsTool.php # list_products
│ │ ├── GetProductTool.php # get_product
│ │ ├── CreateProductTool.php # create_product
│ │ ├── UpdateProductTool.php # update_product
│ │ └── DeleteProductTool.php # delete_product
│ ├── Site/
│ │ ├── ListSitesTool.php # list_sites
│ │ ├── GetSiteTool.php # get_site
│ │ ├── CreateSiteTool.php # create_site
│ │ ├── UpdateSiteTool.php # update_site
│ │ └── DeleteSiteTool.php # delete_site
│ ├── Constructeur/
│ │ ├── ListConstructeursTool.php # list_constructeurs
│ │ ├── GetConstructeurTool.php # get_constructeur
│ │ ├── CreateConstructeurTool.php # create_constructeur
│ │ ├── UpdateConstructeurTool.php # update_constructeur
│ │ └── DeleteConstructeurTool.php # delete_constructeur
│ ├── ModelType/
│ │ ├── ListModelTypesTool.php # list_model_types
│ │ ├── GetModelTypeTool.php # get_model_type
│ │ ├── CreateModelTypeTool.php # create_model_type
│ │ ├── UpdateModelTypeTool.php # update_model_type
│ │ ├── DeleteModelTypeTool.php # delete_model_type
│ │ └── SyncModelTypeTool.php # sync_model_type
│ ├── CustomField/
│ │ ├── ListCustomFieldValuesTool.php # list_custom_field_values
│ │ ├── UpsertCustomFieldValuesTool.php # upsert_custom_field_values
│ │ └── DeleteCustomFieldValueTool.php # delete_custom_field_value
│ ├── Document/
│ │ ├── ListDocumentsTool.php # list_documents
│ │ └── DeleteDocumentTool.php # delete_document
│ └── Comment/
│ ├── ListCommentsTool.php # list_comments
│ ├── CreateCommentTool.php # create_comment
│ ├── ResolveCommentTool.php # resolve_comment
│ └── UnresolvedCountTool.php # get_unresolved_comments_count
├── Resource/
│ ├── SchemaResource.php # inventory://schema/entities
│ ├── ModelTypesResource.php # inventory://model-types/{category}
│ ├── RolesResource.php # inventory://roles
│ └── StatsResource.php # inventory://stats
└── Security/
└── McpHeaderAuthenticator.php # Symfony Authenticator pour firewall MCP
docs/
└── mcp/
└── README.md # Guide utilisateur complet
```
## 14. Documentation utilisateur (docs/mcp/README.md)
Le guide contiendra :
1. **Introduction** — Qu'est-ce que le MCP Inventory, à quoi ça sert, quels clients sont supportés
2. **Prérequis** — Profil avec rôle suffisant, accès au tunnel, client MCP compatible
3. **Installation & configuration par client** — Exemples copier-coller pour :
- Claude Code (stdio via Docker)
- Claude Desktop (HTTP via tunnel)
- ChatGPT Desktop (HTTP via tunnel)
- Codex (HTTP via tunnel)
4. **Catalogue des tools** — Tableau complet avec nom, description, paramètres, rôle requis
5. **Workflows guidés** — Comment créer une machine, un composant, une pièce, un produit (étape par étape avec exemples d'appels)
6. **Resources disponibles** — URIs et contenu exposé
7. **Rôles & permissions** — Quel rôle permet quelles actions
8. **Format des erreurs** — Catégories et exemples
9. **Limitations connues** — Upload documents non supporté via MCP
10. **Troubleshooting** — Erreurs courantes (auth failed, tunnel down, rôle insuffisant, rate limited)
## 15. Sécurité
| Mesure | Détail |
|---|---|
| **Firewall Symfony** | `/_mcp` a son propre firewall avec `McpHeaderAuthenticator` — intégré au système de sécurité standard |
| **Vérification rôle** | Chaque tool vérifie via `$security->isGranted()` avec hiérarchie des rôles |
| **Audit trail** | `AbstractAuditSubscriber.resolveActorProfileId()` fonctionne nativement car `$security->getUser()` retourne le Profile authentifié |
| **Rate limiting** | 5 tentatives d'auth échouées par minute par IP → rejet |
| **Transport chiffré** | Le tunnel assure le chiffrement en transit pour les clients distants |
| **Pas de secrets dans le code** | Credentials dans env vars (stdio) ou headers (HTTP), jamais en dur |
| **Sessions MCP** | TTL 1h, stockage fichier, nettoyage automatique |
| **CORS** | Non nécessaire — les clients MCP sont des apps natives (pas des navigateurs). Le tunnel termine la connexion côté serveur. À réévaluer si un client browser-based apparaît |
## 16. Backward Compatibility
Les tools MCP suivent une politique additive :
- **Ajouts** : nouveaux tools, nouveaux paramètres optionnels → toujours OK
- **Suppressions** : marquer un tool comme deprecated pendant 1 version avant suppression
- **Breaking changes** : changer le type/nom d'un paramètre requis → bumper la version MCP
Le champ `version` dans la config MCP (lu depuis `VERSION`) signale les changements.
## 17. Dépendances à installer
```bash
composer require symfony/mcp-bundle symfony/rate-limiter
```
Le bundle tire `mcp/sdk` automatiquement.
## 18. Tests
Les tools MCP seront testés via :
- **Tests unitaires** : chaque tool testé avec des mocks de repositories, vérification des paramètres et des réponses
- **Tests d'intégration** : appels MCP stdio via `docker exec ... php bin/console mcp:server` avec des fixtures
- **Tests de sécurité** : vérification que les tools rejettent les appels sans auth, avec rôle insuffisant, et après rate limiting
- Pattern : hériter de `AbstractApiTestCase` pour réutiliser les factories existantes (`createProfile()`, `createMachine()`, etc.)
## 19. Spike / PoC initial
Avant l'implémentation complète, une étape de validation :
1. Installer `symfony/mcp-bundle` dans le projet
2. Créer un tool minimal (`get_dashboard_stats`) avec l'attribut `#[McpTool]`
3. Tester le transport stdio : `docker exec -i php-inventory-apache php bin/console mcp:server`
4. Tester le transport HTTP : appel POST sur `/_mcp`
5. Valider que l'authenticator custom fonctionne avec le firewall
6. Confirmer que `$security->getUser()` retourne le bon Profile dans un tool
Si le PoC révèle des incompatibilités avec l'API du bundle, adapter le design avant de continuer.