Commit f80e98f
feat(mcp): support multiple active index bindings (RAAE-1604) (#629)
## 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 -->
---
### 1 parent 80fa258 commit f80e98f
14 files changed
Lines changed: 885 additions & 371 deletions
File tree
- redisvl/mcp
- tools
- tests
- integration/test_mcp
- unit/test_mcp
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
282 | 282 | | |
283 | 283 | | |
284 | 284 | | |
285 | | - | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
286 | 291 | | |
287 | 292 | | |
| 293 | + | |
| 294 | + | |
288 | 295 | | |
289 | 296 | | |
290 | 297 | | |
| |||
355 | 362 | | |
356 | 363 | | |
357 | 364 | | |
358 | | - | |
359 | | - | |
360 | | - | |
361 | | - | |
362 | | - | |
363 | | - | |
364 | | - | |
365 | | - | |
366 | | - | |
367 | | - | |
368 | | - | |
369 | | - | |
370 | | - | |
371 | | - | |
372 | | - | |
373 | | - | |
374 | | - | |
375 | | - | |
376 | | - | |
377 | | - | |
378 | | - | |
379 | | - | |
380 | | - | |
381 | | - | |
382 | | - | |
383 | | - | |
384 | | - | |
385 | | - | |
386 | | - | |
387 | | - | |
388 | | - | |
389 | | - | |
390 | | - | |
391 | | - | |
392 | | - | |
393 | | - | |
394 | | - | |
395 | | - | |
396 | | - | |
397 | | - | |
398 | | - | |
399 | | - | |
400 | | - | |
401 | | - | |
402 | | - | |
403 | | - | |
404 | | - | |
405 | | - | |
406 | | - | |
407 | | - | |
408 | | - | |
409 | | - | |
410 | | - | |
411 | | - | |
412 | | - | |
413 | | - | |
414 | | - | |
415 | | - | |
416 | | - | |
417 | | - | |
418 | | - | |
419 | | - | |
420 | | - | |
421 | | - | |
422 | | - | |
423 | | - | |
424 | | - | |
425 | | - | |
426 | | - | |
427 | | - | |
428 | | - | |
429 | | - | |
430 | | - | |
431 | | - | |
432 | | - | |
| 365 | + | |
433 | 366 | | |
434 | | - | |
| 367 | + | |
435 | 368 | | |
436 | 369 | | |
437 | 370 | | |
| |||
478 | 411 | | |
479 | 412 | | |
480 | 413 | | |
481 | | - | |
| 414 | + | |
482 | 415 | | |
483 | 416 | | |
484 | 417 | | |
| |||
575 | 508 | | |
576 | 509 | | |
577 | 510 | | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
578 | 534 | | |
579 | 535 | | |
580 | 536 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
0 commit comments