Skip to content

Commit f35702a

Browse files
refactor: Decouple Server Core from Transport & Improve API
- Replaced Server->run() with Server->listen(Transport) for explicit binding. - Separated core Server logic from transport implementations (Stdio/Http). - Introduced ServerBuilder for configuration and ServerProtocolHandler for mediation. - Made attribute discovery an explicit step via Server->discover(). - Refined caching to only cache discovered elements, respecting manual registrations. - Replaced ConfigurationRepository with Configuration VO and Capabilities VO. - Simplified core component dependencies (Processor, Registry, ClientStateManager). - Renamed TransportState to ClientStateManager. - Renamed McpException to McpServerException and added specific exception types. - Updated transport implementations (StdioServerTransport, HttpServerTransport). - Improved default BasicContainer with simple auto-wiring. - Revised documentation (README) and examples to reflect new architecture.
1 parent 1f11b8e commit f35702a

File tree

101 files changed

+6602
-6507
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

101 files changed

+6602
-6507
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ All notable changes to `php-mcp/server` will be documented in this file.
1212

1313
### Changed
1414

15-
* **Dependency Injection:** Refactored internal dependency management. Core server components (`Processor`, `Registry`, `TransportState`, etc.) now resolve `LoggerInterface`, `CacheInterface`, and `ConfigurationRepositoryInterface` Just-In-Time from the provided PSR-11 container. See **Breaking Changes** for implications.
15+
* **Dependency Injection:** Refactored internal dependency management. Core server components (`Processor`, `Registry`, `ClientStateManager`, etc.) now resolve `LoggerInterface`, `CacheInterface`, and `ConfigurationRepositoryInterface` Just-In-Time from the provided PSR-11 container. See **Breaking Changes** for implications.
1616
* **Default Logging Behavior:** Logging is now **disabled by default**. To enable logging, provide a `LoggerInterface` implementation via `withLogger()` (when using the default container) or by registering it within your custom PSR-11 container.
1717
* **Transport Handler Constructors:** Transport Handlers (e.g., `StdioTransportHandler`, `HttpTransportHandler`) now primarily accept the `Server` instance in their constructor, simplifying their instantiation.
1818

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"friendsofphp/php-cs-fixer": "^3.75",
2727
"mockery/mockery": "^1.6",
2828
"pestphp/pest": "^2.36.0|^3.5.0",
29+
"react/async": "^4.0",
2930
"react/http": "^1.11",
3031
"symfony/var-dumper": "^6.4.11|^7.1.5"
3132
},
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<?php
2+
3+
namespace Mcp\HttpUserProfileExample;
4+
5+
use PhpMcp\Server\Attributes\McpPrompt;
6+
use PhpMcp\Server\Attributes\McpResource;
7+
use PhpMcp\Server\Attributes\McpResourceTemplate;
8+
use PhpMcp\Server\Attributes\McpTool;
9+
use PhpMcp\Server\Exception\McpServerException;
10+
use Psr\Log\LoggerInterface;
11+
12+
class McpElements
13+
{
14+
// Simulate a simple user database
15+
private array $users = [
16+
'101' => ['name' => 'Alice', 'email' => '[email protected]', 'role' => 'admin'],
17+
'102' => ['name' => 'Bob', 'email' => '[email protected]', 'role' => 'user'],
18+
'103' => ['name' => 'Charlie', 'email' => '[email protected]', 'role' => 'user'],
19+
];
20+
21+
private LoggerInterface $logger;
22+
23+
public function __construct(LoggerInterface $logger)
24+
{
25+
$this->logger = $logger;
26+
$this->logger->debug('HttpUserProfileExample McpElements instantiated.');
27+
}
28+
29+
/**
30+
* Retrieves the profile data for a specific user.
31+
*
32+
* @param string $userId The ID of the user (from URI).
33+
* @return array User profile data.
34+
*
35+
* @throws McpServerException If the user is not found.
36+
*/
37+
#[McpResourceTemplate(
38+
uriTemplate: 'user://{userId}/profile',
39+
name: 'user_profile',
40+
description: 'Get profile information for a specific user ID.',
41+
mimeType: 'application/json'
42+
)]
43+
public function getUserProfile(string $userId): array
44+
{
45+
$this->logger->info('Reading resource: user profile', ['userId' => $userId]);
46+
if (! isset($this->users[$userId])) {
47+
// Throwing an exception that Processor can turn into an error response
48+
throw McpServerException::invalidParams("User profile not found for ID: {$userId}");
49+
}
50+
51+
return $this->users[$userId];
52+
}
53+
54+
/**
55+
* Retrieves a list of all known user IDs.
56+
*
57+
* @return array List of user IDs.
58+
*/
59+
#[McpResource(
60+
uri: 'user://list/ids',
61+
name: 'user_id_list',
62+
description: 'Provides a list of all available user IDs.',
63+
mimeType: 'application/json'
64+
)]
65+
public function listUserIds(): array
66+
{
67+
$this->logger->info('Reading resource: user ID list');
68+
69+
return array_keys($this->users);
70+
}
71+
72+
/**
73+
* Sends a welcome message to a user.
74+
* (This is a placeholder - in a real app, it might queue an email)
75+
*
76+
* @param string $userId The ID of the user to message.
77+
* @param string|null $customMessage An optional custom message part.
78+
* @return array Status of the operation.
79+
*/
80+
#[McpTool(name: 'send_welcome')]
81+
public function sendWelcomeMessage(string $userId, ?string $customMessage = null): array
82+
{
83+
$this->logger->info('Executing tool: send_welcome', ['userId' => $userId]);
84+
if (! isset($this->users[$userId])) {
85+
return ['success' => false, 'error' => "User ID {$userId} not found."];
86+
}
87+
$user = $this->users[$userId];
88+
$message = "Welcome, {$user['name']}!";
89+
if ($customMessage) {
90+
$message .= ' '.$customMessage;
91+
}
92+
// Simulate sending
93+
$this->logger->info("Simulated sending message to {$user['email']}: {$message}");
94+
95+
return ['success' => true, 'message_sent' => $message];
96+
}
97+
98+
/**
99+
* Generates a prompt to write a bio for a user.
100+
*
101+
* @param string $userId The user ID to generate the bio for.
102+
* @param string $tone Desired tone (e.g., 'formal', 'casual').
103+
* @return array Prompt messages.
104+
*
105+
* @throws McpServerException If user not found.
106+
*/
107+
#[McpPrompt(name: 'generate_bio_prompt')]
108+
public function generateBio(string $userId, string $tone = 'professional'): array
109+
{
110+
$this->logger->info('Executing prompt: generate_bio', ['userId' => $userId, 'tone' => $tone]);
111+
if (! isset($this->users[$userId])) {
112+
throw McpServerException::invalidParams("User not found for bio prompt: {$userId}");
113+
}
114+
$user = $this->users[$userId];
115+
116+
return [
117+
['role' => 'user', 'content' => "Write a short, {$tone} biography for {$user['name']} (Role: {$user['role']}, Email: {$user['email']}). Highlight their role within the system."],
118+
];
119+
}
120+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#!/usr/bin/env php
2+
<?php
3+
4+
declare(strict_types=1);
5+
6+
chdir(__DIR__);
7+
require_once '../../vendor/autoload.php';
8+
require_once 'McpElements.php';
9+
10+
use PhpMcp\Server\Defaults\BasicContainer;
11+
use PhpMcp\Server\Server;
12+
use PhpMcp\Server\Transports\HttpServerTransport;
13+
use Psr\Log\AbstractLogger;
14+
use Psr\Log\LoggerInterface;
15+
16+
class StderrLogger extends AbstractLogger
17+
{
18+
public function log($level, \Stringable|string $message, array $context = []): void
19+
{
20+
fwrite(STDERR, sprintf("[%s][%s] %s %s\n", date('Y-m-d H:i:s'), strtoupper($level), $message, empty($context) ? '' : json_encode($context)));
21+
}
22+
}
23+
24+
try {
25+
$logger = new StderrLogger();
26+
$logger->info('Starting MCP HTTP User Profile Server...');
27+
28+
// --- Setup DI Container for DI in McpElements class ---
29+
$container = new BasicContainer();
30+
$container->set(LoggerInterface::class, $logger);
31+
32+
$server = Server::make()
33+
->withServerInfo('HTTP User Profiles', '1.0.0')
34+
->withLogger($logger)
35+
->withContainer($container)
36+
->build();
37+
38+
$server->discover(__DIR__, ['.']);
39+
40+
$transport = new HttpServerTransport(
41+
host: '127.0.0.1',
42+
port: 8080,
43+
mcpPathPrefix: 'mcp'
44+
);
45+
46+
$server->listen($transport);
47+
48+
$logger->info('Server listener stopped gracefully.');
49+
exit(0);
50+
51+
} catch (\Throwable $e) {
52+
fwrite(STDERR, "[MCP SERVER CRITICAL ERROR]\n");
53+
fwrite(STDERR, 'Error: '.$e->getMessage()."\n");
54+
fwrite(STDERR, 'File: '.$e->getFile().':'.$e->getLine()."\n");
55+
fwrite(STDERR, $e->getTraceAsString()."\n");
56+
exit(1);
57+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
<?php
2+
3+
namespace Mcp\StdioCalculatorExample;
4+
5+
use PhpMcp\Server\Attributes\McpResource;
6+
use PhpMcp\Server\Attributes\McpTool;
7+
8+
class McpElements
9+
{
10+
private array $config = [
11+
'precision' => 2,
12+
'allow_negative' => true,
13+
];
14+
15+
/**
16+
* Performs a calculation based on the operation.
17+
*
18+
* Supports 'add', 'subtract', 'multiply', 'divide'.
19+
* Obeys the 'precision' and 'allow_negative' settings from the config resource.
20+
*
21+
* @param float $a The first operand.
22+
* @param float $b The second operand.
23+
* @param string $operation The operation ('add', 'subtract', 'multiply', 'divide').
24+
* @return float|string The result of the calculation, or an error message string.
25+
*/
26+
#[McpTool(name: 'calculate')]
27+
public function calculate(float $a, float $b, string $operation): float|string
28+
{
29+
// Use STDERR for logs
30+
fwrite(STDERR, "Calculate tool called: a=$a, b=$b, op=$operation\n");
31+
32+
$op = strtolower($operation);
33+
$result = null;
34+
35+
switch ($op) {
36+
case 'add':
37+
$result = $a + $b;
38+
break;
39+
case 'subtract':
40+
$result = $a - $b;
41+
break;
42+
case 'multiply':
43+
$result = $a * $b;
44+
break;
45+
case 'divide':
46+
if ($b == 0) {
47+
return 'Error: Division by zero.';
48+
}
49+
$result = $a / $b;
50+
break;
51+
default:
52+
return "Error: Unknown operation '{$operation}'. Supported: add, subtract, multiply, divide.";
53+
}
54+
55+
if (! $this->config['allow_negative'] && $result < 0) {
56+
return 'Error: Negative results are disabled.';
57+
}
58+
59+
return round($result, $this->config['precision']);
60+
}
61+
62+
/**
63+
* Provides the current calculator configuration.
64+
* Can be read by clients to understand precision etc.
65+
*
66+
* @return array The configuration array.
67+
*/
68+
#[McpResource(
69+
uri: 'config://calculator/settings',
70+
name: 'calculator_config',
71+
description: 'Current settings for the calculator tool (precision, allow_negative).',
72+
mimeType: 'application/json' // Return as JSON
73+
)]
74+
public function getConfiguration(): array
75+
{
76+
fwrite(STDERR, "Resource config://calculator/settings read.\n");
77+
78+
return $this->config;
79+
}
80+
81+
/**
82+
* Updates a specific configuration setting.
83+
* Note: This requires more robust validation in a real app.
84+
*
85+
* @param string $setting The setting key ('precision' or 'allow_negative').
86+
* @param mixed $value The new value (int for precision, bool for allow_negative).
87+
* @return array Success message or error.
88+
*/
89+
#[McpTool(name: 'update_setting')]
90+
public function updateSetting(string $setting, mixed $value): array
91+
{
92+
fwrite(STDERR, "Update Setting tool called: setting=$setting, value=".var_export($value, true)."\n");
93+
if (! array_key_exists($setting, $this->config)) {
94+
return ['success' => false, 'error' => "Unknown setting '{$setting}'."];
95+
}
96+
97+
if ($setting === 'precision') {
98+
if (! is_int($value) || $value < 0 || $value > 10) {
99+
return ['success' => false, 'error' => 'Invalid precision value. Must be integer between 0 and 10.'];
100+
}
101+
$this->config['precision'] = $value;
102+
103+
// In real app, notify subscribers of config://calculator/settings change
104+
// $registry->notifyResourceChanged('config://calculator/settings');
105+
return ['success' => true, 'message' => "Precision updated to {$value}."];
106+
}
107+
108+
if ($setting === 'allow_negative') {
109+
if (! is_bool($value)) {
110+
// Attempt basic cast for flexibility
111+
if (in_array(strtolower((string) $value), ['true', '1', 'yes', 'on'])) {
112+
$value = true;
113+
} elseif (in_array(strtolower((string) $value), ['false', '0', 'no', 'off'])) {
114+
$value = false;
115+
} else {
116+
return ['success' => false, 'error' => 'Invalid allow_negative value. Must be boolean (true/false).'];
117+
}
118+
}
119+
$this->config['allow_negative'] = $value;
120+
121+
// $registry->notifyResourceChanged('config://calculator/settings');
122+
return ['success' => true, 'message' => 'Allow negative results set to '.($value ? 'true' : 'false').'.'];
123+
}
124+
125+
return ['success' => false, 'error' => 'Internal error handling setting.']; // Should not happen
126+
}
127+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/usr/bin/env php
2+
<?php
3+
4+
declare(strict_types=1);
5+
6+
chdir(__DIR__);
7+
require_once '../../vendor/autoload.php';
8+
require_once 'McpElements.php';
9+
10+
use PhpMcp\Server\Server;
11+
use PhpMcp\Server\Transports\StdioServerTransport;
12+
use Psr\Log\AbstractLogger;
13+
14+
class StderrLogger extends AbstractLogger
15+
{
16+
public function log($level, \Stringable|string $message, array $context = []): void
17+
{
18+
fwrite(STDERR, sprintf(
19+
"[%s] %s %s\n",
20+
strtoupper($level),
21+
$message,
22+
empty($context) ? '' : json_encode($context)
23+
));
24+
}
25+
}
26+
27+
try {
28+
$logger = new StderrLogger();
29+
$logger->info('Starting MCP Stdio Calculator Server...');
30+
31+
$server = Server::make()
32+
->withServerInfo('Stdio Calculator', '1.1.0')
33+
->withLogger($logger)
34+
->build();
35+
36+
$server->discover(__DIR__, ['.']);
37+
38+
$transport = new StdioServerTransport();
39+
40+
$server->listen($transport);
41+
42+
$logger->info('Server listener stopped gracefully.');
43+
exit(0);
44+
45+
} catch (\Throwable $e) {
46+
fwrite(STDERR, "[MCP SERVER CRITICAL ERROR]\n");
47+
fwrite(STDERR, 'Error: '.$e->getMessage()."\n");
48+
fwrite(STDERR, 'File: '.$e->getFile().':'.$e->getLine()."\n");
49+
fwrite(STDERR, $e->getTraceAsString()."\n");
50+
exit(1);
51+
}

0 commit comments

Comments
 (0)