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 composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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/"
}
},
Expand Down
28 changes: 28 additions & 0 deletions docs/mcp-elements.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 Psr\Log\LoggerInterface;

#[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.
Expand Down
81 changes: 81 additions & 0 deletions examples/client-logging/LoggingShowcaseHandlers.php
Original file line number Diff line number Diff line change
@@ -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,
];
}
}
29 changes: 29 additions & 0 deletions examples/client-logging/server.php
Original file line number Diff line number Diff line change
@@ -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);
3 changes: 2 additions & 1 deletion src/Capability/Discovery/SchemaGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use Mcp\Capability\Attribute\Schema;
use Mcp\Server\ClientGateway;
use Mcp\Server\RequestContext;
use phpDocumentor\Reflection\DocBlock\Tags\Param;

/**
Expand Down Expand Up @@ -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;
}
}
Expand Down
99 changes: 99 additions & 0 deletions src/Capability/Logger/ClientLogger.php
Original file line number Diff line number Diff line change
@@ -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,
};
}
}
7 changes: 7 additions & 0 deletions src/Capability/Registry/ReferenceHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
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;

Expand Down Expand Up @@ -109,9 +110,15 @@ private function prepareArguments(\ReflectionFunctionAbstract $reflection, array
$typeName = $type->getName();

if (ClientGateway::class === $typeName && isset($arguments['_session'])) {
// Deprecated, use RequestContext instead
$finalArgs[$paramPosition] = new ClientGateway($arguments['_session']);
continue;
}

if (RequestContext::class === $typeName && isset($arguments['_session'], $arguments['_request'])) {
$finalArgs[$paramPosition] = new RequestContext($arguments['_session'], $arguments['_request']);
continue;
}
}

if (isset($arguments[$paramName])) {
Expand Down
2 changes: 1 addition & 1 deletion src/Schema/Request/SetLogLevelRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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".');
}

Expand Down
3 changes: 2 additions & 1 deletion src/Server/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);

Expand All @@ -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, [
Expand Down
3 changes: 3 additions & 0 deletions src/Server/ClientAwareInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@

namespace Mcp\Server;

/**
* @deprecated This is deprecated since 0.2.0 and will be removed in 0.3.0. Use RequestContext with argument injection instead.
*/
interface ClientAwareInterface
{
public function setClient(ClientGateway $clientGateway): void;
Expand Down
2 changes: 2 additions & 0 deletions src/Server/ClientAwareTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

/**
* @phpstan-import-type SampleOptions from ClientGateway
*
* @deprecated since 0.2.0, to be removed in 0.3.0. Use the RequestContext->getClientGateway() directly instead.
*/
trait ClientAwareTrait
{
Expand Down
3 changes: 2 additions & 1 deletion src/Server/ClientGateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -64,7 +65,7 @@
*
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
*/
final class ClientGateway
class ClientGateway
{
public function __construct(
private readonly SessionInterface $session,
Expand Down
4 changes: 2 additions & 2 deletions src/Server/Handler/Notification/InitializedHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
1 change: 1 addition & 0 deletions src/Server/Handler/Request/CallToolHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
1 change: 1 addition & 0 deletions src/Server/Handler/Request/GetPromptHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Loading