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\Handler\Request;
15:
16: use Nexus\Mcp\Core\Exception\AbstractJsonRpcProtocolException;
17: use Nexus\Mcp\Core\Handler\AbstractContext;
18: use Nexus\Mcp\Core\Handler\RequestHandlerInterface;
19: use Nexus\Mcp\Core\Schema\ContentBlock\TextContent;
20: use Nexus\Mcp\Core\Schema\JsonRpc\JsonRpcRequest;
21: use Nexus\Mcp\Core\Schema\Request\CallToolRequest;
22: use Nexus\Mcp\Core\Schema\Result\CallToolResult;
23: use Nexus\Mcp\Core\Schema\Result\InputRequiredResult;
24: use Nexus\Mcp\Server\Exception\ToolOutputValidationException;
25: use Nexus\Mcp\Server\ServerContext;
26: use Nexus\Mcp\Server\Tool\ToolStoreInterface;
27: use Psr\Log\LoggerInterface;
28: use Psr\Log\NullLogger;
29:
30: /**
31: * Handles the `tools/call` request by delegating to a `ToolStoreInterface`.
32: *
33: * @implements RequestHandlerInterface<'tools/call', CallToolResult|InputRequiredResult, ServerContext>
34: */
35: final readonly class CallToolRequestHandler implements RequestHandlerInterface
36: {
37: public function __construct(private ToolStoreInterface $store, private LoggerInterface $logger = new NullLogger())
38: {
39: }
40:
41: #[\Override]
42: public function handle(JsonRpcRequest $request, AbstractContext $context): CallToolResult|InputRequiredResult
43: {
44: \assert($request instanceof CallToolRequest);
45: \assert($context instanceof ServerContext);
46:
47: try {
48: $result = $this->store->call($request->params->name, $request->params->arguments, $context);
49:
50: if ($result instanceof InputRequiredResult) {
51: // The tool is asking the client for input, so it has produced no content to back-fill.
52: return $result;
53: }
54:
55: // Spec, "Structured Content": "For backwards compatibility, a tool that returns
56: // structured content SHOULD also return the serialized JSON in a TextContent block."
57: if (null !== $result->structuredContent && [] === $result->content) {
58: return new CallToolResult(
59: content: [new TextContent(text: json_encode(
60: $result->structuredContent,
61: \JSON_THROW_ON_ERROR | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE,
62: ))],
63: structuredContent: $result->structuredContent,
64: isError: $result->isError,
65: meta: $result->meta,
66: );
67: }
68:
69: return $result;
70: } catch (AbstractJsonRpcProtocolException $e) {
71: throw $e;
72: } catch (ToolOutputValidationException $e) {
73: $this->logger->error(
74: 'Tool returned structuredContent that does not conform to its outputSchema.',
75: ['tool' => $request->params->name, 'exception' => $e],
76: );
77:
78: return new CallToolResult(
79: content: [new TextContent(text: 'Tool execution failed.')],
80: isError: true,
81: );
82: } catch (\Throwable $e) {
83: // Generic peer-facing text. Raw $e->getMessage() can carry paths or secrets.
84: $this->logger->error(
85: 'Uncaught tool executor exception. Returning generic error to peer.',
86: ['tool' => $request->params->name, 'exception' => $e],
87: );
88:
89: return new CallToolResult(
90: content: [new TextContent(text: 'Tool execution failed.')],
91: isError: true,
92: );
93: }
94: }
95: }
96: