diff --git a/README.md b/README.md
index 55f790cd..426e7cf0 100644
--- a/README.md
+++ b/README.md
@@ -35,7 +35,7 @@ Perfect for building **RAG pipelines** with real-time retrieval, **AI agents** w
| **[Vector Search](#retrieval)**
*Similarity search with metadata filters* | **[LLM Memory](#llm-memory)**
*Agentic AI context management* | **Async Support**
*Async indexing and search for improved performance* |
| **[Complex Filtering](#retrieval)**
*Combine multiple filter types* | **[Semantic Routing](#semantic-routing)**
*Intelligent query classification* | **[Vectorizers](#vectorizers)**
*8+ embedding provider integrations* |
| **[Hybrid Search](#retrieval)**
*Combine semantic & full-text signals* | **[Embedding Caching](#embedding-caching)**
*Cache embeddings for efficiency* | **[Rerankers](#rerankers)**
*Improve search result relevancy* |
-| | | **[MCP Server](#mcp-server)**
*Expose an existing Redis index to MCP clients* |
+| | | **[MCP Server](#mcp-server)**
*Expose one or more existing Redis indexes to MCP clients* |
@@ -51,7 +51,7 @@ Install `redisvl` into your Python (>=3.10) environment using `pip`:
pip install redisvl
```
-Install the MCP server extra when you want to expose an existing Redis index through MCP:
+Install the MCP server extra when you want to expose one or more existing Redis indexes through MCP:
```bash
pip install redisvl[mcp]
@@ -572,16 +572,18 @@ Use `--read-only` to expose search without upsert.
### MCP Server
-RedisVL includes an MCP server that lets MCP-compatible clients search or upsert data in an existing Redis index through a small, stable tool contract.
+RedisVL includes an MCP server that lets MCP-compatible clients search or upsert data in one or more existing Redis indexes through a small, stable tool contract.
The server:
-- connects to one existing Redis Search index
-- reconstructs the schema from Redis at startup
-- uses the configured vectorizer for query embedding and optional upsert embedding
-- exposes `search-records` and, unless read-only mode is enabled, `upsert-records`
+- connects to one or more existing Redis Search indexes, each addressed by a logical id
+- reconstructs each index's schema from Redis at startup
+- uses each index's configured vectorizer for query embedding and optional upsert embedding
+- exposes `list-indexes` for discovery, `search-records`, and (unless every index is read-only) `upsert-records`
- supports stdio (default), Streamable HTTP, and SSE transports
+A single configured index is the simplest case and works exactly as before — callers omit the index selector. With multiple indexes, clients call `list-indexes` first and pass the chosen `index` to `search-records` and `upsert-records`.
+
Run it over stdio (default):
```bash
diff --git a/docs/concepts/mcp.md b/docs/concepts/mcp.md
index e6d44011..e311c9a5 100644
--- a/docs/concepts/mcp.md
+++ b/docs/concepts/mcp.md
@@ -7,28 +7,30 @@ myst:
# RedisVL MCP
-RedisVL includes an MCP server that exposes a Redis-backed retrieval surface through a small, deterministic tool contract. It is designed for AI applications that want to search or maintain data in an existing Redis index without each client reimplementing Redis query logic.
+RedisVL includes an MCP server that exposes a Redis-backed retrieval surface through a small, deterministic tool contract. It is designed for AI applications that want to search or maintain data in one or more existing Redis indexes without each client reimplementing Redis query logic.
## What RedisVL MCP Does
The RedisVL MCP server sits between an MCP client and Redis:
-1. It connects to an existing Redis Search index.
-2. It inspects that index at startup and reconstructs its schema.
+1. It connects to one or more existing Redis Search indexes.
+2. It inspects each index at startup and reconstructs its schema.
3. It initializes vector capabilities only when the configured search or upsert behavior needs them.
-4. It exposes stable MCP tools for search, and optionally upsert.
+4. It exposes stable MCP tools for discovery, search, and optionally upsert.
-This keeps the Redis index as the source of truth for search behavior while giving MCP clients a predictable interface.
+This keeps each Redis index as the source of truth for its search behavior while giving MCP clients a predictable interface.
## How RedisVL MCP Runs
RedisVL MCP works with a focused model:
-- One server process binds to exactly one existing Redis index.
+- One server process binds to one *or several* existing Redis indexes, each addressed by a logical id.
- The server supports stdio (default), Streamable HTTP, and SSE transports.
-- Search behavior is owned by configuration, not by MCP callers.
-- Vector search and server-side embedding are optional capabilities configured explicitly.
-- Upsert is optional and can be disabled with read-only mode.
+- Search behavior is owned by per-index configuration, not by MCP callers.
+- Vector search and server-side embedding are optional capabilities configured explicitly per index.
+- Upsert is optional and can be disabled globally with read-only mode or per index with a `read_only` flag.
+
+A single-index server remains the simplest deployment: when exactly one index is configured, callers can omit the index selector entirely and every tool call targets that index. Multi-index support is fully formal — it adds discovery and explicit routing without changing the single-index contract.
## Config-Owned Search Behavior
@@ -44,17 +46,30 @@ These request-time controls are still bounded by runtime config. In particular,
deep paging is limited by a configured maximum result window, enforced as
`offset + limit`.
-MCP callers do not choose:
+On a multi-index server, callers also choose **which index to target** through an optional `index` argument (see [Index Selection](#index-selection-and-discovery)). Callers do not choose:
-- which index to target
- whether retrieval is `vector`, `fulltext`, or `hybrid`
- query tuning parameters such as hybrid fusion or vector runtime settings
-That behavior lives in the server config under `indexes..search`. The response includes `search_type` as informational metadata, but it is not a request parameter.
+That behavior lives in the per-index server config under `indexes..search`. The response includes `search_type` as informational metadata, but it is not a request parameter.
+
+## Single and Multiple Index Bindings
+
+The YAML config uses an `indexes` mapping. Each entry is a logical binding keyed by an id (for example `knowledge` or `tickets`) that points to an existing Redis index through `redis_name`. The mapping may contain one entry or several; each binding is inspected, validated, and given its own search config, runtime limits, and optional vectorizer independently at startup. Startup is all-or-nothing — if any binding fails to initialize, the server does not start.
+
+A single-binding config is the simplest case and behaves exactly as before: the lone binding is the implicit target of every call. With multiple bindings the server stays a single process and endpoint, but callers select a binding per call.
+
+## Index Selection and Discovery
+
+On a multi-index server, every tool call must say which logical index it targets:
-## Single Index Binding
+- `search-records` and `upsert-records` accept an optional `index` argument naming the logical id.
+- When exactly one index is configured, `index` may be omitted and resolves to that sole binding (backward compatible).
+- When multiple indexes are configured, omitting `index` is an `invalid_request`; the caller must name one.
+- An unknown logical id is an `invalid_request`.
+- Both tools echo the resolved `index` in their response so clients can confirm routing.
-The YAML config uses an `indexes` mapping with one configured entry. That binding points to an existing Redis index through `redis_name`, and every tool call targets that configured index.
+Because a client cannot guess the configured logical ids, multi-index servers expose a `list-indexes` discovery tool. **Clients should call `list-indexes` first** to enumerate the available indexes and their filterable fields, then pass the chosen id as `index` on subsequent calls.
## Schema Inspection and Overrides
@@ -71,14 +86,16 @@ MCP-reserved score metadata field names for the configured search mode.
## Read-Only and Read-Write Modes
-RedisVL MCP always registers `search-records`.
+RedisVL MCP always registers `search-records` and `list-indexes`.
-`upsert-records` is only registered when the server is not in read-only mode. Read-only mode is controlled by:
+Write availability is enforced at two levels:
-- the CLI flag `--read-only`
-- or the environment variable `REDISVL_MCP_READ_ONLY=true`
+- **Global read-only mode** disables writes across every binding. It is controlled by the CLI flag `--read-only` or the environment variable `REDISVL_MCP_READ_ONLY=true`.
+- **Per-index read-only** disables writes for a single binding via `indexes..read_only: true`, while other bindings stay writable.
-Use read-only mode when Redis is serving approved content to assistants and another system owns ingestion.
+These combine into each binding's *effective* write availability: a binding is read-only if global read-only is on **or** that binding sets `read_only: true`. The `upsert-records` tool is registered only when at least one binding is writable, so a fully read-only server does not advertise it at all. When the tool is registered, a write to a read-only binding is rejected with `invalid_request` before any data is changed. `list-indexes` reports each binding's effective write availability as `upsert_available`.
+
+Use read-only mode when Redis is serving approved content to assistants and another system owns ingestion — globally when no binding should accept writes, or per index when only some indexes are writable.
## Authentication and Authorization
@@ -88,18 +105,36 @@ For configuration and the gateway boundary, see {doc}`/user_guide/how_to_guides/
## Tool Surface
-RedisVL MCP exposes two tools:
+RedisVL MCP exposes up to three tools:
-- `search-records` searches the configured index using the server-owned search mode
-- `upsert-records` validates and upserts records, embedding them only when that capability is configured
+- `list-indexes` enumerates the configured logical indexes for discovery (always available)
+- `search-records` searches a selected index using that index's server-owned search mode
+- `upsert-records` validates and upserts records into a selected writable index, embedding them only when that capability is configured
These tools follow a stable contract:
- request validation happens before query or write execution
+- the resolved logical `index` is echoed in every `search-records` and `upsert-records` response
- filters support either raw strings or a RedisVL-backed JSON DSL
-- `search-records` describes the inspected schema by advertising typed JSON DSL filter fields, object-filter `exists` support, and valid `return_fields`
+- on a single-index server, `search-records` describes the inspected schema by advertising typed JSON DSL filter fields, object-filter `exists` support, and valid `return_fields`; on a multi-index server those hints are ambiguous, so the description instead directs clients to call `list-indexes` and pass `index`
- error codes are mapped into a stable set of MCP-facing categories
+### `list-indexes`
+
+`list-indexes` returns one entry per configured binding so clients can route subsequent calls. Each entry reports:
+
+- the logical `id`
+- an optional `description` (only when configured)
+- `upsert_available`, reflecting the binding's effective write availability
+- `fields`, the filterable fields discovered from the index
+- `limits`, only the runtime limits that were explicitly configured
+
+The discovery payload is deliberately minimal:
+
+- the underlying Redis index name (`redis_name`) is **never** exposed
+- the vector field and the configured embed-source text field are **omitted** from `fields`, since they are implementation inputs rather than fields a client filters on
+- `limits` shows only explicitly set values (such as `max_limit` or `max_upsert_records`); defaults are not echoed
+
## Why Use MCP Instead of Direct RedisVL Calls
Use RedisVL MCP when you want a standard tool boundary for agent frameworks or assistants that already speak MCP.
diff --git a/docs/user_guide/how_to_guides/mcp.md b/docs/user_guide/how_to_guides/mcp.md
index b0a4132b..206302ee 100644
--- a/docs/user_guide/how_to_guides/mcp.md
+++ b/docs/user_guide/how_to_guides/mcp.md
@@ -71,7 +71,7 @@ uvx --from redisvl[mcp] rvl mcp --config /path/to/mcp.yaml --read-only
| `--transport` | `stdio` | Transport protocol: `stdio`, `sse`, or `streamable-http` |
| `--host` | `127.0.0.1` | Bind address (only used with `sse` and `streamable-http`) |
| `--port` | `8000` | Bind port (only used with `sse` and `streamable-http`) |
-| `--read-only` | off | Disable the `upsert-records` tool |
+| `--read-only` | off | Disable writes across every index (global read-only) |
### Environment Variables
@@ -81,7 +81,7 @@ You can also control boot settings through environment variables:
|----------|---------|
| `REDISVL_MCP_CONFIG` | Path to the MCP YAML config |
| `REDISVL_MCP_READ_ONLY` | Disable `upsert-records` when set to `true` |
-| `REDISVL_MCP_TOOL_SEARCH_DESCRIPTION` | Set the base search tool description text; RedisVL still appends schema-derived typed filter, `exists`, and `return_fields` hints |
+| `REDISVL_MCP_TOOL_SEARCH_DESCRIPTION` | Set the base search tool description text. On a single-index server RedisVL appends schema-derived typed filter, `exists`, and `return_fields` hints; on a multi-index server it appends a note directing clients to call `list-indexes` and pass `index` |
| `REDISVL_MCP_TOOL_UPSERT_DESCRIPTION` | Override the upsert tool description |
## Connect a Remote MCP Client
@@ -108,7 +108,7 @@ For example, to configure a remote MCP client to connect to a Streamable HTTP se
## Example Config
-This example binds one logical MCP server to one existing Redis index called `knowledge`.
+This example binds one logical MCP server to one existing Redis index called `knowledge`. A single configured index is the simplest deployment, and callers never need to name it. See [Multiple Indexes](#multiple-indexes) below to expose several indexes from the same server.
The config uses `${REDIS_URL}` and `${OPENAI_API_KEY}` as environment-variable placeholders. These values are resolved when the server starts. You can also use `${VAR:-default}` to provide a fallback value.
@@ -198,14 +198,117 @@ indexes:
max_concurrency: 16
```
+### Multiple Indexes
+
+The `indexes` mapping can hold more than one binding. Each entry is keyed by a logical id, points at its own existing Redis index through `redis_name`, and carries its own `search`, `runtime`, and optional `vectorizer`. The example below exposes a writable vector index `knowledge` alongside a read-only fulltext index `tickets` from the same server:
+
+```yaml
+server:
+ redis_url: ${REDIS_URL}
+
+indexes:
+ knowledge:
+ redis_name: knowledge
+ description: Internal runbooks and operational guidance.
+
+ vectorizer:
+ class: OpenAITextVectorizer
+ model: text-embedding-3-small
+ api_config:
+ api_key: ${OPENAI_API_KEY}
+
+ search:
+ type: vector
+
+ runtime:
+ text_field_name: content
+ vector_field_name: embedding
+ default_embed_text_field: content
+ default_limit: 10
+ max_limit: 25
+
+ tickets:
+ redis_name: support-tickets
+ description: Read-only mirror of resolved support tickets.
+ read_only: true
+
+ search:
+ type: fulltext
+ params:
+ text_scorer: BM25STD
+ stopwords: english
+
+ runtime:
+ text_field_name: body
+ default_limit: 10
+ max_limit: 50
+```
+
+Notes:
+
+- Each binding is inspected and validated independently at startup. Startup is all-or-nothing: if any binding fails, the server does not start.
+- The optional per-index `description` and `read_only` flags are surfaced through `list-indexes`.
+- `read_only: true` makes that binding reject writes even though the server as a whole is not in global read-only mode. Because `knowledge` is still writable, the `upsert-records` tool is registered; an upsert targeting `tickets` is rejected with `invalid_request`.
+- A single-index config keeps working unchanged — adding bindings does not change how the sole-binding case behaves.
+
+### Index Selection
+
+On a multi-index server, `search-records` and `upsert-records` take an optional `index` argument naming the logical id to target:
+
+- With exactly one index configured, `index` may be omitted and resolves to that binding.
+- With multiple indexes configured, omitting `index` returns `invalid_request`; an unknown id also returns `invalid_request`.
+- Clients should call [`list-indexes`](#list-indexes) first to discover the available ids and their filterable fields.
+
## Tool Contracts
RedisVL MCP exposes a small, implementation-owned contract.
+### `list-indexes`
+
+`list-indexes` is always available and takes no arguments. Call it first on a multi-index server to discover which logical ids exist and how to filter each one.
+
+Example response payload:
+
+```json
+{
+ "indexes": [
+ {
+ "id": "knowledge",
+ "description": "Internal runbooks and operational guidance.",
+ "upsert_available": true,
+ "fields": [
+ { "name": "title", "type": "text" },
+ { "name": "category", "type": "tag" },
+ { "name": "rating", "type": "numeric" }
+ ],
+ "limits": { "max_limit": 25 }
+ },
+ {
+ "id": "tickets",
+ "description": "Read-only mirror of resolved support tickets.",
+ "upsert_available": false,
+ "fields": [
+ { "name": "category", "type": "tag" }
+ ],
+ "limits": { "max_limit": 50 }
+ }
+ ]
+}
+```
+
+Notes:
+
+- `upsert_available` reflects the binding's effective write availability (global read-only **or** the per-index `read_only` flag)
+- `fields` lists the filterable fields discovered from the index; the vector field and the configured embed-source text field are intentionally omitted
+- `limits` includes only runtime limits that were explicitly configured (such as `max_limit` or `max_upsert_records`); defaults are not echoed
+- the underlying Redis index name (`redis_name`) is never exposed
+- `description` appears only when configured for that binding
+
### `search-records`
Arguments:
+- `index` (optional; required when multiple indexes are configured)
- `query`
- `limit`
- `offset`
@@ -216,6 +319,7 @@ Example request payload:
```json
{
+ "index": "knowledge",
"query": "incident response runbook",
"limit": 2,
"offset": 0,
@@ -233,6 +337,7 @@ Example response payload:
```json
{
+ "index": "knowledge",
"search_type": "hybrid",
"offset": 0,
"limit": 2,
@@ -254,6 +359,7 @@ Example response payload:
Notes:
+- `index` selects the logical binding; omit it only on a single-index server. The resolved id is echoed back in the response
- `search_type` is response metadata, not a request argument
- when `return_fields` is omitted, RedisVL MCP returns all non-vector fields
- returning the configured vector field is rejected
@@ -267,6 +373,7 @@ Notes:
Arguments:
+- `index` (optional; required when multiple indexes are configured)
- `records`
- `id_field`
- `skip_embedding_if_present`
@@ -275,6 +382,7 @@ Example request payload:
```json
{
+ "index": "knowledge",
"records": [
{
"doc_id": "doc-42",
@@ -291,6 +399,7 @@ Example response payload:
```json
{
+ "index": "knowledge",
"status": "success",
"keys_upserted": 1,
"keys": ["knowledge:doc-42"]
@@ -299,13 +408,42 @@ Example response payload:
Notes:
-- this tool is not registered in read-only mode
+- `index` selects the logical binding; omit it only on a single-index server. The resolved id is echoed back in the response
+- this tool is not registered when every binding is read-only (global read-only mode or every binding setting `read_only: true`)
+- a write targeting a read-only binding is rejected with `invalid_request` before any data is changed, even when the tool is registered because other bindings are writable
- when server-side embedding is configured, records that need embedding must contain `runtime.default_embed_text_field`
- when `skip_embedding_if_present` is `true`, records that already contain the configured vector field can skip re-embedding
- when a vector field is configured but server-side embedding is disabled, callers must supply vectors explicitly
## Search Examples
+### Discovery-First Multi-Index Flow
+
+On a multi-index server, call `list-indexes` first, pick a logical id from the response, then pass it as `index`:
+
+```json
+{
+ "index": "knowledge",
+ "query": "cache invalidation incident",
+ "limit": 3,
+ "return_fields": ["title", "content", "category"]
+}
+```
+
+The same `index` argument routes an `upsert-records` write to a specific binding:
+
+```json
+{
+ "index": "knowledge",
+ "records": [
+ { "doc_id": "doc-7", "content": "New runbook entry", "category": "operations" }
+ ],
+ "id_field": "doc_id"
+}
+```
+
+On a single-index server you can omit `index` entirely; the examples below show that backward-compatible shape.
+
### Read-Only Vector Search
Use read-only mode when assistants should only retrieve data: