This repository was archived by the owner on Nov 12, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathToolPass.php
More file actions
116 lines (99 loc) · 4.18 KB
/
Copy pathToolPass.php
File metadata and controls
116 lines (99 loc) · 4.18 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
<?php
declare(strict_types=1);
namespace Ecourty\McpServerBundle\DependencyInjection\CompilerPass;
use Ecourty\McpServerBundle\Attribute\AsTool;
use Ecourty\McpServerBundle\IO\ToolResult;
use Ecourty\McpServerBundle\Service\SchemaExtractor;
use Ecourty\McpServerBundle\Service\ToolRegistry;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* ToolPass is a compiler pass that processes MCP server tools.
*
* It validates the class, method signatures, and extracts tool metadata.
*/
class ToolPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
$schemaExtractor = new SchemaExtractor();
$toolRegistry = $container->getDefinition(ToolRegistry::class);
foreach ($container->getDefinitions() as $definition) {
$class = $definition->getClass();
try {
if ($class === null || class_exists($class) === false) {
continue;
}
} catch (\Throwable) {
continue;
}
$refClass = new \ReflectionClass($class);
$attributes = $refClass->getAttributes(AsTool::class);
if (\count($attributes) === 0) {
continue;
}
if (\count($attributes) > 1) {
throw new \LogicException(\sprintf(
'Multiple AsTool attributes found on class "%s". Only one is allowed.',
$class,
));
}
/** @var AsTool|null $asTool */
$asTool = $attributes[0]->newInstance();
if ($asTool === null) {
throw new \LogicException(\sprintf(
'Failed to instantiate AsTool attribute for class "%s".',
$class,
));
}
if (method_exists($class, '__invoke') === false) {
throw new \LogicException(\sprintf(
'Class "%s" must implement the __invoke method to be used as a tool.',
$class,
));
}
$invokeMethodParameters = $refClass->getMethod('__invoke')->getParameters();
if (\count($invokeMethodParameters) > 1) {
throw new \LogicException(\sprintf(
'Class "%s" must have a maximum of 1 parameter in the __invoke method to be used as a tool.',
$class,
));
}
$invokeMethodParameter = $invokeMethodParameters[0] ?? null;
if (
$invokeMethodParameter !== null
&& (
$invokeMethodParameter->getType() === null
|| $invokeMethodParameter->getType()->isBuiltin() // @phpstan-ignore method.notFound
)
) {
throw new \LogicException(\sprintf(
'The parameter of the __invoke method in class "%s" must be a non-builtin type.',
$class,
));
}
$returnType = $refClass->getMethod('__invoke')->getReturnType();
if ($returnType?->getName() !== ToolResult::class) { // @phpstan-ignore method.notFound
throw new \LogicException(\sprintf(
'The __invoke method in class "%s" must return an instance of %s.',
$class,
ToolResult::class,
));
}
$inputSchemaClassName = $invokeMethodParameter?->getType()?->getName(); // @phpstan-ignore method.notFound
$inputSchema = $invokeMethodParameter === null
? []
: $schemaExtractor->extract($inputSchemaClassName);
$definition->addTag(name: 'mcp_server.tool', attributes: [
'name' => $asTool->name,
]);
$toolRegistry->addMethodCall(method: 'addToolDefinition', arguments: [
$asTool->name,
$asTool->description,
$inputSchema,
$inputSchemaClassName,
$asTool->getToolAnnotations()->asArray(),
]);
}
}
}