{
  "schema_version": 2,
  "id": "develop/ai/redisvl/user_guide/how_to_guides/mcp",
  "title": "Run RedisVL MCP",
  "url": "https://redis.io/docs/latest/develop/ai/redisvl/user_guide/how_to_guides/mcp/",
  "summary": "",
  "aliases": [
    "/integrate/redisvl/user_guide/how_to_guides/mcp"
  ],
  "tags": [],
  "last_updated": "2026-10-12T09:34:21+02:00",
  "page_type": "content",
  "content_hash": "1bb3ab12f883b5c55ac4cb81eadeefcd97018cdcbb5ae0792f117981f36b3ffb",
  "sections": [
    {
      "id": "overview",
      "title": "Overview",
      "role": "overview",
      "text": "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.\n\nFor the higher-level design, see [RedisVL MCP](https://redis.io/docs/latest/../../concepts/mcp)."
    },
    {
      "id": "before-you-start",
      "title": "Before You Start",
      "role": "content",
      "text": "RedisVL MCP assumes all of the following are already true:\n\n- you have Python 3.10 or newer\n- you have Redis with Search capabilities available\n- the Redis index already exists\n- you know which text field and vector field the server should use\n- you have installed the vectorizer provider dependencies your config needs\n\nInstall the MCP extra:\n\n[code example]\n\nIf your vectorizer needs a provider extra, install that too:\n\n[code example]"
    },
    {
      "id": "start-the-server",
      "title": "Start the Server",
      "role": "content",
      "text": "Run the server over stdio (default):\n\n[code example]\n\nRun 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:\n\n[code example]\n\nRun it over SSE:\n\n[code example]\n\n> [!WARNING]\n> 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."
    },
    {
      "id": "transport-security-host--origin-validation",
      "title": "Transport Security (Host / Origin Validation)",
      "role": "content",
      "text": "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.\n\n- **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.\n- **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.\n\nYou 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:\n\n[code example]\n\n> [!NOTE]\n> 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.\n\nRun it in read-only mode to expose search without upsert:\n\n[code example]"
    },
    {
      "id": "cli-flags",
      "title": "CLI Flags",
      "role": "content",
      "text": "| Flag          | Default     | Purpose                                                   |\n|---------------|-------------|-----------------------------------------------------------|\n| `--config`    | —           | Path to the MCP YAML config (required)                    |\n| `--transport` | `stdio`     | Transport protocol: `stdio`, `sse`, or `streamable-http`  |\n| `--host`      | `127.0.0.1` | Bind address (only used with `sse` and `streamable-http`) |\n| `--port`      | `8000`      | Bind port (only used with `sse` and `streamable-http`)    |\n| `--read-only` | off         | Disable writes across every index (global read-only)      |"
    },
    {
      "id": "environment-variables",
      "title": "Environment Variables",
      "role": "content",
      "text": "You can also control boot settings through environment variables:\n\n| Variable                                 | Purpose                                                                                                                                                                                                                                                   |\n|------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `REDISVL_MCP_CONFIG`                     | Path to the MCP YAML config                                                                                                                                                                                                                               |\n| `REDISVL_MCP_READ_ONLY`                  | Disable `upsert-records` when set to `true`                                                                                                                                                                                                               |\n| `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` |\n| `REDISVL_MCP_TOOL_UPSERT_DESCRIPTION`    | Override the upsert tool description                                                                                                                                                                                                                      |\n| `REDISVL_MCP_ALLOWED_HOSTS`              | Comma-separated extra `Host` header values to accept on HTTP transports (in addition to the bind-derived defaults)                                                                                                                                        |\n| `REDISVL_MCP_ALLOWED_ORIGINS`            | Comma-separated browser `Origin` values to accept on HTTP transports                                                                                                                                                                                      |\n| `REDISVL_MCP_ALLOW_ANY_ORIGIN`           | Accept any `Origin` (for trusted-proxy setups) when set to `true`                                                                                                                                                                                         |\n| `REDISVL_MCP_TRANSPORT_SECURITY_ENABLED` | Set to `false` to disable Host/Origin validation (e.g. behind a validating proxy)                                                                                                                                                                         |"
    },
    {
      "id": "connect-a-remote-mcp-client",
      "title": "Connect a Remote MCP Client",
      "role": "content",
      "text": "When using Streamable HTTP or SSE transport, point your MCP client at the server URL:\n\n- **Streamable HTTP**: `http://<host>:<port>/mcp`\n- **SSE**: `http://<host>:<port>/sse`\n\n**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]().\n\nFor example, to configure a remote MCP client to connect to a Streamable HTTP server running on `192.168.1.10:8000`:\n\n[code example]"
    },
    {
      "id": "example-config",
      "title": "Example Config",
      "role": "example",
      "text": "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.\n\nThe 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.\n\n[code example]"
    },
    {
      "id": "what-this-config-means",
      "title": "What This Config Means",
      "role": "content",
      "text": "- `redis_name` must point to an index that already exists in Redis\n- `search.type` fixes retrieval behavior for every MCP caller\n- `runtime.text_field_name` is required for `fulltext` and `hybrid` search\n- `runtime.vector_field_name` is required for `vector` and `hybrid` search, and optional for plain full-text deployments\n- `runtime.default_embed_text_field` is only required when the server should generate embeddings during upsert\n- `vectorizer` is required for query embedding and server-side embedding, but optional for fulltext-only configs\n- `runtime.max_result_window` caps deep paging by limiting the maximum `offset + limit`\n- `schema_overrides` is only for patching incomplete field attrs discovered from Redis"
    },
    {
      "id": "fulltext-only-config",
      "title": "Fulltext-Only Config",
      "role": "content",
      "text": "For a non-vector deployment, omit vector-only settings entirely:\n\n[code example]"
    },
    {
      "id": "multiple-indexes",
      "title": "Multiple Indexes",
      "role": "content",
      "text": "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:\n\n[code example]\n\nNotes:\n\n- Each binding is inspected and validated independently at startup. Startup is all-or-nothing: if any binding fails, the server does not start.\n- The optional per-index `description` and `read_only` flags are surfaced through `list-indexes`.\n- `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`.\n- A single-index config keeps working unchanged — adding bindings does not change how the sole-binding case behaves."
    },
    {
      "id": "index-selection",
      "title": "Index Selection",
      "role": "content",
      "text": "On a multi-index server, `search-records` and `upsert-records` take an optional `index` argument naming the logical id to target:\n\n- With exactly one index configured, `index` may be omitted and resolves to that binding.\n- With multiple indexes configured, omitting `index` returns `invalid_request`; an unknown id also returns `invalid_request`.\n- Clients should call [`list-indexes`]() first to discover the available ids and their filterable fields."
    },
    {
      "id": "custom-tool-profiles",
      "title": "Custom Tool Profiles",
      "role": "content",
      "text": "A **profile** publishes `search-records` under your own name and description with some arguments pre-filled and frozen. It is pure config — no Python — and it resolves to a built-in call, so it inherits the same limits, read-only policy, auth scoping, and error contract.\n\n[code example]\n\nWhat the client sees for `search-resolved-tickets`:\n\n- `query` (required), `limit` (≤ 10), `offset`, and `filter` — and nothing else.\n- No `index` argument: the binding is pinned by `index:` in the config.\n- No `return_fields` argument: locking a projection removes it from the contract.\n- `filter` accepts the **object** form only. A raw string filter is refused — by the advertised schema for a compliant client, and by the tool itself otherwise.\n\nRules worth knowing:\n\n- **Anything not listed under `params` stays exposed.** Locking only a filter keeps the rest of the built-in’s contract intact.\n- **Locked and caller filters are AND-combined.** The caller can narrow within the locked scope but never widen past it — an `or` from the caller nests inside the locked AND rather than replacing it.\n- **`params.limit.max` bounds the result count either way.** An omitted `limit` is capped rather than falling through to `default_limit`; an explicit request above the cap returns `invalid_request`. With `expose: false`, the cap becomes the fixed result count. The cap may not exceed the binding’s `max_limit`. It bounds page size, not total reachable data — `offset` still pages within `max_result_window`.\n- **`suppress_schema_hints: true`** drops the auto-generated filter/return-field hints from the description. By default they are appended only for arguments the model can still use.\n- **`index` may be omitted only with exactly one index configured.**\n\n`builtin_tools` is independent of profiles: use it to stop advertising any built-in, for example `upsert-records: disabled` on a server that should only read. Disabling `list-indexes` on a multi-index server logs a warning, because `search-records` tells clients to call it to discover index ids.\n\nMisconfiguration fails at startup, not at the first call — a name colliding with a built-in or using a reserved `redisvl-`/`redisvl_` prefix, a duplicate name, a missing or unknown `index`, a cap above `max_limit`, hiding `query`, locking `return_fields` while also exposing them, or a locked filter or projection naming a field the index does not have. Unrecognized keys are rejected too, so a typo in `lock` fails loudly instead of quietly producing a tool that reads as locked but enforces nothing."
    },
    {
      "id": "tenant-scoping-with-claim-injection",
      "title": "Tenant Scoping With Claim Injection",
      "role": "content",
      "text": "When tenants share one index, a profile can take the tenant from the caller’s verified token instead of trusting the model to pass a filter. This needs authentication, so the example configures it; see [Authenticate RedisVL MCP](https://redis.io/docs/latest/mcp_authentication) for the rest of that block.\n\nThe index needs the tenant on every document, as a tag. Declare it `CASESENSITIVE` unless your identity provider guarantees one case, because tag fields otherwise treat `Acme` and `acme` as the same tenant:\n\n[code example]\n\nThen point a profile at it:\n\n[code example]\n\nServe it over HTTP:\n\n[code example]\n\nWhat the client sees for `search-customer-kb`:\n\n- `query` (required), `limit`, `offset`, `filter` and `return_fields`, with no argument for `org_id`. A call that passes `org_id` anyway is rejected.\n- A description whose field hints list `content` but not `org_id`.\n- Results from its own tenant only. A `filter` naming `org_id` ANDs with the injected value, so naming another tenant returns nothing. Results still carry `org_id`, always with the caller’s own value, unless you lock `return_fields` to leave it out.\n\nThe server refuses to start while anything else can reach the same Redis index without the tenant scope: `search-records`, `upsert-records` unless every binding over the index is `read_only`, or another custom tool on that index whose `lock.inject` differs, including one with none. Bindings that share a `redis_name` are the same index, so a vector tool and a fulltext tool over one set of documents must inject the same scope. Each of those routes would hand every caller a way round the profile, and a write could retag another tenant’s document as the writer’s own. Ingest documents outside the server, stamping `org_id` exactly as the identity provider emits it.\n\nA tool meant to read across tenants, such as an internal support search, belongs on a separate server with its own authentication. Every tool on one server sits behind the same read scope, so any tenant could call it.\n\nListing the tenant claim under `required_claims` makes the verifier reject a token without it before any tool runs. That checks presence only: the profile still validates the value on every call, and refuses a missing, empty, list-valued or otherwise unusable claim with a `forbidden` error before any query runs.\n\nThe server refuses to start an injecting profile without authentication, over `stdio`, or on a field the index does not hold as an indexed tag. The `stdio` check applies when the server starts through `rvl mcp` or `run_async`. For what the guarantee covers and what it rests on, read the threat model in [RedisVL MCP](https://redis.io/docs/latest/../../concepts/mcp)."
    },
    {
      "id": "tool-contracts",
      "title": "Tool Contracts",
      "role": "content",
      "text": "RedisVL MCP exposes a small, implementation-owned contract."
    },
    {
      "id": "list-indexes",
      "title": "`list-indexes`",
      "role": "content",
      "text": "`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.\n\nExample response payload:\n\n[code example]\n\nNotes:\n\n- `upsert_available` reflects the binding’s effective write availability (global read-only **or** the per-index `read_only` flag)\n- `fields` lists the filterable fields discovered from the index; the vector field and the configured embed-source text field are intentionally omitted\n- `limits` includes only runtime limits that were explicitly configured (such as `max_limit` or `max_upsert_records`); defaults are not echoed\n- the underlying Redis index name (`redis_name`) is never exposed\n- `description` appears only when configured for that binding"
    },
    {
      "id": "search-records",
      "title": "`search-records`",
      "role": "content",
      "text": "Arguments:\n\n- `index` (optional; required when multiple indexes are configured)\n- `query`\n- `limit`\n- `offset`\n- `filter`\n- `return_fields`\n\nExample request payload:\n\n[code example]\n\nExample response payload:\n\n[code example]\n\nNotes:\n\n- `index` selects the logical binding; omit it only on a single-index server. The resolved id is echoed back in the response\n- `search_type` is response metadata, not a request argument\n- when `return_fields` is omitted, RedisVL MCP returns all non-vector fields\n- returning the configured vector field is rejected\n- `filter` accepts either a raw string or a JSON DSL object\n- the `search-records` tool description includes schema-derived hints for typed JSON DSL filter fields, object-filter `exists` support, and valid `return_fields`\n- `offset + limit` must stay within `runtime.max_result_window`\n- startup rejects schemas that use MCP-reserved score metadata field names:\n  `id`, `__key`, `key`, `score`, `vector_distance`, `__score`, `text_score`, `vector_similarity`, `hybrid_score`"
    },
    {
      "id": "upsert-records",
      "title": "`upsert-records`",
      "role": "content",
      "text": "Arguments:\n\n- `index` (optional; required when multiple indexes are configured)\n- `records`\n- `id_field`\n- `skip_embedding_if_present`\n\nExample request payload:\n\n[code example]\n\nExample response payload:\n\n[code example]\n\nNotes:\n\n- `index` selects the logical binding; omit it only on a single-index server. The resolved id is echoed back in the response\n- this tool is not registered when every binding is read-only (global read-only mode or every binding setting `read_only: true`)\n- 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\n- when server-side embedding is configured, records that need embedding must contain `runtime.default_embed_text_field`\n- when `skip_embedding_if_present` is `true`, records that already contain the configured vector field can skip re-embedding\n- when a vector field is configured but server-side embedding is disabled, callers must supply vectors explicitly"
    },
    {
      "id": "search-examples",
      "title": "Search Examples",
      "role": "content",
      "text": ""
    },
    {
      "id": "discovery-first-multi-index-flow",
      "title": "Discovery-First Multi-Index Flow",
      "role": "content",
      "text": "On a multi-index server, call `list-indexes` first, pick a logical id from the response, then pass it as `index`:\n\n[code example]\n\nThe same `index` argument routes an `upsert-records` write to a specific binding:\n\n[code example]\n\nOn a single-index server you can omit `index` entirely; the examples below show that backward-compatible shape."
    },
    {
      "id": "read-only-vector-search",
      "title": "Read-Only Vector Search",
      "role": "content",
      "text": "Use read-only mode when assistants should only retrieve data:\n\n[code example]\n\nWith a `search.type` of `vector`, callers send only the query, filters, pagination, and field projection:\n\n[code example]"
    },
    {
      "id": "raw-string-filter",
      "title": "Raw String Filter",
      "role": "content",
      "text": "Pass a raw Redis filter string through unchanged:\n\n[code example]"
    },
    {
      "id": "json-dsl-filter",
      "title": "JSON DSL Filter",
      "role": "content",
      "text": "The DSL supports logical operators and type-checked field operators:\n\n[code example]"
    },
    {
      "id": "pagination-and-field-projection",
      "title": "Pagination and Field Projection",
      "role": "content",
      "text": "[code example]"
    },
    {
      "id": "hybrid-search-with-schema_overrides",
      "title": "Hybrid Search With `schema_overrides`",
      "role": "content",
      "text": "Use `schema_overrides` when Redis inspection cannot recover complete vector attrs, then keep hybrid behavior in config:\n\n[code example]\n\nThe MCP caller still sends the same request shape:\n\n[code example]"
    },
    {
      "id": "upsert-examples",
      "title": "Upsert Examples",
      "role": "content",
      "text": ""
    },
    {
      "id": "auto-embed-new-records",
      "title": "Auto-Embed New Records",
      "role": "content",
      "text": "If a record does not include the configured vector field, RedisVL MCP embeds `runtime.default_embed_text_field` and writes the result:\n\n[code example]"
    },
    {
      "id": "update-existing-records-with-id_field",
      "title": "Update Existing Records With `id_field`",
      "role": "content",
      "text": "[code example]"
    },
    {
      "id": "control-re-embedding-with-skip_embedding_if_present",
      "title": "Control Re-Embedding With `skip_embedding_if_present`",
      "role": "content",
      "text": "[code example]\n\nSet `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`."
    },
    {
      "id": "plain-writes-without-embedding",
      "title": "Plain Writes Without Embedding",
      "role": "content",
      "text": "For fulltext-only indexes, `upsert-records` can write records without any vectorizer or vector field configuration:\n\n[code example]\n\nIf 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."
    },
    {
      "id": "troubleshooting",
      "title": "Troubleshooting",
      "role": "errors",
      "text": ""
    },
    {
      "id": "missing-mcp-dependencies",
      "title": "Missing MCP Dependencies",
      "role": "content",
      "text": "If `rvl mcp` reports missing optional dependencies, install the MCP extra:\n\n[code example]\n\nIf the configured vectorizer needs a provider SDK, install that provider extra too. Fulltext-only configs can omit the vectorizer entirely."
    },
    {
      "id": "configured-redis-index-does-not-exist",
      "title": "Configured Redis Index Does Not Exist",
      "role": "configuration",
      "text": "The server only binds to an existing index. Create the index first, then point `indexes.<id>.redis_name` at that index name."
    },
    {
      "id": "missing-required-environment-variables",
      "title": "Missing Required Environment Variables",
      "role": "content",
      "text": "YAML values support `${VAR}` and `${VAR:-default}` substitution. Missing required variables fail startup before the server registers tools."
    },
    {
      "id": "vectorizer-dimension-mismatch",
      "title": "Vectorizer Dimension Mismatch",
      "role": "content",
      "text": "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."
    },
    {
      "id": "hybrid-config-requires-native-runtime-support",
      "title": "Hybrid Config Requires Native Runtime Support",
      "role": "content",
      "text": "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."
    },
    {
      "id": "claim-injection-requires-authentication",
      "title": "Claim Injection Requires Authentication",
      "role": "content",
      "text": "A profile with `lock.inject` refuses to start when authentication is not enabled, or when the server runs over `stdio`, because neither can supply a verified token. Configure `server.auth` and serve over `sse` or `streamable-http`. The check runs before the server connects to Redis, so it reports even when Redis is unreachable."
    },
    {
      "id": "claim-injection-refuses-an-unscoped-route",
      "title": "Claim Injection Refuses an Unscoped Route",
      "role": "content",
      "text": "A profile with `lock.inject` refuses to start while its Redis index is also reachable through `search-records`, through `upsert-records` on any writable binding over it, or through another custom tool on it whose `lock.inject` differs, including one with none. Bindings that share a `redis_name` are the same index. The error names the Redis index and the bindings over it, each route it found, and each tool’s injected scope. Disable the built-ins under `server.builtin_tools`, mark every binding over the index `read_only`, and give every custom tool on the index the same `lock.inject`. Move a tool meant to read across tenants to a separate server with its own authentication."
    },
    {
      "id": "claim-injection-fails-every-request-with-forbidden",
      "title": "Claim Injection Fails Every Request With `forbidden`",
      "role": "content",
      "text": "The token is verified but its claim is unusable: missing, empty, padded with whitespace, not a single string, or containing a control character or backtick. The error names the claim and the tool. A misspelled `claim` name in the config is the usual cause, since a JWT claim name such as `https://acme.example/org` must match exactly."
    }
  ],
  "examples": [
    {
      "id": "before-you-start-ex0",
      "language": "bash",
      "code": "pip install redisvl[mcp]",
      "section_id": "before-you-start"
    },
    {
      "id": "before-you-start-ex1",
      "language": "bash",
      "code": "pip install redisvl[mcp,openai]",
      "section_id": "before-you-start"
    },
    {
      "id": "start-the-server-ex0",
      "language": "bash",
      "code": "uvx --from redisvl[mcp] rvl mcp --config /path/to/mcp.yaml",
      "section_id": "start-the-server"
    },
    {
      "id": "start-the-server-ex1",
      "language": "bash",
      "code": "uvx --from redisvl[mcp] rvl mcp --config /path/to/mcp.yaml --transport streamable-http --host 0.0.0.0 --port 8000 --allow-unauthenticated",
      "section_id": "start-the-server"
    },
    {
      "id": "start-the-server-ex2",
      "language": "bash",
      "code": "uvx --from redisvl[mcp] rvl mcp --config /path/to/mcp.yaml --transport sse --host 0.0.0.0 --port 9000 --allow-unauthenticated",
      "section_id": "start-the-server"
    },
    {
      "id": "transport-security-host--origin-validation-ex0",
      "language": "yaml",
      "code": "server:\n  redis_url: ${REDIS_URL}\n  transport_security:\n    allowed_hosts: [mcp.example.com]          # extra Host values to accept\n    allowed_origins: [https://app.example.com]  # browser origins to accept\n    # allow_any_origin: true                   # trust upstream Origin validation\n    # enabled: false                           # disable when a trusted proxy validates",
      "section_id": "transport-security-host--origin-validation"
    },
    {
      "id": "transport-security-host--origin-validation-ex1",
      "language": "bash",
      "code": "uvx --from redisvl[mcp] rvl mcp --config /path/to/mcp.yaml --read-only",
      "section_id": "transport-security-host--origin-validation"
    },
    {
      "id": "connect-a-remote-mcp-client-ex0",
      "language": "json",
      "code": "{\n  \"mcpServers\": {\n    \"redisvl\": {\n      \"url\": \"http://192.168.1.10:8000/mcp\",\n      \"transport\": \"streamable-http\"\n    }\n  }\n}",
      "section_id": "connect-a-remote-mcp-client"
    },
    {
      "id": "example-config-ex0",
      "language": "yaml",
      "code": "server:\n  redis_url: ${REDIS_URL}\n\nindexes:\n  knowledge:\n    redis_name: knowledge\n\n    vectorizer:\n      class: OpenAITextVectorizer\n      model: text-embedding-3-small\n      api_config:\n        api_key: ${OPENAI_API_KEY}\n\n    schema_overrides:\n      fields:\n        - name: embedding\n          type: vector\n          attrs:\n            dims: 1536\n            datatype: float32\n\n    search:\n      type: hybrid\n      params:\n        text_scorer: BM25STD\n        stopwords: english\n        vector_search_method: KNN\n        combination_method: LINEAR\n        linear_text_weight: 0.3\n\n    runtime:\n      text_field_name: content\n      vector_field_name: embedding\n      default_embed_text_field: content\n      default_limit: 10\n      max_limit: 25\n      max_result_window: 1000\n      max_upsert_records: 64\n      skip_embedding_if_present: true\n      startup_timeout_seconds: 30\n      request_timeout_seconds: 60\n      max_concurrency: 16",
      "section_id": "example-config"
    },
    {
      "id": "fulltext-only-config-ex0",
      "language": "yaml",
      "code": "server:\n  redis_url: ${REDIS_URL}\n\nindexes:\n  knowledge:\n    redis_name: knowledge\n\n    search:\n      type: fulltext\n      params:\n        text_scorer: BM25STD\n        stopwords: english\n\n    runtime:\n      text_field_name: content\n      default_limit: 10\n      max_limit: 25\n      max_result_window: 1000\n      max_upsert_records: 64\n      skip_embedding_if_present: true\n      startup_timeout_seconds: 30\n      request_timeout_seconds: 60\n      max_concurrency: 16",
      "section_id": "fulltext-only-config"
    },
    {
      "id": "multiple-indexes-ex0",
      "language": "yaml",
      "code": "server:\n  redis_url: ${REDIS_URL}\n\nindexes:\n  knowledge:\n    redis_name: knowledge\n    description: Internal runbooks and operational guidance.\n\n    vectorizer:\n      class: OpenAITextVectorizer\n      model: text-embedding-3-small\n      api_config:\n        api_key: ${OPENAI_API_KEY}\n\n    search:\n      type: vector\n\n    runtime:\n      text_field_name: content\n      vector_field_name: embedding\n      default_embed_text_field: content\n      default_limit: 10\n      max_limit: 25\n\n  tickets:\n    redis_name: support-tickets\n    description: Read-only mirror of resolved support tickets.\n    read_only: true\n\n    search:\n      type: fulltext\n      params:\n        text_scorer: BM25STD\n        stopwords: english\n\n    runtime:\n      text_field_name: body\n      default_limit: 10\n      max_limit: 50",
      "section_id": "multiple-indexes"
    },
    {
      "id": "custom-tool-profiles-ex0",
      "language": "yaml",
      "code": "server:\n  redis_url: redis://localhost:6379\n  builtin_tools:\n    search-records: disabled          # the curated tool replaces it\n\nindexes:\n  tickets:\n    redis_name: support-tickets\n    search:\n      type: fulltext\n    runtime:\n      text_field_name: body\n      default_limit: 5\n      max_limit: 20\n\ncustom_tools:\n  - name: search-resolved-tickets\n    based_on: search-records\n    index: tickets\n    description: >\n      Search resolved customer support tickets by relevance.\n      Use this to find how a similar problem was fixed before.\n    lock:\n      return_fields: [subject, resolution, created_at]\n      filter: { field: status, op: eq, value: resolved }\n    params:\n      limit: { expose: true, max: 10 }\n      filter: { expose: true }",
      "section_id": "custom-tool-profiles"
    },
    {
      "id": "tenant-scoping-with-claim-injection-ex0",
      "language": "yaml",
      "code": "# The RedisVL schema the index was created from\nindex:\n  name: customer-kb\n  prefix: kb\nfields:\n  - name: content\n    type: text\n  - name: org_id\n    type: tag\n    attrs:\n      case_sensitive: true",
      "section_id": "tenant-scoping-with-claim-injection"
    },
    {
      "id": "tenant-scoping-with-claim-injection-ex1",
      "language": "yaml",
      "code": "server:\n  redis_url: redis://localhost:6379\n  builtin_tools:\n    search-records: disabled          # both built-ins reach the index unscoped,\n    upsert-records: disabled          # so the server refuses to start with either on\n  auth:\n    type: jwt\n    jwks_uri: ${MCP_JWKS_URI}\n    issuer: ${MCP_ISSUER}\n    audience: api://redisvl-mcp\n    required_claims: [exp, iat, \"https://acme.example/org\"]\n\nindexes:\n  customer_kb:\n    redis_name: customer-kb\n    search:\n      type: fulltext\n    runtime:\n      text_field_name: content\n\ncustom_tools:\n  - name: search-customer-kb\n    index: customer_kb\n    description: Search this customer's knowledge base.\n    lock:\n      inject:\n        - field: org_id\n          from: claim\n          claim: \"https://acme.example/org\"\n          required: true",
      "section_id": "tenant-scoping-with-claim-injection"
    },
    {
      "id": "tenant-scoping-with-claim-injection-ex2",
      "language": "bash",
      "code": "rvl mcp --config /path/to/mcp_config.yaml --transport streamable-http",
      "section_id": "tenant-scoping-with-claim-injection"
    },
    {
      "id": "list-indexes-ex0",
      "language": "json",
      "code": "{\n  \"indexes\": [\n    {\n      \"id\": \"knowledge\",\n      \"description\": \"Internal runbooks and operational guidance.\",\n      \"upsert_available\": true,\n      \"fields\": [\n        { \"name\": \"title\", \"type\": \"text\" },\n        { \"name\": \"category\", \"type\": \"tag\" },\n        { \"name\": \"rating\", \"type\": \"numeric\" }\n      ],\n      \"limits\": { \"max_limit\": 25 }\n    },\n    {\n      \"id\": \"tickets\",\n      \"description\": \"Read-only mirror of resolved support tickets.\",\n      \"upsert_available\": false,\n      \"fields\": [\n        { \"name\": \"category\", \"type\": \"tag\" }\n      ],\n      \"limits\": { \"max_limit\": 50 }\n    }\n  ]\n}",
      "section_id": "list-indexes"
    },
    {
      "id": "search-records-ex0",
      "language": "json",
      "code": "{\n  \"index\": \"knowledge\",\n  \"query\": \"incident response runbook\",\n  \"limit\": 2,\n  \"offset\": 0,\n  \"filter\": {\n    \"and\": [\n      { \"field\": \"category\", \"op\": \"eq\", \"value\": \"operations\" },\n      { \"field\": \"rating\", \"op\": \"gte\", \"value\": 4 }\n    ]\n  },\n  \"return_fields\": [\"title\", \"content\", \"category\", \"rating\"]\n}",
      "section_id": "search-records"
    },
    {
      "id": "search-records-ex1",
      "language": "json",
      "code": "{\n  \"index\": \"knowledge\",\n  \"search_type\": \"hybrid\",\n  \"offset\": 0,\n  \"limit\": 2,\n  \"results\": [\n    {\n      \"id\": \"knowledge:runbook:eu-failover\",\n      \"score\": 0.82,\n      \"score_type\": \"hybrid_score\",\n      \"record\": {\n        \"title\": \"EU failover runbook\",\n        \"content\": \"Restore traffic after a regional failover.\",\n        \"category\": \"operations\",\n        \"rating\": 5\n      }\n    }\n  ]\n}",
      "section_id": "search-records"
    },
    {
      "id": "upsert-records-ex0",
      "language": "json",
      "code": "{\n  \"index\": \"knowledge\",\n  \"records\": [\n    {\n      \"doc_id\": \"doc-42\",\n      \"content\": \"Updated operational guidance for failover handling.\",\n      \"category\": \"operations\",\n      \"rating\": 5\n    }\n  ],\n  \"id_field\": \"doc_id\"\n}",
      "section_id": "upsert-records"
    },
    {
      "id": "upsert-records-ex1",
      "language": "json",
      "code": "{\n  \"index\": \"knowledge\",\n  \"status\": \"success\",\n  \"keys_upserted\": 1,\n  \"keys\": [\"knowledge:doc-42\"]\n}",
      "section_id": "upsert-records"
    },
    {
      "id": "discovery-first-multi-index-flow-ex0",
      "language": "json",
      "code": "{\n  \"index\": \"knowledge\",\n  \"query\": \"cache invalidation incident\",\n  \"limit\": 3,\n  \"return_fields\": [\"title\", \"content\", \"category\"]\n}",
      "section_id": "discovery-first-multi-index-flow"
    },
    {
      "id": "discovery-first-multi-index-flow-ex1",
      "language": "json",
      "code": "{\n  \"index\": \"knowledge\",\n  \"records\": [\n    { \"doc_id\": \"doc-7\", \"content\": \"New runbook entry\", \"category\": \"operations\" }\n  ],\n  \"id_field\": \"doc_id\"\n}",
      "section_id": "discovery-first-multi-index-flow"
    },
    {
      "id": "read-only-vector-search-ex0",
      "language": "bash",
      "code": "uvx --from redisvl[mcp] rvl mcp --config /path/to/mcp.yaml --read-only",
      "section_id": "read-only-vector-search"
    },
    {
      "id": "read-only-vector-search-ex1",
      "language": "json",
      "code": "{\n  \"query\": \"cache invalidation incident\",\n  \"limit\": 3,\n  \"return_fields\": [\"title\", \"content\", \"category\"]\n}",
      "section_id": "read-only-vector-search"
    },
    {
      "id": "raw-string-filter-ex0",
      "language": "json",
      "code": "{\n  \"query\": \"science\",\n  \"filter\": \"@category:{science}\",\n  \"return_fields\": [\"content\", \"category\"]\n}",
      "section_id": "raw-string-filter"
    },
    {
      "id": "json-dsl-filter-ex0",
      "language": "json",
      "code": "{\n  \"query\": \"science\",\n  \"filter\": {\n    \"and\": [\n      { \"field\": \"category\", \"op\": \"eq\", \"value\": \"science\" },\n      { \"field\": \"rating\", \"op\": \"gte\", \"value\": 4 }\n    ]\n  },\n  \"return_fields\": [\"content\", \"category\", \"rating\"]\n}",
      "section_id": "json-dsl-filter"
    },
    {
      "id": "pagination-and-field-projection-ex0",
      "language": "json",
      "code": "{\n  \"query\": \"science\",\n  \"limit\": 1,\n  \"offset\": 1,\n  \"return_fields\": [\"content\", \"category\"]\n}",
      "section_id": "pagination-and-field-projection"
    },
    {
      "id": "hybrid-search-with-schema_overrides-ex0",
      "language": "yaml",
      "code": "schema_overrides:\n  fields:\n    - name: embedding\n      type: vector\n      attrs:\n        algorithm: flat\n        dims: 1536\n        datatype: float32\n        distance_metric: cosine\n\nsearch:\n  type: hybrid\n  params:\n    text_scorer: BM25STD\n    stopwords: english\n    vector_search_method: KNN\n    combination_method: LINEAR\n    linear_text_weight: 0.3",
      "section_id": "hybrid-search-with-schema_overrides"
    },
    {
      "id": "hybrid-search-with-schema_overrides-ex1",
      "language": "json",
      "code": "{\n  \"query\": \"legacy cache invalidation flow\",\n  \"filter\": { \"field\": \"category\", \"op\": \"eq\", \"value\": \"release-notes\" },\n  \"return_fields\": [\"title\", \"content\", \"release_version\"]\n}",
      "section_id": "hybrid-search-with-schema_overrides"
    },
    {
      "id": "auto-embed-new-records-ex0",
      "language": "json",
      "code": "{\n  \"records\": [\n    {\n      \"content\": \"First upserted document\",\n      \"category\": \"science\",\n      \"rating\": 5\n    },\n    {\n      \"content\": \"Second upserted document\",\n      \"category\": \"health\",\n      \"rating\": 4\n    }\n  ]\n}",
      "section_id": "auto-embed-new-records"
    },
    {
      "id": "update-existing-records-with-id_field-ex0",
      "language": "json",
      "code": "{\n  \"records\": [\n    {\n      \"doc_id\": \"doc-1\",\n      \"content\": \"Updated content\",\n      \"category\": \"engineering\",\n      \"rating\": 5\n    }\n  ],\n  \"id_field\": \"doc_id\"\n}",
      "section_id": "update-existing-records-with-id_field"
    },
    {
      "id": "control-re-embedding-with-skip_embedding_if_present-ex0",
      "language": "json",
      "code": "{\n  \"records\": [\n    {\n      \"doc_id\": \"doc-2\",\n      \"content\": \"Existing content\",\n      \"category\": \"science\",\n      \"rating\": 4\n    }\n  ],\n  \"id_field\": \"doc_id\",\n  \"skip_embedding_if_present\": false\n}",
      "section_id": "control-re-embedding-with-skip_embedding_if_present"
    },
    {
      "id": "plain-writes-without-embedding-ex0",
      "language": "json",
      "code": "{\n  \"records\": [\n    {\n      \"content\": \"Updated FAQ entry\",\n      \"category\": \"support\",\n      \"rating\": 5\n    }\n  ]\n}",
      "section_id": "plain-writes-without-embedding"
    },
    {
      "id": "missing-mcp-dependencies-ex0",
      "language": "bash",
      "code": "pip install redisvl[mcp]",
      "section_id": "missing-mcp-dependencies"
    }
  ]
}
