feat: add PSR-17 factory auto-discovery to HTTP transport (#119)

This commit is contained in:
Kyrian Obikwelu 2025-10-26 22:06:10 +01:00 • committed by GitHub
commit 0963d243bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 73 additions and 68 deletions

View file

@ -21,6 +21,7 @@
"php": "^8.1", "php": "^8.1",
"ext-fileinfo": "*", "ext-fileinfo": "*",
"opis/json-schema": "^2.4", "opis/json-schema": "^2.4",
"php-http/discovery": "^1.20",
"phpdocumentor/reflection-docblock": "^5.6", "phpdocumentor/reflection-docblock": "^5.6",
"psr/clock": "^1.0", "psr/clock": "^1.0",
"psr/container": "^2.0", "psr/container": "^2.0",
@ -64,6 +65,9 @@
} }
}, },
"config": { "config": {
"sort-packages": true "sort-packages": true,
"allow-plugins": {
"php-http/discovery": false
}
} }
} }

View file

@ -95,13 +95,12 @@ and process requests and send responses. It provides a flexible architecture tha
```php ```php
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
// PSR-17 factories are automatically discovered
$transport = new StreamableHttpTransport( $transport = new StreamableHttpTransport(
request: $serverRequest, // PSR-7 server request request: $serverRequest, // PSR-7 server request
responseFactory: $responseFactory, // PSR-17 response factory responseFactory: null, // Optional: PSR-17 response factory (auto-discovered if null)
streamFactory: $streamFactory, // PSR-17 stream factory streamFactory: null, // Optional: PSR-17 stream factory (auto-discovered if null)
logger: $logger // Optional PSR-3 logger logger: $logger // Optional PSR-3 logger
); );
``` ```
@ -109,10 +108,34 @@ $transport = new StreamableHttpTransport(
### Parameters ### Parameters
- **`request`** (required): `ServerRequestInterface` - The incoming PSR-7 HTTP request - **`request`** (required): `ServerRequestInterface` - The incoming PSR-7 HTTP request
- **`responseFactory`** (required): `ResponseFactoryInterface` - PSR-17 factory for creating HTTP responses - **`responseFactory`** (optional): `ResponseFactoryInterface` - PSR-17 factory for creating HTTP responses. Auto-discovered if not provided.
- **`streamFactory`** (required): `StreamFactoryInterface` - PSR-17 factory for creating response body streams - **`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`. - **`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 ### Architecture
The HTTP transport doesn't run its own web server. Instead, it processes PSR-7 requests and returns PSR-7 responses that 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) ### 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 ```php
use Http\Discovery\Psr17Factory;
use Mcp\Server; use Mcp\Server;
use Mcp\Server\Transport\StreamableHttpTransport; use Mcp\Server\Transport\StreamableHttpTransport;
use Mcp\Server\Session\FileSessionStore; use Mcp\Server\Session\FileSessionStore;
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7Server\ServerRequestCreator;
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter; use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
$psr17Factory = new Psr17Factory(); $psr17Factory = new Psr17Factory();
$creator = new ServerRequestCreator($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory); $request = $psr17Factory->createServerRequestFromGlobals();
$request = $creator->fromGlobals();
$server = Server::builder() $server = Server::builder()
->setServerInfo('HTTP Server', '1.0.0') ->setServerInfo('HTTP Server', '1.0.0')
@ -146,7 +167,7 @@ $server = Server::builder()
->setSession(new FileSessionStore(__DIR__ . '/sessions')) // HTTP needs persistent sessions ->setSession(new FileSessionStore(__DIR__ . '/sessions')) // HTTP needs persistent sessions
->build(); ->build();
$transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory); $transport = new StreamableHttpTransport($request);
$response = $server->run($transport); $response = $server->run($transport);
@ -174,25 +195,21 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory; use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory; use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
use Nyholm\Psr7\Factory\Psr17Factory;
use Mcp\Server; use Mcp\Server;
use Mcp\Server\Transport\StreamableHttpTransport; use Mcp\Server\Transport\StreamableHttpTransport;
class McpController class McpController
{ {
#[Route('/mcp', name: 'mcp_endpoint'] #[Route('/mcp', name: 'mcp_endpoint')]
public function handle(Request $request, Server $server): Response public function handle(Request $request, Server $server): Response
{ {
// Create PSR-7 factories // Convert Symfony request to PSR-7 (PSR-17 factories auto-discovered)
$psr17Factory = new Psr17Factory(); $psrHttpFactory = new PsrHttpFactory();
$psrHttpFactory = new PsrHttpFactory($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
$httpFoundationFactory = new HttpFoundationFactory(); $httpFoundationFactory = new HttpFoundationFactory();
// Convert Symfony request to PSR-7
$psrRequest = $psrHttpFactory->createRequest($request); $psrRequest = $psrHttpFactory->createRequest($request);
// Process with MCP // Process with MCP (factories auto-discovered)
$transport = new StreamableHttpTransport($psrRequest, $psr17Factory, $psr17Factory); $transport = new StreamableHttpTransport($psrRequest);
$psrResponse = $server->run($transport); $psrResponse = $server->run($transport);
// Convert PSR-7 response back to Symfony // Convert PSR-7 response back to Symfony
@ -219,16 +236,13 @@ use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Mcp\Server; use Mcp\Server;
use Mcp\Server\Transport\StreamableHttpTransport; use Mcp\Server\Transport\StreamableHttpTransport;
use Nyholm\Psr7\Factory\Psr17Factory;
class McpController class McpController
{ {
public function handle(ServerRequestInterface $request, Server $server): ResponseInterface public function handle(ServerRequestInterface $request, Server $server): ResponseInterface
{ {
$psr17Factory = new Psr17Factory();
// Create the MCP HTTP transport // Create the MCP HTTP transport
$transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory); $transport = new StreamableHttpTransport($request);
// Process MCP request and return PSR-7 response // Process MCP request and return PSR-7 response
// Laravel automatically handles PSR-7 responses // Laravel automatically handles PSR-7 responses
@ -248,8 +262,6 @@ Create a route handler using Slim's built-in factories and container:
```php ```php
use Slim\Factory\AppFactory; use Slim\Factory\AppFactory;
use Slim\Psr7\Factory\ResponseFactory;
use Slim\Psr7\Factory\StreamFactory;
use Mcp\Server; use Mcp\Server;
use Mcp\Server\Transport\StreamableHttpTransport; use Mcp\Server\Transport\StreamableHttpTransport;
@ -261,10 +273,7 @@ $app->any('/mcp', function ($request, $response) {
->setDiscovery(__DIR__, ['.']) ->setDiscovery(__DIR__, ['.'])
->build(); ->build();
$responseFactory = new ResponseFactory(); $transport = new StreamableHttpTransport($request);
$streamFactory = new StreamFactory();
$transport = new StreamableHttpTransport($request, $responseFactory, $streamFactory);
return $server->run($transport); 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. 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 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. 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).

View file

@ -13,18 +13,15 @@
require_once dirname(__DIR__).'/bootstrap.php'; require_once dirname(__DIR__).'/bootstrap.php';
chdir(__DIR__); chdir(__DIR__);
use Http\Discovery\Psr17Factory;
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter; use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
use Mcp\Example\HttpCombinedRegistration\ManualHandlers; use Mcp\Example\HttpCombinedRegistration\ManualHandlers;
use Mcp\Server; use Mcp\Server;
use Mcp\Server\Session\FileSessionStore; use Mcp\Server\Session\FileSessionStore;
use Mcp\Server\Transport\StreamableHttpTransport; use Mcp\Server\Transport\StreamableHttpTransport;
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7Server\ServerRequestCreator;
$psr17Factory = new Psr17Factory(); $psr17Factory = new Psr17Factory();
$creator = new ServerRequestCreator($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory); $request = $psr17Factory->createServerRequestFromGlobals();
$request = $creator->fromGlobals();
$server = Server::builder() $server = Server::builder()
->setServerInfo('Combined HTTP Server', '1.0.0') ->setServerInfo('Combined HTTP Server', '1.0.0')
@ -40,7 +37,7 @@ $server = Server::builder()
) )
->build(); ->build();
$transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory); $transport = new StreamableHttpTransport($request);
$response = $server->run($transport); $response = $server->run($transport);

View file

@ -13,17 +13,14 @@
require_once dirname(__DIR__).'/bootstrap.php'; require_once dirname(__DIR__).'/bootstrap.php';
chdir(__DIR__); chdir(__DIR__);
use Http\Discovery\Psr17Factory;
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter; use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
use Mcp\Server; use Mcp\Server;
use Mcp\Server\Session\FileSessionStore; use Mcp\Server\Session\FileSessionStore;
use Mcp\Server\Transport\StreamableHttpTransport; use Mcp\Server\Transport\StreamableHttpTransport;
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7Server\ServerRequestCreator;
$psr17Factory = new Psr17Factory(); $psr17Factory = new Psr17Factory();
$creator = new ServerRequestCreator($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory); $request = $psr17Factory->createServerRequestFromGlobals();
$request = $creator->fromGlobals();
$server = Server::builder() $server = Server::builder()
->setServerInfo('Event Scheduler Server', '1.0.0') ->setServerInfo('Event Scheduler Server', '1.0.0')
@ -33,7 +30,7 @@ $server = Server::builder()
->setDiscovery(__DIR__) ->setDiscovery(__DIR__)
->build(); ->build();
$transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory); $transport = new StreamableHttpTransport($request);
$response = $server->run($transport); $response = $server->run($transport);

View file

@ -13,17 +13,14 @@
require_once dirname(__DIR__).'/bootstrap.php'; require_once dirname(__DIR__).'/bootstrap.php';
chdir(__DIR__); chdir(__DIR__);
use Http\Discovery\Psr17Factory;
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter; use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
use Mcp\Server; use Mcp\Server;
use Mcp\Server\Session\FileSessionStore; use Mcp\Server\Session\FileSessionStore;
use Mcp\Server\Transport\StreamableHttpTransport; use Mcp\Server\Transport\StreamableHttpTransport;
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7Server\ServerRequestCreator;
$psr17Factory = new Psr17Factory(); $psr17Factory = new Psr17Factory();
$creator = new ServerRequestCreator($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory); $request = $psr17Factory->createServerRequestFromGlobals();
$request = $creator->fromGlobals();
$server = Server::builder() $server = Server::builder()
->setServerInfo('HTTP User Profiles', '1.0.0') ->setServerInfo('HTTP User Profiles', '1.0.0')
@ -75,7 +72,7 @@ $server = Server::builder()
) )
->build(); ->build();
$transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory); $transport = new StreamableHttpTransport($request);
$response = $server->run($transport); $response = $server->run($transport);

View file

@ -13,17 +13,14 @@
require_once dirname(__DIR__).'/bootstrap.php'; require_once dirname(__DIR__).'/bootstrap.php';
chdir(__DIR__); chdir(__DIR__);
use Http\Discovery\Psr17Factory;
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter; use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
use Mcp\Server; use Mcp\Server;
use Mcp\Server\Session\FileSessionStore; use Mcp\Server\Session\FileSessionStore;
use Mcp\Server\Transport\StreamableHttpTransport; use Mcp\Server\Transport\StreamableHttpTransport;
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7Server\ServerRequestCreator;
$psr17Factory = new Psr17Factory(); $psr17Factory = new Psr17Factory();
$creator = new ServerRequestCreator($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory); $request = $psr17Factory->createServerRequestFromGlobals();
$request = $creator->fromGlobals();
$server = Server::builder() $server = Server::builder()
->setServerInfo('Schema Showcase', '1.0.0') ->setServerInfo('Schema Showcase', '1.0.0')
@ -33,7 +30,7 @@ $server = Server::builder()
->setDiscovery(__DIR__) ->setDiscovery(__DIR__)
->build(); ->build();
$transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory); $transport = new StreamableHttpTransport($request);
$response = $server->run($transport); $response = $server->run($transport);

View file

@ -11,6 +11,7 @@
namespace Mcp\Server\Transport; namespace Mcp\Server\Transport;
use Http\Discovery\Psr17FactoryDiscovery;
use Mcp\Schema\JsonRpc\Error; use Mcp\Schema\JsonRpc\Error;
use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
@ -27,6 +28,9 @@ use Symfony\Component\Uid\Uuid;
*/ */
class StreamableHttpTransport implements TransportInterface class StreamableHttpTransport implements TransportInterface
{ {
private ResponseFactoryInterface $responseFactory;
private StreamFactoryInterface $streamFactory;
/** @var callable(string, ?Uuid): void */ /** @var callable(string, ?Uuid): void */
private $messageListener; private $messageListener;
@ -49,12 +53,15 @@ class StreamableHttpTransport implements TransportInterface
public function __construct( public function __construct(
private readonly ServerRequestInterface $request, private readonly ServerRequestInterface $request,
private readonly ResponseFactoryInterface $responseFactory, ?ResponseFactoryInterface $responseFactory = null,
private readonly StreamFactoryInterface $streamFactory, ?StreamFactoryInterface $streamFactory = null,
private readonly LoggerInterface $logger = new NullLogger(), private readonly LoggerInterface $logger = new NullLogger(),
) { ) {
$sessionIdString = $this->request->getHeaderLine('Mcp-Session-Id'); $sessionIdString = $this->request->getHeaderLine('Mcp-Session-Id');
$this->sessionId = $sessionIdString ? Uuid::fromString($sessionIdString) : null; $this->sessionId = $sessionIdString ? Uuid::fromString($sessionIdString) : null;
$this->responseFactory = $responseFactory ?? Psr17FactoryDiscovery::findResponseFactory();
$this->streamFactory = $streamFactory ?? Psr17FactoryDiscovery::findStreamFactory();
} }
public function initialize(): void public function initialize(): void