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:
14
.mcp.json
Normal file
14
.mcp.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"inventory": {
|
||||||
|
"command": "docker",
|
||||||
|
"args": [
|
||||||
|
"exec", "-i",
|
||||||
|
"-e", "MCP_PROFILE_ID=REPLACE_WITH_YOUR_PROFILE_ID",
|
||||||
|
"-e", "MCP_PROFILE_PASSWORD=REPLACE_WITH_YOUR_PASSWORD",
|
||||||
|
"php-inventory-apache",
|
||||||
|
"php", "bin/console", "mcp:server"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
185
docs/mcp/README.md
Normal file
185
docs/mcp/README.md
Normal 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 |
|
||||||
1472
docs/superpowers/plans/2026-03-16-mcp-server.md
Normal file
1472
docs/superpowers/plans/2026-03-16-mcp-server.md
Normal file
File diff suppressed because it is too large
Load Diff
669
docs/superpowers/specs/2026-03-16-mcp-server-design.md
Normal file
669
docs/superpowers/specs/2026-03-16-mcp-server-design.md
Normal 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.
|
||||||
35
src/Mcp/Resource/RolesResource.php
Normal file
35
src/Mcp/Resource/RolesResource.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Mcp\Resource;
|
||||||
|
|
||||||
|
use Mcp\Capability\Attribute\McpResource;
|
||||||
|
use Mcp\Schema\Content\TextContent;
|
||||||
|
|
||||||
|
#[McpResource(
|
||||||
|
uri: 'inventory://roles',
|
||||||
|
name: 'Roles & Permissions',
|
||||||
|
description: 'Role hierarchy and permissions for MCP tools.',
|
||||||
|
mimeType: 'application/json'
|
||||||
|
)]
|
||||||
|
class RolesResource
|
||||||
|
{
|
||||||
|
public function __invoke(): array
|
||||||
|
{
|
||||||
|
$roles = [
|
||||||
|
'hierarchy' => [
|
||||||
|
'ROLE_ADMIN' => 'Inherits ROLE_GESTIONNAIRE. Can manage profiles.',
|
||||||
|
'ROLE_GESTIONNAIRE' => 'Inherits ROLE_VIEWER. Can create, update, delete all entities.',
|
||||||
|
'ROLE_VIEWER' => 'Inherits ROLE_USER. Can read all entities, create comments, search.',
|
||||||
|
'ROLE_USER' => 'Base role. Authenticated but minimal access.',
|
||||||
|
],
|
||||||
|
'tool_permissions' => [
|
||||||
|
'ROLE_VIEWER' => 'list_*, get_*, search_inventory, get_dashboard_stats, get_entity_history, get_activity_log, list_comments, create_comment, get_unresolved_comments_count, list_custom_field_values, list_documents, list_slots',
|
||||||
|
'ROLE_GESTIONNAIRE' => 'All VIEWER tools + create_*, update_*, delete_*, clone_machine, update_slots, add_machine_links, remove_machine_link, resolve_comment, upsert_custom_field_values, sync_model_type',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
return [new TextContent(text: json_encode($roles, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE))];
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/Mcp/Resource/SchemaResource.php
Normal file
53
src/Mcp/Resource/SchemaResource.php
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Mcp\Resource;
|
||||||
|
|
||||||
|
use Mcp\Capability\Attribute\McpResource;
|
||||||
|
use Mcp\Schema\Content\TextContent;
|
||||||
|
|
||||||
|
#[McpResource(
|
||||||
|
uri: 'inventory://schema/entities',
|
||||||
|
name: 'Entity Schema',
|
||||||
|
description: 'Complete schema of all inventory entities with their fields, types, and relationships.',
|
||||||
|
mimeType: 'application/json'
|
||||||
|
)]
|
||||||
|
class SchemaResource
|
||||||
|
{
|
||||||
|
public function __invoke(): array
|
||||||
|
{
|
||||||
|
$schema = [
|
||||||
|
'Machine' => [
|
||||||
|
'fields' => ['id (string)', 'name (string, unique)', 'reference (string?)', 'prix (string?)', 'createdAt', 'updatedAt'],
|
||||||
|
'relationships' => ['site (Site, required)', 'constructeurs (Constructeur[])', 'componentLinks (MachineComponentLink[])', 'pieceLinks (MachinePieceLink[])', 'productLinks (MachineProductLink[])', 'customFields (CustomField[])', 'customFieldValues (CustomFieldValue[])'],
|
||||||
|
],
|
||||||
|
'Composant' => [
|
||||||
|
'fields' => ['id (string)', 'name (string, unique)', 'reference (string?)', 'description (text?)', 'prix (string?)', 'createdAt', 'updatedAt'],
|
||||||
|
'relationships' => ['typeComposant (ModelType?)', 'constructeurs (Constructeur[])', 'pieceSlots (ComposantPieceSlot[])', 'productSlots (ComposantProductSlot[])', 'subcomponentSlots (ComposantSubcomponentSlot[])', 'customFieldValues (CustomFieldValue[])'],
|
||||||
|
],
|
||||||
|
'Piece' => [
|
||||||
|
'fields' => ['id (string)', 'name (string)', 'reference (string?, unique)', 'description (text?)', 'prix (string?)', 'createdAt', 'updatedAt'],
|
||||||
|
'relationships' => ['typePiece (ModelType?)', 'product (Product?)', 'constructeurs (Constructeur[])', 'productSlots (PieceProductSlot[])', 'customFieldValues (CustomFieldValue[])'],
|
||||||
|
],
|
||||||
|
'Product' => [
|
||||||
|
'fields' => ['id (string)', 'name (string, unique)', 'reference (string?)', 'supplierPrice (string?)', 'createdAt', 'updatedAt'],
|
||||||
|
'relationships' => ['typeProduct (ModelType?)', 'constructeurs (Constructeur[])'],
|
||||||
|
],
|
||||||
|
'Site' => [
|
||||||
|
'fields' => ['id (string)', 'name (string)', 'contactName (string)', 'contactPhone (string)', 'contactAddress (string)', 'contactPostalCode (string)', 'contactCity (string)', 'color (string)', 'createdAt', 'updatedAt'],
|
||||||
|
'relationships' => ['machines (Machine[])'],
|
||||||
|
],
|
||||||
|
'Constructeur' => [
|
||||||
|
'fields' => ['id (string)', 'name (string, unique)', 'email (string?)', 'phone (string?)', 'createdAt', 'updatedAt'],
|
||||||
|
'relationships' => ['machines (Machine[])', 'composants (Composant[])', 'pieces (Piece[])', 'products (Product[])'],
|
||||||
|
],
|
||||||
|
'ModelType' => [
|
||||||
|
'fields' => ['id (string)', 'name (string)', 'category (machine|composant|piece|product)', 'code (string?)', 'createdAt', 'updatedAt'],
|
||||||
|
'relationships' => ['skeletonPieceRequirements[]', 'skeletonProductRequirements[]', 'skeletonSubcomponentRequirements[]'],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
return [new TextContent(text: json_encode($schema, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE))];
|
||||||
|
}
|
||||||
|
}
|
||||||
48
src/Mcp/Resource/StatsResource.php
Normal file
48
src/Mcp/Resource/StatsResource.php
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Mcp\Resource;
|
||||||
|
|
||||||
|
use App\Repository\ComposantRepository;
|
||||||
|
use App\Repository\MachineRepository;
|
||||||
|
use App\Repository\PieceRepository;
|
||||||
|
use App\Repository\ProductRepository;
|
||||||
|
use App\Repository\SiteRepository;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Mcp\Capability\Attribute\McpResource;
|
||||||
|
use Mcp\Schema\Content\TextContent;
|
||||||
|
|
||||||
|
#[McpResource(
|
||||||
|
uri: 'inventory://stats',
|
||||||
|
name: 'Inventory Statistics',
|
||||||
|
description: 'Global counters: machines, pieces, composants, products, sites, unresolved comments.',
|
||||||
|
mimeType: 'application/json'
|
||||||
|
)]
|
||||||
|
class StatsResource
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly MachineRepository $machines,
|
||||||
|
private readonly PieceRepository $pieces,
|
||||||
|
private readonly ComposantRepository $composants,
|
||||||
|
private readonly ProductRepository $products,
|
||||||
|
private readonly SiteRepository $sites,
|
||||||
|
private readonly EntityManagerInterface $em,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function __invoke(): array
|
||||||
|
{
|
||||||
|
$unresolvedComments = (int) $this->em->createQuery(
|
||||||
|
"SELECT COUNT(c.id) FROM App\\Entity\\Comment c WHERE c.status = 'open'"
|
||||||
|
)->getSingleScalarResult();
|
||||||
|
|
||||||
|
return [new TextContent(text: json_encode([
|
||||||
|
'machines' => $this->machines->count([]),
|
||||||
|
'pieces' => $this->pieces->count([]),
|
||||||
|
'composants' => $this->composants->count([]),
|
||||||
|
'products' => $this->products->count([]),
|
||||||
|
'sites' => $this->sites->count([]),
|
||||||
|
'unresolvedComments' => $unresolvedComments,
|
||||||
|
], JSON_THROW_ON_ERROR))];
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user