Initial commit

This commit is contained in:
Kacper Łukawski
2024-12-02 13:10:16 +01:00
commit 701db26495
8 changed files with 1741 additions and 0 deletions

162
.gitignore vendored Normal file
View 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
View File

@@ -0,0 +1 @@
3.10

85
README.md Normal file
View 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 youre 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
View 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"

View 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"]

View 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]

View 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())

1247
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff