From 5878cc1267c87c9d58697d30fc2b81c885d880db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=81ukawski?= Date: Tue, 11 Mar 2025 21:05:39 +0100 Subject: [PATCH] Custom tool descriptions (#29) * Allow setting up the tool descriptions with env variables * Document the env variables as a table in README * Link settings.py in README.md * Allow to choose transport protocol: stdio or sse * Fix metadata handling in Cursor * Improve README to cover more cases * Add info about Cursor rules * Fix Github note type --- README.md | 117 ++++++++++++++++----- src/mcp_server_qdrant/main.py | 21 +++- src/mcp_server_qdrant/server.py | 46 ++++---- src/mcp_server_qdrant/settings.py | 25 +++++ tests/{test_config.py => test_settings.py} | 49 ++++++++- 5 files changed, 208 insertions(+), 50 deletions(-) rename tests/{test_config.py => test_settings.py} (54%) diff --git a/README.md b/README.md index c8e21c1..54e9909 100644 --- a/README.md +++ b/README.md @@ -31,20 +31,59 @@ It acts as a semantic memory layer on top of the Qdrant database. - `query` (string): Query to use for searching - Returns: Information stored in the Qdrant database as separate messages -## Installation in Claude Desktop +> [!NOTE] +> The tool descriptions provided above are examples and may need to be customized for your specific use case. Consider adjusting the descriptions to better match your team's workflow and the specific types of code snippets you want to store and retrieve. -### Using mcp (recommended) +## Environment Variables -When using [`mcp`](https://github.com/modelcontextprotocol/python-sdk) no specific installation is needed to directly run *mcp-server-qdrant*. +The configuration of the server is done using environment variables: + +| Name | Description | Default Value | +|--------------------------|---------------------------------------------------------------------|-------------------------------------------------------------------| +| `QDRANT_URL` | URL of the Qdrant server | None | +| `QDRANT_API_KEY` | API key for the Qdrant server | None | +| `COLLECTION_NAME` | Name of the collection to use | *Required* | +| `QDRANT_LOCAL_PATH` | Path to the local Qdrant database (alternative to `QDRANT_URL`) | None | +| `EMBEDDING_PROVIDER` | Embedding provider to use (currently only "fastembed" is supported) | `fastembed` | +| `EMBEDDING_MODEL` | Name of the embedding model to use | `sentence-transformers/all-MiniLM-L6-v2` | +| `TOOL_STORE_DESCRIPTION` | Custom description for the store tool | See default in [`settings.py`](src/mcp_server_qdrant/settings.py) | +| `TOOL_FIND_DESCRIPTION` | Custom description for the find tool | See default in [`settings.py`](src/mcp_server_qdrant/settings.py) | + +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. + +## Installation + +### Using uvx + +When using [`uvx`](https://docs.astral.sh/uv/guides/tools/#running-tools) no specific installation is needed to directly run *mcp-server-qdrant*. ```shell -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" +QDRANT_URL="http://localhost:6333" \ +COLLECTION_NAME="my-collection" \ +EMBEDDING_MODEL="sentence-transformers/all-MiniLM-L6-v2" \ +uvx mcp-server-qdrant ``` +#### Transport Protocols + +The server supports different transport protocols that can be specified using the `--transport` flag: + +```shell +QDRANT_URL="http://localhost:6333" \ +COLLECTION_NAME="my-collection" \ +uvx mcp-server-qdrant --transport sse +``` + +Supported transport protocols: + +- `stdio` (default): Standard input/output transport, might only be used by local MCP clients +- `sse`: Server-Sent Events transport, perfect for remote clients + +The default transport is `stdio` if not specified. + ### Installing via Smithery To install Qdrant MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/protocol/mcp-server-qdrant): @@ -53,7 +92,7 @@ To install Qdrant MCP Server for Claude Desktop automatically via [Smithery](htt npx @smithery/cli install mcp-server-qdrant --client claude ``` -### Manual configuration +### Manual configuration of 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`: @@ -64,9 +103,9 @@ To use this server with the Claude Desktop app, add the following configuration "command": "uvx", "args": ["mcp-server-qdrant"], "env": { - "QDRANT_URL": "http://localhost:6333", + "QDRANT_URL": "https://xyz-example.eu-central.aws.cloud.qdrant.io:6333", "QDRANT_API_KEY": "your_api_key", - "COLLECTION_NAME": "your_collection_name", + "COLLECTION_NAME": "your-collection-name", "EMBEDDING_MODEL": "sentence-transformers/all-MiniLM-L6-v2" } } @@ -82,7 +121,7 @@ For local Qdrant mode: "args": ["mcp-server-qdrant"], "env": { "QDRANT_LOCAL_PATH": "/path/to/qdrant/database", - "COLLECTION_NAME": "your_collection_name", + "COLLECTION_NAME": "your-collection-name", "EMBEDDING_MODEL": "sentence-transformers/all-MiniLM-L6-v2" } } @@ -94,27 +133,57 @@ This MCP server will automatically create a collection with the specified name i 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. -### Support for other tools +## Support for other tools 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 +### Using with Cursor/Windsurf -The configuration of the server is done using environment variables: +You can configure this MCP server to work as a code search tool for Cursor or Windsurf by customizing the tool +descriptions: -- `QDRANT_URL`: URL of the Qdrant server, e.g. `http://localhost:6333` -- `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 (alternative to `QDRANT_URL`) +```bash +QDRANT_URL="http://localhost:6333" \ +COLLECTION_NAME="code-snippets" \ +TOOL_STORE_DESCRIPTION="Store reusable code snippets for later retrieval. The 'information' parameter should contain a natural language description of what the code does, while the actual code should be included in the 'metadata' parameter as a 'code' property. The value of 'metadata' is a Python dictionary with strings as keys. Use this whenever you generate some code snippet." \ +TOOL_FIND_DESCRIPTION="Search for relevant code snippets based on natural language descriptions. The 'query' parameter should describe what you're looking for, and the tool will return the most relevant code snippets. Use this when you need to find existing code snippets for reuse or reference." \ +uvx mcp-server-qdrant --transport sse # Enable SSE transport +``` -Note: You cannot provide both `QDRANT_URL` and `QDRANT_LOCAL_PATH` at the same time. +In Cursor/Windsurf, you can then configure the MCP server in your settings by pointing to this running server using +SSE transport protocol. The description on how to add an MCP server to Cursor can be found in the [Cursor +documentation](https://docs.cursor.com/context/model-context-protocol#adding-an-mcp-server-to-cursor). If you are +running Cursor/Windsurf locally, you can use the following URL: -> [!IMPORTANT] -> Command-line arguments are not supported anymore! Please use environment variables for all configuration. +``` +http://localhost:8000/sse +``` + +> [!TIP] +> We suggest SSE transport as a preferred way to connect Cursor/Windsurf to the MCP server, as it can support remote +> connections. That makes it easy to share the server with your team or use it in a cloud environment. + +This configuration transforms the Qdrant MCP server into a specialized code search tool that can: + +1. Store code snippets, documentation, and implementation details +2. Retrieve relevant code examples based on semantic search +3. Help developers find specific implementations or usage patterns + +You can populate the database by storing natural language descriptions of code snippets (in the `information` parameter) +along with the actual code (in the `metadata.code` property), and then search for them using natural language queries +that describe what you're looking for. + +> [!NOTE] +> The tool descriptions provided above are examples and may need to be customized for your specific use case. Consider +> adjusting the descriptions to better match your team's workflow and the specific types of code snippets you want to +> store and retrieve. + +**If you have successfully installed the `mcp-server-qdrant`, but still can't get it to work with Cursor, please +consider creating the [Cursor rules](https://docs.cursor.com/context/rules-for-ai) so the MCP tools are always used when +the agent produces a new code snippet.** You can restrict the rules to only work for certain file types, to avoid using +the MCP server for the documentation or other types of content. ## Contributing diff --git a/src/mcp_server_qdrant/main.py b/src/mcp_server_qdrant/main.py index 1c0f186..96ce833 100644 --- a/src/mcp_server_qdrant/main.py +++ b/src/mcp_server_qdrant/main.py @@ -1,9 +1,24 @@ -from mcp_server_qdrant.server import mcp +import argparse def main(): """ Main entry point for the mcp-server-qdrant script defined - in pyproject.toml. It runs the MCP server. + in pyproject.toml. It runs the MCP server with a specific transport + protocol. """ - mcp.run() + + # Parse the command-line arguments to determine the transport protocol. + parser = argparse.ArgumentParser(description="mcp-server-qdrant") + parser.add_argument( + "--transport", + choices=["stdio", "sse"], + default="stdio", + ) + args = parser.parse_args() + + # Import is done here to make sure environment variables are loaded + # only after we make the changes. + from mcp_server_qdrant.server import mcp + + mcp.run(transport=args.transport) diff --git a/src/mcp_server_qdrant/server.py b/src/mcp_server_qdrant/server.py index df94fe2..051a999 100644 --- a/src/mcp_server_qdrant/server.py +++ b/src/mcp_server_qdrant/server.py @@ -1,14 +1,18 @@ import json import logging from contextlib import asynccontextmanager -from typing import AsyncIterator, List, Optional +from typing import AsyncIterator, List from mcp.server import Server from mcp.server.fastmcp import Context, FastMCP from mcp_server_qdrant.embeddings.factory import create_embedding_provider from mcp_server_qdrant.qdrant import Entry, Metadata, QdrantConnector -from mcp_server_qdrant.settings import EmbeddingProviderSettings, QdrantSettings +from mcp_server_qdrant.settings import ( + EmbeddingProviderSettings, + QdrantSettings, + ToolSettings, +) logger = logging.getLogger(__name__) @@ -54,20 +58,26 @@ async def server_lifespan(server: Server) -> AsyncIterator[dict]: # noqa pass +# FastMCP is an alternative interface for declaring the capabilities +# of the server. Its API is based on FastAPI. mcp = FastMCP("mcp-server-qdrant", lifespan=server_lifespan) +# Load the tool settings from the env variables, if they are set, +# or use the default values otherwise. +tool_settings = ToolSettings() -@mcp.tool( - name="qdrant-store", - description=( - "Keep the memory for later use, when you are asked to remember something." - ), -) + +@mcp.tool(name="qdrant-store", description=tool_settings.tool_store_description) async def store( - ctx: Context, information: str, metadata: Optional[Metadata] = None + ctx: Context, + information: str, + # The `metadata` parameter is defined as non-optional, but it can be None. + # If we set it to be optional, some of the MCP clients, like Cursor, cannot + # handle the optional parameter correctly. + metadata: Metadata = None, ) -> str: """ - Store a memory in Qdrant. + Store some information in Qdrant. :param ctx: The context for the request. :param information: The information to store. :param metadata: JSON metadata to store with the information, optional. @@ -82,15 +92,7 @@ async def store( return f"Remembered: {information}" -@mcp.tool( - name="qdrant-find", - 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" - ), -) +@mcp.tool(name="qdrant-find", description=tool_settings.tool_find_description) async def find(ctx: Context, query: str) -> List[str]: """ Find memories in Qdrant. @@ -98,15 +100,15 @@ async def find(ctx: Context, query: str) -> List[str]: :param query: The query to use for the search. :return: A list of entries found. """ - await ctx.debug(f"Finding points for query {query}") + await ctx.debug(f"Finding results for query {query}") qdrant_connector: QdrantConnector = ctx.request_context.lifespan_context[ "qdrant_connector" ] entries = await qdrant_connector.search(query) if not entries: - return [f"No memories found for the query '{query}'"] + return [f"No information found for the query '{query}'"] content = [ - f"Memories for the query '{query}'", + f"Results for the query '{query}'", ] for entry in entries: # Format the metadata as a JSON string and produce XML-like output diff --git a/src/mcp_server_qdrant/settings.py b/src/mcp_server_qdrant/settings.py index f290fe5..cad8834 100644 --- a/src/mcp_server_qdrant/settings.py +++ b/src/mcp_server_qdrant/settings.py @@ -5,6 +5,31 @@ from pydantic_settings import BaseSettings from mcp_server_qdrant.embeddings.types import EmbeddingProviderType +DEFAULT_TOOL_STORE_DESCRIPTION = ( + "Keep the memory for later use, when you are asked to remember something." +) +DEFAULT_TOOL_FIND_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" +) + + +class ToolSettings(BaseSettings): + """ + Configuration for all the tools. + """ + + tool_store_description: str = Field( + default=DEFAULT_TOOL_STORE_DESCRIPTION, + validation_alias="TOOL_STORE_DESCRIPTION", + ) + tool_find_description: str = Field( + default=DEFAULT_TOOL_FIND_DESCRIPTION, + validation_alias="TOOL_FIND_DESCRIPTION", + ) + class EmbeddingProviderSettings(BaseSettings): """ diff --git a/tests/test_config.py b/tests/test_settings.py similarity index 54% rename from tests/test_config.py rename to tests/test_settings.py index ee40200..8390ff8 100644 --- a/tests/test_config.py +++ b/tests/test_settings.py @@ -4,7 +4,13 @@ from unittest.mock import patch import pytest from mcp_server_qdrant.embeddings.types import EmbeddingProviderType -from mcp_server_qdrant.settings import EmbeddingProviderSettings, QdrantSettings +from mcp_server_qdrant.settings import ( + DEFAULT_TOOL_FIND_DESCRIPTION, + DEFAULT_TOOL_STORE_DESCRIPTION, + EmbeddingProviderSettings, + QdrantSettings, + ToolSettings, +) class TestQdrantSettings: @@ -60,3 +66,44 @@ class TestEmbeddingProviderSettings: settings = EmbeddingProviderSettings() assert settings.provider_type == EmbeddingProviderType.FASTEMBED assert settings.model_name == "custom_model" + + +class TestToolSettings: + def test_default_values(self): + """Test that default values are set correctly when no env vars are provided.""" + settings = ToolSettings() + assert settings.tool_store_description == DEFAULT_TOOL_STORE_DESCRIPTION + assert settings.tool_find_description == DEFAULT_TOOL_FIND_DESCRIPTION + + @patch.dict( + os.environ, + {"TOOL_STORE_DESCRIPTION": "Custom store description"}, + ) + def test_custom_store_description(self): + """Test loading custom store description from environment variable.""" + settings = ToolSettings() + assert settings.tool_store_description == "Custom store description" + assert settings.tool_find_description == DEFAULT_TOOL_FIND_DESCRIPTION + + @patch.dict( + os.environ, + {"TOOL_FIND_DESCRIPTION": "Custom find description"}, + ) + def test_custom_find_description(self): + """Test loading custom find description from environment variable.""" + settings = ToolSettings() + assert settings.tool_store_description == DEFAULT_TOOL_STORE_DESCRIPTION + assert settings.tool_find_description == "Custom find description" + + @patch.dict( + os.environ, + { + "TOOL_STORE_DESCRIPTION": "Custom store description", + "TOOL_FIND_DESCRIPTION": "Custom find description", + }, + ) + def test_all_custom_values(self): + """Test loading all custom values from environment variables.""" + settings = ToolSettings() + assert settings.tool_store_description == "Custom store description" + assert settings.tool_find_description == "Custom find description"