Initial commit
This commit is contained in:
162
.gitignore
vendored
Normal file
162
.gitignore
vendored
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||||
|
.pdm.toml
|
||||||
|
.pdm-python
|
||||||
|
.pdm-build/
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.10
|
||||||
85
README.md
Normal file
85
README.md
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# mcp-server-qdrant: A Qdrant MCP server
|
||||||
|
|
||||||
|
> The [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) is an open protocol that enables seamless integration between LLM applications and external data sources and tools. Whether you’re building an AI-powered IDE, enhancing a chat interface, or creating custom AI workflows, MCP provides a standardized way to connect LLMs with the context they need.
|
||||||
|
|
||||||
|
This repository is an example of how to create a MCP server for [Qdrant](https://qdrant.tech/), a vector search engine.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
A basic Model Context Protocol server for keeping and retrieving memories in the Qdrant vector search engine.
|
||||||
|
It acts as a semantic memory layer on top of the Qdrant database.
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### Tools
|
||||||
|
|
||||||
|
1. `qdrant-store-memory`
|
||||||
|
- Store a memory in the Qdrant database
|
||||||
|
- Input:
|
||||||
|
- `information` (string): Memory to store
|
||||||
|
- Returns: Confirmation message
|
||||||
|
2. `qdrant-find-memories`
|
||||||
|
- Retrieve a memory from the Qdrant database
|
||||||
|
- Input:
|
||||||
|
- `query` (string): Query to retrieve a memory
|
||||||
|
- Returns: Memories stored in the Qdrant database as separate messages
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Using uv (recommended)
|
||||||
|
|
||||||
|
When using [`uv`](https://docs.astral.sh/uv/) no specific installation is needed to directly run *mcp-server-qdrant*.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
uv run mcp-server-qdrant \
|
||||||
|
--qdrant-url "http://localhost:6333" \
|
||||||
|
--qdrant-api-key "your_api_key" \
|
||||||
|
--collection-name "my_collection" \
|
||||||
|
--fastembed-model-name "sentence-transformers/all-MiniLM-L6-v2"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage with Claude Desktop
|
||||||
|
|
||||||
|
To use this server with the Claude Desktop app, add the following configuration to the "mcpServers" section of your `claude_desktop_config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"qdrant": {
|
||||||
|
"command": "uvx",
|
||||||
|
"args": [
|
||||||
|
"mcp-server-qdrant",
|
||||||
|
"--qdrant-url",
|
||||||
|
"http://localhost:6333",
|
||||||
|
"--qdrant-api-key",
|
||||||
|
"your_api_key",
|
||||||
|
"--collection-name",
|
||||||
|
"your_collection_name"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `http://localhost:6333`, `your_api_key` and `your_collection_name` with your Qdrant server URL, Qdrant API key
|
||||||
|
and collection name, respectively. The use of API key is optional, but recommended for security reasons, and depends on
|
||||||
|
the Qdrant server configuration.
|
||||||
|
|
||||||
|
This MCP server will automatically create a collection with the specified name if it doesn't exist.
|
||||||
|
|
||||||
|
By default, the server will use the `sentence-transformers/all-MiniLM-L6-v2` embedding model to encode memories.
|
||||||
|
For the time being, only [FastEmbed](https://qdrant.github.io/fastembed/) models are supported, and you can change it
|
||||||
|
by passing the `--fastembed-model-name` argument to the server.
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
The configuration of the server can be also done using environment variables:
|
||||||
|
|
||||||
|
- `QDRANT_URL`: URL of the Qdrant server
|
||||||
|
- `QDRANT_API_KEY`: API key for the Qdrant server
|
||||||
|
- `COLLECTION_NAME`: Name of the collection to use
|
||||||
|
- `FASTEMBED_MODEL_NAME`: Name of the FastEmbed model to use
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software,
|
||||||
|
subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project
|
||||||
|
repository.
|
||||||
20
pyproject.toml
Normal file
20
pyproject.toml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
[project]
|
||||||
|
name = "mcp-server-qdrant"
|
||||||
|
version = "0.5.1"
|
||||||
|
description = "MCP server for retrieving context from a Qdrant vector database"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
dependencies = [
|
||||||
|
"mcp>=0.9.1",
|
||||||
|
"qdrant-client[fastembed]>=1.12.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[tool.uv]
|
||||||
|
dev-dependencies = ["pyright>=1.1.389", "pytest>=8.3.3", "ruff>=0.8.0"]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
mcp-server-qdrant = "mcp_server_qdrant:main"
|
||||||
10
src/mcp_server_qdrant/__init__.py
Normal file
10
src/mcp_server_qdrant/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from . import server
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main entry point for the package."""
|
||||||
|
server.main()
|
||||||
|
|
||||||
|
|
||||||
|
# Optionally expose other important items at package level
|
||||||
|
__all__ = ["main", "server"]
|
||||||
52
src/mcp_server_qdrant/qdrant.py
Normal file
52
src/mcp_server_qdrant/qdrant.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
from typing import Optional
|
||||||
|
from qdrant_client import AsyncQdrantClient, models
|
||||||
|
|
||||||
|
|
||||||
|
class QdrantConnector:
|
||||||
|
"""
|
||||||
|
Encapsulates the connection to a Qdrant server and all the methods to interact with it.
|
||||||
|
:param qdrant_url: The URL of the Qdrant server.
|
||||||
|
:param qdrant_api_key: The API key to use for the Qdrant server.
|
||||||
|
:param collection_name: The name of the collection to use.
|
||||||
|
:param fastembed_model_name: The name of the FastEmbed model to use.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
qdrant_url: str,
|
||||||
|
qdrant_api_key: Optional[str],
|
||||||
|
collection_name: str,
|
||||||
|
fastembed_model_name: str,
|
||||||
|
):
|
||||||
|
self._qdrant_url = qdrant_url.rstrip("/")
|
||||||
|
self._qdrant_api_key = qdrant_api_key
|
||||||
|
self._collection_name = collection_name
|
||||||
|
self._fastembed_model_name = fastembed_model_name
|
||||||
|
# For the time being, FastEmbed models are the only supported ones.
|
||||||
|
# A list of all available models can be found here:
|
||||||
|
# https://qdrant.github.io/fastembed/examples/Supported_Models/
|
||||||
|
self._client = AsyncQdrantClient(qdrant_url, api_key=qdrant_api_key)
|
||||||
|
self._client.set_model(fastembed_model_name)
|
||||||
|
|
||||||
|
async def store_memory(self, information: str):
|
||||||
|
"""
|
||||||
|
Store a memory in the Qdrant collection.
|
||||||
|
:param information: The information to store.
|
||||||
|
"""
|
||||||
|
await self._client.add(
|
||||||
|
self._collection_name,
|
||||||
|
documents=[information],
|
||||||
|
)
|
||||||
|
|
||||||
|
async def find_memories(self, query: str) -> list[str]:
|
||||||
|
"""
|
||||||
|
Find memories in the Qdrant collection.
|
||||||
|
:param query: The query to use for the search.
|
||||||
|
:return: A list of memories found.
|
||||||
|
"""
|
||||||
|
search_results = await self._client.query(
|
||||||
|
self._collection_name,
|
||||||
|
query_text=query,
|
||||||
|
limit=10,
|
||||||
|
)
|
||||||
|
return [result.document for result in search_results]
|
||||||
164
src/mcp_server_qdrant/server.py
Normal file
164
src/mcp_server_qdrant/server.py
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from mcp.server import Server, NotificationOptions
|
||||||
|
from mcp.server.models import InitializationOptions
|
||||||
|
|
||||||
|
import click
|
||||||
|
import mcp.types as types
|
||||||
|
import asyncio
|
||||||
|
import mcp
|
||||||
|
|
||||||
|
from .qdrant import QdrantConnector
|
||||||
|
|
||||||
|
|
||||||
|
def serve(
|
||||||
|
qdrant_url: str,
|
||||||
|
qdrant_api_key: Optional[str],
|
||||||
|
collection_name: str,
|
||||||
|
fastembed_model_name: str,
|
||||||
|
) -> Server:
|
||||||
|
"""
|
||||||
|
Instantiate the server and configure tools to store and find memories in Qdrant.
|
||||||
|
:param qdrant_url: The URL of the Qdrant server.
|
||||||
|
:param qdrant_api_key: The API key to use for the Qdrant server.
|
||||||
|
:param collection_name: The name of the collection to use.
|
||||||
|
:param fastembed_model_name: The name of the FastEmbed model to use.
|
||||||
|
"""
|
||||||
|
server = Server("qdrant")
|
||||||
|
|
||||||
|
qdrant = QdrantConnector(
|
||||||
|
qdrant_url, qdrant_api_key, collection_name, fastembed_model_name
|
||||||
|
)
|
||||||
|
|
||||||
|
@server.list_tools()
|
||||||
|
async def handle_list_tools() -> list[types.Tool]:
|
||||||
|
"""
|
||||||
|
Return the list of tools that the server provides. By default, there are two
|
||||||
|
tools: one to store memories and another to find them. Finding the memories is not
|
||||||
|
implemented as a resource, as it requires a query to be passed and resources point
|
||||||
|
to a very specific piece of data.
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
types.Tool(
|
||||||
|
name="qdrant-store-memory",
|
||||||
|
description=(
|
||||||
|
"Keep the memory for later use, when you are asked to remember something."
|
||||||
|
),
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"information": {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["information"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
types.Tool(
|
||||||
|
name="qdrant-find-memories",
|
||||||
|
description=(
|
||||||
|
"Look up memories in Qdrant. Use this tool when you need to: \n"
|
||||||
|
" - Find memories by their content \n"
|
||||||
|
" - Access memories for further analysis \n"
|
||||||
|
" - Get some personal information about the user"
|
||||||
|
),
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"query": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The query to search for in the memories",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["query"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
@server.call_tool()
|
||||||
|
async def handle_tool_call(
|
||||||
|
name: str, arguments: dict | None
|
||||||
|
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
|
||||||
|
if name not in ["qdrant-store-memory", "qdrant-find-memories"]:
|
||||||
|
raise ValueError(f"Unknown tool: {name}")
|
||||||
|
|
||||||
|
if name == "qdrant-store-memory":
|
||||||
|
if not arguments or "information" not in arguments:
|
||||||
|
raise ValueError("Missing required argument 'information'")
|
||||||
|
information = arguments["information"]
|
||||||
|
await qdrant.store_memory(information)
|
||||||
|
return [types.TextContent(type="text", text=f"Remembered: {information}")]
|
||||||
|
|
||||||
|
if name == "qdrant-find-memories":
|
||||||
|
if not arguments or "query" not in arguments:
|
||||||
|
raise ValueError("Missing required argument 'query'")
|
||||||
|
query = arguments["query"]
|
||||||
|
memories = await qdrant.find_memories(query)
|
||||||
|
content = [
|
||||||
|
types.TextContent(
|
||||||
|
type="text", text=f"Memories for the query '{query}'"
|
||||||
|
),
|
||||||
|
]
|
||||||
|
for memory in memories:
|
||||||
|
content.append(
|
||||||
|
types.TextContent(type="text", text=f"<memory>{memory}</memory>")
|
||||||
|
)
|
||||||
|
return content
|
||||||
|
|
||||||
|
return server
|
||||||
|
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.option(
|
||||||
|
"--qdrant-url",
|
||||||
|
envvar="QDRANT_URL",
|
||||||
|
required=True,
|
||||||
|
help="Qdrant URL",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--qdrant-api-key",
|
||||||
|
envvar="QDRANT_API_KEY",
|
||||||
|
required=False,
|
||||||
|
help="Qdrant API key",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--collection-name",
|
||||||
|
envvar="COLLECTION_NAME",
|
||||||
|
required=True,
|
||||||
|
help="Collection name",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--fastembed-model-name",
|
||||||
|
envvar="FASTEMBED_MODEL_NAME",
|
||||||
|
required=True,
|
||||||
|
help="FastEmbed model name",
|
||||||
|
default="sentence-transformers/all-MiniLM-L6-v2",
|
||||||
|
)
|
||||||
|
def main(
|
||||||
|
qdrant_url: str,
|
||||||
|
qdrant_api_key: str,
|
||||||
|
collection_name: Optional[str],
|
||||||
|
fastembed_model_name: str,
|
||||||
|
):
|
||||||
|
async def _run():
|
||||||
|
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
|
||||||
|
server = serve(
|
||||||
|
qdrant_url,
|
||||||
|
qdrant_api_key,
|
||||||
|
collection_name,
|
||||||
|
fastembed_model_name,
|
||||||
|
)
|
||||||
|
await server.run(
|
||||||
|
read_stream,
|
||||||
|
write_stream,
|
||||||
|
InitializationOptions(
|
||||||
|
server_name="qdrant",
|
||||||
|
server_version="0.5.1",
|
||||||
|
capabilities=server.get_capabilities(
|
||||||
|
notification_options=NotificationOptions(),
|
||||||
|
experimental_capabilities={},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
asyncio.run(_run())
|
||||||
Reference in New Issue
Block a user