feat(mcp): validate Host/Origin headers on MCP HTTP transports#643
Merged
Conversation
🛡️ Jit Security Scan Results✅ No security findings were detected in this PR
Security scan by Jit
|
Harden the MCP server's streamable-http and SSE transports by validating the Host and Origin request headers against allowlists before any tool call runs. The guard is on by default and auto-derives a safe allowlist from the bind address, so loopback and local clients work with no config. Non-loopback and reverse-proxy deployments can extend the allowlist (or disable the guard) via a server.transport_security config block and REDISVL_MCP_* environment variables, mirroring the existing auth pattern. stdio has no network surface and is unaffected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
vishal-bala
force-pushed
the
feat/mcp-host-origin-validation
branch
from
July 2, 2026 13:29
9d05390 to
eab122d
Compare
vishal-bala
marked this pull request as ready for review
July 2, 2026 13:45
vishal-bala
added a commit
that referenced
this pull request
Jul 3, 2026
## Overview Rolls up the **RedisVL MCP multi-index server** epic ([RAAE-1603](https://redislabs.atlassian.net/browse/RAAE-1603)) into `main`. A single MCP server can now expose **one or more** configured Redis index bindings — with discovery (`list-indexes`), explicit per-call routing (an optional `index` argument on `search-records`/`upsert-records`), and per-index write policy — while existing **single-index configs and callers behave exactly as before**. ## This is a roll-up — every change was already reviewed and merged This PR introduces **no new code**. It is the aggregate of a stacked series of per-ticket PRs, each reviewed and squash-merged into the `feature/raae-1603-mcp-multi-index` collector branch: - [#629](#629) — RAAE-1604: config + runtime refactor (immutable `BindingRuntime`, per-binding startup/teardown, `resolve_binding` routing, single global concurrency semaphore) - [#630](#630) — RAAE-1605: `list-indexes` discovery tool - [#631](#631) — RAAE-1606: `search-records` index routing - [#632](#632) — RAAE-1607: `upsert-records` index routing + per-index write policy - [#633](#633) — RAAE-1608: concept/how-to/README docs Each stacked PR was reviewed (human + Cursor Bugbot) and its threads resolved before merge; this branch is the sum of those merges plus a sync with `main`. ## Synced with `main` The branch is up to date with `main`, including the recently merged MCP security work — **Host/Origin header validation ([#643](#643 and the **nltk drop ([#645](#645 — pulled in via merge, so this PR's diff contains **only** the epic's own changes (main's commits are in the merge base). ## Security review A security review of the epic diff was run (auth scope enforcement, read-only/write policy, index routing, info leakage, injection, config/vectorizer resolution). **No findings** — tokenless HTTP is rejected upstream by the SDK auth middleware, scopes and read-only policy are enforced at both registration and per-call, routing is confined to configured bindings, and `redis_name` is never exposed by `list-indexes`. ## Verification - Full MCP unit + integration suite: **283 passed / 1 skipped** on `redis:8.2` (now runs without nltk). - `mypy` clean; `black`/`isort` clean. - Backward compatibility covered by dedicated tests (single-binding defaults, omitted-`index` behavior, unchanged response contract aside from the additive `index` echo). 🤖 Generated with [Claude Code](https://claude.com/claude-code) [RAAE-1603]: https://redislabs.atlassian.net/browse/RAAE-1603?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes MCP server startup, routing, and write boundaries across bindings; single-index behavior is preserved but multi-index misconfiguration or routing errors could affect production assistants. > > **Overview** > **Multi-index MCP server** — One process can expose several logical index bindings from YAML `indexes`, each with its own schema inspection, search config, vectorizer, and runtime limits. Startup is all-or-nothing: any binding failure prevents the server from starting. > > **Routing and discovery** — New **`list-indexes`** tool enumerates logical ids, filterable fields, `upsert_available`, and optional limits (without exposing `redis_name`). **`search-records`** and **`upsert-records`** accept an optional **`index`** argument; responses echo the resolved id. With a single binding, omitting `index` still works; with multiple bindings, omitting or using an unknown id returns `invalid_request`. > > **Write policy** — Global `--read-only` / `REDISVL_MCP_READ_ONLY` plus per-binding **`read_only: true`** combine into **`effective_read_only`**. `upsert-records` is registered only if at least one binding is writable; writes to a read-only binding fail with **`forbidden`** before Redis is touched. > > **Implementation** — Config drops the “exactly one index” rule and adds **`description`** / **`read_only`** on bindings. Server state moves to per-binding **`BindingRuntime`** and **`resolve_binding()`**; removed single-index helpers like `get_index()`. Search tool descriptions omit per-schema hints when multiple indexes are configured and point clients to `list-indexes`. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 553e55f. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
🚀 PR was released in |
ahsd-coder
pushed a commit
to ahsd-coder/LLM-Smart-Cache
that referenced
this pull request
Jul 7, 2026
## Overview Rolls up the **RedisVL MCP multi-index server** epic ([RAAE-1603](https://redislabs.atlassian.net/browse/RAAE-1603)) into `main`. A single MCP server can now expose **one or more** configured Redis index bindings — with discovery (`list-indexes`), explicit per-call routing (an optional `index` argument on `search-records`/`upsert-records`), and per-index write policy — while existing **single-index configs and callers behave exactly as before**. ## This is a roll-up — every change was already reviewed and merged This PR introduces **no new code**. It is the aggregate of a stacked series of per-ticket PRs, each reviewed and squash-merged into the `feature/raae-1603-mcp-multi-index` collector branch: - [#629](redis/redis-vl-python#629) — RAAE-1604: config + runtime refactor (immutable `BindingRuntime`, per-binding startup/teardown, `resolve_binding` routing, single global concurrency semaphore) - [#630](redis/redis-vl-python#630) — RAAE-1605: `list-indexes` discovery tool - [#631](redis/redis-vl-python#631) — RAAE-1606: `search-records` index routing - [#632](redis/redis-vl-python#632) — RAAE-1607: `upsert-records` index routing + per-index write policy - [#633](redis/redis-vl-python#633) — RAAE-1608: concept/how-to/README docs Each stacked PR was reviewed (human + Cursor Bugbot) and its threads resolved before merge; this branch is the sum of those merges plus a sync with `main`. ## Synced with `main` The branch is up to date with `main`, including the recently merged MCP security work — **Host/Origin header validation ([#643](redis/redis-vl-python#643 and the **nltk drop ([#645](redis/redis-vl-python#645 — pulled in via merge, so this PR's diff contains **only** the epic's own changes (main's commits are in the merge base). ## Security review A security review of the epic diff was run (auth scope enforcement, read-only/write policy, index routing, info leakage, injection, config/vectorizer resolution). **No findings** — tokenless HTTP is rejected upstream by the SDK auth middleware, scopes and read-only policy are enforced at both registration and per-call, routing is confined to configured bindings, and `redis_name` is never exposed by `list-indexes`. ## Verification - Full MCP unit + integration suite: **283 passed / 1 skipped** on `redis:8.2` (now runs without nltk). - `mypy` clean; `black`/`isort` clean. - Backward compatibility covered by dedicated tests (single-binding defaults, omitted-`index` behavior, unchanged response contract aside from the additive `index` echo). 🤖 Generated with [Claude Code](https://claude.com/claude-code) [RAAE-1603]: https://redislabs.atlassian.net/browse/RAAE-1603?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes MCP server startup, routing, and write boundaries across bindings; single-index behavior is preserved but multi-index misconfiguration or routing errors could affect production assistants. > > **Overview** > **Multi-index MCP server** — One process can expose several logical index bindings from YAML `indexes`, each with its own schema inspection, search config, vectorizer, and runtime limits. Startup is all-or-nothing: any binding failure prevents the server from starting. > > **Routing and discovery** — New **`list-indexes`** tool enumerates logical ids, filterable fields, `upsert_available`, and optional limits (without exposing `redis_name`). **`search-records`** and **`upsert-records`** accept an optional **`index`** argument; responses echo the resolved id. With a single binding, omitting `index` still works; with multiple bindings, omitting or using an unknown id returns `invalid_request`. > > **Write policy** — Global `--read-only` / `REDISVL_MCP_READ_ONLY` plus per-binding **`read_only: true`** combine into **`effective_read_only`**. `upsert-records` is registered only if at least one binding is writable; writes to a read-only binding fail with **`forbidden`** before Redis is touched. > > **Implementation** — Config drops the “exactly one index” rule and adds **`description`** / **`read_only`** on bindings. Server state moves to per-binding **`BindingRuntime`** and **`resolve_binding()`**; removed single-index helpers like `get_index()`. Search tool descriptions omit per-schema hints when multiple indexes are configured and point clients to `list-indexes`. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 553e55f4e93b306b643e3b8aca157e7a40a1497b. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
RedisVL can expose a configured Redis index to MCP clients over HTTP, through the
streamable-httpandssetransports. Until now, once such a server was listening it would answer any HTTP request that reached its port, without confirming that the request was actually addressed to it. Validating that requests are intended for this server is a standard piece of transport hardening, and one worth having on by default rather than left to each deployment.This change adds that check: the HTTP transports now confirm, on every request, that it is genuinely addressed to this server before any tool runs.
What changed
The server validates the
HostandOriginheaders of each HTTP request against allowlists and rejects anything that doesn't match, before the request reaches the search or upsert tools. The protection is on by default and needs no configuration for the common cases — the allowlist is derived automatically from the address the server binds to, so loopback and same-machine clients keep working exactly as before. Requests that carry noOrigin(typical of non-browser MCP clients) are unaffected; only cross-site origins are scrutinized.For deployments where the client-visible host legitimately differs from the bind address — behind a reverse proxy, or bound to
0.0.0.0and reached by a public hostname — the allowlist can be extended, or the check relaxed, through a newserver.transport_securityblock in the MCP config and matchingREDISVL_MCP_*environment variables. This mirrors the existing authentication configuration, so it should feel familiar. Authentication and this check are complementary and independent: auth decides who may call, while this decides which authority a request may claim. Thestdiotransport has no network surface and is untouched.Implementation notes
redisvl/mcp/transport_security.pymodule holds a small ASGI middleware plus the config resolution, with FastMCP/Starlette imports deferred so the module stays importable without the optionalmcpextra.RedisVLMCPServer.run_async, so it applies to both the CLI and any programmatic embedder, and runs outermost — ahead of auth and the tool handlers.0.0.0.0bind trusts only loopback until the operator names its public hosts.Tests & docs
Adds unit coverage for the middleware (host/origin matching, port and IPv6 handling, case-insensitivity, enable/disable) and for its config resolution, plus integration tests over real HTTP confirming that a legitimate local client still succeeds while spoofed hosts and cross-site origins are rejected. The MCP how-to and authentication guides gain a "Transport Security" section covering the defaults and the configuration knobs.
🤖 Generated with Claude Code
Note
Medium Risk
Changes request handling on all HTTP MCP traffic (fail-closed by default); misconfigured proxies or remote
0.0.0.0deployments withoutallowed_hostscan break legitimate clients until allowlists are set.Overview
Adds default-on Host/Origin validation on
streamable-httpandsseMCP transports to mitigate DNS rebinding and untrusted cross-site browser calls, before JWT auth or tool handlers run.A new
redisvl/mcp/transport_securitymodule provides ASGIHostOriginValidationMiddleware, bind-derived default hosts (loopback for127.0.0.1/0.0.0.0), and config resolution (YAMLserver.transport_securityplusREDISVL_MCP_*env vars, env wins).RedisVLMCPServer.run_asyncprepends this middleware outermost for HTTP transports;stdiois unchanged.Operator knobs: extra
allowed_hosts/allowed_origins,allow_any_origin, orenabled: falsefor reverse-proxy setups. Docs cover transport security separately from JWT auth and note that0.0.0.0binds need explicit client-visible hosts in the allowlist.Reviewed by Cursor Bugbot for commit eab122d. Bugbot is set up for automated code reviews on this repo. Configure here.