diff --git a/README.md b/README.md index f3fcf6d..adcfaf3 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,10 @@ # mcp-server-qdrant: A Qdrant MCP server [![smithery badge](https://smithery.ai/badge/mcp-server-qdrant)](https://smithery.ai/protocol/mcp-server-qdrant) -> 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. +> 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. @@ -27,18 +30,18 @@ It acts as a semantic memory layer on top of the Qdrant database. - `query` (string): Query to retrieve a memory - Returns: Memories stored in the Qdrant database as separate messages -## Installation +## Installation in Claude Desktop -### Using uv (recommended) +### Using mcp (recommended) -When using [`uv`](https://docs.astral.sh/uv/) no specific installation is needed to directly run *mcp-server-qdrant*. +When using [`mcp`](https://github.com/modelcontextprotocol/python-sdk) 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" \ - --embedding-model "sentence-transformers/all-MiniLM-L6-v2" +mcp install src/mcp_server_qdrant/server.py \ + -v QDRANT_URL="http://localhost:6333" \ + -v QDRANT_API_KEY="your_api_key" \ + -v COLLECTION_NAME="my_collection" \ + -v EMBEDDING_MODEL="sentence-transformers/all-MiniLM-L6-v2" ``` ### Installing via Smithery @@ -49,70 +52,68 @@ To install Qdrant MCP Server for Claude Desktop automatically via [Smithery](htt npx @smithery/cli install mcp-server-qdrant --client claude ``` -## Usage with Claude Desktop +### Manual configuration -To use this server with the Claude Desktop app, add the following configuration to the "mcpServers" section of your `claude_desktop_config.json`: +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" - ] + "args": ["mcp-server-qdrant"], + "env": { + "QDRANT_URL": "http://localhost:6333", + "QDRANT_API_KEY": "your_api_key", + "COLLECTION_NAME": "your_collection_name", + "EMBEDDING_MODEL": "sentence-transformers/all-MiniLM-L6-v2" + } } } ``` -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. +For local Qdrant mode: + +```json +{ + "qdrant": { + "command": "uvx", + "args": ["mcp-server-qdrant"], + "env": { + "QDRANT_LOCAL_PATH": "/path/to/qdrant/database", + "COLLECTION_NAME": "your_collection_name", + "EMBEDDING_MODEL": "sentence-transformers/all-MiniLM-L6-v2" + } + } +} +``` 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 `--embedding-model` argument to the server. +For the time being, only [FastEmbed](https://qdrant.github.io/fastembed/) models are supported. -### Using the local mode of Qdrant +### Support for other tools -To use a local mode of Qdrant, you can specify the path to the database using the `--qdrant-local-path` argument: - -```json -{ - "qdrant": { - "command": "uvx", - "args": [ - "mcp-server-qdrant", - "--qdrant-local-path", - "/path/to/qdrant/database", - "--collection-name", - "your_collection_name" - ] - } -} -``` - -It will run Qdrant local mode inside the same process as the MCP server. Although it is not recommended for production. +This MCP server can be used with any MCP-compatible client. For example, you can use it with +[Cursor](https://docs.cursor.com/context/model-context-protocol), which provides built-in support for the Model Context +Protocol. ## Environment Variables -The configuration of the server can be also done using environment variables: +The configuration of the server is done using environment variables: - `QDRANT_URL`: URL of the Qdrant server, e.g. `http://localhost:6333` -- `QDRANT_API_KEY`: API key for the Qdrant server -- `COLLECTION_NAME`: Name of the collection to use -- `EMBEDDING_MODEL`: Name of the embedding model to use +- `QDRANT_API_KEY`: API key for the Qdrant server (optional, depends on Qdrant server configuration) +- `COLLECTION_NAME`: Name of the collection to use (required) +- `EMBEDDING_MODEL`: Name of the embedding model to use (default: `sentence-transformers/all-MiniLM-L6-v2`) - `EMBEDDING_PROVIDER`: Embedding provider to use (currently only "fastembed" is supported) -- `QDRANT_LOCAL_PATH`: Path to the local Qdrant database +- `QDRANT_LOCAL_PATH`: Path to the local Qdrant database (alternative to `QDRANT_URL`) -You cannot provide `QDRANT_URL` and `QDRANT_LOCAL_PATH` at the same time. +Note: You cannot provide both `QDRANT_URL` and `QDRANT_LOCAL_PATH` at the same time. + +> [!IMPORTANT] +> Command-line arguments are not supported anymore! Please use environment variables for all configuration. ## Contributing @@ -126,9 +127,8 @@ servers. It runs both a client UI (default port 5173) and an MCP proxy server (d your browser to use the inspector. ```shell -npx @modelcontextprotocol/inspector uv run mcp-server-qdrant \ - --collection-name test \ - --qdrant-local-path /tmp/qdrant-local-test +QDRANT_URL=":memory:" COLLECTION_NAME="test" \ +mcp dev src/mcp_server_qdrant/server.py ``` Once started, open your browser to http://localhost:5173 to access the inspector interface. diff --git a/pyproject.toml b/pyproject.toml index 98ad061..c33a87e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,6 @@ dependencies = [ "mcp[cli]>=1.3.0", "fastembed>=0.6.0", "qdrant-client>=1.12.0", - "typer>=0.15.2", ] [build-system] @@ -26,7 +25,7 @@ dev-dependencies = [ ] [project.scripts] -mcp-server-qdrant = "mcp_server_qdrant:main" +mcp-server-qdrant = "mcp_server_qdrant.main:main" [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/src/mcp_server_qdrant/__init__.py b/src/mcp_server_qdrant/__init__.py index 50f6835..e69de29 100644 --- a/src/mcp_server_qdrant/__init__.py +++ b/src/mcp_server_qdrant/__init__.py @@ -1,10 +0,0 @@ -from . import server - - -def main(): - """Main entry point for the package.""" - server.mcp.run() - - -# Optionally expose other important items at package level -__all__ = ["main", "server"] diff --git a/src/mcp_server_qdrant/embeddings/__init__.py b/src/mcp_server_qdrant/embeddings/__init__.py index 94f0a4c..e69de29 100644 --- a/src/mcp_server_qdrant/embeddings/__init__.py +++ b/src/mcp_server_qdrant/embeddings/__init__.py @@ -1,5 +0,0 @@ -from .base import EmbeddingProvider -from .factory import create_embedding_provider -from .fastembed import FastEmbedProvider - -__all__ = ["EmbeddingProvider", "FastEmbedProvider", "create_embedding_provider"] diff --git a/src/mcp_server_qdrant/embeddings/factory.py b/src/mcp_server_qdrant/embeddings/factory.py index af73a75..a0a56f0 100644 --- a/src/mcp_server_qdrant/embeddings/factory.py +++ b/src/mcp_server_qdrant/embeddings/factory.py @@ -1,4 +1,5 @@ from mcp_server_qdrant.embeddings import EmbeddingProvider +from mcp_server_qdrant.embeddings.types import EmbeddingProviderType from mcp_server_qdrant.settings import EmbeddingProviderSettings @@ -8,7 +9,7 @@ def create_embedding_provider(settings: EmbeddingProviderSettings) -> EmbeddingP :param settings: The settings for the embedding provider. :return: An instance of the specified embedding provider. """ - if settings.provider_type.lower() == "fastembed": + if settings.provider_type == EmbeddingProviderType.FASTEMBED: from mcp_server_qdrant.embeddings.fastembed import FastEmbedProvider return FastEmbedProvider(settings.model_name) diff --git a/src/mcp_server_qdrant/embeddings/types.py b/src/mcp_server_qdrant/embeddings/types.py new file mode 100644 index 0000000..5290703 --- /dev/null +++ b/src/mcp_server_qdrant/embeddings/types.py @@ -0,0 +1,5 @@ +from enum import Enum + + +class EmbeddingProviderType(Enum): + FASTEMBED = "fastembed" diff --git a/src/mcp_server_qdrant/main.py b/src/mcp_server_qdrant/main.py new file mode 100644 index 0000000..1c0f186 --- /dev/null +++ b/src/mcp_server_qdrant/main.py @@ -0,0 +1,9 @@ +from mcp_server_qdrant.server import mcp + + +def main(): + """ + Main entry point for the mcp-server-qdrant script defined + in pyproject.toml. It runs the MCP server. + """ + mcp.run() diff --git a/src/mcp_server_qdrant/server.py b/src/mcp_server_qdrant/server.py index fe36b80..bb38f71 100644 --- a/src/mcp_server_qdrant/server.py +++ b/src/mcp_server_qdrant/server.py @@ -1,5 +1,4 @@ import logging -import os from contextlib import asynccontextmanager from typing import AsyncIterator, List @@ -8,27 +7,18 @@ from mcp.server.fastmcp import Context, FastMCP from mcp_server_qdrant.embeddings.factory import create_embedding_provider from mcp_server_qdrant.qdrant import QdrantConnector -from mcp_server_qdrant.settings import ( - EmbeddingProviderSettings, - QdrantSettings, - parse_args, -) +from mcp_server_qdrant.settings import EmbeddingProviderSettings, QdrantSettings logger = logging.getLogger(__name__) -# Parse command line arguments and set them as environment variables. -# This is done for backwards compatibility with the previous versions -# of the MCP server. -env_vars = parse_args() -for key, value in env_vars.items(): - os.environ[key] = value - @asynccontextmanager async def server_lifespan(server: Server) -> AsyncIterator[dict]: # noqa """ Context manager to handle the lifespan of the server. This is used to configure the embedding provider and Qdrant connector. + All the configuration is now loaded from the environment variables. + Settings handle that for us. """ try: # Embedding provider is created with a factory function so we can add @@ -63,7 +53,7 @@ async def server_lifespan(server: Server) -> AsyncIterator[dict]: # noqa pass -mcp = FastMCP("Qdrant", lifespan=server_lifespan) +mcp = FastMCP("mcp-server-qdrant", lifespan=server_lifespan) @mcp.tool( @@ -116,7 +106,3 @@ async def find(query: str, ctx: Context) -> List[str]: for entry in entries: content.append(f"{entry}") return content - - -if __name__ == "__main__": - mcp.run() diff --git a/src/mcp_server_qdrant/settings.py b/src/mcp_server_qdrant/settings.py index e0aa6d0..f290fe5 100644 --- a/src/mcp_server_qdrant/settings.py +++ b/src/mcp_server_qdrant/settings.py @@ -1,17 +1,19 @@ -import argparse -from typing import Any, Dict, Optional +from typing import Optional from pydantic import Field from pydantic_settings import BaseSettings +from mcp_server_qdrant.embeddings.types import EmbeddingProviderType + class EmbeddingProviderSettings(BaseSettings): """ Configuration for the embedding provider. """ - provider_type: str = Field( - default="fastembed", validation_alias="EMBEDDING_PROVIDER" + provider_type: EmbeddingProviderType = Field( + default=EmbeddingProviderType.FASTEMBED, + validation_alias="EMBEDDING_PROVIDER", ) model_name: str = Field( default="sentence-transformers/all-MiniLM-L6-v2", @@ -36,66 +38,3 @@ class QdrantSettings(BaseSettings): Get the Qdrant location, either the URL or the local path. """ return self.location or self.local_path - - -def parse_args() -> Dict[str, Any]: - """ - Parse command line arguments for the MCP server. - - Returns: - Dict[str, Any]: Dictionary of parsed arguments - """ - parser = argparse.ArgumentParser(description="Qdrant MCP Server") - - # Qdrant connection options - connection_group = parser.add_mutually_exclusive_group() - connection_group.add_argument( - "--qdrant-url", - help="URL of the Qdrant server, e.g. http://localhost:6333", - ) - connection_group.add_argument( - "--qdrant-local-path", - help="Path to the local Qdrant database", - ) - - # Other Qdrant settings - parser.add_argument( - "--qdrant-api-key", - help="API key for the Qdrant server", - ) - parser.add_argument( - "--collection-name", - help="Name of the collection to use", - ) - - # Embedding settings - parser.add_argument( - "--embedding-provider", - help="Embedding provider to use (currently only 'fastembed' is supported)", - ) - parser.add_argument( - "--embedding-model", - help="Name of the embedding model to use", - ) - - args = parser.parse_args() - - # Convert to dictionary and filter out None values - args_dict = {k: v for k, v in vars(args).items() if v is not None} - - # Convert argument names to environment variable format - env_vars = {} - if "qdrant_url" in args_dict: - env_vars["QDRANT_URL"] = args_dict["qdrant_url"] - if "qdrant_api_key" in args_dict: - env_vars["QDRANT_API_KEY"] = args_dict["qdrant_api_key"] - if "collection_name" in args_dict: - env_vars["COLLECTION_NAME"] = args_dict["collection_name"] - if "embedding_model" in args_dict: - env_vars["EMBEDDING_MODEL"] = args_dict["embedding_model"] - if "embedding_provider" in args_dict: - env_vars["EMBEDDING_PROVIDER"] = args_dict["embedding_provider"] - if "qdrant_local_path" in args_dict: - env_vars["QDRANT_LOCAL_PATH"] = args_dict["qdrant_local_path"] - - return env_vars diff --git a/tests/test_config.py b/tests/test_config.py index ab0a057..ee40200 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -3,6 +3,7 @@ from unittest.mock import patch import pytest +from mcp_server_qdrant.embeddings.types import EmbeddingProviderType from mcp_server_qdrant.settings import EmbeddingProviderSettings, QdrantSettings @@ -47,15 +48,15 @@ class TestEmbeddingProviderSettings: def test_default_values(self): """Test default values are set correctly.""" settings = EmbeddingProviderSettings() - assert settings.provider_type == "fastembed" + assert settings.provider_type == EmbeddingProviderType.FASTEMBED assert settings.model_name == "sentence-transformers/all-MiniLM-L6-v2" @patch.dict( os.environ, - {"EMBEDDING_PROVIDER": "custom_provider", "EMBEDDING_MODEL": "custom_model"}, + {"EMBEDDING_MODEL": "custom_model"}, ) def test_custom_values(self): """Test loading custom values from environment variables.""" settings = EmbeddingProviderSettings() - assert settings.provider_type == "custom_provider" + assert settings.provider_type == EmbeddingProviderType.FASTEMBED assert settings.model_name == "custom_model" diff --git a/uv.lock b/uv.lock index 33de017..b954e12 100644 --- a/uv.lock +++ b/uv.lock @@ -498,7 +498,6 @@ dependencies = [ { name = "fastembed" }, { name = "mcp", extra = ["cli"] }, { name = "qdrant-client" }, - { name = "typer" }, ] [package.dev-dependencies] @@ -515,7 +514,6 @@ requires-dist = [ { name = "fastembed", specifier = ">=0.6.0" }, { name = "mcp", extras = ["cli"], specifier = ">=1.3.0" }, { name = "qdrant-client", specifier = ">=1.12.0" }, - { name = "typer", specifier = ">=0.15.2" }, ] [package.metadata.requires-dev]