Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.62% covered (success)
97.62%
164 / 168
77.78% covered (warning)
77.78%
7 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
ToolFactory
97.62% covered (success)
97.62%
164 / 168
77.78% covered (warning)
77.78%
7 / 9
21
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createList
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
5
 findByName
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 createObjectMethodCallTool
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
1
 createCreateObjectTool
86.96% covered (warning)
86.96%
20 / 23
0.00% covered (danger)
0.00%
0 / 1
4.04
 createModifyObjectTool
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
4
 createRemoveObjectTool
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
1
 createGetObjectTool
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
1
 createListObjectTool
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2namespace Apie\McpServer\Tool;
3
4use Apie\Common\ActionDefinitionProvider;
5use Apie\Common\ActionDefinitions\CreateResourceActionDefinition;
6use Apie\Common\ActionDefinitions\GetResourceActionDefinition;
7use Apie\Common\ActionDefinitions\GetResourceListActionDefinition;
8use Apie\Common\ActionDefinitions\ModifyResourceActionDefinition;
9use Apie\Common\ActionDefinitions\RemoveResourceActionDefinition;
10use Apie\Common\ActionDefinitions\ReplaceResourceActionDefinition;
11use Apie\Common\ActionDefinitions\RunResourceMethodDefinition;
12use Apie\Common\Actions\CreateObjectAction;
13use Apie\Common\Actions\GetItemAction;
14use Apie\Common\Actions\GetListAction;
15use Apie\Common\Actions\ModifyObjectAction;
16use Apie\Common\Actions\RemoveObjectAction;
17use Apie\Common\Actions\RunItemMethodAction;
18use Apie\Core\BoundedContext\BoundedContext;
19use Apie\Core\BoundedContext\BoundedContextHashmap;
20use Apie\Core\Context\ApieContext;
21use Apie\Core\ContextBuilders\ContextBuilderFactory;
22use Apie\Core\ContextConstants;
23use Apie\Core\Identifiers\KebabCaseSlug;
24use Apie\Core\Metadata\MetadataFactory;
25use Apie\SchemaGenerator\SchemaGenerator;
26use Mcp\Types\ListToolsResult;
27use Mcp\Types\Tool;
28use Mcp\Types\ToolInputSchema;
29
30class 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}