Skip to content

Commit 9f166ae

Browse files
committed
refactor: registry loader
1 parent 54a5d14 commit 9f166ae

File tree

4 files changed

+379
-262
lines changed

4 files changed

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

0 commit comments

Comments
 (0)