diff --git a/composer.json b/composer.json index 411fe45a..94e96de9 100644 --- a/composer.json +++ b/composer.json @@ -63,6 +63,7 @@ "Mcp\\Example\\EnvVariables\\": "examples/env-variables/", "Mcp\\Example\\ExplicitRegistration\\": "examples/explicit-registration/", "Mcp\\Example\\SchemaShowcase\\": "examples/schema-showcase/", + "Mcp\\Example\\ClientLogging\\": "examples/client-logging/", "Mcp\\Tests\\": "tests/" } }, diff --git a/docs/client-communication.md b/docs/client-communication.md index 8da4bc65..5122d0be 100644 --- a/docs/client-communication.md +++ b/docs/client-communication.md @@ -13,44 +13,21 @@ MCP supports various ways a server can communicate back to a server on top of th ## ClientGateway Every communication back to client is handled using the `Mcp\Server\ClientGateway` and its dedicated methods per -operation. To use the `ClientGateway` in your code, there are two ways to do so: +operation. To use the `ClientGateway` in your code, you need to use method argument injection for `RequestContext`. -### 1. Method Argument Injection - -Every refernce of a MCP element, that translates to an actual method call, can just add an type-hinted argument for the -`ClientGateway` and the SDK will take care to include the gateway in the arguments of the method call: +Every reference of a MCP element, that translates to an actual method call, can just add an type-hinted argument for the +`RequestContext` and the SDK will take care to include the gateway in the arguments of the method call: ```php use Mcp\Capability\Attribute\McpTool; -use Mcp\Server\ClientGateway; +use Mcp\Server\RequestContext; class MyService { #[McpTool('my_tool', 'My Tool Description')] - public function myTool(ClientGateway $client): string - { - $client->log(...); -``` - -### 2. Implementing `ClientAwareInterface` - -Whenever a service class of an MCP element implements the interface `Mcp\Server\ClientAwareInterface` the `setClient` -method of that class will get called while handling the reference, and in combination with `Mcp\Server\ClientAwareTrait` -this ends up with code like this: - -```php -use Mcp\Capability\Attribute\McpTool; -use Mcp\Server\ClientAwareInterface; -use Mcp\Server\ClientAwareTrait; - -class MyService implements ClientAwareInterface -{ - use ClientAwareTrait; - - #[McpTool('my_tool', 'My Tool Description')] - public function myTool(): string + public function myTool(RequestContext $context): string { - $this->log(...); + $context->getClientGateway()->log(...); ``` ## Sampling diff --git a/docs/examples.md b/docs/examples.md index e004636e..402d8594 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -166,11 +166,10 @@ $server = Server::builder() **File**: `examples/client-communication/` -**What it demostrates:** -- Server initiated communcation back to the client +**What it demonstrates:** +- Server initiated communication back to the client - Logging, sampling, progress and notifications -- Using `ClientGateway` in service class via `ClientAwareInterface` and corresponding trait -- Using `ClientGateway` in tool method via method argument injection +- Using `ClientGateway` in tool method via method argument injection of `RequestContext` ### Discovery User Profile diff --git a/docs/mcp-elements.md b/docs/mcp-elements.md index 1846a7dd..b1a045f5 100644 --- a/docs/mcp-elements.md +++ b/docs/mcp-elements.md @@ -11,6 +11,7 @@ discovery and manual registration methods. - [Resources](#resources) - [Resource Templates](#resource-templates) - [Prompts](#prompts) +- [Logging](#logging) - [Completion Providers](#completion-providers) - [Schema Generation and Validation](#schema-generation-and-validation) - [Discovery vs Manual Registration](#discovery-vs-manual-registration) @@ -504,6 +505,33 @@ public function generatePrompt(string $topic, string $style): array **Recommendation**: Use `PromptGetException` when you want to communicate specific errors to clients. Any other exception will still be converted to JSON-RPC compliant errors but with generic error messages. +## Logging + +The SDK provides support to send structured log messages to clients. All standard PSR-3 log levels are supported. +Level **warning** as the default level. + +### Usage + +The SDK automatically injects a `RequestContext` instance into handlers. This can be used to create a `ClientLogger`. + +```php +use Mcp\Capability\Logger\ClientLogger; +use Mcp\Server\RequestContext; + +#[McpTool] +public function processData(string $input, RequestContext $context): array { + $logger = $context->getClientLogger(); + + $logger->info('Processing started', ['input' => $input]); + $logger->warning('Deprecated API used'); + + // ... processing logic ... + + $logger->info('Processing completed'); + return ['result' => 'processed']; +} +``` + ## Completion Providers Completion providers help MCP clients offer auto-completion suggestions for Resource Templates and Prompts. Unlike Tools and static Resources (which can be listed via `tools/list` and `resources/list`), Resource Templates and Prompts have dynamic parameters that benefit from completion hints. diff --git a/examples/client-communication/ClientAwareService.php b/examples/client-communication/ClientAwareService.php index 3733614e..70b77bdd 100644 --- a/examples/client-communication/ClientAwareService.php +++ b/examples/client-communication/ClientAwareService.php @@ -14,14 +14,11 @@ use Mcp\Capability\Attribute\McpTool; use Mcp\Schema\Content\TextContent; use Mcp\Schema\Enum\LoggingLevel; -use Mcp\Server\ClientAwareInterface; -use Mcp\Server\ClientAwareTrait; +use Mcp\Server\RequestContext; use Psr\Log\LoggerInterface; -final class ClientAwareService implements ClientAwareInterface +final class ClientAwareService { - use ClientAwareTrait; - public function __construct( private readonly LoggerInterface $logger, ) { @@ -32,9 +29,10 @@ public function __construct( * @return array{incident: string, recommended_actions: string, model: string} */ #[McpTool('coordinate_incident_response', 'Coordinate an incident response with logging, progress, and sampling.')] - public function coordinateIncident(string $incidentTitle): array + public function coordinateIncident(RequestContext $context, string $incidentTitle): array { - $this->log(LoggingLevel::Warning, \sprintf('Incident triage started: %s', $incidentTitle)); + $clientGateway = $context->getClientGateway(); + $clientGateway->log(LoggingLevel::Warning, \sprintf('Incident triage started: %s', $incidentTitle)); $steps = [ 'Collecting telemetry', @@ -45,7 +43,7 @@ public function coordinateIncident(string $incidentTitle): array foreach ($steps as $index => $step) { $progress = ($index + 1) / \count($steps); - $this->progress($progress, 1, $step); + $clientGateway->progress($progress, 1, $step); usleep(180_000); // Simulate work being done } @@ -56,11 +54,11 @@ public function coordinateIncident(string $incidentTitle): array implode(', ', $steps) ); - $result = $this->sample($prompt, 350, 90, ['temperature' => 0.5]); + $result = $clientGateway->sample($prompt, 350, 90, ['temperature' => 0.5]); $recommendation = $result->content instanceof TextContent ? trim((string) $result->content->text) : ''; - $this->log(LoggingLevel::Info, \sprintf('Incident triage completed for %s', $incidentTitle)); + $clientGateway->log(LoggingLevel::Info, \sprintf('Incident triage completed for %s', $incidentTitle)); return [ 'incident' => $incidentTitle, diff --git a/examples/client-logging/LoggingShowcaseHandlers.php b/examples/client-logging/LoggingShowcaseHandlers.php new file mode 100644 index 00000000..35dfa232 --- /dev/null +++ b/examples/client-logging/LoggingShowcaseHandlers.php @@ -0,0 +1,81 @@ + + */ + #[McpTool(name: 'log_message', description: 'Demonstrates MCP logging with different levels')] + public function logMessage(RequestContext $context, string $message, string $level): array + { + $logger = $context->getClientLogger(); + $logger->info('🚀 Starting log_message tool', [ + 'requested_level' => $level, + 'message_length' => \strlen($message), + ]); + + switch (strtolower($level)) { + case 'debug': + $logger->debug("Debug: $message", ['tool' => 'log_message']); + break; + case 'info': + $logger->info("Info: $message", ['tool' => 'log_message']); + break; + case 'notice': + $logger->notice("Notice: $message", ['tool' => 'log_message']); + break; + case 'warning': + $logger->warning("Warning: $message", ['tool' => 'log_message']); + break; + case 'error': + $logger->error("Error: $message", ['tool' => 'log_message']); + break; + case 'critical': + $logger->critical("Critical: $message", ['tool' => 'log_message']); + break; + case 'alert': + $logger->alert("Alert: $message", ['tool' => 'log_message']); + break; + case 'emergency': + $logger->emergency("Emergency: $message", ['tool' => 'log_message']); + break; + default: + $logger->warning("Unknown level '$level', defaulting to info"); + $logger->info("Info: $message", ['tool' => 'log_message']); + } + + $logger->debug('log_message tool completed successfully'); + + return [ + 'message' => "Logged message with level: $level", + 'logged_at' => date('Y-m-d H:i:s'), + 'level_used' => $level, + ]; + } +} diff --git a/examples/client-logging/server.php b/examples/client-logging/server.php new file mode 100644 index 00000000..3ca523f2 --- /dev/null +++ b/examples/client-logging/server.php @@ -0,0 +1,29 @@ +#!/usr/bin/env php +setServerInfo('Client Logging', '1.0.0', 'Demonstration of MCP logging in capability handlers.') + ->setContainer(container()) + ->setLogger(logger()) + ->setDiscovery(__DIR__) + ->build(); + +$result = $server->run(transport()); + +logger()->info('Server listener stopped gracefully.', ['result' => $result]); + +shutdown($result); diff --git a/src/Capability/Discovery/SchemaGenerator.php b/src/Capability/Discovery/SchemaGenerator.php index 2557f559..2431c510 100644 --- a/src/Capability/Discovery/SchemaGenerator.php +++ b/src/Capability/Discovery/SchemaGenerator.php @@ -13,6 +13,7 @@ use Mcp\Capability\Attribute\Schema; use Mcp\Server\ClientGateway; +use Mcp\Server\RequestContext; use phpDocumentor\Reflection\DocBlock\Tags\Param; /** @@ -415,7 +416,7 @@ private function parseParametersInfo(\ReflectionMethod|\ReflectionFunction $refl if ($reflectionType instanceof \ReflectionNamedType && !$reflectionType->isBuiltin()) { $typeName = $reflectionType->getName(); - if (is_a($typeName, ClientGateway::class, true)) { + if (is_a($typeName, ClientGateway::class, true) || is_a($typeName, RequestContext::class, true)) { continue; } } diff --git a/src/Capability/Logger/ClientLogger.php b/src/Capability/Logger/ClientLogger.php new file mode 100644 index 00000000..1e0a959a --- /dev/null +++ b/src/Capability/Logger/ClientLogger.php @@ -0,0 +1,99 @@ + + * @author Tobias Nyholm + */ +final class ClientLogger extends AbstractLogger +{ + public function __construct( + private ClientGateway $client, + private SessionInterface $session, + ) { + } + + /** + * Logs with an arbitrary level. + * + * @param string|\Stringable $message + * @param array $context + */ + public function log($level, $message, array $context = []): void + { + // Convert PSR-3 level to MCP LoggingLevel + $mcpLevel = $this->convertToMcpLevel($level); + if (null === $mcpLevel) { + return; // Unknown level, skip MCP notification + } + + $minimumLevel = $this->session->get(Protocol::SESSION_LOGGING_LEVEL, ''); + $minimumLevel = LoggingLevel::tryFrom($minimumLevel) ?? LoggingLevel::Warning; + + if ($this->getSeverityIndex($minimumLevel) > $this->getSeverityIndex($mcpLevel)) { + return; + } + + $this->client->log($mcpLevel, $message); + } + + /** + * Converts PSR-3 log level to MCP LoggingLevel. + * + * @param mixed $level PSR-3 level + * + * @return LoggingLevel|null MCP level or null if unknown + */ + private function convertToMcpLevel($level): ?LoggingLevel + { + return match (strtolower((string) $level)) { + 'emergency' => LoggingLevel::Emergency, + 'alert' => LoggingLevel::Alert, + 'critical' => LoggingLevel::Critical, + 'error' => LoggingLevel::Error, + 'warning' => LoggingLevel::Warning, + 'notice' => LoggingLevel::Notice, + 'info' => LoggingLevel::Info, + 'debug' => LoggingLevel::Debug, + default => null, + }; + } + + /** + * Gets the severity index for this log level. + * Higher values indicate more severe log levels. + * + * @return int Severity index (0-7, where 7 is most severe) + */ + private function getSeverityIndex(LoggingLevel $level): int + { + return match ($level) { + LoggingLevel::Debug => 0, + LoggingLevel::Info => 1, + LoggingLevel::Notice => 2, + LoggingLevel::Warning => 3, + LoggingLevel::Error => 4, + LoggingLevel::Critical => 5, + LoggingLevel::Alert => 6, + LoggingLevel::Emergency => 7, + }; + } +} diff --git a/src/Capability/Registry/ReferenceHandler.php b/src/Capability/Registry/ReferenceHandler.php index 7ce8c737..f01b1870 100644 --- a/src/Capability/Registry/ReferenceHandler.php +++ b/src/Capability/Registry/ReferenceHandler.php @@ -13,8 +13,7 @@ use Mcp\Exception\InvalidArgumentException; use Mcp\Exception\RegistryException; -use Mcp\Server\ClientAwareInterface; -use Mcp\Server\ClientGateway; +use Mcp\Server\RequestContext; use Mcp\Server\Session\SessionInterface; use Psr\Container\ContainerInterface; @@ -41,10 +40,6 @@ public function handle(ElementReference $reference, array $arguments): mixed $instance = $this->getClassInstance($reference->handler); $arguments = $this->prepareArguments($reflection, $arguments); - if ($instance instanceof ClientAwareInterface) { - $instance->setClient(new ClientGateway($session)); - } - return \call_user_func($instance, ...$arguments); } @@ -67,11 +62,6 @@ public function handle(ElementReference $reference, array $arguments): mixed [$className, $methodName] = $reference->handler; $reflection = new \ReflectionMethod($className, $methodName); $instance = $this->getClassInstance($className); - - if ($instance instanceof ClientAwareInterface) { - $instance->setClient(new ClientGateway($session)); - } - $arguments = $this->prepareArguments($reflection, $arguments); return \call_user_func([$instance, $methodName], ...$arguments); @@ -108,8 +98,8 @@ private function prepareArguments(\ReflectionFunctionAbstract $reflection, array if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) { $typeName = $type->getName(); - if (ClientGateway::class === $typeName && isset($arguments['_session'])) { - $finalArgs[$paramPosition] = new ClientGateway($arguments['_session']); + if (RequestContext::class === $typeName && isset($arguments['_session'], $arguments['_request'])) { + $finalArgs[$paramPosition] = new RequestContext($arguments['_session'], $arguments['_request']); continue; } } @@ -156,10 +146,6 @@ private function getReflectionForCallable(callable $handler, SessionInterface $s if (\is_array($handler) && 2 === \count($handler)) { [$class, $method] = $handler; - if ($class instanceof ClientAwareInterface) { - $class->setClient(new ClientGateway($session)); - } - return new \ReflectionMethod($class, $method); } diff --git a/src/Schema/Request/SetLogLevelRequest.php b/src/Schema/Request/SetLogLevelRequest.php index ad7bee68..eb83e1c8 100644 --- a/src/Schema/Request/SetLogLevelRequest.php +++ b/src/Schema/Request/SetLogLevelRequest.php @@ -39,7 +39,7 @@ public static function getMethod(): string protected static function fromParams(?array $params): static { - if (!isset($params['level']) || !\is_string($params['level']) || empty($params['level'])) { + if (!isset($params['level']) || !\is_string($params['level']) || '' === $params['level']) { throw new InvalidArgumentException('Missing or invalid "level" parameter for "logging/setLevel".'); } diff --git a/src/Server/Builder.php b/src/Server/Builder.php index 4142b97a..da77afbc 100644 --- a/src/Server/Builder.php +++ b/src/Server/Builder.php @@ -478,7 +478,7 @@ public function build(): Server resourcesListChanged: $this->eventDispatcher instanceof EventDispatcherInterface, prompts: $registry->hasPrompts(), promptsListChanged: $this->eventDispatcher instanceof EventDispatcherInterface, - logging: false, + logging: true, completions: true, ); @@ -497,6 +497,7 @@ public function build(): Server new Handler\Request\ListToolsHandler($registry, $this->paginationLimit), new Handler\Request\PingHandler(), new Handler\Request\ReadResourceHandler($registry, $referenceHandler, $logger), + new Handler\Request\SetLogLevelHandler(), ]); $notificationHandlers = array_merge($this->notificationHandlers, [ diff --git a/src/Server/ClientAwareInterface.php b/src/Server/ClientAwareInterface.php deleted file mode 100644 index 86c8c2ef..00000000 --- a/src/Server/ClientAwareInterface.php +++ /dev/null @@ -1,17 +0,0 @@ -client = $client; - } - - private function notify(Notification $notification): void - { - $this->client->notify($notification); - } - - private function log(LoggingLevel $level, mixed $data, ?string $logger = null): void - { - $this->client->log($level, $data, $logger); - } - - private function progress(float $progress, ?float $total = null, ?string $message = null): void - { - $this->client->progress($progress, $total, $message); - } - - /** - * @param SampleOptions $options - */ - private function sample(string $prompt, int $maxTokens = 1000, int $timeout = 120, array $options = []): CreateSamplingMessageResult - { - return $this->client->sample($prompt, $maxTokens, $timeout, $options); - } -} diff --git a/src/Server/ClientGateway.php b/src/Server/ClientGateway.php index 8179aee2..e044f055 100644 --- a/src/Server/ClientGateway.php +++ b/src/Server/ClientGateway.php @@ -34,6 +34,7 @@ use Mcp\Server\Session\SessionInterface; /** + * @final * Helper class for tools to communicate with the client. * * This class provides a clean API for element handlers to send requests and notifications @@ -64,7 +65,7 @@ * * @author Kyrian Obikwelu */ -final class ClientGateway +class ClientGateway { public function __construct( private readonly SessionInterface $session, diff --git a/src/Server/Handler/Notification/InitializedHandler.php b/src/Server/Handler/Notification/InitializedHandler.php index 08dec76a..2dccc896 100644 --- a/src/Server/Handler/Notification/InitializedHandler.php +++ b/src/Server/Handler/Notification/InitializedHandler.php @@ -25,9 +25,9 @@ public function supports(Notification $notification): bool return $notification instanceof InitializedNotification; } - public function handle(Notification $message, SessionInterface $session): void + public function handle(Notification $notification, SessionInterface $session): void { - \assert($message instanceof InitializedNotification); + \assert($notification instanceof InitializedNotification); $session->set('initialized', true); } diff --git a/src/Server/Handler/Request/CallToolHandler.php b/src/Server/Handler/Request/CallToolHandler.php index e0430802..dd159afc 100644 --- a/src/Server/Handler/Request/CallToolHandler.php +++ b/src/Server/Handler/Request/CallToolHandler.php @@ -61,6 +61,7 @@ public function handle(Request $request, SessionInterface $session): Response|Er $reference = $this->registry->getTool($toolName); $arguments['_session'] = $session; + $arguments['_request'] = $request; $result = $this->referenceHandler->handle($reference, $arguments); diff --git a/src/Server/Handler/Request/GetPromptHandler.php b/src/Server/Handler/Request/GetPromptHandler.php index 274b8422..eb968a12 100644 --- a/src/Server/Handler/Request/GetPromptHandler.php +++ b/src/Server/Handler/Request/GetPromptHandler.php @@ -57,6 +57,7 @@ public function handle(Request $request, SessionInterface $session): Response|Er $reference = $this->registry->getPrompt($promptName); $arguments['_session'] = $session; + $arguments['_request'] = $request; $result = $this->referenceHandler->handle($reference, $arguments); diff --git a/src/Server/Handler/Request/ReadResourceHandler.php b/src/Server/Handler/Request/ReadResourceHandler.php index f955f4b1..105ef483 100644 --- a/src/Server/Handler/Request/ReadResourceHandler.php +++ b/src/Server/Handler/Request/ReadResourceHandler.php @@ -61,6 +61,7 @@ public function handle(Request $request, SessionInterface $session): Response|Er $arguments = [ 'uri' => $uri, '_session' => $session, + '_request' => $request, ]; if ($reference instanceof ResourceTemplateReference) { diff --git a/src/Server/Handler/Request/SetLogLevelHandler.php b/src/Server/Handler/Request/SetLogLevelHandler.php new file mode 100644 index 00000000..55638983 --- /dev/null +++ b/src/Server/Handler/Request/SetLogLevelHandler.php @@ -0,0 +1,49 @@ + + * + * @author Adam Jamiu + */ +final class SetLogLevelHandler implements RequestHandlerInterface +{ + public function supports(Request $request): bool + { + return $request instanceof SetLogLevelRequest; + } + + /** + * @return Response + */ + public function handle(Request $request, SessionInterface $session): Response + { + \assert($request instanceof SetLogLevelRequest); + + $session->set(Protocol::SESSION_LOGGING_LEVEL, $request->level->value); + + return new Response($request->getId(), new EmptyResult()); + } +} diff --git a/src/Server/Protocol.php b/src/Server/Protocol.php index 0851e922..5c2d1f0c 100644 --- a/src/Server/Protocol.php +++ b/src/Server/Protocol.php @@ -55,6 +55,8 @@ class Protocol /** Session key for active request meta */ public const SESSION_ACTIVE_REQUEST_META = '_mcp.active_request_meta'; + public const SESSION_LOGGING_LEVEL = '_mcp.logging_level'; + /** * @param array>> $requestHandlers * @param array $notificationHandlers diff --git a/src/Server/RequestContext.php b/src/Server/RequestContext.php new file mode 100644 index 00000000..158057cf --- /dev/null +++ b/src/Server/RequestContext.php @@ -0,0 +1,64 @@ + + */ +final class RequestContext +{ + private ?ClientGateway $clientGateway = null; + private ?ClientLogger $clientLogger = null; + + public function __construct( + private readonly SessionInterface $session, + private readonly Request $request, + ) { + } + + public function getRequest(): Request + { + return $this->request; + } + + public function getSession(): SessionInterface + { + return $this->session; + } + + public function getClientGateway(): ClientGateway + { + if (null == $this->clientGateway) { + $this->clientGateway = new ClientGateway($this->session); + } + + return $this->clientGateway; + } + + public function getClientLogger(): ClientLogger + { + if (null === $this->clientLogger) { + $this->clientLogger = new ClientLogger($this->getClientGateway(), $this->session); + } + + return $this->clientLogger; + } +} diff --git a/tests/Unit/Capability/Logger/ClientLoggerTest.php b/tests/Unit/Capability/Logger/ClientLoggerTest.php new file mode 100644 index 00000000..aa0dc486 --- /dev/null +++ b/tests/Unit/Capability/Logger/ClientLoggerTest.php @@ -0,0 +1,92 @@ +getMockBuilder(Session::class) + ->disableOriginalConstructor() + ->onlyMethods(['get']) + ->getMock(); + $session->expects($this->once())->method('get')->willReturn('info'); + $clientGateway = $this->getMockBuilder(ClientGateway::class) + ->disableOriginalConstructor() + ->onlyMethods(['log']) + ->getMock(); + $clientGateway->expects($this->once())->method('log')->with(LoggingLevel::Notice, 'test'); + + $logger = new ClientLogger($clientGateway, $session); + $logger->notice('test'); + } + + public function testLogFilter() + { + $session = $this->getMockBuilder(Session::class) + ->disableOriginalConstructor() + ->onlyMethods(['get']) + ->getMock(); + $session->expects($this->once())->method('get')->willReturn('info'); + $clientGateway = $this->getMockBuilder(ClientGateway::class) + ->disableOriginalConstructor() + ->onlyMethods(['log']) + ->getMock(); + $clientGateway->expects($this->never())->method('log'); + + $logger = new ClientLogger($clientGateway, $session); + $logger->debug('test'); + } + + public function testLogFilterSameLevel() + { + $session = $this->getMockBuilder(Session::class) + ->disableOriginalConstructor() + ->onlyMethods(['get']) + ->getMock(); + $session->expects($this->once())->method('get')->willReturn('info'); + $clientGateway = $this->getMockBuilder(ClientGateway::class) + ->disableOriginalConstructor() + ->onlyMethods(['log']) + ->getMock(); + $clientGateway->expects($this->once())->method('log'); + + $logger = new ClientLogger($clientGateway, $session); + $logger->info('test'); + } + + public function testLogWithInvalidLevel() + { + $session = $this->getMockBuilder(Session::class) + ->disableOriginalConstructor() + ->onlyMethods(['get']) + ->getMock(); + $session->expects($this->any())->method('get')->willReturn('info'); + $clientGateway = $this->getMockBuilder(ClientGateway::class) + ->disableOriginalConstructor() + ->onlyMethods(['log']) + ->getMock(); + $clientGateway->expects($this->never())->method('log'); + + $logger = new ClientLogger($clientGateway, $session); + $logger->log('foo', 'test'); + } +} diff --git a/tests/Unit/Server/Handler/Request/CallToolHandlerTest.php b/tests/Unit/Server/Handler/Request/CallToolHandlerTest.php index 5b03f2bb..eb2f2ab6 100644 --- a/tests/Unit/Server/Handler/Request/CallToolHandlerTest.php +++ b/tests/Unit/Server/Handler/Request/CallToolHandlerTest.php @@ -71,7 +71,7 @@ public function testHandleSuccessfulToolCall(): void $this->referenceHandler ->expects($this->once()) ->method('handle') - ->with($toolReference, ['name' => 'John', '_session' => $this->session]) + ->with($toolReference, ['name' => 'John', '_session' => $this->session, '_request' => $request]) ->willReturn('Hello, John!'); $toolReference @@ -104,7 +104,7 @@ public function testHandleToolCallWithEmptyArguments(): void $this->referenceHandler ->expects($this->once()) ->method('handle') - ->with($toolReference, ['_session' => $this->session]) + ->with($toolReference, ['_session' => $this->session, '_request' => $request]) ->willReturn('Simple result'); $toolReference @@ -141,7 +141,7 @@ public function testHandleToolCallWithComplexArguments(): void $this->referenceHandler ->expects($this->once()) ->method('handle') - ->with($toolReference, array_merge($arguments, ['_session' => $this->session])) + ->with($toolReference, array_merge($arguments, ['_session' => $this->session, '_request' => $request])) ->willReturn('Complex result'); $toolReference @@ -192,7 +192,7 @@ public function testHandleToolCallExceptionReturnsResponseWithErrorResult(): voi $this->referenceHandler ->expects($this->once()) ->method('handle') - ->with($toolReference, ['param' => 'value', '_session' => $this->session]) + ->with($toolReference, ['param' => 'value', '_session' => $this->session, '_request' => $request]) ->willThrowException($exception); $this->logger @@ -227,7 +227,7 @@ public function testHandleWithNullResult(): void $this->referenceHandler ->expects($this->once()) ->method('handle') - ->with($toolReference, ['_session' => $this->session]) + ->with($toolReference, ['_session' => $this->session, '_request' => $request]) ->willReturn(null); $toolReference @@ -264,7 +264,7 @@ public function testHandleLogsErrorWithCorrectParameters(): void $this->referenceHandler ->expects($this->once()) ->method('handle') - ->with($toolReference, ['key1' => 'value1', 'key2' => 42, '_session' => $this->session]) + ->with($toolReference, ['key1' => 'value1', 'key2' => 42, '_session' => $this->session, '_request' => $request]) ->willThrowException($exception); $this->logger @@ -274,7 +274,7 @@ public function testHandleLogsErrorWithCorrectParameters(): void 'Error while executing tool "test_tool": "Custom error message".', [ 'tool' => 'test_tool', - 'arguments' => ['key1' => 'value1', 'key2' => 42, '_session' => $this->session], + 'arguments' => ['key1' => 'value1', 'key2' => 42, '_session' => $this->session, '_request' => $request], ], ); @@ -307,7 +307,7 @@ public function testHandleGenericExceptionReturnsError(): void $this->referenceHandler ->expects($this->once()) ->method('handle') - ->with($toolReference, ['param' => 'value', '_session' => $this->session]) + ->with($toolReference, ['param' => 'value', '_session' => $this->session, '_request' => $request]) ->willThrowException($exception); $response = $this->handler->handle($request, $this->session); @@ -334,7 +334,7 @@ public function testHandleWithSpecialCharactersInToolName(): void $this->referenceHandler ->expects($this->once()) ->method('handle') - ->with($toolReference, ['_session' => $this->session]) + ->with($toolReference, ['_session' => $this->session, '_request' => $request]) ->willReturn('Special tool result'); $toolReference @@ -369,7 +369,7 @@ public function testHandleWithSpecialCharactersInArguments(): void $this->referenceHandler ->expects($this->once()) ->method('handle') - ->with($toolReference, array_merge($arguments, ['_session' => $this->session])) + ->with($toolReference, array_merge($arguments, ['_session' => $this->session, '_request' => $request])) ->willReturn('Unicode handled'); $toolReference @@ -399,7 +399,7 @@ public function testHandleReturnsStructuredContentResult(): void $this->referenceHandler ->expects($this->once()) ->method('handle') - ->with($toolReference, ['query' => 'php', '_session' => $this->session]) + ->with($toolReference, ['query' => 'php', '_session' => $this->session, '_request' => $request]) ->willReturn($structuredResult); $toolReference @@ -428,7 +428,7 @@ public function testHandleReturnsCallToolResult(): void $this->referenceHandler ->expects($this->once()) ->method('handle') - ->with($toolReference, ['query' => 'php', '_session' => $this->session]) + ->with($toolReference, ['query' => 'php', '_session' => $this->session, '_request' => $request]) ->willReturn($callToolResult); $toolReference diff --git a/tests/Unit/Server/Handler/Request/GetPromptHandlerTest.php b/tests/Unit/Server/Handler/Request/GetPromptHandlerTest.php index 95b2e5c1..75503e39 100644 --- a/tests/Unit/Server/Handler/Request/GetPromptHandlerTest.php +++ b/tests/Unit/Server/Handler/Request/GetPromptHandlerTest.php @@ -70,7 +70,7 @@ public function testHandleSuccessfulPromptGet(): void $this->referenceHandler ->expects($this->once()) ->method('handle') - ->with($promptReference, ['_session' => $this->session]) + ->with($promptReference, ['_session' => $this->session, '_request' => $request]) ->willReturn($expectedMessages); $promptReference @@ -112,7 +112,7 @@ public function testHandlePromptGetWithArguments(): void $this->referenceHandler ->expects($this->once()) ->method('handle') - ->with($promptReference, array_merge($arguments, ['_session' => $this->session])) + ->with($promptReference, array_merge($arguments, ['_session' => $this->session, '_request' => $request])) ->willReturn($expectedMessages); $promptReference @@ -145,7 +145,7 @@ public function testHandlePromptGetWithNullArguments(): void $this->referenceHandler ->expects($this->once()) ->method('handle') - ->with($promptReference, ['_session' => $this->session]) + ->with($promptReference, ['_session' => $this->session, '_request' => $request]) ->willReturn($expectedMessages); $promptReference @@ -178,7 +178,7 @@ public function testHandlePromptGetWithEmptyArguments(): void $this->referenceHandler ->expects($this->once()) ->method('handle') - ->with($promptReference, ['_session' => $this->session]) + ->with($promptReference, ['_session' => $this->session, '_request' => $request]) ->willReturn($expectedMessages); $promptReference @@ -213,7 +213,7 @@ public function testHandlePromptGetWithMultipleMessages(): void $this->referenceHandler ->expects($this->once()) ->method('handle') - ->with($promptReference, ['_session' => $this->session]) + ->with($promptReference, ['_session' => $this->session, '_request' => $request]) ->willReturn($expectedMessages); $promptReference @@ -299,7 +299,7 @@ public function testHandlePromptGetWithComplexArguments(): void $this->referenceHandler ->expects($this->once()) ->method('handle') - ->with($promptReference, array_merge($arguments, ['_session' => $this->session])) + ->with($promptReference, array_merge($arguments, ['_session' => $this->session, '_request' => $request])) ->willReturn($expectedMessages); $promptReference @@ -337,7 +337,7 @@ public function testHandlePromptGetWithSpecialCharacters(): void $this->referenceHandler ->expects($this->once()) ->method('handle') - ->with($promptReference, array_merge($arguments, ['_session' => $this->session])) + ->with($promptReference, array_merge($arguments, ['_session' => $this->session, '_request' => $request])) ->willReturn($expectedMessages); $promptReference @@ -367,7 +367,7 @@ public function testHandlePromptGetReturnsEmptyMessages(): void $this->referenceHandler ->expects($this->once()) ->method('handle') - ->with($promptReference, ['_session' => $this->session]) + ->with($promptReference, ['_session' => $this->session, '_request' => $request]) ->willReturn([]); $promptReference @@ -405,7 +405,7 @@ public function testHandlePromptGetWithLargeNumberOfArguments(): void $this->referenceHandler ->expects($this->once()) ->method('handle') - ->with($promptReference, array_merge($arguments, ['_session' => $this->session])) + ->with($promptReference, array_merge($arguments, ['_session' => $this->session, '_request' => $request])) ->willReturn($expectedMessages); $promptReference diff --git a/tests/Unit/Server/Handler/Request/ReadResourceHandlerTest.php b/tests/Unit/Server/Handler/Request/ReadResourceHandlerTest.php index a4ed0b17..7483f1c1 100644 --- a/tests/Unit/Server/Handler/Request/ReadResourceHandlerTest.php +++ b/tests/Unit/Server/Handler/Request/ReadResourceHandlerTest.php @@ -75,7 +75,7 @@ public function testHandleSuccessfulResourceRead(): void $this->referenceHandler ->expects($this->once()) ->method('handle') - ->with($resourceReference, ['uri' => $uri, '_session' => $this->session]) + ->with($resourceReference, ['uri' => $uri, '_session' => $this->session, '_request' => $request]) ->willReturn('test'); $resourceReference @@ -115,7 +115,7 @@ public function testHandleResourceReadWithBlobContent(): void $this->referenceHandler ->expects($this->once()) ->method('handle') - ->with($resourceReference, ['uri' => $uri, '_session' => $this->session]) + ->with($resourceReference, ['uri' => $uri, '_session' => $this->session, '_request' => $request]) ->willReturn('fake-image-data'); $resourceReference @@ -159,7 +159,7 @@ public function testHandleResourceReadWithMultipleContents(): void $this->referenceHandler ->expects($this->once()) ->method('handle') - ->with($resourceReference, ['uri' => $uri, '_session' => $this->session]) + ->with($resourceReference, ['uri' => $uri, '_session' => $this->session, '_request' => $request]) ->willReturn('binary-data'); $resourceReference @@ -267,7 +267,7 @@ public function testHandleResourceReadWithDifferentUriSchemes(): void $this->referenceHandler ->expects($this->once()) ->method('handle') - ->with($resourceReference, ['uri' => $uri, '_session' => $this->session]) + ->with($resourceReference, ['uri' => $uri, '_session' => $this->session, '_request' => $request]) ->willReturn('test'); $resourceReference @@ -312,7 +312,7 @@ public function testHandleResourceReadWithEmptyContent(): void $this->referenceHandler ->expects($this->once()) ->method('handle') - ->with($resourceReference, ['uri' => $uri, '_session' => $this->session]) + ->with($resourceReference, ['uri' => $uri, '_session' => $this->session, '_request' => $request]) ->willReturn(''); $resourceReference @@ -374,7 +374,7 @@ public function testHandleResourceReadWithDifferentMimeTypes(): void $this->referenceHandler ->expects($this->once()) ->method('handle') - ->with($resourceReference, ['uri' => $uri, '_session' => $this->session]) + ->with($resourceReference, ['uri' => $uri, '_session' => $this->session, '_request' => $request]) ->willReturn($expectedContent); $resourceReference diff --git a/tests/Unit/Server/Handler/Request/SetLogLevelHandlerTest.php b/tests/Unit/Server/Handler/Request/SetLogLevelHandlerTest.php new file mode 100644 index 00000000..6aca135a --- /dev/null +++ b/tests/Unit/Server/Handler/Request/SetLogLevelHandlerTest.php @@ -0,0 +1,74 @@ + + */ +class SetLogLevelHandlerTest extends TestCase +{ + public function testSupports(): void + { + $request = $this->createSetLogLevelRequest(LoggingLevel::Info); + $handler = new SetLogLevelHandler(); + $this->assertTrue($handler->supports($request)); + } + + public function testDoesNotSupportOtherRequests(): void + { + $otherRequest = $this->createMock(Request::class); + $handler = new SetLogLevelHandler(); + $this->assertFalse($handler->supports($otherRequest)); + } + + public function testHandleAllLogLevelsAndSupport(): void + { + $handler = new SetLogLevelHandler(); + + foreach (LoggingLevel::cases() as $level) { + $request = $this->createSetLogLevelRequest($level); + + $session = $this->getMockBuilder(Session::class) + ->disableOriginalConstructor() + ->onlyMethods(['set']) + ->getMock(); + $session->expects($this->once()) + ->method('set') + ->with(Protocol::SESSION_LOGGING_LEVEL, $level->value); + + $response = $handler->handle($request, $session); + $this->assertEquals($request->getId(), $response->id); + $this->assertInstanceOf(EmptyResult::class, $response->result); + } + } + + private function createSetLogLevelRequest(LoggingLevel $level): SetLogLevelRequest + { + return SetLogLevelRequest::fromArray([ + 'jsonrpc' => '2.0', + 'method' => SetLogLevelRequest::getMethod(), + 'id' => 'test-request-'.uniqid(), + 'params' => [ + 'level' => $level->value, + ], + ]); + } +}