[Server] Make CORS headers configurable in StreamableHttpTransport (#118)

* feat: make CORS headers configurable in StreamableHttpTransport

* docs: add CORS configuration documentation for HTTP transport

* refactor: streamline request creation and transport initialization in HTTP client example
This commit is contained in:
Kyrian Obikwelu 2025-10-26 23:58:26 +01:00 • committed by GitHub
commit 48a19b93a7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 59 additions and 12 deletions

View file

@ -110,6 +110,7 @@ $transport = new StreamableHttpTransport(
- **`request`** (required): `ServerRequestInterface` - The incoming PSR-7 HTTP request
- **`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.
- **`corsHeaders`** (optional): `array` - Custom CORS headers to override defaults. Merges with secure defaults. Defaults to `[]`.
- **`logger`** (optional): `LoggerInterface` - PSR-3 logger for debugging. Defaults to `NullLogger`.
### PSR-17 Auto-Discovery
@ -136,6 +137,48 @@ $psr17Factory = new Psr17Factory();
$transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory);
```
### CORS Configuration
The transport sets secure CORS defaults that can be customized or disabled:
```php
// Default CORS headers (backward compatible)
$transport = new StreamableHttpTransport($request, $responseFactory, $streamFactory);
// Restrict to specific origin
$transport = new StreamableHttpTransport(
$request,
$responseFactory,
$streamFactory,
['Access-Control-Allow-Origin' => 'https://myapp.com']
);
// Disable CORS for proxy scenarios
$transport = new StreamableHttpTransport(
$request,
$responseFactory,
$streamFactory,
['Access-Control-Allow-Origin' => '']
);
// Custom headers with logger
$transport = new StreamableHttpTransport(
$request,
$responseFactory,
$streamFactory,
[
'Access-Control-Allow-Origin' => 'https://api.example.com',
'Access-Control-Max-Age' => '86400'
],
$logger
);
```
Default CORS headers:
- `Access-Control-Allow-Origin: *`
- `Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS`
- `Access-Control-Allow-Headers: Content-Type, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID, Authorization, Accept`
### Architecture
The HTTP transport doesn't run its own web server. Instead, it processes PSR-7 requests and returns PSR-7 responses that

View file

@ -12,6 +12,7 @@
require_once dirname(__DIR__).'/bootstrap.php';
chdir(__DIR__);
use Http\Discovery\Psr17Factory;
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
use Mcp\Schema\Content\TextContent;
use Mcp\Schema\Enum\LoggingLevel;
@ -21,19 +22,16 @@ use Mcp\Server;
use Mcp\Server\ClientGateway;
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 = (new Psr17Factory())->createServerRequestFromGlobals();
$sessionDir = __DIR__.'/sessions';
$capabilities = new ServerCapabilities(logging: true, tools: true);
$logger = logger();
$server = Server::builder()
->setServerInfo('HTTP Client Communication Demo', '1.0.0')
->setLogger(logger())
->setLogger($logger)
->setContainer(container())
->setSession(new FileSessionStore($sessionDir))
->setCapabilities($capabilities)
@ -117,7 +115,7 @@ $server = Server::builder()
)
->build();
$transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory, logger());
$transport = new StreamableHttpTransport($request, logger: $logger);
$response = $server->run($transport);

View file

@ -35,16 +35,16 @@ class StreamableHttpTransport extends BaseTransport implements TransportInterfac
private ?int $immediateStatusCode = null;
/** @var array<string, string> */
private array $corsHeaders = [
'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Methods' => 'GET, POST, DELETE, OPTIONS',
'Access-Control-Allow-Headers' => 'Content-Type, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID, Authorization, Accept',
];
private array $corsHeaders;
/**
* @param array<string, string> $corsHeaders
*/
public function __construct(
private readonly ServerRequestInterface $request,
?ResponseFactoryInterface $responseFactory = null,
?StreamFactoryInterface $streamFactory = null,
array $corsHeaders = [],
LoggerInterface $logger = new NullLogger(),
) {
parent::__construct($logger);
@ -53,6 +53,12 @@ class StreamableHttpTransport extends BaseTransport implements TransportInterfac
$this->responseFactory = $responseFactory ?? Psr17FactoryDiscovery::findResponseFactory();
$this->streamFactory = $streamFactory ?? Psr17FactoryDiscovery::findStreamFactory();
$this->corsHeaders = array_merge([
'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Methods' => 'GET, POST, DELETE, OPTIONS',
'Access-Control-Allow-Headers' => 'Content-Type, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID, Authorization, Accept',
], $corsHeaders);
}
public function initialize(): void