Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/Capability/Tool/ToolCaller.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use Mcp\Capability\Registry\ReferenceHandlerInterface;
use Mcp\Capability\Registry\ReferenceProviderInterface;
use Mcp\Exception\RegistryException;
use Mcp\Exception\ToolCallException;
use Mcp\Exception\ToolNotFoundException;
use Mcp\Schema\Content\AudioContent;
Expand Down Expand Up @@ -68,14 +69,20 @@ public function call(CallToolRequest $request): CallToolResult
]);

return new CallToolResult($formattedResult);
} catch (RegistryException $e) {
throw new ToolCallException($request, $e);
} catch (\Throwable $e) {
if ($e instanceof ToolCallException) {
throw $e;
}

$this->logger->error('Tool execution failed', [
'name' => $toolName,
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);

throw new ToolCallException($request, $e);
throw new ToolCallException($request, RegistryException::internalError('Error while executing tool', $e));
}
}
}
5 changes: 4 additions & 1 deletion src/Capability/ToolChain.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Mcp\Capability\Tool\MetadataInterface;
use Mcp\Capability\Tool\ToolCallerInterface;
use Mcp\Exception\InvalidCursorException;
use Mcp\Exception\RegistryException;
use Mcp\Exception\ToolCallException;
use Mcp\Exception\ToolNotFoundException;
use Mcp\Schema\Request\CallToolRequest;
Expand Down Expand Up @@ -66,8 +67,10 @@ public function call(CallToolRequest $request): CallToolResult
if ($item instanceof ToolCallerInterface && $request->name === $item->getName()) {
try {
return $item->call($request);
} catch (ToolCallException|ToolNotFoundException $e) {
throw $e;
} catch (\Throwable $e) {
throw new ToolCallException($request, $e);
throw new ToolCallException($request, RegistryException::internalError('Error while executing tool', $e));
}
}
}
Expand Down
25 changes: 25 additions & 0 deletions src/Exception/ReferenceExecutionException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

/*
* This file is part of the official PHP MCP SDK.
*
* A collaboration between Symfony and the PHP Foundation.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Mcp\Exception;

final class ReferenceExecutionException extends RegistryException
{
/**
* @param non-empty-list<non-empty-string> $messages
*/
public function __construct(
public readonly array $messages,
?\Throwable $previous = null,
) {
parent::__construct(implode("\n", $this->messages), previous: $previous);
}
}
2 changes: 1 addition & 1 deletion src/Exception/RegistryException.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

use Mcp\Schema\JsonRpc\Error;

final class RegistryException extends \Exception implements ExceptionInterface
class RegistryException extends \Exception implements ExceptionInterface
{
public static function invalidParams(string $message = 'Invalid params', ?\Throwable $previous = null): self
{
Expand Down
8 changes: 6 additions & 2 deletions src/Exception/ToolCallException.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@ final class ToolCallException extends \RuntimeException implements ExceptionInte
{
public function __construct(
public readonly CallToolRequest $request,
?\Throwable $previous = null,
public readonly RegistryException $registryException,
) {
parent::__construct(\sprintf('Tool call "%s" failed with error: "%s".', $request->name, $previous?->getMessage() ?? ''), previous: $previous);
$message = ($this->registryException->getPrevious() ?? $this->registryException)->getMessage();
parent::__construct(
\sprintf('Tool call "%s" failed with error: "%s".', $request->name, $message),
previous: $this->registryException->getPrevious(),
);
}
}
20 changes: 20 additions & 0 deletions src/Exception/ToolExecutionExceptionInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

/*
* This file is part of the official PHP MCP SDK.
*
* A collaboration between Symfony and the PHP Foundation.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Mcp\Exception;

interface ToolExecutionExceptionInterface extends \Throwable
{
/**
* @return non-empty-list<non-empty-string>
*/
public function getErrorMessages(): array;
}
2 changes: 1 addition & 1 deletion src/JsonRpc/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public static function make(
new RequestHandler\GetPromptHandler($promptGetter),
new RequestHandler\ListResourcesHandler($referenceProvider),
new RequestHandler\ReadResourceHandler($resourceReader),
new RequestHandler\CallToolHandler($toolCaller, $logger),
new RequestHandler\CallToolHandler($toolCaller),
new RequestHandler\ListToolsHandler($referenceProvider),
],
logger: $logger,
Expand Down
32 changes: 18 additions & 14 deletions src/Server/RequestHandler/CallToolHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@
namespace Mcp\Server\RequestHandler;

use Mcp\Capability\Tool\ToolCallerInterface;
use Mcp\Exception\ExceptionInterface;
use Mcp\Exception\ReferenceExecutionException;
use Mcp\Exception\ToolCallException;
use Mcp\Exception\ToolNotFoundException;
use Mcp\Schema\Content\TextContent;
use Mcp\Schema\JsonRpc\Error;
use Mcp\Schema\JsonRpc\HasMethodInterface;
use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\Request\CallToolRequest;
use Mcp\Schema\Result\CallToolResult;
use Mcp\Server\MethodHandlerInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;

/**
* @author Christopher Hertel <[email protected]>
Expand All @@ -29,7 +31,6 @@ final class CallToolHandler implements MethodHandlerInterface
{
public function __construct(
private readonly ToolCallerInterface $toolCaller,
private readonly LoggerInterface $logger = new NullLogger(),
) {
}

Expand All @@ -44,16 +45,19 @@ public function handle(CallToolRequest|HasMethodInterface $message): Response|Er

try {
$content = $this->toolCaller->call($message);
} catch (ExceptionInterface $exception) {
$this->logger->error(
\sprintf('Error while executing tool "%s": "%s".', $message->name, $exception->getMessage()),
[
'tool' => $message->name,
'arguments' => $message->arguments,
],
);

return Error::forInternalError('Error while executing tool', $message->getId());
} catch (ToolNotFoundException $exception) {
return Error::forInvalidParams($exception->getMessage(), $message->getId());
} catch (ToolCallException $exception) {
$registryException = $exception->registryException;

if ($registryException instanceof ReferenceExecutionException) {
return new Response($message->getId(), CallToolResult::error(array_map(
fn (string $message): TextContent => new TextContent($message),
$registryException->messages,
)));
}

return new Error($message->getId(), $registryException->getCode(), $registryException->getMessage());
}

return new Response($message->getId(), $content);
Expand Down
26 changes: 26 additions & 0 deletions tests/Capability/Tool/ToolCallerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Mcp\Capability\Registry\ReferenceProviderInterface;
use Mcp\Capability\Registry\ToolReference;
use Mcp\Capability\Tool\ToolCaller;
use Mcp\Exception\ReferenceExecutionException;
use Mcp\Exception\ToolCallException;
use Mcp\Exception\ToolNotFoundException;
use Mcp\Schema\Content\TextContent;
Expand Down Expand Up @@ -175,6 +176,31 @@ public function testCallThrowsToolNotFoundExceptionWhenToolNotFound(): void
$this->toolCaller->call($request);
}

public function testCallThrowsToolExecutionException(): void
{
$request = new CallToolRequest('test_tool', ['param' => 'value']);
$tool = $this->createValidTool('test_tool');
$exception = new ReferenceExecutionException(['test error']);
$toolReference = new ToolReference($tool, fn () => throw $exception);

$this->referenceProvider
->expects($this->once())
->method('getTool')
->with('test_tool')
->willReturn($toolReference);

$this->referenceHandler
->expects($this->once())
->method('handle')
->with($toolReference, ['param' => 'value'])
->willThrowException($exception);

$this->expectException(ToolCallException::class);
$this->expectExceptionMessage('Tool call "test_tool" failed with error: "test error".');

$this->toolCaller->call($request);
}

public function testCallThrowsToolExecutionExceptionWhenHandlerThrowsException(): void
{
$request = new CallToolRequest('failing_tool', ['param' => 'value']);
Expand Down
56 changes: 7 additions & 49 deletions tests/Server/RequestHandler/CallToolHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Mcp\Tests\Server\RequestHandler;

use Mcp\Capability\Tool\ToolCallerInterface;
use Mcp\Exception\RegistryException;
use Mcp\Exception\ToolCallException;
use Mcp\Exception\ToolNotFoundException;
use Mcp\Schema\Content\TextContent;
Expand All @@ -22,23 +23,17 @@
use Mcp\Server\RequestHandler\CallToolHandler;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;

class CallToolHandlerTest extends TestCase
{
private CallToolHandler $handler;
private ToolCallerInterface|MockObject $toolExecutor;
private LoggerInterface|MockObject $logger;

protected function setUp(): void
{
$this->toolExecutor = $this->createMock(ToolCallerInterface::class);
$this->logger = $this->createMock(LoggerInterface::class);

$this->handler = new CallToolHandler(
$this->toolExecutor,
$this->logger,
);
$this->handler = new CallToolHandler($this->toolExecutor);
}

public function testSupportsCallToolRequest(): void
Expand All @@ -59,10 +54,6 @@ public function testHandleSuccessfulToolCall(): void
->with($request)
->willReturn($expectedResult);

$this->logger
->expects($this->never())
->method('error');

$response = $this->handler->handle($request);

$this->assertInstanceOf(Response::class, $response);
Expand Down Expand Up @@ -122,53 +113,31 @@ public function testHandleToolNotFoundExceptionReturnsError(): void
->with($request)
->willThrowException($exception);

$this->logger
->expects($this->once())
->method('error')
->with(
'Error while executing tool "nonexistent_tool": "Tool not found for call: "nonexistent_tool".".',
[
'tool' => 'nonexistent_tool',
'arguments' => ['param' => 'value'],
],
);

$response = $this->handler->handle($request);

$this->assertInstanceOf(Error::class, $response);
$this->assertEquals($request->getId(), $response->id);
$this->assertEquals(Error::INTERNAL_ERROR, $response->code);
$this->assertEquals('Error while executing tool', $response->message);
$this->assertEquals(Error::INVALID_PARAMS, $response->code);
$this->assertEquals('Tool not found for call: "nonexistent_tool".', $response->message);
}

public function testHandleToolExecutionExceptionReturnsError(): void
{
$request = $this->createCallToolRequest('failing_tool', ['param' => 'value']);
$exception = new ToolCallException($request, new \RuntimeException('Tool execution failed'));
$exception = new ToolCallException($request, RegistryException::internalError('Tool execution failed', previous: new \RuntimeException()));

$this->toolExecutor
->expects($this->once())
->method('call')
->with($request)
->willThrowException($exception);

$this->logger
->expects($this->once())
->method('error')
->with(
'Error while executing tool "failing_tool": "Tool call "failing_tool" failed with error: "Tool execution failed".".',
[
'tool' => 'failing_tool',
'arguments' => ['param' => 'value'],
],
);

$response = $this->handler->handle($request);

$this->assertInstanceOf(Error::class, $response);
$this->assertEquals($request->getId(), $response->id);
$this->assertEquals(Error::INTERNAL_ERROR, $response->code);
$this->assertEquals('Error while executing tool', $response->message);
$this->assertEquals('Internal error: Tool execution failed (See server logs)', $response->message);
}

public function testHandleWithNullResult(): void
Expand Down Expand Up @@ -216,24 +185,13 @@ public function testConstructorWithDefaultLogger(): void
public function testHandleLogsErrorWithCorrectParameters(): void
{
$request = $this->createCallToolRequest('test_tool', ['key1' => 'value1', 'key2' => 42]);
$exception = new ToolCallException($request, new \RuntimeException('Custom error message'));
$exception = new ToolCallException($request, RegistryException::internalError(previous: new \RuntimeException('Custom error message')));

$this->toolExecutor
->expects($this->once())
->method('call')
->willThrowException($exception);

$this->logger
->expects($this->once())
->method('error')
->with(
'Error while executing tool "test_tool": "Tool call "test_tool" failed with error: "Custom error message".".',
[
'tool' => 'test_tool',
'arguments' => ['key1' => 'value1', 'key2' => 42],
],
);

$this->handler->handle($request);
}

Expand Down