# Run RedisVL MCP

```json metadata
{
  "title": "Run RedisVL MCP",
  "description": "",
  "categories": null,
  "tableOfContents": {"sections":[{"id":"before-you-start","title":"Before You Start"},{"children":[{"id":"transport-security-host-origin-validation","title":"Transport Security (Host / Origin Validation)"},{"id":"cli-flags","title":"CLI Flags"},{"id":"environment-variables","title":"Environment Variables"}],"id":"start-the-server","title":"Start the Server"},{"id":"connect-a-remote-mcp-client","title":"Connect a Remote MCP Client"},{"children":[{"id":"what-this-config-means","title":"What This Config Means"},{"id":"fulltext-only-config","title":"Fulltext-Only Config"},{"id":"multiple-indexes","title":"Multiple Indexes"},{"id":"index-selection","title":"Index Selection"}],"id":"example-config","title":"Example Config"},{"children":[{"id":"list-indexes","title":"list-indexes"},{"id":"search-records","title":"search-records"},{"id":"upsert-records","title":"upsert-records"}],"id":"tool-contracts","title":"Tool Contracts"},{"children":[{"id":"discovery-first-multi-index-flow","title":"Discovery-First Multi-Index Flow"},{"id":"read-only-vector-search","title":"Read-Only Vector Search"},{"id":"raw-string-filter","title":"Raw String Filter"},{"id":"json-dsl-filter","title":"JSON DSL Filter"},{"id":"pagination-and-field-projection","title":"Pagination and Field Projection"},{"id":"hybrid-search-with-schemaoverrides","title":"Hybrid Search With schema_overrides"}],"id":"search-examples","title":"Search Examples"},{"children":[{"id":"auto-embed-new-records","title":"Auto-Embed New Records"},{"id":"update-existing-records-with-idfield","title":"Update Existing Records With id_field"},{"id":"control-re-embedding-with-skipembeddingifpresent","title":"Control Re-Embedding With skipembeddingif_present"},{"id":"plain-writes-without-embedding","title":"Plain Writes Without Embedding"}],"id":"upsert-examples","title":"Upsert Examples"},{"children":[{"id":"missing-mcp-dependencies","title":"Missing MCP Dependencies"},{"id":"configured-redis-index-does-not-exist","title":"Configured Redis Index Does Not Exist"},{"id":"missing-required-environment-variables","title":"Missing Required Environment Variables"},{"id":"vectorizer-dimension-mismatch","title":"Vectorizer Dimension Mismatch"},{"id":"hybrid-config-requires-native-runtime-support","title":"Hybrid Config Requires Native Runtime Support"}],"id":"troubleshooting","title":"Troubleshooting"}]}

,
  "codeExamples": []
}
```

This guide shows how to run the RedisVL MCP server against an existing Redis index, configure its behavior, and use the MCP tools it exposes.

For the higher-level design, see [RedisVL MCP](https://redis.io/docs/latest/../../concepts/mcp).

## Before You Start

RedisVL MCP assumes all of the following are already true:

- you have Python 3.10 or newer
- you have Redis with Search capabilities available
- the Redis index already exists
- you know which text field and vector field the server should use
- you have installed the vectorizer provider dependencies your config needs

Install the MCP extra:

```bash
pip install redisvl[mcp]
```

If your vectorizer needs a provider extra, install that too:

```bash
pip install redisvl[mcp,openai]
```

## Start the Server

Run the server over stdio (default):

```bash
uvx --from redisvl[mcp] rvl mcp --config /path/to/mcp.yaml
```

Run it over Streamable HTTP for remote MCP clients. Binding to a non-loopback host (`--host 0.0.0.0`) requires either JWT authentication (see [Authenticate RedisVL MCP](https://redis.io/docs/latest/mcp_authentication)) or the explicit `--allow-unauthenticated` flag; otherwise the server refuses to start:

```bash
uvx --from redisvl[mcp] rvl mcp --config /path/to/mcp.yaml --transport streamable-http --host 0.0.0.0 --port 8000 --allow-unauthenticated
```

Run it over SSE:

```bash
uvx --from redisvl[mcp] rvl mcp --config /path/to/mcp.yaml --transport sse --host 0.0.0.0 --port 9000 --allow-unauthenticated
```


Streamable HTTP and SSE endpoints are **unauthenticated by default**. Binding to a non-loopback host without auth fails closed unless you pass `--allow-unauthenticated`; binding to loopback without auth only warns. For real deployments, enable JWT authentication (see [Authenticate RedisVL MCP](https://redis.io/docs/latest/mcp_authentication)) rather than using `--allow-unauthenticated`. When not using `--read-only`, the `upsert-records` tool is also exposed to any client that can reach the server.


### Transport Security (Host / Origin Validation)

On the HTTP transports the server validates the request `Host` and `Origin` headers against allowlists before any tool runs. This is **on by default** and defends against [DNS rebinding](https://en.wikipedia.org/wiki/DNS_rebinding): without it, a malicious web page could rebind its own hostname to `127.0.0.1` and use the victim’s browser to reach a local MCP server, even one bound to loopback.

- **Host:** the allowlist is derived automatically from the bind address. Loopback binds accept `localhost`, `127.0.0.1`, and `[::1]` (with or without the port), so local MCP clients work with no configuration.
- **Origin:** requests with no `Origin` header (typical of non-browser MCP clients) always pass. A request that carries a cross-site `Origin` is rejected unless that origin is explicitly allowlisted.

You only need to configure this for deployments where the client-visible `Host` differs from the bind address (a reverse proxy, or a `--host 0.0.0.0` bind reached via a public hostname). Use the `REDISVL_MCP_ALLOWED_HOSTS` / `REDISVL_MCP_ALLOWED_ORIGINS` env vars, or a `server.transport_security` block in the config:

```yaml
server:
  redis_url: ${REDIS_URL}
  transport_security:
    allowed_hosts: [mcp.example.com]          # extra Host values to accept
    allowed_origins: [https://app.example.com]  # browser origins to accept
    # allow_any_origin: true                   # trust upstream Origin validation
    # enabled: false                           # disable when a trusted proxy validates
```


Behind a reverse proxy or gateway that already validates `Host`/`Origin`, set `server.transport_security.enabled: false` (or `allow_any_origin: true`) so the proxy’s rewritten headers are not rejected.


Run it in read-only mode to expose search without upsert:

```bash
uvx --from redisvl[mcp] rvl mcp --config /path/to/mcp.yaml --read-only
```

### CLI Flags

| Flag          | Default     | Purpose                                                   |
|---------------|-------------|-----------------------------------------------------------|
| `--config`    | —           | Path to the MCP YAML config (required)                    |
| `--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 writes across every index (global read-only)      |

### Environment Variables

You can also control boot settings through environment variables:

| Variable                                 | Purpose                                                                                                                                                                                                                                                   |
|------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `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. 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                                                                                                                                                                                                                      |
| `REDISVL_MCP_ALLOWED_HOSTS`              | Comma-separated extra `Host` header values to accept on HTTP transports (in addition to the bind-derived defaults)                                                                                                                                        |
| `REDISVL_MCP_ALLOWED_ORIGINS`            | Comma-separated browser `Origin` values to accept on HTTP transports                                                                                                                                                                                      |
| `REDISVL_MCP_ALLOW_ANY_ORIGIN`           | Accept any `Origin` (for trusted-proxy setups) when set to `true`                                                                                                                                                                                         |
| `REDISVL_MCP_TRANSPORT_SECURITY_ENABLED` | Set to `false` to disable Host/Origin validation (e.g. behind a validating proxy)                                                                                                                                                                         |

## Connect a Remote MCP Client

When using Streamable HTTP or SSE transport, point your MCP client at the server URL:

- **Streamable HTTP**: `http://<host>:<port>/mcp`
- **SSE**: `http://<host>:<port>/sse`

**Note:** `<host>` here is the bind address the server was started with. The default `127.0.0.1` only accepts connections from the same machine. To allow connections from other machines, start the server with `--host 0.0.0.0` and use the machine’s actual IP or hostname in the client URL. Because a `0.0.0.0` bind has no single canonical `Host`, add the client-visible hostname(s) to `REDISVL_MCP_ALLOWED_HOSTS` (or `server.transport_security.allowed_hosts`), or Host validation will reject those requests. See [Transport Security]().

For example, to configure a remote MCP client to connect to a Streamable HTTP server running on `192.168.1.10:8000`:

```json
{
  "mcpServers": {
    "redisvl": {
      "url": "http://192.168.1.10:8000/mcp",
      "transport": "streamable-http"
    }
  }
}
```

## Example Config

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]() 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.

```yaml
server:
  redis_url: ${REDIS_URL}

indexes:
  knowledge:
    redis_name: knowledge

    vectorizer:
      class: OpenAITextVectorizer
      model: text-embedding-3-small
      api_config:
        api_key: ${OPENAI_API_KEY}

    schema_overrides:
      fields:
        - name: embedding
          type: vector
          attrs:
            dims: 1536
            datatype: float32

    search:
      type: hybrid
      params:
        text_scorer: BM25STD
        stopwords: english
        vector_search_method: KNN
        combination_method: LINEAR
        linear_text_weight: 0.3

    runtime:
      text_field_name: content
      vector_field_name: embedding
      default_embed_text_field: content
      default_limit: 10
      max_limit: 25
      max_result_window: 1000
      max_upsert_records: 64
      skip_embedding_if_present: true
      startup_timeout_seconds: 30
      request_timeout_seconds: 60
      max_concurrency: 16
```

### What This Config Means

- `redis_name` must point to an index that already exists in Redis
- `search.type` fixes retrieval behavior for every MCP caller
- `runtime.text_field_name` is required for `fulltext` and `hybrid` search
- `runtime.vector_field_name` is required for `vector` and `hybrid` search, and optional for plain full-text deployments
- `runtime.default_embed_text_field` is only required when the server should generate embeddings during upsert
- `vectorizer` is required for query embedding and server-side embedding, but optional for fulltext-only configs
- `runtime.max_result_window` caps deep paging by limiting the maximum `offset + limit`
- `schema_overrides` is only for patching incomplete field attrs discovered from Redis

### Fulltext-Only Config

For a non-vector deployment, omit vector-only settings entirely:

```yaml
server:
  redis_url: ${REDIS_URL}

indexes:
  knowledge:
    redis_name: knowledge

    search:
      type: fulltext
      params:
        text_scorer: BM25STD
        stopwords: english

    runtime:
      text_field_name: content
      default_limit: 10
      max_limit: 25
      max_result_window: 1000
      max_upsert_records: 64
      skip_embedding_if_present: true
      startup_timeout_seconds: 30
      request_timeout_seconds: 60
      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 `forbidden`.
- 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`]() 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`
- `filter`
- `return_fields`

Example request payload:

```json
{
  "index": "knowledge",
  "query": "incident response runbook",
  "limit": 2,
  "offset": 0,
  "filter": {
    "and": [
      { "field": "category", "op": "eq", "value": "operations" },
      { "field": "rating", "op": "gte", "value": 4 }
    ]
  },
  "return_fields": ["title", "content", "category", "rating"]
}
```

Example response payload:

```json
{
  "index": "knowledge",
  "search_type": "hybrid",
  "offset": 0,
  "limit": 2,
  "results": [
    {
      "id": "knowledge:runbook:eu-failover",
      "score": 0.82,
      "score_type": "hybrid_score",
      "record": {
        "title": "EU failover runbook",
        "content": "Restore traffic after a regional failover.",
        "category": "operations",
        "rating": 5
      }
    }
  ]
}
```

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
- `filter` accepts either a raw string or a JSON DSL object
- the `search-records` tool description includes schema-derived hints for typed JSON DSL filter fields, object-filter `exists` support, and valid `return_fields`
- `offset + limit` must stay within `runtime.max_result_window`
- startup rejects schemas that use MCP-reserved score metadata field names:
  `id`, `__key`, `key`, `score`, `vector_distance`, `__score`, `text_score`, `vector_similarity`, `hybrid_score`

### `upsert-records`

Arguments:

- `index` (optional; required when multiple indexes are configured)
- `records`
- `id_field`
- `skip_embedding_if_present`

Example request payload:

```json
{
  "index": "knowledge",
  "records": [
    {
      "doc_id": "doc-42",
      "content": "Updated operational guidance for failover handling.",
      "category": "operations",
      "rating": 5
    }
  ],
  "id_field": "doc_id"
}
```

Example response payload:

```json
{
  "index": "knowledge",
  "status": "success",
  "keys_upserted": 1,
  "keys": ["knowledge:doc-42"]
}
```

Notes:

- `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 `forbidden` 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:

```bash
uvx --from redisvl[mcp] rvl mcp --config /path/to/mcp.yaml --read-only
```

With a `search.type` of `vector`, callers send only the query, filters, pagination, and field projection:

```json
{
  "query": "cache invalidation incident",
  "limit": 3,
  "return_fields": ["title", "content", "category"]
}
```

### Raw String Filter

Pass a raw Redis filter string through unchanged:

```json
{
  "query": "science",
  "filter": "@category:{science}",
  "return_fields": ["content", "category"]
}
```

### JSON DSL Filter

The DSL supports logical operators and type-checked field operators:

```json
{
  "query": "science",
  "filter": {
    "and": [
      { "field": "category", "op": "eq", "value": "science" },
      { "field": "rating", "op": "gte", "value": 4 }
    ]
  },
  "return_fields": ["content", "category", "rating"]
}
```

### Pagination and Field Projection

```json
{
  "query": "science",
  "limit": 1,
  "offset": 1,
  "return_fields": ["content", "category"]
}
```

### Hybrid Search With `schema_overrides`

Use `schema_overrides` when Redis inspection cannot recover complete vector attrs, then keep hybrid behavior in config:

```yaml
schema_overrides:
  fields:
    - name: embedding
      type: vector
      attrs:
        algorithm: flat
        dims: 1536
        datatype: float32
        distance_metric: cosine

search:
  type: hybrid
  params:
    text_scorer: BM25STD
    stopwords: english
    vector_search_method: KNN
    combination_method: LINEAR
    linear_text_weight: 0.3
```

The MCP caller still sends the same request shape:

```json
{
  "query": "legacy cache invalidation flow",
  "filter": { "field": "category", "op": "eq", "value": "release-notes" },
  "return_fields": ["title", "content", "release_version"]
}
```

## Upsert Examples

### Auto-Embed New Records

If a record does not include the configured vector field, RedisVL MCP embeds `runtime.default_embed_text_field` and writes the result:

```json
{
  "records": [
    {
      "content": "First upserted document",
      "category": "science",
      "rating": 5
    },
    {
      "content": "Second upserted document",
      "category": "health",
      "rating": 4
    }
  ]
}
```

### Update Existing Records With `id_field`

```json
{
  "records": [
    {
      "doc_id": "doc-1",
      "content": "Updated content",
      "category": "engineering",
      "rating": 5
    }
  ],
  "id_field": "doc_id"
}
```

### Control Re-Embedding With `skip_embedding_if_present`

```json
{
  "records": [
    {
      "doc_id": "doc-2",
      "content": "Existing content",
      "category": "science",
      "rating": 4
    }
  ],
  "id_field": "doc_id",
  "skip_embedding_if_present": false
}
```

Set `skip_embedding_if_present` to `false` when you want the server to regenerate embeddings during upsert. In most cases, the caller should omit the vector field and let the server manage embeddings from `runtime.default_embed_text_field`.

### Plain Writes Without Embedding

For fulltext-only indexes, `upsert-records` can write records without any vectorizer or vector field configuration:

```json
{
  "records": [
    {
      "content": "Updated FAQ entry",
      "category": "support",
      "rating": 5
    }
  ]
}
```

If you configure a vector field but omit server-side embedding support, the caller must send vectors in each record instead of relying on the server to generate them.

## Troubleshooting

### Missing MCP Dependencies

If `rvl mcp` reports missing optional dependencies, install the MCP extra:

```bash
pip install redisvl[mcp]
```

If the configured vectorizer needs a provider SDK, install that provider extra too. Fulltext-only configs can omit the vectorizer entirely.

### Configured Redis Index Does Not Exist

The server only binds to an existing index. Create the index first, then point `indexes.<id>.redis_name` at that index name.

### Missing Required Environment Variables

YAML values support `${VAR}` and `${VAR:-default}` substitution. Missing required variables fail startup before the server registers tools.

### Vectorizer Dimension Mismatch

If the vectorizer dims do not match the configured vector field dims, startup fails. Make sure the embedding model and the effective vector field dimensions are aligned.

### Hybrid Config Requires Native Runtime Support

Some hybrid params depend on native hybrid support in Redis and redis-py. If your environment does not support that path, remove native-only params such as `knn_ef_runtime` or upgrade Redis and redis-py.

