Make MCP server optional dependency

The fastmcp server code is now an optional dependency that can be
installed with the "mcp" extra. Core vector search functionality is
available without the MCP server dependency.
This commit is contained in:
2025-09-26 17:12:37 +00:00
parent 250dfa728e
commit b1021bcbd5
11 changed files with 140 additions and 46 deletions

View File

@@ -5,13 +5,17 @@ description = "Add your description here"
readme = "README.md" readme = "README.md"
requires-python = ">=3.13" requires-python = ">=3.13"
dependencies = [ dependencies = [
"fastmcp>=2.12.3",
"qdrant-client==1.13", "qdrant-client==1.13",
"vault-settings>=0.1.0", "vault-settings>=0.1.0",
] ]
[project.optional-dependencies]
mcp = [
"fastmcp>=2.12.3",
]
[project.scripts] [project.scripts]
vector-search-mcp = "vector_search_mcp:run" vector-search-mcp = "vector_search_mcp.mcp_server:run"
[build-system] [build-system]
requires = ["uv_build"] requires = ["uv_build"]

View File

@@ -1,37 +0,0 @@
"""Vector Search MCP (Model Context Protocol) Package.
This package provides a Model Context Protocol server for vector similarity search
operations. It supports multiple vector database backends and provides a unified
interface for semantic search functionality.
The package includes:
- Abstract engine interface for pluggable vector database backends
- Qdrant vector database implementation
- Pydantic models for search operations and conditions
- MCP server implementation with transport support
Example:
Run the MCP server:
>>> from vector_search_mcp import run
>>> run("sse") # Start with Server-Sent Events transport
"""
from fastmcp.server.server import Transport
from .main import mcp
def run(transport: Transport = "sse"):
"""Run the vector search MCP server with the specified transport.
Args:
transport: The transport protocol to use. Either "sse" for Server-Sent Events
or "stdio" for standard input/output communication.
Example:
>>> run("sse") # Start with Server-Sent Events
>>> run("stdio") # Start with stdio transport
"""
mcp.run(transport=transport)

View File

@@ -0,0 +1,28 @@
from typing import final
from .engine import Backend, get_engine
from .models import Chunk, Condition
@final
class Client:
def __init__(self, backend: Backend, collection: str):
self.engine = get_engine(backend)
self.collection = collection
async def create_index(self, size: int) -> bool:
return await self.engine.create_index(self.collection, size)
async def upload_chunk(self, chunk: Chunk) -> bool:
return await self.engine.upload_chunk(self.collection, chunk)
async def semantic_search(
self,
embedding: list[float],
limit: int = 10,
conditions: list[Condition] | None = None,
threshold: float | None = None,
):
return await self.engine.semantic_search(
embedding, self.collection, limit, conditions, threshold
)

View File

@@ -14,15 +14,16 @@ maintaining a consistent interface for the semantic search workflow.
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import TypeVar from typing import TypeVar
from ..models import Condition, SearchRow from ..models import Chunk, Condition, SearchRow
ResponseType = TypeVar("ResponseType") ResponseType = TypeVar("ResponseType")
ConditionType = TypeVar("ConditionType") ConditionType = TypeVar("ConditionType")
ChunkType = TypeVar("ChunkType")
__all__ = ["BaseEngine"] __all__ = ["BaseEngine"]
class BaseEngine[ResponseType, ConditionType](ABC): class BaseEngine[ResponseType, ConditionType, ChunkType](ABC):
"""Abstract base class for vector search engines. """Abstract base class for vector search engines.
This class defines the interface that all vector search engine implementations This class defines the interface that all vector search engine implementations
@@ -34,6 +35,8 @@ class BaseEngine[ResponseType, ConditionType](ABC):
For example, list[ScoredPoint] for Qdrant. For example, list[ScoredPoint] for Qdrant.
ConditionType: The backend-specific filter/condition type. ConditionType: The backend-specific filter/condition type.
For example, models.Filter for Qdrant. For example, models.Filter for Qdrant.
ChunkType: The backend-specific chunk type.
For example, models.Point for Qdrant.
The class implements the Template Method pattern where semantic_search() The class implements the Template Method pattern where semantic_search()
orchestrates calls to the abstract methods that subclasses must implement. orchestrates calls to the abstract methods that subclasses must implement.
@@ -192,3 +195,16 @@ class BaseEngine[ResponseType, ConditionType](ABC):
embedding, collection, limit, transformed_conditions, threshold embedding, collection, limit, transformed_conditions, threshold
) )
return self.transform_response(response) return self.transform_response(response)
@abstractmethod
async def create_index(self, name: str, size: int) -> bool: ...
@abstractmethod
def transform_chunk(self, chunk: Chunk) -> ChunkType: ...
@abstractmethod
async def run_upload_chunk(self, index_name: str, chunk: ChunkType) -> bool: ...
async def upload_chunk(self, index_name: str, chunk: Chunk) -> bool:
transformed_chunk = self.transform_chunk(chunk)
return await self.run_upload_chunk(index_name, transformed_chunk)

View File

@@ -16,14 +16,16 @@ from typing import final, override
from qdrant_client import AsyncQdrantClient, models from qdrant_client import AsyncQdrantClient, models
from ..config import Settings from ..config import Settings
from ..models import Condition, Match, MatchAny, MatchExclude, SearchRow from ..models import Chunk, Condition, Match, MatchAny, MatchExclude, SearchRow
from .base_engine import BaseEngine from .base_engine import BaseEngine
__all__ = ["QdrantEngine"] __all__ = ["QdrantEngine"]
@final @final
class QdrantEngine(BaseEngine[list[models.ScoredPoint], models.Filter]): class QdrantEngine(
BaseEngine[list[models.ScoredPoint], models.Filter, models.PointStruct]
):
"""Qdrant vector database engine implementation. """Qdrant vector database engine implementation.
This class provides a concrete implementation of the BaseEngine interface This class provides a concrete implementation of the BaseEngine interface
@@ -195,3 +197,30 @@ class QdrantEngine(BaseEngine[list[models.ScoredPoint], models.Filter]):
with_vectors=False, with_vectors=False,
score_threshold=threshold, score_threshold=threshold,
) )
@override
async def create_index(self, name: str, size: int) -> bool:
return await self.client.create_collection(
collection_name=name,
vectors_config=models.VectorParams(
size=size, distance=models.Distance.COSINE
),
)
@override
async def run_upload_chunk(
self, index_name: str, chunk: models.PointStruct
) -> bool:
result = await self.client.upsert(
collection_name=index_name,
points=[chunk],
)
return result.status == models.UpdateStatus.ACKNOWLEDGED
@override
def transform_chunk(self, chunk: Chunk) -> models.PointStruct:
return models.PointStruct(
id=chunk.id,
vector=chunk.vector,
payload=chunk.payload.model_dump(),
)

View File

@@ -0,0 +1,37 @@
"""Vector Search MCP (Model Context Protocol) Package.
This package provides a Model Context Protocol server for vector similarity search
operations. It supports multiple vector database backends and provides a unified
interface for semantic search functionality.
The package includes:
- Abstract engine interface for pluggable vector database backends
- Qdrant vector database implementation
- Pydantic models for search operations and conditions
- MCP server implementation with transport support
Example:
Run the MCP server:
>>> from vector_search_mcp import run
>>> run("sse") # Start with Server-Sent Events transport
"""
from fastmcp.server.server import Transport
from .server import mcp
def run(transport: Transport = "sse"):
"""Run the vector search MCP server with the specified transport.
Args:
transport: The transport protocol to use. Either "sse" for Server-Sent Events
or "stdio" for standard input/output communication.
Example:
>>> run("sse") # Start with Server-Sent Events
>>> run("stdio") # Start with stdio transport
"""
mcp.run(transport=transport)

View File

@@ -18,7 +18,7 @@ Example:
from fastmcp import FastMCP from fastmcp import FastMCP
from .engine import Backend, get_engine from ..engine import Backend, get_engine
mcp = FastMCP("Vector Search MCP") mcp = FastMCP("Vector Search MCP")

View File

@@ -119,3 +119,15 @@ class MatchExclude(Condition):
key: str key: str
exclude: list[str] exclude: list[str]
class ChunkData(BaseModel):
page_content: str
filename: str
page: int
class Chunk(BaseModel):
id: str
vector: list[float]
payload: ChunkData

9
uv.lock generated
View File

@@ -1620,11 +1620,15 @@ name = "vector-search-mcp"
version = "0.1.0" version = "0.1.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "fastmcp" },
{ name = "qdrant-client" }, { name = "qdrant-client" },
{ name = "vault-settings" }, { name = "vault-settings" },
] ]
[package.optional-dependencies]
mcp = [
{ name = "fastmcp" },
]
[package.dev-dependencies] [package.dev-dependencies]
dev = [ dev = [
{ name = "fastembed" }, { name = "fastembed" },
@@ -1636,10 +1640,11 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "fastmcp", specifier = ">=2.12.3" }, { name = "fastmcp", marker = "extra == 'mcp'", specifier = ">=2.12.3" },
{ name = "qdrant-client", specifier = "==1.13" }, { name = "qdrant-client", specifier = "==1.13" },
{ name = "vault-settings", specifier = ">=0.1.0" }, { name = "vault-settings", specifier = ">=0.1.0" },
] ]
provides-extras = ["mcp"]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [