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>
471 lines
18 KiB
PHP
471 lines
18 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Mcp\Tool\Machine;
|
|
|
|
use App\Entity\Composant;
|
|
use App\Entity\CustomField;
|
|
use App\Entity\CustomFieldValue;
|
|
use App\Entity\Machine;
|
|
use App\Entity\MachineComponentLink;
|
|
use App\Entity\MachinePieceLink;
|
|
use App\Entity\MachineProductLink;
|
|
use App\Entity\ModelType;
|
|
use App\Entity\Piece;
|
|
use App\Entity\Product;
|
|
use App\Mcp\Tool\McpToolHelper;
|
|
use App\Repository\MachineComponentLinkRepository;
|
|
use App\Repository\MachinePieceLinkRepository;
|
|
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',
|
|
description: 'Get the full machine hierarchy: machine info, component links (with composant details, slots, overrides), piece links (with piece details, quantity, overrides), and product links.',
|
|
)]
|
|
class MachineStructureTool
|
|
{
|
|
use McpToolHelper;
|
|
|
|
public function __construct(
|
|
private readonly MachineRepository $machineRepository,
|
|
private readonly MachineComponentLinkRepository $machineComponentLinkRepository,
|
|
private readonly MachinePieceLinkRepository $machinePieceLinkRepository,
|
|
private readonly MachineProductLinkRepository $machineProductLinkRepository,
|
|
) {}
|
|
|
|
public function __invoke(string $machineId): CallToolResult
|
|
{
|
|
$machine = $this->machineRepository->find($machineId);
|
|
|
|
if (!$machine instanceof Machine) {
|
|
$this->mcpError('not_found', "Machine not found: {$machineId}");
|
|
}
|
|
|
|
$componentLinks = $this->machineComponentLinkRepository->findBy(['machine' => $machine]);
|
|
$pieceLinks = $this->machinePieceLinkRepository->findBy(['machine' => $machine]);
|
|
$productLinks = $this->machineProductLinkRepository->findBy(['machine' => $machine]);
|
|
|
|
return $this->jsonResponse($this->normalizeStructureResponse(
|
|
$machine,
|
|
$componentLinks,
|
|
$pieceLinks,
|
|
$productLinks,
|
|
));
|
|
}
|
|
|
|
/**
|
|
* @param MachineComponentLink[] $componentLinks
|
|
* @param MachinePieceLink[] $pieceLinks
|
|
* @param MachineProductLink[] $productLinks
|
|
*/
|
|
private function normalizeStructureResponse(
|
|
Machine $machine,
|
|
array $componentLinks,
|
|
array $pieceLinks,
|
|
array $productLinks,
|
|
): array {
|
|
$normalizedComponentLinks = $this->normalizeComponentLinks($componentLinks);
|
|
$componentIndex = $this->indexById($normalizedComponentLinks);
|
|
$normalizedPieceLinks = $this->normalizePieceLinks($pieceLinks);
|
|
|
|
$childIds = [];
|
|
foreach ($normalizedComponentLinks as $link) {
|
|
$parentId = $link['parentComponentLinkId'] ?? null;
|
|
if ($parentId && isset($componentIndex[$parentId])) {
|
|
$componentIndex[$parentId]['childLinks'][] = $link;
|
|
$childIds[$link['id']] = true;
|
|
}
|
|
}
|
|
|
|
$this->attachPiecesToComponents($componentIndex, $normalizedPieceLinks);
|
|
|
|
$rootComponents = array_filter(
|
|
$componentIndex,
|
|
static fn (array $link) => !isset($childIds[$link['id']]),
|
|
);
|
|
|
|
return [
|
|
'machine' => $this->normalizeMachine($machine),
|
|
'componentLinks' => array_values($rootComponents),
|
|
'pieceLinks' => $normalizedPieceLinks,
|
|
'productLinks' => $this->normalizeProductLinks($productLinks),
|
|
];
|
|
}
|
|
|
|
private function attachPiecesToComponents(array &$componentIndex, array $pieceLinks): void
|
|
{
|
|
foreach ($pieceLinks as $pieceLink) {
|
|
$parentId = $pieceLink['parentComponentLinkId'] ?? null;
|
|
if ($parentId && isset($componentIndex[$parentId])) {
|
|
$componentIndex[$parentId]['pieceLinks'][] = $pieceLink;
|
|
}
|
|
}
|
|
|
|
foreach ($componentIndex as &$component) {
|
|
if (!empty($component['childLinks'])) {
|
|
$this->attachPiecesToChildComponents($component['childLinks'], $pieceLinks);
|
|
}
|
|
}
|
|
}
|
|
|
|
private function attachPiecesToChildComponents(array &$childLinks, array $pieceLinks): void
|
|
{
|
|
foreach ($childLinks as &$child) {
|
|
$childId = $child['id'] ?? null;
|
|
if ($childId) {
|
|
foreach ($pieceLinks as $pieceLink) {
|
|
if (($pieceLink['parentComponentLinkId'] ?? null) === $childId) {
|
|
$child['pieceLinks'][] = $pieceLink;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!empty($child['childLinks'])) {
|
|
$this->attachPiecesToChildComponents($child['childLinks'], $pieceLinks);
|
|
}
|
|
}
|
|
}
|
|
|
|
private function normalizeMachine(Machine $machine): array
|
|
{
|
|
$site = $machine->getSite();
|
|
|
|
return [
|
|
'id' => $machine->getId(),
|
|
'name' => $machine->getName(),
|
|
'reference' => $machine->getReference(),
|
|
'prix' => $machine->getPrix(),
|
|
'siteId' => $site->getId(),
|
|
'site' => [
|
|
'id' => $site->getId(),
|
|
'name' => $site->getName(),
|
|
],
|
|
'constructeurs' => $this->normalizeConstructeurs($machine->getConstructeurs()),
|
|
'customFields' => $this->normalizeCustomFields($machine->getCustomFields()),
|
|
'customFieldValues' => $this->normalizeCustomFieldValues($machine->getCustomFieldValues()),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param MachineComponentLink[] $links
|
|
*/
|
|
private function normalizeComponentLinks(array $links): array
|
|
{
|
|
return array_map(function (MachineComponentLink $link): array {
|
|
$composant = $link->getComposant();
|
|
$parentLink = $link->getParentLink();
|
|
|
|
return [
|
|
'id' => $link->getId(),
|
|
'linkId' => $link->getId(),
|
|
'machineId' => $link->getMachine()->getId(),
|
|
'composantId' => $composant->getId(),
|
|
'composant' => $this->normalizeComposant($composant),
|
|
'parentLinkId' => $parentLink?->getId(),
|
|
'parentComponentLinkId' => $parentLink?->getId(),
|
|
'parentComponentId' => $parentLink?->getComposant()->getId(),
|
|
'overrides' => $this->normalizeOverrides($link),
|
|
'childLinks' => [],
|
|
'pieceLinks' => [],
|
|
];
|
|
}, $links);
|
|
}
|
|
|
|
/**
|
|
* @param MachinePieceLink[] $links
|
|
*/
|
|
private function normalizePieceLinks(array $links): array
|
|
{
|
|
return array_map(function (MachinePieceLink $link): array {
|
|
$piece = $link->getPiece();
|
|
$parentLink = $link->getParentLink();
|
|
|
|
return [
|
|
'id' => $link->getId(),
|
|
'linkId' => $link->getId(),
|
|
'machineId' => $link->getMachine()->getId(),
|
|
'pieceId' => $piece->getId(),
|
|
'piece' => $this->normalizePiece($piece),
|
|
'parentLinkId' => $parentLink?->getId(),
|
|
'parentComponentLinkId' => $parentLink?->getId(),
|
|
'parentComponentId' => $parentLink?->getComposant()->getId(),
|
|
'overrides' => $this->normalizeOverrides($link),
|
|
'quantity' => $this->resolvePieceQuantity($link),
|
|
];
|
|
}, $links);
|
|
}
|
|
|
|
private function resolvePieceQuantity(MachinePieceLink $link): int
|
|
{
|
|
$parentLink = $link->getParentLink();
|
|
|
|
if (!$parentLink) {
|
|
return $link->getQuantity();
|
|
}
|
|
|
|
$composant = $parentLink->getComposant();
|
|
$piece = $link->getPiece();
|
|
|
|
foreach ($composant->getPieceSlots() as $slot) {
|
|
if ($slot->getSelectedPiece()?->getId() === $piece->getId()) {
|
|
return $slot->getQuantity();
|
|
}
|
|
}
|
|
|
|
return $link->getQuantity();
|
|
}
|
|
|
|
/**
|
|
* @param MachineProductLink[] $links
|
|
*/
|
|
private function normalizeProductLinks(array $links): array
|
|
{
|
|
return array_map(function (MachineProductLink $link): array {
|
|
$product = $link->getProduct();
|
|
|
|
return [
|
|
'id' => $link->getId(),
|
|
'linkId' => $link->getId(),
|
|
'machineId' => $link->getMachine()->getId(),
|
|
'productId' => $product->getId(),
|
|
'product' => $this->normalizeProduct($product),
|
|
'parentLinkId' => $link->getParentLink()?->getId(),
|
|
'parentComponentLinkId' => $link->getParentComponentLink()?->getId(),
|
|
'parentPieceLinkId' => $link->getParentPieceLink()?->getId(),
|
|
];
|
|
}, $links);
|
|
}
|
|
|
|
private function normalizeComposant(Composant $composant): array
|
|
{
|
|
$type = $composant->getTypeComposant();
|
|
|
|
return [
|
|
'id' => $composant->getId(),
|
|
'name' => $composant->getName(),
|
|
'reference' => $composant->getReference(),
|
|
'prix' => $composant->getPrix(),
|
|
'typeComposantId' => $type?->getId(),
|
|
'typeComposant' => $this->normalizeModelType($type),
|
|
'productId' => $composant->getProduct()?->getId(),
|
|
'product' => $composant->getProduct() ? $this->normalizeProduct($composant->getProduct()) : null,
|
|
'structure' => $this->buildStructureFromSlots($composant),
|
|
'constructeurs' => $this->normalizeConstructeurs($composant->getConstructeurs()),
|
|
'customFields' => $type ? $this->normalizeCustomFieldDefinitions($type->getComponentCustomFields()) : [],
|
|
'customFieldValues' => $this->normalizeCustomFieldValues($composant->getCustomFieldValues()),
|
|
];
|
|
}
|
|
|
|
private function buildStructureFromSlots(Composant $composant): array
|
|
{
|
|
$pieces = [];
|
|
foreach ($composant->getPieceSlots() as $slot) {
|
|
$pieceData = [
|
|
'slotId' => $slot->getId(),
|
|
'typePieceId' => $slot->getTypePiece()?->getId(),
|
|
'quantity' => $slot->getQuantity(),
|
|
'selectedPieceId' => $slot->getSelectedPiece()?->getId(),
|
|
];
|
|
if ($slot->getSelectedPiece()) {
|
|
$pieceData['resolvedPiece'] = $this->normalizePiece($slot->getSelectedPiece());
|
|
}
|
|
$pieces[] = $pieceData;
|
|
}
|
|
|
|
$subcomponents = [];
|
|
foreach ($composant->getSubcomponentSlots() as $slot) {
|
|
$subcomponents[] = [
|
|
'alias' => $slot->getAlias(),
|
|
'familyCode' => $slot->getFamilyCode(),
|
|
'typeComposantId' => $slot->getTypeComposant()?->getId(),
|
|
'selectedComponentId' => $slot->getSelectedComposant()?->getId(),
|
|
];
|
|
}
|
|
|
|
$products = [];
|
|
foreach ($composant->getProductSlots() as $slot) {
|
|
$products[] = [
|
|
'typeProductId' => $slot->getTypeProduct()?->getId(),
|
|
'familyCode' => $slot->getFamilyCode(),
|
|
'selectedProductId' => $slot->getSelectedProduct()?->getId(),
|
|
];
|
|
}
|
|
|
|
return [
|
|
'pieces' => $pieces,
|
|
'subcomponents' => $subcomponents,
|
|
'products' => $products,
|
|
];
|
|
}
|
|
|
|
private function normalizePiece(Piece $piece): array
|
|
{
|
|
$type = $piece->getTypePiece();
|
|
|
|
return [
|
|
'id' => $piece->getId(),
|
|
'name' => $piece->getName(),
|
|
'reference' => $piece->getReference(),
|
|
'prix' => $piece->getPrix(),
|
|
'typePieceId' => $type?->getId(),
|
|
'typePiece' => $this->normalizeModelType($type),
|
|
'productId' => $piece->getProduct()?->getId(),
|
|
'product' => $piece->getProduct() ? $this->normalizeProduct($piece->getProduct()) : null,
|
|
'constructeurs' => $this->normalizeConstructeurs($piece->getConstructeurs()),
|
|
'customFields' => $type ? $this->normalizeCustomFieldDefinitions($type->getPieceCustomFields()) : [],
|
|
'customFieldValues' => $this->normalizeCustomFieldValues($piece->getCustomFieldValues()),
|
|
];
|
|
}
|
|
|
|
private function normalizeProduct(Product $product): array
|
|
{
|
|
$type = $product->getTypeProduct();
|
|
|
|
return [
|
|
'id' => $product->getId(),
|
|
'name' => $product->getName(),
|
|
'reference' => $product->getReference(),
|
|
'supplierPrice' => $product->getSupplierPrice(),
|
|
'typeProductId' => $type?->getId(),
|
|
'typeProduct' => $this->normalizeModelType($type),
|
|
'constructeurs' => $this->normalizeConstructeurs($product->getConstructeurs()),
|
|
'customFields' => $type ? $this->normalizeCustomFieldDefinitions($type->getProductCustomFields()) : [],
|
|
'customFieldValues' => $this->normalizeCustomFieldValues($product->getCustomFieldValues()),
|
|
];
|
|
}
|
|
|
|
private function normalizeModelType(?ModelType $type): ?array
|
|
{
|
|
if (!$type instanceof ModelType) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'id' => $type->getId(),
|
|
'name' => $type->getName(),
|
|
'code' => $type->getCode(),
|
|
'category' => $type->getCategory()->value,
|
|
'structure' => $type->getStructure(),
|
|
];
|
|
}
|
|
|
|
private function normalizeConstructeurs(Collection $constructeurs): array
|
|
{
|
|
$items = [];
|
|
foreach ($constructeurs as $constructeur) {
|
|
$items[] = [
|
|
'id' => $constructeur->getId(),
|
|
'name' => $constructeur->getName(),
|
|
'email' => $constructeur->getEmail(),
|
|
'phone' => $constructeur->getPhone(),
|
|
];
|
|
}
|
|
|
|
return $items;
|
|
}
|
|
|
|
private function normalizeCustomFields(Collection $customFields): array
|
|
{
|
|
$items = [];
|
|
foreach ($customFields as $customField) {
|
|
if (!$customField instanceof CustomField) {
|
|
continue;
|
|
}
|
|
$items[] = [
|
|
'id' => $customField->getId(),
|
|
'name' => $customField->getName(),
|
|
'type' => $customField->getType(),
|
|
'required' => $customField->isRequired(),
|
|
'options' => $customField->getOptions(),
|
|
'defaultValue' => $customField->getDefaultValue(),
|
|
'orderIndex' => $customField->getOrderIndex(),
|
|
];
|
|
}
|
|
|
|
return $items;
|
|
}
|
|
|
|
private function normalizeCustomFieldDefinitions(Collection $customFields): array
|
|
{
|
|
$items = [];
|
|
foreach ($customFields as $cf) {
|
|
if (!$cf instanceof CustomField) {
|
|
continue;
|
|
}
|
|
$items[] = [
|
|
'id' => $cf->getId(),
|
|
'name' => $cf->getName(),
|
|
'type' => $cf->getType(),
|
|
'required' => $cf->isRequired(),
|
|
'options' => $cf->getOptions(),
|
|
'defaultValue' => $cf->getDefaultValue(),
|
|
'orderIndex' => $cf->getOrderIndex(),
|
|
];
|
|
}
|
|
|
|
usort($items, static fn (array $a, array $b) => $a['orderIndex'] <=> $b['orderIndex']);
|
|
|
|
return $items;
|
|
}
|
|
|
|
private function normalizeCustomFieldValues(Collection $customFieldValues): array
|
|
{
|
|
$items = [];
|
|
foreach ($customFieldValues as $cfv) {
|
|
if (!$cfv instanceof CustomFieldValue) {
|
|
continue;
|
|
}
|
|
$cf = $cfv->getCustomField();
|
|
$items[] = [
|
|
'id' => $cfv->getId(),
|
|
'value' => $cfv->getValue(),
|
|
'customField' => [
|
|
'id' => $cf->getId(),
|
|
'name' => $cf->getName(),
|
|
'type' => $cf->getType(),
|
|
'required' => $cf->isRequired(),
|
|
'options' => $cf->getOptions(),
|
|
'defaultValue' => $cf->getDefaultValue(),
|
|
'orderIndex' => $cf->getOrderIndex(),
|
|
],
|
|
];
|
|
}
|
|
|
|
return $items;
|
|
}
|
|
|
|
private function normalizeOverrides(object $link): ?array
|
|
{
|
|
$name = method_exists($link, 'getNameOverride') ? $link->getNameOverride() : null;
|
|
$reference = method_exists($link, 'getReferenceOverride') ? $link->getReferenceOverride() : null;
|
|
$prix = method_exists($link, 'getPrixOverride') ? $link->getPrixOverride() : null;
|
|
|
|
if (null === $name && null === $reference && null === $prix) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'name' => $name,
|
|
'reference' => $reference,
|
|
'prix' => $prix,
|
|
];
|
|
}
|
|
|
|
private function indexById(array $links): array
|
|
{
|
|
$indexed = [];
|
|
foreach ($links as $link) {
|
|
if (is_array($link) && isset($link['id'])) {
|
|
$indexed[$link['id']] = $link;
|
|
}
|
|
}
|
|
|
|
return $indexed;
|
|
}
|
|
}
|