Skip to content

Commit 72360c3

Browse files
feat: add PSR-17 factory auto-discovery to HTTP transport
1 parent e4a82f7 commit 72360c3

File tree

7 files changed

+73
-68
lines changed

7 files changed

+73
-68
lines changed

composer.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"php": "^8.1",
2222
"ext-fileinfo": "*",
2323
"opis/json-schema": "^2.4",
24+
"php-http/discovery": "^1.20",
2425
"phpdocumentor/reflection-docblock": "^5.6",
2526
"psr/clock": "^1.0",
2627
"psr/container": "^2.0",
@@ -64,6 +65,9 @@
6465
}
6566
},
6667
"config": {
67-
"sort-packages": true
68+
"sort-packages": true,
69+
"allow-plugins": {
70+
"php-http/discovery": false
71+
}
6872
}
6973
}

docs/transports.md

Lines changed: 47 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -95,24 +95,47 @@ and process requests and send responses. It provides a flexible architecture tha
9595

9696
```php
9797
use Psr\Http\Message\ServerRequestInterface;
98-
use Psr\Http\Message\ResponseFactoryInterface;
99-
use Psr\Http\Message\StreamFactoryInterface;
10098

99+
// PSR-17 factories are automatically discovered
101100
$transport = new StreamableHttpTransport(
102-
request: $serverRequest, // PSR-7 server request
103-
responseFactory: $responseFactory, // PSR-17 response factory
104-
streamFactory: $streamFactory, // PSR-17 stream factory
105-
logger: $logger // Optional PSR-3 logger
101+
request: $serverRequest, // PSR-7 server request
102+
responseFactory: null, // Optional: PSR-17 response factory (auto-discovered if null)
103+
streamFactory: null, // Optional: PSR-17 stream factory (auto-discovered if null)
104+
logger: $logger // Optional PSR-3 logger
106105
);
107106
```
108107

109108
### Parameters
110109

111110
- **`request`** (required): `ServerRequestInterface` - The incoming PSR-7 HTTP request
112-
- **`responseFactory`** (required): `ResponseFactoryInterface` - PSR-17 factory for creating HTTP responses
113-
- **`streamFactory`** (required): `StreamFactoryInterface` - PSR-17 factory for creating response body streams
111+
- **`responseFactory`** (optional): `ResponseFactoryInterface` - PSR-17 factory for creating HTTP responses. Auto-discovered if not provided.
112+
- **`streamFactory`** (optional): `StreamFactoryInterface` - PSR-17 factory for creating response body streams. Auto-discovered if not provided.
114113
- **`logger`** (optional): `LoggerInterface` - PSR-3 logger for debugging. Defaults to `NullLogger`.
115114

115+
### PSR-17 Auto-Discovery
116+
117+
The transport automatically discovers PSR-17 factory implementations from these popular packages:
118+
119+
- `nyholm/psr7`
120+
- `guzzlehttp/psr7`
121+
- `slim/psr7`
122+
- `laminas/laminas-diactoros`
123+
- And other PSR-17 compatible implementations
124+
125+
```bash
126+
# Install any PSR-17 package - discovery works automatically
127+
composer require nyholm/psr7
128+
```
129+
130+
If auto-discovery fails or you want to use a specific implementation, you can pass factories explicitly:
131+
132+
```php
133+
use Nyholm\Psr7\Factory\Psr17Factory;
134+
135+
$psr17Factory = new Psr17Factory();
136+
$transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory);
137+
```
138+
116139
### Architecture
117140

118141
The HTTP transport doesn't run its own web server. Instead, it processes PSR-7 requests and returns PSR-7 responses that
@@ -126,27 +149,25 @@ This design allows integration with any PHP framework or application that suppor
126149

127150
### Basic Usage (Standalone)
128151

129-
Here's an opinionated example using Nyholm PSR-7 and Laminas emitter:
152+
Here's a simplified example using PSR-17 discovery and Laminas emitter:
130153

131154
```php
155+
use Http\Discovery\Psr17Factory;
132156
use Mcp\Server;
133157
use Mcp\Server\Transport\StreamableHttpTransport;
134158
use Mcp\Server\Session\FileSessionStore;
135-
use Nyholm\Psr7\Factory\Psr17Factory;
136-
use Nyholm\Psr7Server\ServerRequestCreator;
137159
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
138160

139161
$psr17Factory = new Psr17Factory();
140-
$creator = new ServerRequestCreator($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
141-
$request = $creator->fromGlobals();
162+
$request = $psr17Factory->createServerRequestFromGlobals();
142163

143164
$server = Server::builder()
144165
->setServerInfo('HTTP Server', '1.0.0')
145166
->setDiscovery(__DIR__, ['.'])
146167
->setSession(new FileSessionStore(__DIR__ . '/sessions')) // HTTP needs persistent sessions
147168
->build();
148169

149-
$transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory);
170+
$transport = new StreamableHttpTransport($request);
150171

151172
$response = $server->run($transport);
152173

@@ -174,27 +195,23 @@ use Symfony\Component\HttpFoundation\Response;
174195
use Symfony\Component\Routing\Attribute\Route;
175196
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
176197
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
177-
use Nyholm\Psr7\Factory\Psr17Factory;
178198
use Mcp\Server;
179199
use Mcp\Server\Transport\StreamableHttpTransport;
180200

181201
class McpController
182202
{
183-
#[Route('/mcp', name: 'mcp_endpoint']
203+
#[Route('/mcp', name: 'mcp_endpoint')]
184204
public function handle(Request $request, Server $server): Response
185205
{
186-
// Create PSR-7 factories
187-
$psr17Factory = new Psr17Factory();
188-
$psrHttpFactory = new PsrHttpFactory($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
206+
// Convert Symfony request to PSR-7 (PSR-17 factories auto-discovered)
207+
$psrHttpFactory = new PsrHttpFactory();
189208
$httpFoundationFactory = new HttpFoundationFactory();
190-
191-
// Convert Symfony request to PSR-7
192209
$psrRequest = $psrHttpFactory->createRequest($request);
193-
194-
// Process with MCP
195-
$transport = new StreamableHttpTransport($psrRequest, $psr17Factory, $psr17Factory);
210+
211+
// Process with MCP (factories auto-discovered)
212+
$transport = new StreamableHttpTransport($psrRequest);
196213
$psrResponse = $server->run($transport);
197-
214+
198215
// Convert PSR-7 response back to Symfony
199216
return $httpFoundationFactory->createResponse($psrResponse);
200217
}
@@ -219,17 +236,14 @@ use Psr\Http\Message\ServerRequestInterface;
219236
use Psr\Http\Message\ResponseInterface;
220237
use Mcp\Server;
221238
use Mcp\Server\Transport\StreamableHttpTransport;
222-
use Nyholm\Psr7\Factory\Psr17Factory;
223239

224240
class McpController
225241
{
226242
public function handle(ServerRequestInterface $request, Server $server): ResponseInterface
227243
{
228-
$psr17Factory = new Psr17Factory();
229-
230244
// Create the MCP HTTP transport
231-
$transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory);
232-
245+
$transport = new StreamableHttpTransport($request);
246+
233247
// Process MCP request and return PSR-7 response
234248
// Laravel automatically handles PSR-7 responses
235249
return $server->run($transport);
@@ -248,8 +262,6 @@ Create a route handler using Slim's built-in factories and container:
248262

249263
```php
250264
use Slim\Factory\AppFactory;
251-
use Slim\Psr7\Factory\ResponseFactory;
252-
use Slim\Psr7\Factory\StreamFactory;
253265
use Mcp\Server;
254266
use Mcp\Server\Transport\StreamableHttpTransport;
255267

@@ -260,12 +272,9 @@ $app->any('/mcp', function ($request, $response) {
260272
->setServerInfo('My MCP Server', '1.0.0')
261273
->setDiscovery(__DIR__, ['.'])
262274
->build();
263-
264-
$responseFactory = new ResponseFactory();
265-
$streamFactory = new StreamFactory();
266-
267-
$transport = new StreamableHttpTransport($request, $responseFactory, $streamFactory);
268-
275+
276+
$transport = new StreamableHttpTransport($request);
277+
269278
return $server->run($transport);
270279
});
271280
```
@@ -330,6 +339,3 @@ npx @modelcontextprotocol/inspector http://localhost:8000
330339
The choice between STDIO and HTTP transport depends on the client you want to integrate with.
331340
If you are integrating with a client that is running **locally** (like Claude Desktop), use STDIO.
332341
If you are building a server in a distributed environment and need to integrate with a **remote** client, use Streamable HTTP.
333-
334-
One additiona difference to consider is that STDIO is process-based (one session per process) while HTTP is
335-
request-based (multiple sessions via headers).

examples/http-combined-registration/server.php

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,15 @@
1313
require_once dirname(__DIR__).'/bootstrap.php';
1414
chdir(__DIR__);
1515

16+
use Http\Discovery\Psr17Factory;
1617
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
1718
use Mcp\Example\HttpCombinedRegistration\ManualHandlers;
1819
use Mcp\Server;
1920
use Mcp\Server\Session\FileSessionStore;
2021
use Mcp\Server\Transport\StreamableHttpTransport;
21-
use Nyholm\Psr7\Factory\Psr17Factory;
22-
use Nyholm\Psr7Server\ServerRequestCreator;
2322

2423
$psr17Factory = new Psr17Factory();
25-
$creator = new ServerRequestCreator($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
26-
27-
$request = $creator->fromGlobals();
24+
$request = $psr17Factory->createServerRequestFromGlobals();
2825

2926
$server = Server::builder()
3027
->setServerInfo('Combined HTTP Server', '1.0.0')
@@ -40,7 +37,7 @@
4037
)
4138
->build();
4239

43-
$transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory);
40+
$transport = new StreamableHttpTransport($request);
4441

4542
$response = $server->run($transport);
4643

examples/http-complex-tool-schema/server.php

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,14 @@
1313
require_once dirname(__DIR__).'/bootstrap.php';
1414
chdir(__DIR__);
1515

16+
use Http\Discovery\Psr17Factory;
1617
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
1718
use Mcp\Server;
1819
use Mcp\Server\Session\FileSessionStore;
1920
use Mcp\Server\Transport\StreamableHttpTransport;
20-
use Nyholm\Psr7\Factory\Psr17Factory;
21-
use Nyholm\Psr7Server\ServerRequestCreator;
2221

2322
$psr17Factory = new Psr17Factory();
24-
$creator = new ServerRequestCreator($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
25-
26-
$request = $creator->fromGlobals();
23+
$request = $psr17Factory->createServerRequestFromGlobals();
2724

2825
$server = Server::builder()
2926
->setServerInfo('Event Scheduler Server', '1.0.0')
@@ -33,7 +30,7 @@
3330
->setDiscovery(__DIR__, ['.'])
3431
->build();
3532

36-
$transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory);
33+
$transport = new StreamableHttpTransport($request);
3734

3835
$response = $server->run($transport);
3936

examples/http-discovery-userprofile/server.php

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,14 @@
1313
require_once dirname(__DIR__).'/bootstrap.php';
1414
chdir(__DIR__);
1515

16+
use Http\Discovery\Psr17Factory;
1617
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
1718
use Mcp\Server;
1819
use Mcp\Server\Session\FileSessionStore;
1920
use Mcp\Server\Transport\StreamableHttpTransport;
20-
use Nyholm\Psr7\Factory\Psr17Factory;
21-
use Nyholm\Psr7Server\ServerRequestCreator;
2221

2322
$psr17Factory = new Psr17Factory();
24-
$creator = new ServerRequestCreator($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
25-
26-
$request = $creator->fromGlobals();
23+
$request = $psr17Factory->createServerRequestFromGlobals();
2724

2825
$server = Server::builder()
2926
->setServerInfo('HTTP User Profiles', '1.0.0')
@@ -75,7 +72,7 @@ function (): array {
7572
)
7673
->build();
7774

78-
$transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory);
75+
$transport = new StreamableHttpTransport($request);
7976

8077
$response = $server->run($transport);
8178

examples/http-schema-showcase/server.php

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,14 @@
1313
require_once dirname(__DIR__).'/bootstrap.php';
1414
chdir(__DIR__);
1515

16+
use Http\Discovery\Psr17Factory;
1617
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
1718
use Mcp\Server;
1819
use Mcp\Server\Session\FileSessionStore;
1920
use Mcp\Server\Transport\StreamableHttpTransport;
20-
use Nyholm\Psr7\Factory\Psr17Factory;
21-
use Nyholm\Psr7Server\ServerRequestCreator;
2221

2322
$psr17Factory = new Psr17Factory();
24-
$creator = new ServerRequestCreator($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
25-
26-
$request = $creator->fromGlobals();
23+
$request = $psr17Factory->createServerRequestFromGlobals();
2724

2825
$server = Server::builder()
2926
->setServerInfo('Schema Showcase', '1.0.0')
@@ -33,7 +30,7 @@
3330
->setDiscovery(__DIR__, ['.'])
3431
->build();
3532

36-
$transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory);
33+
$transport = new StreamableHttpTransport($request);
3734

3835
$response = $server->run($transport);
3936

src/Server/Transport/StreamableHttpTransport.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Mcp\Server\Transport;
1313

14+
use Http\Discovery\Psr17FactoryDiscovery;
1415
use Mcp\Schema\JsonRpc\Error;
1516
use Psr\Http\Message\ResponseFactoryInterface;
1617
use Psr\Http\Message\ResponseInterface;
@@ -27,6 +28,9 @@
2728
*/
2829
class StreamableHttpTransport implements TransportInterface
2930
{
31+
private ResponseFactoryInterface $responseFactory;
32+
private StreamFactoryInterface $streamFactory;
33+
3034
/** @var callable(string, ?Uuid): void */
3135
private $messageListener;
3236

@@ -49,12 +53,15 @@ class StreamableHttpTransport implements TransportInterface
4953

5054
public function __construct(
5155
private readonly ServerRequestInterface $request,
52-
private readonly ResponseFactoryInterface $responseFactory,
53-
private readonly StreamFactoryInterface $streamFactory,
56+
?ResponseFactoryInterface $responseFactory = null,
57+
?StreamFactoryInterface $streamFactory = null,
5458
private readonly LoggerInterface $logger = new NullLogger(),
5559
) {
5660
$sessionIdString = $this->request->getHeaderLine('Mcp-Session-Id');
5761
$this->sessionId = $sessionIdString ? Uuid::fromString($sessionIdString) : null;
62+
63+
$this->responseFactory = $responseFactory ?? Psr17FactoryDiscovery::findResponseFactory();
64+
$this->streamFactory = $streamFactory ?? Psr17FactoryDiscovery::findStreamFactory();
5865
}
5966

6067
public function initialize(): void

0 commit comments

Comments
 (0)