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:
45: try {
46: $result = $this->store->call($request->params->name, $request->params->arguments, $context);
47:
48: // Spec, "Structured Content": "For backwards compatibility, a tool that returns
49: // structured content SHOULD also return the serialized JSON in a TextContent block."
50: if (null !== $result->structuredContent && [] === $result->content) {
51: return new CallToolResult(
52: content: [new TextContent(json_encode(
53: $result->structuredContent,
54: \JSON_THROW_ON_ERROR | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE,
55: ))],
56: structuredContent: $result->structuredContent,
57: isError: $result->isError,
58: meta: $result->meta,
59: );
60: }
61:
62: return $result;
63: } catch (AbstractJsonRpcProtocolException $e) {
64: throw $e;
65: } catch (ToolOutputValidationException $e) {
66: $this->logger->error(
67: 'Tool returned structuredContent that does not conform to its outputSchema.',
68: ['tool' => $request->params->name, 'exception' => $e],
69: );
70:
71: return new CallToolResult(
72: content: [new TextContent('Tool execution failed.')],
73: isError: true,
74: );
75: } catch (\Throwable $e) {
76: // Generic peer-facing text. Raw $e->getMessage() can carry paths or secrets.
77: $this->logger->error(
78: 'Uncaught tool executor exception. Returning generic error to peer.',
79: ['tool' => $request->params->name, 'exception' => $e],
80: );
81:
82: return new CallToolResult(
83: content: [new TextContent('Tool execution failed.')],
84: isError: true,
85: );
86: }
87: }
88: }
89: