mcp-qdrant 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcp_qdrant-1.0.0/LICENSE +21 -0
- mcp_qdrant-1.0.0/PKG-INFO +148 -0
- mcp_qdrant-1.0.0/README.md +110 -0
- mcp_qdrant-1.0.0/pyproject.toml +80 -0
- mcp_qdrant-1.0.0/pyproject.toml.orig +70 -0
- mcp_qdrant-1.0.0/src/mcp_qdrant/__init__.py +1 -0
- mcp_qdrant-1.0.0/src/mcp_qdrant/cli.py +35 -0
- mcp_qdrant-1.0.0/src/mcp_qdrant/config.py +87 -0
- mcp_qdrant-1.0.0/src/mcp_qdrant/http_auth.py +29 -0
- mcp_qdrant-1.0.0/src/mcp_qdrant/logging_setup.py +28 -0
- mcp_qdrant-1.0.0/src/mcp_qdrant/py.typed +0 -0
- mcp_qdrant-1.0.0/src/mcp_qdrant/qdrant_client.py +51 -0
- mcp_qdrant-1.0.0/src/mcp_qdrant/server.py +63 -0
- mcp_qdrant-1.0.0/src/mcp_qdrant/tools/__init__.py +0 -0
- mcp_qdrant-1.0.0/src/mcp_qdrant/tools/collections.py +213 -0
- mcp_qdrant-1.0.0/src/mcp_qdrant/tools/core.py +49 -0
- mcp_qdrant-1.0.0/src/mcp_qdrant/tools/errors.py +49 -0
- mcp_qdrant-1.0.0/src/mcp_qdrant/tools/observability.py +200 -0
- mcp_qdrant-1.0.0/src/mcp_qdrant/tools/payload.py +219 -0
- mcp_qdrant-1.0.0/src/mcp_qdrant/tools/points.py +184 -0
- mcp_qdrant-1.0.0/src/mcp_qdrant/tools/points_shared.py +28 -0
- mcp_qdrant-1.0.0/src/mcp_qdrant/tools/query.py +80 -0
- mcp_qdrant-1.0.0/src/mcp_qdrant/tools/query_shared.py +65 -0
- mcp_qdrant-1.0.0/src/mcp_qdrant/tools/registry.py +57 -0
- mcp_qdrant-1.0.0/src/mcp_qdrant/tools/search.py +483 -0
- mcp_qdrant-1.0.0/src/mcp_qdrant/tools/snapshots.py +284 -0
- mcp_qdrant-1.0.0/src/mcp_qdrant/tools/vectors.py +238 -0
mcp_qdrant-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adrián
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: mcp-qdrant
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: MCP server that wraps the Qdrant vector database API as tools.
|
|
5
|
+
Author: Adrián
|
|
6
|
+
Author-email: Adrián <adrianvazvaz.2117@gmail.com>
|
|
7
|
+
License: MIT License
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2026 Adrián
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
13
|
+
in the Software without restriction, including without limitation the rights
|
|
14
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
16
|
+
furnished to do so, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
The above copyright notice and this permission notice shall be included in all
|
|
19
|
+
copies or substantial portions of the Software.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
|
+
SOFTWARE.
|
|
28
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
29
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
30
|
+
Requires-Dist: mcp==2.1.1
|
|
31
|
+
Requires-Dist: qdrant-client==1.19.0
|
|
32
|
+
Requires-Dist: pydantic>=2.12.0,<3
|
|
33
|
+
Requires-Dist: tenacity>=9.1.4,<10
|
|
34
|
+
Requires-Dist: uvicorn>=0.52.4,<1
|
|
35
|
+
Requires-Python: >=3.12
|
|
36
|
+
Project-URL: Repository, https://github.com/avaazquezz/Qdrant-MCP
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
# Qdrant-MCP
|
|
40
|
+
|
|
41
|
+
MCP server that wraps the Qdrant vector database API as tools. See [ROADMAP.md](ROADMAP.md).
|
|
42
|
+
|
|
43
|
+
## Tools
|
|
44
|
+
|
|
45
|
+
Generated from the live tool registry — run `uv run python scripts/gen_tools_doc.py` after adding or changing a tool.
|
|
46
|
+
|
|
47
|
+
<!-- TOOLS_TABLE_START -->
|
|
48
|
+
| Tool | Toolset | Read-only | Destructive | Idempotent | Description |
|
|
49
|
+
|---|---|---|---|---|---|
|
|
50
|
+
| `qdrant_health_check` | `core` | ✅ | ❌ | ✅ | Confirm the configured Qdrant instance is reachable and responding. |
|
|
51
|
+
| `qdrant_collection_create` | `core` | ❌ | ❌ | ❌ | Create a collection: either a single unnamed vector (`vector_size` + `distance`), or one or more named vectors (`vectors`, each a full `VectorParams` — size, distance, and optionally its own `multivector_config` for ColBERT-style multi-vectors or `quantization_config`) — exactly one of the two. |
|
|
52
|
+
| `qdrant_collection_list` | `core` | ✅ | ❌ | ✅ | List every collection name in the configured Qdrant instance. |
|
|
53
|
+
| `qdrant_collection_info` | `core` | ✅ | ❌ | ✅ | Return full config and status of one collection. |
|
|
54
|
+
| `qdrant_collection_update` | `core` | ❌ | ❌ | ✅ | Update optimizer/HNSW/collection/vector params on an existing collection. |
|
|
55
|
+
| `qdrant_collection_delete` | `core` | ❌ | ✅ | ✅ | Delete a collection and all its points; a no-op if it doesn't exist. |
|
|
56
|
+
| `qdrant_collection_exists` | `core` | ✅ | ❌ | ✅ | Check whether a collection exists, without raising if it doesn't. |
|
|
57
|
+
| `qdrant_points_upsert` | `core` | ❌ | ✅ | ✅ | Insert or replace points (id + vector + payload) in a collection. |
|
|
58
|
+
| `qdrant_points_get` | `core` | ✅ | ❌ | ✅ | Retrieve points by id; unknown ids are simply omitted, not an error. |
|
|
59
|
+
| `qdrant_points_delete` | `core` | ❌ | ✅ | ✅ | Delete points by id list or by payload filter — exactly one of the two. |
|
|
60
|
+
| `qdrant_points_scroll` | `core` | ✅ | ❌ | ✅ | Page through all points in a collection, optionally filtered. |
|
|
61
|
+
| `qdrant_points_count` | `core` | ✅ | ❌ | ✅ | Count points in a collection, optionally matching a filter. |
|
|
62
|
+
| `qdrant_query` | `core` | ✅ | ❌ | ✅ | Vector similarity search, with optional hybrid search over multiple prefetch stages. |
|
|
63
|
+
| `qdrant_query_batch` | `search` | ✅ | ❌ | ✅ | Run multiple independent queries against one collection in a single round trip — same query shapes as `qdrant_query` (plain vector or fusion+prefetch hybrid search), one per list item. |
|
|
64
|
+
| `qdrant_query_groups` | `search` | ✅ | ❌ | ✅ | Vector query grouped by a payload field, up to `group_size` hits per group — e.g. the best-matching chunks per source document. |
|
|
65
|
+
| `qdrant_recommend` | `search` | ✅ | ❌ | ✅ | Find points similar to a set of positive examples and dissimilar to a set of negative ones (vectors or point ids) — Qdrant's recommendation API. |
|
|
66
|
+
| `qdrant_recommend_batch` | `search` | ✅ | ❌ | ✅ | Run multiple independent recommend queries against one collection in a single round trip. |
|
|
67
|
+
| `qdrant_recommend_groups` | `search` | ✅ | ❌ | ✅ | Recommend query grouped by a payload field, up to `group_size` hits per group. |
|
|
68
|
+
| `qdrant_discover` | `search` | ✅ | ❌ | ✅ | Rank points by how well they fit a target within positive/negative context pairs (vectors or point ids) — Qdrant's discovery search, a finer-grained alternative to recommend. |
|
|
69
|
+
| `qdrant_discover_batch` | `search` | ✅ | ❌ | ✅ | Run multiple independent discover queries against one collection in a single round trip. |
|
|
70
|
+
| `qdrant_distance_matrix_pairs` | `search` | ✅ | ❌ | ✅ | Pairwise distance matrix between a random sample of points: for each of `sample` points, its `limit` closest neighbors among that same sample — returned as a flat list of (a, b, score) pairs. |
|
|
71
|
+
| `qdrant_distance_matrix_offsets` | `search` | ✅ | ❌ | ✅ | Same distance matrix as `qdrant_distance_matrix_pairs`, in a column-oriented shape (offsets into a shared id list + a parallel score array) — more compact for large samples. |
|
|
72
|
+
| `qdrant_payload_set` | `payload` | ❌ | ❌ | ✅ | Merge fields into the payload of selected points — exactly one of `ids`/`points_filter`. |
|
|
73
|
+
| `qdrant_payload_overwrite` | `payload` | ❌ | ✅ | ✅ | Replace the entire payload of selected points with `payload` — exactly one of `ids`/`points_filter`. |
|
|
74
|
+
| `qdrant_payload_delete` | `payload` | ❌ | ✅ | ✅ | Delete specific payload keys from selected points — exactly one of `ids`/`points_filter`. |
|
|
75
|
+
| `qdrant_payload_clear` | `payload` | ❌ | ✅ | ✅ | Wipe the entire payload of selected points, keeping their vectors — exactly one of `ids`/`points_filter`. |
|
|
76
|
+
| `qdrant_payload_facet` | `payload` | ✅ | ❌ | ✅ | Count distinct values of a payload field across the collection (or a filtered subset) — e.g. how many points per `city`. |
|
|
77
|
+
| `qdrant_payload_index_create` | `payload` | ❌ | ❌ | ✅ | Create a payload index on `field_name`, speeding up filters that use it. |
|
|
78
|
+
| `qdrant_payload_index_delete` | `payload` | ❌ | ✅ | ✅ | Delete the payload index on `field_name`. |
|
|
79
|
+
| `qdrant_collection_vector_create` | `payload` | ❌ | ❌ | ✅ | Add a new named vector (dense or sparse) to a collection that already has points, without touching them. |
|
|
80
|
+
| `qdrant_collection_vector_delete` | `payload` | ❌ | ✅ | ✅ | Remove a named vector (dense or sparse) from a collection — points keep their other vectors and payload. |
|
|
81
|
+
| `qdrant_points_batch_update` | `payload` | ❌ | ✅ | ❌ | Run multiple point operations (upsert, delete, set/overwrite/delete/clear payload, update/delete vectors) atomically against one collection, in the order given. |
|
|
82
|
+
| `qdrant_vectors_update` | `payload` | ❌ | ✅ | ✅ | Replace the vector(s) of existing points by id — leaves their payload untouched. |
|
|
83
|
+
| `qdrant_vectors_delete` | `payload` | ❌ | ✅ | ✅ | Remove specific named vectors from selected points, keeping their payload and other vectors — exactly one of `ids`/`points_filter`. |
|
|
84
|
+
| `qdrant_snapshot_create` | `snapshots` | ❌ | ❌ | ❌ | Create a snapshot of one collection's current state. |
|
|
85
|
+
| `qdrant_snapshot_list` | `snapshots` | ✅ | ❌ | ✅ | List the snapshots stored for one collection. |
|
|
86
|
+
| `qdrant_snapshot_delete` | `snapshots` | ❌ | ✅ | ✅ | Delete a collection snapshot, freeing its disk space on the server — does not touch the live collection. |
|
|
87
|
+
| `qdrant_snapshot_recover` | `snapshots` | ❌ | ✅ | ✅ | Overwrite `collection_name` with the state captured in a snapshot — everything written since that snapshot is lost. |
|
|
88
|
+
| `qdrant_snapshot_download` | `snapshots` | ✅ | ❌ | ✅ | Confirm a collection snapshot exists and return where to fetch it from — this tool does not transfer the (potentially huge) snapshot file itself; download it yourself (e.g. `curl`) from the returned `url`. |
|
|
89
|
+
| `qdrant_storage_snapshot_create` | `snapshots` | ❌ | ❌ | ❌ | Create a snapshot of the whole storage (every collection and server config), not just one collection. |
|
|
90
|
+
| `qdrant_storage_snapshot_list` | `snapshots` | ✅ | ❌ | ✅ | List the full-storage snapshots stored on the server. |
|
|
91
|
+
| `qdrant_storage_snapshot_delete` | `snapshots` | ❌ | ✅ | ✅ | Delete a full-storage snapshot, freeing its disk space. |
|
|
92
|
+
| `qdrant_storage_snapshot_download` | `snapshots` | ✅ | ❌ | ✅ | Confirm a full-storage snapshot exists and return where to fetch it from — same caveat as `qdrant_snapshot_download`: this tool does not transfer the file itself. |
|
|
93
|
+
| `qdrant_telemetry` | `observability` | ✅ | ❌ | ✅ | Server-wide telemetry: build info, per-collection stats, request counters, memory and hardware usage. |
|
|
94
|
+
| `qdrant_metrics_prometheus` | `observability` | ✅ | ❌ | ✅ | Return the URL where Qdrant serves Prometheus-format metrics — this tool does not fetch the metrics themselves (they're plain text, not JSON); point your Prometheus scraper at the returned `url` instead. |
|
|
95
|
+
| `qdrant_quotas_get` | `observability` | ✅ | ❌ | ✅ | Current server-wide resource quotas (memory/disk limits) and actual usage. |
|
|
96
|
+
| `qdrant_quotas_set` | `observability` | ❌ | ❌ | ✅ | Update server-wide resource quotas. |
|
|
97
|
+
| `qdrant_issues_list` | `observability` | ✅ | ❌ | ✅ | List the issues Qdrant has detected about its own configuration (e.g. a heavily-filtered field with no payload index). |
|
|
98
|
+
| `qdrant_issues_clear` | `observability` | ❌ | ✅ | ✅ | Clear all accumulated issues. |
|
|
99
|
+
<!-- TOOLS_TABLE_END -->
|
|
100
|
+
|
|
101
|
+
## Configuration
|
|
102
|
+
|
|
103
|
+
Environment variables: `QDRANT_URL`, `QDRANT_API_KEY`, `QDRANT_LOCAL_PATH` (exactly one
|
|
104
|
+
of `QDRANT_URL`/`QDRANT_LOCAL_PATH`), `QDRANT_MCP_READ_ONLY`, `QDRANT_MCP_TRANSPORT`
|
|
105
|
+
(`stdio` default, or `streamable-http`), `QDRANT_MCP_TOOLSETS` (comma-separated;
|
|
106
|
+
default `core` only — opt in to `search`, `payload`, `snapshots`, `observability`
|
|
107
|
+
explicitly).
|
|
108
|
+
|
|
109
|
+
### Claude Desktop / Claude Code (local, `stdio`)
|
|
110
|
+
|
|
111
|
+
`claude_desktop_config.json` (Claude Desktop) or `.mcp.json` (Claude Code):
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"mcpServers": {
|
|
116
|
+
"qdrant": {
|
|
117
|
+
"command": "uvx",
|
|
118
|
+
"args": ["mcp-qdrant"],
|
|
119
|
+
"env": {
|
|
120
|
+
"QDRANT_URL": "http://localhost:6333",
|
|
121
|
+
"QDRANT_MCP_TOOLSETS": "core,search"
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Or double-click the `.mcpb` bundle attached to a [release](https://github.com/avaazquezz/Qdrant-MCP/releases) — Claude Desktop prompts for the same settings through its own UI, no JSON to edit.
|
|
129
|
+
|
|
130
|
+
### Remote (`streamable-http`) — e.g. a custom connector in Claude.ai
|
|
131
|
+
|
|
132
|
+
`QDRANT_MCP_SHARED_SECRET` is **required** in this mode — the server refuses to start
|
|
133
|
+
as `streamable-http` without one, to avoid serving an unauthenticated endpoint over the
|
|
134
|
+
network (verified hands-on: an open `streamable-http` server is trivially usable by
|
|
135
|
+
anyone with the URL).
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
QDRANT_URL=http://localhost:6333 \
|
|
139
|
+
QDRANT_MCP_TRANSPORT=streamable-http \
|
|
140
|
+
QDRANT_MCP_HTTP_HOST=0.0.0.0 \
|
|
141
|
+
QDRANT_MCP_SHARED_SECRET=<a long random secret> \
|
|
142
|
+
mcp-qdrant
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
In Claude.ai (**Customize → Connectors → Add custom connector**, verified hands-on
|
|
146
|
+
against a real account): enter the server's HTTPS URL, then on the detected
|
|
147
|
+
authentication screen choose **"None"** and add a **Request header** —
|
|
148
|
+
`Authorization` → `Bearer <the same secret>`.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Qdrant-MCP
|
|
2
|
+
|
|
3
|
+
MCP server that wraps the Qdrant vector database API as tools. See [ROADMAP.md](ROADMAP.md).
|
|
4
|
+
|
|
5
|
+
## Tools
|
|
6
|
+
|
|
7
|
+
Generated from the live tool registry — run `uv run python scripts/gen_tools_doc.py` after adding or changing a tool.
|
|
8
|
+
|
|
9
|
+
<!-- TOOLS_TABLE_START -->
|
|
10
|
+
| Tool | Toolset | Read-only | Destructive | Idempotent | Description |
|
|
11
|
+
|---|---|---|---|---|---|
|
|
12
|
+
| `qdrant_health_check` | `core` | ✅ | ❌ | ✅ | Confirm the configured Qdrant instance is reachable and responding. |
|
|
13
|
+
| `qdrant_collection_create` | `core` | ❌ | ❌ | ❌ | Create a collection: either a single unnamed vector (`vector_size` + `distance`), or one or more named vectors (`vectors`, each a full `VectorParams` — size, distance, and optionally its own `multivector_config` for ColBERT-style multi-vectors or `quantization_config`) — exactly one of the two. |
|
|
14
|
+
| `qdrant_collection_list` | `core` | ✅ | ❌ | ✅ | List every collection name in the configured Qdrant instance. |
|
|
15
|
+
| `qdrant_collection_info` | `core` | ✅ | ❌ | ✅ | Return full config and status of one collection. |
|
|
16
|
+
| `qdrant_collection_update` | `core` | ❌ | ❌ | ✅ | Update optimizer/HNSW/collection/vector params on an existing collection. |
|
|
17
|
+
| `qdrant_collection_delete` | `core` | ❌ | ✅ | ✅ | Delete a collection and all its points; a no-op if it doesn't exist. |
|
|
18
|
+
| `qdrant_collection_exists` | `core` | ✅ | ❌ | ✅ | Check whether a collection exists, without raising if it doesn't. |
|
|
19
|
+
| `qdrant_points_upsert` | `core` | ❌ | ✅ | ✅ | Insert or replace points (id + vector + payload) in a collection. |
|
|
20
|
+
| `qdrant_points_get` | `core` | ✅ | ❌ | ✅ | Retrieve points by id; unknown ids are simply omitted, not an error. |
|
|
21
|
+
| `qdrant_points_delete` | `core` | ❌ | ✅ | ✅ | Delete points by id list or by payload filter — exactly one of the two. |
|
|
22
|
+
| `qdrant_points_scroll` | `core` | ✅ | ❌ | ✅ | Page through all points in a collection, optionally filtered. |
|
|
23
|
+
| `qdrant_points_count` | `core` | ✅ | ❌ | ✅ | Count points in a collection, optionally matching a filter. |
|
|
24
|
+
| `qdrant_query` | `core` | ✅ | ❌ | ✅ | Vector similarity search, with optional hybrid search over multiple prefetch stages. |
|
|
25
|
+
| `qdrant_query_batch` | `search` | ✅ | ❌ | ✅ | Run multiple independent queries against one collection in a single round trip — same query shapes as `qdrant_query` (plain vector or fusion+prefetch hybrid search), one per list item. |
|
|
26
|
+
| `qdrant_query_groups` | `search` | ✅ | ❌ | ✅ | Vector query grouped by a payload field, up to `group_size` hits per group — e.g. the best-matching chunks per source document. |
|
|
27
|
+
| `qdrant_recommend` | `search` | ✅ | ❌ | ✅ | Find points similar to a set of positive examples and dissimilar to a set of negative ones (vectors or point ids) — Qdrant's recommendation API. |
|
|
28
|
+
| `qdrant_recommend_batch` | `search` | ✅ | ❌ | ✅ | Run multiple independent recommend queries against one collection in a single round trip. |
|
|
29
|
+
| `qdrant_recommend_groups` | `search` | ✅ | ❌ | ✅ | Recommend query grouped by a payload field, up to `group_size` hits per group. |
|
|
30
|
+
| `qdrant_discover` | `search` | ✅ | ❌ | ✅ | Rank points by how well they fit a target within positive/negative context pairs (vectors or point ids) — Qdrant's discovery search, a finer-grained alternative to recommend. |
|
|
31
|
+
| `qdrant_discover_batch` | `search` | ✅ | ❌ | ✅ | Run multiple independent discover queries against one collection in a single round trip. |
|
|
32
|
+
| `qdrant_distance_matrix_pairs` | `search` | ✅ | ❌ | ✅ | Pairwise distance matrix between a random sample of points: for each of `sample` points, its `limit` closest neighbors among that same sample — returned as a flat list of (a, b, score) pairs. |
|
|
33
|
+
| `qdrant_distance_matrix_offsets` | `search` | ✅ | ❌ | ✅ | Same distance matrix as `qdrant_distance_matrix_pairs`, in a column-oriented shape (offsets into a shared id list + a parallel score array) — more compact for large samples. |
|
|
34
|
+
| `qdrant_payload_set` | `payload` | ❌ | ❌ | ✅ | Merge fields into the payload of selected points — exactly one of `ids`/`points_filter`. |
|
|
35
|
+
| `qdrant_payload_overwrite` | `payload` | ❌ | ✅ | ✅ | Replace the entire payload of selected points with `payload` — exactly one of `ids`/`points_filter`. |
|
|
36
|
+
| `qdrant_payload_delete` | `payload` | ❌ | ✅ | ✅ | Delete specific payload keys from selected points — exactly one of `ids`/`points_filter`. |
|
|
37
|
+
| `qdrant_payload_clear` | `payload` | ❌ | ✅ | ✅ | Wipe the entire payload of selected points, keeping their vectors — exactly one of `ids`/`points_filter`. |
|
|
38
|
+
| `qdrant_payload_facet` | `payload` | ✅ | ❌ | ✅ | Count distinct values of a payload field across the collection (or a filtered subset) — e.g. how many points per `city`. |
|
|
39
|
+
| `qdrant_payload_index_create` | `payload` | ❌ | ❌ | ✅ | Create a payload index on `field_name`, speeding up filters that use it. |
|
|
40
|
+
| `qdrant_payload_index_delete` | `payload` | ❌ | ✅ | ✅ | Delete the payload index on `field_name`. |
|
|
41
|
+
| `qdrant_collection_vector_create` | `payload` | ❌ | ❌ | ✅ | Add a new named vector (dense or sparse) to a collection that already has points, without touching them. |
|
|
42
|
+
| `qdrant_collection_vector_delete` | `payload` | ❌ | ✅ | ✅ | Remove a named vector (dense or sparse) from a collection — points keep their other vectors and payload. |
|
|
43
|
+
| `qdrant_points_batch_update` | `payload` | ❌ | ✅ | ❌ | Run multiple point operations (upsert, delete, set/overwrite/delete/clear payload, update/delete vectors) atomically against one collection, in the order given. |
|
|
44
|
+
| `qdrant_vectors_update` | `payload` | ❌ | ✅ | ✅ | Replace the vector(s) of existing points by id — leaves their payload untouched. |
|
|
45
|
+
| `qdrant_vectors_delete` | `payload` | ❌ | ✅ | ✅ | Remove specific named vectors from selected points, keeping their payload and other vectors — exactly one of `ids`/`points_filter`. |
|
|
46
|
+
| `qdrant_snapshot_create` | `snapshots` | ❌ | ❌ | ❌ | Create a snapshot of one collection's current state. |
|
|
47
|
+
| `qdrant_snapshot_list` | `snapshots` | ✅ | ❌ | ✅ | List the snapshots stored for one collection. |
|
|
48
|
+
| `qdrant_snapshot_delete` | `snapshots` | ❌ | ✅ | ✅ | Delete a collection snapshot, freeing its disk space on the server — does not touch the live collection. |
|
|
49
|
+
| `qdrant_snapshot_recover` | `snapshots` | ❌ | ✅ | ✅ | Overwrite `collection_name` with the state captured in a snapshot — everything written since that snapshot is lost. |
|
|
50
|
+
| `qdrant_snapshot_download` | `snapshots` | ✅ | ❌ | ✅ | Confirm a collection snapshot exists and return where to fetch it from — this tool does not transfer the (potentially huge) snapshot file itself; download it yourself (e.g. `curl`) from the returned `url`. |
|
|
51
|
+
| `qdrant_storage_snapshot_create` | `snapshots` | ❌ | ❌ | ❌ | Create a snapshot of the whole storage (every collection and server config), not just one collection. |
|
|
52
|
+
| `qdrant_storage_snapshot_list` | `snapshots` | ✅ | ❌ | ✅ | List the full-storage snapshots stored on the server. |
|
|
53
|
+
| `qdrant_storage_snapshot_delete` | `snapshots` | ❌ | ✅ | ✅ | Delete a full-storage snapshot, freeing its disk space. |
|
|
54
|
+
| `qdrant_storage_snapshot_download` | `snapshots` | ✅ | ❌ | ✅ | Confirm a full-storage snapshot exists and return where to fetch it from — same caveat as `qdrant_snapshot_download`: this tool does not transfer the file itself. |
|
|
55
|
+
| `qdrant_telemetry` | `observability` | ✅ | ❌ | ✅ | Server-wide telemetry: build info, per-collection stats, request counters, memory and hardware usage. |
|
|
56
|
+
| `qdrant_metrics_prometheus` | `observability` | ✅ | ❌ | ✅ | Return the URL where Qdrant serves Prometheus-format metrics — this tool does not fetch the metrics themselves (they're plain text, not JSON); point your Prometheus scraper at the returned `url` instead. |
|
|
57
|
+
| `qdrant_quotas_get` | `observability` | ✅ | ❌ | ✅ | Current server-wide resource quotas (memory/disk limits) and actual usage. |
|
|
58
|
+
| `qdrant_quotas_set` | `observability` | ❌ | ❌ | ✅ | Update server-wide resource quotas. |
|
|
59
|
+
| `qdrant_issues_list` | `observability` | ✅ | ❌ | ✅ | List the issues Qdrant has detected about its own configuration (e.g. a heavily-filtered field with no payload index). |
|
|
60
|
+
| `qdrant_issues_clear` | `observability` | ❌ | ✅ | ✅ | Clear all accumulated issues. |
|
|
61
|
+
<!-- TOOLS_TABLE_END -->
|
|
62
|
+
|
|
63
|
+
## Configuration
|
|
64
|
+
|
|
65
|
+
Environment variables: `QDRANT_URL`, `QDRANT_API_KEY`, `QDRANT_LOCAL_PATH` (exactly one
|
|
66
|
+
of `QDRANT_URL`/`QDRANT_LOCAL_PATH`), `QDRANT_MCP_READ_ONLY`, `QDRANT_MCP_TRANSPORT`
|
|
67
|
+
(`stdio` default, or `streamable-http`), `QDRANT_MCP_TOOLSETS` (comma-separated;
|
|
68
|
+
default `core` only — opt in to `search`, `payload`, `snapshots`, `observability`
|
|
69
|
+
explicitly).
|
|
70
|
+
|
|
71
|
+
### Claude Desktop / Claude Code (local, `stdio`)
|
|
72
|
+
|
|
73
|
+
`claude_desktop_config.json` (Claude Desktop) or `.mcp.json` (Claude Code):
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"mcpServers": {
|
|
78
|
+
"qdrant": {
|
|
79
|
+
"command": "uvx",
|
|
80
|
+
"args": ["mcp-qdrant"],
|
|
81
|
+
"env": {
|
|
82
|
+
"QDRANT_URL": "http://localhost:6333",
|
|
83
|
+
"QDRANT_MCP_TOOLSETS": "core,search"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Or double-click the `.mcpb` bundle attached to a [release](https://github.com/avaazquezz/Qdrant-MCP/releases) — Claude Desktop prompts for the same settings through its own UI, no JSON to edit.
|
|
91
|
+
|
|
92
|
+
### Remote (`streamable-http`) — e.g. a custom connector in Claude.ai
|
|
93
|
+
|
|
94
|
+
`QDRANT_MCP_SHARED_SECRET` is **required** in this mode — the server refuses to start
|
|
95
|
+
as `streamable-http` without one, to avoid serving an unauthenticated endpoint over the
|
|
96
|
+
network (verified hands-on: an open `streamable-http` server is trivially usable by
|
|
97
|
+
anyone with the URL).
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
QDRANT_URL=http://localhost:6333 \
|
|
101
|
+
QDRANT_MCP_TRANSPORT=streamable-http \
|
|
102
|
+
QDRANT_MCP_HTTP_HOST=0.0.0.0 \
|
|
103
|
+
QDRANT_MCP_SHARED_SECRET=<a long random secret> \
|
|
104
|
+
mcp-qdrant
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
In Claude.ai (**Customize → Connectors → Add custom connector**, verified hands-on
|
|
108
|
+
against a real account): enter the server's HTTPS URL, then on the detected
|
|
109
|
+
authentication screen choose **"None"** and add a **Request header** —
|
|
110
|
+
`Authorization` → `Bearer <the same secret>`.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mcp-qdrant"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "MCP server that wraps the Qdrant vector database API as tools."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
classifiers = [
|
|
8
|
+
"License :: OSI Approved :: MIT License",
|
|
9
|
+
"Programming Language :: Python :: 3.12",
|
|
10
|
+
]
|
|
11
|
+
dependencies = [
|
|
12
|
+
"mcp==2.1.1",
|
|
13
|
+
"qdrant-client==1.19.0",
|
|
14
|
+
"pydantic>=2.12.0,<3",
|
|
15
|
+
"tenacity>=9.1.4,<10",
|
|
16
|
+
"uvicorn>=0.52.4,<1",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.license]
|
|
20
|
+
file = "LICENSE"
|
|
21
|
+
|
|
22
|
+
[[project.authors]]
|
|
23
|
+
name = "Adrián"
|
|
24
|
+
email = "adrianvazvaz.2117@gmail.com"
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
mcp-qdrant = "mcp_qdrant.cli:main"
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Repository = "https://github.com/avaazquezz/Qdrant-MCP"
|
|
31
|
+
|
|
32
|
+
[build-system]
|
|
33
|
+
requires = ["uv_build>=0.12.5,<0.13.0"]
|
|
34
|
+
build-backend = "uv_build"
|
|
35
|
+
|
|
36
|
+
[tool.uv.build-backend]
|
|
37
|
+
module-name = "mcp_qdrant"
|
|
38
|
+
|
|
39
|
+
[tool.ruff]
|
|
40
|
+
line-length = 100
|
|
41
|
+
target-version = "py312"
|
|
42
|
+
|
|
43
|
+
[tool.ruff.lint]
|
|
44
|
+
select = [
|
|
45
|
+
"E",
|
|
46
|
+
"F",
|
|
47
|
+
"I",
|
|
48
|
+
"UP",
|
|
49
|
+
"B",
|
|
50
|
+
"SIM",
|
|
51
|
+
"ASYNC",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
[tool.mypy]
|
|
55
|
+
python_version = "3.12"
|
|
56
|
+
strict = true
|
|
57
|
+
files = [
|
|
58
|
+
"src",
|
|
59
|
+
"tests",
|
|
60
|
+
"scripts",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
[tool.pytest.ini_options]
|
|
64
|
+
testpaths = ["tests"]
|
|
65
|
+
asyncio_mode = "auto"
|
|
66
|
+
addopts = "--cov=src/mcp_qdrant --cov-report=term-missing"
|
|
67
|
+
markers = ["integration: requires a live Qdrant instance (CI provides one as a Docker service)"]
|
|
68
|
+
|
|
69
|
+
[tool.coverage.run]
|
|
70
|
+
source = ["src/mcp_qdrant"]
|
|
71
|
+
|
|
72
|
+
[dependency-groups]
|
|
73
|
+
dev = [
|
|
74
|
+
"mypy>=2.3.1,<3",
|
|
75
|
+
"pre-commit>=4.6.2,<5",
|
|
76
|
+
"pytest>=9.1.1,<10",
|
|
77
|
+
"pytest-asyncio>=1.4.0,<2",
|
|
78
|
+
"pytest-cov>=7.1.0,<8",
|
|
79
|
+
"ruff>=0.16.5,<0.17",
|
|
80
|
+
]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mcp-qdrant"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "MCP server that wraps the Qdrant vector database API as tools."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { file = "LICENSE" }
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "Adrián", email = "adrianvazvaz.2117@gmail.com" }
|
|
9
|
+
]
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
classifiers = [
|
|
12
|
+
"License :: OSI Approved :: MIT License",
|
|
13
|
+
"Programming Language :: Python :: 3.12",
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"mcp==2.1.1",
|
|
17
|
+
"qdrant-client==1.19.0",
|
|
18
|
+
"pydantic>=2.12.0,<3",
|
|
19
|
+
"tenacity>=9.1.4,<10",
|
|
20
|
+
"uvicorn>=0.52.4,<1",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
mcp-qdrant = "mcp_qdrant.cli:main"
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Repository = "https://github.com/avaazquezz/Qdrant-MCP"
|
|
28
|
+
|
|
29
|
+
[build-system]
|
|
30
|
+
requires = ["uv_build>=0.12.5,<0.13.0"]
|
|
31
|
+
build-backend = "uv_build"
|
|
32
|
+
|
|
33
|
+
[tool.uv.build-backend]
|
|
34
|
+
module-name = "mcp_qdrant"
|
|
35
|
+
# No /website directory exists yet (Fase 0). If docs land there later,
|
|
36
|
+
# exclude it from the sdist so it never ships inside the package:
|
|
37
|
+
# source-exclude = ["/website"]
|
|
38
|
+
|
|
39
|
+
[dependency-groups]
|
|
40
|
+
dev = [
|
|
41
|
+
"mypy>=2.3.1,<3",
|
|
42
|
+
"pre-commit>=4.6.2,<5",
|
|
43
|
+
"pytest>=9.1.1,<10",
|
|
44
|
+
"pytest-asyncio>=1.4.0,<2",
|
|
45
|
+
"pytest-cov>=7.1.0,<8",
|
|
46
|
+
"ruff>=0.16.5,<0.17",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[tool.ruff]
|
|
50
|
+
line-length = 100
|
|
51
|
+
target-version = "py312"
|
|
52
|
+
|
|
53
|
+
[tool.ruff.lint]
|
|
54
|
+
select = ["E", "F", "I", "UP", "B", "SIM", "ASYNC"]
|
|
55
|
+
|
|
56
|
+
[tool.mypy]
|
|
57
|
+
python_version = "3.12"
|
|
58
|
+
strict = true
|
|
59
|
+
files = ["src", "tests", "scripts"]
|
|
60
|
+
|
|
61
|
+
[tool.pytest.ini_options]
|
|
62
|
+
testpaths = ["tests"]
|
|
63
|
+
asyncio_mode = "auto"
|
|
64
|
+
addopts = "--cov=src/mcp_qdrant --cov-report=term-missing"
|
|
65
|
+
markers = [
|
|
66
|
+
"integration: requires a live Qdrant instance (CI provides one as a Docker service)",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
[tool.coverage.run]
|
|
70
|
+
source = ["src/mcp_qdrant"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""CLI entrypoint: the `mcp-qdrant` console script."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from mcp_qdrant.config import Settings
|
|
6
|
+
from mcp_qdrant.http_auth import SharedSecretMiddleware
|
|
7
|
+
from mcp_qdrant.logging_setup import configure_logging
|
|
8
|
+
from mcp_qdrant.server import build_server
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main() -> int:
|
|
12
|
+
# Order matters: logging must be configured before MCPServer() exists, so
|
|
13
|
+
# our stderr handler wins over the SDK's own internal logging configuration
|
|
14
|
+
# (a no-op once the root logger already has handlers).
|
|
15
|
+
configure_logging()
|
|
16
|
+
settings = Settings.from_env()
|
|
17
|
+
server = build_server(settings)
|
|
18
|
+
|
|
19
|
+
if settings.transport == "streamable-http":
|
|
20
|
+
# server.run(transport="streamable-http") builds and serves its own
|
|
21
|
+
# app with no hook for middleware, so the shared-secret auth check
|
|
22
|
+
# is wired in by building the app ourselves instead.
|
|
23
|
+
import uvicorn
|
|
24
|
+
|
|
25
|
+
assert settings.shared_secret is not None # enforced by Settings validation
|
|
26
|
+
app = server.streamable_http_app(host=settings.http_host)
|
|
27
|
+
app.add_middleware(SharedSecretMiddleware, secret=settings.shared_secret)
|
|
28
|
+
uvicorn.run(app, host=settings.http_host, port=settings.http_port)
|
|
29
|
+
else:
|
|
30
|
+
server.run(transport=settings.transport)
|
|
31
|
+
return 0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if __name__ == "__main__":
|
|
35
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Environment-driven runtime configuration for mcp_qdrant.
|
|
2
|
+
|
|
3
|
+
All external configuration enters here and only here: Settings.from_env()
|
|
4
|
+
reads os.environ once at startup and validates it with Pydantic so a
|
|
5
|
+
misconfigured deployment fails loudly before the server starts serving
|
|
6
|
+
tool calls, instead of failing obscurely on the first tool call.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from typing import Literal
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel, model_validator
|
|
15
|
+
|
|
16
|
+
Transport = Literal["stdio", "streamable-http"]
|
|
17
|
+
Toolset = Literal["core", "search", "payload", "snapshots", "admin", "observability"]
|
|
18
|
+
|
|
19
|
+
ALL_TOOLSETS: tuple[Toolset, ...] = (
|
|
20
|
+
"core",
|
|
21
|
+
"search",
|
|
22
|
+
"payload",
|
|
23
|
+
"snapshots",
|
|
24
|
+
"admin",
|
|
25
|
+
"observability",
|
|
26
|
+
)
|
|
27
|
+
DEFAULT_TOOLSETS: tuple[Toolset, ...] = ("core",)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Settings(BaseModel):
|
|
31
|
+
"""Validated process configuration, built once from environment variables."""
|
|
32
|
+
|
|
33
|
+
qdrant_url: str | None = None
|
|
34
|
+
qdrant_api_key: str | None = None
|
|
35
|
+
qdrant_local_path: str | None = None
|
|
36
|
+
read_only: bool = False
|
|
37
|
+
transport: Transport = "stdio"
|
|
38
|
+
toolsets: tuple[Toolset, ...] = DEFAULT_TOOLSETS
|
|
39
|
+
shared_secret: str | None = None
|
|
40
|
+
http_host: str = "127.0.0.1"
|
|
41
|
+
http_port: int = 8000
|
|
42
|
+
|
|
43
|
+
@model_validator(mode="after")
|
|
44
|
+
def _check_single_connection_target(self) -> Settings:
|
|
45
|
+
if self.qdrant_url and self.qdrant_local_path:
|
|
46
|
+
raise ValueError("Set only one of QDRANT_URL or QDRANT_LOCAL_PATH, not both.")
|
|
47
|
+
return self
|
|
48
|
+
|
|
49
|
+
@model_validator(mode="after")
|
|
50
|
+
def _require_shared_secret_for_streamable_http(self) -> Settings:
|
|
51
|
+
# Verified hands-on (a real public tunnel + Claude.ai's own connector
|
|
52
|
+
# UI) that an unauthenticated streamable-http server is trivially
|
|
53
|
+
# usable by anyone with the URL — refuse to start that way silently.
|
|
54
|
+
if self.transport == "streamable-http" and not self.shared_secret:
|
|
55
|
+
raise ValueError(
|
|
56
|
+
"QDRANT_MCP_SHARED_SECRET is required when "
|
|
57
|
+
"QDRANT_MCP_TRANSPORT=streamable-http, to avoid serving an "
|
|
58
|
+
"unauthenticated endpoint over the network."
|
|
59
|
+
)
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def from_env(cls, env: dict[str, str] | None = None) -> Settings:
|
|
64
|
+
e = env if env is not None else dict(os.environ)
|
|
65
|
+
raw_toolsets = e.get("QDRANT_MCP_TOOLSETS", "core")
|
|
66
|
+
toolsets = tuple(t.strip() for t in raw_toolsets.split(",") if t.strip())
|
|
67
|
+
unknown = sorted(set(toolsets) - set(ALL_TOOLSETS))
|
|
68
|
+
if unknown:
|
|
69
|
+
raise ValueError(
|
|
70
|
+
f"Unknown toolset(s) in QDRANT_MCP_TOOLSETS: {unknown}. "
|
|
71
|
+
f"Valid values: {ALL_TOOLSETS}."
|
|
72
|
+
)
|
|
73
|
+
return cls(
|
|
74
|
+
qdrant_url=e.get("QDRANT_URL") or None,
|
|
75
|
+
qdrant_api_key=e.get("QDRANT_API_KEY") or None,
|
|
76
|
+
qdrant_local_path=e.get("QDRANT_LOCAL_PATH") or None,
|
|
77
|
+
read_only=_parse_bool(e.get("QDRANT_MCP_READ_ONLY")),
|
|
78
|
+
transport=e.get("QDRANT_MCP_TRANSPORT", "stdio"), # type: ignore[arg-type]
|
|
79
|
+
toolsets=toolsets, # type: ignore[arg-type]
|
|
80
|
+
shared_secret=e.get("QDRANT_MCP_SHARED_SECRET") or None,
|
|
81
|
+
http_host=e.get("QDRANT_MCP_HTTP_HOST", "127.0.0.1"),
|
|
82
|
+
http_port=int(e.get("QDRANT_MCP_HTTP_PORT", "8000")),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _parse_bool(value: str | None) -> bool:
|
|
87
|
+
return (value or "").strip().lower() in {"1", "true", "yes", "on"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Shared-secret auth for the `streamable-http` transport.
|
|
2
|
+
|
|
3
|
+
`MCPServer.run(transport="streamable-http")` builds and serves its own
|
|
4
|
+
Starlette app internally with no hook for middleware, so `cli.py` builds the
|
|
5
|
+
app itself via `server.streamable_http_app(...)` and wraps it with
|
|
6
|
+
`SharedSecretMiddleware` before serving it. Chosen header/scheme
|
|
7
|
+
(`Authorization: Bearer <secret>`) matches one of the two header names
|
|
8
|
+
Claude.ai's remote-connector "Request headers" UI sends without needing
|
|
9
|
+
Anthropic's manual approval for a custom header name — verified hands-on
|
|
10
|
+
against a real Claude.ai account.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
|
16
|
+
from starlette.requests import Request
|
|
17
|
+
from starlette.responses import JSONResponse, Response
|
|
18
|
+
from starlette.types import ASGIApp
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SharedSecretMiddleware(BaseHTTPMiddleware):
|
|
22
|
+
def __init__(self, app: ASGIApp, *, secret: str) -> None:
|
|
23
|
+
super().__init__(app)
|
|
24
|
+
self._expected = f"Bearer {secret}"
|
|
25
|
+
|
|
26
|
+
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
|
|
27
|
+
if request.headers.get("authorization") != self._expected:
|
|
28
|
+
return JSONResponse({"error": "Unauthorized"}, status_code=401)
|
|
29
|
+
return await call_next(request)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Stderr-only logging setup.
|
|
2
|
+
|
|
3
|
+
MCP's stdio transport treats stdout as the JSON-RPC wire; anything else
|
|
4
|
+
written there corrupts every message after it. This is the single place
|
|
5
|
+
allowed to touch the root logger's handlers, and it hard-codes
|
|
6
|
+
stream=sys.stderr — called before MCPServer() is constructed, so the SDK's
|
|
7
|
+
own internal logging configuration (which also defaults to stderr, but we
|
|
8
|
+
don't want two configurations racing) finds handlers already in place.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def configure_logging(level: str = "INFO") -> None:
|
|
18
|
+
"""Configure the root logger to write exclusively to stderr.
|
|
19
|
+
|
|
20
|
+
Idempotent: clears existing handlers first, so calling this twice (e.g.
|
|
21
|
+
once per test) never accumulates duplicate log lines.
|
|
22
|
+
"""
|
|
23
|
+
root = logging.getLogger()
|
|
24
|
+
root.handlers.clear()
|
|
25
|
+
handler = logging.StreamHandler(stream=sys.stderr)
|
|
26
|
+
handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s"))
|
|
27
|
+
root.addHandler(handler)
|
|
28
|
+
root.setLevel(level)
|
|
File without changes
|