* * @author Christopher Hertel * @author Tobias Nyholm */ final class CallToolHandler implements RequestHandlerInterface { private SchemaValidator $schemaValidator; public function __construct( private readonly RegistryInterface $registry, private readonly ReferenceHandlerInterface $referenceHandler, private readonly LoggerInterface $logger = new NullLogger(), ?SchemaValidator $schemaValidator = null, ) { $this->schemaValidator = $schemaValidator ?? new SchemaValidator($logger); } public function supports(Request $request): bool { return $request instanceof CallToolRequest; } /** * @return Response|Error */ public function handle(Request $request, SessionInterface $session): Response|Error { \assert($request instanceof CallToolRequest); $toolName = $request->name; $arguments = $request->arguments; $this->logger->debug('Executing tool', ['name' => $toolName, 'arguments' => $arguments]); try { $reference = $this->registry->getTool($toolName); } catch (ToolNotFoundException $e) { $this->logger->error('Tool not found', ['name' => $toolName, 'exception' => $e]); // -32601 answers an unknown *method*; tools/call exists, it is the // name in its params that does not. return Error::forInvalidParams($e->getMessage(), $request->getId()); } $inputSchema = $reference->tool->inputSchema; $validationErrors = $this->schemaValidator->validateAgainstJsonSchema($arguments, $inputSchema); if (!empty($validationErrors)) { $errorMessages = []; foreach ($validationErrors as $errorDetail) { $pointer = $errorDetail['pointer'] ?? ''; $message = $errorDetail['message'] ?? 'Unknown validation error'; $errorMessages[] = ('/' !== $pointer && '' !== $pointer ? "Property '{$pointer}': " : '').$message; } $summaryMessage = "Invalid parameters for tool '{$toolName}': ".implode('; ', \array_slice($errorMessages, 0, 3)); if (\count($errorMessages) > 3) { $summaryMessage .= '; ...and more errors.'; } return Error::forInvalidParams($summaryMessage, $request->getId(), ['validation_errors' => $validationErrors]); } $arguments['_session'] = $session; $arguments['_request'] = $request; $context = new RequestContext($session, $request); try { $result = $this->referenceHandler->handle($reference, $arguments); // An ask is a result in its own right, not tool output. if ($result instanceof InputRequiredResult) { return new Response($request->getId(), $result); } $protocolVersion = $context->getProtocolVersion(); $structuredContent = null; if (!$result instanceof CallToolResult) { $structuredContent = $reference->extractStructuredContent($result, $protocolVersion); if (null === $structuredContent && null !== $reference->tool->outputSchema) { $this->logger->warning('Tool declares an "outputSchema" but returned a value that cannot be sent as "structuredContent"; the value is only carried in "content".', [ 'name' => $toolName, 'result_type' => get_debug_type($result), ]); } $result = new CallToolResult($reference->formatResult($result), structuredContent: $structuredContent); } elseif ($protocolVersion->requiresObjectStructuredContent() && null !== $result->structuredContent && [] !== $result->structuredContent && !self::isJsonObject($result->structuredContent) ) { // A tool building its own `CallToolResult` bypasses the extraction // rules on purpose, so the value is sent as it was set — but before // SEP-2106 only a JSON object is valid here, whether the value is a // list or a scalar, and clients may reject it. $this->logger->warning('Tool returned a "CallToolResult" whose "structuredContent" is not a JSON object, which the negotiated protocol revision requires; sending it unchanged.', [ 'name' => $toolName, 'protocol_version' => $protocolVersion->value, 'structured_content_type' => get_debug_type($result->structuredContent), ]); } $this->logger->debug('Tool executed successfully', [ 'name' => $toolName, 'result_type' => \gettype($result), 'structured_content' => $structuredContent, ]); return new Response($request->getId(), $result); } catch (MissingRequiredClientCapabilityException $e) { // Not a tool failure — the request was unservable, and the client // needs to retry declaring the capability. Rendered as -32021. throw $e; } catch (ToolCallException $e) { $this->logger->error(\sprintf('Error while executing tool "%s": "%s".', $toolName, $e->getMessage()), [ 'tool' => $toolName, 'arguments' => $arguments, 'exception' => $e, ]); $errorContent = [new TextContent($e->getMessage())]; return new Response($request->getId(), CallToolResult::error($errorContent)); } catch (\Throwable $e) { $this->logger->error('Unhandled error during tool execution', [ 'name' => $toolName, 'exception' => $e, ]); return Error::forInternalError('Error while executing tool', $request->getId()); } } /** * Whether a `structuredContent` value encodes as a JSON object — the only shape * revisions predating SEP-2106 accept. */ private static function isJsonObject(mixed $value): bool { return \is_array($value) && !array_is_list($value); } }