1: <?php
2:
3: declare(strict_types=1);
4:
5: /**
6: * This file is part of the Nexus MCP SDK package.
7: *
8: * (c) 2026 John Paul E. Balandan, CPA <paulbalandan@gmail.com>
9: *
10: * For the full copyright and license information, please view
11: * the LICENSE file that was distributed with this source code.
12: */
13:
14: namespace Nexus\Mcp\Server\Transport\Http\Middleware;
15:
16: use Nexus\Mcp\Core\Http\HttpStatus;
17: use Psr\Http\Message\ResponseFactoryInterface;
18: use Psr\Http\Message\ResponseInterface;
19: use Psr\Http\Message\ServerRequestInterface;
20: use Psr\Http\Server\MiddlewareInterface;
21: use Psr\Http\Server\RequestHandlerInterface;
22:
23: /**
24: * Grants browser clients cross-origin access to the MCP endpoint additively, leaving rejection to the
25: * DNS-rebinding gate.
26: */
27: final readonly class CorsMiddleware implements MiddlewareInterface
28: {
29: /**
30: * @param list<non-empty-string> $allowedOrigins Origins granted cross-origin access, or `['*']` to allow any
31: * @param int $maxAge Seconds a browser may cache the preflight result
32: */
33: public function __construct(
34: private array $allowedOrigins,
35: private ResponseFactoryInterface $responseFactory,
36: private int $maxAge = 600,
37: ) {
38: }
39:
40: #[\Override]
41: public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
42: {
43: if ($this->isPreflight($request)) {
44: return $this->answerPreflight($request);
45: }
46:
47: return $this->decorate($request, $handler->handle($request));
48: }
49:
50: private function answerPreflight(ServerRequestInterface $request): ResponseInterface
51: {
52: $response = $this->responseFactory->createResponse(HttpStatus::NoContent->value)
53: ->withAddedHeader('Vary', 'Origin')
54: ->withAddedHeader('Vary', 'Access-Control-Request-Headers')
55: ;
56:
57: if (! $this->isAllowedOrigin($request)) {
58: return $response;
59: }
60:
61: $response = $response
62: ->withHeader('Access-Control-Allow-Origin', $request->getHeaderLine('Origin'))
63: ->withHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')
64: ->withHeader('Access-Control-Max-Age', (string) $this->maxAge)
65: ;
66:
67: $requestedHeaders = $request->getHeaderLine('Access-Control-Request-Headers');
68:
69: return '' === $requestedHeaders
70: ? $response
71: : $response->withHeader('Access-Control-Allow-Headers', $requestedHeaders);
72: }
73:
74: private function decorate(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
75: {
76: $response = $response->withAddedHeader('Vary', 'Origin');
77:
78: if (! $this->isAllowedOrigin($request)) {
79: return $response;
80: }
81:
82: return $response->withHeader('Access-Control-Allow-Origin', $request->getHeaderLine('Origin'));
83: }
84:
85: private function isAllowedOrigin(ServerRequestInterface $request): bool
86: {
87: if (! $request->hasHeader('Origin')) {
88: return false;
89: }
90:
91: $origin = $request->getHeaderLine('Origin');
92:
93: return \in_array('*', $this->allowedOrigins, true) || \in_array($origin, $this->allowedOrigins, true);
94: }
95:
96: private function isPreflight(ServerRequestInterface $request): bool
97: {
98: return $request->getMethod() === 'OPTIONS'
99: && $request->hasHeader('Access-Control-Request-Method');
100: }
101: }
102: