# MCP Server Implementation Plan > **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add a Model Context Protocol (MCP) server to the Inventory Symfony app, exposing ~55 tools for full CRUD + business operations on all entities, with dual transport (stdio + HTTP) and pass-through authentication. **Architecture:** `symfony/mcp-bundle` v0.6.0 integrated into the existing Symfony 8 app. A dedicated firewall (`^/_mcp`) with `McpHeaderAuthenticator` handles pass-through auth via `X-Profile-Id`/`X-Profile-Password` headers. Tools are PHP classes with `#[McpTool]` attributes that access Doctrine repositories directly. Resources expose schema metadata. **Tech Stack:** PHP 8.4, Symfony 8.0, `symfony/mcp-bundle` ^0.6, `symfony/rate-limiter`, PostgreSQL 16, Docker Compose **Spec:** `docs/superpowers/specs/2026-03-16-mcp-server-design.md` --- ## Pre-requisite: Create feature branch ```bash git checkout -b feat/mcp-server develop ``` --- ## Chunk 1: Foundation — Bundle Install, Auth, PoC Tool ### Task 1: Install dependencies and configure the MCP bundle **Files:** - Modify: `composer.json` - Create: `config/packages/mcp.yaml` - Modify: `config/routes.yaml` - Create: `config/packages/rate_limiter.yaml` - [ ] **Step 1: Install symfony/mcp-bundle and symfony/rate-limiter** ```bash docker exec -u www-data php-inventory-apache composer require symfony/mcp-bundle symfony/rate-limiter ``` - [ ] **Step 2: Create MCP bundle config** Create `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 ``` - [ ] **Step 3: Add MCP route** Add to `config/routes.yaml` (after existing routes): ```yaml mcp: resource: . type: mcp ``` - [ ] **Step 4: Create rate limiter config** Create `config/packages/rate_limiter.yaml`: ```yaml framework: rate_limiter: mcp_auth: policy: sliding_window limit: 5 interval: '1 minute' ``` - [ ] **Step 5: Add MCP logging channel** Create `config/packages/monolog.yaml` (if not exists) or add to it: ```yaml monolog: channels: ['mcp'] handlers: mcp: type: rotating_file path: '%kernel.logs_dir%/mcp.log' level: info channels: ['mcp'] max_files: 30 ``` - [ ] **Step 6: Verify Symfony cache clear succeeds** ```bash docker exec -u www-data php-inventory-apache php bin/console cache:clear ``` Expected: no errors, MCP bundle loaded. - [ ] **Step 7: Verify MCP routes are registered** ```bash docker exec -u www-data php-inventory-apache php bin/console debug:router | grep mcp ``` Expected: `/_mcp` route listed. - [ ] **Step 8: Commit** ```bash git add composer.json composer.lock symfony.lock config/packages/mcp.yaml config/packages/rate_limiter.yaml config/packages/monolog.yaml config/routes.yaml git commit -m "feat(mcp) : install symfony/mcp-bundle and configure transports" ``` --- ### Task 2: Create McpHeaderAuthenticator **Files:** - Create: `src/Mcp/Security/McpHeaderAuthenticator.php` - Modify: `config/packages/security.yaml` - [ ] **Step 1: Write the authenticator test** Create `tests/Mcp/Security/McpHeaderAuthenticatorTest.php`: ```php createUnauthenticatedClient(); $client->request('POST', '/_mcp', [ 'headers' => ['Content-Type' => 'application/json'], 'body' => json_encode([ 'jsonrpc' => '2.0', 'method' => 'initialize', 'params' => [ 'protocolVersion' => '2025-03-26', 'capabilities' => new \stdClass(), 'clientInfo' => ['name' => 'test', 'version' => '1.0'], ], 'id' => 1, ]), ]); // Without credentials, should get 401 $this->assertResponseStatusCodeSame(401); } public function testMcpEndpointRejectsInvalidPassword(): void { $profile = $this->createProfile( firstName: 'McpTest', role: 'ROLE_VIEWER', password: 'correct-password', ); $client = $this->createUnauthenticatedClient(); $client->request('POST', '/_mcp', [ 'headers' => [ 'Content-Type' => 'application/json', 'X-Profile-Id' => $profile->getId(), 'X-Profile-Password' => 'wrong-password', ], 'body' => json_encode([ 'jsonrpc' => '2.0', 'method' => 'initialize', 'params' => [ 'protocolVersion' => '2025-03-26', 'capabilities' => new \stdClass(), 'clientInfo' => ['name' => 'test', 'version' => '1.0'], ], 'id' => 1, ]), ]); $this->assertResponseStatusCodeSame(401); } public function testMcpEndpointAcceptsValidCredentials(): void { $profile = $this->createProfile( firstName: 'McpTest', role: 'ROLE_VIEWER', password: 'valid-password', ); $client = $this->createUnauthenticatedClient(); $client->request('POST', '/_mcp', [ 'headers' => [ 'Content-Type' => 'application/json', 'X-Profile-Id' => $profile->getId(), 'X-Profile-Password' => 'valid-password', ], 'body' => json_encode([ 'jsonrpc' => '2.0', 'method' => 'initialize', 'params' => [ 'protocolVersion' => '2025-03-26', 'capabilities' => new \stdClass(), 'clientInfo' => ['name' => 'test', 'version' => '1.0'], ], 'id' => 1, ]), ]); // Should NOT be 401 — may be 200 or other MCP response $this->assertResponseStatusCodeSame(200); } } ``` - [ ] **Step 2: Run test to verify it fails** ```bash make test FILES=tests/Mcp/Security/McpHeaderAuthenticatorTest.php ``` Expected: FAIL — class `McpHeaderAuthenticator` does not exist. - [ ] **Step 3: Create the authenticator** Create `src/Mcp/Security/McpHeaderAuthenticator.php`: ```php headers->has('X-Profile-Id') || !$request->headers->has('X-Profile-Password')) { return false; } return true; } public function authenticate(Request $request): Passport { $profileId = $request->headers->get('X-Profile-Id', ''); $password = $request->headers->get('X-Profile-Password', ''); // Rate limiting per IP — consume 1 token per attempt $limiter = $this->mcpAuthLimiter->create($request->getClientIp() ?? 'unknown'); $limit = $limiter->consume(1); if (!$limit->isAccepted()) { $this->mcpLogger->warning('MCP auth rate limited', ['ip' => $request->getClientIp()]); throw new CustomUserMessageAuthenticationException('Rate limited: too many authentication attempts.'); } return new SelfValidatingPassport( new UserBadge($profileId, function (string $id) use ($password, $limiter, $request): Profile { $profile = $this->profiles->find($id); if (!$profile || !$profile->isActive()) { $this->mcpLogger->warning('MCP auth failed: profile not found', ['profileId' => $id]); throw new CustomUserMessageAuthenticationException('Authentication failed: invalid credentials.'); } if (!$this->passwordHasher->isPasswordValid($profile, $password)) { $this->mcpLogger->warning('MCP auth failed: invalid password', ['profileId' => $id]); throw new CustomUserMessageAuthenticationException('Authentication failed: invalid credentials.'); } // Reset limiter on successful auth $limiter->reset(); $this->mcpLogger->info('MCP auth success', [ 'profileId' => $id, 'roles' => $profile->getRoles(), 'ip' => $request->getClientIp(), ]); return $profile; }) ); } public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response { return null; } public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response { $statusCode = str_contains($exception->getMessageKey(), 'Rate limited') ? Response::HTTP_TOO_MANY_REQUESTS : Response::HTTP_UNAUTHORIZED; return new JsonResponse( ['message' => $exception->getMessageKey()], $statusCode, ); } } ``` - [ ] **Step 4: Add MCP firewall to security.yaml** Add the `mcp` firewall BEFORE the `api` firewall in `config/packages/security.yaml`: ```yaml firewalls: dev: pattern: ^/(_profiler|_wdt|assets|build)/ security: false session_public: pattern: ^/api/session/profiles?$ security: false mcp: pattern: ^/_mcp stateless: true custom_authenticators: - App\Mcp\Security\McpHeaderAuthenticator api: pattern: ^/api stateless: false custom_authenticators: - App\Security\SessionProfileAuthenticator ``` Also add access control for `/_mcp` before the `/api` rules: ```yaml access_control: - { path: ^/api/session/profile$, roles: PUBLIC_ACCESS } - { path: ^/api/session/profiles, roles: PUBLIC_ACCESS, methods: [GET] } - { path: ^/api/admin, roles: ROLE_ADMIN } - { path: ^/api/docs, roles: PUBLIC_ACCESS } - { path: ^/api/health$, roles: PUBLIC_ACCESS } - { path: ^/_mcp, roles: ROLE_USER } - { path: ^/docs, roles: PUBLIC_ACCESS } - { path: ^/contexts, roles: PUBLIC_ACCESS } - { path: ^/\.well-known, roles: PUBLIC_ACCESS } - { path: ^/api, roles: ROLE_VIEWER } ``` - [ ] **Step 5: Run test to verify it passes** ```bash make test FILES=tests/Mcp/Security/McpHeaderAuthenticatorTest.php ``` Expected: 3 tests pass. - [ ] **Step 6: Commit** ```bash git add src/Mcp/Security/McpHeaderAuthenticator.php tests/Mcp/Security/McpHeaderAuthenticatorTest.php config/packages/security.yaml git commit -m "feat(mcp) : add McpHeaderAuthenticator with rate limiting" ``` --- ### Task 3: Stdio auth — EventSubscriber for console transport The `McpHeaderAuthenticator` only works for HTTP requests. For stdio transport (`bin/console mcp:server`), we need to read credentials from env vars and inject a security token manually. **Files:** - Create: `src/Mcp/Security/McpStdioAuthSubscriber.php` - [ ] **Step 1: Create the subscriber** Create `src/Mcp/Security/McpStdioAuthSubscriber.php`: ```php getCommand(); if (!$command || !str_starts_with($command->getName() ?? '', 'mcp:')) { return; } $profileId = $_ENV['MCP_PROFILE_ID'] ?? ''; $password = $_ENV['MCP_PROFILE_PASSWORD'] ?? ''; if ($profileId === '' || $password === '') { $this->mcpLogger->error('MCP stdio: missing MCP_PROFILE_ID or MCP_PROFILE_PASSWORD env vars'); $event->disableCommand(); $event->getOutput()->writeln('MCP auth: MCP_PROFILE_ID and MCP_PROFILE_PASSWORD env vars required'); return; } $profile = $this->profiles->find($profileId); if (!$profile || !$profile->isActive()) { $this->mcpLogger->error('MCP stdio: profile not found or inactive', ['profileId' => $profileId]); $event->disableCommand(); $event->getOutput()->writeln('MCP auth: invalid profile'); return; } if (!$this->passwordHasher->isPasswordValid($profile, $password)) { $this->mcpLogger->error('MCP stdio: invalid password', ['profileId' => $profileId]); $event->disableCommand(); $event->getOutput()->writeln('MCP auth: invalid password'); return; } // Inject a synthetic security token so $security->getUser() works in tools $token = new UsernamePasswordToken($profile, 'mcp', $profile->getRoles()); $this->tokenStorage->setToken($token); $this->mcpLogger->info('MCP stdio auth success', [ 'profileId' => $profileId, 'roles' => $profile->getRoles(), ]); } } ``` - [ ] **Step 2: Commit** ```bash git add src/Mcp/Security/McpStdioAuthSubscriber.php git commit -m "feat(mcp) : add stdio auth subscriber for console transport" ``` --- ### Task 4: PoC Tool — get_dashboard_stats **Files:** - Create: `src/Mcp/Tool/DashboardStatsTool.php` - Create: `tests/Mcp/Tool/DashboardStatsToolTest.php` - [ ] **Step 1: Write the test** Create `tests/Mcp/Tool/DashboardStatsToolTest.php`: ```php createSite(); $this->createMachine(site: $site); $this->createMachine(site: $site); $constructeur = $this->createConstructeur(); $modelType = $this->createModelType(category: 'piece'); $this->createPiece(modelType: $modelType, constructeur: $constructeur); $profile = $this->createProfile(role: 'ROLE_VIEWER', password: 'test123'); $client = $this->createUnauthenticatedClient(); $response = $client->request('POST', '/_mcp', [ 'headers' => [ 'Content-Type' => 'application/json', 'X-Profile-Id' => $profile->getId(), 'X-Profile-Password' => 'test123', ], 'body' => json_encode([ 'jsonrpc' => '2.0', 'method' => 'tools/call', 'params' => [ 'name' => 'get_dashboard_stats', 'arguments' => new \stdClass(), ], 'id' => 2, ]), ]); $this->assertResponseIsSuccessful(); $data = $response->toArray(); $this->assertArrayHasKey('result', $data); } } ``` - [ ] **Step 2: Run test to verify it fails** ```bash make test FILES=tests/Mcp/Tool/DashboardStatsToolTest.php ``` Expected: FAIL — tool `get_dashboard_stats` not found. - [ ] **Step 3: Create the tool** Create `src/Mcp/Tool/DashboardStatsTool.php`: ```php 'object', 'properties' => new \stdClass(), ] )] 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)), ]; } } ``` - [ ] **Step 4: Run test to verify it passes** ```bash make test FILES=tests/Mcp/Tool/DashboardStatsToolTest.php ``` Expected: PASS. - [ ] **Step 5: Verify stdio transport works** ```bash echo '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":1}' | docker exec -i -e MCP_PROFILE_ID= -e MCP_PROFILE_PASSWORD= php-inventory-apache php bin/console mcp:server ``` Expected: JSON-RPC response with server capabilities. > **Note:** This step validates the PoC. If stdio does not work, investigate the MCP bundle's console command API and adapt. Update this plan if the command name or args differ. - [ ] **Step 6: Commit** ```bash git add src/Mcp/Tool/DashboardStatsTool.php tests/Mcp/Tool/DashboardStatsToolTest.php git commit -m "feat(mcp) : add get_dashboard_stats PoC tool" ``` --- ### Task 5: Base helper trait for MCP tools Tools share common patterns: role checking, error formatting, pagination. Extract a reusable trait. **Files:** - Create: `src/Mcp/Tool/McpToolHelper.php` - [ ] **Step 1: Create the helper trait** Create `src/Mcp/Tool/McpToolHelper.php`: ```php isGranted($role)) { throw new \RuntimeException("Permission denied: {$role} required."); } } /** * @return array{TextContent} */ private function jsonResponse(array $data): array { return [new TextContent(text: json_encode($data, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE))]; } /** * Throws a RuntimeException that the MCP bundle catches and converts to * an isError: true response. The message is prefixed with the category * so LLMs can parse error types. * * Verify during PoC (Task 4) that the bundle converts RuntimeException * to proper MCP error responses. If not, adapt to the bundle's error API. */ private function mcpError(string $category, string $message): never { throw new \RuntimeException("{$category}: {$message}"); } private function paginationParams(int $page = 1, int $limit = 30): array { $page = max(1, $page); $limit = min(100, max(1, $limit)); $offset = ($page - 1) * $limit; return ['page' => $page, 'limit' => $limit, 'offset' => $offset]; } private function paginatedResponse(array $items, int $total, int $page, int $limit): array { return $this->jsonResponse([ 'items' => $items, 'total' => $total, 'page' => $page, 'limit' => $limit, 'pageCount' => (int) ceil($total / $limit), ]); } } ``` - [ ] **Step 2: Commit** ```bash git add src/Mcp/Tool/McpToolHelper.php git commit -m "feat(mcp) : add McpToolHelper trait for shared tool utilities" ``` --- ## Chunk 2: CRUD Tools — Sites, Constructeurs, Products ### Task 6: Sites CRUD tools **Files:** - Create: `src/Mcp/Tool/Site/ListSitesTool.php` - Create: `src/Mcp/Tool/Site/GetSiteTool.php` - Create: `src/Mcp/Tool/Site/CreateSiteTool.php` - Create: `src/Mcp/Tool/Site/UpdateSiteTool.php` - Create: `src/Mcp/Tool/Site/DeleteSiteTool.php` - Create: `tests/Mcp/Tool/Site/SitesCrudToolTest.php` - [ ] **Step 1: Write tests for all 5 site CRUD tools** Create `tests/Mcp/Tool/Site/SitesCrudToolTest.php` — test each tool: list returns paginated sites, get returns a single site, create makes a new site (GESTIONNAIRE), update modifies fields, delete removes. Test that VIEWER can list/get but not create/update/delete. Pattern for each test: 1. Create fixtures with `$this->createSite()` / `$this->createProfile()` 2. Call `POST /_mcp` with `tools/call` JSON-RPC, `name: "list_sites"`, `arguments: {page: 1, limit: 10}` 3. Assert response contains expected data - [ ] **Step 2: Run tests — expect failure** ```bash make test FILES=tests/Mcp/Tool/Site/SitesCrudToolTest.php ``` - [ ] **Step 3: Implement ListSitesTool** Create `src/Mcp/Tool/Site/ListSitesTool.php`: ```php 'object', 'properties' => [ 'page' => ['type' => 'integer', 'description' => 'Page number (1-indexed)', 'default' => 1], 'limit' => ['type' => 'integer', 'description' => 'Items per page (max 100)', 'default' => 30], 'search' => ['type' => 'string', 'description' => 'Search by site name'], ], ] )] public function __invoke(int $page = 1, int $limit = 30, ?string $search = null): array { $p = $this->paginationParams($page, $limit); $qb = $this->sites->createQueryBuilder('s')->orderBy('s.name', 'ASC'); if ($search) { $qb->andWhere('LOWER(s.name) LIKE LOWER(:search)') ->setParameter('search', "%{$search}%"); } $total = (int) (clone $qb)->select('COUNT(s.id)')->getQuery()->getSingleScalarResult(); $items = $qb->setFirstResult($p['offset']) ->setMaxResults($p['limit']) ->getQuery() ->getArrayResult(); return $this->paginatedResponse($items, $total, $p['page'], $p['limit']); } } ``` - [ ] **Step 4: Implement GetSiteTool, CreateSiteTool, UpdateSiteTool, DeleteSiteTool** Follow the same pattern as ListSitesTool. Each tool: - Has `#[McpTool]` attribute with name, description, inputSchema - Uses `McpToolHelper` trait - Get: takes `siteId: string`, returns site data or error if not found - Create: takes `name, address?, phone?, email?, contactName?`, calls `$this->requireRole($security, 'ROLE_GESTIONNAIRE')`, persists via EntityManager - Update: takes `siteId` + optional fields, merges changes - Delete: takes `siteId`, removes entity - [ ] **Step 5: Run tests — expect pass** ```bash make test FILES=tests/Mcp/Tool/Site/SitesCrudToolTest.php ``` - [ ] **Step 6: Run php-cs-fixer** ```bash make php-cs-fixer-allow-risky ``` - [ ] **Step 7: Commit** ```bash git add src/Mcp/Tool/Site/ tests/Mcp/Tool/Site/ git commit -m "feat(mcp) : add Sites CRUD tools (list, get, create, update, delete)" ``` --- ### Task 7: Constructeurs CRUD tools **Files:** - Create: `src/Mcp/Tool/Constructeur/ListConstructeursTool.php` - Create: `src/Mcp/Tool/Constructeur/GetConstructeurTool.php` - Create: `src/Mcp/Tool/Constructeur/CreateConstructeurTool.php` - Create: `src/Mcp/Tool/Constructeur/UpdateConstructeurTool.php` - Create: `src/Mcp/Tool/Constructeur/DeleteConstructeurTool.php` - Create: `tests/Mcp/Tool/Constructeur/ConstructeursCrudToolTest.php` - [ ] **Step 1: Write tests** - [ ] **Step 2: Run tests — expect failure** - [ ] **Step 3: Implement all 5 tools** (same pattern as Sites) - [ ] **Step 4: Run tests — expect pass** - [ ] **Step 5: Run php-cs-fixer** - [ ] **Step 6: Commit** ```bash git add src/Mcp/Tool/Constructeur/ tests/Mcp/Tool/Constructeur/ git commit -m "feat(mcp) : add Constructeurs CRUD tools" ``` --- ### Task 8: Products CRUD tools **Files:** - Create: `src/Mcp/Tool/Product/ListProductsTool.php` - Create: `src/Mcp/Tool/Product/GetProductTool.php` - Create: `src/Mcp/Tool/Product/CreateProductTool.php` - Create: `src/Mcp/Tool/Product/UpdateProductTool.php` - Create: `src/Mcp/Tool/Product/DeleteProductTool.php` - Create: `tests/Mcp/Tool/Product/ProductsCrudToolTest.php` - [ ] **Step 1: Write tests** Important: `prix` (price) must be sent as **string**, not number (see project memory `feedback_prix_type.md`). - [ ] **Step 2: Run tests — expect failure** - [ ] **Step 3: Implement all 5 tools** `CreateProductTool` takes: `name: string`, `reference?: string`, `modelTypeId?: string`, `prix?: string` (STRING not number), `constructeurIds?: string[]`. - [ ] **Step 4: Run tests — expect pass** - [ ] **Step 5: Run php-cs-fixer** - [ ] **Step 6: Commit** ```bash git add src/Mcp/Tool/Product/ tests/Mcp/Tool/Product/ git commit -m "feat(mcp) : add Products CRUD tools" ``` --- ## Chunk 3: CRUD Tools — Pieces, Composants, Machines ### Task 9: Pieces CRUD tools **Files:** - Create: `src/Mcp/Tool/Piece/ListPiecesTool.php` - Create: `src/Mcp/Tool/Piece/GetPieceTool.php` - Create: `src/Mcp/Tool/Piece/CreatePieceTool.php` - Create: `src/Mcp/Tool/Piece/UpdatePieceTool.php` - Create: `src/Mcp/Tool/Piece/DeletePieceTool.php` - Create: `tests/Mcp/Tool/Piece/PiecesCrudToolTest.php` - [ ] **Step 1: Write tests** `CreatePieceTool` test must verify that after creation, the response includes auto-generated product slots from the ModelType's skeleton requirements. - [ ] **Step 2: Run tests — expect failure** - [ ] **Step 3: Implement all 5 tools** `CreatePieceTool`: after persisting the Piece, read back its `PieceProductSlot` entities (auto-created by Doctrine lifecycle) and include them in the response. `GetPieceTool`: include product slots in the response. - [ ] **Step 4: Run tests — expect pass** - [ ] **Step 5: Run php-cs-fixer** - [ ] **Step 6: Commit** ```bash git add src/Mcp/Tool/Piece/ tests/Mcp/Tool/Piece/ git commit -m "feat(mcp) : add Pieces CRUD tools with slot auto-generation" ``` --- ### Task 10: Composants CRUD tools **Files:** - Create: `src/Mcp/Tool/Composant/ListComposantsTool.php` - Create: `src/Mcp/Tool/Composant/GetComposantTool.php` - Create: `src/Mcp/Tool/Composant/CreateComposantTool.php` - Create: `src/Mcp/Tool/Composant/UpdateComposantTool.php` - Create: `src/Mcp/Tool/Composant/DeleteComposantTool.php` - Create: `tests/Mcp/Tool/Composant/ComposantsCrudToolTest.php` - [ ] **Step 1: Write tests** `CreateComposantTool` test must verify that the response includes auto-generated slots: `ComposantPieceSlot`, `ComposantProductSlot`, `ComposantSubcomponentSlot`. - [ ] **Step 2: Run tests — expect failure** - [ ] **Step 3: Implement all 5 tools** `CreateComposantTool`: after persisting, read back all 3 types of slots and include in response. `GetComposantTool`: include all slots with their state (selectedPieceId/selectedProductId/selectedComposantId + requirement name). - [ ] **Step 4: Run tests — expect pass** - [ ] **Step 5: Run php-cs-fixer** - [ ] **Step 6: Commit** ```bash git add src/Mcp/Tool/Composant/ tests/Mcp/Tool/Composant/ git commit -m "feat(mcp) : add Composants CRUD tools with slot auto-generation" ``` --- ### Task 11: Machines CRUD tools **Files:** - Create: `src/Mcp/Tool/Machine/ListMachinesTool.php` - Create: `src/Mcp/Tool/Machine/GetMachineTool.php` - Create: `src/Mcp/Tool/Machine/CreateMachineTool.php` - Create: `src/Mcp/Tool/Machine/UpdateMachineTool.php` - Create: `src/Mcp/Tool/Machine/DeleteMachineTool.php` - Create: `tests/Mcp/Tool/Machine/MachinesCrudToolTest.php` - [ ] **Step 1: Write tests** - [ ] **Step 2: Run tests — expect failure** - [ ] **Step 3: Implement all 5 tools** `CreateMachineTool`: takes `name, reference?, siteId, constructeurIds?`. Returns machine data. - [ ] **Step 4: Run tests — expect pass** - [ ] **Step 5: Run php-cs-fixer** - [ ] **Step 6: Commit** ```bash git add src/Mcp/Tool/Machine/ tests/Mcp/Tool/Machine/ git commit -m "feat(mcp) : add Machines CRUD tools" ``` --- ## Chunk 4: Specialized Tools — Slots, Links, Structure, Clone ### Task 12: Slot tools (list_slots, update_slots) **Files:** - Create: `src/Mcp/Tool/Slot/ListSlotsTool.php` - Create: `src/Mcp/Tool/Slot/UpdateSlotsTool.php` - Create: `tests/Mcp/Tool/Slot/SlotsToolTest.php` - [ ] **Step 1: Write tests** Test `list_slots` for both composant and piece entity types. Test `update_slots` by selecting a piece into a composant's piece slot. - [ ] **Step 2: Run tests — expect failure** - [ ] **Step 3: Implement ListSlotsTool** Accepts `entityType: "composant"|"piece"`, `entityId: string`. Queries the appropriate slot repositories (`ComposantPieceSlotRepository`, `ComposantProductSlotRepository`, `ComposantSubcomponentSlotRepository` for composant; `PieceProductSlotRepository` for piece). Returns slots with requirement name, selected entity, position. - [ ] **Step 4: Implement UpdateSlotsTool** Accepts `slots: [{slotId, slotType: "piece"|"product"|"subcomponent", selectedPieceId?|selectedProductId?|selectedComposantId?}]`. Uses the existing slot controllers' logic: find slot, set selected entity, flush. - [ ] **Step 5: Run tests — expect pass** - [ ] **Step 6: Run php-cs-fixer and commit** ```bash git add src/Mcp/Tool/Slot/ tests/Mcp/Tool/Slot/ git commit -m "feat(mcp) : add list_slots and update_slots tools" ``` --- ### Task 13: Machine links tools **Files:** - Create: `src/Mcp/Tool/Machine/ListMachineLinksTool.php` - Create: `src/Mcp/Tool/Machine/AddMachineLinksTool.php` - Create: `src/Mcp/Tool/Machine/UpdateMachineLinkTool.php` - Create: `src/Mcp/Tool/Machine/RemoveMachineLinkTool.php` - Create: `tests/Mcp/Tool/Machine/MachineLinksToolTest.php` - [ ] **Step 1: Write tests** - [ ] **Step 2: Run tests — expect failure** - [ ] **Step 3: Implement all 4 tools** `AddMachineLinksTool`: accepts `machineId`, `links: [{type: "composant"|"piece"|"product", entityId, quantity?, parentLinkId?}]`. Creates `MachineComponentLink`, `MachinePieceLink`, or `MachineProductLink` accordingly. `ListMachineLinksTool`: returns all links for a machine, grouped by type. - [ ] **Step 4: Run tests — expect pass** - [ ] **Step 5: Run php-cs-fixer and commit** ```bash git add src/Mcp/Tool/Machine/ListMachineLinksTool.php src/Mcp/Tool/Machine/AddMachineLinksTool.php src/Mcp/Tool/Machine/UpdateMachineLinkTool.php src/Mcp/Tool/Machine/RemoveMachineLinkTool.php tests/Mcp/Tool/Machine/MachineLinksToolTest.php git commit -m "feat(mcp) : add Machine links tools (list, add, update, remove)" ``` --- ### Task 14: Machine structure and clone tools **Files:** - Create: `src/Mcp/Tool/Machine/MachineStructureTool.php` - Create: `src/Mcp/Tool/Machine/CloneMachineTool.php` - Create: `tests/Mcp/Tool/Machine/MachineStructureToolTest.php` - [ ] **Step 1: Write tests** Test `get_machine_structure` returns the full hierarchy. Test `clone_machine` creates a copy with all links. - [ ] **Step 2: Run tests — expect failure** - [ ] **Step 3: Implement MachineStructureTool** Reuse the logic from `MachineStructureController::getStructure()` — query machine with all links, composants, pieces, products, custom fields, and return as nested JSON. - [ ] **Step 4: Implement CloneMachineTool** Reuse the logic from `MachineStructureController::cloneMachine()`. Accepts `machineId, name, siteId, reference?`. Returns the new machine's structure. - [ ] **Step 5: Run tests — expect pass** - [ ] **Step 6: Run php-cs-fixer and commit** ```bash git add src/Mcp/Tool/Machine/MachineStructureTool.php src/Mcp/Tool/Machine/CloneMachineTool.php tests/Mcp/Tool/Machine/MachineStructureToolTest.php git commit -m "feat(mcp) : add get_machine_structure and clone_machine tools" ``` --- ## Chunk 5: Business Tools — Search, History, Comments, Custom Fields, ModelTypes ### Task 15: search_inventory tool **Files:** - Create: `src/Mcp/Tool/SearchInventoryTool.php` - Create: `tests/Mcp/Tool/SearchInventoryToolTest.php` - [ ] **Step 1: Write tests** Test searching across multiple entity types (machines, pieces, composants, products, sites, constructeurs) by name/reference. - [ ] **Step 2: Run tests — expect failure** - [ ] **Step 3: Implement SearchInventoryTool** Accepts `query: string`, `types?: string[]` (defaults to all), `limit?: int` (default 20). Runs LIKE queries across entity tables, merges and sorts results by relevance. Returns `[{type, id, name, reference, ...}]`. - [ ] **Step 4: Run tests — expect pass** - [ ] **Step 5: Run php-cs-fixer and commit** ```bash git add src/Mcp/Tool/SearchInventoryTool.php tests/Mcp/Tool/SearchInventoryToolTest.php git commit -m "feat(mcp) : add search_inventory global search tool" ``` --- ### Task 16: History and activity log tools **Files:** - Create: `src/Mcp/Tool/EntityHistoryTool.php` - Create: `src/Mcp/Tool/ActivityLogTool.php` - Create: `tests/Mcp/Tool/HistoryToolsTest.php` - [ ] **Step 1: Write tests** - [ ] **Step 2: Run tests — expect failure** - [ ] **Step 3: Implement EntityHistoryTool** Reuse logic from `EntityHistoryController`. Accepts `entityType: string`, `entityId: string`. Queries `AuditLog` by entity type/id. - [ ] **Step 4: Implement ActivityLogTool** Reuse logic from `ActivityLogController`. Accepts `page?, limit?, entityType?, action?`. - [ ] **Step 5: Run tests — expect pass** - [ ] **Step 6: Run php-cs-fixer and commit** ```bash git add src/Mcp/Tool/EntityHistoryTool.php src/Mcp/Tool/ActivityLogTool.php tests/Mcp/Tool/HistoryToolsTest.php git commit -m "feat(mcp) : add entity history and activity log tools" ``` --- ### Task 17: Comment tools **Files:** - Create: `src/Mcp/Tool/Comment/ListCommentsTool.php` - Create: `src/Mcp/Tool/Comment/CreateCommentTool.php` - Create: `src/Mcp/Tool/Comment/ResolveCommentTool.php` - Create: `src/Mcp/Tool/Comment/UnresolvedCountTool.php` - Create: `tests/Mcp/Tool/Comment/CommentsToolTest.php` - [ ] **Step 1: Write tests** - [ ] **Step 2: Run tests — expect failure** - [ ] **Step 3: Implement all 4 comment tools** Reuse logic from `CommentController`. `CreateCommentTool` requires ROLE_VIEWER. `ResolveCommentTool` requires ROLE_GESTIONNAIRE. - [ ] **Step 4: Run tests — expect pass** - [ ] **Step 5: Run php-cs-fixer and commit** ```bash git add src/Mcp/Tool/Comment/ tests/Mcp/Tool/Comment/ git commit -m "feat(mcp) : add comment tools (list, create, resolve, unresolved count)" ``` --- ### Task 18: Custom field tools **Files:** - Create: `src/Mcp/Tool/CustomField/ListCustomFieldValuesTool.php` - Create: `src/Mcp/Tool/CustomField/UpsertCustomFieldValuesTool.php` - Create: `src/Mcp/Tool/CustomField/DeleteCustomFieldValueTool.php` - Create: `tests/Mcp/Tool/CustomField/CustomFieldToolsTest.php` - [ ] **Step 1: Write tests** - [ ] **Step 2: Run tests — expect failure** - [ ] **Step 3: Implement all 3 tools** Reuse logic from `CustomFieldValueController`. `UpsertCustomFieldValuesTool` accepts `entityType, entityId, fields: [{customFieldId OR (customFieldName + customFieldType), value}]`. - [ ] **Step 4: Run tests — expect pass** - [ ] **Step 5: Run php-cs-fixer and commit** ```bash git add src/Mcp/Tool/CustomField/ tests/Mcp/Tool/CustomField/ git commit -m "feat(mcp) : add custom field value tools (list, upsert, delete)" ``` --- ### Task 19: Document tools **Files:** - Create: `src/Mcp/Tool/Document/ListDocumentsTool.php` - Create: `src/Mcp/Tool/Document/DeleteDocumentTool.php` - Create: `tests/Mcp/Tool/Document/DocumentToolsTest.php` - [ ] **Step 1: Write tests** - [ ] **Step 2: Run tests — expect failure** - [ ] **Step 3: Implement both tools** Reuse logic from `DocumentQueryController` for list and `DocumentRepository` for delete. No upload tool (documented limitation). - [ ] **Step 4: Run tests — expect pass** - [ ] **Step 5: Run php-cs-fixer and commit** ```bash git add src/Mcp/Tool/Document/ tests/Mcp/Tool/Document/ git commit -m "feat(mcp) : add document tools (list, delete)" ``` --- ### Task 20: ModelType tools **Files:** - Create: `src/Mcp/Tool/ModelType/ListModelTypesTool.php` - Create: `src/Mcp/Tool/ModelType/GetModelTypeTool.php` - Create: `src/Mcp/Tool/ModelType/CreateModelTypeTool.php` - Create: `src/Mcp/Tool/ModelType/UpdateModelTypeTool.php` - Create: `src/Mcp/Tool/ModelType/DeleteModelTypeTool.php` - Create: `src/Mcp/Tool/ModelType/SyncModelTypeTool.php` - Create: `tests/Mcp/Tool/ModelType/ModelTypeToolsTest.php` - [ ] **Step 1: Write tests** `GetModelTypeTool` test must verify skeleton requirements and custom fields are included. `SyncModelTypeTool` test must verify preview and sync actions. - [ ] **Step 2: Run tests — expect failure** - [ ] **Step 3: Implement CRUD tools** `GetModelTypeTool` returns full detail including `SkeletonPieceRequirement`, `SkeletonProductRequirement`, `SkeletonSubcomponentRequirement` with their custom fields. - [ ] **Step 4: Implement SyncModelTypeTool** Reuse logic from `ModelTypeSyncController`. Accepts `modelTypeId, action: "preview"|"sync", structure?, confirmDeletions?, confirmTypeChanges?`. - [ ] **Step 5: Run tests — expect pass** - [ ] **Step 6: Run php-cs-fixer and commit** ```bash git add src/Mcp/Tool/ModelType/ tests/Mcp/Tool/ModelType/ git commit -m "feat(mcp) : add ModelType tools (CRUD + sync)" ``` --- ## Chunk 6: Resources, Documentation, Final Validation ### Task 21: MCP Resources **Files:** - Create: `src/Mcp/Resource/SchemaResource.php` - Create: `src/Mcp/Resource/ModelTypesResource.php` - Create: `src/Mcp/Resource/RolesResource.php` - Create: `src/Mcp/Resource/StatsResource.php` - [ ] **Step 1: Implement SchemaResource** ```php [ 'fields' => ['id (string, CUID)', 'name (string)', 'reference (string, nullable)', 'siteId (string)', 'createdAt', 'updatedAt'], 'relationships' => ['site (Site)', 'constructeurs (Constructeur[])', 'componentLinks (MachineComponentLink[])', 'pieceLinks (MachinePieceLink[])', 'productLinks (MachineProductLink[])', 'customFieldValues (CustomFieldValue[])', 'documents (Document[])'], ], 'Composant' => [ 'fields' => ['id (string, CUID)', 'name (string)', 'reference (string, nullable)', 'modelTypeId (string, nullable)', 'createdAt', 'updatedAt'], 'relationships' => ['modelType (ModelType)', 'constructeurs (Constructeur[])', 'pieceSlots (ComposantPieceSlot[])', 'productSlots (ComposantProductSlot[])', 'subcomponentSlots (ComposantSubcomponentSlot[])', 'customFieldValues (CustomFieldValue[])', 'documents (Document[])'], ], 'Piece' => [ 'fields' => ['id (string, CUID)', 'name (string)', 'reference (string, nullable)', 'modelTypeId (string, nullable)', 'createdAt', 'updatedAt'], 'relationships' => ['modelType (ModelType)', 'constructeurs (Constructeur[])', 'productSlots (PieceProductSlot[])', 'customFieldValues (CustomFieldValue[])', 'documents (Document[])'], ], 'Product' => [ 'fields' => ['id (string, CUID)', 'name (string)', 'reference (string, nullable)', 'prix (string, nullable)', 'modelTypeId (string, nullable)', 'createdAt', 'updatedAt'], 'relationships' => ['modelType (ModelType)', 'constructeurs (Constructeur[])', 'customFieldValues (CustomFieldValue[])', 'documents (Document[])'], ], 'Site' => [ 'fields' => ['id (string, CUID)', 'name (string)', 'address (string, nullable)', 'phone (string, nullable)', 'email (string, nullable)', 'contactName (string, nullable)', 'createdAt', 'updatedAt'], 'relationships' => ['machines (Machine[])', 'documents (Document[])'], ], 'Constructeur' => [ 'fields' => ['id (string, CUID)', 'name (string)', 'website (string, nullable)', 'phone (string, nullable)', 'email (string, nullable)', 'createdAt', 'updatedAt'], 'relationships' => ['machines (Machine[])', 'composants (Composant[])', 'pieces (Piece[])', 'products (Product[])'], ], 'ModelType' => [ 'fields' => ['id (string, CUID)', 'name (string)', 'category (string: machine|composant|piece|product)', 'createdAt', 'updatedAt'], 'relationships' => ['skeletonPieceRequirements[]', 'skeletonProductRequirements[]', 'skeletonSubcomponentRequirements[]', 'customFields (CustomField[])'], ], ]; return [new TextContent(text: json_encode($schema, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE))]; } } ``` - [ ] **Step 2: Implement RolesResource, StatsResource, ModelTypesResource** `RolesResource`: static content describing role hierarchy and tool permissions. `StatsResource`: same logic as `DashboardStatsTool` but as a resource. `ModelTypesResource`: resource template with `{category}` parameter showing model types and their skeleton requirements. - [ ] **Step 3: Run php-cs-fixer and commit** ```bash git add src/Mcp/Resource/ git commit -m "feat(mcp) : add MCP resources (schema, roles, stats, model types)" ``` --- ### Task 22: User documentation **Files:** - Create: `docs/mcp/README.md` - [ ] **Step 1: Write the documentation** Create `docs/mcp/README.md` with sections as defined in the spec (section 14): 1. Introduction — what is MCP Inventory, which clients are supported 2. Prerequisites — profile with sufficient role, tunnel access 3. Client configuration — copy-paste examples for Claude Code (stdio via Docker), Claude Desktop (HTTP), ChatGPT Desktop (HTTP), Codex (HTTP) 4. Tool catalogue — table of all ~55 tools with name, description, parameters, required role 5. Guided workflows — step-by-step for creating machine, composant, piece, product 6. Resources — URIs and content 7. Roles & permissions — role hierarchy and tool mapping 8. Error format — categories and examples 9. Known limitations — document upload 10. Troubleshooting — common errors - [ ] **Step 2: Commit** ```bash git add docs/mcp/README.md git commit -m "docs(mcp) : add user documentation for MCP server" ``` --- ### Task 23: Create .mcp.json for Claude Code **Files:** - Create: `.mcp.json` - [ ] **Step 1: Create .mcp.json** Create `.mcp.json` at project root: ```json { "mcpServers": { "inventory": { "command": "docker", "args": [ "exec", "-i", "-e", "MCP_PROFILE_ID=", "-e", "MCP_PROFILE_PASSWORD=", "php-inventory-apache", "php", "bin/console", "mcp:server" ] } } } ``` > **Note:** This file contains credentials placeholders. Add `.mcp.json` to `.gitignore` if you don't want it committed, or use a `.mcp.json.example` template. - [ ] **Step 2: Commit** ```bash git add .mcp.json git commit -m "feat(mcp) : add .mcp.json for Claude Code stdio transport" ``` --- ### Task 24: Update CLAUDE.md with MCP documentation **Files:** - Modify: `CLAUDE.md` - [ ] **Step 1: Add MCP section to CLAUDE.md** Add a new section after "## Architecture Backend" covering: - New directory `src/Mcp/` — Tools, Resources, Security - MCP endpoint `/_mcp` with dedicated firewall - Console command `mcp:server` for stdio transport - Tool naming convention: `verb_entity` (e.g., `list_machines`, `create_composant`) - `#[McpTool]` attribute pattern with `inputSchema` - Return type: `array{TextContent}` with JSON-encoded content - Auth: `McpHeaderAuthenticator` for HTTP, `McpStdioAuthSubscriber` for stdio - [ ] **Step 2: Commit** ```bash git add CLAUDE.md git commit -m "docs(mcp) : add MCP architecture section to CLAUDE.md" ``` --- ### Task 25: Full test suite run and final validation - [ ] **Step 1: Run all MCP tests** ```bash make test FILES=tests/Mcp/ ``` Expected: all tests pass. - [ ] **Step 2: Run full project test suite** ```bash make test ``` Expected: no regressions — existing tests still pass. - [ ] **Step 3: Run php-cs-fixer on all MCP files** ```bash make php-cs-fixer-allow-risky ``` - [ ] **Step 4: Test stdio transport end-to-end** ```bash echo '{"jsonrpc":"2.0","method":"tools/list","params":{},"id":1}' | docker exec -i -e MCP_PROFILE_ID= -e MCP_PROFILE_PASSWORD= php-inventory-apache php bin/console mcp:server ``` Expected: JSON response listing all ~55 tools. - [ ] **Step 5: Test HTTP transport end-to-end** ```bash curl -X POST http://localhost:8081/_mcp \ -H "Content-Type: application/json" \ -H "X-Profile-Id: " \ -H "X-Profile-Password: " \ -d '{"jsonrpc":"2.0","method":"tools/list","params":{},"id":1}' ``` Expected: JSON response listing all tools. - [ ] **Step 6: Final commit (if any remaining unstaged files)** ```bash git status # Only add specific files if there are remaining changes git commit -m "feat(mcp) : finalize MCP server implementation" ```