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
This commit is contained in:
Kacper Łukawski
2025-03-11 21:05:39 +01:00
committed by GitHub
parent 41bab919be
commit 5878cc1267
5 changed files with 208 additions and 50 deletions

117
README.md
View File

@@ -31,20 +31,59 @@ It acts as a semantic memory layer on top of the Qdrant database.
- `query` (string): Query to use for searching - `query` (string): Query to use for searching
- Returns: Information stored in the Qdrant database as separate messages - 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 ```shell
mcp install src/mcp_server_qdrant/server.py \ QDRANT_URL="http://localhost:6333" \
-v QDRANT_URL="http://localhost:6333" \ COLLECTION_NAME="my-collection" \
-v QDRANT_API_KEY="your_api_key" \ EMBEDDING_MODEL="sentence-transformers/all-MiniLM-L6-v2" \
-v COLLECTION_NAME="my_collection" \ uvx mcp-server-qdrant
-v EMBEDDING_MODEL="sentence-transformers/all-MiniLM-L6-v2"
``` ```
#### 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 ### Installing via Smithery
To install Qdrant MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/protocol/mcp-server-qdrant): 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 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 To use this server with the Claude Desktop app, add the following configuration to the "mcpServers" section of your
`claude_desktop_config.json`: `claude_desktop_config.json`:
@@ -64,9 +103,9 @@ To use this server with the Claude Desktop app, add the following configuration
"command": "uvx", "command": "uvx",
"args": ["mcp-server-qdrant"], "args": ["mcp-server-qdrant"],
"env": { "env": {
"QDRANT_URL": "http://localhost:6333", "QDRANT_URL": "https://xyz-example.eu-central.aws.cloud.qdrant.io:6333",
"QDRANT_API_KEY": "your_api_key", "QDRANT_API_KEY": "your_api_key",
"COLLECTION_NAME": "your_collection_name", "COLLECTION_NAME": "your-collection-name",
"EMBEDDING_MODEL": "sentence-transformers/all-MiniLM-L6-v2" "EMBEDDING_MODEL": "sentence-transformers/all-MiniLM-L6-v2"
} }
} }
@@ -82,7 +121,7 @@ For local Qdrant mode:
"args": ["mcp-server-qdrant"], "args": ["mcp-server-qdrant"],
"env": { "env": {
"QDRANT_LOCAL_PATH": "/path/to/qdrant/database", "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" "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. 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. 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 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 [Cursor](https://docs.cursor.com/context/model-context-protocol), which provides built-in support for the Model Context
Protocol. 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` ```bash
- `QDRANT_API_KEY`: API key for the Qdrant server (optional, depends on Qdrant server configuration) QDRANT_URL="http://localhost:6333" \
- `COLLECTION_NAME`: Name of the collection to use (required) COLLECTION_NAME="code-snippets" \
- `EMBEDDING_MODEL`: Name of the embedding model to use (default: `sentence-transformers/all-MiniLM-L6-v2`) 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." \
- `EMBEDDING_PROVIDER`: Embedding provider to use (currently only "fastembed" is supported) 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." \
- `QDRANT_LOCAL_PATH`: Path to the local Qdrant database (alternative to `QDRANT_URL`) 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 ## Contributing

View File

@@ -1,9 +1,24 @@
from mcp_server_qdrant.server import mcp import argparse
def main(): def main():
""" """
Main entry point for the mcp-server-qdrant script defined 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)

View File

@@ -1,14 +1,18 @@
import json import json
import logging import logging
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from typing import AsyncIterator, List, Optional from typing import AsyncIterator, List
from mcp.server import Server from mcp.server import Server
from mcp.server.fastmcp import Context, FastMCP from mcp.server.fastmcp import Context, FastMCP
from mcp_server_qdrant.embeddings.factory import create_embedding_provider from mcp_server_qdrant.embeddings.factory import create_embedding_provider
from mcp_server_qdrant.qdrant import Entry, Metadata, QdrantConnector 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__) logger = logging.getLogger(__name__)
@@ -54,20 +58,26 @@ async def server_lifespan(server: Server) -> AsyncIterator[dict]: # noqa
pass 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) 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", @mcp.tool(name="qdrant-store", description=tool_settings.tool_store_description)
description=(
"Keep the memory for later use, when you are asked to remember something."
),
)
async def store( 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: ) -> str:
""" """
Store a memory in Qdrant. Store some information in Qdrant.
:param ctx: The context for the request. :param ctx: The context for the request.
:param information: The information to store. :param information: The information to store.
:param metadata: JSON metadata to store with the information, optional. :param metadata: JSON metadata to store with the information, optional.
@@ -82,15 +92,7 @@ async def store(
return f"Remembered: {information}" return f"Remembered: {information}"
@mcp.tool( @mcp.tool(name="qdrant-find", description=tool_settings.tool_find_description)
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"
),
)
async def find(ctx: Context, query: str) -> List[str]: async def find(ctx: Context, query: str) -> List[str]:
""" """
Find memories in Qdrant. 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. :param query: The query to use for the search.
:return: A list of entries found. :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: QdrantConnector = ctx.request_context.lifespan_context[
"qdrant_connector" "qdrant_connector"
] ]
entries = await qdrant_connector.search(query) entries = await qdrant_connector.search(query)
if not entries: if not entries:
return [f"No memories found for the query '{query}'"] return [f"No information found for the query '{query}'"]
content = [ content = [
f"Memories for the query '{query}'", f"Results for the query '{query}'",
] ]
for entry in entries: for entry in entries:
# Format the metadata as a JSON string and produce XML-like output # Format the metadata as a JSON string and produce XML-like output

View File

@@ -5,6 +5,31 @@ from pydantic_settings import BaseSettings
from mcp_server_qdrant.embeddings.types import EmbeddingProviderType 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): class EmbeddingProviderSettings(BaseSettings):
""" """

View File

@@ -4,7 +4,13 @@ from unittest.mock import patch
import pytest import pytest
from mcp_server_qdrant.embeddings.types import EmbeddingProviderType 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: class TestQdrantSettings:
@@ -60,3 +66,44 @@ class TestEmbeddingProviderSettings:
settings = EmbeddingProviderSettings() settings = EmbeddingProviderSettings()
assert settings.provider_type == EmbeddingProviderType.FASTEMBED assert settings.provider_type == EmbeddingProviderType.FASTEMBED
assert settings.model_name == "custom_model" 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"