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