- Logs default to warnings and errors (MCP_LOG_LEVEL): the library logged every tool on every request (~48 lines per call). - DNS rebinding protection on /mcp, as the MCP spec asks of HTTP servers: the Host header must be in MCP_ALLOWED_HOSTS (default: localhost; compose default mcp.taskstream.pireon.dev,localhost) and a browser Origin, when present, too; otherwise 403 with a JSON-RPC error. Uses the official validators from @modelcontextprotocol/server. - Test for allowed/foreign Host and Origin; .env.example, compose and docs/09-mcp.md document the variables; Word/PDF regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
47 lines
1.7 KiB
TypeScript
47 lines
1.7 KiB
TypeScript
import { Injectable, NestMiddleware } from '@nestjs/common';
|
|
import { ConfigService } from '@nestjs/config';
|
|
import {
|
|
localhostAllowedHostnames,
|
|
validateHostHeader,
|
|
validateOriginHeader,
|
|
} from '@modelcontextprotocol/server';
|
|
import type { NextFunction, Request, Response } from 'express';
|
|
|
|
/**
|
|
* Protection against DNS rebinding on /mcp (what the MCP spec asks of HTTP
|
|
* servers): the Host header must be one of MCP_ALLOWED_HOSTS (default: only
|
|
* localhost), and a browser's Origin, when sent, too. AI clients calling from
|
|
* a server send no Origin, so they only need the right domain.
|
|
*/
|
|
@Injectable()
|
|
export class McpHostsMiddleware implements NestMiddleware {
|
|
private readonly hosts: string[];
|
|
|
|
constructor(config: ConfigService) {
|
|
const configured = (config.get<string>('MCP_ALLOWED_HOSTS') ?? '')
|
|
.split(',')
|
|
.map((host) => host.trim().toLowerCase())
|
|
.filter(Boolean);
|
|
this.hosts = configured.length ? configured : localhostAllowedHostnames();
|
|
}
|
|
|
|
use(req: Request, res: Response, next: NextFunction): void {
|
|
const error = this.rejection(req);
|
|
if (!error) return next();
|
|
res.status(403).json({
|
|
jsonrpc: '2.0',
|
|
id: null,
|
|
error: { code: -32000, message: error },
|
|
});
|
|
}
|
|
|
|
/** Why the request is refused, or null when its Host (and Origin, if any) are allowed. */
|
|
private rejection(req: Request): string | null {
|
|
const host = validateHostHeader(req.headers.host, this.hosts);
|
|
if ('message' in host) return host.message;
|
|
if (!req.headers.origin) return null;
|
|
const origin = validateOriginHeader(req.headers.origin, this.hosts);
|
|
return 'message' in origin ? origin.message : null;
|
|
}
|
|
}
|