Run RedisVL MCP#

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.

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:

pip install redisvl[mcp]

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

pip install redisvl[mcp,openai]

Start the Server#

Run the server over stdio (default):

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) or the explicit --allow-unauthenticated flag; otherwise the server refuses to start:

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:

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

Warning

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) 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: 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:

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

Note

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:

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:

{
  "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.

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:

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:

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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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.

Raw String Filter#

Pass a raw Redis filter string through unchanged:

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

JSON DSL Filter#

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

{
  "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#

{
  "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:

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:

{
  "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:

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

Update Existing Records With id_field#

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

Control Re-Embedding With skip_embedding_if_present#

{
  "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:

{
  "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:

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.