Skip to content

Commit abfeb2c

Browse files
refactor(core)!: Introduce RegisteredElement objects to encapsulate element logic
This commit introduces a new set of classes under the `PhpMcp\Server\Elements` namespace (`RegisteredElement`, `RegisteredTool`, `RegisteredResource`, `RegisteredPrompt`, `RegisteredResourceTemplate`). These classes now encapsulate: - The MCP schema DTO (e.g., `PhpMcp\Schema\Tool`). - The handler invocation logic (previously in `Support\Handler` and `Support\ArgumentPreparer`), including argument preparation and type casting, now part of the `RegisteredElement` base class. - Specific execution methods (e.g., `call()` on `RegisteredTool`, `read()` on `RegisteredResource`). - Result formatting logic (previously in `Traits\ResponseFormatter`) moved into the respective `Registered*` classes. - URI template matching logic is now self-contained within `RegisteredResourceTemplate`. - Completion provider storage is now part of `RegisteredTool`, `RegisteredPrompt`, and `RegisteredResourceTemplate`.
1 parent b51fe79 commit abfeb2c

33 files changed

+898
-2625
lines changed

examples/02-discovery-http-userprofile/server.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public function log($level, \Stringable|string $message, array $context = []): v
6767

6868
$server = Server::make()
6969
->withServerInfo('HTTP User Profiles', '1.0.0')
70-
->withCapabilities(ServerCapabilities::make(completionsEnabled: true))
70+
->withCapabilities(ServerCapabilities::make(completionsEnabled: true, loggingEnabled: true))
7171
->withLogger($logger)
7272
->withContainer($container)
7373
->build();

src/Dispatcher.php

Lines changed: 35 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,13 @@
3939
use PhpMcp\Server\Protocol;
4040
use PhpMcp\Server\Registry;
4141
use PhpMcp\Server\Session\SubscriptionManager;
42-
use PhpMcp\Server\Support\SchemaValidator;
43-
use PhpMcp\Server\Traits\ResponseFormatter;
42+
use PhpMcp\Server\Utils\SchemaValidator;
4443
use Psr\Container\ContainerInterface;
4544
use Psr\Log\LoggerInterface;
4645
use Throwable;
4746

4847
class Dispatcher
4948
{
50-
use ResponseFormatter;
51-
5249
protected ContainerInterface $container;
5350
protected LoggerInterface $logger;
5451

@@ -160,12 +157,12 @@ public function handleToolCall(CallToolRequest $request): CallToolResult
160157
$toolName = $request->name;
161158
$arguments = $request->arguments;
162159

163-
['tool' => $tool, 'handler' => $handler] = $this->registry->getTool($toolName);
164-
if (! $tool) {
160+
$registeredTool = $this->registry->getTool($toolName);
161+
if (! $registeredTool) {
165162
throw McpServerException::methodNotFound("Tool '{$toolName}' not found.");
166163
}
167164

168-
$inputSchema = $tool->inputSchema;
165+
$inputSchema = $registeredTool->schema->inputSchema;
169166

170167
$validationErrors = $this->schemaValidator->validateAgainstJsonSchema($arguments, $inputSchema);
171168

@@ -188,20 +185,19 @@ public function handleToolCall(CallToolRequest $request): CallToolResult
188185
}
189186

190187
try {
191-
$result = $handler->handle($this->container, $arguments);
192-
$formattedResult = $this->formatToolResult($result);
188+
$result = $registeredTool->call($this->container, $arguments);
193189

194-
return new CallToolResult($formattedResult, false);
190+
return new CallToolResult($result, false);
195191
} catch (JsonException $e) {
196-
$this->logger->warning('MCP SDK: Failed to JSON encode tool result.', ['tool' => $toolName, 'exception' => $e]);
192+
$this->logger->warning('Failed to JSON encode tool result.', ['tool' => $toolName, 'exception' => $e]);
197193
$errorMessage = "Failed to serialize tool result: {$e->getMessage()}";
198194

199195
return new CallToolResult([new TextContent($errorMessage)], true);
200196
} catch (Throwable $toolError) {
201-
$this->logger->error('MCP SDK: Tool execution failed.', ['tool' => $toolName, 'exception' => $toolError]);
202-
$errorContent = $this->formatToolErrorResult($toolError);
197+
$this->logger->error('Tool execution failed.', ['tool' => $toolName, 'exception' => $toolError]);
198+
$errorMessage = "Tool execution failed: {$toolError->getMessage()}";
203199

204-
return new CallToolResult($errorContent, true);
200+
return new CallToolResult([new TextContent($errorMessage)], true);
205201
}
206202
}
207203

@@ -231,25 +227,23 @@ public function handleResourceRead(ReadResourceRequest $request): ReadResourceRe
231227
{
232228
$uri = $request->uri;
233229

234-
['resource' => $resource, 'handler' => $handler, 'variables' => $uriVariables] = $this->registry->getResource($uri);
230+
$registeredResource = $this->registry->getResource($uri);
235231

236-
if (! $resource) {
232+
if (! $registeredResource) {
237233
throw McpServerException::invalidParams("Resource URI '{$uri}' not found.");
238234
}
239235

240236
try {
241-
$arguments = array_merge($uriVariables, ['uri' => $uri]);
242-
$result = $handler->handle($this->container, $arguments);
243-
$contents = $this->formatResourceContents($result, $uri, $resource->mimeType);
237+
$result = $registeredResource->read($this->container, $uri);
244238

245-
return new ReadResourceResult($contents);
239+
return new ReadResourceResult($result);
246240
} catch (JsonException $e) {
247-
$this->logger->warning('MCP SDK: Failed to JSON encode resource content.', ['exception' => $e, 'uri' => $uri]);
241+
$this->logger->warning('Failed to JSON encode resource content.', ['exception' => $e, 'uri' => $uri]);
248242
throw McpServerException::internalError("Failed to serialize resource content for '{$uri}'.", $e);
249243
} catch (McpServerException $e) {
250244
throw $e;
251245
} catch (Throwable $e) {
252-
$this->logger->error('MCP SDK: Resource read failed.', ['uri' => $uri, 'exception' => $e]);
246+
$this->logger->error('Resource read failed.', ['uri' => $uri, 'exception' => $e]);
253247
throw McpServerException::resourceReadFailed($uri, $e);
254248
}
255249
}
@@ -282,32 +276,31 @@ public function handlePromptGet(GetPromptRequest $request): GetPromptResult
282276
$promptName = $request->name;
283277
$arguments = $request->arguments;
284278

285-
['prompt' => $prompt, 'handler' => $handler] = $this->registry->getPrompt($promptName);
286-
if (! $prompt) {
279+
$registeredPrompt = $this->registry->getPrompt($promptName);
280+
if (! $registeredPrompt) {
287281
throw McpServerException::invalidParams("Prompt '{$promptName}' not found.");
288282
}
289283

290284
$arguments = (array) $arguments;
291285

292-
foreach ($prompt->arguments as $argDef) {
286+
foreach ($registeredPrompt->schema->arguments as $argDef) {
293287
if ($argDef->required && ! array_key_exists($argDef->name, $arguments)) {
294288
throw McpServerException::invalidParams("Missing required argument '{$argDef->name}' for prompt '{$promptName}'.");
295289
}
296290
}
297291

298292
try {
299-
$result = $handler->handle($this->container, $arguments);
300-
$messages = $this->formatPromptMessages($result);
293+
$result = $registeredPrompt->get($this->container, $arguments);
301294

302-
return new GetPromptResult($messages, $prompt->description);
295+
return new GetPromptResult($result, $registeredPrompt->schema->description);
303296
} catch (JsonException $e) {
304-
$this->logger->warning('MCP SDK: Failed to JSON encode prompt messages.', ['exception' => $e, 'promptName' => $promptName]);
297+
$this->logger->warning('Failed to JSON encode prompt messages.', ['exception' => $e, 'promptName' => $promptName]);
305298
throw McpServerException::internalError("Failed to serialize prompt messages for '{$promptName}'.", $e);
306299
} catch (McpServerException $e) {
307-
throw $e; // Re-throw known MCP errors
300+
throw $e;
308301
} catch (Throwable $e) {
309-
$this->logger->error('MCP SDK: Prompt generation failed.', ['promptName' => $promptName, 'exception' => $e]);
310-
throw McpServerException::promptGenerationFailed($promptName, $e); // Use specific factory
302+
$this->logger->error('Prompt generation failed.', ['promptName' => $promptName, 'exception' => $e]);
303+
throw McpServerException::promptGenerationFailed($promptName, $e);
311304
}
312305
}
313306

@@ -332,13 +325,13 @@ public function handleCompletionComplete(CompletionCompleteRequest $request, Ses
332325

333326
if ($ref->type === 'ref/prompt') {
334327
$identifier = $ref->name;
335-
['prompt' => $prompt] = $this->registry->getPrompt($identifier);
336-
if (! $prompt) {
328+
$registeredPrompt = $this->registry->getPrompt($identifier);
329+
if (! $registeredPrompt) {
337330
throw McpServerException::invalidParams("Prompt '{$identifier}' not found.");
338331
}
339332

340333
$foundArg = false;
341-
foreach ($prompt->arguments as $arg) {
334+
foreach ($registeredPrompt->schema->arguments as $arg) {
342335
if ($arg->name === $argumentName) {
343336
$foundArg = true;
344337
break;
@@ -347,15 +340,17 @@ public function handleCompletionComplete(CompletionCompleteRequest $request, Ses
347340
if (! $foundArg) {
348341
throw McpServerException::invalidParams("Argument '{$argumentName}' not found in prompt '{$identifier}'.");
349342
}
343+
344+
$providerClass = $registeredPrompt->getCompletionProvider($argumentName);
350345
} else if ($ref->type === 'ref/resource') {
351346
$identifier = $ref->uri;
352-
['resourceTemplate' => $resourceTemplate, 'variables' => $uriVariables] = $this->registry->getResourceTemplate($identifier);
353-
if (! $resourceTemplate) {
347+
$registeredResourceTemplate = $this->registry->getResourceTemplate($identifier);
348+
if (! $registeredResourceTemplate) {
354349
throw McpServerException::invalidParams("Resource template '{$identifier}' not found.");
355350
}
356351

357352
$foundArg = false;
358-
foreach ($uriVariables as $uriVariableName) {
353+
foreach ($registeredResourceTemplate->getVariableNames() as $uriVariableName) {
359354
if ($uriVariableName === $argumentName) {
360355
$foundArg = true;
361356
break;
@@ -365,11 +360,12 @@ public function handleCompletionComplete(CompletionCompleteRequest $request, Ses
365360
if (! $foundArg) {
366361
throw McpServerException::invalidParams("URI variable '{$argumentName}' not found in resource template '{$identifier}'.");
367362
}
363+
364+
$providerClass = $registeredResourceTemplate->getCompletionProvider($argumentName);
368365
} else {
369366
throw McpServerException::invalidParams("Invalid ref type '{$ref->type}' for completion complete request.");
370367
}
371368

372-
$providerClass = $this->registry->getCompletionProvider($ref->type, $identifier, $argumentName);
373369
if (! $providerClass) {
374370
$this->logger->warning("No completion provider found for argument '{$argumentName}' in '{$ref->type}' '{$identifier}'.");
375371
return new CompletionCompleteResult([]);

src/Support/Handler.php renamed to src/Elements/RegisteredElement.php

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
declare(strict_types=1);
44

5-
namespace PhpMcp\Server\Support;
5+
namespace PhpMcp\Server\Elements;
66

77
use InvalidArgumentException;
88
use PhpMcp\Server\Exception\McpServerException;
@@ -14,30 +14,30 @@
1414
use Throwable;
1515
use TypeError;
1616

17-
class Handler
17+
class RegisteredElement
1818
{
1919
public function __construct(
20-
public readonly string $className,
21-
public readonly string $methodName,
22-
) {
23-
}
20+
public readonly string $handlerClass,
21+
public readonly string $handlerMethod,
22+
public readonly bool $isManual = false,
23+
) {}
2424

2525
public function handle(ContainerInterface $container, array $arguments): mixed
2626
{
27-
$instance = $container->get($this->className);
27+
$instance = $container->get($this->handlerClass);
2828
$arguments = $this->prepareArguments($instance, $arguments);
29-
$method = $this->methodName;
29+
$method = $this->handlerMethod;
3030

3131
return $instance->$method(...$arguments);
3232
}
3333

34-
private function prepareArguments(object $instance, array $arguments): array
34+
protected function prepareArguments(object $instance, array $arguments): array
3535
{
36-
if (! method_exists($instance, $this->methodName)) {
37-
throw new ReflectionException("Method does not exist: {$this->className}::{$this->methodName}");
36+
if (! method_exists($instance, $this->handlerMethod)) {
37+
throw new ReflectionException("Method does not exist: {$this->handlerClass}::{$this->handlerMethod}");
3838
}
3939

40-
$reflectionMethod = new ReflectionMethod($instance, $this->methodName);
40+
$reflectionMethod = new ReflectionMethod($instance, $this->handlerMethod);
4141

4242
$finalArgs = [];
4343

@@ -65,27 +65,14 @@ private function prepareArguments(object $instance, array $arguments): array
6565
continue;
6666
} else {
6767
throw McpServerException::internalError(
68-
"Missing required argument `{$paramName}` for {$reflectionMethod->class}::{$this->methodName}."
68+
"Missing required argument `{$paramName}` for {$reflectionMethod->class}::{$this->handlerMethod}."
6969
);
7070
}
7171
}
7272

7373
return array_values($finalArgs);
7474
}
7575

76-
public static function fromArray(array $data): self
77-
{
78-
return new self($data['className'], $data['methodName']);
79-
}
80-
81-
public function toArray(): array
82-
{
83-
return [
84-
'className' => $this->className,
85-
'methodName' => $this->methodName,
86-
];
87-
}
88-
8976
/**
9077
* Attempts type casting based on ReflectionParameter type hints.
9178
*

0 commit comments

Comments
 (0)