['GET', false, 401]; yield 'POST (middleware returns 401)' => ['POST', false, 401]; yield 'DELETE (middleware returns 401)' => ['DELETE', false, 401]; yield 'OPTIONS (middleware delegates -> transport handles preflight)' => ['OPTIONS', true, 204]; yield 'GET (middleware delegates -> transport handles preflight)' => ['GET', true, 405]; yield 'POST (middleware delegates -> transport handles preflight)' => ['POST', true, 202]; yield 'DELETE (middleware delegates -> transport handles preflight)' => ['DELETE', true, 400]; } #[DataProvider('corsHeaderProvider')] #[TestDox('CORS headers are always applied')] public function testCorsHeader(string $method, bool $middlewareDelegatesToTransport, int $expectedStatusCode): void { $factory = new Psr17Factory(); $request = $factory->createServerRequest($method, 'https://example.com'); $middleware = new class($factory, $expectedStatusCode, $middlewareDelegatesToTransport) implements MiddlewareInterface { public function __construct( private ResponseFactoryInterface $responseFactory, private int $expectedStatusCode, private bool $middlewareDelegatesToTransport, ) { } public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { if ($this->middlewareDelegatesToTransport) { return $handler->handle($request); } return $this->responseFactory->createResponse($this->expectedStatusCode); } }; $transport = new StreamableHttpTransport( $request, $factory, $factory, [], null, [$middleware], ); $response = $transport->listen(); $this->assertSame($expectedStatusCode, $response->getStatusCode(), $response->getBody()->getContents()); $this->assertTrue($response->hasHeader('Access-Control-Allow-Origin')); $this->assertTrue($response->hasHeader('Access-Control-Allow-Methods')); $this->assertTrue($response->hasHeader('Access-Control-Allow-Headers')); $this->assertTrue($response->hasHeader('Access-Control-Expose-Headers')); $this->assertSame('*', $response->getHeaderLine('Access-Control-Allow-Origin')); $this->assertSame('GET, POST, DELETE, OPTIONS', $response->getHeaderLine('Access-Control-Allow-Methods')); $this->assertSame( 'Accept,Authorization,Content-Type,Last-Event-ID,Mcp-Protocol-Version,Mcp-Session-Id', $response->getHeaderLine('Access-Control-Allow-Headers') ); $this->assertSame('Mcp-Session-Id', $response->getHeaderLine('Access-Control-Expose-Headers')); } #[TestDox('transport replaces existing CORS headers on the response')] public function testCorsHeadersAreReplacedWhenAlreadyPresent(): void { $factory = new Psr17Factory(); $request = $factory->createServerRequest('GET', 'https://example.com'); $middleware = new class($factory) implements MiddlewareInterface { public function __construct(private ResponseFactoryInterface $responses) { } public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { return $this->responses->createResponse(200) ->withHeader('Access-Control-Allow-Origin', 'https://another.com'); } }; $transport = new StreamableHttpTransport( $request, $factory, $factory, [], null, [$middleware], ); $response = $transport->listen(); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('https://another.com', $response->getHeaderLine('Access-Control-Allow-Origin')); $this->assertSame('GET, POST, DELETE, OPTIONS', $response->getHeaderLine('Access-Control-Allow-Methods')); $this->assertSame( 'Accept,Authorization,Content-Type,Last-Event-ID,Mcp-Protocol-Version,Mcp-Session-Id', $response->getHeaderLine('Access-Control-Allow-Headers') ); $this->assertSame('Mcp-Session-Id', $response->getHeaderLine('Access-Control-Expose-Headers')); } #[TestDox('middleware runs before transport handles the request')] public function testMiddlewareRunsBeforeTransportHandlesRequest(): void { $factory = new Psr17Factory(); $request = $factory->createServerRequest('OPTIONS', 'https://example.com'); $state = new \stdClass(); $state->called = false; $middleware = new class($state) implements MiddlewareInterface { public function __construct(private \stdClass $state) { } public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $this->state->called = true; return $handler->handle($request); } }; $transport = new StreamableHttpTransport( $request, $factory, $factory, [], null, [$middleware], ); $response = $transport->listen(); $this->assertTrue($state->called); $this->assertSame(204, $response->getStatusCode()); } }