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;
15:
16: use Nexus\Mcp\Server\Tool\ToolStoreInterface;
17: use Nexus\Mcp\Server\Transport\Http\Middleware\CorsMiddleware;
18: use Nexus\Mcp\Server\Transport\Http\Middleware\DnsRebindingProtectionMiddleware;
19: use Nexus\Mcp\Server\Transport\Http\Middleware\ParameterHeaderValidationMiddleware;
20: use Nexus\Mcp\Server\Transport\Http\Middleware\RequestBodySizeLimitMiddleware;
21: use Psr\Http\Message\ResponseFactoryInterface;
22: use Psr\Http\Message\ResponseInterface;
23: use Psr\Http\Message\ServerRequestInterface;
24: use Psr\Http\Message\StreamFactoryInterface;
25: use Psr\Http\Server\MiddlewareInterface;
26: use Psr\Http\Server\RequestHandlerInterface;
27: use Psr\Log\LoggerInterface;
28: use Psr\Log\NullLogger;
29:
30: /**
31: * Wraps an inner handler (typically the Streamable HTTP transport) with the recommended security middleware,
32: * ordered CORS, then DNS-rebinding protection, then `Mcp-Param-{Name}` validation, then the optional
33: * body-size cap.
34: *
35: * Origin allow-listing is required. `Host` allow-listing, bearer authentication, parameter-header validation,
36: * and the body-size cap apply only when configured. A server whose tools declare `x-mcp-header` must pass its
37: * tool store so the spec-required header-to-body validation runs.
38: */
39: final readonly class SecuredHttpEndpoint implements RequestHandlerInterface
40: {
41: private MiddlewarePipeline $pipeline;
42:
43: /**
44: * @param list<non-empty-string> $allowedOrigins Origins permitted to reach the endpoint, or `['*']` to allow any
45: * @param list<non-empty-string> $allowedHosts Hosts permitted to reach the endpoint (empty disables `Host` validation), or `['*']` to allow any
46: * @param null|int $maxBodyBytes Request body byte cap, or `null` to leave the body uncapped
47: * @param null|ToolStoreInterface $toolStore The served tool store, enabling `Mcp-Param-{Name}` validation
48: * @param null|MiddlewareInterface $authentication Bearer token enforcement, making the endpoint an OAuth resource server
49: */
50: public function __construct(
51: RequestHandlerInterface $handler,
52: array $allowedOrigins,
53: ResponseFactoryInterface $responseFactory,
54: StreamFactoryInterface $streamFactory,
55: array $allowedHosts = [],
56: ?int $maxBodyBytes = null,
57: ?ToolStoreInterface $toolStore = null,
58: LoggerInterface $logger = new NullLogger(),
59: ?MiddlewareInterface $authentication = null,
60: ) {
61: $middleware = [
62: new CorsMiddleware($allowedOrigins, $responseFactory),
63: new DnsRebindingProtectionMiddleware($allowedOrigins, $allowedHosts, $responseFactory, $streamFactory),
64: ];
65:
66: // Authentication runs before anything reads the body, so an unauthorized request is turned away
67: // without it being parsed.
68: if (null !== $authentication) {
69: $middleware[] = $authentication;
70: }
71:
72: if (null !== $toolStore) {
73: $middleware[] = new ParameterHeaderValidationMiddleware($toolStore, $responseFactory, $streamFactory, $logger);
74: }
75:
76: if (null !== $maxBodyBytes) {
77: $middleware[] = new RequestBodySizeLimitMiddleware($maxBodyBytes, $responseFactory, $streamFactory);
78: }
79:
80: $this->pipeline = new MiddlewarePipeline($handler, ...$middleware);
81: }
82:
83: #[\Override]
84: public function handle(ServerRequestInterface $request): ResponseInterface
85: {
86: return $this->pipeline->handle($request);
87: }
88: }
89: