Content
qdrant-mcp
Based on the official [mcp-server-qdrant](https://github.com/qdrant/mcp-server-qdrant) v0.8.1, with additional features:
- **Gemini embedding provider** (`gemini-embedding-2-preview`, 768- **Cohere embedding provider** (`embed-v4.0`, Matryoshka dimensions configurable, default 1536)
- **Voyage embedding provider** (`voyage-code-3`, default 1024 dimensions, suitable for code collections)
- **Ollama embedding provider**
- **Hybrid search** — Dense + Sparse (BM25) dual recall
- **RRF fusion** — Using Qdrant native `Fusion.RRF` to merge dense / sparse results
- **Cross-encoder rerank** — Using FastEmbed cross-encoder for reranking fusion candidates
- **Voyage rerank provider** — Using `rerank-2.5` for API reranking
- **Streamable HTTP transport** — Can run as a shared MCP service
- **Langfuse observability** — Trace / session / score / retrieval details / token cost
- **qdrant-delete tool** — Delete matching memories by similarity threshold
- **Automatic metadata completion** — Automatically add `timestamp` and `is_code` when storing
- **Recency tie-break** — Recent memories are slightly weighted, no longer dominant in sorting
- **Collapse search results by identity** — Default to remove duplicate hits with the same `type+name+category`
- **Metadata governance** — Enforce `type` / `name` / `category`, and automatically maintain `updated_ts`
- **Automatic version pruning** — Automatically clean up high similarity clusters with the same identity / name, keep only the latest 50 versions
- **Deletion with filtering and dry-run** — Preview deletion by category/type/name/source/time range
- **Memory pruning** — Support dry-run pruning of expired, same identity, same name/category, and high similarity old memories
- **Payload index automatic creation** — `metadata.category` / `is_code` / `timestamp`
- **Configurable tool descriptions** — `TOOL_FIND_DESCRIPTION` / `TOOL_STORE_DESCRIPTION` / `TOOL_DELETE_DESCRIPTION` / `TOOL_PRUNE_DESCRIPTION`
## One-Stop Deployment (Recommended)
The `deploy/` directory contains a docker-compose stack that can start `qdrant` + `qdrant-mcp` + `langfuse` + `langfuse-postgres` + `nginx` (with external reverse proxy + TLS) with one click, and all external dependencies are self-contained.
```bash
cd deploy
cp .env.example .env
$EDITOR .env # Fill in DOMAIN_* / COHERE / VOYAGE / LANGFUSE_* keys
docker compose up -d
```
For detailed steps (domain, certificate, Langfuse API key, backup, upgrade), see [`deploy/README.md`](deploy/README.md).
If you only want to run a stdio / streamable-http MCP service locally without UI observation and HTTPS, continue to the "Run" section.
## Run
```bash
uv venv .venv --python 3.12
uv pip install --python .venv/bin/python \
"fastembed>=0.6.0" "fastmcp==2.7.0" "pydantic>=2.10.6,<2.12.0" \
"qdrant-client>=1.12.0" "pydantic-settings" "requests" "langfuse>=2.60.0,<3"
PYTHONPATH=. \
QDRANT_URL="http://your-qdrant-host:6333" \
COLLECTION_NAME="memory" \
EMBEDDING_PROVIDER="cohere" \
EMBEDDING_MODEL="embed-v4.0" \
COHERE_API_KEY="your-key" \
COHERE_OUTPUT_DIMENSION="1536" \
COHERE_VECTOR_NAME="dense"STEMBED_CACHEHOME/Library/Caches/fastembed" \
FASTEMBED_PREWARM_ENABLED="true" \
SPARSE_RETRIEVAL_ENABLED="true" \
SPARSE_EMBEDDING_MODEL="Qdrant/bm25" \
SPARSE_VECTOR_NAME="bm25" \
QDRANT_DENSE_PREFETCH_LIMIT="24" \
QDRANTSE_PREFETCH_LIMIT="24" \
QDRANT_FUSION_LIMIT="24" \
RERANKER_ENABLED="true" \
RERANKERvoyage" \
RERANKER_MODEL="rerank-2.5" \
VOYAGE_API_KEY="your-key" \
RERANKER_CANDIDATE_LIMIT="24" \
RERANKER_BATCH_SIZE="16" \
.venv/bin/python run.py
```
Shared HTTP mode:
```bash
PYTHONPATH=. \
QDRANT_URL="http://your-qdrant-host:6333" \
COLLECTION_NAME="memory_v2" \
QDRANT_QUERY_COLLECTIONS="memory_v2,obsidian_v2" \
QDRANT_OBSIDIAN_AB_COLLECTIONS="obsidian_v2,obsidian_v3_ctx" \
EMBEDDING_PROVIDERhere" \
EMBEDDING_MODEL="embed-v4.0" \
COHERE_API_KEY="your-key" \
QDRANT_COLLECTION_EMBEDDERS='{"obsidian_v3_ctx":{"provider":"voyage","model":"voyage-context-3","output_dimension":1024,"vector_name":"dense"}}' \
RERANKER_ENABLED="true" \
RERANKER_PROVIDER="voyage" \
RERANKER_MODEL="rerank-2.5" \
VOYAGE_API_KEY="your-key" \
.venv/bin/python run.py \
--transport streamable-http \
--host 127.0.0.1 \
--port 8765 \
--path /mcp/ \
--log-level warning
```
## MCP Client Configuration
`stdio` mode:
```json
{
"mcpServers": {
"qdrant": {
"command": "/path/to/qdrant-mcp/.venv/bin/python",
"args": ["/path/to/qdrant-mcp/run.py"],
"env": {
"PYTHONPATH": "/path/to/qdrant-mcp",
"QDRANT_URL": "...",
"COLLECTION_NAME": "memory",
"EMBEDDING_PROVIDER": "cohere",
"COHERE_API_KEY": "...",
"COHERE_OUTPUT_DIMENSION": "1536",
"FASTEMBED_CACHE_PATH": "/Users/you/Library/Caches/fastembed",
"FASTEMBED_PREWARM_ENABLED": "true",
"SPARSE_RETRIEVAL_ENABLED": "true",
"SPARSE_EMBEDDING_MODEL": "Qdrant/bm25",
"SPARSE_VECTOR_NAME": "bm25",
"QDRANT_DENSE_PREFETCH_LIMIT": "24",
"QDRANT_SPARSE_PREFETCH_LIMIT": "24",
"QDRANT_FUSION_LIMIT": "24",
"RERANKER_ENABLED": "true",
"RERANKER_PROVIDER": "voyage",
"RERANKER_MODEL": "rerank-2.5",
"VOYAGE_API_KEY": "...",
"RERANKER_CANDIDATE_LIMIT": "24",
"TOOL_FIND_DESCRIPTION": "...",
"TOOL_STORE_DESCRIPTION": "...",
"TOOL_DELETE_DESCRIPTION": "...",
"TOOL_PRUNE_DESCRIPTION": "..."
}
}
}
}
```
Shared `streamable-http` mode:
```json
{
"mcpServers": {
"qdrant": {
"type": "http",
"url": "http://127.0.0.1:8765/mcp/"
}
}
}
```
Suitable for Claude Desktop / Codex to share the same backend instance, avoiding each client or session to start a separate `stdio` subprocess.
## Langfuse Observability and Cost
The current retrieval chain writes the following to Langfuse:
- Root trace with `session_id`
- Retrieval details of `qdrant.search` / `qdrant.rank_points`
- Retrieval score (Langfuse score entity)
- `cohere.embed_query` / `cohere.embed_documents` generation
- `voyage.embed_query` / `voyage.embed_documents` generation
- `voyage.rerank` generation
For generation, the billed tokens returned by the provider are reported, so Langfuse can directly calculate the cost.
Built-in custom model prices:
- `embed-v4.0` — `$0.12 / 1M input tokens`
- `voyage-code-3` — `$0.18 / 1M input tokens`
- `rerank-2.5` — `$0.05 / 1M input tokens`
To sync model definitions to Langfuse project:
```bash
PYTHONPATH=. .venv/bin/python scripts/sync_langfuse_models.py
```
Note:
- Old traces will not be filled with historical costs, only new ingestion takes effect
- If only traces/spans are seen but no cost, it's likely the client doesn't have the `langfuse` package installed, or the current call chain hasn't reached the generation tracing point
## Tuning Parameters
Default retrieval chain:
```text
query
-> dense prefetch top 24
-> sparse prefetch top 24
-> RRF fusion top 24
-> rerank top 12
-> final top N
```
Common parameters:
- `QDRANT_DENSE_PREFETCH_LIMIT` — Dense recall pool size, default `24`
- `QDRANT_SPARSE_PREFETCH_LIMIT` — Sparse recall pool size, default `24`
- `QDRANT_FUSION_LIMIT` — Candidate pool size after RRF fusion, default `24`
- `QDRANT_QUERY_COLLECTIONS` — Default query collection list, comma-separated, e.g., `memory_v2,obsidian_v2`
- `QDRANT_QUERY_INTENT_ROUTES` — JSON list of query text routes to specific collections, matched routes override default collection selection (first hit applies). e.g., `[{"name":"finance","pattern":"Monarch|账户|资产分类|币种|净资产|现金流|贷款|信用卡","collections":"obsidian_v2"}]`; pattern is `re.search` semantic and case-insensitive, `collections` if containing `obsidian_v2` will automatically expand to `QDRANT_OBSIDIAN_AB_COLLECTIONS` corresponding A/B group. Unconfigured maintains original `default_expanded` behavior
- `QDRANT_COLLECTION_EMBEDDERS` — JSON mapping of collection to dense embedder, e.g., `{"code_v1":{"provider":"voyage","model":"voyage-code-3","output_dimension":1024}}`
- `RERANKER_CANDIDATE_LIMIT` — Number of candidates entering cross-encoder, default `24`
- `RERANKER_PROVIDER` — `fastembed` or `voyage`
- `RERANKER_MODEL` — `fastembed` default `jinaai/jina-reranker-v2-base-multilingual`; `voyage` recommended `rerank-2.5`
- `SPARSE_EMBEDDING_MODEL` — Default `Qdrant/bm25`
- `FASTEMBED_CACHE_PATH` — Persistent cache directory shared by sparse / rerank / FastEmbed dense models; unset will fallback to macOS `~/Library/Caches/fastembed` or Linux `~/.cache/fastembed`
- `FASTEMBED_PREWARM_ENABLED` — Default `true`; preheat sparse and reranker in background after service startup to avoid first real query downloading and loading models
Recency tie-break can now be configured via environment variables, default is still lightweight; sorting prioritizes `updated_ts`, falls back to `updated` / `timestamp` when absent:
- `QDRANT_RECENCY_HALF_LIFE_DAYS=90`
- `QDRANT_RECENCY_BONUS_WEIGHT=0.02`
- `QDRANT_RECENCY_FETCH_MULTIPLIER=2`
Note:
- `FASTEMBED_CACHE_PATH` now affects sparse and reranker, not just cross-encoder
- Preheating is a background thread, doesn't change the retrieval chain; still `dense + sparse -> RRF -> rerank`
- If running in a container or read-only environment, remember to point `FASTEMBED_CACHE_PATH` to a writable directory
## Current Recommended Architecture
The current recommendation is two main retrieval collections:
- `memory_v2`
- Full migration of memory
- One memory = one point
- No chunking
- `dense(Cohere embed-v4, 1536d) + sparse(BM25)`
- `obsidian_v2`
- Only retain `dashboard / override / spec`
- Use structure-aware chunking
- `dense(Cohere embed-v4, 1536d) + sparse(BM25)`
- If the number of code scripts is not large, it is recommended to directly merge them into `memory_v2`
The default query still has only one public entrance `qdrant-find`. The minimum available configuration is:
```bash
QDRANT_QUERY_COLLECTIONS="memory_v2,obsidian_v2"
QDRANT_OBSIDIAN_AB_COLLECTIONS="obsidian_v2,obsidian_v3_ctx"
EMBEDDING_PROVIDER="cohere"
EMBEDDING_MODEL="embed-v4.0"
COHERE_OUTPUT_DIMENSION="1536"
COHERE_VECTOR_NAME="dense"
QDRANT_CROSS_COLLECTION_FUSION="rrf"
QDRANT_CROSS_COLLECTION_RRF_K="60"
QDRANT_COLLECTION_EMBEDDERS='{"obsidian_v3_ctx":{"provider":"voyage","model":"voyage-context-3","output_dimension":1024,"vector_name":"dense"}}'
```
Meaning:
- `memory_v2` / `obsidian_v2` continue to use the default dense provider, which is `cohere embed-v4.0`
- `qdrant-find` does not look at the query text; it only performs internal expansion according to the collection configuration
- If the default collection contains `obsidian_v2` and `QDRANT_OBSIDIAN_AB_COLLECTIONS` is configured, it will internally expand to `obsidian_v2,obsidian_v3_ctx`
- `obsidian_v3_ctx` uses a separate `voyage-context-3` model and only participates in Obsidian A/B or ensemble
- `QDRANT_CROSS_COLLECTION_FUSION=rrf` will perform cross-collection RRF before final reranking to avoid duplicate top-ranking of the same chunk by `obsidian_v2` / `obsidian_v3_ctx`
- The `vector_name` must be consistent with the actual named vector of the collection; currently, all three libraries use `dense`
The default call chain is as follows:
```text
user query
-> memory_v2: dense top-k + sparse top-k
-> obsidian_v2: dense top-k + sparse top-k
-> obsidian_v3_ctx: dense top-k + sparse top-k
-> per-collection RRF
-> cross-collection RRF
-> rerank (Voyage rerank-2.5 or FastEmbed)
-> optional grounded generation
```
## End-to-End Calling Process
If you enable the currently recommended configuration, the entire calling sequence is:
```text
1. Client / MCP tool call
2. qdrant-find
3. Cohere embed query -> dense query vector
4. BM25 tokenize -> sparse query vector
5. Qdrant hybrid prefetch
6. RRF fusion
7. Rerank
- default recommended: Voyage rerank-2.5
- fallback option: FastEmbed cross-encoder
8. Return retrieved entries
9. Optional generation
- current helper script: Claude Haiku 4.5 via Anthropic Messages API
```
Description:
- `qdrant-mcp` itself is mainly responsible for retrieval and storage, and does not strongly depend on generation.
- `scripts/query_rag.py` is an end-to-end verification script used to connect "retrieval + generation".
- If you only want to verify retrieval, you can use `--no-generate`.
## Migrating to the New Architecture
New script:
```bash
PYTHONPATH=. \
QDRANT_URL="http://your-qdrant-host:6333" \
COHERE_API_KEY="your-key" \
VOYAGE_API_KEY="your-key" \
.venv/bin/python scripts/migrate_rag_architecture.py --apply
```
This script will:
- Delete `test_knowledge`
- Fully migrate `memory` to `memory_v2`
- Rebuild `obsidian_v2` according to filtering rules
- Apply structure-aware chunking to `obsidian`
- Write `dense + sparse` named vectors to `memory_v2` and `obsidian_v2`
If the migration is interrupted, you can directly continue running:
```bash
PYTHONPATH=. \
QDRANT_URL="http://your-qdrant-host:6333" \
COHERE_API_KEY="your-key" \
VOYAGE_API_KEY="your-key" \
.venv/bin/python scripts/migrate_rag_architecture.py --apply --resume
```
`--resume` will skip the point IDs that already exist in the target collection to avoid duplicate embedding requests.
After migration, you can directly use the query script to verify:
```bash
PYTHONPATH=. \
QDRANT_QUERY_COLLECTIONS="memory_v2,obsidian_v2" \
QDRANT_OBSIDIAN_AB_COLLECTIONS="obsidian_v2,obsidian_v3_ctx" \
QDRANT_COLLECTION_EMBEDDERS='{"obsidian_v3_ctx":{"provider":"voyage","model":"voyage-context-3","output_dimension":1024,"vector_name":"dense"}}' \
ANTHROPIC_API_KEY="your-key" \
.venv/bin/python scripts/query_rag.py "your question"
```
Verify only retrieval:
```bash
PYTHONPATH=. \
QDRANT_QUERY_COLLECTIONS="memory_v2,obsidian_v2" \
QDRANT_OBSIDIAN_AB_COLLECTIONS="obsidian_v2,obsidian_v3_ctx" \
QDRANT_COLLECTION_EMBEDDERS='{"obsidian_v3_ctx":{"provider":"voyage","model":"voyage-context-3","output_dimension":1024,"vector_name":"dense"}}' \
.venv/bin/python scripts/query_rag.py "your question" --no-generate
```
Obsidian `context-3` bypass reconstruction:
```bash
PYTHONPATH=. \
QDRANT_URL="http://your-qdrant-host:6333" \
VOYAGE_API_KEY="your-key" \
.venv/bin/python scripts/reembed_obsidian_context.py --apply
```
Compare the raw score / RRF / rerank of `obsidian_v2` and `obsidian_v3_ctx`:
```bash
PYTHONPATH=. \
QDRANT_OBSIDIAN_AB_COLLECTIONS="obsidian_v2,obsidian_v3_ctx" \
QDRANT_CROSS_COLLECTION_FUSION="rrf" \
QDRANT_COLLECTION_EMBEDDERS='{"obsidian_v3_ctx":{"provider":"voyage","model":"voyage-context-3","output_dimension":1024,"vector_name":"dense"}}' \
VOYAGE_API_KEY="your-key" \
COHERE_API_KEY="your-key" \
.venv/bin/python scripts/compare_obsidian_retrievers.py "your question" --json
```
## Local MCP Configuration Suggestions
It is recommended to separate the default write and default query:
- `COLLECTION_NAME=memory_v2`
- `qdrant-store` writes to here by default
- `QDRANT_QUERY_COLLECTIONS=memory_v2,obsidian_v2`
- The base collection for `qdrant-find`
- `QDRANT_OBSIDIAN_AB_COLLECTIONS=obsidian_v2,obsidian_v3_ctx`
- `qdrant-find` will automatically expand `obsidian_v2` in the default collection to this set of collections
- `QDRANT_COLLECTION_EMBEDDERS={"obsidian_v3_ctx":{...}}`
- When `qdrant-find` hits `obsidian_v3_ctx`, it will route to a separate dense provider according to the collection
Recommended environment variables:
```json
{
"PYTHONPATH": "/path/to/qdrant-mcp",
"QDRANT_URL": "http://your-qdrant-host:6333",
"COLLECTION_NAME": "memory_v2",
"QDRANT_QUERY_COLLECTIONS": "memory_v2,obsidian_v2",
"QDRANT_OBSIDIAN_AB_COLLECTIONS": "obsidian_v2,obsidian_v3_ctx",
"QDRANT_CROSS_COLLECTION_FUSION": "rrf",
"QDRANT_CROSS_COLLECTION_RRF_K": "60",
"EMBEDDING_PROVIDER": "cohere",
"EMBEDDING_MODEL": "embed-v4.0",
"COHERE_API_KEY": "...",
"COHERE_OUTPUT_DIMENSION": "1536",
"COHERE_VECTOR_NAME": "dense",
"QDRANT_COLLECTION_EMBEDDERS": "{\"obsidian_v3_ctx\":{\"provider\":\"voyage\",\"model\":\"voyage-context-3\",\"output_dimension\":1024,\"vector_name\":\"dense\"}}",
"SPARSE_RETRIEVAL_ENABLED": "true",
"SPARSE_EMBEDDING_MODEL": "Qdrant/bm25",
"SPARSE_VECTOR_NAME": "bm25",
"RERANKER_ENABLED": "true",
"RERANKER_PROVIDER": "voyage",
"RERANKER_MODEL": "rerank-2.5",
"VOYAGE_API_KEY": "..."
}
```
## Claude / Codex MCP Verification
After modifying the MCP configuration, it is recommended to verify using the following two methods:
1. Code-level verification
```bash
PYTHONPATH=. .venv/bin/python -m py_compile \
mcp_server_qdrant/embeddings/cohere.py \
mcp_server_qdrant/reranker.py \
mcp_server_qdrant/generation.py \
mcp_server_qdrant/qdrant.py \
scripts/migrate_rag_architecture.py \
scripts/query_rag.py
```
2. Behavior-level verification
```bash
PYTHONPATH=. \
QDRANT_QUERY_COLLECTIONS="memory_v2,obsidian_v2" \
.venv/bin/python scripts/query_rag.py "Monarch override rule" --no-generate
```
If this command returns retrieval results, it means:
- The embedding provider is available
- The sparse provider is available
- The reranker configuration can be loaded
- The default query across multiple collections is normal
## Backfilling Sparse Vectors for Existing Collections
If an existing collection previously only had dense vectors, the service will automatically supplement the sparse schema after restarting, but old points will not automatically supplement sparse values.
You can use a script to backfill once:
```bash
PYTHONPATH=. \
QDRANT_URL="http://your-qdrant-host:6333" \
COLLECTION_NAME="memory" \
SPARSE_RETRIEVAL_ENABLED="true" \
SPARSE_EMBEDDING_MODEL="Qdrant/bm25" \
SPARSE_VECTOR_NAME="bm25" \
.venv/bin/python scripts/backfill_sparse_vectors.py --collection memory
```
You can add `--dry-run` for a dry run.
## Metadata Constraints
`qdrant-store` now requires `metadata` to contain at least:
- `type`
- `name`
- `category`
Optional but recommended:
- `source`
- `updated` (ISO date or time, the system will automatically generate `updated_ts`)
- `expires_at`
The system will not automatically expire and delete memories due to their age.
The system will automatically prune overlapping memories with the same topic after writing, retaining only the 50 most recent versions.
## Pruning Memories
MCP adds `qdrant-prune`, which defaults to `dry_run=true`, to preview the following pruning actions:
- `expired`
- `identity_duplicates`
- `same_name`
- `semantic_duplicates`
You can also use a script:
```bash
PYTHONPATH=. \
QDRANT_URL="http://your-qdrant-host:6333" \
COLLECTION_NAME="memory" \
EMBEDDING_PROVIDER="gemini" \
EMBEDDING_MODEL="gemini-embedding-2-preview" \
GEMINI_API_KEY="your-key" \
GEMINI_EMBEDDING_DIM="768" \
GEMINI_VECTOR_NAME="gemini-embedding" \
.venv/bin/python scripts/prune_memory.py --strategy semantic_duplicates --strategy expired
```
Execute deletion only when adding `--apply`.
## Auditing Existing Memories
```bash
PYTHONPATH=. \
QDRANT_URL="http://your-qdrant-host:6333" \
COLLECTION_NAME="memory" \
EMBEDDING_PROVIDER="gemini" \
EMBEDDING_MODEL="gemini-embedding-2-preview" \
GEMINI_API_KEY="your-key" \
GEMINI_EMBEDDING_DIM="768" \
GEMINI_VECTOR_NAME="gemini-embedding" \
.venv/bin/python scripts/audit_memory.py --top-k 20
```
The script will report:
- The number of entries missing `type/name/category/timestamp/is_code`
- Classification distribution
- Duplicate entries with the same identity or name
- Overlapping memory pairs with high semantic similarity
Connection Info
You Might Also Like
Train-in-Silence
The first Task-Aware MCP server and automated VRAM calculator for LLM...
stacklit
108,000 lines of code. 4,000 tokens of index. One command makes any repo...
AppClaw
AI-powered mobile automation agent — describe what you want in plain...
pdf-mcp
Production-ready MCP server for PDF processing with intelligent caching....
kotadb
Local-only code intelligence API for AI developer workflows (Bun +...
gemini-api-docs-mcp
A remote HTTP MCP server for searching Google Gemini API documentation.