╭──────────────────────────────────────────────────────────────╮
│ Q D R A N T // M C P │
│ VECTOR MEMORY · INGEST · RETRIEVE · GOVERN │
╰──────────────────────────────────────────────────────────────╯
A typed Model Context Protocol server for agents that need to search, store, ingest, inspect, and maintain data in a Qdrant vector database. It combines broad Qdrant coverage with compact agent navigation, bounded document workflows, dry-run/confirmation controls, and two authenticated HTTP deployment modes.
Release integrity:
v2.0.0source, Python artifacts, container digest, and GitHub Release form one verified release only after the exact-tag workflow completes. Deploy the container by the digest recorded in that release; a branch, mutable tag, or HTTP 200 alone is not release proof.
- Policy-derived catalog: 77 tools by default (69 agent-ready + 8 legacy), 79 with admin tools enabled (71 + 8), and 45 in read-only mode (40 + 5). Health and discovery report the active set.
- Agent navigation:
check_configuration,list_capabilities,get_endpoint_coverage,get_tool_usage, andfind_toolshelp a client choose the right operation before spending calls. - Vector-memory workflows: compact retrieval, context packs, short-term memory, promotion, validation, deduplication, lifecycle maintenance, and embedding migration.
- Document ingestion: text, base64, bounded uploads, HTTPS sources, PDF extraction, OCR, school manifests, and observable asynchronous textbook jobs.
- Operational controls: read-only mode, admin gating, dry-run previews, explicit confirmation, batch/output bounds, connector allowlists, and deny-by-default outbound URL fetching.
- Portable runtime: local
stdio, authenticated standalone HTTP, or brokered Portal HTTP with request-scoped bring-your-own Qdrant credentials.
The maintained coverage matrix is in the endpoint coverage guide.
| Runtime | Service authentication | Qdrant credentials | Intended use |
|---|---|---|---|
Local stdio |
Local process boundary | Environment | Desktop and local agents |
Standalone HTTP / server |
Authorization: Bearer … |
Server environment | One operator-controlled connector |
Standalone HTTP / request |
Authorization: Bearer … |
Per-request headers | One authenticated service serving multiple connector contexts |
| Portal HTTP | Portal grant header | Per-request headers | Brokered, tenant-scoped BYOK |
There is no unauthenticated MCP-over-HTTP mode. /health remains reachable for orchestration and
must return only non-secret readiness metadata. QDRANT_CREDENTIAL_MODE is a startup choice; it is
not remotely mutable through an MCP tool.
From source:
git clone https://github.com/MADPANDA3D/QDRANT-MCP.git
cd QDRANT-MCP
uv sync --frozen --group dev --python 3.12.13
export QDRANT_URL='https://qdrant.example.com'
export QDRANT_API_KEY='replace-with-your-qdrant-key'
export COLLECTION_NAME='agent-memory'
uv run mad-mcp-qdrant --transport stdioDirect Python installs need native document helpers for the complete ingest surface. On
Debian/Ubuntu install antiword, poppler-utils, and tesseract-ocr; on Arch install antiword,
poppler, and tesseract. Without antiword, legacy .doc ingest fails. Without Poppler, PDF
fallback extraction and OCR rendering are unavailable; without Tesseract, OCR produces no recovered
text. Text-native PDF extraction can still succeed, but scanned or damaged PDFs may fail or return
bounded warnings. The stock container includes all three helpers.
Example MCP client entry:
{
"mcpServers": {
"qdrant": {
"command": "uv",
"args": [
"--directory",
"/absolute/path/to/QDRANT-MCP",
"run",
"mad-mcp-qdrant",
"--transport",
"stdio"
],
"env": {
"QDRANT_URL": "https://qdrant.example.com",
"QDRANT_API_KEY": "replace-with-your-qdrant-key",
"COLLECTION_NAME": "agent-memory"
}
}
}
}The equivalent pinned package command is:
uvx --from mad-mcp-qdrant==2.0.0 mad-mcp-qdrant --transport stdioCopy the placeholder file and generate a high-entropy service token:
cp .env.example .env
openssl rand -hex 32Set at least these values in .env for server-owned credentials:
MCP_ACCESS_TOKEN=replace-with-generated-token
QDRANT_CREDENTIAL_MODE=server
QDRANT_URL=https://qdrant.example.com
QDRANT_API_KEY=replace-with-your-qdrant-keyStart only the standalone profile:
docker compose --profile standalone up --detach --buildAn HTTP client sends:
Authorization: Bearer replace-with-generated-token
To keep connector credentials out of the service environment, stop the server-owned profile and select the request-scoped profile with:
QDRANT_URL=
QDRANT_API_KEY=
MCP_QDRANT_HOST_ALLOWLIST=qdrant.example.com
MCP_QDRANT_ALLOWED_PORTS=443,6333docker compose --profile standalone down
docker compose --profile standalone-request up --detach --buildThen send the normal Bearer token plus X-Qdrant-Url, X-Qdrant-Api-Key, and optionally
X-Collection-Name on each data-tool request. Authenticated navigation tools can report missing
setup without connector headers. Request mode disables server credential fallback.
The Compose profile fixes QDRANT_CREDENTIAL_MODE=request and the associated fail-closed override
flags at startup. It also blanks OPENAI_API_KEY, OPENAI_BASE_URL, OPENAI_ORG, and
OPENAI_PROJECT so host environment values cannot become request-mode credentials. The
credential-free local FastEmbed default remains available.
Portal mode always uses QDRANT_CREDENTIAL_MODE=request. Set:
MCP_PORTAL_GRANT_TOKEN=replace-with-the-broker-grant
MCP_PORTAL_GRANT_HEADER=x-madpanda-portal-grant
MCP_TENANT_ID_HEADER=x-madpanda-user-id
MCP_QDRANT_HOST_ALLOWLIST=qdrant.example.com
MCP_QDRANT_ALLOWED_PORTS=443,6333Start only the Portal profile:
docker compose --profile portal up --detach --buildThe trusted broker forwards these headers over TLS:
X-MADPANDA-PORTAL-GRANT: replace-with-the-broker-grant
X-MADPANDA-USER-ID: stable-tenant-identifier
X-QDRANT-URL: https://qdrant.example.com
X-QDRANT-API-KEY: replace-with-the-tenant-qdrant-key
X-COLLECTION-NAME: agent-memory
X-MADPANDA-USER-ID is a required tenant-routing identifier, not a credential. It is used to keep
request-scoped resources from crossing tenant boundaries. Do not treat it as authorization and do
not put real tenant identifiers in public issue reports or logs. Header names are configurable at
startup; their security meaning is not.
Portal data-tool requests without a valid grant, tenant identifier, Qdrant URL, or Qdrant API key fail before provider work. Authenticated navigation tools remain available without connector headers so an agent can discover the missing setup. The Portal service has no server-side Qdrant or OpenAI credential/endpoint fallback; request-scoped OpenAI settings arrive only through the documented headers. The credential-free local FastEmbed default remains available.
The release image namespace is:
ghcr.io/madpanda3d/qdrant-mcp-server
After release, select the exact digest shown in the GitHub Release rather than a mutable tag:
MCP_RUNTIME_IMAGE=ghcr.io/madpanda3d/qdrant-mcp-server@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdefResolve and start that image without invoking a source build:
docker compose --profile portal config --quiet
docker compose --profile portal pull
docker compose --profile portal up --detach --no-buildThe stock Compose profiles publish only to 127.0.0.1, run as UID/GID 10001, drop all Linux
capabilities, enable no-new-privileges, use a read-only root filesystem, bound PIDs/CPU/memory,
and mount only an ephemeral /tmp. Put a TLS reverse proxy in front of HTTP deployments and keep
the service port off public interfaces. Add the proxy's exact public Host to MCP_ALLOWED_HOSTS;
leave MCP_ALLOWED_ORIGINS empty unless exact browser origins are deliberately supported.
The stock image also includes
sentence-transformers/all-MiniLM-L6-v2
from
qdrant/all-MiniLM-L6-v2-onnx@5f1b8cd78bc4fb444dd171e59b18f3a3af89a079.
Its archive and all
six runtime files are SHA-256 pinned during the build. Runtime uses an exact local model path with
HF_HUB_OFFLINE=1 and local_files_only=True; release smoke performs a real 384-dimension embedding
under --network none. Startup rejects a different FastEmbed model name while this exact baked
revision is active. Direct Python installs retain normal FastEmbed cache/acquisition behavior
when FASTEMBED_MODEL_PATH is unset. See the
deployment guide and
security model for
the two acquisition modes and complete provenance.
Remote document and textbook downloads are denied by default:
MCP_OUTBOUND_HOST_ALLOWLIST=
MCP_OUTBOUND_ALLOWED_PORTS=443
MCP_ALLOW_INSECURE_OUTBOUND_HTTP=falseAllowlisting a remote host is a trust decision: the service will connect to it and process its
response. Prefer the bounded upload:// bridge when you cannot trust a remote host. To allow an
HTTPS source, list exact hostnames, for example:
MCP_OUTBOUND_HOST_ALLOWLIST=docs.example.com,cdn.example.com
MCP_OUTBOUND_ALLOWED_PORTS=443Plain HTTP remains blocked unless the operator explicitly sets
MCP_ALLOW_INSECURE_OUTBOUND_HTTP=true; that opt-in enables only standard HTTP/80. Nonstandard HTTP
ports remain forbidden. MCP_OUTBOUND_ALLOWED_PORTS governs trusted HTTPS ports and defaults to
443. The outbound document allowlist is separate from MCP_QDRANT_HOST_ALLOWLIST, which constrains
request-scoped Qdrant connector URLs.
Request-scoped custom OpenAI-compatible base URLs are also fail closed. The default policy allows
only api.openai.com on port 443. Add an exact trusted hostname to MCP_OPENAI_HOST_ALLOWLIST and
its trusted HTTPS port to MCP_OPENAI_ALLOWED_PORTS before accepting a custom
X-OpenAI-Base-URL. These allowlists never authorize private, loopback, link-local, or cloud
metadata destinations in Portal/request mode.
Request mode may reuse the server's credential-free local FastEmbed default. The stock
standalone-request and Portal Compose profiles blank all server-owned OpenAI credential and endpoint
fields. A direct-Python request-mode deployment configured with a server OpenAI default must set
MCP_DISABLE_DEFAULT_EMBEDDING_FALLBACK=true; each embedding request must then supply its own
provider, model, and OpenAI key headers.
| Variable | Purpose | Safe default |
|---|---|---|
MCP_MODE |
standalone or portal; fixed by the Compose profile |
standalone |
MCP_ACCESS_TOKEN |
Bearer token for standalone HTTP | Empty; HTTP startup must fail |
MCP_PORTAL_GRANT_TOKEN |
Shared broker-to-server Portal grant | Empty; Portal startup must fail |
MCP_PORTAL_GRANT_HEADER |
Portal grant header name | x-madpanda-portal-grant |
MCP_TENANT_ID_HEADER |
Required Portal tenant header name | x-madpanda-user-id |
QDRANT_CREDENTIAL_MODE |
server or request; fixed by Compose profile and restart required |
server in standalone, request in standalone-request/Portal |
MCP_QDRANT_HOST_ALLOWLIST |
Allowed request-scoped Qdrant hostnames | Empty; request mode must fail closed |
MCP_QDRANT_ALLOWED_PORTS |
Allowed request-scoped Qdrant ports | 443,6333 |
MCP_OPENAI_HOST_ALLOWLIST |
Allowed request-scoped OpenAI-compatible hosts | api.openai.com |
MCP_OPENAI_ALLOWED_PORTS |
Allowed request-scoped OpenAI-compatible ports | 443 |
MCP_DISABLE_DEFAULT_EMBEDDING_FALLBACK |
Require request-scoped embedding headers; mandatory when the server default is OpenAI | false for local FastEmbed |
FASTEMBED_MODEL_PATH |
Trusted preinstalled FastEmbed directory; enables specific_model_path plus offline-only loading |
Unset for direct Python; fixed inside the stock image |
FASTEMBED_MODEL_REVISION |
Source revision reported for the preinstalled model | Unset for direct Python; fixed to the reviewed commit in the stock image |
MCP_OUTBOUND_HOST_ALLOWLIST |
Allowed HTTPS document/textbook hosts | Empty; URL ingest denied |
MCP_OUTBOUND_ALLOWED_PORTS |
Allowed outbound URL ports | 443 |
MCP_ALLOW_INSECURE_OUTBOUND_HTTP |
Explicit plain-HTTP opt-in | false |
MCP_ALLOWED_HOSTS |
Accepted HTTP Host patterns | Loopback and all three Compose service names |
MCP_ALLOWED_ORIGINS |
Exact allowed browser origins | Empty |
MCP_REQUEST_BODY_MAX_BYTES |
Maximum authenticated HTTP request body | 1048576 |
MCP_REQUEST_BODY_TIMEOUT_SECONDS |
Request-body read timeout | 10 |
QDRANT_READ_ONLY |
Hide or block mutating tools | false |
MCP_ADMIN_TOOLS_ENABLED |
Enable the two optional admin-only tools, for 79 active tools instead of 77 | false |
QDRANT_ALLOW_ARBITRARY_FILTER |
Permit raw provider filters | false |
Upload and background-job quotas are enforced per server process/container, not across a replicated
fleet. In Portal mode an owner is the authenticated Portal tenant. Standalone and local modes use
the shared process principal, so all callers of that process share one owner budget.
The stock container's ephemeral /tmp is a 1 GiB tmpfs shared by uploads, job records, OCR
intermediates, and other temporary data.
| Quota variable | Default |
|---|---|
MCP_FILE_UPLOAD_MAX_ACTIVE_PER_OWNER |
4 |
MCP_FILE_UPLOAD_MAX_ACTIVE_GLOBAL |
32 |
MCP_FILE_UPLOAD_MAX_AGGREGATE_BYTES_PER_OWNER |
268435456 (256 MiB) |
MCP_FILE_UPLOAD_MAX_AGGREGATE_BYTES_GLOBAL |
536870912 (512 MiB) |
MCP_BACKGROUND_JOB_MAX_ACTIVE_PER_OWNER |
2 |
MCP_BACKGROUND_JOB_MAX_ACTIVE_GLOBAL |
16 |
MCP_BACKGROUND_JOB_MAX_RETAINED_PER_OWNER |
20 |
MCP_BACKGROUND_JOB_MAX_RETAINED_GLOBAL |
100 |
MCP_BACKGROUND_JOB_LOG_MESSAGE_MAX_CHARS |
512 |
MCP_BACKGROUND_JOB_MAX_LOGS_PER_JOB |
100 |
MCP_BACKGROUND_JOB_MAX_LOG_TAIL |
100 |
MCP_BACKGROUND_JOB_MAX_RESULT_BYTES |
262144 |
MCP_BACKGROUND_JOB_MAX_RECORD_BYTES |
524288 |
See the environment template for the complete placeholder-only surface. Allowlist and workload limits are deployment policy, not universal recommendations; tune them for the data and hosts you actually trust.
Start with the five native navigation tools:
check_configuration— validate access, connector, embedding, cache, and upload readiness without returning credential values.list_capabilities— inspect capability groups and catalog counts; request full descriptors only when a catalog client needs them.find_tools— rank tools for a natural-language task.get_tool_usage— retrieve one complete descriptor before a complex or destructive call.get_endpoint_coverage— inspect provider coverage and deliberate exclusions.
For retrieval, begin with qdrant-build-context, qdrant-find, or qdrant-study-search using small
top_k and output budgets. For large inputs, use document/manifest/textbook workflows rather than
placing whole files in a tool argument. Preview maintenance and destructive work with dry_run=true
where supported, inspect the returned diff, and supply the documented confirmation only after
review. The maintenance playbooks
contain safe sequences.
uv sync --frozen --group dev --python 3.12.13
uv run python -m compileall -q src tests scripts
uv run pytest
uv run ruff check src tests scripts
uv run ruff format --check src tests scripts
uv run python scripts/check_source_safety.py
uv build
uv run twine check dist/*
uv run python scripts/check_package_archives.pyPublic CI uses no live provider credentials. Its authenticated HTTP smokes are provider-free and its local FastEmbed image smoke runs with networking disabled. Tests must use unmistakably synthetic credentials and must not contact a live Qdrant cluster, Portal, tenant, or deployment.
The clean release line is intentionally narrow:
Before tagging, the canonical repository's GitHub Actions identity must have write access to the
qdrant-mcp-server GHCR package namespace. Repository replacement or renaming does not itself
transfer an existing package's Actions access. The linked package must also be public before the
anonymous candidate and promoted-tag gates can pass. As an explicit operator cutover gate, read back
successful public Verify and CodeQL runs for the exact current default-branch SHA before
creating the tag.
- a maintainer creates the exact annotated tag
v2.0.0at the current protectedmaintip; the release job refetches the default branch and requires exact SHA equality; - locked Python tests, lint, dependency audit, secret/history scan, a successful public CodeQL job for the exact commit, package allowlist, deterministic double-build, and Compose validation pass;
- the canonical source repository must already be public before any package mutation;
- the image job publishes only the run-scoped
candidate-<commit-sha>-<workflow-run-id>reference with SBOM and provenance, then verifies, scans, attests, anonymously pulls, and preflights every stable tag as absent or already identical before it starts through Compose with--no-buildand smokes that exact digest; - PyPI Trusted Publishing publishes
mad-mcp-qdrant==2.0.0, and the workflow verifies the exact public files and PyPI provenance; - only after PyPI succeeds does a separate job attach
2.0.0,2.0,2, andlatestto the same candidate digest without rebuilding; the promotion job repeats the absent-or-identical check for race protection, and every promoted tag is rechecked anonymously; - the GitHub Release is created last and records the immutable container digest.
No API token is accepted by the PyPI job. If a gate fails, correct the external condition and rerun the failed jobs in the same workflow run so the run-scoped candidate remains stable. In particular, do not start a new full run to repair a failure after image-tag promotion has begun; the promotion job is retry-safe for absent or already-identical tags and refuses conflicting tags.
- Security policy
- Security model
- Deployment guide
- Portal and request-mode guide
- Contributing
- Support
- Changelog
- Notice and attribution
Apache-2.0 licensed. Qdrant is a trademark of Qdrant Solutions GmbH; this independent project is not affiliated with or endorsed by Qdrant.
