Skip to content

Commit 6982781

Browse files
refactor(Server): Decouple discovery from build and refine core components
- ServerBuilder no longer configures discovery paths or runs discovery automatically during build; discovery is now an explicit step via `Server->discover()`. - `Server::discover()` now accepts path configurations, clears only discovered/cached elements (preserving manual ones), and saves only discovered elements to cache. - `Registry` distinguishes between manually registered and discovered elements: - Manual registrations take precedence over discovered/cached elements with the same identifier. - Caching methods (`loadDiscoveredElementsFromCache`, `saveDiscoveredElementsToCache`, `clearDiscoveredElements`) now operate only on discovered elements. - `ClientStateManager` constructor defaults to `ArrayCache` if no PSR-16 cache is provided, ensuring basic stateful functionality. - `ClientStateManager` now stores client-requested log levels. - `Processor` updated to use `ClientStateManager` for persisting client-requested log levels and relies on `Configuration` VO for server capabilities. - `JsonRpc\Response` constructor and `fromArray` updated to correctly handle `null` IDs for error responses as per JSON-RPC 2.0 spec, fixing related TypeErrors. - `StdioServerTransport` constructor now accepts optional input/output stream resources, improving testability and flexibility. - `HttpServerTransport` POST message handling reverted to synchronous body reading (`getBody()->getContents()`) for reliability with current `react/http` behavior, resolving hangs. - Corrected `Support\DocBlockParser` and `Support\Discoverer` constructors to directly accept `LoggerInterface` instead of `ContainerInterface`. - Updated unit tests for `ServerBuilder`, `Server`, `Registry`, `ClientStateManager`, `StdioServerTransport`, `HttpServerTransport`, `ProtocolHandler`, and JSON-RPC classes to reflect architectural changes and improve reliability, including fixes for Mockery state and loop interactions. - Renamed `Server::getProtocolHandler()` to `Server::getProtocol()` and updated relevant class names (`ProtocolHandler` to `Protocol`). - Renamed `Server::isLoaded()` in Registry to `discoveryRanOrCached()`. - Renamed examples and updated their internal scripts and documentation comments to reflect new server API and best practices. - Added more examples to showcase other use cases of the library
1 parent f35702a commit 6982781

File tree

66 files changed

+2668
-1472
lines changed

Some content is hidden

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

66 files changed

+2668
-1472
lines changed
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
#!/usr/bin/env php
2+
<?php
3+
4+
/*
5+
|--------------------------------------------------------------------------
6+
| MCP Stdio Calculator Server (Attribute Discovery)
7+
|--------------------------------------------------------------------------
8+
|
9+
| This server demonstrates using attribute-based discovery to find MCP
10+
| elements (Tools, Resources) in the 'McpElements.php' file within this
11+
| directory. It runs via the STDIO transport.
12+
|
13+
| To Use:
14+
| 1. Ensure 'McpElements.php' defines classes with MCP attributes.
15+
| 2. Configure your MCP Client (e.g., Cursor) for this server:
16+
|
17+
| {
18+
| "mcpServers": {
19+
| "php-stdio-calculator": {
20+
| "command": "php",
21+
| "args": ["/full/path/to/examples/01-discovery-stdio-calculator/server.php"]
22+
| }
23+
| }
24+
| }
25+
|
26+
| The ServerBuilder builds the server instance, then $server->discover()
27+
| scans the current directory (specified by basePath: __DIR__, scanDirs: ['.'])
28+
| to find and register elements before listening on STDIN/STDOUT.
29+
|
30+
| If you provided a `CacheInterface` implementation to the ServerBuilder,
31+
| the discovery process will be cached, so you can comment out the
32+
| discovery call after the first run to speed up subsequent runs.
33+
|
34+
*/
35+
declare(strict_types=1);
36+
37+
chdir(__DIR__);
38+
require_once '../../vendor/autoload.php';
39+
require_once 'McpElements.php';
40+
41+
use PhpMcp\Server\Server;
42+
use PhpMcp\Server\Transports\StdioServerTransport;
43+
use Psr\Log\AbstractLogger;
44+
45+
class StderrLogger extends AbstractLogger
46+
{
47+
public function log($level, \Stringable|string $message, array $context = []): void
48+
{
49+
fwrite(STDERR, sprintf(
50+
"[%s] %s %s\n",
51+
strtoupper($level),
52+
$message,
53+
empty($context) ? '' : json_encode($context)
54+
));
55+
}
56+
}
57+
58+
try {
59+
$logger = new StderrLogger;
60+
$logger->info('Starting MCP Stdio Calculator Server...');
61+
62+
$server = Server::make()
63+
->withServerInfo('Stdio Calculator', '1.1.0')
64+
->withLogger($logger)
65+
->build();
66+
67+
$server->discover(__DIR__, ['.']);
68+
69+
$transport = new StdioServerTransport;
70+
71+
$server->listen($transport);
72+
73+
$logger->info('Server listener stopped gracefully.');
74+
exit(0);
75+
76+
} catch (\Throwable $e) {
77+
fwrite(STDERR, "[MCP SERVER CRITICAL ERROR]\n");
78+
fwrite(STDERR, 'Error: '.$e->getMessage()."\n");
79+
fwrite(STDERR, 'File: '.$e->getFile().':'.$e->getLine()."\n");
80+
fwrite(STDERR, $e->getTraceAsString()."\n");
81+
exit(1);
82+
}

examples/standalone_http_userprofile/server.php renamed to examples/02-discovery-http-userprofile/server.php

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,39 @@
11
#!/usr/bin/env php
22
<?php
33

4+
/*
5+
|--------------------------------------------------------------------------
6+
| MCP HTTP User Profile Server (Attribute Discovery)
7+
|--------------------------------------------------------------------------
8+
|
9+
| This server demonstrates attribute-based discovery for MCP elements
10+
| (ResourceTemplates, Resources, Tools, Prompts) defined in 'McpElements.php'.
11+
| It runs via the HTTP transport, listening for SSE and POST requests.
12+
|
13+
| To Use:
14+
| 1. Ensure 'McpElements.php' defines classes with MCP attributes.
15+
| 2. Run this script from your CLI: `php server.php`
16+
| The server will listen on http://127.0.0.1:8080 by default.
17+
| 3. Configure your MCP Client (e.g., Cursor) for this server:
18+
|
19+
| {
20+
| "mcpServers": {
21+
| "php-http-userprofile": {
22+
| "url": "http://127.0.0.1:8080/mcp/sse" // Use the SSE endpoint
23+
| // Ensure your client can reach this address
24+
| }
25+
| }
26+
| }
27+
|
28+
| The ServerBuilder builds the server, $server->discover() scans for elements,
29+
| and then $server->listen() starts the ReactPHP HTTP server.
30+
|
31+
| If you provided a `CacheInterface` implementation to the ServerBuilder,
32+
| the discovery process will be cached, so you can comment out the
33+
| discovery call after the first run to speed up subsequent runs.
34+
|
35+
*/
36+
437
declare(strict_types=1);
538

639
chdir(__DIR__);
@@ -22,11 +55,11 @@ public function log($level, \Stringable|string $message, array $context = []): v
2255
}
2356

2457
try {
25-
$logger = new StderrLogger();
58+
$logger = new StderrLogger;
2659
$logger->info('Starting MCP HTTP User Profile Server...');
2760

2861
// --- Setup DI Container for DI in McpElements class ---
29-
$container = new BasicContainer();
62+
$container = new BasicContainer;
3063
$container->set(LoggerInterface::class, $logger);
3164

3265
$server = Server::make()
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
namespace Mcp\ManualStdioExample;
4+
5+
use Psr\Log\LoggerInterface;
6+
7+
class SimpleHandlers
8+
{
9+
private LoggerInterface $logger;
10+
11+
private string $appVersion = '1.0-manual';
12+
13+
public function __construct(LoggerInterface $logger)
14+
{
15+
$this->logger = $logger;
16+
$this->logger->info('SimpleHandlers instantiated for manual registration example.');
17+
}
18+
19+
/**
20+
* A manually registered tool to echo input.
21+
*
22+
* @param string $text The text to echo.
23+
* @return string The echoed text.
24+
*/
25+
public function echoText(string $text): string
26+
{
27+
$this->logger->info("Manual tool 'echo_text' called.", ['text' => $text]);
28+
29+
return 'Echo: '.$text;
30+
}
31+
32+
/**
33+
* A manually registered resource providing app version.
34+
*
35+
* @return string The application version.
36+
*/
37+
public function getAppVersion(): string
38+
{
39+
$this->logger->info("Manual resource 'app://version' read.");
40+
41+
return $this->appVersion;
42+
}
43+
44+
/**
45+
* A manually registered prompt template.
46+
*
47+
* @param string $userName The name of the user.
48+
* @return array The prompt messages.
49+
*/
50+
public function greetingPrompt(string $userName): array
51+
{
52+
$this->logger->info("Manual prompt 'personalized_greeting' called.", ['userName' => $userName]);
53+
54+
return [
55+
['role' => 'user', 'content' => "Craft a personalized greeting for {$userName}."],
56+
];
57+
}
58+
59+
/**
60+
* A manually registered resource template.
61+
*
62+
* @param string $itemId The ID of the item.
63+
* @return array Item details.
64+
*/
65+
public function getItemDetails(string $itemId): array
66+
{
67+
$this->logger->info("Manual template 'item://{itemId}' resolved.", ['itemId' => $itemId]);
68+
69+
return ['id' => $itemId, 'name' => "Item {$itemId}", 'description' => "Details for item {$itemId} from manual template."];
70+
}
71+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#!/usr/bin/env php
2+
<?php
3+
4+
/*
5+
|--------------------------------------------------------------------------
6+
| MCP Stdio Server (Manual Element Registration)
7+
|--------------------------------------------------------------------------
8+
|
9+
| This server demonstrates how to manually register all MCP elements
10+
| (Tools, Resources, Prompts, ResourceTemplates) using the ServerBuilder's
11+
| fluent `withTool()`, `withResource()`, etc., methods.
12+
| It does NOT use attribute discovery. Handlers are in 'SimpleHandlers.php'.
13+
| It runs via the STDIO transport.
14+
|
15+
| To Use:
16+
| 1. Configure your MCP Client (e.g., Cursor) for this server:
17+
|
18+
| {
19+
| "mcpServers": {
20+
| "php-stdio-manual": {
21+
| "command": "php",
22+
| "args": ["/full/path/to/examples/03-manual-registration-stdio/server.php"]
23+
| }
24+
| }
25+
| }
26+
|
27+
| All elements are explicitly defined during the ServerBuilder chain.
28+
| The $server->discover() method is NOT called.
29+
|
30+
*/
31+
32+
declare(strict_types=1);
33+
34+
chdir(__DIR__);
35+
require_once '../../vendor/autoload.php';
36+
require_once './SimpleHandlers.php';
37+
38+
use Mcp\ManualStdioExample\SimpleHandlers;
39+
use PhpMcp\Server\Defaults\BasicContainer;
40+
use PhpMcp\Server\Server;
41+
use PhpMcp\Server\Transports\StdioServerTransport;
42+
use Psr\Log\AbstractLogger;
43+
use Psr\Log\LoggerInterface;
44+
45+
class StderrLogger extends AbstractLogger
46+
{
47+
public function log($level, \Stringable|string $message, array $context = []): void
48+
{
49+
fwrite(STDERR, sprintf("[%s][%s] %s %s\n", date('Y-m-d H:i:s'), strtoupper($level), $message, empty($context) ? '' : json_encode($context)));
50+
}
51+
}
52+
53+
try {
54+
$logger = new StderrLogger;
55+
$logger->info('Starting MCP Manual Registration (Stdio) Server...');
56+
57+
$container = new BasicContainer;
58+
$container->set(LoggerInterface::class, $logger);
59+
60+
$server = Server::make()
61+
->withServerInfo('Manual Reg Server', '1.0.0')
62+
->withLogger($logger)
63+
->withContainer($container)
64+
->withTool([SimpleHandlers::class, 'echoText'], 'echo_text')
65+
->withResource([SimpleHandlers::class, 'getAppVersion'], 'app://version', 'application_version', mimeType: 'text/plain')
66+
->withPrompt([SimpleHandlers::class, 'greetingPrompt'], 'personalized_greeting')
67+
->withResourceTemplate([SimpleHandlers::class, 'getItemDetails'], 'item://{itemId}/details', 'get_item_details', mimeType: 'application/json')
68+
->build();
69+
70+
$transport = new StdioServerTransport;
71+
$server->listen($transport);
72+
73+
$logger->info('Server listener stopped gracefully.');
74+
exit(0);
75+
76+
} catch (\Throwable $e) {
77+
fwrite(STDERR, "[MCP SERVER CRITICAL ERROR]\n".$e."\n");
78+
exit(1);
79+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
namespace Mcp\CombinedHttpExample\Discovered;
4+
5+
use PhpMcp\Server\Attributes\McpResource;
6+
use PhpMcp\Server\Attributes\McpTool;
7+
8+
class DiscoveredElements
9+
{
10+
/**
11+
* A tool discovered via attributes.
12+
*
13+
* @return string A status message.
14+
*/
15+
#[McpTool(name: 'discovered_status_check')]
16+
public function checkSystemStatus(): string
17+
{
18+
return 'System status: OK (discovered)';
19+
}
20+
21+
/**
22+
* A resource discovered via attributes.
23+
* This will be overridden by a manual registration with the same URI.
24+
*
25+
* @return string Content.
26+
*/
27+
#[McpResource(uri: 'config://priority', name: 'priority_config_discovered')]
28+
public function getPriorityConfigDiscovered(): string
29+
{
30+
return 'Discovered Priority Config: Low';
31+
}
32+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
namespace Mcp\CombinedHttpExample\Manual;
4+
5+
use Psr\Log\LoggerInterface;
6+
7+
class ManualHandlers
8+
{
9+
private LoggerInterface $logger;
10+
11+
public function __construct(LoggerInterface $logger)
12+
{
13+
$this->logger = $logger;
14+
}
15+
16+
/**
17+
* A manually registered tool.
18+
*
19+
* @param string $user The user to greet.
20+
* @return string Greeting.
21+
*/
22+
public function manualGreeter(string $user): string
23+
{
24+
$this->logger->info("Manual tool 'manual_greeter' called for {$user}");
25+
26+
return "Hello {$user}, from manual registration!";
27+
}
28+
29+
/**
30+
* Manually registered resource that overrides a discovered one.
31+
*
32+
* @return string Content.
33+
*/
34+
public function getPriorityConfigManual(): string
35+
{
36+
$this->logger->info("Manual resource 'config://priority' read.");
37+
38+
return 'Manual Priority Config: HIGH (overrides discovered)';
39+
}
40+
}

0 commit comments

Comments
 (0)