Skip to content

Commit ed0ae92

Browse files
feat: add support of collection to MCP (#7724)
* feat: add support of collection to MCP * feat: add test of MCP * feat: add an output DTO to MCP * feat: use a DTO processor in tests
1 parent e938e7b commit ed0ae92

File tree

13 files changed

+450
-8
lines changed

13 files changed

+450
-8
lines changed

src/Mcp/Capability/Registry/Loader.php

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,22 @@ public function load(RegistryInterface $registry): void
4747
foreach ($resource->getMcp() ?? [] as $mcp) {
4848
if ($mcp instanceof McpTool) {
4949
$inputClass = $mcp->getInput()['class'] ?? $mcp->getClass();
50-
$schema = $this->schemaFactory->buildSchema($inputClass, 'json', Schema::TYPE_INPUT, $mcp, null, [SchemaFactory::FORCE_SUBSCHEMA => true]);
51-
$outputSchema = $this->schemaFactory->buildSchema($inputClass, 'json', Schema::TYPE_OUTPUT, $mcp, null, [SchemaFactory::FORCE_SUBSCHEMA => true]);
50+
$inputFormat = array_first($mcp->getInputFormats() ?? ['json']);
51+
$inputSchema = $this->schemaFactory->buildSchema($inputClass, $inputFormat, Schema::TYPE_INPUT, $mcp, null, [SchemaFactory::FORCE_SUBSCHEMA => true]);
52+
53+
$outputClass = $mcp->getOutput()['class'] ?? $mcp->getClass();
54+
$outputFormat = array_first($mcp->getOutputFormats() ?? ['jsonld']);
55+
$outputSchema = $this->schemaFactory->buildSchema($outputClass, $outputFormat, Schema::TYPE_OUTPUT, $mcp, null, [SchemaFactory::FORCE_SUBSCHEMA => true]);
56+
5257
$registry->registerTool(
5358
new Tool(
5459
name: $mcp->getName(),
55-
inputSchema: $schema->getDefinitions()[$schema->getRootDefinitionKey()]->getArrayCopy(),
60+
inputSchema: $inputSchema->getDefinitions()[$inputSchema->getRootDefinitionKey()]->getArrayCopy(),
5661
description: $mcp->getDescription(),
5762
annotations: $mcp->getAnnotations() ? ToolAnnotations::fromArray($mcp->getAnnotations()) : null,
5863
icons: $mcp->getIcons(),
5964
meta: $mcp->getMeta(),
60-
outputSchema: $outputSchema->getDefinitions()[$outputSchema->getRootDefinitionKey()]->getArrayCopy(),
65+
outputSchema: $outputSchema->getArrayCopy(),
6166
),
6267
self::HANDLER,
6368
true,

src/Mcp/State/StructuredContentProcessor.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public function process(mixed $data, Operation $operation, array $uriVariables =
6363
'operation' => $operation,
6464
]);
6565
$serializerContext['uri_variables'] = $uriVariables;
66-
$format = $request->getRequestFormat('') ?: 'json';
66+
$format = $request->getRequestFormat('') ?: 'jsonld';
6767
$structuredContent = $this->serializer->normalize($result, $format, $serializerContext);
6868
$result = $this->serializer->encode($structuredContent, $format, $serializerContext);
6969
}

src/Mcp/composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
"php": ">=8.2",
3131
"api-platform/metadata": "^4.2",
3232
"api-platform/json-schema": "^4.2",
33-
"mcp/sdk": "^0.3.0"
33+
"mcp/sdk": "^0.3.0",
34+
"symfony/polyfill-php85": "^1.32"
3435
},
3536
"autoload": {
3637
"psr-4": {

src/Metadata/McpTool.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
use Symfony\Component\WebLink\Link as WebLink;
2121

2222
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
23-
final class McpTool extends HttpOperation
23+
class McpTool extends HttpOperation
2424
{
2525
/**
2626
* @param string|null $name The name of the tool (defaults to the method name)

src/Metadata/McpToolCollection.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Metadata;
15+
16+
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
17+
final class McpToolCollection extends McpTool implements CollectionOperationInterface
18+
{
19+
}

src/Metadata/Resource/Factory/InputOutputResourceMetadataCollectionFactory.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ public function create(string $resourceClass): ResourceMetadataCollection
4848
$resourceMetadata = $resourceMetadata->withGraphQlOperations($this->getTransformedOperations($resourceMetadata->getGraphQlOperations(), $resourceMetadata));
4949
}
5050

51+
if ($resourceMetadata->getMcp()) {
52+
$resourceMetadata = $resourceMetadata->withMcp($this->getTransformedOperations($resourceMetadata->getMcp(), $resourceMetadata));
53+
}
54+
5155
$resourceMetadataCollection[$key] = $resourceMetadata;
5256
}
5357

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Fixtures\TestBundle\Dto;
15+
16+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\McpBook as McpBookEntity;
17+
use Symfony\Component\ObjectMapper\Attribute\Map;
18+
19+
#[Map(source: McpBookEntity::class)]
20+
final class McpBookOutputDto
21+
{
22+
public int $id;
23+
24+
public string $name;
25+
26+
public string $isbn;
27+
28+
public static function fromMcpBook(McpBookEntity $mcpBook): self
29+
{
30+
$mcpBookOutputDto = new self();
31+
$mcpBookOutputDto->id = $mcpBook->getId();
32+
$mcpBookOutputDto->name = $mcpBook->getTitle();
33+
$mcpBookOutputDto->isbn = $mcpBook->getIsbn();
34+
35+
return $mcpBookOutputDto;
36+
}
37+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Fixtures\TestBundle\Dto;
15+
16+
final class SearchDto
17+
{
18+
public string $search;
19+
}

tests/Fixtures/TestBundle/Entity/McpBook.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515

1616
use ApiPlatform\Metadata\ApiResource;
1717
use ApiPlatform\Metadata\McpTool;
18+
use ApiPlatform\Metadata\McpToolCollection;
19+
use ApiPlatform\Tests\Fixtures\TestBundle\Dto\McpBookOutputDto;
20+
use ApiPlatform\Tests\Fixtures\TestBundle\Dto\SearchDto;
21+
use ApiPlatform\Tests\Fixtures\TestBundle\State\McpBookListDtoProcessor;
22+
use ApiPlatform\Tests\Fixtures\TestBundle\State\McpBookListProcessor;
1823
use Doctrine\ORM\Mapping as ORM;
1924

2025
#[ApiResource(
@@ -27,6 +32,19 @@
2732
'update_book_status' => new McpTool(
2833
processor: [self::class, 'process']
2934
),
35+
'list_books' => new McpToolCollection(
36+
description: 'List Books',
37+
input: SearchDto::class,
38+
processor: McpBookListProcessor::class,
39+
structuredContent: true,
40+
),
41+
'list_books_dto' => new McpTool(
42+
description: 'List Books and return a DTO',
43+
input: SearchDto::class,
44+
output: McpBookOutputDto::class,
45+
processor: McpBookListDtoProcessor::class,
46+
structuredContent: true,
47+
),
3048
]
3149
)]
3250
#[ORM\Entity]
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Fixtures\TestBundle\State;
15+
16+
use ApiPlatform\Metadata\Operation;
17+
use ApiPlatform\State\ProcessorInterface;
18+
use ApiPlatform\Tests\Fixtures\TestBundle\Dto\McpBookOutputDto;
19+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\McpBook;
20+
use Doctrine\Persistence\ManagerRegistry;
21+
22+
class McpBookListDtoProcessor implements ProcessorInterface
23+
{
24+
public function __construct(private readonly ManagerRegistry $managerRegistry)
25+
{
26+
}
27+
28+
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): ?McpBookOutputDto
29+
{
30+
$search = $context['data']->search ?? null;
31+
32+
$mcpBookRepository = $this->managerRegistry->getRepository(McpBook::class);
33+
34+
$queryBuilder = $mcpBookRepository->createQueryBuilder('b');
35+
$queryBuilder
36+
->where($queryBuilder->expr()->like('b.title', ':title'))
37+
->setParameter(':title', '%'.$search.'%');
38+
39+
$book = $queryBuilder->getQuery()->getOneOrNullResult();
40+
41+
if ($book instanceof McpBook) {
42+
return McpBookOutputDto::fromMcpBook($book);
43+
}
44+
45+
return null;
46+
}
47+
}

0 commit comments

Comments
 (0)