Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c218537
feat(mcp): add MCPAuthConfig with JWT validation
nkanu17 Jun 9, 2026
b3fa4b3
feat(mcp): add REDISVL_MCP_AUTH_* settings
nkanu17 Jun 9, 2026
da31365
feat(mcp): add auth resolver and JWT provider builder
nkanu17 Jun 9, 2026
37daae7
feat(mcp): wire auth provider into the MCP server
nkanu17 Jun 9, 2026
a08bec2
test(mcp): add HTTP transport JWT auth e2e
nkanu17 Jun 9, 2026
54a764e
docs(mcp): add authentication guide and design spec
nkanu17 Jun 9, 2026
a52f88f
feat(mcp): add forbidden error code and authorization_claim config
nkanu17 Jun 10, 2026
18980d3
feat(mcp): add read/write scope gating helpers
nkanu17 Jun 10, 2026
c232189
feat(mcp): enforce read/write scope on search and upsert tools
nkanu17 Jun 10, 2026
4fb3566
test(mcp): add e2e write gating and roles-claim auth
nkanu17 Jun 10, 2026
3cbc8a3
docs(mcp): wire auth guide into nav and cross-link
nkanu17 Jun 10, 2026
7bbaaff
docs: enable sphinxcontrib-mermaid for diagram rendering
nkanu17 Jun 10, 2026
989e05b
docs(mcp): clarify OAuth resource-server scope
nkanu17 Jun 10, 2026
5e6d2ea
update mcp-docs
nkanu17 Jun 10, 2026
b349552
fix(mcp): require issuer and reject untyped JWT auth config
nkanu17 Jun 10, 2026
45462ad
fix(mcp): do not gate tools without an auth context
nkanu17 Jun 10, 2026
d2de62a
feat(mcp): warn or fail closed on unauthenticated HTTP bind
nkanu17 Jun 10, 2026
312c8d9
test(mcp): assert specific 401/ToolError and use get_running_loop
nkanu17 Jun 10, 2026
71a1664
feat(mcp): require exp/iat claims and let env type=none disable auth
nkanu17 Jun 10, 2026
34eccd9
fix(mcp): fail closed on stale construction-time auth
nkanu17 Jun 10, 2026
f6c5a7b
test(mcp): skip scope tests without fastmcp; doc fail-closed CLI
nkanu17 Jun 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/concepts/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ RedisVL MCP always registers `search-records`.

Use read-only mode when Redis is serving approved content to assistants and another system owns ingestion.

## Authentication and Authorization

The HTTP transports can require a JWT bearer token issued by an existing identity provider. The server validates the token signature, issuer, and audience, and can gate read vs write by scope or role claim. This is coarse, per-tool authorization; it does not map token claims to Redis ACL users or per-tenant filters, which remain a gateway concern. The `stdio` transport is local and is never authenticated.

For configuration and the gateway boundary, see {doc}`/user_guide/how_to_guides/mcp_authentication`.

## Tool Surface

RedisVL MCP exposes two tools:
Expand Down
7 changes: 6 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@
"sphinx_copybutton",
"_extension.gallery_directive",
"myst_nb",
"sphinx_favicon"
"sphinx_favicon",
"sphinxcontrib.mermaid",
]


Expand Down Expand Up @@ -91,6 +92,10 @@
myst_enable_extensions = ["colon_fence"]
myst_heading_anchors = 3

# Route ```mermaid fenced blocks to the sphinxcontrib.mermaid directive so they
# render as diagrams on the docs site (they already render natively on GitHub).
myst_fence_as_directive = ["mermaid"]

# Sphinx Book Theme options
html_theme_options = {
"repository_url": "https://github.com/redis/redis-vl-python",
Expand Down
3 changes: 3 additions & 0 deletions docs/user_guide/how_to_guides/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ How-to guides are **task-oriented** recipes that help you accomplish specific go

- [Manage Indices with the CLI](../cli.ipynb): create, inspect, and delete indices from your terminal
- [Run RedisVL MCP](mcp.md): expose an existing Redis index to MCP clients
- [Authenticate RedisVL MCP](mcp_authentication.md): require JWT bearer tokens and gate read vs write
:::

::::
Expand All @@ -65,6 +66,7 @@ How-to guides are **task-oriented** recipes that help you accomplish specific go
| Decide on storage format | [Choose a Storage Type](../05_hash_vs_json.ipynb) |
| Manage indices from terminal | [Manage Indices with the CLI](../cli.ipynb) |
| Expose an index through MCP | [Run RedisVL MCP](mcp.md) |
| Authenticate the MCP server | [Authenticate RedisVL MCP](mcp_authentication.md) |
| Plan and run a supported index migration | [Migrate an Index](migrate-indexes.md) |
| Quantize vectors with resume, rollback, and the wizard | [Migrate an Index: Quantization, Resume, Backup, Wizard](../14_index_migration.ipynb) |

Expand All @@ -84,6 +86,7 @@ Cache Embeddings <../10_embeddings_cache>
Use Advanced Query Types <../11_advanced_queries>
Write SQL Queries for Redis <../12_sql_to_redis_queries>
Run RedisVL MCP <mcp>
Authenticate RedisVL MCP <mcp_authentication>
Migrate an Index <migrate-indexes>
Migrate an Index: Quantization, Resume, Backup, Wizard <../14_index_migration>
```
8 changes: 4 additions & 4 deletions docs/user_guide/how_to_guides/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,20 @@ 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:
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 {doc}`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
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
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**. Only bind to public interfaces (`--host 0.0.0.0`) on trusted networks or behind an authenticating reverse proxy. When not using `--read-only`, the `upsert-records` tool is also exposed to any client that can reach the server.
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 {doc}`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.
```

Run it in read-only mode to expose search without upsert:
Expand Down
205 changes: 205 additions & 0 deletions docs/user_guide/how_to_guides/mcp_authentication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
---
myst:
html_meta:
"description lang=en": |
How RedisVL MCP authenticates clients with JWT bearer tokens and gates
read vs write access by scope or role claim.
---

# Authenticate RedisVL MCP

This guide explains how the RedisVL MCP server authenticates clients on its HTTP
transports and how it gates read vs write access. It also draws the boundary
between what RedisVL enforces and what belongs in a gateway or policy layer.

```{note}
Authentication applies only to the HTTP transports (`streamable-http`, `sse`).
The `stdio` transport is a local subprocess with no network surface and is never
authenticated.
```

## What RedisVL Enforces

RedisVL validates a bearer **JWT** that an existing identity provider (IdP)
issued. It does not run an OAuth authorization server and does not issue tokens.

On each request it checks:

- **Signature**, against a JWKS endpoint or a static public key.
- **Issuer** (`iss`), so only tokens from your IdP are accepted.
- **Audience** (`aud`), so a token minted for a different service cannot be
replayed against this server (RFC 8707).
- **Expiration**: a present `exp` in the past is rejected, and tokens are
required to carry `exp` and `iat` (configurable via `required_claims`), so a
token with no expiration, which would never expire, is rejected.
- **Required scopes** to connect, and (optionally) a **read scope** to call
`search-records` and a **write scope** to call `upsert-records`.

```{important}
This is **coarse** authorization: it decides whether a caller may connect and
whether it may read or write. It does **not** map token claims to a Redis ACL
user, a per-tenant index, or query filters. See [The Authorization Boundary](#the-authorization-boundary).
```

## OAuth: Which Part RedisVL Handles

In OAuth terms, RedisVL MCP is a **resource server**: it validates access
tokens that your identity provider issued. It does not run an OAuth login flow
and does not issue tokens.

```mermaid
flowchart LR
Client -->|1 - OAuth login flow| IdP[Identity Provider]
IdP -->|2 - issues JWT access token| Client
Client -->|3 - Bearer JWT| MCP[RedisVL MCP]
MCP -->|validates token| Redis[(Redis)]
```

Steps 1 and 2 (obtaining the token) are handled by your client and IdP. RedisVL
only performs the validation in step 3. As a result:

- It works with any OAuth 2.0 / OIDC provider (for example Auth0, Okta, Azure AD
/ Entra, Cognito, Keycloak): point `jwks_uri` at the provider and validate its
tokens.
- RedisVL does not broker interactive "log in with..." flows and does not mint
tokens.

You would only need more than token validation when you want the server itself
to drive an interactive browser login (an OAuth proxy), or to act as its own
authorization server that issues tokens. Both are out of scope today. If
interactive login is ever needed, a single generic `oauth-proxy` option can be
added behind the `auth.type` switch. For enterprise and agent deployments where
the caller already holds a token, JWT validation is sufficient.

## Request Flow

```mermaid
sequenceDiagram
actor User
participant IdP as Identity Provider
participant MCP as RedisVL MCP Server
participant Redis

User->>IdP: Authenticate
IdP-->>User: Signed JWT (iss, aud, scopes/roles)
User->>MCP: MCP request + Bearer JWT
MCP->>MCP: Validate signature (JWKS / public key)
MCP->>MCP: Check issuer + audience
alt token invalid / wrong audience / missing connect scope
MCP-->>User: 401 Unauthorized
else token valid
MCP->>MCP: Gate tool by read / write scope
alt scope present
MCP->>Redis: Search or upsert (single configured ACL user)
Redis-->>MCP: Results
MCP-->>User: Tool result
else scope missing
MCP-->>User: Forbidden
end
end
```

## Configure JWT Authentication

Add a `server.auth` block to your MCP config. Secrets can be injected with
`${ENV}` substitution.

```yaml
server:
redis_url: ${REDIS_URL:-redis://localhost:6379}
auth:
type: jwt
jwks_uri: ${MCP_JWKS_URI} # or set public_key for a static key
issuer: ${MCP_ISSUER}
audience: api://redisvl-mcp
required_scopes: [kb.read] # required to connect
required_claims: [exp, iat] # claims every token must carry (default)
read_scope: kb.search.read # required for search-records
write_scope: kb.search.write # required for upsert-records

indexes:
knowledge:
redis_name: docs_index
search:
type: fulltext
runtime:
text_field_name: content
```

Every field is also settable through `REDISVL_MCP_AUTH_*` environment variables,
which take precedence over the YAML block.

## Choosing the Authorization Claim

Different identity providers carry authorization in different claims. The
default JWT scope claim is `scp` (or `scope`). Some enterprise providers carry
authorization in a **`roles`** claim instead, which does not appear in the
standard scope set.

```mermaid
flowchart TD
A[Validated JWT claims] --> B{authorization_claim}
B -->|scp / scope| C["access.scopes<br/>e.g. kb.read"]
B -->|roles| D["access.claims.roles<br/>e.g. kb.search.read"]
C --> E[Check read_scope / write_scope]
D --> E
E -->|present| F[Allow tool]
E -->|absent| G[Deny tool]
```

Set the claim that holds your authorization values so read and write gating
reads the right place:

```yaml
server:
auth:
type: jwt
# ...
authorization_claim: roles # default: scp
read_scope: kb.search.read
write_scope: kb.search.write
```

A token like the following would then pass the read gate, because
`kb.search.read` is present in `roles`:

```json
{
"iss": "https://your-idp.example/{tenant}/v2.0",
"aud": "api://redisvl-mcp",
"sub": "nitin",
"roles": ["kb.search.read"],
"scp": "kb.read"
}
```

## The Authorization Boundary

RedisVL MCP authenticates the caller and gates read vs write. It does **not**
translate token claims (such as a tenant id or role) into a specific Redis ACL
user, a per-tenant index, or injected query filters. The server holds one Redis
connection for one index, established at startup.

Fine-grained, per-tenant data isolation belongs in a **gateway or policy layer**
in front of the MCP server, which validates the token, looks up a binding of
claim to Redis identity, and injects credentials and filters.

```mermaid
flowchart LR
subgraph Gateway["Gateway / policy layer (out of scope for RedisVL)"]
T[Validate token] --> M["Map claims to<br/>Redis user + index + filters"]
end
subgraph RedisVL["RedisVL MCP (this guide)"]
A[Validate JWT] --> S[Gate read / write by scope]
end
Client --> Gateway --> RedisVL --> Redis[(Redis)]
```

Use RedisVL's JWT validation for authentication and coarse read/write
authorization. Layer a gateway on top when you need per-tenant Redis ACL
enforcement.

## See Also

- {doc}`mcp`: run and configure the RedisVL MCP server.

Loading
Loading