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\Assert\ExpectationFailedException;
18: use Nexus\Mcp\Core\Schema\ContentBlock;
19: use Nexus\Mcp\Core\Schema\ContentBlock\AudioContent;
20: use Nexus\Mcp\Core\Schema\ContentBlock\EmbeddedResource;
21: use Nexus\Mcp\Core\Schema\ContentBlock\ImageContent;
22: use Nexus\Mcp\Core\Schema\ContentBlock\ResourceLink;
23: use Nexus\Mcp\Core\Schema\ContentBlock\TextContent;
24: use Nexus\Mcp\Core\Schema\Result\CallToolResult;
25: use Nexus\Mcp\Core\Schema\Result\InputRequiredResult;
26: use Nexus\Mcp\Server\Discovery\ArgumentBinder;
27: use Nexus\Mcp\Server\Exception\UnsupportedReturnValueException;
28: use Nexus\Mcp\Server\ServerContext;
29:
30: /**
31: * Adapts an attribute-discovered handler method to the `ToolExecutorInterface` contract.
32: */
33: final readonly class ReflectedToolExecutor implements ToolExecutorInterface
34: {
35: public function __construct(
36: private object $handler,
37: private \ReflectionMethod $method,
38: private ArgumentBinder $binder = new ArgumentBinder(),
39: ) {
40: }
41:
42: #[\Override]
43: public function execute(?array $arguments, ServerContext $context): CallToolResult|InputRequiredResult
44: {
45: $bound = $this->binder->bind($this->method, $arguments ?? [], $context);
46:
47: return $this->adapt($this->method->invokeArgs($this->handler, $bound));
48: }
49:
50: private function adapt(mixed $result): CallToolResult|InputRequiredResult
51: {
52: if ($result instanceof CallToolResult || $result instanceof InputRequiredResult) {
53: return $result;
54: }
55:
56: if (\is_string($result)) {
57: return new CallToolResult(content: [new TextContent(text: $result)]);
58: }
59:
60: if ($result instanceof ContentBlock) {
61: return new CallToolResult(content: self::contentBlocks([$result]));
62: }
63:
64: if (\is_array($result)) {
65: return self::structuredOrContent($result, $this->method);
66: }
67:
68: throw self::buildUnsupportedError($this->method, $result);
69: }
70:
71: /**
72: * @param array<array-key, mixed> $result
73: */
74: private static function structuredOrContent(array $result, \ReflectionMethod $method): CallToolResult
75: {
76: if (array_is_list($result) && [] !== $result) {
77: $blocks = self::contentBlocks($result);
78:
79: if (\count($blocks) !== \count($result)) {
80: throw self::buildUnsupportedError($method, $result);
81: }
82:
83: return new CallToolResult(content: $blocks);
84: }
85:
86: try {
87: Assert::that($result)->isMap('Tool structured content must be a string-keyed object.');
88: } catch (ExpectationFailedException) {
89: throw self::buildUnsupportedError($method, $result);
90: }
91:
92: return new CallToolResult(content: [], structuredContent: $result);
93: }
94:
95: /**
96: * @param array<array-key, mixed> $items
97: *
98: * @return list<AudioContent|EmbeddedResource|ImageContent|ResourceLink|TextContent>
99: */
100: private static function contentBlocks(array $items): array
101: {
102: $blocks = [];
103:
104: foreach ($items as $item) {
105: if ($item instanceof AudioContent
106: || $item instanceof EmbeddedResource
107: || $item instanceof ImageContent
108: || $item instanceof ResourceLink
109: || $item instanceof TextContent
110: ) {
111: $blocks[] = $item;
112: }
113: }
114:
115: return $blocks;
116: }
117:
118: private static function buildUnsupportedError(\ReflectionMethod $method, mixed $result): UnsupportedReturnValueException
119: {
120: return new UnsupportedReturnValueException(
121: $method->getDeclaringClass()->getName(),
122: $method->getName(),
123: \sprintf('a %s, a string, content blocks, or an array', CallToolResult::class),
124: $result,
125: );
126: }
127: }
128: