feat(mcp): multi-index MCP server support (RAAE-1603)#646
Merged
Conversation
## Motivation The RedisVL MCP server currently binds to exactly one Redis index per process. That single-binding assumption is enforced by a config validator and baked throughout the codebase — single-resource server state, single-binding convenience accessors on `MCPConfig`, and the search/upsert tools. Before the server can expose multiple logical indexes from a single endpoint ([RAAE-1603](https://redislabs.atlassian.net/browse/RAAE-1603)), that assumption has to be removed and replaced with a real multi-binding model. This PR ([RAAE-1604](https://redislabs.atlassian.net/browse/RAAE-1604)) does exactly that, and nothing more: it reshapes the configuration and runtime model so the server can start, inspect, validate, and serve one *or many* bindings, while keeping existing single-index configs and callers behaving identically. It is the foundation the rest of the epic (discovery via `list-indexes`, index routing on `search-records`/`upsert-records`, docs) builds on, so it intentionally does not yet add any new request parameters or tools. ## Implementation The core of the change is a new immutable `BindingRuntime` (in `redisvl/mcp/runtime.py`) that bundles everything a tool call needs for one logical index: the binding config, the connected `AsyncSearchIndex`, its effective (inspected + overridden) schema, an optional vectorizer, the resolved native-hybrid-search capability, and the effective read-only flag. The server now holds a `dict[str, BindingRuntime]` keyed by logical id instead of a single set of `_index`/`_vectorizer` fields. Startup iterates every configured binding and inspects, validates, and initializes each one independently — each binding owns its own Redis client — with all-or-nothing teardown so a single bad binding fails startup cleanly without leaking connections. On the config side, the "exactly one configured index binding" validator is gone (we now simply require at least one binding with non-blank ids), and the schema-inspection, runtime-mapping, and search-validation methods move from `MCPConfig` onto `MCPIndexBindingConfig` where they naturally belong per binding. The single-binding convenience accessors on `MCPConfig` are removed. Each binding gains optional `description` and `read_only` fields, and a binding's effective write availability is computed as global `--read-only` OR the per-index `read_only`. Tool resolution goes through a new `server.resolve_binding(index_id)` helper that defaults to the sole binding when one is configured (preserving backward compatibility) and returns an `invalid_request` error when an index is omitted with multiple bindings configured or when an unknown id is given. The search and upsert tools were re-threaded to operate on a resolved `BindingRuntime` rather than reaching into single-binding server accessors. Additional notes: - Native-hybrid-search support is now probed eagerly per binding at startup and stored on the `BindingRuntime`, replacing the previous lazy single-index cache. - The concurrency semaphore is a single process-wide ceiling sized from the maximum `max_concurrency` across bindings; the request timeout is sourced per-binding and passed explicitly into `run_guarded`. - `get_index()` / `get_vectorizer()` are retained as thin convenience wrappers over `resolve_binding(None)`. - Implemented test-first: new coverage for multi-binding config loading, `description`/`read_only` defaults, `resolve_binding` routing semantics, semaphore sizing, per-binding teardown, and three integration tests (multi-binding startup, global read-only override, and a single invalid binding failing startup), alongside the updated single-index tests that confirm backward compatibility. ## Verification - `mypy` clean across all source files; `black`/`isort` formatted. - 182 MCP unit tests pass. - 44 MCP integration tests pass (2 skipped on Redis-version gates) against Redis 8. 🤖 Generated with [Claude Code](https://claude.com/claude-code) [RAAE-1603]: https://redislabs.atlassian.net/browse/RAAE-1603?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ [RAAE-1604]: https://redislabs.atlassian.net/browse/RAAE-1604?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > MCP startup/teardown and binding resolution affect how indexes are served; the required `run_guarded(..., timeout_seconds=)` change breaks custom MCP extensions that call it directly. > > **Overview** > **MCP** moves from a single enforced index binding to a **`dict` of `BindingRuntime`** entries: startup inspects and initializes each configured index independently (own client, vectorizer, hybrid probe, effective read-only), with **`resolve_binding(index_id)`** defaulting when only one index is configured and rejecting ambiguous or unknown ids. Config drops the “exactly one binding” rule and **`MCPConfig`** convenience accessors; per-binding **`description`**, **`read_only`**, and schema/search helpers live on **`MCPIndexBindingConfig`**. Search/upsert tools read from the resolved runtime; **`run_guarded`** now requires **`timeout_seconds=`** per binding (breaking for direct callers). > > Also in this release: **`SearchIndex.drop_keys`** uses **`UNLINK`** instead of **`DEL`**; semantic router **`delete()`** removes the standalone route-config key; **`sql-redis>=0.7.1`** with docs for **`hybrid_vector_search` / FT.HYBRID**; auto-release publishes to PyPI via **`pypa/gh-action-pypi-publish`** with OIDC; version **0.22.0**. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit bd2a28a. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --- ###⚠️ Breaking change for downstream authors `RedisVLMCPServer.run_guarded(operation_name, awaitable)` now requires a keyword-only `timeout_seconds` argument (sourced from each binding's `request_timeout_seconds`). Any code that subclasses `RedisVLMCPServer` or calls `run_guarded` directly (custom tools/plugins) must update its call sites to pass `timeout_seconds=`, otherwise it raises `TypeError: run_guarded() missing 1 required keyword-only argument: 'timeout_seconds'` at call time. This repository has no CHANGELOG file, so this note serves as the migration callout (per review feedback on #629). --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
> Stacked on #629 (RAAE-1604). Review/merge that first; this PR targets the 1604 branch so the diff is scoped to the discovery tool. ## Motivation Once a single MCP server can expose multiple logical indexes ([RAAE-1604](https://redislabs.atlassian.net/browse/RAAE-1604)), clients need a lightweight way to discover what's available so they can pick the right index instead of guessing. This PR ([RAAE-1605](https://redislabs.atlassian.net/browse/RAAE-1605)) adds an always-registered, read-only `list-indexes` tool for exactly that, and it grounds discovery in the schema the server already inspected at startup rather than asking users to re-declare field metadata in config. ## Implementation The new tool (`redisvl/mcp/tools/list_indexes.py`) returns one entry per configured binding, in configured order: the logical `id`, an optional `description`, an `upsert_available` flag, the shared filterable `fields`, and — only when explicitly configured — a `limits` object. `upsert_available` is simply `not effective_read_only`, so it already reflects both the global `--read-only` flag and the per-index `read_only` policy resolved at startup. The `fields` list is built from the binding's effective (inspected + overridden) schema that already lives on its `BindingRuntime`, so the output stays consistent with what the index actually contains; the vector field and the configured default embed-source text field are omitted because they are implementation inputs rather than fields a client would filter on. The Redis index name (`redis_name`) is deliberately never exposed. Limits are included only when the operator set them explicitly — detected via the runtime model's `model_fields_set` — so defaults don't masquerade as deliberate overrides; per the contract this covers `max_limit` and `max_upsert_records`. The tool is registered unconditionally during the server's tool registration (alongside `search-records` and the conditionally-registered `upsert-records`) and is gated by the same read scope as search when auth is enabled, since it is read-only. - Output is deterministic and ordered by configured binding. - No new configuration surface or settings are required. ## Verification - `mypy` clean; `black`/`isort` formatted. - New unit coverage: field omission (vector + embed-source), description/limits inclusion rules, `redis_name` secrecy, read-only reflection, single- and multi-binding output, and tool registration. - New integration test starts a real two-binding server (one vector, one fulltext) and asserts the discovered fields come from the inspected schema and follow the omission rules. - Full MCP suite green: 178 unit + 45 integration (2 skipped on Redis-version gates) against Redis 8. 🤖 Generated with [Claude Code](https://claude.com/claude-code) [RAAE-1604]: https://redislabs.atlassian.net/browse/RAAE-1604?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ [RAAE-1605]: https://redislabs.atlassian.net/browse/RAAE-1605?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Read-only discovery metadata with no new config; behavior is additive and gated like existing search tools when auth is enabled. > > **Overview** > Adds a read-only **`list-indexes`** MCP tool so clients can discover logical indexes on multi-binding servers before calling **`search-records`** or **`upsert-records`**. > > The tool is **always registered** during `_register_tools` (alongside search; upsert remains conditional). Each binding is returned in config order with **`id`**, optional **`description`**, **`upsert_available`** (`not effective_read_only`), filterable **`fields`** from the startup-inspected schema (vector and default embed-source text omitted), and **`limits`** only when **`max_limit`** / **`max_upsert_records`** were explicitly set. **`redis_name`** is never exposed; empty bindings raise **`RuntimeError`** like other pre-startup paths. When auth is on, the tool uses the same **read scope** as search. > > Unit and integration tests cover payload rules, registration, and a two-binding startup scenario. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 45ce686. 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>
## Motivation With a single MCP server now able to expose multiple logical index bindings ([RAAE-1604](https://redislabs.atlassian.net/browse/RAAE-1604)) and advertise them via `list-indexes` ([RAAE-1605](https://redislabs.atlassian.net/browse/RAAE-1605)), the `search-records` tool still implicitly resolved the sole binding. On a multi-binding server it had no way to say *which* index a query should run against. This change ([RAAE-1606](https://redislabs.atlassian.net/browse/RAAE-1606)) makes that routing explicit while keeping the public contract backwards-safe for existing single-index callers. The design goal is that v1 clients see no behavioral change: when exactly one binding is configured and the caller omits `index`, the request resolves to that binding exactly as before. Routing only becomes mandatory once ambiguity exists. Query construction, validation, pagination, filtering, and the configured search mode all remain owned by the selected binding — this ticket adds only the selection and a confirmation echo, not new query behavior. ## Implemented changes `search-records` gains an optional `index` argument naming the logical binding to query. Resolution flows through the shared `resolve_binding` routing introduced in RAAE-1604, so the three cases fall out consistently: an omitted `index` with one binding resolves to that binding; an omitted `index` with multiple bindings returns `invalid_request`; and an unknown id returns `invalid_request`. The resolved logical id is echoed back as the `index` field of the response payload so multi-index clients can confirm where a query actually ran. All downstream work (limits, schema, vectorizer, search config) continues to read from the resolved binding's runtime. When multiple bindings are configured the tool description is ambiguous about fields, so instead of emitting per-field filter hints it now appends a short routing note directing the client to call `list-indexes` first and pass the chosen id as `index`. Single-binding servers keep their full schema-derived description unchanged. Minor additional changes: - The FastMCP wrapper exposes `index` as a tool parameter so MCP clients can supply it. - Unit coverage for default-to-sole-binding, named routing, unknown-id rejection, the wrapper param, and the ambiguous-schema description note. - Integration coverage for a two-binding server: routing to each named binding, omitted-index rejection, unknown-id rejection, and single-binding echo. ## Verification - `make format` (isort + black) and mypy clean on changed files. - Full MCP suite: **232 passed, 2 skipped** (Redis-version-gated) across unit + integration. ## Stacking This PR targets `feature/raae-1605-list-indexes` so its diff stays scoped to search routing. Review/merge bottom-up: [#629](#629) → [#630](#630) → this PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code) [RAAE-1604]: https://redislabs.atlassian.net/browse/RAAE-1604?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ [RAAE-1605]: https://redislabs.atlassian.net/browse/RAAE-1605?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ [RAAE-1606]: https://redislabs.atlassian.net/browse/RAAE-1606?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Backward-compatible for single-index callers; changes are limited to MCP search routing and an additive response field, with validation delegated to existing `resolve_binding`. > > **Overview** > The MCP **`search-records`** tool now accepts an optional **`index`** argument so clients can target a specific logical binding on multi-index servers. Resolution goes through **`resolve_binding`**: omitting **`index`** still works when only one binding exists; with multiple bindings, **`index`** is required and unknown ids return **`invalid_request`**. > > Successful responses include a new **`index`** field with the resolved binding id so callers can confirm routing. The FastMCP wrapper exposes **`index`** as a tool parameter. > > When the server has multiple indexes, the tool description skips per-schema filter/return-field hints and instead tells clients to call **`list-indexes`** and pass the chosen id as **`index`**. Single-index servers keep the full schema-derived description. > > Unit and integration tests cover named routing, omitted-index errors on multi-binding setups, unknown-index rejection, single-binding echo behavior, and the ambiguous-schema description. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit c2f30d5. 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>
…ds (RAAE-1607) (#632) ## Motivation This completes the multi-index tool surface for the RedisVL MCP server. After [RAAE-1606](https://redislabs.atlassian.net/browse/RAAE-1606) taught `search-records` to route by logical index, `upsert-records` ([RAAE-1607](https://redislabs.atlassian.net/browse/RAAE-1607)) needs the same explicit routing — but writes also carry a policy dimension that reads do not. A single server can now host a mix of writable and read-only bindings, and the tool must respect both the global `--read-only` override and each binding's own `read_only` flag while staying backwards-safe for existing single-index clients. The design keeps single-index behavior identical: when one binding is configured and `index` is omitted, the write resolves to that binding exactly as before. Routing becomes mandatory only once multiple bindings exist. Write enforcement happens at two complementary levels so the contract is unambiguous: a server with no writable bindings should not advertise the tool at all, while a server with some writable bindings still needs to protect the read-only ones on a per-call basis. ## Implemented changes `upsert-records` gains an optional `index` argument naming the logical binding to write to, resolved through the shared `resolve_binding` routing introduced in RAAE-1604. An omitted `index` with one binding resolves to that binding; an omitted `index` with multiple bindings returns `invalid_request`; and an unknown id returns `invalid_request`. The resolved logical id is echoed back as the `index` field of the response, and the selected binding's embedding, runtime limits, and schema validation drive the rest of the write unchanged. Write availability is enforced at two levels. The tool registration gate is refined from "global read-only is off" to "at least one binding is writable" — expressed via `effective_read_only`, which already folds in both global read-only mode and a binding's own `read_only` policy — so an all-read-only server does not expose `upsert-records` at all. When the tool is registered, a per-call guard rejects writes to any individual read-only binding with `invalid_request` *before* any embedding or backend write occurs, so a writable server can still protect specific indexes. Minor additional changes: - The FastMCP wrapper exposes `index` as a tool parameter. - Unit coverage for default-to-sole-binding, named routing, unknown-id rejection, read-only-binding rejection, the wrapper param, and both registration-gate outcomes (any-writable exposes the tool; all-read-only hides it). - Integration coverage on a two-binding server (one writable vector index, one read-only fulltext index): routing to the writable binding, omitted-index rejection, unknown-id rejection, read-only-binding rejection, and single-binding echo. ## Verification - `make format` (isort + black) and mypy clean on changed files. - Full MCP suite: **244 passed, 2 skipped** (Redis-version-gated) across unit + integration. ## Stacking This PR targets `feature/raae-1606-search-routing` so its diff stays scoped to upsert routing + write policy. Review/merge bottom-up: [#629](#629) → [#630](#630) → [#631](#631) → this PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code) [RAAE-1606]: https://redislabs.atlassian.net/browse/RAAE-1606?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ [RAAE-1607]: https://redislabs.atlassian.net/browse/RAAE-1607?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes write routing and when the upsert tool is exposed on multi-index servers; mistakes could allow writes to the wrong index or hide/show the tool unexpectedly, though enforcement is fail-closed before backend writes. > > **Overview** > **`upsert-records`** now accepts an optional **`index`** logical binding id (via shared **`resolve_binding`**), matching multi-index **`search-records`**: omit **`index`** when one binding is configured; require it when several exist; reject unknown ids. Successful responses include an **`index`** field naming the binding that was written. > > Write policy is split between **tool advertisement** and **per-call enforcement**. **`upsert-records`** is registered only when at least one binding is writable (**`effective_read_only`** is false for some binding), not merely when global read-only mode is off. Each call still rejects writes to read-only bindings (**`FORBIDDEN`**, before embedding or Redis load) with a clearer message naming the binding. > > The FastMCP wrapper exposes **`index`** as a tool parameter. Unit and integration tests cover routing, registration gating, and read-only rejection. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 98aded7. 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>
## Motivation The RedisVL MCP docs were written for the original single-index model and still stated that "one server process binds to exactly one existing Redis index." With multi-index support now implemented across [RAAE-1604](https://redislabs.atlassian.net/browse/RAAE-1604)–[RAAE-1607](https://redislabs.atlassian.net/browse/RAAE-1607), the user-facing documentation ([RAAE-1608](https://redislabs.atlassian.net/browse/RAAE-1608)) needs to describe the new compatibility story without confusing existing single-index users. The guiding principle in the rewrite is that single-index remains the simplest deployment and works exactly as before — callers never name an index — while multi-index is presented as a formal, additive capability layered on top via discovery (`list-indexes`) and explicit routing (the `index` argument). No documentation still claims a server must bind to exactly one index. ## Implemented changes The concept doc ([concepts/mcp.md](docs/concepts/mcp.md)) now frames the server as binding one *or several* logical indexes, each addressed by an id, and adds an "Index Selection and Discovery" section covering the optional `index` argument, the omitted-index and unknown-id rules, the response echo, and discovery-first guidance. The "Single Index Binding" section becomes "Single and Multiple Index Bindings," the read-only section explains the two-level write policy (global `--read-only` vs per-index `read_only`, folded into effective write availability), and the tool surface gains a `list-indexes` subsection documenting its minimal payload — filterable fields only, vector/embed-source fields omitted, explicit-only limits, and `redis_name` never exposed. The how-to guide ([how_to_guides/mcp.md](docs/user_guide/how_to_guides/mcp.md)) adds a two-binding config example (a writable vector index alongside a read-only fulltext index), an Index Selection subsection, a `list-indexes` tool contract with a response example, and threads the optional `index` argument through the `search-records` and `upsert-records` argument lists and request/response examples. A discovery-first multi-index flow is shown at the top of the search examples, and the CLI/env-var notes are updated for per-index read-only and the multi-index search description. Minor additional changes: - README MCP section and feature-table entry reworded from "an existing Redis index" to "one or more existing Redis indexes," with `list-indexes` and the discovery-first flow noted. (The README edit was explicitly authorized for this ticket, overriding the repo's default no-README-edits rule.) ## Verification - `sphinx-build` (the `docs` dependency group) completes cleanly (exit 0). The only warnings are pre-existing and unrelated (upstream `redis-py` docstrings and `index.md` heading levels); no warnings reference the MCP pages, and no broken cross-reference/anchor warnings were introduced. ## Stacking This PR targets `feature/raae-1607-upsert-routing` and is the top of the stack. Review/merge bottom-up: [#629](#629) → [#630](#630) → [#631](#631) → [#632](#632) → this PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code) [RAAE-1604]: https://redislabs.atlassian.net/browse/RAAE-1604?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ [RAAE-1607]: https://redislabs.atlassian.net/browse/RAAE-1607?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ [RAAE-1608]: https://redislabs.atlassian.net/browse/RAAE-1608?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Markdown-only changes with no runtime, API, or security behavior modified. > > **Overview** > **Documentation-only** update aligning RedisVL MCP user-facing docs with multi-index support: one server can bind **one or several** logical indexes while **single-index deployments stay backward compatible** (omit `index`). > > **README** rewords MCP copy from a single index to one or more, and notes **`list-indexes`**, discovery-first routing, and per-index tools. > > **`docs/concepts/mcp.md`** reframes the model (per-index config, all-or-nothing startup), adds **Index Selection and Discovery** (`index` argument rules, response echo), expands **read-only** to global vs per-index `read_only` and effective `upsert_available`, and documents the **`list-indexes`** payload (minimal fields, no `redis_name`). > > **`docs/user_guide/how_to_guides/mcp.md`** adds a **two-binding YAML example**, **`list-indexes`** contract and examples, threads optional **`index`** through search/upsert docs, a discovery-first flow, and updates CLI/env notes for multi-index search tool descriptions. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit de2eb4f. 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>
🛡️ Jit Security Scan Results✅ No security findings were detected in this PR
Security scan by Jit
|
|
🚀 PR was released in |
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.
Overview
Rolls up the RedisVL MCP multi-index server epic (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 optionalindexargument onsearch-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-indexcollector branch:BindingRuntime, per-binding startup/teardown,resolve_bindingrouting, single global concurrency semaphore)list-indexesdiscovery toolsearch-recordsindex routingupsert-recordsindex routing + per-index write policyEach 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
mainThe branch is up to date with
main, including the recently merged MCP security work — Host/Origin header validation (#643) and the nltk drop (#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_nameis never exposed bylist-indexes.Verification
redis:8.2(now runs without nltk).mypyclean;black/isortclean.indexbehavior, unchanged response contract aside from the additiveindexecho).🤖 Generated with Claude Code
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-indexestool enumerates logical ids, filterable fields,upsert_available, and optional limits (without exposingredis_name).search-recordsandupsert-recordsaccept an optionalindexargument; responses echo the resolved id. With a single binding, omittingindexstill works; with multiple bindings, omitting or using an unknown id returnsinvalid_request.Write policy — Global
--read-only/REDISVL_MCP_READ_ONLYplus per-bindingread_only: truecombine intoeffective_read_only.upsert-recordsis registered only if at least one binding is writable; writes to a read-only binding fail withforbiddenbefore Redis is touched.Implementation — Config drops the “exactly one index” rule and adds
description/read_onlyon bindings. Server state moves to per-bindingBindingRuntimeandresolve_binding(); removed single-index helpers likeget_index(). Search tool descriptions omit per-schema hints when multiple indexes are configured and point clients tolist-indexes.Reviewed by Cursor Bugbot for commit 553e55f. Bugbot is set up for automated code reviews on this repo. Configure here.