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