3.9 KiB
Context
The MCP Python SDK includes DNS rebinding protection via TransportSecuritySettings in mcp.server.transport_security. When enabled, it validates the Host header against an allowlist and returns 421 for unrecognised hosts.
FastMCP auto-enables this protection when host is 127.0.0.1, localhost, or ::1, with a default allowlist of those three values (wildcard port). The kb MCP server does not pass a host to FastMCP() and does not pass transport_security, so the behaviour depends on the SDK's defaults — which have changed between versions and will likely keep changing.
Currently the server calls mcp.streamable_http_app() to get a Starlette sub-app and wraps it in its own Starlette app with BearerAuthMiddleware. The transport_security settings flow through FastMCP() → Settings → StreamableHTTPSessionManager, so they must be set at FastMCP construction time.
When a remote client connects (e.g. Host: 192.168.1.50:3000), the SDK rejects the request with 421 before our auth middleware even runs.
Goals / Non-Goals
Goals:
- Allow operators to configure additional allowed hosts via environment variable so remote clients can connect.
- Support both IP addresses and FQDNs, with or without port.
- Preserve DNS rebinding protection (keep it enabled, just widen the allowlist).
- Maintain backward compatibility — unset variable means localhost-only, same as today.
Non-Goals:
- Disabling DNS rebinding protection entirely.
- Configuring allowed origins separately from allowed hosts (derive origins automatically from hosts).
- TLS termination or HTTPS — that belongs to a reverse proxy in front of the MCP container.
Decisions
1. Environment variable format
KB_MCP_ALLOWED_HOSTS is a comma-separated list of hosts. Each entry is an IP address or FQDN without port and without scheme.
Examples: 192.168.1.50, kb.example.com, 192.168.1.50,kb.example.com,10.0.0.1
Rationale: Comma-separated is the simplest format that doesn't require quoting in Docker Compose YAML or shell environments. Ports are omitted because the wildcard-port pattern (host:*) covers all ports — operators shouldn't need to know the internal port.
Alternative considered: JSON array — rejected, awkward in env vars and Compose files.
2. Merge with localhost defaults
The parsed hosts are merged with the hardcoded localhost set (127.0.0.1, localhost, [::1]). Localhost is always allowed regardless of the env var value.
Rationale: Removing localhost would break local development and health checks. There's no reason to ever disallow it.
3. Auto-derive allowed origins from allowed hosts
For each allowed host, generate http://<host>:* as an allowed origin. No separate env var for origins.
Rationale: The MCP server doesn't serve HTTPS (TLS is terminated by a reverse proxy), so http:// is always correct at the container level. If HTTPS origins are needed in future, a separate env var can be added then.
4. Pass TransportSecuritySettings explicitly to FastMCP
Always construct a TransportSecuritySettings with enable_dns_rebinding_protection=True and the merged allowlist, and pass it as transport_security= to FastMCP(). This makes the behaviour explicit rather than depending on SDK defaults.
Rationale: The SDK's auto-detection logic depends on the host parameter which we don't set, and the defaults may change between SDK versions. Being explicit removes the ambiguity.
Risks / Trade-offs
- Operator must know their Host header value — If a reverse proxy rewrites the Host header, the operator needs to allowlist the rewritten value, not the original. → Mitigation: document this in Compose file comments.
- No HTTPS origin support — If a client sends
Origin: https://..., it will be rejected. → Mitigation: acceptable for now; the MCP server sits behind a proxy that terminates TLS. Can addKB_MCP_ALLOWED_ORIGINSlater if needed.