Skip to content

Commit 06aec0e

Browse files
Adebayo120Nyholm
authored andcommitted
Implemented MCP logging specification with auto-injection
1 parent 32757f6 commit 06aec0e

26 files changed

+1804
-6
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
"Mcp\\Example\\EnvVariables\\": "examples/env-variables/",
6464
"Mcp\\Example\\ExplicitRegistration\\": "examples/explicit-registration/",
6565
"Mcp\\Example\\SchemaShowcase\\": "examples/schema-showcase/",
66+
"Mcp\\Example\\StdioLoggingShowcase\\": "examples/stdio-logging-showcase/",a
6667
"Mcp\\Tests\\": "tests/"
6768
}
6869
},

docs/mcp-elements.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ discovery and manual registration methods.
1111
- [Resources](#resources)
1212
- [Resource Templates](#resource-templates)
1313
- [Prompts](#prompts)
14+
- [Logging](#logging)
1415
- [Completion Providers](#completion-providers)
1516
- [Schema Generation and Validation](#schema-generation-and-validation)
1617
- [Discovery vs Manual Registration](#discovery-vs-manual-registration)
@@ -504,6 +505,55 @@ public function generatePrompt(string $topic, string $style): array
504505

505506
**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.
506507

508+
## Logging
509+
510+
The SDK provides automatic logging support, handlers can receive logger instances automatically to send structured log messages to clients.
511+
512+
### Configuration
513+
514+
Logging is **enabled by default**. Use `disableClientLogging()` to turn it off:
515+
516+
```php
517+
// Logging enabled (default)
518+
$server = Server::builder()->build();
519+
520+
// Disable logging
521+
$server = Server::builder()
522+
->disableClientLogging()
523+
->build();
524+
```
525+
526+
### Auto-injection
527+
528+
The SDK automatically injects logger instances into handlers:
529+
530+
```php
531+
use Mcp\Capability\Logger\ClientLogger;
532+
use Psr\Log\LoggerInterface;
533+
534+
#[McpTool]
535+
public function processData(string $input, ClientLogger $logger): array {
536+
$logger->info('Processing started', ['input' => $input]);
537+
$logger->warning('Deprecated API used');
538+
539+
// ... processing logic ...
540+
541+
$logger->info('Processing completed');
542+
return ['result' => 'processed'];
543+
}
544+
545+
// Also works with PSR-3 LoggerInterface
546+
#[McpResource(uri: 'data://config')]
547+
public function getConfig(LoggerInterface $logger): array {
548+
$logger->info('Configuration accessed');
549+
return ['setting' => 'value'];
550+
}
551+
```
552+
553+
### Log Levels
554+
555+
The SDK supports all standard PSR-3 log levels with **warning** as the default level:
556+
507557
## Completion Providers
508558

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

docs/server-builder.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,7 @@ $server = Server::builder()
577577
| `addRequestHandlers()` | handlers | Prepend multiple custom request handlers |
578578
| `addNotificationHandler()` | handler | Prepend a single custom notification handler |
579579
| `addNotificationHandlers()` | handlers | Prepend multiple custom notification handlers |
580+
| `disableClientLogging()` | - | Disable MCP client logging (enabled by default) |
580581
| `addTool()` | handler, name?, description?, annotations?, inputSchema? | Register tool |
581582
| `addResource()` | handler, uri, name?, description?, mimeType?, size?, annotations? | Register resource |
582583
| `addResourceTemplate()` | handler, uriTemplate, name?, description?, mimeType?, annotations? | Register resource template |
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Mcp\Example\StdioLoggingShowcase;
13+
14+
use Mcp\Capability\Attribute\McpTool;
15+
use Mcp\Capability\Logger\ClientLogger;
16+
17+
/**
18+
* Example handlers showcasing auto-injected MCP logging capabilities.
19+
*
20+
* This demonstrates how handlers can receive ClientLogger automatically
21+
* without any manual configuration - just declare it as a parameter!
22+
*/
23+
final class LoggingShowcaseHandlers
24+
{
25+
/**
26+
* Tool that demonstrates different logging levels with auto-injected ClientLogger.
27+
*
28+
* @param string $message The message to log
29+
* @param string $level The logging level (debug, info, warning, error)
30+
* @param ClientLogger $logger Auto-injected MCP logger
31+
*
32+
* @return array<string, mixed>
33+
*/
34+
#[McpTool(name: 'log_message', description: 'Demonstrates MCP logging with different levels')]
35+
public function logMessage(string $message, string $level, ClientLogger $logger): array
36+
{
37+
$logger->info('🚀 Starting log_message tool', [
38+
'requested_level' => $level,
39+
'message_length' => \strlen($message),
40+
]);
41+
42+
switch (strtolower($level)) {
43+
case 'debug':
44+
$logger->debug("Debug: $message", ['tool' => 'log_message']);
45+
break;
46+
case 'info':
47+
$logger->info("Info: $message", ['tool' => 'log_message']);
48+
break;
49+
case 'notice':
50+
$logger->notice("Notice: $message", ['tool' => 'log_message']);
51+
break;
52+
case 'warning':
53+
$logger->warning("Warning: $message", ['tool' => 'log_message']);
54+
break;
55+
case 'error':
56+
$logger->error("Error: $message", ['tool' => 'log_message']);
57+
break;
58+
case 'critical':
59+
$logger->critical("Critical: $message", ['tool' => 'log_message']);
60+
break;
61+
case 'alert':
62+
$logger->alert("Alert: $message", ['tool' => 'log_message']);
63+
break;
64+
case 'emergency':
65+
$logger->emergency("Emergency: $message", ['tool' => 'log_message']);
66+
break;
67+
default:
68+
$logger->warning("Unknown level '$level', defaulting to info");
69+
$logger->info("Info: $message", ['tool' => 'log_message']);
70+
}
71+
72+
$logger->debug('log_message tool completed successfully');
73+
74+
return [
75+
'message' => "Logged message with level: $level",
76+
'logged_at' => date('Y-m-d H:i:s'),
77+
'level_used' => $level,
78+
];
79+
}
80+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#!/usr/bin/env php
2+
<?php
3+
4+
/*
5+
* This file is part of the official PHP MCP SDK.
6+
*
7+
* A collaboration between Symfony and the PHP Foundation.
8+
*
9+
* For the full copyright and license information, please view the LICENSE
10+
* file that was distributed with this source code.
11+
*/
12+
13+
require_once dirname(__DIR__).'/bootstrap.php';
14+
chdir(__DIR__);
15+
16+
use Mcp\Server;
17+
use Mcp\Server\Transport\StdioTransport;
18+
19+
logger()->info('Starting MCP Stdio Logging Showcase Server...');
20+
21+
// Create server with auto-discovery of MCP capabilities and ENABLE MCP LOGGING
22+
$server = Server::builder()
23+
->setServerInfo('Stdio Logging Showcase', '1.0.0', 'Demonstration of auto-injected MCP logging in capability handlers.')
24+
->setContainer(container())
25+
->setLogger(logger())
26+
->setDiscovery(__DIR__, ['.'])
27+
->build();
28+
29+
$transport = new StdioTransport(logger: logger());
30+
31+
$server->run($transport);
32+
33+
logger()->info('Logging Showcase Server is ready!');
34+
logger()->info('This example demonstrates auto-injection of ClientLogger into capability handlers.');
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Mcp\Capability\Logger;
13+
14+
use Mcp\Schema\Enum\LoggingLevel;
15+
use Mcp\Server\NotificationSender;
16+
use Psr\Log\AbstractLogger;
17+
use Psr\Log\LoggerInterface;
18+
19+
/**
20+
* MCP-aware PSR-3 logger that sends log messages as MCP notifications.
21+
*
22+
* This logger implements the standard PSR-3 LoggerInterface and forwards
23+
* log messages to the NotificationSender. The NotificationHandler will
24+
* decide whether to actually send the notification based on capabilities.
25+
*
26+
* @author Adam Jamiu <[email protected]>
27+
*/
28+
final class ClientLogger extends AbstractLogger
29+
{
30+
public function __construct(
31+
private readonly NotificationSender $notificationSender,
32+
private readonly ?LoggerInterface $fallbackLogger = null,
33+
) {
34+
}
35+
36+
/**
37+
* Logs with an arbitrary level.
38+
*
39+
* @param string|\Stringable $message
40+
* @param array<string, mixed> $context
41+
*/
42+
public function log($level, $message, array $context = []): void
43+
{
44+
// Always log to fallback logger if provided (for local debugging)
45+
$this->fallbackLogger?->log($level, $message, $context);
46+
47+
// Convert PSR-3 level to MCP LoggingLevel
48+
$mcpLevel = $this->convertToMcpLevel($level);
49+
if (null === $mcpLevel) {
50+
return; // Unknown level, skip MCP notification
51+
}
52+
53+
// Send MCP logging notification - let NotificationHandler decide if it should be sent
54+
try {
55+
$this->notificationSender->send('notifications/message', [
56+
'level' => $mcpLevel->value,
57+
'data' => (string) $message,
58+
'logger' => $context['logger'] ?? null,
59+
]);
60+
} catch (\Throwable $e) {
61+
// If MCP notification fails, at least log to fallback
62+
$this->fallbackLogger?->error('Failed to send MCP log notification', [
63+
'original_level' => $level,
64+
'original_message' => $message,
65+
'error' => $e->getMessage(),
66+
]);
67+
}
68+
}
69+
70+
/**
71+
* Converts PSR-3 log level to MCP LoggingLevel.
72+
*
73+
* @param mixed $level PSR-3 level
74+
*
75+
* @return LoggingLevel|null MCP level or null if unknown
76+
*/
77+
private function convertToMcpLevel($level): ?LoggingLevel
78+
{
79+
return match (strtolower((string) $level)) {
80+
'emergency' => LoggingLevel::Emergency,
81+
'alert' => LoggingLevel::Alert,
82+
'critical' => LoggingLevel::Critical,
83+
'error' => LoggingLevel::Error,
84+
'warning' => LoggingLevel::Warning,
85+
'notice' => LoggingLevel::Notice,
86+
'info' => LoggingLevel::Info,
87+
'debug' => LoggingLevel::Debug,
88+
default => null,
89+
};
90+
}
91+
}

src/Capability/Registry.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use Mcp\Exception\PromptNotFoundException;
2626
use Mcp\Exception\ResourceNotFoundException;
2727
use Mcp\Exception\ToolNotFoundException;
28+
use Mcp\Schema\Enum\LoggingLevel;
2829
use Mcp\Schema\Page;
2930
use Mcp\Schema\Prompt;
3031
use Mcp\Schema\Resource;
@@ -61,6 +62,10 @@ final class Registry implements RegistryInterface
6162
*/
6263
private array $resourceTemplates = [];
6364

65+
private bool $logging = true;
66+
67+
private LoggingLevel $loggingLevel = LoggingLevel::Warning;
68+
6469
public function __construct(
6570
private readonly ?EventDispatcherInterface $eventDispatcher = null,
6671
private readonly LoggerInterface $logger = new NullLogger(),
@@ -391,6 +396,46 @@ public function setDiscoveryState(DiscoveryState $state): void
391396
}
392397
}
393398

399+
400+
/**
401+
* Disable logging message notifications for this registry.
402+
*/
403+
public function disableLogging(): void
404+
{
405+
$this->logging = false;
406+
}
407+
408+
/**
409+
* Checks if logging message notification capability is enabled.
410+
*
411+
* @return bool True if logging capability is enabled, false otherwise
412+
*/
413+
public function isLoggingEnabled(): bool
414+
{
415+
return $this->logging;
416+
}
417+
418+
/**
419+
* Sets the current logging message notification level for the client.
420+
*
421+
* This determines which log messages should be sent to the client.
422+
* Only messages at this level and higher (more severe) will be sent.
423+
*/
424+
public function setLoggingLevel(LoggingLevel $level): void
425+
{
426+
$this->loggingLevel = $level;
427+
}
428+
429+
/**
430+
* Gets the current logging message notification level set by the client.
431+
*
432+
* @return LoggingLevel The current log level
433+
*/
434+
public function getLoggingLevel(): LoggingLevel
435+
{
436+
return $this->loggingLevel;
437+
}
438+
394439
/**
395440
* Calculate next cursor for pagination.
396441
*

src/Schema/Enum/LoggingLevel.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
* https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1
1919
*
2020
* @author Kyrian Obikwelu <[email protected]>
21+
* @author Adam Jamiu <[email protected]>
2122
*/
2223
enum LoggingLevel: string
2324
{
@@ -29,4 +30,24 @@ enum LoggingLevel: string
2930
case Critical = 'critical';
3031
case Alert = 'alert';
3132
case Emergency = 'emergency';
33+
34+
/**
35+
* Gets the severity index for this log level.
36+
* Higher values indicate more severe log levels.
37+
*
38+
* @return int Severity index (0-7, where 7 is most severe)
39+
*/
40+
public function getSeverityIndex(): int
41+
{
42+
return match ($this) {
43+
self::Debug => 0,
44+
self::Info => 1,
45+
self::Notice => 2,
46+
self::Warning => 3,
47+
self::Error => 4,
48+
self::Critical => 5,
49+
self::Alert => 6,
50+
self::Emergency => 7,
51+
};
52+
}
3253
}

0 commit comments

Comments
 (0)