feat(server): Rework transport architecture and add StreamableHttpTransport (#49)
* feat(server): Rework transport architecture and add StreamableHttpTransport - `Server::connect()` no longer contains a processing loop. - `TransportInterface` is updated with `setMessageHandler()` and `listen()`. - `StdioTransport` is updated to implement the new interface. - A new, minimal `StreamableHttpTransport` is added for stateless HTTP. * refactor(server): use event-driven message handling * feat(server): Introduce a formal session management system * fix: ensure session elements are preserved when building server (regression from #46) * refactor: consolidate HTTP example to use shared dependencies * feat(server): Enhance message handling with session support - Updated `TransportInterface` to use `onMessage` for handling incoming messages with session IDs. - Refactored `Server`, `Handler`, and transport classes to accommodate session management using `Uuid`. - Introduced methods for creating sessions with auto-generated and specific UUIDs in `SessionFactory` and `SessionFactoryInterface`. * feat(server): Integrate session management in message handling - Added session support to the `Server` and `Handler` classes, allowing for session data to be managed during message processing. - Updated `TransportInterface` to include session context in the `send` method. - Refactored various request handlers to utilize session information, ensuring proper session handling for incoming requests. - Introduced a file-based session store for persistent session data management * feat: Update session builder method to use set* instead of with* * feat: Fix test compatibility with new session management architecture * chore: apply code style fixes * fix: remove deprecated SSE transport and resolve PHPStan issues
This commit is contained in:
parent
b1e54f1f86
commit
719262b15d
49 changed files with 1614 additions and 406 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -3,3 +3,4 @@
|
||||||
composer.lock
|
composer.lock
|
||||||
vendor
|
vendor
|
||||||
examples/**/dev.log
|
examples/**/dev.log
|
||||||
|
examples/**/sessions
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,11 @@
|
||||||
"ext-fileinfo": "*",
|
"ext-fileinfo": "*",
|
||||||
"opis/json-schema": "^2.4",
|
"opis/json-schema": "^2.4",
|
||||||
"phpdocumentor/reflection-docblock": "^5.6",
|
"phpdocumentor/reflection-docblock": "^5.6",
|
||||||
|
"psr/clock": "^1.0",
|
||||||
"psr/container": "^2.0",
|
"psr/container": "^2.0",
|
||||||
"psr/event-dispatcher": "^1.0",
|
"psr/event-dispatcher": "^1.0",
|
||||||
|
"psr/http-factory": "^1.1",
|
||||||
|
"psr/http-message": "^2.0",
|
||||||
"psr/log": "^1.0 || ^2.0 || ^3.0",
|
"psr/log": "^1.0 || ^2.0 || ^3.0",
|
||||||
"symfony/finder": "^6.4 || ^7.3",
|
"symfony/finder": "^6.4 || ^7.3",
|
||||||
"symfony/uid": "^6.4 || ^7.3"
|
"symfony/uid": "^6.4 || ^7.3"
|
||||||
|
|
@ -34,7 +37,10 @@
|
||||||
"phpunit/phpunit": "^10.5",
|
"phpunit/phpunit": "^10.5",
|
||||||
"psr/cache": "^3.0",
|
"psr/cache": "^3.0",
|
||||||
"symfony/console": "^6.4 || ^7.3",
|
"symfony/console": "^6.4 || ^7.3",
|
||||||
"symfony/process": "^6.4 || ^7.3"
|
"symfony/process": "^6.4 || ^7.3",
|
||||||
|
"nyholm/psr7": "^1.8",
|
||||||
|
"nyholm/psr7-server": "^1.1",
|
||||||
|
"laminas/laminas-httphandlerrunner": "^2.12"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
|
@ -51,10 +57,11 @@
|
||||||
"Mcp\\Example\\DependenciesStdioExample\\": "examples/06-custom-dependencies-stdio/",
|
"Mcp\\Example\\DependenciesStdioExample\\": "examples/06-custom-dependencies-stdio/",
|
||||||
"Mcp\\Example\\ComplexSchemaHttpExample\\": "examples/07-complex-tool-schema-http/",
|
"Mcp\\Example\\ComplexSchemaHttpExample\\": "examples/07-complex-tool-schema-http/",
|
||||||
"Mcp\\Example\\SchemaShowcaseExample\\": "examples/08-schema-showcase-streamable/",
|
"Mcp\\Example\\SchemaShowcaseExample\\": "examples/08-schema-showcase-streamable/",
|
||||||
|
"Mcp\\Example\\HttpTransportExample\\": "examples/10-simple-http-transport/",
|
||||||
"Mcp\\Tests\\": "tests/"
|
"Mcp\\Tests\\": "tests/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"sort-packages": true
|
"sort-packages": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -18,12 +18,17 @@ use Mcp\Server\Transport\StdioTransport;
|
||||||
|
|
||||||
logger()->info('Starting MCP Stdio Calculator Server...');
|
logger()->info('Starting MCP Stdio Calculator Server...');
|
||||||
|
|
||||||
Server::make()
|
$server = Server::make()
|
||||||
->setServerInfo('Stdio Calculator', '1.1.0', 'Basic Calculator over STDIO transport.')
|
->setServerInfo('Stdio Calculator', '1.1.0', 'Basic Calculator over STDIO transport.')
|
||||||
->setContainer(container())
|
->setContainer(container())
|
||||||
->setLogger(logger())
|
->setLogger(logger())
|
||||||
->setDiscovery(__DIR__, ['.'])
|
->setDiscovery(__DIR__, ['.'])
|
||||||
->build()
|
->build();
|
||||||
->connect(new StdioTransport(logger: logger()));
|
|
||||||
|
$transport = new StdioTransport(logger: logger());
|
||||||
|
|
||||||
|
$server->connect($transport);
|
||||||
|
|
||||||
|
$transport->listen();
|
||||||
|
|
||||||
logger()->info('Server listener stopped gracefully.');
|
logger()->info('Server listener stopped gracefully.');
|
||||||
|
|
|
||||||
|
|
@ -13,20 +13,23 @@
|
||||||
require_once dirname(__DIR__).'/bootstrap.php';
|
require_once dirname(__DIR__).'/bootstrap.php';
|
||||||
chdir(__DIR__);
|
chdir(__DIR__);
|
||||||
|
|
||||||
use Mcp\Capability\Registry\Container;
|
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
|
||||||
use Mcp\Server;
|
use Mcp\Server;
|
||||||
use Psr\Log\LoggerInterface;
|
use Mcp\Server\Session\FileSessionStore;
|
||||||
|
use Mcp\Server\Transport\StreamableHttpTransport;
|
||||||
|
use Nyholm\Psr7\Factory\Psr17Factory;
|
||||||
|
use Nyholm\Psr7Server\ServerRequestCreator;
|
||||||
|
|
||||||
logger()->info('Starting MCP HTTP User Profile Server...');
|
$psr17Factory = new Psr17Factory();
|
||||||
|
$creator = new ServerRequestCreator($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
|
||||||
|
|
||||||
// --- Setup DI Container for DI in McpElements class ---
|
$request = $creator->fromGlobals();
|
||||||
$container = new Container();
|
|
||||||
$container->set(LoggerInterface::class, logger());
|
|
||||||
|
|
||||||
Server::make()
|
$server = Server::make()
|
||||||
->setServerInfo('HTTP User Profiles', '1.0.0')
|
->setServerInfo('HTTP User Profiles', '1.0.0')
|
||||||
->setLogger(logger())
|
->setLogger(logger())
|
||||||
->setContainer($container)
|
->setContainer(container())
|
||||||
|
->setSession(new FileSessionStore(__DIR__.'/sessions'))
|
||||||
->setDiscovery(__DIR__, ['.'])
|
->setDiscovery(__DIR__, ['.'])
|
||||||
->addTool(
|
->addTool(
|
||||||
function (float $a, float $b, string $operation = 'add'): array {
|
function (float $a, float $b, string $operation = 'add'): array {
|
||||||
|
|
@ -70,7 +73,12 @@ Server::make()
|
||||||
description: 'Current system status and runtime information',
|
description: 'Current system status and runtime information',
|
||||||
mimeType: 'application/json'
|
mimeType: 'application/json'
|
||||||
)
|
)
|
||||||
->build()
|
->build();
|
||||||
->connect(new StreamableHttpServerTransport('127.0.0.1', 8080, 'mcp'));
|
|
||||||
|
|
||||||
logger()->info('Server listener stopped gracefully.');
|
$transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory);
|
||||||
|
|
||||||
|
$server->connect($transport);
|
||||||
|
|
||||||
|
$response = $transport->listen();
|
||||||
|
|
||||||
|
(new SapiEmitter())->emit($response);
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ use Mcp\Server\Transport\StdioTransport;
|
||||||
|
|
||||||
logger()->info('Starting MCP Manual Registration (Stdio) Server...');
|
logger()->info('Starting MCP Manual Registration (Stdio) Server...');
|
||||||
|
|
||||||
Server::make()
|
$server = Server::make()
|
||||||
->setServerInfo('Manual Reg Server', '1.0.0')
|
->setServerInfo('Manual Reg Server', '1.0.0')
|
||||||
->setLogger(logger())
|
->setLogger(logger())
|
||||||
->setContainer(container())
|
->setContainer(container())
|
||||||
|
|
@ -27,7 +27,12 @@ Server::make()
|
||||||
->addResource([SimpleHandlers::class, 'getAppVersion'], 'app://version', 'application_version', mimeType: 'text/plain')
|
->addResource([SimpleHandlers::class, 'getAppVersion'], 'app://version', 'application_version', mimeType: 'text/plain')
|
||||||
->addPrompt([SimpleHandlers::class, 'greetingPrompt'], 'personalized_greeting')
|
->addPrompt([SimpleHandlers::class, 'greetingPrompt'], 'personalized_greeting')
|
||||||
->addResourceTemplate([SimpleHandlers::class, 'getItemDetails'], 'item://{itemId}/details', 'get_item_details', mimeType: 'application/json')
|
->addResourceTemplate([SimpleHandlers::class, 'getItemDetails'], 'item://{itemId}/details', 'get_item_details', mimeType: 'application/json')
|
||||||
->build()
|
->build();
|
||||||
->connect(new StdioTransport(logger: logger()));
|
|
||||||
|
$transport = new StdioTransport(logger: logger());
|
||||||
|
|
||||||
|
$server->connect($transport);
|
||||||
|
|
||||||
|
$transport->listen();
|
||||||
|
|
||||||
logger()->info('Server listener stopped gracefully.');
|
logger()->info('Server listener stopped gracefully.');
|
||||||
|
|
|
||||||
|
|
@ -13,16 +13,24 @@
|
||||||
require_once dirname(__DIR__).'/bootstrap.php';
|
require_once dirname(__DIR__).'/bootstrap.php';
|
||||||
chdir(__DIR__);
|
chdir(__DIR__);
|
||||||
|
|
||||||
|
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
|
||||||
use Mcp\CombinedHttpExample\Manual\ManualHandlers;
|
use Mcp\CombinedHttpExample\Manual\ManualHandlers;
|
||||||
use Mcp\Server;
|
use Mcp\Server;
|
||||||
use Mcp\Server\Transports\HttpServerTransport;
|
use Mcp\Server\Session\FileSessionStore;
|
||||||
|
use Mcp\Server\Transport\StreamableHttpTransport;
|
||||||
|
use Nyholm\Psr7\Factory\Psr17Factory;
|
||||||
|
use Nyholm\Psr7Server\ServerRequestCreator;
|
||||||
|
|
||||||
logger()->info('Starting MCP Combined Registration (HTTP) Server...');
|
$psr17Factory = new Psr17Factory();
|
||||||
|
$creator = new ServerRequestCreator($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
|
||||||
|
|
||||||
Server::make()
|
$request = $creator->fromGlobals();
|
||||||
|
|
||||||
|
$server = Server::make()
|
||||||
->setServerInfo('Combined HTTP Server', '1.0.0')
|
->setServerInfo('Combined HTTP Server', '1.0.0')
|
||||||
->setLogger(logger())
|
->setLogger(logger())
|
||||||
->setContainer(container())
|
->setContainer(container())
|
||||||
|
->setSession(new FileSessionStore(__DIR__.'/sessions'))
|
||||||
->setDiscovery(__DIR__, ['.'])
|
->setDiscovery(__DIR__, ['.'])
|
||||||
->addTool([ManualHandlers::class, 'manualGreeter'])
|
->addTool([ManualHandlers::class, 'manualGreeter'])
|
||||||
->addResource(
|
->addResource(
|
||||||
|
|
@ -30,7 +38,12 @@ Server::make()
|
||||||
'config://priority',
|
'config://priority',
|
||||||
'priority_config_manual',
|
'priority_config_manual',
|
||||||
)
|
)
|
||||||
->build()
|
->build();
|
||||||
->connect(new HttpServerTransport('127.0.0.1', 8081, 'mcp_combined'));
|
|
||||||
|
|
||||||
logger()->info('Server listener stopped gracefully.');
|
$transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory);
|
||||||
|
|
||||||
|
$server->connect($transport);
|
||||||
|
|
||||||
|
$response = $transport->listen();
|
||||||
|
|
||||||
|
(new SapiEmitter())->emit($response);
|
||||||
|
|
|
||||||
|
|
@ -49,11 +49,16 @@ use Mcp\Server\Transport\StdioTransport;
|
||||||
|
|
||||||
logger()->info('Starting MCP Stdio Environment Variable Example Server...');
|
logger()->info('Starting MCP Stdio Environment Variable Example Server...');
|
||||||
|
|
||||||
Server::make()
|
$server = Server::make()
|
||||||
->setServerInfo('Env Var Server', '1.0.0')
|
->setServerInfo('Env Var Server', '1.0.0')
|
||||||
->setLogger(logger())
|
->setLogger(logger())
|
||||||
->setDiscovery(__DIR__, ['.'])
|
->setDiscovery(__DIR__, ['.'])
|
||||||
->build()
|
->build();
|
||||||
->connect(new StdioTransport(logger: logger()));
|
|
||||||
|
$transport = new StdioTransport(logger: logger());
|
||||||
|
|
||||||
|
$server->connect($transport);
|
||||||
|
|
||||||
|
$transport->listen();
|
||||||
|
|
||||||
logger()->info('Server listener stopped gracefully.');
|
logger()->info('Server listener stopped gracefully.');
|
||||||
|
|
|
||||||
|
|
@ -27,12 +27,17 @@ $container->set(Services\TaskRepositoryInterface::class, $taskRepo);
|
||||||
$statsService = new Services\SystemStatsService($taskRepo);
|
$statsService = new Services\SystemStatsService($taskRepo);
|
||||||
$container->set(Services\StatsServiceInterface::class, $statsService);
|
$container->set(Services\StatsServiceInterface::class, $statsService);
|
||||||
|
|
||||||
Server::make()
|
$server = Server::make()
|
||||||
->setServerInfo('Task Manager Server', '1.0.0')
|
->setServerInfo('Task Manager Server', '1.0.0')
|
||||||
->setLogger(logger())
|
->setLogger(logger())
|
||||||
->setContainer($container)
|
->setContainer($container)
|
||||||
->setDiscovery(__DIR__, ['.'])
|
->setDiscovery(__DIR__, ['.'])
|
||||||
->build()
|
->build();
|
||||||
->connect(new StdioTransport(logger: logger()));
|
|
||||||
|
$transport = new StdioTransport(logger: logger());
|
||||||
|
|
||||||
|
$server->connect($transport);
|
||||||
|
|
||||||
|
$transport->listen();
|
||||||
|
|
||||||
logger()->info('Server listener stopped gracefully.');
|
logger()->info('Server listener stopped gracefully.');
|
||||||
|
|
|
||||||
|
|
@ -13,17 +13,30 @@
|
||||||
require_once dirname(__DIR__).'/bootstrap.php';
|
require_once dirname(__DIR__).'/bootstrap.php';
|
||||||
chdir(__DIR__);
|
chdir(__DIR__);
|
||||||
|
|
||||||
|
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
|
||||||
use Mcp\Server;
|
use Mcp\Server;
|
||||||
use Mcp\Server\Transports\HttpServerTransport;
|
use Mcp\Server\Session\FileSessionStore;
|
||||||
|
use Mcp\Server\Transport\StreamableHttpTransport;
|
||||||
|
use Nyholm\Psr7\Factory\Psr17Factory;
|
||||||
|
use Nyholm\Psr7Server\ServerRequestCreator;
|
||||||
|
|
||||||
logger()->info('Starting MCP Complex Schema HTTP Server...');
|
$psr17Factory = new Psr17Factory();
|
||||||
|
$creator = new ServerRequestCreator($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
|
||||||
|
|
||||||
Server::make()
|
$request = $creator->fromGlobals();
|
||||||
|
|
||||||
|
$server = Server::make()
|
||||||
->setServerInfo('Event Scheduler Server', '1.0.0')
|
->setServerInfo('Event Scheduler Server', '1.0.0')
|
||||||
->setLogger(logger())
|
->setLogger(logger())
|
||||||
->setContainer(container())
|
->setContainer(container())
|
||||||
|
->setSession(new FileSessionStore(__DIR__.'/sessions'))
|
||||||
->setDiscovery(__DIR__, ['.'])
|
->setDiscovery(__DIR__, ['.'])
|
||||||
->build()
|
->build();
|
||||||
->connect(new HttpServerTransport('127.0.0.1', 8082, 'mcp_scheduler'));
|
|
||||||
|
|
||||||
logger()->info('Server listener stopped gracefully.');
|
$transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory);
|
||||||
|
|
||||||
|
$server->connect($transport);
|
||||||
|
|
||||||
|
$response = $transport->listen();
|
||||||
|
|
||||||
|
(new SapiEmitter())->emit($response);
|
||||||
|
|
|
||||||
|
|
@ -13,16 +13,29 @@
|
||||||
require_once dirname(__DIR__).'/bootstrap.php';
|
require_once dirname(__DIR__).'/bootstrap.php';
|
||||||
chdir(__DIR__);
|
chdir(__DIR__);
|
||||||
|
|
||||||
|
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
|
||||||
use Mcp\Server;
|
use Mcp\Server;
|
||||||
use Mcp\Server\Transports\StreamableHttpServerTransport;
|
use Mcp\Server\Session\FileSessionStore;
|
||||||
|
use Mcp\Server\Transport\StreamableHttpTransport;
|
||||||
|
use Nyholm\Psr7\Factory\Psr17Factory;
|
||||||
|
use Nyholm\Psr7Server\ServerRequestCreator;
|
||||||
|
|
||||||
logger()->info('Starting MCP Schema Showcase Server...');
|
$psr17Factory = new Psr17Factory();
|
||||||
|
$creator = new ServerRequestCreator($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
|
||||||
|
|
||||||
Server::make()
|
$request = $creator->fromGlobals();
|
||||||
|
|
||||||
|
$server = Server::make()
|
||||||
->setServerInfo('Schema Showcase', '1.0.0')
|
->setServerInfo('Schema Showcase', '1.0.0')
|
||||||
->setLogger(logger())
|
->setLogger(logger())
|
||||||
|
->setSession(new FileSessionStore(__DIR__.'/sessions'))
|
||||||
->setDiscovery(__DIR__, ['.'])
|
->setDiscovery(__DIR__, ['.'])
|
||||||
->build()
|
->build();
|
||||||
->connect(new StreamableHttpServerTransport('127.0.0.1', 8080, 'mcp'));
|
|
||||||
|
|
||||||
logger()->info('Server listener stopped gracefully.');
|
$transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory);
|
||||||
|
|
||||||
|
$server->connect($transport);
|
||||||
|
|
||||||
|
$response = $transport->listen();
|
||||||
|
|
||||||
|
(new SapiEmitter())->emit($response);
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@
|
||||||
|
|
||||||
require __DIR__.'/vendor/autoload.php';
|
require __DIR__.'/vendor/autoload.php';
|
||||||
|
|
||||||
|
use Mcp\Server;
|
||||||
|
use Mcp\Server\Transport\StdioTransport;
|
||||||
use Symfony\Component\Console as SymfonyConsole;
|
use Symfony\Component\Console as SymfonyConsole;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
|
|
@ -20,18 +22,14 @@ $debug = (bool) ($_SERVER['DEBUG'] ?? false);
|
||||||
$output = new SymfonyConsole\Output\ConsoleOutput($debug ? OutputInterface::VERBOSITY_VERY_VERBOSE : OutputInterface::VERBOSITY_NORMAL);
|
$output = new SymfonyConsole\Output\ConsoleOutput($debug ? OutputInterface::VERBOSITY_VERY_VERBOSE : OutputInterface::VERBOSITY_NORMAL);
|
||||||
$logger = new SymfonyConsole\Logger\ConsoleLogger($output);
|
$logger = new SymfonyConsole\Logger\ConsoleLogger($output);
|
||||||
|
|
||||||
// Configure the JsonRpcHandler and build the functionality
|
$server = Server::make()
|
||||||
$jsonRpcHandler = new Mcp\JsonRpc\Handler(
|
->setServerInfo('Standalone CLI', '1.0.0')
|
||||||
Mcp\JsonRpc\MessageFactory::make(),
|
->setLogger($logger)
|
||||||
App\Builder::buildMethodHandlers(),
|
->setDiscovery(__DIR__, ['.'])
|
||||||
$logger
|
->build();
|
||||||
);
|
|
||||||
|
|
||||||
// Set up the server
|
$transport = new StdioTransport(logger: $logger);
|
||||||
$sever = new Mcp\Server($jsonRpcHandler, $logger);
|
|
||||||
|
|
||||||
// Create the transport layer using Stdio
|
$server->connect($transport);
|
||||||
$transport = new Mcp\Server\Transport\StdioTransport(logger: $logger);
|
|
||||||
|
|
||||||
// Start our application
|
$transport->listen();
|
||||||
$sever->connect($transport);
|
|
||||||
|
|
|
||||||
118
examples/10-simple-http-transport/McpElements.php
Normal file
118
examples/10-simple-http-transport/McpElements.php
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
<?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\HttpTransportExample;
|
||||||
|
|
||||||
|
use Mcp\Capability\Attribute\McpPrompt;
|
||||||
|
use Mcp\Capability\Attribute\McpResource;
|
||||||
|
use Mcp\Capability\Attribute\McpTool;
|
||||||
|
|
||||||
|
class McpElements
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current server time.
|
||||||
|
*/
|
||||||
|
#[McpTool(name: 'current_time')]
|
||||||
|
public function getCurrentTime(string $format = 'Y-m-d H:i:s'): string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return date($format);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return date('Y-m-d H:i:s'); // fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate simple math operations.
|
||||||
|
*/
|
||||||
|
#[McpTool(name: 'calculate')]
|
||||||
|
public function calculate(float $a, float $b, string $operation): float|string
|
||||||
|
{
|
||||||
|
return match (strtolower($operation)) {
|
||||||
|
'add', '+' => $a + $b,
|
||||||
|
'subtract', '-' => $a - $b,
|
||||||
|
'multiply', '*' => $a * $b,
|
||||||
|
'divide', '/' => 0 != $b ? $a / $b : 'Error: Division by zero',
|
||||||
|
default => 'Error: Unknown operation. Use: add, subtract, multiply, divide',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server information resource.
|
||||||
|
*
|
||||||
|
* @return array{status: string, timestamp: int, version: string, transport: string, uptime: int}
|
||||||
|
*/
|
||||||
|
#[McpResource(
|
||||||
|
uri: 'info://server/status',
|
||||||
|
name: 'server_status',
|
||||||
|
description: 'Current server status and information',
|
||||||
|
mimeType: 'application/json'
|
||||||
|
)]
|
||||||
|
public function getServerStatus(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'status' => 'running',
|
||||||
|
'timestamp' => time(),
|
||||||
|
'version' => '1.0.0',
|
||||||
|
'transport' => 'HTTP',
|
||||||
|
'uptime' => time() - $_SERVER['REQUEST_TIME'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration resource.
|
||||||
|
*
|
||||||
|
* @return array{debug: bool, environment: string, timezone: string, locale: string}
|
||||||
|
*/
|
||||||
|
#[McpResource(
|
||||||
|
uri: 'config://app/settings',
|
||||||
|
name: 'app_config',
|
||||||
|
description: 'Application configuration settings',
|
||||||
|
mimeType: 'application/json'
|
||||||
|
)]
|
||||||
|
public function getAppConfig(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'debug' => $_SERVER['DEBUG'] ?? false,
|
||||||
|
'environment' => $_SERVER['APP_ENV'] ?? 'production',
|
||||||
|
'timezone' => date_default_timezone_get(),
|
||||||
|
'locale' => 'en_US',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Greeting prompt.
|
||||||
|
*
|
||||||
|
* @return array{role: string, content: string}
|
||||||
|
*/
|
||||||
|
#[McpPrompt(
|
||||||
|
name: 'greet',
|
||||||
|
description: 'Generate a personalized greeting message'
|
||||||
|
)]
|
||||||
|
public function greetPrompt(string $firstName = 'World', string $timeOfDay = 'day'): array
|
||||||
|
{
|
||||||
|
$greeting = match (strtolower($timeOfDay)) {
|
||||||
|
'morning' => 'Good morning',
|
||||||
|
'afternoon' => 'Good afternoon',
|
||||||
|
'evening', 'night' => 'Good evening',
|
||||||
|
default => 'Hello',
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
'role' => 'user',
|
||||||
|
'content' => "# {$greeting}, {$firstName}!\n\nWelcome to our MCP HTTP Server example. This demonstrates how to use the Model Context Protocol over HTTP transport.",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
24
examples/10-simple-http-transport/README.md
Normal file
24
examples/10-simple-http-transport/README.md
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# HTTP MCP Server Example
|
||||||
|
|
||||||
|
This example demonstrates how to use the MCP SDK with HTTP transport using the StreamableHttpTransport. It provides a complete HTTP-based MCP server that can handle JSON-RPC requests over HTTP POST.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
**Step 1: Start the HTTP server**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd examples/10-simple-http-transport
|
||||||
|
php -S localhost:8000 server.php
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Connect with MCP Inspector**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx @modelcontextprotocol/inspector http://localhost:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Features
|
||||||
|
|
||||||
|
- **Tools**: `current_time`, `calculate`
|
||||||
|
- **Resources**: `info://server/status`, `config://app/settings`
|
||||||
|
- **Prompts**: `greet`
|
||||||
40
examples/10-simple-http-transport/server.php
Normal file
40
examples/10-simple-http-transport/server.php
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
<?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 Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
|
||||||
|
use Mcp\Server;
|
||||||
|
use Mcp\Server\Session\FileSessionStore;
|
||||||
|
use Mcp\Server\Transport\StreamableHttpTransport;
|
||||||
|
use Nyholm\Psr7\Factory\Psr17Factory;
|
||||||
|
use Nyholm\Psr7Server\ServerRequestCreator;
|
||||||
|
|
||||||
|
$psr17Factory = new Psr17Factory();
|
||||||
|
$creator = new ServerRequestCreator($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
|
||||||
|
|
||||||
|
$request = $creator->fromGlobals();
|
||||||
|
|
||||||
|
$server = Server::make()
|
||||||
|
->setServerInfo('HTTP MCP Server', '1.0.0', 'MCP Server over HTTP transport')
|
||||||
|
->setContainer(container())
|
||||||
|
->setSession(new FileSessionStore(__DIR__.'/sessions'))
|
||||||
|
->setDiscovery(__DIR__, ['.'])
|
||||||
|
->build();
|
||||||
|
|
||||||
|
$transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory);
|
||||||
|
|
||||||
|
$server->connect($transport);
|
||||||
|
|
||||||
|
$response = $transport->listen();
|
||||||
|
|
||||||
|
(new SapiEmitter())->emit($response);
|
||||||
|
|
@ -54,17 +54,6 @@ parameters:
|
||||||
count: 1
|
count: 1
|
||||||
path: examples/02-discovery-http-userprofile/server.php
|
path: examples/02-discovery-http-userprofile/server.php
|
||||||
|
|
||||||
-
|
|
||||||
message: '#^Instantiated class StreamableHttpServerTransport not found\.$#'
|
|
||||||
identifier: class.notFound
|
|
||||||
count: 1
|
|
||||||
path: examples/02-discovery-http-userprofile/server.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: '#^Parameter \#1 \$transport of method Mcp\\Server\:\:connect\(\) expects Mcp\\Server\\TransportInterface, StreamableHttpServerTransport given\.$#'
|
|
||||||
identifier: argument.type
|
|
||||||
count: 1
|
|
||||||
path: examples/02-discovery-http-userprofile/server.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: '#^Method Mcp\\Example\\ManualStdioExample\\SimpleHandlers\:\:getItemDetails\(\) return type has no value type specified in iterable type array\.$#'
|
message: '#^Method Mcp\\Example\\ManualStdioExample\\SimpleHandlers\:\:getItemDetails\(\) return type has no value type specified in iterable type array\.$#'
|
||||||
|
|
@ -84,17 +73,6 @@ parameters:
|
||||||
count: 2
|
count: 2
|
||||||
path: examples/04-combined-registration-http/server.php
|
path: examples/04-combined-registration-http/server.php
|
||||||
|
|
||||||
-
|
|
||||||
message: '#^Instantiated class Mcp\\Server\\Transports\\HttpServerTransport not found\.$#'
|
|
||||||
identifier: class.notFound
|
|
||||||
count: 1
|
|
||||||
path: examples/04-combined-registration-http/server.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: '#^Parameter \#1 \$transport of method Mcp\\Server\:\:connect\(\) expects Mcp\\Server\\TransportInterface, Mcp\\Server\\Transports\\HttpServerTransport given\.$#'
|
|
||||||
identifier: argument.type
|
|
||||||
count: 1
|
|
||||||
path: examples/04-combined-registration-http/server.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: '#^Method Mcp\\Example\\StdioEnvVariables\\EnvToolHandler\:\:processData\(\) return type has no value type specified in iterable type array\.$#'
|
message: '#^Method Mcp\\Example\\StdioEnvVariables\\EnvToolHandler\:\:processData\(\) return type has no value type specified in iterable type array\.$#'
|
||||||
|
|
@ -288,17 +266,6 @@ parameters:
|
||||||
count: 2
|
count: 2
|
||||||
path: examples/07-complex-tool-schema-http/McpEventScheduler.php
|
path: examples/07-complex-tool-schema-http/McpEventScheduler.php
|
||||||
|
|
||||||
-
|
|
||||||
message: '#^Instantiated class Mcp\\Server\\Transports\\HttpServerTransport not found\.$#'
|
|
||||||
identifier: class.notFound
|
|
||||||
count: 1
|
|
||||||
path: examples/07-complex-tool-schema-http/server.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: '#^Parameter \#1 \$transport of method Mcp\\Server\:\:connect\(\) expects Mcp\\Server\\TransportInterface, Mcp\\Server\\Transports\\HttpServerTransport given\.$#'
|
|
||||||
identifier: argument.type
|
|
||||||
count: 1
|
|
||||||
path: examples/07-complex-tool-schema-http/server.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: '#^Method Mcp\\Example\\SchemaShowcaseExample\\SchemaShowcaseElements\:\:calculateRange\(\) return type has no value type specified in iterable type array\.$#'
|
message: '#^Method Mcp\\Example\\SchemaShowcaseExample\\SchemaShowcaseElements\:\:calculateRange\(\) return type has no value type specified in iterable type array\.$#'
|
||||||
|
|
@ -354,17 +321,6 @@ parameters:
|
||||||
count: 1
|
count: 1
|
||||||
path: examples/08-schema-showcase-streamable/SchemaShowcaseElements.php
|
path: examples/08-schema-showcase-streamable/SchemaShowcaseElements.php
|
||||||
|
|
||||||
-
|
|
||||||
message: '#^Instantiated class Mcp\\Server\\Transports\\StreamableHttpServerTransport not found\.$#'
|
|
||||||
identifier: class.notFound
|
|
||||||
count: 1
|
|
||||||
path: examples/08-schema-showcase-streamable/server.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: '#^Parameter \#1 \$transport of method Mcp\\Server\:\:connect\(\) expects Mcp\\Server\\TransportInterface, Mcp\\Server\\Transports\\StreamableHttpServerTransport given\.$#'
|
|
||||||
identifier: argument.type
|
|
||||||
count: 1
|
|
||||||
path: examples/08-schema-showcase-streamable/server.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: '#^Parameter \#1 \$registry of class Mcp\\Server\\RequestHandler\\ListPromptsHandler constructor expects Mcp\\Capability\\Registry\\ReferenceProviderInterface, Mcp\\Capability\\PromptChain given\.$#'
|
message: '#^Parameter \#1 \$registry of class Mcp\\Server\\RequestHandler\\ListPromptsHandler constructor expects Mcp\\Capability\\Registry\\ReferenceProviderInterface, Mcp\\Capability\\PromptChain given\.$#'
|
||||||
|
|
|
||||||
|
|
@ -24,11 +24,16 @@ use Mcp\Schema\Implementation;
|
||||||
use Mcp\Schema\JsonRpc\Error;
|
use Mcp\Schema\JsonRpc\Error;
|
||||||
use Mcp\Schema\JsonRpc\HasMethodInterface;
|
use Mcp\Schema\JsonRpc\HasMethodInterface;
|
||||||
use Mcp\Schema\JsonRpc\Response;
|
use Mcp\Schema\JsonRpc\Response;
|
||||||
|
use Mcp\Schema\Request\InitializeRequest;
|
||||||
use Mcp\Server\MethodHandlerInterface;
|
use Mcp\Server\MethodHandlerInterface;
|
||||||
use Mcp\Server\NotificationHandler;
|
use Mcp\Server\NotificationHandler;
|
||||||
use Mcp\Server\RequestHandler;
|
use Mcp\Server\RequestHandler;
|
||||||
|
use Mcp\Server\Session\SessionFactoryInterface;
|
||||||
|
use Mcp\Server\Session\SessionInterface;
|
||||||
|
use Mcp\Server\Session\SessionStoreInterface;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Psr\Log\NullLogger;
|
use Psr\Log\NullLogger;
|
||||||
|
use Symfony\Component\Uid\Uuid;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @final
|
* @final
|
||||||
|
|
@ -47,6 +52,8 @@ class Handler
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly MessageFactory $messageFactory,
|
private readonly MessageFactory $messageFactory,
|
||||||
|
private readonly SessionFactoryInterface $sessionFactory,
|
||||||
|
private readonly SessionStoreInterface $sessionStore,
|
||||||
iterable $methodHandlers,
|
iterable $methodHandlers,
|
||||||
private readonly LoggerInterface $logger = new NullLogger(),
|
private readonly LoggerInterface $logger = new NullLogger(),
|
||||||
) {
|
) {
|
||||||
|
|
@ -62,10 +69,14 @@ class Handler
|
||||||
ToolCallerInterface $toolCaller,
|
ToolCallerInterface $toolCaller,
|
||||||
ResourceReaderInterface $resourceReader,
|
ResourceReaderInterface $resourceReader,
|
||||||
PromptGetterInterface $promptGetter,
|
PromptGetterInterface $promptGetter,
|
||||||
|
SessionStoreInterface $sessionStore,
|
||||||
|
SessionFactoryInterface $sessionFactory,
|
||||||
LoggerInterface $logger = new NullLogger(),
|
LoggerInterface $logger = new NullLogger(),
|
||||||
): self {
|
): self {
|
||||||
return new self(
|
return new self(
|
||||||
messageFactory: MessageFactory::make(),
|
messageFactory: MessageFactory::make(),
|
||||||
|
sessionFactory: $sessionFactory,
|
||||||
|
sessionStore: $sessionStore,
|
||||||
methodHandlers: [
|
methodHandlers: [
|
||||||
new NotificationHandler\InitializedHandler(),
|
new NotificationHandler\InitializedHandler(),
|
||||||
new RequestHandler\InitializeHandler($registry->getCapabilities(), $implementation),
|
new RequestHandler\InitializeHandler($registry->getCapabilities(), $implementation),
|
||||||
|
|
@ -82,29 +93,78 @@ class Handler
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return iterable<string|null>
|
* @return iterable<array{string|null, array<string, mixed>}>
|
||||||
*
|
*
|
||||||
* @throws ExceptionInterface When a handler throws an exception during message processing
|
* @throws ExceptionInterface When a handler throws an exception during message processing
|
||||||
* @throws \JsonException When JSON encoding of the response fails
|
* @throws \JsonException When JSON encoding of the response fails
|
||||||
*/
|
*/
|
||||||
public function process(string $input): iterable
|
public function process(string $input, ?Uuid $sessionId): iterable
|
||||||
{
|
{
|
||||||
$this->logger->info('Received message to process.', ['message' => $input]);
|
$this->logger->info('Received message to process.', ['message' => $input]);
|
||||||
|
|
||||||
|
$this->runGarbageCollection();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$messages = $this->messageFactory->create($input);
|
$messages = iterator_to_array($this->messageFactory->create($input));
|
||||||
} catch (\JsonException $e) {
|
} catch (\JsonException $e) {
|
||||||
$this->logger->warning('Failed to decode json message.', ['exception' => $e]);
|
$this->logger->warning('Failed to decode json message.', ['exception' => $e]);
|
||||||
|
$error = Error::forParseError($e->getMessage());
|
||||||
yield $this->encodeResponse(Error::forParseError($e->getMessage()));
|
yield [$this->encodeResponse($error), []];
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$hasInitializeRequest = false;
|
||||||
|
foreach ($messages as $message) {
|
||||||
|
if ($message instanceof InitializeRequest) {
|
||||||
|
$hasInitializeRequest = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$session = null;
|
||||||
|
|
||||||
|
if ($hasInitializeRequest) {
|
||||||
|
// Spec: An initialize request must not be part of a batch.
|
||||||
|
if (\count($messages) > 1) {
|
||||||
|
$error = Error::forInvalidRequest('The "initialize" request MUST NOT be part of a batch.');
|
||||||
|
yield [$this->encodeResponse($error), []];
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spec: An initialize request must not have a session ID.
|
||||||
|
if ($sessionId) {
|
||||||
|
$error = Error::forInvalidRequest('A session ID MUST NOT be sent with an "initialize" request.');
|
||||||
|
yield [$this->encodeResponse($error), []];
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$session = $this->sessionFactory->create($this->sessionStore);
|
||||||
|
} else {
|
||||||
|
if (!$sessionId) {
|
||||||
|
$error = Error::forInvalidRequest('A valid session id is REQUIRED for non-initialize requests.');
|
||||||
|
yield [$this->encodeResponse($error), ['status_code' => 400]];
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->sessionStore->exists($sessionId)) {
|
||||||
|
$error = Error::forInvalidRequest('Session not found or has expired.');
|
||||||
|
yield [$this->encodeResponse($error), ['status_code' => 404]];
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$session = $this->sessionFactory->createWithId($sessionId, $this->sessionStore);
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($messages as $message) {
|
foreach ($messages as $message) {
|
||||||
if ($message instanceof InvalidInputMessageException) {
|
if ($message instanceof InvalidInputMessageException) {
|
||||||
$this->logger->warning('Failed to create message.', ['exception' => $message]);
|
$this->logger->warning('Failed to create message.', ['exception' => $message]);
|
||||||
yield $this->encodeResponse(Error::forInvalidRequest($message->getMessage(), 0));
|
$error = Error::forInvalidRequest($message->getMessage(), 0);
|
||||||
|
yield [$this->encodeResponse($error), []];
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -113,24 +173,32 @@ class Handler
|
||||||
]);
|
]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
yield $this->encodeResponse($this->handle($message));
|
$response = $this->handle($message, $session);
|
||||||
|
yield [$this->encodeResponse($response), ['session_id' => $session->getId()]];
|
||||||
} catch (\DomainException) {
|
} catch (\DomainException) {
|
||||||
yield null;
|
yield [null, []];
|
||||||
} catch (NotFoundExceptionInterface $e) {
|
} catch (NotFoundExceptionInterface $e) {
|
||||||
$this->logger->warning(\sprintf('Failed to create response: %s', $e->getMessage()), ['exception' => $e],
|
$this->logger->warning(
|
||||||
|
\sprintf('Failed to create response: %s', $e->getMessage()),
|
||||||
|
['exception' => $e],
|
||||||
);
|
);
|
||||||
|
|
||||||
yield $this->encodeResponse(Error::forMethodNotFound($e->getMessage()));
|
$error = Error::forMethodNotFound($e->getMessage());
|
||||||
|
yield [$this->encodeResponse($error), []];
|
||||||
} catch (\InvalidArgumentException $e) {
|
} catch (\InvalidArgumentException $e) {
|
||||||
$this->logger->warning(\sprintf('Invalid argument: %s', $e->getMessage()), ['exception' => $e]);
|
$this->logger->warning(\sprintf('Invalid argument: %s', $e->getMessage()), ['exception' => $e]);
|
||||||
|
|
||||||
yield $this->encodeResponse(Error::forInvalidParams($e->getMessage()));
|
$error = Error::forInvalidParams($e->getMessage());
|
||||||
|
yield [$this->encodeResponse($error), []];
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->logger->critical(\sprintf('Uncaught exception: %s', $e->getMessage()), ['exception' => $e]);
|
$this->logger->critical(\sprintf('Uncaught exception: %s', $e->getMessage()), ['exception' => $e]);
|
||||||
|
|
||||||
yield $this->encodeResponse(Error::forInternalError($e->getMessage()));
|
$error = Error::forInternalError($e->getMessage());
|
||||||
|
yield [$this->encodeResponse($error), []];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$session->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -159,7 +227,7 @@ class Handler
|
||||||
* @throws NotFoundExceptionInterface When no handler is found for the request method
|
* @throws NotFoundExceptionInterface When no handler is found for the request method
|
||||||
* @throws ExceptionInterface When a request handler throws an exception
|
* @throws ExceptionInterface When a request handler throws an exception
|
||||||
*/
|
*/
|
||||||
private function handle(HasMethodInterface $message): Response|Error|null
|
private function handle(HasMethodInterface $message, SessionInterface $session): Response|Error|null
|
||||||
{
|
{
|
||||||
$this->logger->info(\sprintf('Handling message for method "%s".', $message::getMethod()), [
|
$this->logger->info(\sprintf('Handling message for method "%s".', $message::getMethod()), [
|
||||||
'message' => $message,
|
'message' => $message,
|
||||||
|
|
@ -167,18 +235,20 @@ class Handler
|
||||||
|
|
||||||
$handled = false;
|
$handled = false;
|
||||||
foreach ($this->methodHandlers as $handler) {
|
foreach ($this->methodHandlers as $handler) {
|
||||||
if ($handler->supports($message)) {
|
if (!$handler->supports($message)) {
|
||||||
$return = $handler->handle($message);
|
continue;
|
||||||
$handled = true;
|
}
|
||||||
|
|
||||||
$this->logger->debug(\sprintf('Message handled by "%s".', $handler::class), [
|
$return = $handler->handle($message, $session);
|
||||||
'method' => $message::getMethod(),
|
$handled = true;
|
||||||
'response' => $return,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (null !== $return) {
|
$this->logger->debug(\sprintf('Message handled by "%s".', $handler::class), [
|
||||||
return $return;
|
'method' => $message::getMethod(),
|
||||||
}
|
'response' => $return,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (null !== $return) {
|
||||||
|
return $return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -188,4 +258,32 @@ class Handler
|
||||||
|
|
||||||
throw new HandlerNotFoundException(\sprintf('No handler found for method "%s".', $message::getMethod()));
|
throw new HandlerNotFoundException(\sprintf('No handler found for method "%s".', $message::getMethod()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run garbage collection on expired sessions.
|
||||||
|
* Uses the session store's internal TTL configuration.
|
||||||
|
*/
|
||||||
|
private function runGarbageCollection(): void
|
||||||
|
{
|
||||||
|
if (random_int(0, 100) > 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$deletedSessions = $this->sessionStore->gc();
|
||||||
|
if (!empty($deletedSessions)) {
|
||||||
|
$this->logger->debug('Garbage collected expired sessions.', [
|
||||||
|
'count' => \count($deletedSessions),
|
||||||
|
'session_ids' => array_map(fn (Uuid $id) => $id->toRfc4122(), $deletedSessions),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy a specific session.
|
||||||
|
*/
|
||||||
|
public function destroySession(Uuid $sessionId): void
|
||||||
|
{
|
||||||
|
$this->sessionStore->destroy($sessionId);
|
||||||
|
$this->logger->info('Session destroyed.', ['session_id' => $sessionId->toRfc4122()]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,11 @@ use Mcp\Server\ServerBuilder;
|
||||||
use Mcp\Server\TransportInterface;
|
use Mcp\Server\TransportInterface;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Psr\Log\NullLogger;
|
use Psr\Log\NullLogger;
|
||||||
|
use Symfony\Component\Uid\Uuid;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Christopher Hertel <mail@christopher-hertel.de>
|
* @author Christopher Hertel <mail@christopher-hertel.de>
|
||||||
|
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
|
||||||
*/
|
*/
|
||||||
final class Server
|
final class Server
|
||||||
{
|
{
|
||||||
|
|
@ -36,37 +38,30 @@ final class Server
|
||||||
public function connect(TransportInterface $transport): void
|
public function connect(TransportInterface $transport): void
|
||||||
{
|
{
|
||||||
$transport->initialize();
|
$transport->initialize();
|
||||||
|
|
||||||
$this->logger->info('Transport initialized.', [
|
$this->logger->info('Transport initialized.', [
|
||||||
'transport' => $transport::class,
|
'transport' => $transport::class,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
while ($transport->isConnected()) {
|
$transport->onMessage(function (string $message, ?Uuid $sessionId) use ($transport) {
|
||||||
foreach ($transport->receive() as $message) {
|
try {
|
||||||
if (null === $message) {
|
foreach ($this->jsonRpcHandler->process($message, $sessionId) as [$response, $context]) {
|
||||||
continue;
|
if (null === $response) {
|
||||||
}
|
continue;
|
||||||
|
|
||||||
try {
|
|
||||||
foreach ($this->jsonRpcHandler->process($message) as $response) {
|
|
||||||
if (null === $response) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$transport->send($response);
|
|
||||||
}
|
}
|
||||||
} catch (\JsonException $e) {
|
|
||||||
$this->logger->error('Failed to encode response to JSON.', [
|
$transport->send($response, $context);
|
||||||
'message' => $message,
|
|
||||||
'exception' => $e,
|
|
||||||
]);
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
} catch (\JsonException $e) {
|
||||||
|
$this->logger->error('Failed to encode response to JSON.', [
|
||||||
|
'message' => $message,
|
||||||
|
'exception' => $e,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
usleep(1000);
|
$transport->onSessionEnd(function (Uuid $sessionId) {
|
||||||
}
|
$this->jsonRpcHandler->destroySession($sessionId);
|
||||||
|
});
|
||||||
$transport->close();
|
|
||||||
$this->logger->info('Transport closed');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ use Mcp\Schema\JsonRpc\Error;
|
||||||
use Mcp\Schema\JsonRpc\HasMethodInterface;
|
use Mcp\Schema\JsonRpc\HasMethodInterface;
|
||||||
use Mcp\Schema\JsonRpc\Request;
|
use Mcp\Schema\JsonRpc\Request;
|
||||||
use Mcp\Schema\JsonRpc\Response;
|
use Mcp\Schema\JsonRpc\Response;
|
||||||
|
use Mcp\Server\Session\SessionInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Christopher Hertel <mail@christopher-hertel.de>
|
* @author Christopher Hertel <mail@christopher-hertel.de>
|
||||||
|
|
@ -27,5 +28,5 @@ interface MethodHandlerInterface
|
||||||
/**
|
/**
|
||||||
* @throws ExceptionInterface When the handler encounters an error processing the request
|
* @throws ExceptionInterface When the handler encounters an error processing the request
|
||||||
*/
|
*/
|
||||||
public function handle(HasMethodInterface $message): Response|Error|null;
|
public function handle(HasMethodInterface $message, SessionInterface $session): Response|Error|null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
24
src/Server/NativeClock.php
Normal file
24
src/Server/NativeClock.php
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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\Server;
|
||||||
|
|
||||||
|
use Psr\Clock\ClockInterface;
|
||||||
|
|
||||||
|
class NativeClock implements ClockInterface
|
||||||
|
{
|
||||||
|
public function now(): \DateTimeImmutable
|
||||||
|
{
|
||||||
|
return new \DateTimeImmutable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,7 @@ use Mcp\Schema\JsonRpc\HasMethodInterface;
|
||||||
use Mcp\Schema\JsonRpc\Response;
|
use Mcp\Schema\JsonRpc\Response;
|
||||||
use Mcp\Schema\Notification\InitializedNotification;
|
use Mcp\Schema\Notification\InitializedNotification;
|
||||||
use Mcp\Server\MethodHandlerInterface;
|
use Mcp\Server\MethodHandlerInterface;
|
||||||
|
use Mcp\Server\Session\SessionInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Christopher Hertel <mail@christopher-hertel.de>
|
* @author Christopher Hertel <mail@christopher-hertel.de>
|
||||||
|
|
@ -27,8 +28,10 @@ final class InitializedHandler implements MethodHandlerInterface
|
||||||
return $message instanceof InitializedNotification;
|
return $message instanceof InitializedNotification;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handle(InitializedNotification|HasMethodInterface $message): Response|Error|null
|
public function handle(InitializedNotification|HasMethodInterface $message, SessionInterface $session): Response|Error|null
|
||||||
{
|
{
|
||||||
|
$session->set('initialized', true);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue