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\Core\Exception\LogicException;
17: use Nexus\Mcp\Server\Tool\ToolStoreInterface;
18: use Nexus\Mcp\Server\Transport\Http\Middleware\CorsMiddleware;
19: use Nexus\Mcp\Server\Transport\Http\Middleware\DnsRebindingProtectionMiddleware;
20: use Nexus\Mcp\Server\Transport\Http\Middleware\ParameterHeaderValidationMiddleware;
21: use Nexus\Mcp\Server\Transport\Http\Middleware\RequestBodySizeLimitMiddleware;
22: use Psr\Http\Message\ResponseFactoryInterface;
23: use Psr\Http\Message\ResponseInterface;
24: use Psr\Http\Message\ServerRequestInterface;
25: use Psr\Http\Message\StreamFactoryInterface;
26: use Psr\Http\Server\MiddlewareInterface;
27: use Psr\Http\Server\RequestHandlerInterface;
28: use Psr\Log\LoggerInterface;
29: use Psr\Log\NullLogger;
30:
31: /**
32: * Wraps an inner handler (typically the Streamable HTTP transport) with the recommended security middleware.
33: */
34: final readonly class SecuredHttpEndpoint implements RequestHandlerInterface
35: {
36: public const int DEFAULT_MAX_BODY_BYTES = 1_048_576;
37:
38: private MiddlewarePipeline $pipeline;
39:
40: /**
41: * @param list<non-empty-string> $allowedOrigins Origins permitted to reach the endpoint, or `['*']` to allow any
42: * @param list<non-empty-string> $allowedHosts Hosts permitted to reach the endpoint (empty disables `Host` validation), or `['*']` to allow any
43: * @param null|int<0, max> $maxBodyBytes Request body bytes past which the request is refused, or `null` for no cap
44: * @param null|ToolStoreInterface $toolStore The served tool store, enabling `Mcp-Param-{Name}` validation
45: * @param null|MiddlewareInterface $authentication Bearer token enforcement, making the endpoint an OAuth resource server
46: * @param null|MiddlewareInterface $operationScopes Per-operation scope enforcement, run once the body is within its cap
47: */
48: public function __construct(
49: RequestHandlerInterface $handler,
50: array $allowedOrigins,
51: ResponseFactoryInterface $responseFactory,
52: StreamFactoryInterface $streamFactory,
53: array $allowedHosts = [],
54: ?int $maxBodyBytes = self::DEFAULT_MAX_BODY_BYTES,
55: ?ToolStoreInterface $toolStore = null,
56: LoggerInterface $logger = new NullLogger(),
57: ?MiddlewareInterface $authentication = null,
58: ?MiddlewareInterface $operationScopes = null,
59: ) {
60: $middleware = [
61: new CorsMiddleware($allowedOrigins, $responseFactory),
62: new DnsRebindingProtectionMiddleware($allowedOrigins, $allowedHosts, $responseFactory, $streamFactory),
63: ];
64:
65: if (null !== $authentication) {
66: $middleware[] = $authentication;
67: }
68:
69: if (null !== $maxBodyBytes) {
70: $middleware[] = new RequestBodySizeLimitMiddleware($maxBodyBytes, $responseFactory, $streamFactory);
71: }
72:
73: if (null !== $operationScopes) {
74: if (null === $authentication) {
75: throw new LogicException('SecuredHttpEndpoint was given "operationScopes" with no "authentication" to validate the token they are checked against.');
76: }
77:
78: $middleware[] = $operationScopes;
79: }
80:
81: if (null !== $toolStore) {
82: $middleware[] = new ParameterHeaderValidationMiddleware($toolStore, $responseFactory, $streamFactory, $logger);
83: }
84:
85: $this->pipeline = new MiddlewarePipeline($handler, ...$middleware);
86: }
87:
88: #[\Override]
89: public function handle(ServerRequestInterface $request): ResponseInterface
90: {
91: return $this->pipeline->handle($request);
92: }
93: }
94: