Add PSR-3 compliant exception context when logging exceptions (#300)

* Add PSR-3 exception context to caught error logs

* Apply review feedback
This commit is contained in:
Sven Münnich 2026-05-12 23:20:42 +02:00 • committed by GitHub
commit 73414d65f6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 88 additions and 27 deletions

View file

@ -117,7 +117,6 @@ final class Discoverer implements DiscovererInterface
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->logger->error('Error during file finding process for MCP discovery'.json_encode($e->getTrace(), \JSON_PRETTY_PRINT), [ $this->logger->error('Error during file finding process for MCP discovery'.json_encode($e->getTrace(), \JSON_PRETTY_PRINT), [
'exception' => $e, 'exception' => $e,
'trace' => $e->getTraceAsString(),
]); ]);
} }
@ -199,7 +198,6 @@ final class Discoverer implements DiscovererInterface
'file' => $file->getPathname(), 'file' => $file->getPathname(),
'class' => $className, 'class' => $className,
'exception' => $e, 'exception' => $e,
'trace' => $e->getTraceAsString(),
]); ]);
} }
} }
@ -300,9 +298,15 @@ final class Discoverer implements DiscovererInterface
break; break;
} }
} catch (ExceptionInterface $e) { } catch (ExceptionInterface $e) {
$this->logger->error("Failed to process MCP attribute on {$className}::{$methodName}", ['attribute' => $attributeClassName, 'exception' => $e, 'trace' => $e->getPrevious() ? $e->getPrevious()->getTraceAsString() : $e->getTraceAsString()]); $this->logger->error("Failed to process MCP attribute on {$className}::{$methodName}", [
'attribute' => $attributeClassName,
'exception' => $e,
]);
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->logger->error("Unexpected error processing attribute on {$className}::{$methodName}", ['attribute' => $attributeClassName, 'exception' => $e, 'trace' => $e->getTraceAsString()]); $this->logger->error("Unexpected error processing attribute on {$className}::{$methodName}", [
'attribute' => $attributeClassName,
'exception' => $e,
]);
} }
} }

View file

@ -47,8 +47,7 @@ class DocBlockParser
} catch (\Throwable $e) { } catch (\Throwable $e) {
// Log error or handle gracefully if invalid DocBlock syntax is encountered // Log error or handle gracefully if invalid DocBlock syntax is encountered
$this->logger->warning('Failed to parse DocBlock', [ $this->logger->warning('Failed to parse DocBlock', [
'error' => $e->getMessage(), 'exception' => $e,
'exception_trace' => $e->getTraceAsString(),
]); ]);
return null; return null;

View file

@ -87,8 +87,7 @@ class SchemaValidator
$result = $validator->validate($dataToValidate, $schemaObject); $result = $validator->validate($dataToValidate, $schemaObject);
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->logger->error('MCP SDK: JSON Schema validation failed internally.', [ $this->logger->error('MCP SDK: JSON Schema validation failed internally.', [
'exception_message' => $e->getMessage(), 'exception' => $e,
'exception_trace' => $e->getTraceAsString(),
'data' => json_encode($dataToValidate), 'data' => json_encode($dataToValidate),
'schema' => json_encode($schemaObject), 'schema' => json_encode($schemaObject),
]); ]);

View file

@ -55,7 +55,7 @@ class SamplingRequestHandler implements RequestHandlerInterface
return new Response($request->getId(), $result); return new Response($request->getId(), $result);
} catch (SamplingException $e) { } catch (SamplingException $e) {
$this->logger->error('Sampling failed: '.$e->getMessage()); $this->logger->error('Sampling failed: '.$e->getMessage(), ['exception' => $e]);
return Error::forInternalError($e->getMessage(), $request->getId()); return Error::forInternalError($e->getMessage(), $request->getId());
} catch (\Throwable $e) { } catch (\Throwable $e) {

View file

@ -170,7 +170,7 @@ class HttpTransport extends BaseTransport
$this->httpClient->sendRequest($request); $this->httpClient->sendRequest($request);
$this->logger->info('Session closed', ['session_id' => $this->sessionId]); $this->logger->info('Session closed', ['session_id' => $this->sessionId]);
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->logger->warning('Failed to close session', ['error' => $e->getMessage()]); $this->logger->warning('Failed to close session', ['exception' => $e]);
} }
} }

View file

@ -65,7 +65,7 @@ final class CallToolHandler implements RequestHandlerInterface
try { try {
$reference = $this->registry->getTool($toolName); $reference = $this->registry->getTool($toolName);
} catch (ToolNotFoundException $e) { } catch (ToolNotFoundException $e) {
$this->logger->error('Tool not found', ['name' => $toolName]); $this->logger->error('Tool not found', ['name' => $toolName, 'exception' => $e]);
return new Error($request->getId(), Error::METHOD_NOT_FOUND, $e->getMessage()); return new Error($request->getId(), Error::METHOD_NOT_FOUND, $e->getMessage());
} }
@ -112,6 +112,7 @@ final class CallToolHandler implements RequestHandlerInterface
$this->logger->error(\sprintf('Error while executing tool "%s": "%s".', $toolName, $e->getMessage()), [ $this->logger->error(\sprintf('Error while executing tool "%s": "%s".', $toolName, $e->getMessage()), [
'tool' => $toolName, 'tool' => $toolName,
'arguments' => $arguments, 'arguments' => $arguments,
'exception' => $e,
]); ]);
$errorContent = [new TextContent($e->getMessage())]; $errorContent = [new TextContent($e->getMessage())];

View file

@ -65,15 +65,15 @@ final class GetPromptHandler implements RequestHandlerInterface
return new Response($request->getId(), new GetPromptResult($formatted)); return new Response($request->getId(), new GetPromptResult($formatted));
} catch (PromptGetException $e) { } catch (PromptGetException $e) {
$this->logger->error(\sprintf('Error while handling prompt "%s": "%s".', $promptName, $e->getMessage())); $this->logger->error(\sprintf('Error while handling prompt "%s": "%s".', $promptName, $e->getMessage()), ['exception' => $e]);
return Error::forInternalError($e->getMessage(), $request->getId()); return Error::forInternalError($e->getMessage(), $request->getId());
} catch (PromptNotFoundException $e) { } catch (PromptNotFoundException $e) {
$this->logger->error('Prompt not found', ['prompt_name' => $promptName]); $this->logger->error('Prompt not found', ['prompt_name' => $promptName, 'exception' => $e]);
return Error::forResourceNotFound($e->getMessage(), $request->getId()); return Error::forResourceNotFound($e->getMessage(), $request->getId());
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->logger->error(\sprintf('Unexpected error while handling prompt "%s": "%s".', $promptName, $e->getMessage())); $this->logger->error(\sprintf('Unexpected error while handling prompt "%s": "%s".', $promptName, $e->getMessage()), ['exception' => $e]);
return Error::forInternalError('Error while handling prompt', $request->getId()); return Error::forInternalError('Error while handling prompt', $request->getId());
} }

View file

@ -77,15 +77,15 @@ final class ReadResourceHandler implements RequestHandlerInterface
return new Response($request->getId(), new ReadResourceResult($formatted)); return new Response($request->getId(), new ReadResourceResult($formatted));
} catch (ResourceReadException $e) { } catch (ResourceReadException $e) {
$this->logger->error(\sprintf('Error while reading resource "%s": "%s".', $uri, $e->getMessage())); $this->logger->error(\sprintf('Error while reading resource "%s": "%s".', $uri, $e->getMessage()), ['exception' => $e]);
return Error::forInternalError($e->getMessage(), $request->getId()); return Error::forInternalError($e->getMessage(), $request->getId());
} catch (ResourceNotFoundException $e) { } catch (ResourceNotFoundException $e) {
$this->logger->error('Resource not found', ['uri' => $uri]); $this->logger->error('Resource not found', ['uri' => $uri, 'exception' => $e]);
return Error::forResourceNotFound($e->getMessage(), $request->getId()); return Error::forResourceNotFound($e->getMessage(), $request->getId());
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->logger->error(\sprintf('Unexpected error while reading resource "%s": "%s".', $uri, $e->getMessage())); $this->logger->error(\sprintf('Unexpected error while reading resource "%s": "%s".', $uri, $e->getMessage()), ['exception' => $e]);
return Error::forInternalError('Error while reading resource', $request->getId()); return Error::forInternalError('Error while reading resource', $request->getId());
} }

View file

@ -55,7 +55,7 @@ final class ResourceSubscribeHandler implements RequestHandlerInterface
try { try {
$this->registry->getResource($uri); $this->registry->getResource($uri);
} catch (ResourceNotFoundException $e) { } catch (ResourceNotFoundException $e) {
$this->logger->error('Resource not found', ['uri' => $uri]); $this->logger->error('Resource not found', ['uri' => $uri, 'exception' => $e]);
return Error::forResourceNotFound($e->getMessage(), $request->getId()); return Error::forResourceNotFound($e->getMessage(), $request->getId());
} }

View file

@ -55,7 +55,7 @@ final class ResourceUnsubscribeHandler implements RequestHandlerInterface
try { try {
$this->registry->getResource($uri); $this->registry->getResource($uri);
} catch (ResourceNotFoundException $e) { } catch (ResourceNotFoundException $e) {
$this->logger->error('Resource not found', ['uri' => $uri]); $this->logger->error('Resource not found', ['uri' => $uri, 'exception' => $e]);
return Error::forResourceNotFound($e->getMessage(), $request->getId()); return Error::forResourceNotFound($e->getMessage(), $request->getId());
} }

View file

@ -96,7 +96,7 @@ try {
$logger->info('Disconnected'); $logger->info('Disconnected');
exit(0); exit(0);
} catch (Throwable $e) { } catch (Throwable $e) {
$logger->error(sprintf('Error: %s', $e->getMessage())); $logger->error(sprintf('Error: %s', $e->getMessage()), ['exception' => $e]);
fwrite(\STDERR, sprintf("Error: %s\n%s\n", $e->getMessage(), $e->getTraceAsString())); fwrite(\STDERR, sprintf("Error: %s\n%s\n", $e->getMessage(), $e->getTraceAsString()));
try { try {

View file

@ -166,16 +166,18 @@ class CallToolHandlerTest extends TestCase
public function testHandleToolNotFoundExceptionReturnsError(): void public function testHandleToolNotFoundExceptionReturnsError(): void
{ {
$request = $this->createCallToolRequest('nonexistent_tool', ['param' => 'value']); $request = $this->createCallToolRequest('nonexistent_tool', ['param' => 'value']);
$exception = new ToolNotFoundException('nonexistent_tool');
$this->registry $this->registry
->expects($this->once()) ->expects($this->once())
->method('getTool') ->method('getTool')
->with('nonexistent_tool') ->with('nonexistent_tool')
->willThrowException(new ToolNotFoundException('nonexistent_tool')); ->willThrowException($exception);
$this->logger $this->logger
->expects($this->once()) ->expects($this->once())
->method('error'); ->method('error')
->with('Tool not found', ['name' => 'nonexistent_tool', 'exception' => $exception]);
$response = $this->handler->handle($request, $this->session); $response = $this->handler->handle($request, $this->session);
@ -206,7 +208,15 @@ class CallToolHandlerTest extends TestCase
$this->logger $this->logger
->expects($this->once()) ->expects($this->once())
->method('error'); ->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,
],
);
$response = $this->handler->handle($request, $this->session); $response = $this->handler->handle($request, $this->session);
@ -288,6 +298,7 @@ class CallToolHandlerTest extends TestCase
[ [
'tool' => 'test_tool', 'tool' => 'test_tool',
'arguments' => ['key1' => 'value1', 'key2' => 42, '_session' => $this->session, '_request' => $request], 'arguments' => ['key1' => 'value1', 'key2' => 42, '_session' => $this->session, '_request' => $request],
'exception' => $exception,
], ],
); );

View file

@ -27,6 +27,7 @@ use Mcp\Server\Handler\Request\GetPromptHandler;
use Mcp\Server\Session\SessionInterface; use Mcp\Server\Session\SessionInterface;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
class GetPromptHandlerTest extends TestCase class GetPromptHandlerTest extends TestCase
{ {
@ -34,14 +35,16 @@ class GetPromptHandlerTest extends TestCase
private RegistryInterface&MockObject $referenceProvider; private RegistryInterface&MockObject $referenceProvider;
private ReferenceHandlerInterface&MockObject $referenceHandler; private ReferenceHandlerInterface&MockObject $referenceHandler;
private SessionInterface&MockObject $session; private SessionInterface&MockObject $session;
private LoggerInterface&MockObject $logger;
protected function setUp(): void protected function setUp(): void
{ {
$this->referenceProvider = $this->createMock(RegistryInterface::class); $this->referenceProvider = $this->createMock(RegistryInterface::class);
$this->referenceHandler = $this->createMock(ReferenceHandlerInterface::class); $this->referenceHandler = $this->createMock(ReferenceHandlerInterface::class);
$this->session = $this->createMock(SessionInterface::class); $this->session = $this->createMock(SessionInterface::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->handler = new GetPromptHandler($this->referenceProvider, $this->referenceHandler); $this->handler = new GetPromptHandler($this->referenceProvider, $this->referenceHandler, $this->logger);
} }
public function testSupportsGetPromptRequest(): void public function testSupportsGetPromptRequest(): void
@ -239,6 +242,11 @@ class GetPromptHandlerTest extends TestCase
->with('nonexistent_prompt') ->with('nonexistent_prompt')
->willThrowException($exception); ->willThrowException($exception);
$this->logger
->expects($this->once())
->method('error')
->with('Prompt not found', ['prompt_name' => 'nonexistent_prompt', 'exception' => $exception]);
$response = $this->handler->handle($request, $this->session); $response = $this->handler->handle($request, $this->session);
$this->assertInstanceOf(Error::class, $response); $this->assertInstanceOf(Error::class, $response);
@ -258,6 +266,11 @@ class GetPromptHandlerTest extends TestCase
->with('failing_prompt') ->with('failing_prompt')
->willThrowException($exception); ->willThrowException($exception);
$this->logger
->expects($this->once())
->method('error')
->with('Error while handling prompt "failing_prompt": "Failed to get prompt".', ['exception' => $exception]);
$response = $this->handler->handle($request, $this->session); $response = $this->handler->handle($request, $this->session);
$this->assertInstanceOf(Error::class, $response); $this->assertInstanceOf(Error::class, $response);

View file

@ -27,6 +27,7 @@ use Mcp\Server\Handler\Request\ReadResourceHandler;
use Mcp\Server\Session\SessionInterface; use Mcp\Server\Session\SessionInterface;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
class ReadResourceHandlerTest extends TestCase class ReadResourceHandlerTest extends TestCase
{ {
@ -34,14 +35,16 @@ class ReadResourceHandlerTest extends TestCase
private RegistryInterface&MockObject $registry; private RegistryInterface&MockObject $registry;
private ReferenceHandlerInterface&MockObject $referenceHandler; private ReferenceHandlerInterface&MockObject $referenceHandler;
private SessionInterface&MockObject $session; private SessionInterface&MockObject $session;
private LoggerInterface&MockObject $logger;
protected function setUp(): void protected function setUp(): void
{ {
$this->registry = $this->createMock(RegistryInterface::class); $this->registry = $this->createMock(RegistryInterface::class);
$this->referenceHandler = $this->createMock(ReferenceHandlerInterface::class); $this->referenceHandler = $this->createMock(ReferenceHandlerInterface::class);
$this->session = $this->createMock(SessionInterface::class); $this->session = $this->createMock(SessionInterface::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->handler = new ReadResourceHandler($this->registry, $this->referenceHandler); $this->handler = new ReadResourceHandler($this->registry, $this->referenceHandler, $this->logger);
} }
public function testSupportsReadResourceRequest(): void public function testSupportsReadResourceRequest(): void
@ -186,6 +189,11 @@ class ReadResourceHandlerTest extends TestCase
->with($uri) ->with($uri)
->willThrowException($exception); ->willThrowException($exception);
$this->logger
->expects($this->once())
->method('error')
->with('Resource not found', ['uri' => $uri, 'exception' => $exception]);
$response = $this->handler->handle($request, $this->session); $response = $this->handler->handle($request, $this->session);
$this->assertInstanceOf(Error::class, $response); $this->assertInstanceOf(Error::class, $response);
@ -206,6 +214,11 @@ class ReadResourceHandlerTest extends TestCase
->with($uri) ->with($uri)
->willThrowException($exception); ->willThrowException($exception);
$this->logger
->expects($this->once())
->method('error')
->with('Error while reading resource "file://corrupted/file.txt": "Failed to read resource: corrupted data".', ['exception' => $exception]);
$response = $this->handler->handle($request, $this->session); $response = $this->handler->handle($request, $this->session);
$this->assertInstanceOf(Error::class, $response); $this->assertInstanceOf(Error::class, $response);
@ -226,6 +239,11 @@ class ReadResourceHandlerTest extends TestCase
->with($uri) ->with($uri)
->willThrowException($exception); ->willThrowException($exception);
$this->logger
->expects($this->once())
->method('error')
->with('Unexpected error while reading resource "file://problematic/file.txt": "Internal database connection failed".', ['exception' => $exception]);
$response = $this->handler->handle($request, $this->session); $response = $this->handler->handle($request, $this->session);
$this->assertInstanceOf(Error::class, $response); $this->assertInstanceOf(Error::class, $response);

View file

@ -26,6 +26,7 @@ use Mcp\Server\Session\SessionInterface;
use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
class ResourceSubscribeTest extends TestCase class ResourceSubscribeTest extends TestCase
{ {
@ -33,13 +34,15 @@ class ResourceSubscribeTest extends TestCase
private RegistryInterface&MockObject $registry; private RegistryInterface&MockObject $registry;
private SessionInterface&MockObject $session; private SessionInterface&MockObject $session;
private SubscriptionManagerInterface&MockObject $subscriptionManager; private SubscriptionManagerInterface&MockObject $subscriptionManager;
private LoggerInterface&MockObject $logger;
protected function setUp(): void protected function setUp(): void
{ {
$this->registry = $this->createMock(RegistryInterface::class); $this->registry = $this->createMock(RegistryInterface::class);
$this->subscriptionManager = $this->createMock(SubscriptionManagerInterface::class); $this->subscriptionManager = $this->createMock(SubscriptionManagerInterface::class);
$this->session = $this->createMock(SessionInterface::class); $this->session = $this->createMock(SessionInterface::class);
$this->handler = new ResourceSubscribeHandler($this->registry, $this->subscriptionManager); $this->logger = $this->createMock(LoggerInterface::class);
$this->handler = new ResourceSubscribeHandler($this->registry, $this->subscriptionManager, $this->logger);
} }
#[TestDox('Client can successfully subscribe to a resource')] #[TestDox('Client can successfully subscribe to a resource')]
@ -121,6 +124,11 @@ class ResourceSubscribeTest extends TestCase
->with($uri) ->with($uri)
->willThrowException($exception); ->willThrowException($exception);
$this->logger
->expects($this->once())
->method('error')
->with('Resource not found', ['uri' => $uri, 'exception' => $exception]);
$response = $this->handler->handle($request, $this->session); $response = $this->handler->handle($request, $this->session);
$this->assertInstanceOf(Error::class, $response); $this->assertInstanceOf(Error::class, $response);

View file

@ -26,6 +26,7 @@ use Mcp\Server\Session\SessionInterface;
use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
class ResourceUnsubscribeTest extends TestCase class ResourceUnsubscribeTest extends TestCase
{ {
@ -33,14 +34,16 @@ class ResourceUnsubscribeTest extends TestCase
private RegistryInterface&MockObject $registry; private RegistryInterface&MockObject $registry;
private SessionInterface&MockObject $session; private SessionInterface&MockObject $session;
private SubscriptionManagerInterface&MockObject $subscriptionManager; private SubscriptionManagerInterface&MockObject $subscriptionManager;
private LoggerInterface&MockObject $logger;
protected function setUp(): void protected function setUp(): void
{ {
$this->registry = $this->createMock(RegistryInterface::class); $this->registry = $this->createMock(RegistryInterface::class);
$this->subscriptionManager = $this->createMock(SubscriptionManagerInterface::class); $this->subscriptionManager = $this->createMock(SubscriptionManagerInterface::class);
$this->session = $this->createMock(SessionInterface::class); $this->session = $this->createMock(SessionInterface::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->handler = new ResourceUnsubscribeHandler($this->registry, $this->subscriptionManager); $this->handler = new ResourceUnsubscribeHandler($this->registry, $this->subscriptionManager, $this->logger);
} }
#[TestDox('Client can unsubscribe from a resource')] #[TestDox('Client can unsubscribe from a resource')]
@ -119,6 +122,11 @@ class ResourceUnsubscribeTest extends TestCase
->with($uri) ->with($uri)
->willThrowException($exception); ->willThrowException($exception);
$this->logger
->expects($this->once())
->method('error')
->with('Resource not found', ['uri' => $uri, 'exception' => $exception]);
$response = $this->handler->handle($request, $this->session); $response = $this->handler->handle($request, $this->session);
$this->assertInstanceOf(Error::class, $response); $this->assertInstanceOf(Error::class, $response);