Skip to content

fix(mcp): apply the Host/Origin guard to every HTTP transport - #746

Open
vishal-bala wants to merge 3 commits into
mainfrom
fix/mcp-http-transport-host-guard
Open

vishal-bala wants to merge 3 commits into
mainfrom
fix/mcp-http-transport-host-guard

Conversation

@vishal-bala

@vishal-bala vishal-bala commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

Motivation

The MCP server's DNS-rebinding protection, the Host/Origin validation middleware, was only applied when run_async received the literal transport name "sse" or "streamable-http". FastMCP serves two more cases over HTTP. One is "http", which is FastMCP's own default HTTP transport name. The other is an omitted transport, which FastMCP resolves from fastmcp.settings.transport and therefore from FASTMCP_TRANSPORT. In both cases the server bound an HTTP port with no Host or Origin validation.

The rvl mcp CLI was not affected, because its --transport choices are only stdio, sse and streamable-http. An application that embeds the server and calls run_async itself was affected.

Changes

The HTTP transport names are now defined once, as HTTP_TRANSPORTS in redisvl/mcp/transport_security.py, and run_async checks the transport after resolving an omitted one the same way FastMCP does:

resolved = transport if transport is not None else fastmcp.settings.transport
if resolved in HTTP_TRANSPORTS:
    ...  # prepend the Host/Origin middleware

FastMCP's run_http_async passes middleware to http_app whatever the transport name, so the middleware takes effect for "http" exactly as it does for "streamable-http".

The CLI's _check_http_auth and _serve now use the same set. Nothing changes for CLI users today, but if "http" is later added to the CLI's choices, it can no longer skip the unauthenticated-bind warning or the bind arguments without anyone noticing. The CLI imports the set lazily, as it does the rest of the MCP code, so rvl stays usable without the mcp extra.

The regression test drives run_async with each transport name, and with an omitted one resolving to stdio, http or streamable-http. Against the previous code it fails three of its seven cases: "http", and an omitted transport resolving to either HTTP name.


Note

High Risk
Fixes missing DNS-rebinding protection on HTTP serving paths; incorrect allowlist/bind resolution could still break legitimate clients, but the change tightens security rather than weakening it.

Overview
Closes a gap where the MCP server could listen over HTTP without Host/Origin (DNS-rebinding) middleware when embedders used FastMCP’s "http" transport or left transport unset so it resolved to HTTP via FASTMCP_TRANSPORT.

HTTP_TRANSPORTS is defined once in transport_security.py (http, sse, streamable-http). run_async resolves the effective transport the same way FastMCP does (including stdio fallback on older FastMCP without settings.transport), then prepends the guard for any resolved HTTP name—not only sse / streamable-http.

The guard’s host allowlist now uses the bind address FastMCP will actually use: explicit host/port kwargs, else fastmcp.settings (FASTMCP_HOST / FASTMCP_PORT), avoiding rejections when settings drive the bind.

The rvl mcp CLI centralizes its HTTP transport list in _HTTP_TRANSPORT_CHOICES so future CLI HTTP options stay tied to auth/bind checks (still no module-level import from redisvl.mcp). Regression tests cover each HTTP transport, omitted transport defaults, and settings-driven bind addresses.

Reviewed by Cursor Bugbot for commit 54c4959. Bugbot is set up for automated code reviews on this repo. Configure here.

`run_async` added the DNS-rebinding middleware only when the transport
argument was literally "sse" or "streamable-http". FastMCP serves two more
cases over HTTP: "http", which is FastMCP's own default HTTP transport name,
and an omitted transport, which FastMCP resolves from
`fastmcp.settings.transport` (settable through FASTMCP_TRANSPORT). In both,
the server bound an HTTP port with no Host or Origin validation.

The `rvl mcp` CLI was not affected, because its `--transport` choices are
only stdio, sse and streamable-http. An application embedding the server
and calling `run_async` itself was.

The HTTP transport names are now defined once, as `HTTP_TRANSPORTS` in
`transport_security`, and `run_async` tests the transport after resolving an
omitted one the way FastMCP does. The CLI's two checks use the same set, so
adding "http" to its choices later cannot silently skip the auth warning or
the bind arguments. The CLI imports the set lazily, as it does the rest of
the MCP code, so `rvl` stays usable without the `mcp` extra.

The regression test drives `run_async` with every transport name and with
an omitted one resolving to each, and fails three of its seven cases against
the previous code.
@vishal-bala
vishal-bala marked this pull request as ready for review October 5, 2026 13:13
@vishal-bala vishal-bala added the auto:patch Increment the patch version when merged label Oct 5, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 1d0371c. Configure here.

Comment thread redisvl/mcp/server.py Outdated
Comment thread redisvl/mcp/server.py
Comment thread redisvl/cli/mcp.py Outdated
Three corrections to the Host/Origin guard change.

`fastmcp.settings.transport` only exists from FastMCP 3.1, but RedisVL
supports `fastmcp>=2.0.0`. On 2.x through 3.0, reading it raised
`AttributeError` before anything was served, breaking the default stdio
path. Those versions always resolved an omitted transport to stdio, so that
is now the fallback when the setting is absent. Checked against the 2.0.0,
2.10.0, 2.14.0, 3.0.0 and 3.1.0 wheels.

The guard built its host allowlist from a hardcoded 127.0.0.1:8000 whenever
no host or port was passed, while FastMCP binds `fastmcp.settings.host` and
`port` (FASTMCP_HOST / FASTMCP_PORT) in that case. On a settings-driven
non-loopback bind the guard would have rejected legitimate Host headers. It
now resolves both the way FastMCP does, with explicit arguments still taking
precedence.

The CLI imported the server's transport set from `redisvl.mcp`, which its
tests replace with a non-package stub, so the import resolved only when the
submodule happened to be cached -- six of the fourteen CLI tests failed when
run alone. The CLI goes back to importing nothing from `redisvl.mcp`, and
instead derives its `--transport` choices and both HTTP-only checks from one
local tuple, which keeps them from drifting apart.
@vishal-bala
vishal-bala requested a review from limjoobin October 7, 2026 15:06
…t-host-guard

# Conflicts:
#	redisvl/mcp/server.py

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto:patch Increment the patch version when merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant