From a4d2d0f8a6a19c8bf9a6d28d7bf8534a6d77fcae Mon Sep 17 00:00:00 2001 From: Teagan Glenn Date: Sat, 13 Sep 2025 22:57:44 -0600 Subject: [PATCH] docs: document PlexServer Qdrant client --- AGENTS.md | 2 ++ mcp_plex/server.py | 43 +++++++++++++++++++++++++------------------ pyproject.toml | 2 +- tests/test_server.py | 3 ++- uv.lock | 2 +- 5 files changed, 31 insertions(+), 21 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a0eaf21..9fb6540 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,6 +11,8 @@ `SPARSE_MODEL` environment variables or the corresponding CLI options. - Hybrid search uses Qdrant's built-in `FusionQuery` with reciprocal rank fusion to combine dense and sparse results before optional cross-encoder reranking. +- Qdrant client initialization moved into `PlexServer` to centralize state and + simplify testing. ## User Queries The project should handle natural-language searches and recommendations such as: diff --git a/mcp_plex/server.py b/mcp_plex/server.py index 29f385d..d2cf832 100644 --- a/mcp_plex/server.py +++ b/mcp_plex/server.py @@ -50,16 +50,21 @@ if _QDRANT_URL is None and _QDRANT_HOST is None: _QDRANT_URL = ":memory:" -# Instantiate global client -_client = AsyncQdrantClient( - location=_QDRANT_URL, - api_key=_QDRANT_API_KEY, - host=_QDRANT_HOST, - port=_QDRANT_PORT, - grpc_port=_QDRANT_GRPC_PORT, - prefer_grpc=_QDRANT_PREFER_GRPC, - https=_QDRANT_HTTPS, -) + +class PlexServer(FastMCP): + """FastMCP server with an attached Qdrant client.""" + + def __init__(self) -> None: # noqa: D401 - short description inherited + super().__init__() + self.qdrant_client = AsyncQdrantClient( + location=_QDRANT_URL, + api_key=_QDRANT_API_KEY, + host=_QDRANT_HOST, + port=_QDRANT_PORT, + grpc_port=_QDRANT_GRPC_PORT, + prefer_grpc=_QDRANT_PREFER_GRPC, + https=_QDRANT_HTTPS, + ) _USE_RERANKER = os.getenv("USE_RERANKER", "1") == "1" _reranker = None @@ -69,7 +74,7 @@ except Exception: _reranker = None -server = FastMCP() +server = PlexServer() _CACHE_SIZE = 128 @@ -98,7 +103,9 @@ async def _find_records(identifier: str, limit: int = 5) -> list[models.Record]: # First, try direct ID lookup try: record_id: Any = int(identifier) if identifier.isdigit() else identifier - recs = await _client.retrieve("media-items", ids=[record_id], with_payload=True) + recs = await server.qdrant_client.retrieve( + "media-items", ids=[record_id], with_payload=True + ) if recs: return recs except Exception: @@ -119,7 +126,7 @@ async def _find_records(identifier: str, limit: int = 5) -> list[models.Record]: models.FieldCondition(key="title", match=models.MatchText(text=identifier)) ) flt = models.Filter(should=should) - points, _ = await _client.scroll( + points, _ = await server.qdrant_client.scroll( collection_name="media-items", limit=limit, scroll_filter=flt, @@ -199,7 +206,7 @@ async def search_media( limit=candidate_limit, ), ] - res = await _client.query_points( + res = await server.qdrant_client.query_points( collection_name="media-items", query=models.FusionQuery(fusion=models.Fusion.RRF), prefetch=prefetch, @@ -274,7 +281,7 @@ async def recommend_media( record = records[0] if record is None: return [] - recs = await _client.recommend( + recs = await server.qdrant_client.recommend( collection_name="media-items", positive=[record.id], limit=limit, @@ -307,7 +314,7 @@ async def new_movies( ) ] ) - res = await _client.query_points( + res = await server.qdrant_client.query_points( collection_name="media-items", query=query, query_filter=flt, @@ -340,7 +347,7 @@ async def new_shows( ) ] ) - res = await _client.query_points( + res = await server.qdrant_client.query_points( collection_name="media-items", query=query, query_filter=flt, @@ -393,7 +400,7 @@ async def actor_movies( query = models.OrderByQuery( order_by=models.OrderBy(key="year", direction=models.Direction.DESC) ) - res = await _client.query_points( + res = await server.qdrant_client.query_points( collection_name="media-items", query=query, query_filter=flt, diff --git a/pyproject.toml b/pyproject.toml index fe48e13..d976ac0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "mcp-plex" -version = "0.26.9" +version = "0.26.10" description = "Plex-Oriented Model Context Protocol Server" requires-python = ">=3.11,<3.13" diff --git a/tests/test_server.py b/tests/test_server.py index 40dddc6..ba518b1 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -54,13 +54,14 @@ def __init__(self, *args, **kwargs): monkeypatch.setenv("QDRANT_GRPC_PORT", "5678") monkeypatch.setenv("QDRANT_PREFER_GRPC", "1") monkeypatch.setenv("QDRANT_HTTPS", "1") - importlib.reload(importlib.import_module("mcp_plex.server")) + module = importlib.reload(importlib.import_module("mcp_plex.server")) assert captured["host"] == "example.com" assert captured["port"] == 1234 assert captured["grpc_port"] == 5678 assert captured["prefer_grpc"] is True assert captured["https"] is True + assert hasattr(module.server, "qdrant_client") def test_server_tools(monkeypatch): diff --git a/uv.lock b/uv.lock index ea53e90..817dfa1 100644 --- a/uv.lock +++ b/uv.lock @@ -690,7 +690,7 @@ wheels = [ [[package]] name = "mcp-plex" -version = "0.26.9" +version = "0.26.10" source = { editable = "." } dependencies = [ { name = "fastapi" },