Problem
isHostAllowed splits the Host header on : to strip the port, which corrupts bracketed IPv6 literals:
packages/mcp-core/src/transports/http.ts:36-41
const host = hostHeader.split(':')[0].toLowerCase();
return allowedHosts.some((allowed) => allowed.split(':')[0].toLowerCase() === host);
For Host: [::1]:3000 this yields '['. The allowlist entry [::1]:3000 is mangled the same way, so both sides reduce to '[' and happen to match; [::1] compared against [::1]:3000 does not. Either way the comparison is not doing what it reads as doing.
Why it matters
Any deployment reachable over IPv6 — a dual-stack cluster, ::1 in local testing — gets host checks decided by string accidents rather than by the configured allowlist. Requests are rejected with a 421 (http.ts:232-236) or accepted for the wrong reason, and the failure is hard to read because the mangled value never appears in a log.
Proposed approach
- Parse the header with
new URL(\http://${hostHeader}`)and compareurl.hostname`, which handles brackets and the missing-port case.
- Normalise allowlist entries the same way at parse time (
parseList) instead of at comparison time.
- Cover
[::1]:3000, [::1], localhost:3000, and localhost in packages/mcp-core/src/transports/__tests__/http.test.ts.
Acceptance criteria
- An IPv6 Host header matches an IPv6 allowlist entry and only that entry.
- A malformed Host header is rejected rather than silently reduced to a token that matches something.
Problem
isHostAllowedsplits the Host header on:to strip the port, which corrupts bracketed IPv6 literals:packages/mcp-core/src/transports/http.ts:36-41For
Host: [::1]:3000this yields'['. The allowlist entry[::1]:3000is mangled the same way, so both sides reduce to'['and happen to match;[::1]compared against[::1]:3000does not. Either way the comparison is not doing what it reads as doing.Why it matters
Any deployment reachable over IPv6 — a dual-stack cluster,
::1in local testing — gets host checks decided by string accidents rather than by the configured allowlist. Requests are rejected with a 421 (http.ts:232-236) or accepted for the wrong reason, and the failure is hard to read because the mangled value never appears in a log.Proposed approach
new URL(\http://${hostHeader}`)and compareurl.hostname`, which handles brackets and the missing-port case.parseList) instead of at comparison time.[::1]:3000,[::1],localhost:3000, andlocalhostinpackages/mcp-core/src/transports/__tests__/http.test.ts.Acceptance criteria