Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -64,6 +65,9 @@
}
},
"config": {
"sort-packages": true
"sort-packages": true,
"allow-plugins": {
"php-http/discovery": false
}
}
}
88 changes: 47 additions & 41 deletions docs/transports.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -126,27 +149,25 @@ 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')
->setDiscovery(__DIR__, ['.'])
->setSession(new FileSessionStore(__DIR__ . '/sessions')) // HTTP needs persistent sessions
->build();

$transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory);
$transport = new StreamableHttpTransport($request);

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

Expand Down Expand Up @@ -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);
}
Expand All @@ -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);
Expand All @@ -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;

Expand All @@ -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);
});
```
Expand Down Expand Up @@ -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).
9 changes: 3 additions & 6 deletions examples/http-combined-registration/server.php
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -40,7 +37,7 @@
)
->build();

$transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory);
$transport = new StreamableHttpTransport($request);

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

Expand Down
9 changes: 3 additions & 6 deletions examples/http-complex-tool-schema/server.php
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -33,7 +30,7 @@
->setDiscovery(__DIR__, ['.'])
->build();

$transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory);
$transport = new StreamableHttpTransport($request);

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

Expand Down
9 changes: 3 additions & 6 deletions examples/http-discovery-userprofile/server.php
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -75,7 +72,7 @@ function (): array {
)
->build();

$transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory);
$transport = new StreamableHttpTransport($request);

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

Expand Down
9 changes: 3 additions & 6 deletions examples/http-schema-showcase/server.php
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -33,7 +30,7 @@
->setDiscovery(__DIR__, ['.'])
->build();

$transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory);
$transport = new StreamableHttpTransport($request);

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

Expand Down
11 changes: 9 additions & 2 deletions src/Server/Transport/StreamableHttpTransport.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -27,6 +28,9 @@
*/
class StreamableHttpTransport implements TransportInterface
{
private ResponseFactoryInterface $responseFactory;
private StreamFactoryInterface $streamFactory;

/** @var callable(string, ?Uuid): void */
private $messageListener;

Expand All @@ -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
Expand Down