qdrant-mcp-server 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,20 @@
1
+ """
2
+ Qdrant MCP Server
3
+
4
+ A Model Context Protocol (MCP) server for Qdrant vector database
5
+ with semantic search capabilities.
6
+ """
7
+
8
+ __version__ = "0.1.0"
9
+ __author__ = "fiyen"
10
+ __email__ = "623320480@qq.com"
11
+
12
+ from .server import create_server, QdrantMCPServer
13
+ from .__main__ import main, run
14
+
15
+ __all__ = [
16
+ "create_server",
17
+ "QdrantMCPServer",
18
+ "main",
19
+ "run",
20
+ ]
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Qdrant MCP Server 主入口点
4
+ """
5
+
6
+ import asyncio
7
+ import logging
8
+
9
+ from mcp.server.models import InitializationOptions
10
+ from mcp.server.lowlevel import NotificationOptions
11
+ import mcp.server.stdio
12
+
13
+ from .server import create_server
14
+
15
+ logging.basicConfig(level=logging.INFO)
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ async def main():
20
+ """主函数"""
21
+ server, qdrant_server = create_server()
22
+
23
+ try:
24
+ async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
25
+ await server.run(
26
+ read_stream,
27
+ write_stream,
28
+ InitializationOptions(
29
+ server_name="qdrant-mcp",
30
+ server_version="0.1.0",
31
+ capabilities=server.get_capabilities(
32
+ notification_options=NotificationOptions(),
33
+ experimental_capabilities={},
34
+ ),
35
+ ),
36
+ )
37
+ finally:
38
+ await qdrant_server.close()
39
+
40
+
41
+ def run():
42
+ """命令行入口点"""
43
+ asyncio.run(main())
44
+
45
+
46
+ if __name__ == "__main__":
47
+ run()
File without changes
@@ -0,0 +1,607 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Qdrant MCP Server - 标准实现
4
+
5
+ 基于Qdrant向量数据库的MCP(Model Context Protocol)工具包
6
+ 提供知识记录、搜索、更新和删除功能
7
+ """
8
+
9
+ import asyncio
10
+ import json
11
+ import logging
12
+ import os
13
+ import uuid
14
+ from typing import Any, Dict, List, Optional, Union
15
+
16
+ import httpx
17
+ from sentence_transformers import SentenceTransformer
18
+ from dotenv import load_dotenv
19
+
20
+ from mcp.server.lowlevel import NotificationOptions, Server
21
+ from mcp.server.models import InitializationOptions
22
+ import mcp.server.stdio
23
+ import mcp.types as types
24
+
25
+ # 加载环境变量
26
+ load_dotenv()
27
+
28
+ # 配置日志
29
+ logging.basicConfig(level=logging.INFO)
30
+ logger = logging.getLogger(__name__)
31
+
32
+ class QdrantMCPServer:
33
+ """Qdrant MCP服务器类"""
34
+
35
+ def __init__(self):
36
+ self.qdrant_url = os.getenv("QDRANT_URL", "http://localhost:6333")
37
+ self.qdrant_token = os.getenv("QDRANT_API_KEY") or os.getenv("QDRANT_TOKEN")
38
+ self.collection_name = os.getenv("COLLECTION_NAME", "default_collection")
39
+ self.embedding_model_name = os.getenv("EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2")
40
+ self.jina_token = os.getenv("JINA_TOKEN")
41
+
42
+ # 从环境变量读取向量名称配置
43
+ self.vector_name = os.getenv("VECTOR_NAME")
44
+
45
+ # HTTP客户端将在需要时延迟初始化
46
+ self.http_client = None
47
+ self._initialized = False
48
+
49
+ # 判断使用哪种嵌入方式(兼容多种别名)
50
+ normalized_name = str(self.embedding_model_name).strip().lower().replace("_", "-")
51
+ self.use_jina_embedding = normalized_name in {"jina-embeddings-v3", "jina-embedding-v3"}
52
+
53
+ if self.use_jina_embedding:
54
+ # 使用Jina在线嵌入
55
+ if not self.jina_token:
56
+ raise ValueError("使用JINA在线嵌入时必须设置JINA_TOKEN环境变量")
57
+ self.embedding_model = None
58
+ # 如果没有指定向量名称,使用默认的Jina向量名称
59
+ if not self.vector_name:
60
+ self.vector_name = "jina-embeddings-v3"
61
+ logger.info(f"已配置Jina在线嵌入模型: {normalized_name} → 使用向量名称: {self.vector_name}")
62
+ else:
63
+ # 使用本地sentence-transformers模型
64
+ try:
65
+ self.embedding_model = SentenceTransformer(self.embedding_model_name)
66
+ # 如果没有指定向量名称,使用默认的本地模型向量名称
67
+ if not self.vector_name:
68
+ self.vector_name = "fast-all-minilm-l6-v2"
69
+ logger.info(f"已加载本地嵌入模型: {self.embedding_model_name},向量名称: {self.vector_name}")
70
+ except Exception as e:
71
+ logger.error(f"加载嵌入模型失败: {e}")
72
+ raise
73
+
74
+ logger.info(f"Qdrant MCP服务器配置完成,连接到: {self.qdrant_url}")
75
+
76
+ async def _ensure_http_client(self):
77
+ """确保HTTP客户端已初始化"""
78
+ if self.http_client is None or self.http_client.is_closed:
79
+ headers = {"Content-Type": "application/json"}
80
+ if self.qdrant_token:
81
+ headers["Authorization"] = f"Bearer {self.qdrant_token}"
82
+
83
+ self.http_client = httpx.AsyncClient(
84
+ base_url=self.qdrant_url,
85
+ headers=headers,
86
+ timeout=30.0
87
+ )
88
+ self._initialized = True
89
+ logger.info("HTTP客户端已初始化")
90
+
91
+ async def _make_request(self, method: str, url: str, **kwargs) -> httpx.Response:
92
+ """发送HTTP请求"""
93
+ await self._ensure_http_client()
94
+
95
+ if method.upper() == "GET":
96
+ return await self.http_client.get(url, **kwargs)
97
+ elif method.upper() == "POST":
98
+ return await self.http_client.post(url, **kwargs)
99
+ elif method.upper() == "PUT":
100
+ return await self.http_client.put(url, **kwargs)
101
+ elif method.upper() == "DELETE":
102
+ return await self.http_client.delete(url, **kwargs)
103
+ else:
104
+ raise ValueError(f"不支持的HTTP方法: {method}")
105
+
106
+ async def generate_embedding(self, text: str) -> List[float]:
107
+ """生成文本嵌入向量"""
108
+ try:
109
+ if self.use_jina_embedding:
110
+ return await self._generate_jina_embedding(text)
111
+ else:
112
+ embedding = self.embedding_model.encode(text)
113
+ return embedding.tolist()
114
+ except Exception as e:
115
+ logger.error(f"生成嵌入向量失败: {e}")
116
+ raise
117
+
118
+ async def _generate_jina_embedding(self, text: str) -> List[float]:
119
+ """使用Jina API生成嵌入向量"""
120
+ try:
121
+ headers = {
122
+ "Content-Type": "application/json",
123
+ "Authorization": f"Bearer {self.jina_token}"
124
+ }
125
+
126
+ payload = {
127
+ "model": "jina-embeddings-v3",
128
+ "task": "text-matching", # 用于语义相似度匹配
129
+ "input": [text],
130
+ "normalized": True, # L2标准化
131
+ "embedding_type": "float"
132
+ }
133
+
134
+ async with httpx.AsyncClient() as client:
135
+ response = await client.post(
136
+ "https://api.jina.ai/v1/embeddings",
137
+ headers=headers,
138
+ json=payload,
139
+ timeout=30.0
140
+ )
141
+
142
+ if response.status_code == 200:
143
+ result = response.json()
144
+ embedding = result["data"][0]["embedding"]
145
+ logger.debug(f"Jina API返回嵌入向量,维度: {len(embedding)}")
146
+ return embedding
147
+ else:
148
+ raise Exception(f"Jina API请求失败: {response.status_code} - {response.text}")
149
+
150
+ except Exception as e:
151
+ logger.error(f"Jina嵌入生成失败: {e}")
152
+ raise
153
+
154
+ async def ensure_collection_exists(self) -> bool:
155
+ """确保集合存在,如果不存在则创建"""
156
+ try:
157
+ # 检查集合是否存在
158
+ response = await self._make_request("GET", f"/collections/{self.collection_name}")
159
+ if response.status_code == 200:
160
+ logger.info(f"集合 {self.collection_name} 已存在")
161
+ return True
162
+
163
+ # 创建集合,使用配置的向量名称和正确的向量维度
164
+ vector_size = 1024 if self.use_jina_embedding else 384
165
+ collection_config = {
166
+ "vectors": {
167
+ self.vector_name: {
168
+ "size": vector_size,
169
+ "distance": "Cosine"
170
+ }
171
+ }
172
+ }
173
+
174
+ logger.info(f"创建集合 {self.collection_name},向量名称: {self.vector_name},向量维度: {vector_size}")
175
+
176
+ response = await self._make_request(
177
+ "PUT",
178
+ f"/collections/{self.collection_name}",
179
+ json=collection_config
180
+ )
181
+
182
+ if response.status_code in [200, 201]:
183
+ logger.info(f"成功创建集合 {self.collection_name}")
184
+ return True
185
+ else:
186
+ logger.error(f"创建集合失败: {response.status_code} - {response.text}")
187
+ return False
188
+
189
+ except Exception as e:
190
+ logger.error(f"确保集合存在时出错: {e}")
191
+ return False
192
+
193
+
194
+ async def record_knowledge(self, document: str, metadata: Optional[Dict[str, Any]] = None, point_id: Optional[Union[str, int]] = None) -> Dict[str, Any]:
195
+ """记录知识到Qdrant,使用document+metadata格式"""
196
+ try:
197
+ # 确保集合存在
198
+ if not await self.ensure_collection_exists():
199
+ raise Exception("无法创建或访问集合")
200
+
201
+ # 生成嵌入向量
202
+ vector = await self.generate_embedding(document)
203
+
204
+ # 准备点数据
205
+ if point_id is None:
206
+ point_id = str(uuid.uuid4())
207
+
208
+ # 使用document+metadata格式
209
+ payload = {
210
+ "document": document
211
+ }
212
+
213
+ # 如果有metadata,添加到payload中
214
+ if metadata:
215
+ payload["metadata"] = metadata
216
+
217
+ point_data = {
218
+ "points": [{
219
+ "id": point_id,
220
+ "vector": {
221
+ self.vector_name: vector # 使用配置的向量名称
222
+ },
223
+ "payload": payload
224
+ }]
225
+ }
226
+
227
+ # 上传到Qdrant
228
+ response = await self._make_request(
229
+ "PUT",
230
+ f"/collections/{self.collection_name}/points",
231
+ json=point_data
232
+ )
233
+
234
+ if response.status_code in [200, 201]:
235
+ result = response.json()
236
+ return {
237
+ "success": True,
238
+ "point_id": point_id,
239
+ "operation_id": result.get("result", {}).get("operation_id"),
240
+ "message": "知识记录成功"
241
+ }
242
+ else:
243
+ raise Exception(f"记录失败: {response.status_code} - {response.text}")
244
+
245
+ except Exception as e:
246
+ logger.error(f"记录知识失败: {e}")
247
+ return {
248
+ "success": False,
249
+ "error": str(e)
250
+ }
251
+
252
+ async def search_knowledge(self, query: str, limit: int = 5, score_threshold: float = 0.0, filter_conditions: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
253
+ """搜索相关知识"""
254
+ try:
255
+ # 确保集合存在
256
+ if not await self.ensure_collection_exists():
257
+ raise Exception("无法访问集合")
258
+
259
+ # 生成查询向量
260
+ query_vector = await self.generate_embedding(query)
261
+
262
+ # 准备搜索请求,使用配置的向量名称
263
+ search_data = {
264
+ "vector": {
265
+ "name": self.vector_name, # 使用配置的向量名称
266
+ "vector": query_vector
267
+ },
268
+ "limit": limit,
269
+ "score_threshold": score_threshold,
270
+ "with_payload": True,
271
+ "with_vector": False
272
+ }
273
+
274
+ # 添加过滤条件
275
+ if filter_conditions:
276
+ search_data["filter"] = filter_conditions
277
+
278
+ # 执行搜索
279
+ response = await self._make_request(
280
+ "POST",
281
+ f"/collections/{self.collection_name}/points/search",
282
+ json=search_data
283
+ )
284
+
285
+ if response.status_code == 200:
286
+ result = response.json()
287
+ search_results = result.get("result", [])
288
+
289
+ return {
290
+ "success": True,
291
+ "query": query,
292
+ "results": [
293
+ {
294
+ "id": item["id"],
295
+ "score": item["score"],
296
+ "document": item["payload"].get("document", item["payload"].get("text", "")), # 兼容新旧格式
297
+ "metadata": item["payload"].get("metadata", {k: v for k, v in item["payload"].items() if k not in ["document", "text"]})
298
+ }
299
+ for item in search_results
300
+ ],
301
+ "total_found": len(search_results)
302
+ }
303
+ else:
304
+ raise Exception(f"搜索失败: {response.status_code} - {response.text}")
305
+
306
+ except Exception as e:
307
+ logger.error(f"搜索知识失败: {e}")
308
+ return {
309
+ "success": False,
310
+ "error": str(e)
311
+ }
312
+
313
+ async def update_knowledge(self, point_id: Union[str, int], document: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
314
+ """更新已存储的知识条目,使用document+metadata格式"""
315
+ try:
316
+ # 确保集合存在
317
+ if not await self.ensure_collection_exists():
318
+ raise Exception("无法访问集合")
319
+
320
+ # 获取现有点数据
321
+ response = await self._make_request(
322
+ "GET",
323
+ f"/collections/{self.collection_name}/points/{point_id}"
324
+ )
325
+
326
+ if response.status_code != 200:
327
+ raise Exception(f"找不到ID为 {point_id} 的知识条目")
328
+
329
+ existing_data = response.json()["result"]
330
+ existing_payload = existing_data.get("payload", {})
331
+
332
+ # 准备更新数据,使用document+metadata格式
333
+ update_payload = {}
334
+
335
+ # 更新文档内容和向量(如果提供了新文档)
336
+ if document is not None:
337
+ update_payload["document"] = document
338
+ vector = {
339
+ self.vector_name: await self.generate_embedding(document) # 使用配置的向量名称
340
+ }
341
+ else:
342
+ # 保持原有文档内容
343
+ update_payload["document"] = existing_payload.get("document", existing_payload.get("text", ""))
344
+ vector = existing_data.get("vector")
345
+
346
+ # 更新元数据
347
+ if metadata:
348
+ update_payload["metadata"] = metadata
349
+ else:
350
+ # 保持原有元数据
351
+ update_payload["metadata"] = existing_payload.get("metadata", {})
352
+
353
+ # 执行更新
354
+ point_data = {
355
+ "points": [{
356
+ "id": point_id,
357
+ "vector": vector,
358
+ "payload": update_payload
359
+ }]
360
+ }
361
+
362
+ response = await self._make_request(
363
+ "PUT",
364
+ f"/collections/{self.collection_name}/points",
365
+ json=point_data
366
+ )
367
+
368
+ if response.status_code in [200, 201]:
369
+ result = response.json()
370
+ return {
371
+ "success": True,
372
+ "point_id": point_id,
373
+ "operation_id": result.get("result", {}).get("operation_id"),
374
+ "message": "知识更新成功"
375
+ }
376
+ else:
377
+ raise Exception(f"更新失败: {response.status_code} - {response.text}")
378
+
379
+ except Exception as e:
380
+ logger.error(f"更新知识失败: {e}")
381
+ return {
382
+ "success": False,
383
+ "error": str(e)
384
+ }
385
+
386
+ async def delete_knowledge(self, point_ids: Union[str, int, List[Union[str, int]]], filter_conditions: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
387
+ """删除知识条目"""
388
+ try:
389
+ # 确保集合存在
390
+ if not await self.ensure_collection_exists():
391
+ raise Exception("无法访问集合")
392
+
393
+ # 准备删除请求
394
+ delete_data = {}
395
+
396
+ if point_ids is not None:
397
+ # 按ID删除
398
+ if not isinstance(point_ids, list):
399
+ point_ids = [point_ids]
400
+ delete_data["points"] = point_ids
401
+
402
+ if filter_conditions:
403
+ # 按条件删除
404
+ delete_data["filter"] = filter_conditions
405
+
406
+ if not delete_data:
407
+ raise Exception("必须提供point_ids或filter_conditions")
408
+
409
+ # 执行删除
410
+ response = await self._make_request(
411
+ "POST",
412
+ f"/collections/{self.collection_name}/points/delete",
413
+ json=delete_data
414
+ )
415
+
416
+ if response.status_code == 200:
417
+ result = response.json()
418
+ return {
419
+ "success": True,
420
+ "operation_id": result.get("result", {}).get("operation_id"),
421
+ "message": "知识删除成功"
422
+ }
423
+ else:
424
+ raise Exception(f"删除失败: {response.status_code} - {response.text}")
425
+
426
+ except Exception as e:
427
+ logger.error(f"删除知识失败: {e}")
428
+ return {
429
+ "success": False,
430
+ "error": str(e)
431
+ }
432
+
433
+ async def close(self):
434
+ """优雅关闭HTTP客户端"""
435
+ try:
436
+ if self.http_client and not self.http_client.is_closed:
437
+ await self.http_client.aclose()
438
+ logger.info("HTTP客户端已关闭")
439
+ except Exception as e:
440
+ logger.warning(f"关闭HTTP客户端时出错: {e}")
441
+
442
+
443
+ def create_server() -> Server:
444
+ """创建并配置MCP服务器"""
445
+ # 创建全局实例
446
+ qdrant_server = QdrantMCPServer()
447
+
448
+ # 创建MCP服务器
449
+ server = Server("qdrant-mcp")
450
+
451
+ @server.list_tools()
452
+ async def handle_list_tools() -> list[types.Tool]:
453
+ """列出可用的工具"""
454
+ return [
455
+ types.Tool(
456
+ name="qdrant-record",
457
+ description="将文档信息转换为向量并存储到Qdrant集合中,使用document+metadata格式",
458
+ inputSchema={
459
+ "type": "object",
460
+ "properties": {
461
+ "document": {
462
+ "type": "string",
463
+ "description": "要存储的文档内容"
464
+ },
465
+ "metadata": {
466
+ "type": "object",
467
+ "description": "可选的元数据信息",
468
+ "additionalProperties": True
469
+ },
470
+ "point_id": {
471
+ "type": ["string", "integer", "null"],
472
+ "description": "可选的点ID,如果不提供将自动生成UUID"
473
+ }
474
+ },
475
+ "required": ["document"]
476
+ }
477
+ ),
478
+ types.Tool(
479
+ name="qdrant-search",
480
+ description="基于语义相似度搜索相关知识条目",
481
+ inputSchema={
482
+ "type": "object",
483
+ "properties": {
484
+ "query": {
485
+ "type": "string",
486
+ "description": "搜索查询文本"
487
+ },
488
+ "limit": {
489
+ "type": "integer",
490
+ "description": "返回结果数量限制",
491
+ "default": 5,
492
+ "minimum": 1,
493
+ "maximum": 100
494
+ },
495
+ "score_threshold": {
496
+ "type": "number",
497
+ "description": "相似度分数阈值",
498
+ "default": 0.0,
499
+ "minimum": 0.0,
500
+ "maximum": 1.0
501
+ },
502
+ "filter_conditions": {
503
+ "type": "object",
504
+ "description": "可选的过滤条件",
505
+ "additionalProperties": True
506
+ }
507
+ },
508
+ "required": ["query"]
509
+ }
510
+ ),
511
+ types.Tool(
512
+ name="qdrant-update",
513
+ description="修改已存储的知识条目内容或元数据,使用document+metadata格式",
514
+ inputSchema={
515
+ "type": "object",
516
+ "properties": {
517
+ "point_id": {
518
+ "type": ["string", "integer"],
519
+ "description": "要更新的点ID"
520
+ },
521
+ "document": {
522
+ "type": "string",
523
+ "description": "新的文档内容(可选)"
524
+ },
525
+ "metadata": {
526
+ "type": "object",
527
+ "description": "要更新的元数据",
528
+ "additionalProperties": True
529
+ }
530
+ },
531
+ "required": ["point_id"]
532
+ }
533
+ ),
534
+ types.Tool(
535
+ name="qdrant-delete",
536
+ description="删除指定的知识条目",
537
+ inputSchema={
538
+ "type": "object",
539
+ "properties": {
540
+ "point_ids": {
541
+ "type": ["string", "integer", "array"],
542
+ "description": "要删除的点ID或ID列表",
543
+ "items": {
544
+ "type": ["string", "integer"]
545
+ }
546
+ },
547
+ "filter_conditions": {
548
+ "type": "object",
549
+ "description": "按条件删除的过滤器",
550
+ "additionalProperties": True
551
+ }
552
+ }
553
+ }
554
+ )
555
+ ]
556
+
557
+ @server.call_tool()
558
+ async def handle_call_tool(name: str, arguments: Dict[str, Any]) -> list[types.TextContent]:
559
+ """处理工具调用"""
560
+ try:
561
+ if name == "qdrant-record":
562
+ result = await qdrant_server.record_knowledge(
563
+ document=arguments["document"],
564
+ metadata=arguments.get("metadata"),
565
+ point_id=arguments.get("point_id")
566
+ )
567
+ elif name == "qdrant-search":
568
+ result = await qdrant_server.search_knowledge(
569
+ query=arguments["query"],
570
+ limit=arguments.get("limit", 5),
571
+ score_threshold=arguments.get("score_threshold", 0.0),
572
+ filter_conditions=arguments.get("filter_conditions")
573
+ )
574
+ elif name == "qdrant-update":
575
+ result = await qdrant_server.update_knowledge(
576
+ point_id=arguments["point_id"],
577
+ document=arguments.get("document"),
578
+ metadata=arguments.get("metadata")
579
+ )
580
+ elif name == "qdrant-delete":
581
+ result = await qdrant_server.delete_knowledge(
582
+ point_ids=arguments.get("point_ids"),
583
+ filter_conditions=arguments.get("filter_conditions")
584
+ )
585
+ else:
586
+ raise ValueError(f"未知工具: {name}")
587
+
588
+ return [
589
+ types.TextContent(
590
+ type="text",
591
+ text=json.dumps(result, ensure_ascii=False, indent=2)
592
+ )
593
+ ]
594
+
595
+ except Exception as e:
596
+ logger.error(f"工具调用失败 {name}: {e}")
597
+ return [
598
+ types.TextContent(
599
+ type="text",
600
+ text=json.dumps({
601
+ "success": False,
602
+ "error": str(e)
603
+ }, ensure_ascii=False, indent=2)
604
+ )
605
+ ]
606
+
607
+ return server, qdrant_server
@@ -0,0 +1,302 @@
1
+ Metadata-Version: 2.4
2
+ Name: qdrant-mcp-server
3
+ Version: 0.1.0
4
+ Summary: A Model Context Protocol (MCP) server for Qdrant vector database with semantic search capabilities
5
+ Author-email: fiyen <623320480@qq.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/fiyen/qdrant-mcp-server
8
+ Project-URL: Repository, https://github.com/fiyen/qdrant-mcp-server
9
+ Project-URL: Documentation, https://github.com/fiyen/qdrant-mcp-server#readme
10
+ Project-URL: Bug Tracker, https://github.com/fiyen/qdrant-mcp-server/issues
11
+ Keywords: mcp,qdrant,vector-database,semantic-search,embeddings,ai
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: mcp>=1.0.0
25
+ Requires-Dist: httpx>=0.27.0
26
+ Requires-Dist: sentence-transformers>=2.2.0
27
+ Requires-Dist: python-dotenv>=1.0.0
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
30
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
31
+ Requires-Dist: black>=23.0.0; extra == "dev"
32
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
33
+ Dynamic: license-file
34
+
35
+ # Qdrant MCP Server
36
+
37
+ [![PyPI version](https://badge.fury.io/py/qdrant-mcp-server.svg)](https://badge.fury.io/py/qdrant-mcp-server)
38
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
39
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
40
+
41
+ 一个基于 [Qdrant](https://qdrant.tech/) 向量数据库的 [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) 服务器,提供强大的语义搜索和知识管理功能。
42
+
43
+ ## ✨ 特性
44
+
45
+ - 🚀 **即插即用**: 使用 `uvx` 一键运行,无需复杂配置
46
+ - 🔍 **语义搜索**: 基于向量相似度的智能搜索
47
+ - 🧠 **多种嵌入模型**: 支持本地 Sentence-Transformers 和在线 Jina AI
48
+ - 📝 **完整 CRUD**: 创建、读取、更新、删除知识条目
49
+ - 🔧 **灵活配置**: 通过环境变量自定义各项参数
50
+ - 🌐 **标准协议**: 完全符合 MCP 规范
51
+
52
+ ## 📦 安装
53
+
54
+ ### 使用 uvx (推荐)
55
+
56
+ 最简单的方式是使用 `uvx` 直接运行:
57
+
58
+ ```bash
59
+ uvx qdrant-mcp-server
60
+ ```
61
+
62
+ ### 使用 uv
63
+
64
+ ```bash
65
+ uv pip install qdrant-mcp-server
66
+ ```
67
+
68
+ ### 使用 pip
69
+
70
+ ```bash
71
+ pip install qdrant-mcp-server
72
+ ```
73
+
74
+ ## 🚀 快速开始
75
+
76
+ ### 1. 配置环境变量
77
+
78
+ 创建 `.env` 文件或设置环境变量:
79
+
80
+ ```bash
81
+ # Qdrant 连接配置
82
+ QDRANT_URL=https://your-qdrant-instance.com:443
83
+ QDRANT_API_KEY=your-api-key-here
84
+ COLLECTION_NAME=my_knowledge_base
85
+
86
+ # 嵌入模型配置 (二选一)
87
+ # 选项1: 使用本地模型 (默认)
88
+ EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
89
+ VECTOR_NAME=fast-all-minilm-l6-v2
90
+
91
+ # 选项2: 使用 Jina AI 在线模型
92
+ # EMBEDDING_MODEL=jina-embeddings-v3
93
+ # JINA_TOKEN=your-jina-api-key
94
+ # VECTOR_NAME=jina-embeddings-v3
95
+ ```
96
+
97
+ ### 2. 在 MCP 客户端中配置
98
+
99
+ #### Claude Desktop 配置
100
+
101
+ 编辑 `~/Library/Application Support/Claude/claude_desktop_config.json`:
102
+
103
+ ```json
104
+ {
105
+ "mcpServers": {
106
+ "qdrant": {
107
+ "command": "uvx",
108
+ "args": ["qdrant-mcp-server"],
109
+ "env": {
110
+ "QDRANT_URL": "https://your-instance.com:443",
111
+ "QDRANT_API_KEY": "your-api-key",
112
+ "COLLECTION_NAME": "knowledge_base"
113
+ }
114
+ }
115
+ }
116
+ }
117
+ ```
118
+
119
+ #### Roo Code / OpenCode 配置
120
+
121
+ 编辑配置文件 (如 `~/.config/opencode/opencode.json`):
122
+
123
+ ```json
124
+ {
125
+ "mcp": {
126
+ "qdrant": {
127
+ "type": "local",
128
+ "command": ["uvx", "qdrant-mcp-server"],
129
+ "environment": {
130
+ "QDRANT_URL": "https://your-instance.com:443",
131
+ "QDRANT_API_KEY": "your-api-key",
132
+ "COLLECTION_NAME": "knowledge_base"
133
+ }
134
+ }
135
+ }
136
+ }
137
+ ```
138
+
139
+ ## 🛠️ 可用工具
140
+
141
+ ### qdrant-record
142
+
143
+ 将文档存储到 Qdrant 向量数据库。
144
+
145
+ ```python
146
+ # 参数
147
+ {
148
+ "document": "要存储的文档内容", # 必需
149
+ "metadata": { # 可选
150
+ "source": "example.pdf",
151
+ "author": "John Doe",
152
+ "date": "2024-01-01"
153
+ },
154
+ "point_id": "custom-id-123" # 可选,不提供则自动生成
155
+ }
156
+ ```
157
+
158
+ ### qdrant-search
159
+
160
+ 基于语义相似度搜索知识。
161
+
162
+ ```python
163
+ # 参数
164
+ {
165
+ "query": "搜索关键词", # 必需
166
+ "limit": 5, # 可选,默认 5
167
+ "score_threshold": 0.7, # 可选,默认 0.0
168
+ "filter_conditions": { # 可选
169
+ "must": [
170
+ {
171
+ "key": "metadata.source",
172
+ "match": {"value": "example.pdf"}
173
+ }
174
+ ]
175
+ }
176
+ }
177
+ ```
178
+
179
+ ### qdrant-update
180
+
181
+ 更新已存储的知识条目。
182
+
183
+ ```python
184
+ # 参数
185
+ {
186
+ "point_id": "existing-id", # 必需
187
+ "document": "新的文档内容", # 可选
188
+ "metadata": { # 可选
189
+ "updated": "2024-01-02"
190
+ }
191
+ }
192
+ ```
193
+
194
+ ### qdrant-delete
195
+
196
+ 删除知识条目。
197
+
198
+ ```python
199
+ # 按 ID 删除
200
+ {
201
+ "point_ids": "single-id" # 或 ["id1", "id2"]
202
+ }
203
+
204
+ # 按条件删除
205
+ {
206
+ "filter_conditions": {
207
+ "must": [
208
+ {
209
+ "key": "metadata.source",
210
+ "match": {"value": "old-doc.pdf"}
211
+ }
212
+ ]
213
+ }
214
+ }
215
+ ```
216
+
217
+ ## 🔧 配置选项
218
+
219
+ | 环境变量 | 默认值 | 说明 |
220
+ |---------|--------|------|
221
+ | `QDRANT_URL` | `http://localhost:6333` | Qdrant 服务器地址 |
222
+ | `QDRANT_API_KEY` | - | Qdrant API 密钥 (云服务需要) |
223
+ | `COLLECTION_NAME` | `default_collection` | 集合名称 |
224
+ | `EMBEDDING_MODEL` | `sentence-transformers/all-MiniLM-L6-v2` | 嵌入模型 |
225
+ | `VECTOR_NAME` | 自动检测 | 向量字段名称 |
226
+ | `JINA_TOKEN` | - | Jina AI API 密钥 (使用 Jina 时需要) |
227
+
228
+ ## 🌟 使用示例
229
+
230
+ ### 1. 存储知识
231
+
232
+ ```
233
+ 请将这段内容存储到知识库:
234
+ "人工智能是计算机科学的一个分支,致力于创建能够执行通常需要人类智能的任务的系统。"
235
+ ```
236
+
237
+ ### 2. 搜索知识
238
+
239
+ ```
240
+ 在知识库中搜索关于"机器学习"的内容
241
+ ```
242
+
243
+ ### 3. 更新知识
244
+
245
+ ```
246
+ 更新 ID 为 "doc-123" 的文档,添加元数据 {"verified": true}
247
+ ```
248
+
249
+ ## 🏗️ 开发
250
+
251
+ ### 从源码安装
252
+
253
+ ```bash
254
+ git clone https://github.com/fiyen/qdrant-mcp-server.git
255
+ cd qdrant-mcp-server
256
+ uv pip install -e ".[dev]"
257
+ ```
258
+
259
+ ### 运行测试
260
+
261
+ ```bash
262
+ pytest
263
+ ```
264
+
265
+ ### 代码格式化
266
+
267
+ ```bash
268
+ black src/
269
+ ruff check src/
270
+ ```
271
+
272
+ ## 📚 文档
273
+
274
+ - [MCP 协议文档](https://modelcontextprotocol.io/)
275
+ - [Qdrant 文档](https://qdrant.tech/documentation/)
276
+ - [Sentence Transformers](https://www.sbert.net/)
277
+ - [Jina AI Embeddings](https://jina.ai/embeddings/)
278
+
279
+ ## 🤝 贡献
280
+
281
+ 欢迎提交 Issue 和 Pull Request!
282
+
283
+ ## 📄 许可证
284
+
285
+ MIT License - 详见 [LICENSE](LICENSE) 文件
286
+
287
+ ## 🙏 致谢
288
+
289
+ - [Qdrant](https://qdrant.tech/) - 高性能向量数据库
290
+ - [Anthropic](https://www.anthropic.com/) - MCP 协议
291
+ - [Sentence Transformers](https://www.sbert.net/) - 嵌入模型
292
+ - [Jina AI](https://jina.ai/) - 在线嵌入服务
293
+
294
+ ## 📧 联系
295
+
296
+ - 作者: fiyen
297
+ - Email: 623320480@qq.com
298
+ - GitHub: [@fiyen](https://github.com/fiyen)
299
+
300
+ ---
301
+
302
+ ⭐ 如果这个项目对您有帮助,请给个 Star!
@@ -0,0 +1,10 @@
1
+ qdrant_mcp_server/__init__.py,sha256=RVBdaMflB321UmTaRQyP8_wLKBY4S_m63j7U8b-aqWM,369
2
+ qdrant_mcp_server/__main__.py,sha256=DRqZCAzfAnZ7Po6WjLRLmt23X98vC5gHkN9s6CCg-hA,1171
3
+ qdrant_mcp_server/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ qdrant_mcp_server/server.py,sha256=SKn0WxNcXWELmJEKjS1C9TGk_0sMCsLJdvkN9Xe4pSw,24239
5
+ qdrant_mcp_server-0.1.0.dist-info/licenses/LICENSE,sha256=qAepqbBtrJD-C4ZO6lxaAxM7MKmfJKMxcwraV79MV9Q,1083
6
+ qdrant_mcp_server-0.1.0.dist-info/METADATA,sha256=ovS1FWcuQN80GB9ngWFNA8KQJewySWceLWkFRJI0lPw,7604
7
+ qdrant_mcp_server-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
8
+ qdrant_mcp_server-0.1.0.dist-info/entry_points.txt,sha256=3DhxwrVvCbb1tW0JkwAukF1KdafC8QxLQBVwa3-Jn_I,69
9
+ qdrant_mcp_server-0.1.0.dist-info/top_level.txt,sha256=dF-2lRto5n9Ve4e_aP4ft7OjAyCg2mKVM9iAnXalkyQ,18
10
+ qdrant_mcp_server-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ qdrant-mcp-server = qdrant_mcp_server.__main__:run
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 fiyen
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 @@
1
+ qdrant_mcp_server