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: