Skip to content

Commit e4a82f7

Browse files
authored
refactor: registry loader (#111)
1 parent d347c84 commit e4a82f7

File tree

4 files changed

+384
-262
lines changed

4 files changed

+384
-262
lines changed
Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
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+
namespace Mcp\Capability\Registry\Loader;
13+
14+
use Mcp\Capability\Attribute\CompletionProvider;
15+
use Mcp\Capability\Completion\EnumCompletionProvider;
16+
use Mcp\Capability\Completion\ListCompletionProvider;
17+
use Mcp\Capability\Completion\ProviderInterface;
18+
use Mcp\Capability\Discovery\DocBlockParser;
19+
use Mcp\Capability\Discovery\HandlerResolver;
20+
use Mcp\Capability\Discovery\SchemaGenerator;
21+
use Mcp\Capability\Registry\ElementReference;
22+
use Mcp\Capability\Registry\ReferenceRegistryInterface;
23+
use Mcp\Exception\ConfigurationException;
24+
use Mcp\Schema\Annotations;
25+
use Mcp\Schema\Prompt;
26+
use Mcp\Schema\PromptArgument;
27+
use Mcp\Schema\Resource;
28+
use Mcp\Schema\ResourceTemplate;
29+
use Mcp\Schema\Tool;
30+
use Mcp\Schema\ToolAnnotations;
31+
use Mcp\Server\Handler;
32+
use Psr\Log\LoggerInterface;
33+
use Psr\Log\NullLogger;
34+
35+
/**
36+
* @author Antoine Bluchet <soyuka@gmail.com>
37+
*
38+
* @phpstan-import-type Handler from ElementReference
39+
*/
40+
final class ArrayLoader implements LoaderInterface
41+
{
42+
/**
43+
* @param array{
44+
* handler: Handler,
45+
* name: ?string,
46+
* description: ?string,
47+
* annotations: ?ToolAnnotations,
48+
* }[] $tools
49+
* @param array{
50+
* handler: Handler,
51+
* uri: string,
52+
* name: ?string,
53+
* description: ?string,
54+
* mimeType: ?string,
55+
* size: int|null,
56+
* annotations: ?Annotations,
57+
* }[] $resources
58+
* @param array{
59+
* handler: Handler,
60+
* uriTemplate: string,
61+
* name: ?string,
62+
* description: ?string,
63+
* mimeType: ?string,
64+
* annotations: ?Annotations,
65+
* }[] $resourceTemplates
66+
* @param array{
67+
* handler: Handler,
68+
* name: ?string,
69+
* description: ?string,
70+
* }[] $prompts
71+
*/
72+
public function __construct(
73+
private array $tools = [],
74+
private array $resources = [],
75+
private array $resourceTemplates = [],
76+
private array $prompts = [],
77+
private LoggerInterface $logger = new NullLogger(),
78+
) {
79+
}
80+
81+
public function load(ReferenceRegistryInterface $registry): void
82+
{
83+
$docBlockParser = new DocBlockParser(logger: $this->logger);
84+
$schemaGenerator = new SchemaGenerator($docBlockParser);
85+
86+
// Register Tools
87+
foreach ($this->tools as $data) {
88+
try {
89+
$reflection = HandlerResolver::resolve($data['handler']);
90+
91+
if ($reflection instanceof \ReflectionFunction) {
92+
$name = $data['name'] ?? 'closure_tool_'.spl_object_id($data['handler']);
93+
$description = $data['description'] ?? null;
94+
} else {
95+
$classShortName = $reflection->getDeclaringClass()->getShortName();
96+
$methodName = $reflection->getName();
97+
$docBlock = $docBlockParser->parseDocBlock($reflection->getDocComment() ?? null);
98+
99+
$name = $data['name'] ?? ('__invoke' === $methodName ? $classShortName : $methodName);
100+
$description = $data['description'] ?? $docBlockParser->getSummary($docBlock) ?? null;
101+
}
102+
103+
$inputSchema = $data['inputSchema'] ?? $schemaGenerator->generate($reflection);
104+
105+
$tool = new Tool($name, $inputSchema, $description, $data['annotations']);
106+
$registry->registerTool($tool, $data['handler'], true);
107+
108+
$handlerDesc = $this->getHandlerDescription($data['handler']);
109+
$this->logger->debug("Registered manual tool {$name} from handler {$handlerDesc}");
110+
} catch (\Throwable $e) {
111+
$this->logger->error(
112+
'Failed to register manual tool',
113+
['handler' => $data['handler'], 'name' => $data['name'], 'exception' => $e],
114+
);
115+
throw new ConfigurationException("Error registering manual tool '{$data['name']}': {$e->getMessage()}", 0, $e);
116+
}
117+
}
118+
119+
// Register Resources
120+
foreach ($this->resources as $data) {
121+
try {
122+
$reflection = HandlerResolver::resolve($data['handler']);
123+
124+
if ($reflection instanceof \ReflectionFunction) {
125+
$name = $data['name'] ?? 'closure_resource_'.spl_object_id($data['handler']);
126+
$description = $data['description'] ?? null;
127+
} else {
128+
$classShortName = $reflection->getDeclaringClass()->getShortName();
129+
$methodName = $reflection->getName();
130+
$docBlock = $docBlockParser->parseDocBlock($reflection->getDocComment() ?? null);
131+
132+
$name = $data['name'] ?? ('__invoke' === $methodName ? $classShortName : $methodName);
133+
$description = $data['description'] ?? $docBlockParser->getSummary($docBlock) ?? null;
134+
}
135+
136+
$uri = $data['uri'];
137+
$mimeType = $data['mimeType'];
138+
$size = $data['size'];
139+
$annotations = $data['annotations'];
140+
141+
$resource = new Resource($uri, $name, $description, $mimeType, $annotations, $size);
142+
$registry->registerResource($resource, $data['handler'], true);
143+
144+
$handlerDesc = $this->getHandlerDescription($data['handler']);
145+
$this->logger->debug("Registered manual resource {$name} from handler {$handlerDesc}");
146+
} catch (\Throwable $e) {
147+
$this->logger->error(
148+
'Failed to register manual resource',
149+
['handler' => $data['handler'], 'uri' => $data['uri'], 'exception' => $e],
150+
);
151+
throw new ConfigurationException("Error registering manual resource '{$data['uri']}': {$e->getMessage()}", 0, $e);
152+
}
153+
}
154+
155+
// Register Templates
156+
foreach ($this->resourceTemplates as $data) {
157+
try {
158+
$reflection = HandlerResolver::resolve($data['handler']);
159+
160+
if ($reflection instanceof \ReflectionFunction) {
161+
$name = $data['name'] ?? 'closure_template_'.spl_object_id($data['handler']);
162+
$description = $data['description'] ?? null;
163+
} else {
164+
$classShortName = $reflection->getDeclaringClass()->getShortName();
165+
$methodName = $reflection->getName();
166+
$docBlock = $docBlockParser->parseDocBlock($reflection->getDocComment() ?? null);
167+
168+
$name = $data['name'] ?? ('__invoke' === $methodName ? $classShortName : $methodName);
169+
$description = $data['description'] ?? $docBlockParser->getSummary($docBlock) ?? null;
170+
}
171+
172+
$uriTemplate = $data['uriTemplate'];
173+
$mimeType = $data['mimeType'];
174+
$annotations = $data['annotations'];
175+
176+
$template = new ResourceTemplate($uriTemplate, $name, $description, $mimeType, $annotations);
177+
$completionProviders = $this->getCompletionProviders($reflection);
178+
$registry->registerResourceTemplate($template, $data['handler'], $completionProviders, true);
179+
180+
$handlerDesc = $this->getHandlerDescription($data['handler']);
181+
$this->logger->debug("Registered manual template {$name} from handler {$handlerDesc}");
182+
} catch (\Throwable $e) {
183+
$this->logger->error(
184+
'Failed to register manual template',
185+
['handler' => $data['handler'], 'uriTemplate' => $data['uriTemplate'], 'exception' => $e],
186+
);
187+
throw new ConfigurationException("Error registering manual resource template '{$data['uriTemplate']}': {$e->getMessage()}", 0, $e);
188+
}
189+
}
190+
191+
// Register Prompts
192+
foreach ($this->prompts as $data) {
193+
try {
194+
$reflection = HandlerResolver::resolve($data['handler']);
195+
196+
if ($reflection instanceof \ReflectionFunction) {
197+
$name = $data['name'] ?? 'closure_prompt_'.spl_object_id($data['handler']);
198+
$description = $data['description'] ?? null;
199+
} else {
200+
$classShortName = $reflection->getDeclaringClass()->getShortName();
201+
$methodName = $reflection->getName();
202+
$docBlock = $docBlockParser->parseDocBlock($reflection->getDocComment() ?? null);
203+
204+
$name = $data['name'] ?? ('__invoke' === $methodName ? $classShortName : $methodName);
205+
$description = $data['description'] ?? $docBlockParser->getSummary($docBlock) ?? null;
206+
}
207+
208+
$arguments = [];
209+
$paramTags = $reflection instanceof \ReflectionMethod ? $docBlockParser->getParamTags(
210+
$docBlockParser->parseDocBlock($reflection->getDocComment() ?? null),
211+
) : [];
212+
foreach ($reflection->getParameters() as $param) {
213+
$reflectionType = $param->getType();
214+
215+
// Basic DI check (heuristic)
216+
if ($reflectionType instanceof \ReflectionNamedType && !$reflectionType->isBuiltin()) {
217+
continue;
218+
}
219+
220+
$paramTag = $paramTags['$'.$param->getName()] ?? null;
221+
$arguments[] = new PromptArgument(
222+
$param->getName(),
223+
$paramTag ? trim((string) $paramTag->getDescription()) : null,
224+
!$param->isOptional() && !$param->isDefaultValueAvailable(),
225+
);
226+
}
227+
228+
$prompt = new Prompt($name, $description, $arguments);
229+
$completionProviders = $this->getCompletionProviders($reflection);
230+
$registry->registerPrompt($prompt, $data['handler'], $completionProviders, true);
231+
232+
$handlerDesc = $this->getHandlerDescription($data['handler']);
233+
$this->logger->debug("Registered manual prompt {$name} from handler {$handlerDesc}");
234+
} catch (\Throwable $e) {
235+
$this->logger->error(
236+
'Failed to register manual prompt',
237+
['handler' => $data['handler'], 'name' => $data['name'], 'exception' => $e],
238+
);
239+
throw new ConfigurationException("Error registering manual prompt '{$data['name']}': {$e->getMessage()}", 0, $e);
240+
}
241+
}
242+
243+
$this->logger->debug('Manual element registration complete.');
244+
}
245+
246+
/**
247+
* @param Handler $handler
248+
*/
249+
private function getHandlerDescription(\Closure|array|string $handler): string
250+
{
251+
if ($handler instanceof \Closure) {
252+
return 'Closure';
253+
}
254+
255+
if (\is_array($handler)) {
256+
return \sprintf(
257+
'%s::%s',
258+
\is_object($handler[0]) ? $handler[0]::class : $handler[0],
259+
$handler[1],
260+
);
261+
}
262+
263+
return (string) $handler;
264+
}
265+
266+
/**
267+
* @return array<string, ProviderInterface>
268+
*/
269+
private function getCompletionProviders(\ReflectionMethod|\ReflectionFunction $reflection): array
270+
{
271+
$completionProviders = [];
272+
foreach ($reflection->getParameters() as $param) {
273+
$reflectionType = $param->getType();
274+
if ($reflectionType instanceof \ReflectionNamedType && !$reflectionType->isBuiltin()) {
275+
continue;
276+
}
277+
278+
$completionAttributes = $param->getAttributes(
279+
CompletionProvider::class,
280+
\ReflectionAttribute::IS_INSTANCEOF,
281+
);
282+
if (!empty($completionAttributes)) {
283+
$attributeInstance = $completionAttributes[0]->newInstance();
284+
285+
if ($attributeInstance->provider) {
286+
$completionProviders[$param->getName()] = $attributeInstance->provider;
287+
} elseif ($attributeInstance->providerClass) {
288+
$completionProviders[$param->getName()] = $attributeInstance->providerClass;
289+
} elseif ($attributeInstance->values) {
290+
$completionProviders[$param->getName()] = new ListCompletionProvider($attributeInstance->values);
291+
} elseif ($attributeInstance->enum) {
292+
$completionProviders[$param->getName()] = new EnumCompletionProvider($attributeInstance->enum);
293+
}
294+
}
295+
}
296+
297+
return $completionProviders;
298+
}
299+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
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+
namespace Mcp\Capability\Registry\Loader;
13+
14+
use Mcp\Capability\Discovery\CachedDiscoverer;
15+
use Mcp\Capability\Discovery\Discoverer;
16+
use Mcp\Capability\Registry\ReferenceRegistryInterface;
17+
use Psr\Log\LoggerInterface;
18+
use Psr\SimpleCache\CacheInterface;
19+
20+
/**
21+
* @author Antoine Bluchet <soyuka@gmail.com>
22+
*/
23+
final class DiscoveryLoader implements LoaderInterface
24+
{
25+
/**
26+
* @param string[] $scanDirs
27+
* @param array|string[] $excludeDirs
28+
*/
29+
public function __construct(
30+
private string $basePath,
31+
private array $scanDirs,
32+
private array $excludeDirs,
33+
private LoggerInterface $logger,
34+
private ?CacheInterface $cache = null,
35+
) {
36+
}
37+
38+
public function load(ReferenceRegistryInterface $registry): void
39+
{
40+
// This now encapsulates the discovery process
41+
$discoverer = new Discoverer($registry, $this->logger);
42+
43+
$cachedDiscoverer = $this->cache
44+
? new CachedDiscoverer($discoverer, $this->cache, $this->logger)
45+
: $discoverer;
46+
47+
$cachedDiscoverer->discover($this->basePath, $this->scanDirs, $this->excludeDirs);
48+
}
49+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
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+
namespace Mcp\Capability\Registry\Loader;
13+
14+
use Mcp\Capability\Registry\ReferenceRegistryInterface;
15+
16+
/**
17+
* @author Antoine Bluchet <soyuka@gmail.com>
18+
*/
19+
interface LoaderInterface
20+
{
21+
public function load(ReferenceRegistryInterface $registry): void;
22+
}

0 commit comments

Comments
 (0)