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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ All notable changes to `mcp/sdk` will be documented in this file.
0.8.0
-----

* Log expected tool execution failures (`ToolCallException`) at debug level instead of error level; unexpected exceptions remain errors.
* [BC Break] Drop the SDK-only name pattern on `ResourceDefinition`/`ResourceTemplate` `$name` — the spec allows any string (its own examples use `main.rs` and `Project Files`). URI/URI-template validation is unchanged.
* Add `ClientGateway::supportsExtension()`, `Client\Builder::enableExtension()`, and `ClientCapabilities::withExtensions()` so clients can negotiate and check protocol extensions (e.g. MCP Apps) the same way servers already do. [BC Break] `ServerExtensionInterface` is replaced by the side-agnostic `Mcp\Schema\Extension\ExtensionInterface`.
* Deprecate Roots, Sampling and Logging per SEP-2577 (protocol revision `2026-07-28`, earliest removal `2027-07-28`). They keep working but using them now triggers a deprecation notice — migrate to tool arguments/resource URIs, a direct LLM provider API, and stderr/OpenTelemetry respectively.
Expand Down
2 changes: 1 addition & 1 deletion src/Server/Handler/Request/CallToolHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ public function handle(Request $request, SessionInterface $session): Response|Er

return new Response($request->getId(), $result);
} catch (ToolCallException $e) {
$this->logger->error(\sprintf('Error while executing tool "%s": "%s".', $toolName, $e->getMessage()), [
$this->logger->debug(\sprintf('Error while executing tool "%s": "%s".', $toolName, $e->getMessage()), [
'tool' => $toolName,
'arguments' => $arguments,
'exception' => $e,
Expand Down
90 changes: 69 additions & 21 deletions tests/Unit/Server/Handler/Request/CallToolHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use Mcp\Server\Session\SessionInterface;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\AbstractLogger;
use Psr\Log\LoggerInterface;

class CallToolHandlerTest extends TestCase
Expand Down Expand Up @@ -207,16 +208,8 @@ public function testHandleToolCallExceptionReturnsResponseWithErrorResult(): voi
->willThrowException($exception);

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

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

Expand All @@ -231,6 +224,61 @@ public function testHandleToolCallExceptionReturnsResponseWithErrorResult(): voi
$this->assertEquals('Tool execution failed', $result->content[0]->text);
}

public function testHandleToolCallExceptionLogsAtDebugLevel(): void
{
$request = $this->createCallToolRequest('failing_tool', ['param' => 'value']);
$exception = new ToolCallException('Expected tool failure');
$logger = new class extends AbstractLogger {
public array $records = [];

// @phpstan-ignore missingType.parameter (compatible with psr/log 1.x)
public function log($level, $message, array $context = []): void
{
$this->records[] = ['level' => $level, 'message' => (string) $message, 'context' => $context];
}
};
$handler = new CallToolHandler($this->registry, $this->referenceHandler, $logger);
$toolReference = $this->createToolReference('failing_tool', static function () {
return 'unused';
});

$this->registry
->expects($this->once())
->method('getTool')
->with('failing_tool')
->willReturn($toolReference);

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

$handler->handle($request, $this->session);

$failureRecords = array_values(array_filter(
$logger->records,
static fn (array $record): bool => str_contains($record['message'], 'Expected tool failure'),
));

$this->assertSame([
[
'level' => 'debug',
'message' => 'Error while executing tool "failing_tool": "Expected tool failure".',
'context' => [
'tool' => 'failing_tool',
'arguments' => $arguments,
'exception' => $exception,
],
],
], $failureRecords);
$this->assertSame([], array_values(array_filter(
$logger->records,
static fn (array $record): bool => \in_array($record['level'], ['error', 'critical'], true),
)));
}

public function testHandleWithNullResult(): void
{
$request = $this->createCallToolRequest('null_tool', []);
Expand Down Expand Up @@ -270,7 +318,7 @@ public function testConstructorWithDefaultLogger(): void
$this->assertInstanceOf(CallToolHandler::class, $handler);
}

public function testHandleLogsErrorWithCorrectParameters(): void
public function testHandleLogsToolCallException(): void
{
$request = $this->createCallToolRequest('test_tool', ['key1' => 'value1', 'key2' => 42]);
$exception = new ToolCallException('Custom error message');
Expand All @@ -291,16 +339,8 @@ public function testHandleLogsErrorWithCorrectParameters(): void
->willThrowException($exception);

$this->logger
->expects($this->once())
->method('error')
->with(
'Error while executing tool "test_tool": "Custom error message".',
[
'tool' => 'test_tool',
'arguments' => ['key1' => 'value1', 'key2' => 42, '_session' => $this->session, '_request' => $request],
'exception' => $exception,
],
);
->expects($this->atLeastOnce())
->method('debug');

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

Expand Down Expand Up @@ -336,6 +376,14 @@ public function testHandleGenericExceptionReturnsError(): void
->with($toolReference, ['param' => 'value', '_session' => $this->session, '_request' => $request])
->willThrowException($exception);

$this->logger
->expects($this->once())
->method('error')
->with('Unhandled error during tool execution', [
'name' => 'failing_tool',
'exception' => $exception,
]);

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

// Generic exceptions should return Error, not Response
Expand Down