diff --git a/docs/transports.md b/docs/transports.md index 83129dd..290fd49 100644 --- a/docs/transports.md +++ b/docs/transports.md @@ -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 diff --git a/examples/http-client-communication/server.php b/examples/http-client-communication/server.php index dd38cfd..2acce33 100644 --- a/examples/http-client-communication/server.php +++ b/examples/http-client-communication/server.php @@ -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\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 @@ function (string $serviceName, ClientGateway $client): array { ) ->build(); -$transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory, logger()); +$transport = new StreamableHttpTransport($request, logger: $logger); $response = $server->run($transport); diff --git a/src/Server/Transport/StreamableHttpTransport.php b/src/Server/Transport/StreamableHttpTransport.php index 8ff918b..297cad1 100644 --- a/src/Server/Transport/StreamableHttpTransport.php +++ b/src/Server/Transport/StreamableHttpTransport.php @@ -35,16 +35,16 @@ class StreamableHttpTransport extends BaseTransport implements TransportInterfac private ?int $immediateStatusCode = null; /** @var array */ - 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 $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 @@ public function __construct( $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