LearnNewsExamplesServices
Frontmatter
id12371
titleMCP transport: configurable host-allowlist for cloud deploy behind a reverse proxy (kb + mc)
stateClosed
labels
bugai
assigneesneo-opus-ada
createdAtJun 2, 2026, 11:14 AM
updatedAtJun 2, 2026, 11:51 AM
githubUrlhttps://github.com/neomjs/neo/issues/12371
authorneo-opus-ada
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtJun 2, 2026, 11:51 AM

MCP transport: configurable host-allowlist for cloud deploy behind a reverse proxy (kb + mc)

neo-opus-ada
neo-opus-ada commented on Jun 2, 2026, 11:14 AM

Problem

neo MCP HTTP/SSE servers (knowledge-base, memory-core) deployed behind a reverse proxy on a real public hostname reject every request with {"jsonrpc":"2.0","error":{"code":-32000,"message":"Invalid Host: <hostname>"},"id":null}.

Root cause: ai/mcp/server/shared/services/TransportService.mjs (~line 104) calls the MCP SDK's createMcpExpressApp() with no options. The SDK then defaults host='127.0.0.1' and installs localhostHostValidation(), allowing only localhost / 127.0.0.1 / [::1] as the Host header. A reverse proxy (Caddy v2 etc.) forwards the original public Host by default → not allowlisted → flat 403 before any MCP/JSON-RPC handling. It is client-agnostic: browser and MCP client are rejected identically.

Canonical gap: ai/deploy/Caddyfile only "works out of the box" because it defaults NEO_DEPLOY_HOSTNAME=localhost. Any real public hostname hits this wall. Surfaced by the first real-world public cloud deployment.

Fix

TransportService should pass a computed allowedHosts list into createMcpExpressApp({ allowedHosts }):

['localhost','127.0.0.1','[::1]']  ∪  (hostname of aiConfig.publicUrl, if set)  ∪  (aiConfig.allowedHosts split on ',')
  • Reuse existing: the publicUrl leaf (NEO_PUBLIC_URL) already exists in all three templates — derive its hostname so a deployment that already sets NEO_PUBLIC_URL for OAuth resource-metadata gets host-allowlisting for free.
  • New leaf + env: add an allowedHosts leaf → NEO_MCP_ALLOWED_HOSTS (comma-separated) for multi-hostname deployments or where the client Host differs from publicUrl.
  • Invariant: localhost / 127.0.0.1 / [::1] are always included — the container healthcheck hits http://127.0.0.1:3000; dropping them breaks the healthcheck → restart loop.

Both servers (kb + mc)

The logic lives in the shared TransportService, so one code change covers both. The new allowedHosts leaf must be declared in both per-server templates and the base, since each declares its transport block independently:

  • ai/config.template.mjs
  • ai/mcp/server/knowledge-base/config.template.mjs
  • ai/mcp/server/memory-core/config.template.mjs

Contract Ledger

Target Surface Source of Authority Behavior Fallback / Edge Evidence
allowedHosts leaf / NEO_MCP_ALLOWED_HOSTS env this ticket comma-separated extra hostnames added to the MCP transport allowlist empty/unset → allowlist is just localhost-set + publicUrl host unit test on computed list
createMcpExpressApp({allowedHosts}) in TransportService SDK host-header validation accepts the configured public Host; still rejects unknown hosts (DNS-rebinding protection retained) localhost-set ALWAYS present (healthcheck-safe) unit + deploy smoke
publicUrl (NEO_PUBLIC_URL) existing leaf, all 3 templates its hostname is auto-added to the allowlist unset → only localhost-set + explicit NEO_MCP_ALLOWED_HOSTS unit test

Acceptance Criteria

  1. With NEO_PUBLIC_URL=https://mcp.example.com, a request with Host: mcp.example.com is accepted (no -32000).
  2. With NEO_MCP_ALLOWED_HOSTS=a.example.com,b.example.com, Hosts a.example.com and b.example.com are accepted.
  3. localhost / 127.0.0.1 / [::1] are accepted regardless of config (healthcheck-safe).
  4. Both knowledge-base and memory-core honor it (shared TransportService + leaf in both per-server templates + base).
  5. A unit test covers the allowlist computation: localhost-always, publicUrl-derived, NEO_MCP_ALLOWED_HOSTS-extra, and empty-config cases.

Refs #12367 (related deployment-hardening work, not a dependency).

tobiu assigned to @neo-opus-ada on Jun 2, 2026, 11:16 AM
tobiu referenced in commit d309029 - "feat(ai): configurable MCP transport host-allowlist for cloud deploy behind a proxy (#12371) (#12373) on Jun 2, 2026, 11:51 AM
tobiu closed this issue on Jun 2, 2026, 11:51 AM