From 72360c3279d4d3d70555a2ea6ae21b824f897df3 Mon Sep 17 00:00:00 2001 From: Kyrian Obikwelu Date: Sat, 25 Oct 2025 06:26:33 +0100 Subject: [PATCH] feat: add PSR-17 factory auto-discovery to HTTP transport --- composer.json | 6 +- docs/transports.md | 88 ++++++++++--------- .../http-combined-registration/server.php | 9 +- examples/http-complex-tool-schema/server.php | 9 +- .../http-discovery-userprofile/server.php | 9 +- examples/http-schema-showcase/server.php | 9 +- .../Transport/StreamableHttpTransport.php | 11 ++- 7 files changed, 73 insertions(+), 68 deletions(-) diff --git a/composer.json b/composer.json index 00188f46..ea472ccb 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,7 @@ "php": "^8.1", "ext-fileinfo": "*", "opis/json-schema": "^2.4", + "php-http/discovery": "^1.20", "phpdocumentor/reflection-docblock": "^5.6", "psr/clock": "^1.0", "psr/container": "^2.0", @@ -64,6 +65,9 @@ } }, "config": { - "sort-packages": true + "sort-packages": true, + "allow-plugins": { + "php-http/discovery": false + } } } diff --git a/docs/transports.md b/docs/transports.md index 75902c93..83129ddf 100644 --- a/docs/transports.md +++ b/docs/transports.md @@ -95,24 +95,47 @@ and process requests and send responses. It provides a flexible architecture tha ```php use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Message\ResponseFactoryInterface; -use Psr\Http\Message\StreamFactoryInterface; +// PSR-17 factories are automatically discovered $transport = new StreamableHttpTransport( - request: $serverRequest, // PSR-7 server request - responseFactory: $responseFactory, // PSR-17 response factory - streamFactory: $streamFactory, // PSR-17 stream factory - logger: $logger // Optional PSR-3 logger + request: $serverRequest, // PSR-7 server request + responseFactory: null, // Optional: PSR-17 response factory (auto-discovered if null) + streamFactory: null, // Optional: PSR-17 stream factory (auto-discovered if null) + logger: $logger // Optional PSR-3 logger ); ``` ### Parameters - **`request`** (required): `ServerRequestInterface` - The incoming PSR-7 HTTP request -- **`responseFactory`** (required): `ResponseFactoryInterface` - PSR-17 factory for creating HTTP responses -- **`streamFactory`** (required): `StreamFactoryInterface` - PSR-17 factory for creating response body streams +- **`responseFactory`** (optional): `ResponseFactoryInterface` - PSR-17 factory for creating HTTP responses. Auto-discovered if not provided. +- **`streamFactory`** (optional): `StreamFactoryInterface` - PSR-17 factory for creating response body streams. Auto-discovered if not provided. - **`logger`** (optional): `LoggerInterface` - PSR-3 logger for debugging. Defaults to `NullLogger`. +### PSR-17 Auto-Discovery + +The transport automatically discovers PSR-17 factory implementations from these popular packages: + +- `nyholm/psr7` +- `guzzlehttp/psr7` +- `slim/psr7` +- `laminas/laminas-diactoros` +- And other PSR-17 compatible implementations + +```bash +# Install any PSR-17 package - discovery works automatically +composer require nyholm/psr7 +``` + +If auto-discovery fails or you want to use a specific implementation, you can pass factories explicitly: + +```php +use Nyholm\Psr7\Factory\Psr17Factory; + +$psr17Factory = new Psr17Factory(); +$transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory); +``` + ### Architecture The HTTP transport doesn't run its own web server. Instead, it processes PSR-7 requests and returns PSR-7 responses that @@ -126,19 +149,17 @@ This design allows integration with any PHP framework or application that suppor ### Basic Usage (Standalone) -Here's an opinionated example using Nyholm PSR-7 and Laminas emitter: +Here's a simplified example using PSR-17 discovery and Laminas emitter: ```php +use Http\Discovery\Psr17Factory; use Mcp\Server; use Mcp\Server\Transport\StreamableHttpTransport; use Mcp\Server\Session\FileSessionStore; -use Nyholm\Psr7\Factory\Psr17Factory; -use Nyholm\Psr7Server\ServerRequestCreator; use Laminas\HttpHandlerRunner\Emitter\SapiEmitter; $psr17Factory = new Psr17Factory(); -$creator = new ServerRequestCreator($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory); -$request = $creator->fromGlobals(); +$request = $psr17Factory->createServerRequestFromGlobals(); $server = Server::builder() ->setServerInfo('HTTP Server', '1.0.0') @@ -146,7 +167,7 @@ $server = Server::builder() ->setSession(new FileSessionStore(__DIR__ . '/sessions')) // HTTP needs persistent sessions ->build(); -$transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory); +$transport = new StreamableHttpTransport($request); $response = $server->run($transport); @@ -174,27 +195,23 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory; use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory; -use Nyholm\Psr7\Factory\Psr17Factory; use Mcp\Server; use Mcp\Server\Transport\StreamableHttpTransport; class McpController { - #[Route('/mcp', name: 'mcp_endpoint'] + #[Route('/mcp', name: 'mcp_endpoint')] public function handle(Request $request, Server $server): Response { - // Create PSR-7 factories - $psr17Factory = new Psr17Factory(); - $psrHttpFactory = new PsrHttpFactory($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory); + // Convert Symfony request to PSR-7 (PSR-17 factories auto-discovered) + $psrHttpFactory = new PsrHttpFactory(); $httpFoundationFactory = new HttpFoundationFactory(); - - // Convert Symfony request to PSR-7 $psrRequest = $psrHttpFactory->createRequest($request); - - // Process with MCP - $transport = new StreamableHttpTransport($psrRequest, $psr17Factory, $psr17Factory); + + // Process with MCP (factories auto-discovered) + $transport = new StreamableHttpTransport($psrRequest); $psrResponse = $server->run($transport); - + // Convert PSR-7 response back to Symfony return $httpFoundationFactory->createResponse($psrResponse); } @@ -219,17 +236,14 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; use Mcp\Server; use Mcp\Server\Transport\StreamableHttpTransport; -use Nyholm\Psr7\Factory\Psr17Factory; class McpController { public function handle(ServerRequestInterface $request, Server $server): ResponseInterface { - $psr17Factory = new Psr17Factory(); - // Create the MCP HTTP transport - $transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory); - + $transport = new StreamableHttpTransport($request); + // Process MCP request and return PSR-7 response // Laravel automatically handles PSR-7 responses return $server->run($transport); @@ -248,8 +262,6 @@ Create a route handler using Slim's built-in factories and container: ```php use Slim\Factory\AppFactory; -use Slim\Psr7\Factory\ResponseFactory; -use Slim\Psr7\Factory\StreamFactory; use Mcp\Server; use Mcp\Server\Transport\StreamableHttpTransport; @@ -260,12 +272,9 @@ $app->any('/mcp', function ($request, $response) { ->setServerInfo('My MCP Server', '1.0.0') ->setDiscovery(__DIR__, ['.']) ->build(); - - $responseFactory = new ResponseFactory(); - $streamFactory = new StreamFactory(); - - $transport = new StreamableHttpTransport($request, $responseFactory, $streamFactory); - + + $transport = new StreamableHttpTransport($request); + return $server->run($transport); }); ``` @@ -330,6 +339,3 @@ npx @modelcontextprotocol/inspector http://localhost:8000 The choice between STDIO and HTTP transport depends on the client you want to integrate with. If you are integrating with a client that is running **locally** (like Claude Desktop), use STDIO. If you are building a server in a distributed environment and need to integrate with a **remote** client, use Streamable HTTP. - -One additiona difference to consider is that STDIO is process-based (one session per process) while HTTP is -request-based (multiple sessions via headers). diff --git a/examples/http-combined-registration/server.php b/examples/http-combined-registration/server.php index 660cf3c1..625fb1b7 100644 --- a/examples/http-combined-registration/server.php +++ b/examples/http-combined-registration/server.php @@ -13,18 +13,15 @@ require_once dirname(__DIR__).'/bootstrap.php'; chdir(__DIR__); +use Http\Discovery\Psr17Factory; use Laminas\HttpHandlerRunner\Emitter\SapiEmitter; use Mcp\Example\HttpCombinedRegistration\ManualHandlers; 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(); +$request = $psr17Factory->createServerRequestFromGlobals(); $server = Server::builder() ->setServerInfo('Combined HTTP Server', '1.0.0') @@ -40,7 +37,7 @@ ) ->build(); -$transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory); +$transport = new StreamableHttpTransport($request); $response = $server->run($transport); diff --git a/examples/http-complex-tool-schema/server.php b/examples/http-complex-tool-schema/server.php index a3795a6c..bf95d2c1 100644 --- a/examples/http-complex-tool-schema/server.php +++ b/examples/http-complex-tool-schema/server.php @@ -13,17 +13,14 @@ require_once dirname(__DIR__).'/bootstrap.php'; chdir(__DIR__); +use Http\Discovery\Psr17Factory; 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(); +$request = $psr17Factory->createServerRequestFromGlobals(); $server = Server::builder() ->setServerInfo('Event Scheduler Server', '1.0.0') @@ -33,7 +30,7 @@ ->setDiscovery(__DIR__, ['.']) ->build(); -$transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory); +$transport = new StreamableHttpTransport($request); $response = $server->run($transport); diff --git a/examples/http-discovery-userprofile/server.php b/examples/http-discovery-userprofile/server.php index 6f859c0a..8aae4f22 100644 --- a/examples/http-discovery-userprofile/server.php +++ b/examples/http-discovery-userprofile/server.php @@ -13,17 +13,14 @@ require_once dirname(__DIR__).'/bootstrap.php'; chdir(__DIR__); +use Http\Discovery\Psr17Factory; 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(); +$request = $psr17Factory->createServerRequestFromGlobals(); $server = Server::builder() ->setServerInfo('HTTP User Profiles', '1.0.0') @@ -75,7 +72,7 @@ function (): array { ) ->build(); -$transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory); +$transport = new StreamableHttpTransport($request); $response = $server->run($transport); diff --git a/examples/http-schema-showcase/server.php b/examples/http-schema-showcase/server.php index e8d6d176..599c6967 100644 --- a/examples/http-schema-showcase/server.php +++ b/examples/http-schema-showcase/server.php @@ -13,17 +13,14 @@ require_once dirname(__DIR__).'/bootstrap.php'; chdir(__DIR__); +use Http\Discovery\Psr17Factory; 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(); +$request = $psr17Factory->createServerRequestFromGlobals(); $server = Server::builder() ->setServerInfo('Schema Showcase', '1.0.0') @@ -33,7 +30,7 @@ ->setDiscovery(__DIR__, ['.']) ->build(); -$transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory); +$transport = new StreamableHttpTransport($request); $response = $server->run($transport); diff --git a/src/Server/Transport/StreamableHttpTransport.php b/src/Server/Transport/StreamableHttpTransport.php index f4952952..f4d06b2d 100644 --- a/src/Server/Transport/StreamableHttpTransport.php +++ b/src/Server/Transport/StreamableHttpTransport.php @@ -11,6 +11,7 @@ namespace Mcp\Server\Transport; +use Http\Discovery\Psr17FactoryDiscovery; use Mcp\Schema\JsonRpc\Error; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; @@ -27,6 +28,9 @@ */ class StreamableHttpTransport implements TransportInterface { + private ResponseFactoryInterface $responseFactory; + private StreamFactoryInterface $streamFactory; + /** @var callable(string, ?Uuid): void */ private $messageListener; @@ -49,12 +53,15 @@ class StreamableHttpTransport implements TransportInterface public function __construct( private readonly ServerRequestInterface $request, - private readonly ResponseFactoryInterface $responseFactory, - private readonly StreamFactoryInterface $streamFactory, + ?ResponseFactoryInterface $responseFactory = null, + ?StreamFactoryInterface $streamFactory = null, private readonly LoggerInterface $logger = new NullLogger(), ) { $sessionIdString = $this->request->getHeaderLine('Mcp-Session-Id'); $this->sessionId = $sessionIdString ? Uuid::fromString($sessionIdString) : null; + + $this->responseFactory = $responseFactory ?? Psr17FactoryDiscovery::findResponseFactory(); + $this->streamFactory = $streamFactory ?? Psr17FactoryDiscovery::findStreamFactory(); } public function initialize(): void