1: <?php
2:
3: declare(strict_types=1);
4:
5: /**
6: * This file is part of the Nexus MCP SDK package.
7: *
8: * (c) 2026 John Paul E. Balandan, CPA <paulbalandan@gmail.com>
9: *
10: * For the full copyright and license information, please view
11: * the LICENSE file that was distributed with this source code.
12: */
13:
14: namespace Nexus\Mcp\Server\Tool;
15:
16: use Nexus\Assert\Assert;
17: use Nexus\Mcp\Core\Schema\ContentBlock\AudioContent;
18: use Nexus\Mcp\Core\Schema\ContentBlock\EmbeddedResource;
19: use Nexus\Mcp\Core\Schema\ContentBlock\ImageContent;
20: use Nexus\Mcp\Core\Schema\ContentBlock\ResourceLink;
21: use Nexus\Mcp\Core\Schema\ContentBlock\TextContent;
22: use Nexus\Mcp\Core\Schema\Result\CallToolResult;
23: use Nexus\Mcp\Core\Schema\Result\InputRequiredResult;
24: use Nexus\Mcp\Server\Discovery\ArgumentBinder;
25: use Nexus\Mcp\Server\Exception\UnsupportedReturnValueException;
26: use Nexus\Mcp\Server\ServerContext;
27:
28: /**
29: * Adapts an attribute-discovered handler method to the `ToolExecutorInterface` contract.
30: */
31: final readonly class ReflectedToolExecutor implements ToolExecutorInterface
32: {
33: public function __construct(
34: private object $handler,
35: private \ReflectionMethod $method,
36: private ArgumentBinder $binder = new ArgumentBinder(),
37: ) {
38: }
39:
40: #[\Override]
41: public function execute(?array $arguments, ServerContext $context): CallToolResult|InputRequiredResult
42: {
43: $bound = $this->binder->bind($this->method, $arguments ?? [], $context);
44:
45: return $this->adapt($this->method->invokeArgs($this->handler, $bound));
46: }
47:
48: private function adapt(mixed $result): CallToolResult|InputRequiredResult
49: {
50: if ($result instanceof CallToolResult || $result instanceof InputRequiredResult) {
51: return $result;
52: }
53:
54: return match (true) {
55: \is_string($result) => new CallToolResult(content: [new TextContent(text: $result)]),
56: $result instanceof AudioContent,
57: $result instanceof EmbeddedResource,
58: $result instanceof ImageContent,
59: $result instanceof ResourceLink,
60: $result instanceof TextContent => new CallToolResult(content: [$result]),
61: \is_array($result) => $this->resolveStructuredOrContent($result, $this->method),
62: default => throw $this->buildUnsupportedError($this->method, $result),
63: };
64: }
65:
66: /**
67: * @param array<array-key, mixed> $result
68: */
69: private function resolveStructuredOrContent(array $result, \ReflectionMethod $method): CallToolResult
70: {
71: if (array_is_list($result) && [] !== $result) {
72: $blocks = [];
73:
74: foreach ($result as $item) {
75: if ($item instanceof AudioContent
76: || $item instanceof EmbeddedResource
77: || $item instanceof ImageContent
78: || $item instanceof ResourceLink
79: || $item instanceof TextContent
80: ) {
81: $blocks[] = $item;
82: }
83: }
84:
85: if (\count($blocks) !== \count($result)) {
86: throw $this->buildUnsupportedError($method, $result);
87: }
88:
89: return new CallToolResult(content: $blocks);
90: }
91:
92: try {
93: Assert::that($result)->isMap('Tool structured content must be a string-keyed object.');
94: } catch (\InvalidArgumentException) {
95: throw $this->buildUnsupportedError($method, $result);
96: }
97:
98: return new CallToolResult(content: [], structuredContent: $result);
99: }
100:
101: private function buildUnsupportedError(\ReflectionMethod $method, mixed $result): UnsupportedReturnValueException
102: {
103: return new UnsupportedReturnValueException(
104: $method->getDeclaringClass()->getName(),
105: $method->getName(),
106: \sprintf('a %s, a string, content blocks, or an array', CallToolResult::class),
107: $result,
108: );
109: }
110: }
111: