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: */
33: final readonly class SecuredHttpEndpoint implements RequestHandlerInterface
34: {
35: public const int DEFAULT_MAX_BODY_BYTES = 1_048_576;
36:
37: private MiddlewarePipeline $pipeline;
38:
39: /**
40: * @param list<non-empty-string> $allowedOrigins Origins permitted to reach the endpoint, or `['*']` to allow any
41: * @param list<non-empty-string> $allowedHosts Hosts permitted to reach the endpoint (empty disables `Host` validation), or `['*']` to allow any
42: * @param null|int<0, max> $maxBodyBytes Request body bytes past which the request is refused, or `null` for no cap
43: * @param null|ToolStoreInterface $toolStore The served tool store, enabling `Mcp-Param-{Name}` validation
44: * @param null|MiddlewareInterface $authentication Bearer token enforcement, making the endpoint an OAuth resource server
45: */
46: public function __construct(
47: RequestHandlerInterface $handler,
48: array $allowedOrigins,
49: ResponseFactoryInterface $responseFactory,
50: StreamFactoryInterface $streamFactory,
51: array $allowedHosts = [],
52: ?int $maxBodyBytes = self::DEFAULT_MAX_BODY_BYTES,
53: ?ToolStoreInterface $toolStore = null,
54: LoggerInterface $logger = new NullLogger(),
55: ?MiddlewareInterface $authentication = null,
56: ) {
57: $middleware = [
58: new CorsMiddleware($allowedOrigins, $responseFactory),
59: new DnsRebindingProtectionMiddleware($allowedOrigins, $allowedHosts, $responseFactory, $streamFactory),
60: ];
61:
62: if (null !== $authentication) {
63: $middleware[] = $authentication;
64: }
65:
66: if (null !== $maxBodyBytes) {
67: $middleware[] = new RequestBodySizeLimitMiddleware($maxBodyBytes, $responseFactory, $streamFactory);
68: }
69:
70: if (null !== $toolStore) {
71: $middleware[] = new ParameterHeaderValidationMiddleware($toolStore, $responseFactory, $streamFactory, $logger);
72: }
73:
74: $this->pipeline = new MiddlewarePipeline($handler, ...$middleware);
75: }
76:
77: #[\Override]
78: public function handle(ServerRequestInterface $request): ResponseInterface
79: {
80: return $this->pipeline->handle($request);
81: }
82: }
83: