# 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.
