Skip to content

feat(server): synthesize host:* siblings for bare allowed_hosts#537

Merged
bokelley merged 2 commits into
mainfrom
bokelley/subdomain-allowlist-symmetry
May 4, 2026
Merged

feat(server): synthesize host:* siblings for bare allowed_hosts#537
bokelley merged 2 commits into
mainfrom
bokelley/subdomain-allowlist-symmetry

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

@bokelley bokelley commented May 4, 2026

Closes #518.

Summary

When allowed_hosts=['acme.localhost'] is passed to create_mcp_server (or serve), acme.localhost:* is now automatically added as a sibling so the FastMCP transport-security check accepts requests on either bare or port-suffixed Host headers.

Why

Salesagent adopter feedback (issue #518): registering acme.localhost once in InMemorySubdomainTenantRouter covers both acme.localhost and acme.localhost:3001 at lookup time (per _normalize_host port-stripping). But the FastMCP allowed_hosts allowlist required both forms registered explicitly — asymmetric.

This change makes the two surfaces symmetric. Single host registration in adopter code now covers both router lookup and transport allowlist.

Behavior

  • Bare host ("acme.localhost") → both "acme.localhost" and "acme.localhost:*" registered.
  • Explicit port or wildcard ("acme.localhost:*", "acme.localhost:3001") → passes through unchanged. Adopter signaled they're managing the form themselves; we don't second-guess.
  • Idempotent: passing both forms doesn't duplicate.

Test plan

  • 5 new tests in tests/test_serve_transport_security.py:
    • bare host synthesizes :* sibling
    • :* form passes through (no bare-host auto-add)
    • explicit port passes through
    • mixed list each gets correct treatment
    • idempotent on duplicate input
  • All 9 tests in the file pass (4 existing + 5 new)
  • ruff, mypy, all pre-commit hooks pass

🤖 Generated with Claude Code

bokelley and others added 2 commits May 3, 2026 21:38
When an adopter passes ``allowed_hosts=['acme.localhost']`` to
``create_mcp_server`` / ``serve``, the ``host:*`` sibling
(``acme.localhost:*``) is now auto-added so requests on either bare
or port-suffixed Host headers pass FastMCP's transport-security check.

Mirrors the port-stripping ``InMemorySubdomainTenantRouter`` does at
lookup time — registering once now covers both surfaces. Salesagent
adopter feedback flagged the asymmetry: their dev setup needed both
``acme.localhost`` (router) and ``acme.localhost:*`` (allowlist) per
host, while the spec'd surface should accept a single declaration.

Hosts that already include ``:`` (explicit port or wildcard) pass
through unchanged — adopter signaled they're managing the form
themselves. Idempotent: both forms in the input produce no duplicates.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per review on #537 — flag that bracketed and raw IPv6 literals contain
colons and pass through without synthesis (correctly), and point
adopters at the explicit `[::1]:*` form for custom v6 hosts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley merged commit 2733423 into main May 4, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

decisioning: SubdomainTenantMiddleware allowlist should auto-synthesize :* host variants

1 participant