Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
97.62% |
164 / 168 |
|
77.78% |
7 / 9 |
CRAP | |
0.00% |
0 / 1 |
ToolFactory | |
97.62% |
164 / 168 |
|
77.78% |
7 / 9 |
21 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
createList | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
5 | |||
findByName | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
3.07 | |||
createObjectMethodCallTool | |
100.00% |
24 / 24 |
|
100.00% |
1 / 1 |
1 | |||
createCreateObjectTool | |
86.96% |
20 / 23 |
|
0.00% |
0 / 1 |
4.04 | |||
createModifyObjectTool | |
100.00% |
26 / 26 |
|
100.00% |
1 / 1 |
4 | |||
createRemoveObjectTool | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
1 | |||
createGetObjectTool | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
1 | |||
createListObjectTool | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | namespace Apie\McpServer\Tool; |
3 | |
4 | use Apie\Common\ActionDefinitionProvider; |
5 | use Apie\Common\ActionDefinitions\CreateResourceActionDefinition; |
6 | use Apie\Common\ActionDefinitions\GetResourceActionDefinition; |
7 | use Apie\Common\ActionDefinitions\GetResourceListActionDefinition; |
8 | use Apie\Common\ActionDefinitions\ModifyResourceActionDefinition; |
9 | use Apie\Common\ActionDefinitions\RemoveResourceActionDefinition; |
10 | use Apie\Common\ActionDefinitions\ReplaceResourceActionDefinition; |
11 | use Apie\Common\ActionDefinitions\RunResourceMethodDefinition; |
12 | use Apie\Common\Actions\CreateObjectAction; |
13 | use Apie\Common\Actions\GetItemAction; |
14 | use Apie\Common\Actions\GetListAction; |
15 | use Apie\Common\Actions\ModifyObjectAction; |
16 | use Apie\Common\Actions\RemoveObjectAction; |
17 | use Apie\Common\Actions\RunItemMethodAction; |
18 | use Apie\Core\BoundedContext\BoundedContext; |
19 | use Apie\Core\BoundedContext\BoundedContextHashmap; |
20 | use Apie\Core\Context\ApieContext; |
21 | use Apie\Core\ContextBuilders\ContextBuilderFactory; |
22 | use Apie\Core\ContextConstants; |
23 | use Apie\Core\Identifiers\KebabCaseSlug; |
24 | use Apie\Core\Metadata\MetadataFactory; |
25 | use Apie\SchemaGenerator\SchemaGenerator; |
26 | use Mcp\Types\ListToolsResult; |
27 | use Mcp\Types\Tool; |
28 | use Mcp\Types\ToolInputSchema; |
29 | |
30 | class ToolFactory |
31 | { |
32 | private const MAPPER = [ |
33 | CreateResourceActionDefinition::class => 'createCreateObjectTool', |
34 | ReplaceResourceActionDefinition::class => 'createCreateObjectTool', |
35 | ModifyResourceActionDefinition::class => 'createModifyObjectTool', |
36 | GetResourceActionDefinition::class => 'createGetObjectTool', |
37 | GetResourceListActionDefinition::class => 'createListObjectTool', |
38 | RemoveResourceActionDefinition::class => 'createRemoveObjectTool', |
39 | RunResourceMethodDefinition::class => 'createObjectMethodCallTool', |
40 | ]; |
41 | |
42 | public function __construct( |
43 | private readonly ContextBuilderFactory $contextBuilder, |
44 | private readonly SchemaGenerator $schemaGenerator, |
45 | private readonly BoundedContextHashmap $boundedContextHashmap, |
46 | private readonly ActionDefinitionProvider $actionDefinitionProvider |
47 | ) { |
48 | } |
49 | |
50 | public function createList(): ListToolsResult |
51 | { |
52 | $context = $this->contextBuilder->createGeneralContext( |
53 | [ |
54 | ToolFactory::class => $this, |
55 | ContextConstants::MCP_SERVER => true, |
56 | SchemaGenerator::class => $this->schemaGenerator |
57 | ] |
58 | ); |
59 | $tools = []; |
60 | foreach ($this->boundedContextHashmap as $id => $boundedContext) { |
61 | $subcontext = $context |
62 | ->withContext( |
63 | ContextConstants::BOUNDED_CONTEXT_ID, |
64 | $id |
65 | ) |
66 | ->withContext( |
67 | BoundedContext::class, |
68 | $boundedContext |
69 | ); |
70 | foreach ($this->actionDefinitionProvider->provideActionDefinitions($boundedContext, $subcontext) as $routeDefinition) { |
71 | foreach (self::MAPPER as $className => $methodName) { |
72 | if (get_class($routeDefinition) === $className) { |
73 | $tools[] = $this->{$methodName}($routeDefinition); |
74 | } |
75 | } |
76 | } |
77 | } |
78 | return new ListToolsResult($tools); |
79 | } |
80 | |
81 | public function findByName(string $name): Tool |
82 | { |
83 | $all = $this->createList(); |
84 | foreach ($all->tools as $tool) { |
85 | if ($tool->name === $name) { |
86 | return $tool; |
87 | } |
88 | } |
89 | |
90 | throw new \LogicException('Tool "' . $name . '" not found!'); |
91 | } |
92 | |
93 | public function createObjectMethodCallTool( |
94 | RunResourceMethodDefinition $definition |
95 | ) { |
96 | $class = $definition->getResourceName(); |
97 | $method = $definition->getMethod(); |
98 | $name = 'run-object-' |
99 | . $definition->getBoundedContextId()->toNative() |
100 | . '-' |
101 | . KebabCaseSlug::fromClass($class) |
102 | . '-method-' |
103 | . KebabCaseSlug::fromClass($method); |
104 | $data = json_decode( |
105 | json_encode($this->schemaGenerator->createMethodSchema($method)->getSerializableData()), |
106 | true |
107 | ); |
108 | $tool = new Tool( |
109 | $name, |
110 | ToolInputSchema::fromArray( |
111 | $data |
112 | ), |
113 | RunItemMethodAction::getDescription($class, $method) |
114 | ); |
115 | $tool->{"x-definition"} = RunItemMethodAction::class; |
116 | $tool->{"x-method-class"} = $method->getDeclaringClass()->name; |
117 | $tool->{"x-method"} = $method->name; |
118 | $tool->{"x-fields"} = RunItemMethodAction::getRouteAttributes($class, $method); |
119 | |
120 | return $tool; |
121 | } |
122 | |
123 | public function createCreateObjectTool(CreateResourceActionDefinition|ReplaceResourceActionDefinition $definition): Tool |
124 | { |
125 | $class = $definition->getResourceName(); |
126 | $name = ($definition instanceof CreateResourceActionDefinition ? 'create-object-' : 'replace-object-') |
127 | . $definition->getBoundedContextId()->toNative() |
128 | . '-' |
129 | . KebabCaseSlug::fromClass($class); |
130 | $data = json_decode( |
131 | json_encode($this->schemaGenerator->createSchema($class->name)->getSerializableData()), |
132 | true |
133 | ); |
134 | if ($definition instanceof ReplaceResourceActionDefinition && !isset($data['properties']['id'])) { |
135 | $data['properties']['id'] = ['type' => 'string']; |
136 | $data['required'] ??= []; |
137 | $data['required'][] = 'id'; |
138 | } |
139 | $tool = new Tool( |
140 | $name, |
141 | ToolInputSchema::fromArray( |
142 | $data |
143 | ), |
144 | CreateObjectAction::getDescription($class) |
145 | ); |
146 | $tool->{"x-definition"} = CreateObjectAction::class; |
147 | $tool->{"x-fields"} = CreateObjectAction::getRouteAttributes($class); |
148 | |
149 | return $tool; |
150 | } |
151 | |
152 | public function createModifyObjectTool(ModifyResourceActionDefinition $definition): Tool |
153 | { |
154 | $class = $definition->getResourceName(); |
155 | $name = 'modify-object-' |
156 | . $definition->getBoundedContextId()->toNative() |
157 | . '-' |
158 | . KebabCaseSlug::fromClass($class); |
159 | $data = json_decode( |
160 | json_encode($this->schemaGenerator->createSchema($class->name)->getSerializableData()), |
161 | true |
162 | ); |
163 | $modifiableKeys = MetadataFactory::getModificationMetadata($class, new ApieContext())->getHashmap()->toArray(); |
164 | foreach ($data['properties'] ?? [] as $prop => $value) { |
165 | if (!isset($modifiableKeys[$prop])) { |
166 | unset($data['properties'][$prop]); |
167 | } |
168 | } |
169 | if (!isset($data['properties']['id'])) { |
170 | $data['properties']['id'] = ['type' => 'string']; |
171 | } |
172 | $data['required'] = ['id']; |
173 | $tool = new Tool( |
174 | $name, |
175 | ToolInputSchema::fromArray( |
176 | $data |
177 | ), |
178 | ModifyObjectAction::getDescription($class) |
179 | ); |
180 | $tool->{"x-definition"} = ModifyObjectAction::class; |
181 | $tool->{"x-fields"} = ModifyObjectAction::getRouteAttributes($class); |
182 | |
183 | return $tool; |
184 | } |
185 | |
186 | public function createRemoveObjectTool(RemoveResourceActionDefinition $definition): Tool |
187 | { |
188 | $class = $definition->getResourceName(); |
189 | $name = 'remove-object-' |
190 | . $definition->getBoundedContextId()->toNative() |
191 | . '-' |
192 | . KebabCaseSlug::fromClass($class); |
193 | $tool = new Tool( |
194 | $name, |
195 | ToolInputSchema::fromArray( |
196 | [ |
197 | 'type' => 'object', |
198 | 'properties' => [ |
199 | 'id' => [ |
200 | 'type' => 'string' |
201 | ], |
202 | ], |
203 | 'required' => ['id'] |
204 | ] |
205 | ), |
206 | RemoveObjectAction::getDescription($class) |
207 | ); |
208 | $tool->{"x-definition"} = RemoveObjectAction::class; |
209 | $tool->{"x-fields"} = RemoveObjectAction::getRouteAttributes($class); |
210 | |
211 | return $tool; |
212 | } |
213 | |
214 | public function createGetObjectTool(GetResourceActionDefinition $definition): Tool |
215 | { |
216 | $class = $definition->getResourceName(); |
217 | $name = 'get-object-' |
218 | . $definition->getBoundedContextId()->toNative() |
219 | . '-' |
220 | . KebabCaseSlug::fromClass($class); |
221 | $tool = new Tool( |
222 | $name, |
223 | ToolInputSchema::fromArray( |
224 | [ |
225 | 'type' => 'object', |
226 | 'properties' => [ |
227 | 'id' => [ |
228 | 'type' => 'string' |
229 | ], |
230 | ], |
231 | 'required' => ['id'] |
232 | ] |
233 | ), |
234 | GetItemAction::getDescription($class) |
235 | ); |
236 | $tool->{"x-definition"} = GetItemAction::class; |
237 | $tool->{"x-fields"} = GetItemAction::getRouteAttributes($class); |
238 | |
239 | return $tool; |
240 | } |
241 | |
242 | public function createListObjectTool(GetResourceListActionDefinition $definition): Tool |
243 | { |
244 | $class = $definition->getResourceName(); |
245 | $name = 'all-object-' |
246 | . $definition->getBoundedContextId()->toNative() |
247 | . '-' |
248 | . KebabCaseSlug::fromClass($class); |
249 | $tool = new Tool( |
250 | $name, |
251 | ToolInputSchema::fromArray( |
252 | [ |
253 | 'type' => 'object', |
254 | 'properties' => [ |
255 | ], |
256 | 'required' => [] |
257 | ] |
258 | ), |
259 | GetListAction::getDescription($class) |
260 | ); |
261 | $tool->{"x-definition"} = GetListAction::class; |
262 | $tool->{"x-fields"} = GetListAction::getRouteAttributes($class); |
263 | |
264 | return $tool; |
265 | } |
266 | } |