Add RequestContext and logging (#176)

* Implemented MCP logging specification with auto-injection

* Add RequestContext

* fix tests and style

* Better deprecation

* fix: minor

* Remove ClientAwareInterface

* fix cs

* fix docs

---------

Co-authored-by: Adam Jamiu <jamiuadam120@gmail.com>
This commit is contained in:
Tobias Nyholm 2025-12-16 00:12:02 +01:00 • committed by GitHub
commit a1e0f61b6b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 578 additions and 162 deletions

View file

@ -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/"
}
},

View file

@ -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
public function myTool(RequestContext $context): 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
{
$this->log(...);
$context->getClientGateway()->log(...);
```
## Sampling

View file

@ -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

View file

@ -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.

View file

@ -14,14 +14,11 @@ namespace Mcp\Example\ClientCommunication;
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 @@ final class ClientAwareService implements ClientAwareInterface
* @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 @@ final class ClientAwareService implements ClientAwareInterface
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 @@ final class ClientAwareService implements ClientAwareInterface
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,

View file

@ -0,0 +1,81 @@
<?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\Example\ClientLogging;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Capability\Logger\ClientLogger;
use Mcp\Server\RequestContext;
/**
* Example handlers showcasing auto-injected MCP logging capabilities.
*
* This demonstrates how handlers can receive ClientLogger automatically
* without any manual configuration - just declare it as a parameter!
*/
final class LoggingShowcaseHandlers
{
/**
* Tool that demonstrates different logging levels.
*
* @param string $message The message to log
* @param string $level The logging level (debug, info, warning, error)
*
* @return array<string, mixed>
*/
#[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,
];
}
}

View file

@ -0,0 +1,29 @@
#!/usr/bin/env php
<?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.
*/
require_once dirname(__DIR__).'/bootstrap.php';
chdir(__DIR__);
use Mcp\Server;
$server = Server::builder()
->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);

View file

@ -13,6 +13,7 @@ namespace Mcp\Capability\Discovery;
use Mcp\Capability\Attribute\Schema;
use Mcp\Server\ClientGateway;
use Mcp\Server\RequestContext;
use phpDocumentor\Reflection\DocBlock\Tags\Param;
/**
@ -415,7 +416,7 @@ class SchemaGenerator
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;
}
}

View file

@ -0,0 +1,99 @@
<?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\Capability\Logger;
use Mcp\Schema\Enum\LoggingLevel;
use Mcp\Server\ClientGateway;
use Mcp\Server\Protocol;
use Mcp\Server\Session\SessionInterface;
use Psr\Log\AbstractLogger;
/**
* MCP-aware PSR-3 logger that sends log messages as MCP notifications.
*
* @author Adam Jamiu <jamiuadam120@gmail.com>
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
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<string, mixed> $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,
};
}
}

View file

@ -13,8 +13,7 @@ namespace Mcp\Capability\Registry;
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 @@ final class ReferenceHandler implements ReferenceHandlerInterface
$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 @@ final class ReferenceHandler implements ReferenceHandlerInterface
[$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 @@ final class ReferenceHandler implements ReferenceHandlerInterface
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 @@ final class ReferenceHandler implements ReferenceHandlerInterface
if (\is_array($handler) && 2 === \count($handler)) {
[$class, $method] = $handler;
if ($class instanceof ClientAwareInterface) {
$class->setClient(new ClientGateway($session));
}
return new \ReflectionMethod($class, $method);
}

View file

@ -39,7 +39,7 @@ final class SetLogLevelRequest extends Request
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".');
}

View file

@ -490,7 +490,7 @@ final class Builder
resourcesListChanged: $this->eventDispatcher instanceof EventDispatcherInterface,
prompts: $registry->hasPrompts(),
promptsListChanged: $this->eventDispatcher instanceof EventDispatcherInterface,
logging: false,
logging: true,
completions: true,
);
@ -509,6 +509,7 @@ final class Builder
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, [

View file

@ -1,17 +0,0 @@
<?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\Server;
interface ClientAwareInterface
{
public function setClient(ClientGateway $clientGateway): void;
}

View file

@ -1,52 +0,0 @@
<?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\Server;
use Mcp\Schema\Enum\LoggingLevel;
use Mcp\Schema\JsonRpc\Notification;
use Mcp\Schema\Result\CreateSamplingMessageResult;
/**
* @phpstan-import-type SampleOptions from ClientGateway
*/
trait ClientAwareTrait
{
private ClientGateway $client;
public function setClient(ClientGateway $client): void
{
$this->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);
}
}

View file

@ -34,6 +34,7 @@ use Mcp\Schema\Result\CreateSamplingMessageResult;
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 @@ use Mcp\Server\Session\SessionInterface;
*
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
*/
final class ClientGateway
class ClientGateway
{
public function __construct(
private readonly SessionInterface $session,

View file

@ -25,9 +25,9 @@ final class InitializedHandler implements NotificationHandlerInterface
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);
}

View file

@ -61,6 +61,7 @@ final class CallToolHandler implements RequestHandlerInterface
$reference = $this->registry->getTool($toolName);
$arguments['_session'] = $session;
$arguments['_request'] = $request;
$result = $this->referenceHandler->handle($reference, $arguments);

View file

@ -57,6 +57,7 @@ final class GetPromptHandler implements RequestHandlerInterface
$reference = $this->registry->getPrompt($promptName);
$arguments['_session'] = $session;
$arguments['_request'] = $request;
$result = $this->referenceHandler->handle($reference, $arguments);

View file

@ -61,6 +61,7 @@ final class ReadResourceHandler implements RequestHandlerInterface
$arguments = [
'uri' => $uri,
'_session' => $session,
'_request' => $request,
];
if ($reference instanceof ResourceTemplateReference) {

View file

@ -0,0 +1,49 @@
<?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\Server\Handler\Request;
use Mcp\Schema\JsonRpc\Request;
use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\Request\SetLogLevelRequest;
use Mcp\Schema\Result\EmptyResult;
use Mcp\Server\Protocol;
use Mcp\Server\Session\SessionInterface;
/**
* Handler for the logging/setLevel request.
*
* Handles client requests to set the logging level for the server.
* The server should send all logs at this level and higher (more severe) to the client.
*
* @implements RequestHandlerInterface<EmptyResult>
*
* @author Adam Jamiu <jamiuadam120@gmail.com>
*/
final class SetLogLevelHandler implements RequestHandlerInterface
{
public function supports(Request $request): bool
{
return $request instanceof SetLogLevelRequest;
}
/**
* @return Response<EmptyResult>
*/
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());
}
}

Some files were not shown because too many files have changed in this diff Show more