Qdrant RBAC

de.fidonis由 fidonis.de 发布v0.4.0更新于 Oct 24, 2026

Role-based access to your Qdrant vector store, via MCP. OIDC in, scoped JWTs out.

概览

AI 生成的概览

一个自托管的 MCP 服务器,在 Qdrant 向量数据库前提供 OIDC 登录与基于角色的访问控制,并为每个用户签发限定范围的 Qdrant JWT。

可以做什么
列出、查看并检索集合,包括通过 OpenAI 兼容端点嵌入查询的自然语言检索,以及分页浏览原始数据点和按来源文件统计分块数量的文档清单。在调用者有写权限的集合中写入和删除数据点。管理授权:按角色列出授权、授予或撤销某角色对某集合的访问、强制重载 ACL 缓存。通过可选文档策略在每次读取时转换为 Qdrant 载荷过滤器,实现文档级可见性控制。
适用场景
当多个人员或团队共用一个 Qdrant 实例,希望每个用户只能查看和修改其身份提供方角色所允许的集合与文档,而不想把访问规则写死在配置文件里时使用。适合已运行 Keycloak 等 OIDC 提供方、并启用了 JWT RBAC 的 Qdrant 的自托管环境。
运行要求
需要可访问的 OIDC 提供方(如 Keycloak),并配置客户端受众与角色;需要可访问且已启用 JWT RBAC 的 Qdrant 实例。必需环境变量:OIDC_ISSUER_URL、OIDC_AUDIENCE、QDRANT_URL,以及密钥 QDRANT_JWT_SECRET,其值必须与 Qdrant 的服务 API 密钥一致。以容器或 Python 3.11+ 配合 uv 运行;客户端通过 streamable HTTP 连接并携带 OIDC 持有者令牌。文本检索还需要一个嵌入端点。
安装前请注意
QDRANT_JWT_SECRET 是签名密钥,必须与 Qdrant 的 API 密钥一致;持有它的人可以签发 Qdrant 接受的令牌,应避免写入日志或镜像。服务器会写入 Qdrant:写入和删除数据点会改变已存数据,授予或撤销授权会改变谁能读取什么。应急管理员角色可签发全局管理令牌,其分配应严格受限。访问令牌依据身份提供方公布的密钥校验,服务器会与该提供方及 Qdrant 通信。

安装

在 SourceWeft 中

  1. 打开 控制台中的 Qdrant RBAC,将其添加到工作区。
  2. 为需要使用其工具的对话启用该服务。

Desktop only,通过 STDIO。 STDIO 服务会启动本地进程,因此需要 SourceWeft 桌面宿主。

其他 MCP 客户端

参照 仓库 中的启动说明。

README

qdrant-mcp-rbac

[License: MIT] Maintained by Fidonis · See TRADEMARK.md for the trademark notice.

[Same MCP tool call, two identities, two different result sets — server-side enforcement.]

A FastMCP server that exposes a Qdrant vector database over streamable HTTP with OIDC-based authentication (Keycloak-compatible) and role-based access control mapped onto Qdrant's native JWT access rules.

Role grants are stored as data inside a dedicated Qdrant collection, not in configuration files — admins manage them at runtime via MCP tools.

┌──────────┐   OIDC Bearer   ┌─────────────────┐   Qdrant JWT    ┌──────────┐│ MCP host │ ──────────────▶ │ qdrant-mcp-rbac │ ──────────────▶ │  Qdrant  ││ (client) │                 │   (FastMCP)     │                 │          │└──────────┘                 └─────────────────┘                 │  ┌─────┐ │                                     │                           │  │_rbac│ │                                     ▼                           │  │_acl │ │                                 Keycloak                        │  └─────┘ │                                 (JWKS, OIDC discovery)          └──────────┘

Quick start (Docker)

bash
docker run --rm -p 8000:8000 \  -e OIDC_ISSUER_URL=https://kc.example.com/realms/myrealm \  -e OIDC_AUDIENCE=mcp-server \  -e QDRANT_URL=http://qdrant:6333 \  -e QDRANT_JWT_SECRET=<same secret as Qdrant's api-key> \  ghcr.io/fidonis/qdrant-mcp-rbac:latest

/mcp is the streamable-HTTP endpoint, /health is unauthenticated. See the full docker/.env.example for every variable.

How it works

  1. The MCP client sends a request with an OIDC access token (Authorization: Bearer …).
  2. The OIDCAuthMiddleware validates the token against the OIDC provider's JWKS (signature, expiry, audience, issuer; see Token validation).
  3. The JWTBuilder derives a Qdrant JWT for the user:
    • If the user is in the break-glass admin role (RBAC_ADMIN_ROLE), a global-manage token is minted directly.
    • Otherwise, the AclResolver returns the cached role -> [grants] mapping built from the ACL collection (RBAC_ACL_COLLECTION, default _rbac_acl).
  4. The Qdrant JWT is attached to the request scope.
  5. Each MCP tool checks the user's effective access for the targeted collection and forwards the request to Qdrant using the derived JWT.

Token validation

Tokens are validated with PyJWT against the keys published by the issuer ({OIDC_ISSUER_URL}/.well-known/openid-configuration, then its jwks_uri):

  • Signature algorithm comes from the signing key (its alg, or RS256 / ES256 for an RSA / EC key without one), never from the token header. Only RS256, RS384, RS512, ES256, ES384 and ES512 are accepted; symmetric (HS*) and none tokens are rejected.
  • Required claims: exp, aud (must contain OIDC_AUDIENCE), iss (must equal the issuer in the discovery document) and a non-empty sub. A token without exp or aud is rejected. nbf is enforced when present.
  • iat is not checked, so clock skew between this host and the identity provider does not reject freshly issued tokens.
  • Key rotation: keys are cached for OIDC_JWKS_CACHE_TTL seconds. A token with an unknown kid triggers exactly one JWKS refresh before it is rejected.
  • Roles are read from realm_access.roles and from every resource_access.<client>.roles.

ACL collection schema

Each grant is one point in _rbac_acl. The point id is a deterministic uuid5(role, collection), so re-granting is idempotent. Payload:

json
{ "role": "data-scientist", "collection": "embeddings_v2", "access": "rw" }

access is one of:

  • r – read-only
  • rw – read & write
  • m – manage (global; collection is informational, convention: "*")

The collection is auto-created on first read by the server using a self-minted service token.

Document-level access control (doc_policy)

Collection-level access decides whether a role can read a collection at all. A grant may additionally carry an optional doc_policy that restricts which individual documents (points) inside that collection the role can see. The server translates the policy into a Qdrant payload filter and injects it as a must clause on every read — search_collection, search_collection_by_text, scroll_collection, and list_documents (faceting only over documents the role may see). Caller-supplied query_filter values are preserved; the doc filter is added alongside, so both apply together.

Grants without doc_policy behave exactly as before — every document in the collection is visible.

Schema
jsonc
{  "default": "allow" | "deny",  "conditions": [    { "field": "<payload field>", "mode": "allow" | "deny", "values": ["<v>", "…"] }  ]}
  • default: "allow" — every document is visible by default; deny conditions hide matching ones.
  • default: "deny" — no documents are visible by default; allow conditions expose matching ones.
  • values: ["*"] — wildcard meaning "all documents". On an allow condition (default-deny) it cancels the filter, exposing the whole collection; on a deny condition (default-allow) it returns an empty result set.
  • default: "deny" with no allow conditions → empty result set.

Multiple conditions of the same mode are unioned (a document is hidden by any deny condition; a document is exposed by any allow condition).

Multi-role merge

When a user holds several grants on the same collection (via different roles), the server merges their policies using the most-permissive rule:

  • If any matching grant has no doc_policy, the user is unrestricted on that collection.
  • If any matching grant has default: "allow", the merged policy is default: "allow" with the union of those grants' deny conditions (default-deny grants are ignored).
  • Otherwise all grants are default: "deny" and their allow conditions are unioned.
Recommended payload field: acl_tags

A new optional acl_tags: list[str] field is reserved for documents that need access control. It is never required — documents without acl_tags remain fully functional:

  • In default-allow grants, an untagged document is never matched by a deny condition on acl_tags → always visible.
  • In default-deny grants, an untagged document never satisfies an allow condition on acl_tags → implicitly hidden.

The bootstrap script demo/bootstrap/vectorize.py reads an optional demo/data/acl_tags.yml sidecar mapping each Markdown source (POSIX path relative to the data directory) to a list of tags applied to every chunk ingested from that file.

Examples

Default-allow, deny confidential documents:

jsonc
{  "role": "finance-analyst",  "collection": "finance",  "access": "r",  "doc_policy": {    "default": "allow",    "conditions": [      { "field": "acl_tags", "mode": "deny", "values": ["confidential", "board-only"] }    ]  }}

Default-deny, allow only public documents:

jsonc
{  "role": "guest",  "collection": "finance",  "access": "r",  "doc_policy": {    "default": "deny",    "conditions": [      { "field": "acl_tags", "mode": "allow", "values": ["public"] }    ]  }}

Filtering on the source field (no acl_tags required):

jsonc
{  "role": "junior-analyst",  "collection": "finance",  "access": "r",  "doc_policy": {    "default": "deny",    "conditions": [      { "field": "source", "mode": "allow", "values": ["annual-report.md", "q4-summary.md"] }    ]  }}

grant_access accepts the same shape:

jsonc
{  "tool": "grant_access",  "args": {    "role": "guest",    "collection": "finance",    "access": "r",    "doc_policy": {      "default": "deny",      "conditions": [        { "field": "acl_tags", "mode": "allow", "values": ["public"] }      ]    }  }}

Layout

The repo separates the production server (src/) from demo material (demo/). src/ and each project under demo/ is its own uv project with its own pyproject.toml and .venv — there is no virtual environment in the repo root.

src/                       # uv project: the FastMCP server  pyproject.toml  uv.lock  .venv/                   # created by `uv sync` (gitignored)  .env                     # local config (gitignored)  .env.example             # template for `.env`  main.py                  # Entry point (uvicorn)  config.py                # Settings (pydantic-settings, .env)  auth/    models.py              # OIDCClaims, CollectionAccess, AclEntry, QdrantToken    oidc.py                # JWKS-based OIDC token validator (PyJWT)    acl.py                 # AclResolver: TTL-cached read of the ACL collection    jwt_builder.py         # Builds Qdrant JWTs from claims + ACL mapping  qdrant/    client.py              # AsyncQdrantClient factory (per-request, JWT-scoped)    collections.py         # Thin async wrappers around Qdrant collection operations    meta.py                # Read/write helpers for the _collection_meta system collection  mcp_app/                 # named `mcp_app` to avoid shadowing the installed `mcp` SDK    server.py              # FastMCP instance + Starlette app wiring    tools.py               # MCP tools: data tools + ACL admin tools    middleware.py          # ASGI auth middleware: OIDC → Qdrant JWT
tests/                     # pytest suite, run from src/ (kept out of the Docker image)  fakes/oidc.py            # In-process OIDC issuer: RSA key, JWKS, token minting  test_doc_filter.py  test_facet.py  test_jwt_builder.py  test_oidc_validator.py  test_scroll.py
demo/                      # everything for the end-to-end demo  README.md                # Demo quickstart (order of operations)  bootstrap/               # uv project: vectorize Markdown → Qdrant collections    pyproject.toml    uv.lock    .venv/    vectorize.py  client/                  # uv project: OIDC + LLM CLI client    pyproject.toml    uv.lock    .venv/    client.py    agent.py    oidc.py    config.py  data/                    # demo seed data (Markdown, collection = file stem up to the first "-")    finance-2025.md    finance-2026.md    finance-aquise.md    it.md    sales.md

Setup

Prerequisites

  • Python 3.11+
  • uv installed
  • A reachable OIDC provider (e.g. Keycloak)
  • A reachable Qdrant instance with JWT RBAC enabled

Install

bash
cd srcuv sync

This creates src/.venv with all dependencies. The repo root has no .venv of its own.

Configure

Copy the example env file and adjust values:

bash
cp src/.env.example src/.env

The server reads src/.env (resolved relative to config.py), so keep it inside src/ regardless of the working directory you run from.

Important variables:

VariablePurpose
OIDC_ISSUER_URLKeycloak realm URL, e.g. https://kc.example.com/realms/myrealm
OIDC_AUDIENCEExpected aud claim, e.g. mcp-server
OIDC_JWKS_CACHE_TTLCache TTL of the OIDC discovery document and JWKS (default 3600 s)
QDRANT_URLQdrant HTTP endpoint
QDRANT_JWT_SECRETSigning secret — must equal Qdrant's service.api_key; use at least 32 bytes, PyJWT warns about shorter HS256 keys (e.g. openssl rand -hex 32)
QDRANT_JWT_TTLLifetime of the Qdrant JWTs derived per request (default 3600 s, minimum 60)
MCP_HOST / MCP_PORT / MCP_PATHListen address and path (default 0.0.0.0:8000/mcp)
LOG_LEVELLog level (default INFO)
EMBEDDING_API_URLOpenAI-compatible embeddings base URL (default http://localhost:11434/v1)
EMBEDDING_API_KEYOptional bearer token for the embeddings endpoint
EMBEDDING_META_COLLECTIONCollection storing per-collection embedding metadata (default _collection_meta)
RBAC_ADMIN_ROLEOIDC role granting global manage (default qdrant-admin)
RBAC_ACL_COLLECTIONQdrant collection storing grants (default _rbac_acl)
RBAC_ACL_CACHE_TTLIn-memory ACL cache TTL (default 60 s)
RBAC_SERVICE_TOKEN_TTLTTL of server-minted service tokens for ACL reads (default 300 s)

Run

bash
cd srcuv run python main.py

Run with Docker

To run the server itself in a container instead of on the host, use the compose file in docker/:

bash
cp docker/.env.example docker/.env   # then edit secrets and OIDC_ISSUER_URLdocker compose -f docker/docker-compose.yml up -d

This builds the image from docker/Dockerfile and starts the mcp-server container (published on :8000, with a /health health-check) alongside Qdrant. Keycloak is not included — point OIDC_ISSUER_URL in docker/.env at an external OIDC provider. Every variable is documented in docker/.env.example.

Bootstrapping access

  1. Assign one user the qdrant-admin realm role in Keycloak.
  2. Start the server.
  3. As that user, call the grant_access tool to provision other roles:
jsonc
// MCP tool call{  "tool": "grant_access",  "args": { "role": "data-scientist", "collection": "embeddings_v2", "access": "rw" }}
  1. Other users with the data-scientist role now get an rw JWT scoped to embeddings_v2.

Tools

Data tools

ToolRequired access
list_collections(any) — returns only collections the caller can read
get_collection_infor on the target collection
search_collectionr on the target collection
search_collection_by_textr on the target collection
scroll_collectionr on the target collection
list_documentsr on the target collection
upsert_pointsrw on the target collection
delete_pointsrw on the target collection

search_collection_by_text accepts a natural-language query, embeds it via the configured OpenAI-compatible endpoint using the model recorded at bootstrap time, and runs the resulting vector search.

scroll_collection lists raw points (chunks) without a query. It pages through the collection via the next_offset cursor it returns (null on the last page). Use it to inspect the actual stored points.

list_documents answers "which / how many documents are in collection X?". It aggregates points by their source payload field server-side (Qdrant faceting) and returns one deduplicated entry per document with its chunk count — prefer it over scroll_collection for inventory questions, since a single document spans many chunks. Faceting requires a keyword payload index on source; the bootstrap creates it, and the server also creates it on first use if it is missing, so no manual step is needed.

upsert_points and delete_points refuse to operate on system collections (_rbac_acl, _collection_meta) — use the admin tools or bootstrap instead.

ACL admin tools (require global manage)

ToolEffect
list_acl(role?)Return all grants, optionally filtered by role
grant_access(role, collection, access)Idempotently upsert one grant
revoke_access(role, collection)Remove a grant
refresh_acl()Force the in-memory cache to reload

Mutations invalidate the local cache immediately; out-of-band edits to the ACL collection (e.g. via the Qdrant API directly) become visible after at most RBAC_ACL_CACHE_TTL seconds.

Local development with docker-compose

To start only Qdrant for local server development, use the qdrant service from docker/docker-compose.yml. Pass src/.env so that Compose can read QDRANT_JWT_SECRET:

bash
docker compose --env-file src/.env -f docker/docker-compose.yml up -d qdrant

This starts:

  • Qdrant on :6333 (with JWT RBAC enabled, signing key from QDRANT_JWT_SECRET)

Create a realm, a confidential client with audience mcp-server, the qdrant-admin role, and any per-team roles you plan to grant access to. Assign roles to test users.

Qdrant JWT setup

qdrant-mcp-rbac signs JWTs with QDRANT_JWT_SECRET (HS256). Qdrant must be configured to accept JWTs and use the same secret as its API key:

yaml
# qdrant config.yamlservice:  api_key: <same value as QDRANT_JWT_SECRET>  jwt_rbac: true

The derived JWT structure for a non-admin user is:

json
{  "exp": 1735689600,  "access": [    { "collection": "col_a", "access": "r" },    { "collection": "col_b", "access": "rw" }  ]}

For admins (or any grant with access: "m"):

json
{ "exp": 1735689600, "access": "m" }

Calling from an MCP client

Pass the OIDC access token in the Authorization header:

bash
TOKEN=$(curl -s -X POST \  -d "client_id=mcp-cli" -d "username=alice" -d "password=…" \  -d "grant_type=password" \  https://kc.example.com/realms/myrealm/protocol/openid-connect/token | jq -r .access_token)
curl -H "Authorization: Bearer $TOKEN" \     -H "Accept: text/event-stream" \     http://localhost:8000/mcp

About Fidonis

qdrant-mcp-rbac is built and maintained by Fidonis. We help companies run their own AI infrastructure end to end — open source, open standards, no vendor lock-in. This project covers one specific piece of the picture: enforce who can read what in your vector store without baking ACLs into config files.

If you are building a similar self-hosted stack and want to talk shop, drop by at fidonis.de.

License

qdrant-mcp-rbac is released under the MIT license — Copyright (c) 2026 Fidonis GmbH (in Gründung) and contributors.

  • See TRADEMARK.md for the trademark notice covering the name "Fidonis" and the project name qdrant-mcp-rbac.
  • See THIRD_PARTY_LICENSES.md for the licenses of the third-party Python dependencies bundled with this project.
  • See CONTRIBUTING.md for the Inbound = Outbound (MIT) rule and the list of license categories that contributions may not introduce without prior approval.

"Qdrant" is a registered trademark of Qdrant Solutions GmbH. Use of the name in this project is nominative — it describes the upstream component the server is built on and does not imply endorsement.

来源:README.md,提交 dde806d

工具

0
SourceWeft 尚未在沙箱中启动过该软件包,因此其工具未知。

版本历史

1
  1. v0.4.0最新Oct 11, 2026

评分

已满足适用检查项的 50%

该评分衡量此收录条目的完整程度,依据注册中心条目、端点与公开来源计算得出。它不是对服务器本身的评价,也不表示服务器安全或当前可用:可连通性是带日期的检查结果,评分也不包含安装量或用户评价。

  • 近 30 天内可连通25 分

    请保持端点能够响应 MCP 握手;SourceWeft 每 30 天探测一次。

  • 已声明认证方式10 分

    请在注册中心条目中声明凭据请求头,或保持端点能够响应探测,以便判定其认证方式。

  • 已列出工具15 分

    请让服务器在无凭据时响应 tools/list,或发布可在沙箱中无凭据启动的软件包。

  • 描述必需15 分
  • 图标10 分
  • 文档15 分
  • 许可证10 分