Skip to content

Commit 75ef304

Browse files
committed
Added basic MCP tools to search for parts and get part details
1 parent 7858b95 commit 75ef304

4 files changed

Lines changed: 198 additions & 0 deletions

File tree

src/Entity/Parts/Part.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,16 @@
3232
use ApiPlatform\Metadata\Delete;
3333
use ApiPlatform\Metadata\Get;
3434
use ApiPlatform\Metadata\GetCollection;
35+
use ApiPlatform\Metadata\McpTool;
36+
use ApiPlatform\Metadata\McpToolCollection;
3537
use ApiPlatform\Metadata\Patch;
3638
use ApiPlatform\Metadata\Post;
3739
use ApiPlatform\Serializer\Filter\PropertyFilter;
3840
use App\ApiPlatform\Filter\EntityFilter;
3941
use App\ApiPlatform\Filter\LikeFilter;
4042
use App\ApiPlatform\Filter\PartStoragelocationFilter;
4143
use App\ApiPlatform\Filter\TagFilter;
44+
use App\DataTables\Filters\PartSearchFilter;
4245
use App\Entity\Attachments\Attachment;
4346
use App\Entity\Attachments\AttachmentContainingDBElement;
4447
use App\Entity\Attachments\PartAttachment;
@@ -55,7 +58,10 @@
5558
use App\Entity\Parts\PartTraits\OrderTrait;
5659
use App\Entity\Parts\PartTraits\ProjectTrait;
5760
use App\EntityListeners\TreeCacheInvalidationListener;
61+
use App\Mcp\DTO\ElementByIdInput;
5862
use App\Repository\PartRepository;
63+
use App\State\Mcp\GetPartByIdProcessor;
64+
use App\State\Mcp\SearchPartsProcessor;
5965
use App\Validator\Constraints\UniqueObjectCollection;
6066
use Doctrine\Common\Collections\ArrayCollection;
6167
use Doctrine\Common\Collections\Collection;
@@ -104,6 +110,23 @@
104110
],
105111
normalizationContext: ['groups' => ['part:read', 'provider_reference:read', 'api:basic:read', 'part_lot:read'], 'openapi_definition_name' => 'Read'],
106112
denormalizationContext: ['groups' => ['part:write', 'api:basic:write', 'eda_info:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
113+
mcp: [
114+
'search_parts' => new McpToolCollection(
115+
title: "Search parts by keyword",
116+
description: 'Search for parts',
117+
annotations: ['readOnlyHint' => true, 'destructiveHint' => false, 'idempotentHint' => true, 'openWorldHint' => false],
118+
input: PartSearchFilter::class,
119+
processor: SearchPartsProcessor::class,
120+
),
121+
'get_part_details' => new McpTool(
122+
title: 'Get part details by ID',
123+
description: 'Get detailed information about a specific part by its database ID',
124+
annotations: ['readOnlyHint' => true, 'destructiveHint' => false, 'idempotentHint' => true, 'openWorldHint' => false],
125+
normalizationContext: ['groups' => ['part:read', 'provider_reference:read', 'api:basic:read', 'part_lot:read', 'orderdetail:read', 'pricedetail:read', 'parameter:read', 'attachment:read', 'eda_info:read']],
126+
input: ElementByIdInput::class,
127+
processor: GetPartByIdProcessor::class
128+
),
129+
],
107130
)]
108131
#[ApiFilter(PropertyFilter::class)]
109132
#[ApiFilter(EntityFilter::class, properties: ["category", "footprint", "manufacturer", "partUnit", "partCustomState"])]

src/Mcp/DTO/ElementByIdInput.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
/*
3+
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
4+
*
5+
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
6+
*
7+
* This program is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU Affero General Public License as published
9+
* by the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU Affero General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU Affero General Public License
18+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
declare(strict_types=1);
22+
23+
namespace App\Mcp\DTO;
24+
25+
use Symfony\Component\Validator\Constraints as Assert;
26+
27+
readonly class ElementByIdInput
28+
{
29+
public function __construct(
30+
#[Assert\NotNull]
31+
#[Assert\Positive]
32+
public int $id,
33+
) {
34+
}
35+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
/*
3+
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
4+
*
5+
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
6+
*
7+
* This program is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU Affero General Public License as published
9+
* by the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU Affero General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU Affero General Public License
18+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
declare(strict_types=1);
22+
23+
namespace App\State\Mcp;
24+
25+
use ApiPlatform\Metadata\Operation;
26+
use ApiPlatform\State\ProcessorInterface;
27+
use App\Entity\Parts\Part;
28+
use App\Mcp\DTO\ElementByIdInput;
29+
use Doctrine\ORM\EntityManagerInterface;
30+
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
31+
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
32+
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
33+
34+
class GetPartByIdProcessor implements ProcessorInterface
35+
{
36+
public function __construct(
37+
private readonly EntityManagerInterface $entityManager,
38+
private readonly AuthorizationCheckerInterface $authorizationChecker,
39+
) {
40+
}
41+
42+
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
43+
{
44+
if (!$data instanceof ElementByIdInput) {
45+
throw new \InvalidArgumentException('Expected PartByIdInput');
46+
}
47+
48+
$part = $this->entityManager->find(Part::class, $data->id);
49+
50+
if (!$part instanceof Part) {
51+
throw new NotFoundHttpException(sprintf('Part with id %d not found', $data->id));
52+
}
53+
54+
if (!$this->authorizationChecker->isGranted('read', $part)) {
55+
throw new AccessDeniedException(sprintf('Access denied to part with id %d', $data->id));
56+
}
57+
58+
return $part;
59+
}
60+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
/*
3+
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
4+
*
5+
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
6+
*
7+
* This program is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU Affero General Public License as published
9+
* by the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU Affero General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU Affero General Public License
18+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
declare(strict_types=1);
22+
23+
24+
namespace App\State\Mcp;
25+
26+
use ApiPlatform\Metadata\Operation;
27+
use ApiPlatform\State\ProcessorInterface;
28+
use App\DataTables\Filters\PartSearchFilter;
29+
use App\Entity\Parts\Part;
30+
use Doctrine\ORM\EntityManagerInterface;
31+
use Doctrine\ORM\QueryBuilder;
32+
33+
class SearchPartsProcessor implements ProcessorInterface
34+
{
35+
36+
public function __construct(
37+
private readonly EntityManagerInterface $entityManager,
38+
) {
39+
40+
}
41+
42+
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
43+
{
44+
if (!$data instanceof PartSearchFilter) {
45+
return [];
46+
}
47+
48+
$qb = $this->entityManager->getRepository(Part::class)->createQueryBuilder('part');
49+
50+
$data->apply($qb);
51+
$this->addJoins($qb);
52+
53+
$qb->addGroupBy('part');
54+
55+
return $qb->getQuery()->getResult();
56+
}
57+
58+
private function addJoins(QueryBuilder $qb): void
59+
{
60+
$dql = $qb->getDQL();
61+
62+
if (str_contains($dql, '_category')) {
63+
$qb->leftJoin('part.category', '_category');
64+
}
65+
if (str_contains($dql, '_storelocations')) {
66+
$qb->leftJoin('part.partLots', '_partLots');
67+
$qb->leftJoin('_partLots.storage_location', '_storelocations');
68+
}
69+
if (str_contains($dql, '_orderdetails') || str_contains($dql, '_suppliers')) {
70+
$qb->leftJoin('part.orderdetails', '_orderdetails');
71+
$qb->leftJoin('_orderdetails.supplier', '_suppliers');
72+
}
73+
if (str_contains($dql, '_manufacturer')) {
74+
$qb->leftJoin('part.manufacturer', '_manufacturer');
75+
}
76+
if (str_contains($dql, '_footprint')) {
77+
$qb->leftJoin('part.footprint', '_footprint');
78+
}
79+
}
80+
}

0 commit comments

Comments
 (0)