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