fix(mcp) : return CallToolResult to prevent structuredContent serialization issue

Tools now return CallToolResult directly instead of Content arrays,
preventing the MCP SDK from auto-generating structuredContent as a
JSON array (which Claude Code rejects — expects a JSON object/record).
Also adds Accept header to test helpers and SSE response parsing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-03-16 17:24:04 +01:00
parent f965affc94
commit add3a9a21f
60 changed files with 156 additions and 84 deletions

View File

@@ -1,14 +1,12 @@
{
"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"
]
"mcpServers": {
"inventory": {
"type": "http",
"url": "http://inventory.malio-dev.fr/_mcp",
"headers": {
"X-Profile-Id": "admin-default-profile",
"X-Profile-Password": "A123"
}
}
}
}
}

View File

@@ -7,6 +7,7 @@ namespace App\Mcp\Tool;
use App\Repository\AuditLogRepository;
use DateTimeInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
use Symfony\Bundle\SecurityBundle\Security;
#[McpTool(
@@ -22,7 +23,7 @@ class ActivityLogTool
private readonly Security $security,
) {}
public function __invoke(int $page = 1, int $limit = 30, string $entityType = '', string $action = ''): array
public function __invoke(int $page = 1, int $limit = 30, string $entityType = '', string $action = ''): CallToolResult
{
$this->requireRole($this->security, 'ROLE_VIEWER');

View File

@@ -9,6 +9,7 @@ use App\Mcp\Tool\McpToolHelper;
use App\Repository\ProfileRepository;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
use Symfony\Bundle\SecurityBundle\Security;
#[McpTool(
@@ -30,7 +31,7 @@ class CreateCommentTool
string $entityType,
string $entityId,
string $entityName = '',
): array {
): CallToolResult {
$this->requireRole($this->security, 'ROLE_VIEWER');
$content = trim($content);

View File

@@ -8,6 +8,7 @@ use App\Entity\Comment;
use App\Mcp\Tool\McpToolHelper;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
#[McpTool(
name: 'list_comments',
@@ -21,7 +22,7 @@ class ListCommentsTool
private readonly EntityManagerInterface $em,
) {}
public function __invoke(string $entityType, string $entityId, int $page = 1, int $limit = 30): array
public function __invoke(string $entityType, string $entityId, int $page = 1, int $limit = 30): CallToolResult
{
$p = $this->paginationParams($page, $limit);

View File

@@ -10,6 +10,7 @@ use App\Repository\ProfileRepository;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
use Symfony\Bundle\SecurityBundle\Security;
#[McpTool(
@@ -26,7 +27,7 @@ class ResolveCommentTool
private readonly ProfileRepository $profiles,
) {}
public function __invoke(string $commentId): array
public function __invoke(string $commentId): CallToolResult
{
$this->requireRole($this->security, 'ROLE_GESTIONNAIRE');

View File

@@ -8,6 +8,7 @@ use App\Entity\Comment;
use App\Mcp\Tool\McpToolHelper;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
#[McpTool(
name: 'get_unresolved_comments_count',
@@ -21,7 +22,7 @@ class UnresolvedCountTool
private readonly EntityManagerInterface $em,
) {}
public function __invoke(): array
public function __invoke(): CallToolResult
{
$count = (int) $this->em->getRepository(Comment::class)
->createQueryBuilder('c')

View File

@@ -10,6 +10,7 @@ use App\Repository\ConstructeurRepository;
use App\Repository\ModelTypeRepository;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
use Symfony\Bundle\SecurityBundle\Security;
#[McpTool(
@@ -37,7 +38,7 @@ class CreateComposantTool
string $prix = '',
string $modelTypeId = '',
array $constructeurIds = [],
): array {
): CallToolResult {
$this->requireRole($this->security, 'ROLE_GESTIONNAIRE');
$composant = new Composant();

View File

@@ -8,6 +8,7 @@ use App\Mcp\Tool\McpToolHelper;
use App\Repository\ComposantRepository;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
use Symfony\Bundle\SecurityBundle\Security;
#[McpTool(
@@ -24,7 +25,7 @@ class DeleteComposantTool
private readonly Security $security,
) {}
public function __invoke(string $composantId): array
public function __invoke(string $composantId): CallToolResult
{
$this->requireRole($this->security, 'ROLE_GESTIONNAIRE');

View File

@@ -7,6 +7,7 @@ namespace App\Mcp\Tool\Composant;
use App\Mcp\Tool\McpToolHelper;
use App\Repository\ComposantRepository;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
#[McpTool(
name: 'get_composant',
@@ -20,7 +21,7 @@ class GetComposantTool
private readonly ComposantRepository $composants,
) {}
public function __invoke(string $composantId): array
public function __invoke(string $composantId): CallToolResult
{
$composant = $this->composants->find($composantId);

View File

@@ -7,6 +7,7 @@ namespace App\Mcp\Tool\Composant;
use App\Mcp\Tool\McpToolHelper;
use App\Repository\ComposantRepository;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
#[McpTool(
name: 'list_composants',
@@ -20,7 +21,7 @@ class ListComposantsTool
private readonly ComposantRepository $composants,
) {}
public function __invoke(int $page = 1, int $limit = 30, string $search = ''): array
public function __invoke(int $page = 1, int $limit = 30, string $search = ''): CallToolResult
{
$p = $this->paginationParams($page, $limit);

View File

@@ -10,6 +10,7 @@ use App\Repository\ConstructeurRepository;
use App\Repository\ModelTypeRepository;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
use Symfony\Bundle\SecurityBundle\Security;
#[McpTool(
@@ -39,7 +40,7 @@ class UpdateComposantTool
?string $prix = null,
?string $modelTypeId = null,
?array $constructeurIds = null,
): array {
): CallToolResult {
$this->requireRole($this->security, 'ROLE_GESTIONNAIRE');
$composant = $this->composants->find($composantId);

View File

@@ -8,6 +8,7 @@ use App\Entity\Constructeur;
use App\Mcp\Tool\McpToolHelper;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
use Symfony\Bundle\SecurityBundle\Security;
#[McpTool(
@@ -27,7 +28,7 @@ class CreateConstructeurTool
string $name,
string $email = '',
string $phone = '',
): array {
): CallToolResult {
$this->requireRole($this->security, 'ROLE_GESTIONNAIRE');
$constructeur = new Constructeur();

View File

@@ -8,6 +8,7 @@ use App\Mcp\Tool\McpToolHelper;
use App\Repository\ConstructeurRepository;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
use Symfony\Bundle\SecurityBundle\Security;
#[McpTool(
@@ -24,7 +25,7 @@ class DeleteConstructeurTool
private readonly Security $security,
) {}
public function __invoke(string $constructeurId): array
public function __invoke(string $constructeurId): CallToolResult
{
$this->requireRole($this->security, 'ROLE_GESTIONNAIRE');

View File

@@ -7,6 +7,7 @@ namespace App\Mcp\Tool\Constructeur;
use App\Mcp\Tool\McpToolHelper;
use App\Repository\ConstructeurRepository;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
#[McpTool(
name: 'get_constructeur',
@@ -20,7 +21,7 @@ class GetConstructeurTool
private readonly ConstructeurRepository $constructeurs,
) {}
public function __invoke(string $constructeurId): array
public function __invoke(string $constructeurId): CallToolResult
{
$constructeur = $this->constructeurs->find($constructeurId);

View File

@@ -7,6 +7,7 @@ namespace App\Mcp\Tool\Constructeur;
use App\Mcp\Tool\McpToolHelper;
use App\Repository\ConstructeurRepository;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
#[McpTool(
name: 'list_constructeurs',
@@ -20,7 +21,7 @@ class ListConstructeursTool
private readonly ConstructeurRepository $constructeurs,
) {}
public function __invoke(int $page = 1, int $limit = 30, string $search = ''): array
public function __invoke(int $page = 1, int $limit = 30, string $search = ''): CallToolResult
{
$p = $this->paginationParams($page, $limit);

View File

@@ -8,6 +8,7 @@ use App\Mcp\Tool\McpToolHelper;
use App\Repository\ConstructeurRepository;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
use Symfony\Bundle\SecurityBundle\Security;
#[McpTool(
@@ -29,7 +30,7 @@ class UpdateConstructeurTool
?string $name = null,
?string $email = null,
?string $phone = null,
): array {
): CallToolResult {
$this->requireRole($this->security, 'ROLE_GESTIONNAIRE');
$constructeur = $this->constructeurs->find($constructeurId);

View File

@@ -8,6 +8,7 @@ use App\Entity\CustomFieldValue;
use App\Mcp\Tool\McpToolHelper;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
use Symfony\Bundle\SecurityBundle\Security;
#[McpTool(
@@ -23,7 +24,7 @@ class DeleteCustomFieldValueTool
private readonly Security $security,
) {}
public function __invoke(string $customFieldValueId): array
public function __invoke(string $customFieldValueId): CallToolResult
{
$this->requireRole($this->security, 'ROLE_GESTIONNAIRE');

View File

@@ -8,6 +8,7 @@ use App\Entity\CustomFieldValue;
use App\Mcp\Tool\McpToolHelper;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
#[McpTool(
name: 'list_custom_field_values',
@@ -23,7 +24,7 @@ class ListCustomFieldValuesTool
private readonly EntityManagerInterface $em,
) {}
public function __invoke(string $entityType, string $entityId): array
public function __invoke(string $entityType, string $entityId): CallToolResult
{
$entityType = strtolower($entityType);

View File

@@ -13,6 +13,7 @@ use App\Entity\Product;
use App\Mcp\Tool\McpToolHelper;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
use Symfony\Bundle\SecurityBundle\Security;
#[McpTool(
@@ -30,7 +31,7 @@ class UpsertCustomFieldValuesTool
private readonly Security $security,
) {}
public function __invoke(string $entityType, string $entityId, array $fields): array
public function __invoke(string $entityType, string $entityId, array $fields): CallToolResult
{
$this->requireRole($this->security, 'ROLE_GESTIONNAIRE');

View File

@@ -12,6 +12,7 @@ use App\Repository\SiteRepository;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Content\TextContent;
use Mcp\Schema\Result\CallToolResult;
#[McpTool(
name: 'get_dashboard_stats',
@@ -28,14 +29,14 @@ class DashboardStatsTool
private readonly EntityManagerInterface $em,
) {}
public function __invoke(): array
public function __invoke(): CallToolResult
{
$unresolvedComments = (int) $this->em->createQuery(
"SELECT COUNT(c.id) FROM App\\Entity\\Comment c WHERE c.status = 'open'"
)->getSingleScalarResult();
return [
new TextContent(
return new CallToolResult(
content: [new TextContent(
text: json_encode([
'machines' => $this->machines->count([]),
'pieces' => $this->pieces->count([]),
@@ -44,7 +45,7 @@ class DashboardStatsTool
'sites' => $this->sites->count([]),
'unresolvedComments' => $unresolvedComments,
], JSON_THROW_ON_ERROR)
),
];
)],
);
}
}

View File

@@ -8,6 +8,7 @@ use App\Mcp\Tool\McpToolHelper;
use App\Repository\DocumentRepository;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
use Symfony\Bundle\SecurityBundle\Security;
#[McpTool(
@@ -24,7 +25,7 @@ class DeleteDocumentTool
private readonly Security $security,
) {}
public function __invoke(string $documentId): array
public function __invoke(string $documentId): CallToolResult
{
$this->requireRole($this->security, 'ROLE_GESTIONNAIRE');

View File

@@ -7,6 +7,7 @@ namespace App\Mcp\Tool\Document;
use App\Mcp\Tool\McpToolHelper;
use App\Repository\DocumentRepository;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
#[McpTool(
name: 'list_documents',
@@ -28,7 +29,7 @@ class ListDocumentsTool
private readonly DocumentRepository $documents,
) {}
public function __invoke(string $entityType, string $entityId): array
public function __invoke(string $entityType, string $entityId): CallToolResult
{
if (!isset(self::ENTITY_FIELDS[$entityType])) {
$this->mcpError('validation', "Invalid entityType '{$entityType}'. Must be one of: site, machine, composant, piece, product.");

View File

@@ -7,6 +7,7 @@ namespace App\Mcp\Tool;
use App\Repository\AuditLogRepository;
use DateTimeInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
use Symfony\Bundle\SecurityBundle\Security;
#[McpTool(
@@ -24,7 +25,7 @@ class EntityHistoryTool
private readonly Security $security,
) {}
public function __invoke(string $entityType, string $entityId): array
public function __invoke(string $entityType, string $entityId): CallToolResult
{
$this->requireRole($this->security, 'ROLE_VIEWER');

View File

@@ -16,6 +16,7 @@ use App\Repository\PieceRepository;
use App\Repository\ProductRepository;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
use Symfony\Bundle\SecurityBundle\Security;
#[McpTool(
@@ -37,7 +38,7 @@ class AddMachineLinksTool
private readonly MachinePieceLinkRepository $pieceLinks,
) {}
public function __invoke(string $machineId, array $links): array
public function __invoke(string $machineId, array $links): CallToolResult
{
$this->requireRole($this->security, 'ROLE_GESTIONNAIRE');

View File

@@ -18,6 +18,7 @@ use App\Repository\MachineProductLinkRepository;
use App\Repository\MachineRepository;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
use Symfony\Bundle\SecurityBundle\Security;
#[McpTool(
@@ -42,7 +43,7 @@ class CloneMachineTool
string $name,
string $siteId,
string $reference = '',
): array {
): CallToolResult {
$this->requireRole($this->security, 'ROLE_GESTIONNAIRE');
$source = $this->machineRepository->find($machineId);

View File

@@ -10,6 +10,7 @@ use App\Repository\ConstructeurRepository;
use App\Repository\SiteRepository;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
use Symfony\Bundle\SecurityBundle\Security;
#[McpTool(
@@ -36,7 +37,7 @@ class CreateMachineTool
string $reference = '',
string $prix = '',
array $constructeurIds = [],
): array {
): CallToolResult {
$this->requireRole($this->security, 'ROLE_GESTIONNAIRE');
$site = $this->sites->find($siteId);

View File

@@ -8,6 +8,7 @@ use App\Mcp\Tool\McpToolHelper;
use App\Repository\MachineRepository;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
use Symfony\Bundle\SecurityBundle\Security;
#[McpTool(
@@ -24,7 +25,7 @@ class DeleteMachineTool
private readonly Security $security,
) {}
public function __invoke(string $machineId): array
public function __invoke(string $machineId): CallToolResult
{
$this->requireRole($this->security, 'ROLE_GESTIONNAIRE');

View File

@@ -7,6 +7,7 @@ namespace App\Mcp\Tool\Machine;
use App\Mcp\Tool\McpToolHelper;
use App\Repository\MachineRepository;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
#[McpTool(
name: 'get_machine',
@@ -20,7 +21,7 @@ class GetMachineTool
private readonly MachineRepository $machines,
) {}
public function __invoke(string $machineId): array
public function __invoke(string $machineId): CallToolResult
{
$machine = $this->machines->find($machineId);

View File

@@ -10,6 +10,7 @@ use App\Repository\MachinePieceLinkRepository;
use App\Repository\MachineProductLinkRepository;
use App\Repository\MachineRepository;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
#[McpTool(
name: 'list_machine_links',
@@ -26,7 +27,7 @@ class ListMachineLinksTool
private readonly MachineProductLinkRepository $productLinks,
) {}
public function __invoke(string $machineId): array
public function __invoke(string $machineId): CallToolResult
{
$machine = $this->machines->find($machineId);
if (null === $machine) {

View File

@@ -7,6 +7,7 @@ namespace App\Mcp\Tool\Machine;
use App\Mcp\Tool\McpToolHelper;
use App\Repository\MachineRepository;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
#[McpTool(
name: 'list_machines',
@@ -20,7 +21,7 @@ class ListMachinesTool
private readonly MachineRepository $machines,
) {}
public function __invoke(int $page = 1, int $limit = 30, string $search = ''): array
public function __invoke(int $page = 1, int $limit = 30, string $search = ''): CallToolResult
{
$p = $this->paginationParams($page, $limit);

View File

@@ -21,6 +21,7 @@ use App\Repository\MachineProductLinkRepository;
use App\Repository\MachineRepository;
use Doctrine\Common\Collections\Collection;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
#[McpTool(
name: 'get_machine_structure',
@@ -37,7 +38,7 @@ class MachineStructureTool
private readonly MachineProductLinkRepository $machineProductLinkRepository,
) {}
public function __invoke(string $machineId): array
public function __invoke(string $machineId): CallToolResult
{
$machine = $this->machineRepository->find($machineId);

View File

@@ -10,6 +10,7 @@ use App\Repository\MachinePieceLinkRepository;
use App\Repository\MachineProductLinkRepository;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
use Symfony\Bundle\SecurityBundle\Security;
#[McpTool(
@@ -28,7 +29,7 @@ class RemoveMachineLinkTool
private readonly MachineProductLinkRepository $productLinks,
) {}
public function __invoke(string $linkId, string $linkType): array
public function __invoke(string $linkId, string $linkType): CallToolResult
{
$this->requireRole($this->security, 'ROLE_GESTIONNAIRE');

View File

@@ -11,6 +11,7 @@ use App\Repository\MachineComponentLinkRepository;
use App\Repository\MachinePieceLinkRepository;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
use Symfony\Bundle\SecurityBundle\Security;
#[McpTool(
@@ -35,7 +36,7 @@ class UpdateMachineLinkTool
?string $referenceOverride = null,
?string $prixOverride = null,
?int $quantity = null,
): array {
): CallToolResult {
$this->requireRole($this->security, 'ROLE_GESTIONNAIRE');
switch ($linkType) {

View File

@@ -10,6 +10,7 @@ use App\Repository\MachineRepository;
use App\Repository\SiteRepository;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
use Symfony\Bundle\SecurityBundle\Security;
#[McpTool(
@@ -38,7 +39,7 @@ class UpdateMachineTool
?string $prix = null,
?string $siteId = null,
?array $constructeurIds = null,
): array {
): CallToolResult {
$this->requireRole($this->security, 'ROLE_GESTIONNAIRE');
$machine = $this->machines->find($machineId);

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Mcp\Tool;
use Mcp\Schema\Content\TextContent;
use Mcp\Schema\Result\CallToolResult;
use RuntimeException;
use Symfony\Bundle\SecurityBundle\Security;
@@ -17,12 +18,11 @@ trait McpToolHelper
}
}
/**
* @return array{TextContent}
*/
private function jsonResponse(array $data): array
private function jsonResponse(array $data): CallToolResult
{
return [new TextContent(text: json_encode($data, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE))];
return new CallToolResult(
content: [new TextContent(text: json_encode($data, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE))],
);
}
private function mcpError(string $category, string $message): never
@@ -42,10 +42,7 @@ trait McpToolHelper
return ['page' => $page, 'limit' => $limit, 'offset' => $offset];
}
/**
* @return array{TextContent}
*/
private function paginatedResponse(array $items, int $total, int $page, int $limit): array
private function paginatedResponse(array $items, int $total, int $page, int $limit): CallToolResult
{
return $this->jsonResponse([
'items' => $items,

View File

@@ -9,6 +9,7 @@ use App\Enum\ModelCategory;
use App\Mcp\Tool\McpToolHelper;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
use Symfony\Bundle\SecurityBundle\Security;
#[McpTool(
@@ -24,7 +25,7 @@ class CreateModelTypeTool
private readonly Security $security,
) {}
public function __invoke(string $name, string $category, string $code = ''): array
public function __invoke(string $name, string $category, string $code = ''): CallToolResult
{
$this->requireRole($this->security, 'ROLE_GESTIONNAIRE');

View File

@@ -8,6 +8,7 @@ use App\Mcp\Tool\McpToolHelper;
use App\Repository\ModelTypeRepository;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
use Symfony\Bundle\SecurityBundle\Security;
#[McpTool(
@@ -24,7 +25,7 @@ class DeleteModelTypeTool
private readonly Security $security,
) {}
public function __invoke(string $modelTypeId): array
public function __invoke(string $modelTypeId): CallToolResult
{
$this->requireRole($this->security, 'ROLE_GESTIONNAIRE');

View File

@@ -7,6 +7,7 @@ namespace App\Mcp\Tool\ModelType;
use App\Mcp\Tool\McpToolHelper;
use App\Repository\ModelTypeRepository;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
#[McpTool(
name: 'get_model_type',
@@ -20,7 +21,7 @@ class GetModelTypeTool
private readonly ModelTypeRepository $modelTypes,
) {}
public function __invoke(string $modelTypeId): array
public function __invoke(string $modelTypeId): CallToolResult
{
$mt = $this->modelTypes->find($modelTypeId);

View File

@@ -8,6 +8,7 @@ use App\Enum\ModelCategory;
use App\Mcp\Tool\McpToolHelper;
use App\Repository\ModelTypeRepository;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
#[McpTool(
name: 'list_model_types',
@@ -21,7 +22,7 @@ class ListModelTypesTool
private readonly ModelTypeRepository $modelTypes,
) {}
public function __invoke(int $page = 1, int $limit = 30, string $category = ''): array
public function __invoke(int $page = 1, int $limit = 30, string $category = ''): CallToolResult
{
$p = $this->paginationParams($page, $limit);

View File

@@ -10,6 +10,7 @@ use App\Repository\ModelTypeRepository;
use App\Service\ModelTypeSyncService;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
use Symfony\Bundle\SecurityBundle\Security;
#[McpTool(
@@ -33,7 +34,7 @@ class SyncModelTypeTool
?array $structure = null,
bool $confirmDeletions = false,
bool $confirmTypeChanges = false,
): array {
): CallToolResult {
$this->requireRole($this->security, 'ROLE_GESTIONNAIRE');
if (!in_array($action, ['preview', 'sync'], true)) {

View File

@@ -8,6 +8,7 @@ use App\Mcp\Tool\McpToolHelper;
use App\Repository\ModelTypeRepository;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
use Symfony\Bundle\SecurityBundle\Security;
#[McpTool(
@@ -24,7 +25,7 @@ class UpdateModelTypeTool
private readonly Security $security,
) {}
public function __invoke(string $modelTypeId, ?string $name = null, ?string $code = null): array
public function __invoke(string $modelTypeId, ?string $name = null, ?string $code = null): CallToolResult
{
$this->requireRole($this->security, 'ROLE_GESTIONNAIRE');

View File

@@ -10,6 +10,7 @@ use App\Repository\ConstructeurRepository;
use App\Repository\ModelTypeRepository;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
use Symfony\Bundle\SecurityBundle\Security;
#[McpTool(
@@ -37,7 +38,7 @@ class CreatePieceTool
string $prix = '',
string $modelTypeId = '',
array $constructeurIds = [],
): array {
): CallToolResult {
$this->requireRole($this->security, 'ROLE_GESTIONNAIRE');
$piece = new Piece();

View File

@@ -8,6 +8,7 @@ use App\Mcp\Tool\McpToolHelper;
use App\Repository\PieceRepository;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
use Symfony\Bundle\SecurityBundle\Security;
#[McpTool(
@@ -24,7 +25,7 @@ class DeletePieceTool
private readonly Security $security,
) {}
public function __invoke(string $pieceId): array
public function __invoke(string $pieceId): CallToolResult
{
$this->requireRole($this->security, 'ROLE_GESTIONNAIRE');

View File

@@ -7,6 +7,7 @@ namespace App\Mcp\Tool\Piece;
use App\Mcp\Tool\McpToolHelper;
use App\Repository\PieceRepository;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
#[McpTool(
name: 'get_piece',
@@ -20,7 +21,7 @@ class GetPieceTool
private readonly PieceRepository $pieces,
) {}
public function __invoke(string $pieceId): array
public function __invoke(string $pieceId): CallToolResult
{
$piece = $this->pieces->find($pieceId);

View File

@@ -7,6 +7,7 @@ namespace App\Mcp\Tool\Piece;
use App\Mcp\Tool\McpToolHelper;
use App\Repository\PieceRepository;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
#[McpTool(
name: 'list_pieces',
@@ -20,7 +21,7 @@ class ListPiecesTool
private readonly PieceRepository $pieces,
) {}
public function __invoke(int $page = 1, int $limit = 30, string $search = ''): array
public function __invoke(int $page = 1, int $limit = 30, string $search = ''): CallToolResult
{
$p = $this->paginationParams($page, $limit);

View File

@@ -10,6 +10,7 @@ use App\Repository\ModelTypeRepository;
use App\Repository\PieceRepository;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
use Symfony\Bundle\SecurityBundle\Security;
#[McpTool(
@@ -39,7 +40,7 @@ class UpdatePieceTool
?string $prix = null,
?string $modelTypeId = null,
?array $constructeurIds = null,
): array {
): CallToolResult {
$this->requireRole($this->security, 'ROLE_GESTIONNAIRE');
$piece = $this->pieces->find($pieceId);

View File

@@ -10,6 +10,7 @@ use App\Repository\ConstructeurRepository;
use App\Repository\ModelTypeRepository;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
use Symfony\Bundle\SecurityBundle\Security;
#[McpTool(
@@ -36,7 +37,7 @@ class CreateProductTool
string $supplierPrice = '',
string $modelTypeId = '',
array $constructeurIds = [],
): array {
): CallToolResult {
$this->requireRole($this->security, 'ROLE_GESTIONNAIRE');
$product = new Product();

View File

@@ -8,6 +8,7 @@ use App\Mcp\Tool\McpToolHelper;
use App\Repository\ProductRepository;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
use Symfony\Bundle\SecurityBundle\Security;
#[McpTool(
@@ -24,7 +25,7 @@ class DeleteProductTool
private readonly Security $security,
) {}
public function __invoke(string $productId): array
public function __invoke(string $productId): CallToolResult
{
$this->requireRole($this->security, 'ROLE_GESTIONNAIRE');

View File

@@ -7,6 +7,7 @@ namespace App\Mcp\Tool\Product;
use App\Mcp\Tool\McpToolHelper;
use App\Repository\ProductRepository;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
#[McpTool(
name: 'get_product',
@@ -20,7 +21,7 @@ class GetProductTool
private readonly ProductRepository $products,
) {}
public function __invoke(string $productId): array
public function __invoke(string $productId): CallToolResult
{
$product = $this->products->find($productId);

View File

@@ -7,6 +7,7 @@ namespace App\Mcp\Tool\Product;
use App\Mcp\Tool\McpToolHelper;
use App\Repository\ProductRepository;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
#[McpTool(
name: 'list_products',
@@ -20,7 +21,7 @@ class ListProductsTool
private readonly ProductRepository $products,
) {}
public function __invoke(int $page = 1, int $limit = 30, string $search = ''): array
public function __invoke(int $page = 1, int $limit = 30, string $search = ''): CallToolResult
{
$p = $this->paginationParams($page, $limit);

View File

@@ -10,6 +10,7 @@ use App\Repository\ModelTypeRepository;
use App\Repository\ProductRepository;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
use Symfony\Bundle\SecurityBundle\Security;
#[McpTool(
@@ -38,7 +39,7 @@ class UpdateProductTool
?string $supplierPrice = null,
?string $modelTypeId = null,
?array $constructeurIds = null,
): array {
): CallToolResult {
$this->requireRole($this->security, 'ROLE_GESTIONNAIRE');
$product = $this->products->find($productId);

View File

@@ -11,6 +11,7 @@ use App\Repository\PieceRepository;
use App\Repository\ProductRepository;
use App\Repository\SiteRepository;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
#[McpTool(
name: 'search_inventory',
@@ -31,7 +32,7 @@ class SearchInventoryTool
private readonly ConstructeurRepository $constructeurs,
) {}
public function __invoke(string $query, string $types = '', int $limit = 20): array
public function __invoke(string $query, string $types = '', int $limit = 20): CallToolResult
{
$query = trim($query);
if ('' === $query) {

View File

@@ -8,6 +8,7 @@ use App\Entity\Site;
use App\Mcp\Tool\McpToolHelper;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
use Symfony\Bundle\SecurityBundle\Security;
#[McpTool(
@@ -31,7 +32,7 @@ class CreateSiteTool
string $contactPostalCode = '',
string $contactCity = '',
string $color = '',
): array {
): CallToolResult {
$this->requireRole($this->security, 'ROLE_GESTIONNAIRE');
$site = new Site();

View File

@@ -8,6 +8,7 @@ use App\Mcp\Tool\McpToolHelper;
use App\Repository\SiteRepository;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
use Symfony\Bundle\SecurityBundle\Security;
#[McpTool(
@@ -24,7 +25,7 @@ class DeleteSiteTool
private readonly Security $security,
) {}
public function __invoke(string $siteId): array
public function __invoke(string $siteId): CallToolResult
{
$this->requireRole($this->security, 'ROLE_GESTIONNAIRE');

View File

@@ -7,6 +7,7 @@ namespace App\Mcp\Tool\Site;
use App\Mcp\Tool\McpToolHelper;
use App\Repository\SiteRepository;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
#[McpTool(
name: 'get_site',
@@ -20,7 +21,7 @@ class GetSiteTool
private readonly SiteRepository $sites,
) {}
public function __invoke(string $siteId): array
public function __invoke(string $siteId): CallToolResult
{
$site = $this->sites->find($siteId);

View File

@@ -7,6 +7,7 @@ namespace App\Mcp\Tool\Site;
use App\Mcp\Tool\McpToolHelper;
use App\Repository\SiteRepository;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
#[McpTool(
name: 'list_sites',
@@ -20,7 +21,7 @@ class ListSitesTool
private readonly SiteRepository $sites,
) {}
public function __invoke(int $page = 1, int $limit = 30, string $search = ''): array
public function __invoke(int $page = 1, int $limit = 30, string $search = ''): CallToolResult
{
$p = $this->paginationParams($page, $limit);

View File

@@ -8,6 +8,7 @@ use App\Mcp\Tool\McpToolHelper;
use App\Repository\SiteRepository;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
use Symfony\Bundle\SecurityBundle\Security;
#[McpTool(
@@ -33,7 +34,7 @@ class UpdateSiteTool
?string $contactPostalCode = null,
?string $contactCity = null,
?string $color = null,
): array {
): CallToolResult {
$this->requireRole($this->security, 'ROLE_GESTIONNAIRE');
$site = $this->sites->find($siteId);

View File

@@ -11,6 +11,7 @@ use App\Entity\PieceProductSlot;
use App\Mcp\Tool\McpToolHelper;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
#[McpTool(
name: 'list_slots',
@@ -24,7 +25,7 @@ class ListSlotsTool
private readonly EntityManagerInterface $em,
) {}
public function __invoke(string $entityType, string $entityId): array
public function __invoke(string $entityType, string $entityId): CallToolResult
{
if ('composant' === $entityType) {
return $this->listComposantSlots($entityId);
@@ -37,7 +38,7 @@ class ListSlotsTool
$this->mcpError('validation', "entityType must be 'composant' or 'piece', got '{$entityType}'.");
}
private function listComposantSlots(string $composantId): array
private function listComposantSlots(string $composantId): CallToolResult
{
$pieceSlots = $this->em->createQueryBuilder()
->select(
@@ -108,7 +109,7 @@ class ListSlotsTool
]);
}
private function listPieceSlots(string $pieceId): array
private function listPieceSlots(string $pieceId): CallToolResult
{
$slots = $this->em->createQueryBuilder()
->select(

View File

@@ -14,6 +14,7 @@ use App\Entity\Product;
use App\Mcp\Tool\McpToolHelper;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;
use Symfony\Bundle\SecurityBundle\Security;
#[McpTool(
@@ -29,7 +30,7 @@ class UpdateSlotsTool
private readonly Security $security,
) {}
public function __invoke(array $slots): array
public function __invoke(array $slots): CallToolResult
{
$this->requireRole($this->security, 'ROLE_GESTIONNAIRE');

View File

@@ -108,6 +108,7 @@ abstract class AbstractApiTestCase extends ApiTestCase
$response = $client->request('POST', '/_mcp', [
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json, text/event-stream',
'X-Profile-Id' => $profileId,
'X-Profile-Password' => $password,
],
@@ -128,6 +129,7 @@ abstract class AbstractApiTestCase extends ApiTestCase
$client->request('POST', '/_mcp', [
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json, text/event-stream',
'X-Profile-Id' => $profileId,
'X-Profile-Password' => $password,
'Mcp-Session-Id' => $sessionId,
@@ -149,6 +151,7 @@ abstract class AbstractApiTestCase extends ApiTestCase
$response = $session['client']->request('POST', '/_mcp', [
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json, text/event-stream',
'X-Profile-Id' => $session['profileId'],
'X-Profile-Password' => $session['password'],
'Mcp-Session-Id' => $session['sessionId'],
@@ -164,7 +167,24 @@ abstract class AbstractApiTestCase extends ApiTestCase
]),
]);
$data = $response->toArray(false);
$raw = $response->getContent(false);
$data = json_decode($raw, true);
if (null === $data) {
// SSE format: parse "data: {...}" lines
foreach (explode("\n", $raw) as $line) {
if (str_starts_with($line, 'data: ')) {
$parsed = json_decode(substr($line, 6), true);
if ($parsed && (isset($parsed['result']) || isset($parsed['error']))) {
$data = $parsed;
break;
}
}
}
}
$data ??= [];
if (isset($data['result']['content'][0]['text'])) {
$data['_parsed'] = json_decode($data['result']['content'][0]['text'], true);