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 @@ -60,6 +60,7 @@
"Mcp\\Example\\StdioDiscoveryCalculator\\": "examples/stdio-discovery-calculator/",
"Mcp\\Example\\StdioEnvVariables\\": "examples/stdio-env-variables/",
"Mcp\\Example\\StdioExplicitRegistration\\": "examples/stdio-explicit-registration/",
"Mcp\\Example\\StdioLoggingShowcase\\": "examples/stdio-logging-showcase/",
"Mcp\\Tests\\": "tests/"
}
},
Expand Down
50 changes: 50 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 @@ -478,6 +479,55 @@ public function generatePrompt(string $topic, string $style): array

The SDK automatically validates that all messages have valid roles and converts the result into the appropriate MCP prompt message format.

## Logging

The SDK provides automatic logging support, handlers can receive logger instances automatically to send structured log messages to clients.

### Configuration

Logging is **enabled by default**. Use `disableClientLogging()` to turn it off:

```php
// Logging enabled (default)
$server = Server::builder()->build();

// Disable logging
$server = Server::builder()
->disableClientLogging()
->build();
```

### Auto-injection

The SDK automatically injects logger instances into handlers:

```php
use Mcp\Capability\Logger\ClientLogger;
use Psr\Log\LoggerInterface;

#[McpTool]
public function processData(string $input, ClientLogger $logger): array {
$logger->info('Processing started', ['input' => $input]);
$logger->warning('Deprecated API used');

// ... processing logic ...

$logger->info('Processing completed');
return ['result' => 'processed'];
}

// Also works with PSR-3 LoggerInterface
#[McpResource(uri: 'data://config')]
public function getConfig(LoggerInterface $logger): array {
$logger->info('Configuration accessed');
return ['setting' => 'value'];
}
```

### Log Levels

The SDK supports all standard PSR-3 log levels with **warning** as the default level:

## Completion Providers

Completion providers help MCP clients offer auto-completion suggestions for Resource Templates and Prompts. Unlike Tools
Expand Down
1 change: 1 addition & 0 deletions docs/server-builder.md
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,7 @@ $server = Server::builder()
| `addRequestHandlers()` | handlers | Prepend multiple custom request handlers |
| `addNotificationHandler()` | handler | Prepend a single custom notification handler |
| `addNotificationHandlers()` | handlers | Prepend multiple custom notification handlers |
| `disableClientLogging()` | - | Disable MCP client logging (enabled by default) |
| `addTool()` | handler, name?, description?, annotations?, inputSchema? | Register tool |
| `addResource()` | handler, uri, name?, description?, mimeType?, size?, annotations? | Register resource |
| `addResourceTemplate()` | handler, uriTemplate, name?, description?, mimeType?, annotations? | Register resource template |
Expand Down
72 changes: 62 additions & 10 deletions examples/stdio-discovery-calculator/McpElements.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use Mcp\Capability\Attribute\McpResource;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Capability\Logger\ClientLogger;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;

Expand Down Expand Up @@ -40,14 +41,15 @@ public function __construct(
* Supports 'add', 'subtract', 'multiply', 'divide'.
* Obeys the 'precision' and 'allow_negative' settings from the config resource.
*
* @param float $a the first operand
* @param float $b the second operand
* @param string $operation the operation ('add', 'subtract', 'multiply', 'divide')
* @param float $a the first operand
* @param float $b the second operand
* @param string $operation the operation ('add', 'subtract', 'multiply', 'divide')
* @param ClientLogger $logger Auto-injected MCP logger
*
* @return float|string the result of the calculation, or an error message string
*/
#[McpTool(name: 'calculate')]
public function calculate(float $a, float $b, string $operation): float|string
public function calculate(float $a, float $b, string $operation, ClientLogger $logger): float|string
{
$this->logger->info(\sprintf('Calculating: %f %s %f', $a, $operation, $b));

Expand All @@ -65,25 +67,48 @@ public function calculate(float $a, float $b, string $operation): float|string
break;
case 'divide':
if (0 == $b) {
$logger->warning('Division by zero attempted', [
'operand_a' => $a,
'operand_b' => $b,
]);

return 'Error: Division by zero.';
}
$result = $a / $b;
break;
default:
$logger->error('Unknown operation requested', [
'operation' => $operation,
'supported_operations' => ['add', 'subtract', 'multiply', 'divide'],
]);

return "Error: Unknown operation '{$operation}'. Supported: add, subtract, multiply, divide.";
}

if (!$this->config['allow_negative'] && $result < 0) {
$logger->warning('Negative result blocked by configuration', [
'result' => $result,
'allow_negative_setting' => false,
]);

return 'Error: Negative results are disabled.';
}

return round($result, $this->config['precision']);
$finalResult = round($result, $this->config['precision']);
$logger->info('Calculation completed successfully', [
'result' => $finalResult,
'precision' => $this->config['precision'],
]);

return $finalResult;
}

/**
* Provides the current calculator configuration.
* Can be read by clients to understand precision etc.
*
* @param ClientLogger $logger Auto-injected MCP logger for demonstration
*
* @return Config the configuration array
*/
#[McpResource(
Expand All @@ -92,9 +117,12 @@ public function calculate(float $a, float $b, string $operation): float|string
description: 'Current settings for the calculator tool (precision, allow_negative).',
mimeType: 'application/json',
)]
public function getConfiguration(): array
public function getConfiguration(ClientLogger $logger): array
{
$this->logger->info('Resource config://calculator/settings read.');
$logger->info('📊 Resource config://calculator/settings accessed via auto-injection!', [
'current_config' => $this->config,
'auto_injection_demo' => 'ClientLogger was automatically injected into this resource handler',
]);

return $this->config;
}
Expand All @@ -103,8 +131,9 @@ public function getConfiguration(): array
* Updates a specific configuration setting.
* Note: This requires more robust validation in a real app.
*
* @param string $setting the setting key ('precision' or 'allow_negative')
* @param mixed $value the new value (int for precision, bool for allow_negative)
* @param string $setting the setting key ('precision' or 'allow_negative')
* @param mixed $value the new value (int for precision, bool for allow_negative)
* @param ClientLogger $logger Auto-injected MCP logger
*
* @return array{
* success: bool,
Expand All @@ -113,18 +142,32 @@ public function getConfiguration(): array
* } success message or error
*/
#[McpTool(name: 'update_setting')]
public function updateSetting(string $setting, mixed $value): array
public function updateSetting(string $setting, mixed $value, ClientLogger $logger): array
{
$this->logger->info(\sprintf('Setting tool called: setting=%s, value=%s', $setting, var_export($value, true)));
if (!\array_key_exists($setting, $this->config)) {
$logger->error('Unknown setting requested', [
'setting' => $setting,
'available_settings' => array_keys($this->config),
]);

return ['success' => false, 'error' => "Unknown setting '{$setting}'."];
}

if ('precision' === $setting) {
if (!\is_int($value) || $value < 0 || $value > 10) {
$logger->warning('Invalid precision value provided', [
'value' => $value,
'valid_range' => '0-10',
]);

return ['success' => false, 'error' => 'Invalid precision value. Must be integer between 0 and 10.'];
}
$this->config['precision'] = $value;
$logger->info('Precision setting updated', [
'new_precision' => $value,
'previous_config' => $this->config,
]);

// In real app, notify subscribers of config://calculator/settings change
// $registry->notifyResourceChanged('config://calculator/settings');
Expand All @@ -138,10 +181,19 @@ public function updateSetting(string $setting, mixed $value): array
} elseif (\in_array(strtolower((string) $value), ['false', '0', 'no', 'off'])) {
$value = false;
} else {
$logger->warning('Invalid allow_negative value provided', [
'value' => $value,
'expected_type' => 'boolean',
]);

return ['success' => false, 'error' => 'Invalid allow_negative value. Must be boolean (true/false).'];
}
}
$this->config['allow_negative'] = $value;
$logger->info('Allow negative setting updated', [
'new_allow_negative' => $value,
'updated_config' => $this->config,
]);

// $registry->notifyResourceChanged('config://calculator/settings');
return ['success' => true, 'message' => 'Allow negative results set to '.($value ? 'true' : 'false').'.'];
Expand Down
80 changes: 80 additions & 0 deletions examples/stdio-logging-showcase/LoggingShowcaseHandlers.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?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\StdioLoggingShowcase;

use Mcp\Capability\Attribute\McpTool;
use Mcp\Capability\Logger\ClientLogger;

/**
* 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 with auto-injected ClientLogger.
*
* @param string $message The message to log
* @param string $level The logging level (debug, info, warning, error)
* @param ClientLogger $logger Auto-injected MCP logger
*
* @return array<string, mixed>
*/
#[McpTool(name: 'log_message', description: 'Demonstrates MCP logging with different levels')]
public function logMessage(string $message, string $level, ClientLogger $logger): array
{
$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,
];
}
}
34 changes: 34 additions & 0 deletions examples/stdio-logging-showcase/server.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/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;
use Mcp\Server\Transport\StdioTransport;

logger()->info('Starting MCP Stdio Logging Showcase Server...');

// Create server with auto-discovery of MCP capabilities and ENABLE MCP LOGGING
$server = Server::builder()
->setServerInfo('Stdio Logging Showcase', '1.0.0', 'Demonstration of auto-injected MCP logging in capability handlers.')
->setContainer(container())
->setLogger(logger())
->setDiscovery(__DIR__, ['.'])
->build();

$transport = new StdioTransport(logger: logger());

$server->run($transport);

logger()->info('Logging Showcase Server is ready!');
logger()->info('This example demonstrates auto-injection of ClientLogger into capability handlers.');
25 changes: 25 additions & 0 deletions src/Capability/Discovery/SchemaGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,10 @@ private function parseParametersInfo(\ReflectionMethod|\ReflectionFunction $refl
$parametersInfo = [];

foreach ($reflection->getParameters() as $rp) {
if ($this->isAutoInjectedParameter($rp)) {
continue;
}

$paramName = $rp->getName();
$paramTag = $paramTags['$'.$paramName] ?? null;

Expand Down Expand Up @@ -784,4 +788,25 @@ private function mapSimpleTypeToJsonSchema(string $type): string
default => \in_array(strtolower($type), ['datetime', 'datetimeinterface']) ? 'string' : 'object',
};
}

/**
* Determines if a parameter was auto-injected and should be excluded from schema generation.
*
* Parameters that are auto-injected by the framework (like ClientLogger) should not appear
* in the tool schema since they're not provided by the client.
*/
private function isAutoInjectedParameter(\ReflectionParameter $parameter): bool
{
$type = $parameter->getType();

if (!$type instanceof \ReflectionNamedType) {
return false;
}

$typeName = $type->getName();

// Auto-inject for ClientLogger or LoggerInterface types
return 'Mcp\\Capability\\Logger\\ClientLogger' === $typeName
|| 'Psr\\Log\\LoggerInterface' === $typeName;
Comment on lines +809 to +810
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please use ::class instead

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually you could simplify this to sth like

Suggested change
return 'Mcp\\Capability\\Logger\\ClientLogger' === $typeName
|| 'Psr\\Log\\LoggerInterface' === $typeName;
return in_array($type->getName(), [ClientLogger::class, LoggerInterface::class], true);

}
}
Loading