diff --git a/docker/pyproject.deps.toml b/docker/pyproject.deps.toml index 6179374..04f2ca3 100644 --- a/docker/pyproject.deps.toml +++ b/docker/pyproject.deps.toml @@ -1,6 +1,6 @@ [project] name = "mcp-plex" -version = "1.0.20" +version = "1.0.22" requires-python = ">=3.11,<3.13" dependencies = [ "fastmcp>=2.11.2", diff --git a/mcp_plex/server/__init__.py b/mcp_plex/server/__init__.py index ece8115..27e843b 100644 --- a/mcp_plex/server/__init__.py +++ b/mcp_plex/server/__init__.py @@ -820,43 +820,18 @@ def _listify(value: Sequence[str] | str | None) -> list[str]: ) ) - prefetch_entries: list[models.Prefetch] = [] - for name, doc in vector_queries: - prefetch_entries.append( - models.Prefetch( - query=models.NearestQuery(nearest=doc), - using=name, - limit=limit, - ) - ) - - if len(prefetch_entries) > 1: - candidate_limit = limit * 3 - prefetch_entries = [ - models.Prefetch(query=p.query, using=p.using, limit=candidate_limit) - for p in prefetch_entries - ] - query_obj: models.Query = models.FusionQuery(fusion=models.Fusion.RRF) - using_param = None - prefetch_param: Sequence[models.Prefetch] | None = prefetch_entries - elif prefetch_entries: - query_obj = prefetch_entries[0].query - using_param = prefetch_entries[0].using - prefetch_param = None - else: - query_obj = None - using_param = None - prefetch_param = None - must: list[models.FieldCondition] = [] + keyword_prefetch_conditions: list[models.FieldCondition] = [] if title: must.append(models.FieldCondition(key="title", match=models.MatchText(text=title))) media_type = type if media_type: - must.append( - models.FieldCondition(key="type", match=models.MatchValue(value=media_type)) + condition = models.FieldCondition( + key="type", match=models.MatchValue(value=media_type) ) + must.append(condition) + keyword_prefetch_conditions.append(condition) if year is not None: must.append(models.FieldCondition(key="year", match=models.MatchValue(value=year))) if year_from is not None or year_to is not None: @@ -875,30 +850,42 @@ def _listify(value: Sequence[str] | str | None) -> list[str]: must.append(models.FieldCondition(key="added_at", range=models.Range(**rng_at))) for actor in _listify(actors): - must.append(models.FieldCondition(key="actors", match=models.MatchValue(value=actor))) + condition = models.FieldCondition( + key="actors", match=models.MatchValue(value=actor) + ) + must.append(condition) + keyword_prefetch_conditions.append(condition) for director in _listify(directors): - must.append( - models.FieldCondition(key="directors", match=models.MatchValue(value=director)) + condition = models.FieldCondition( + key="directors", match=models.MatchValue(value=director) ) + must.append(condition) + keyword_prefetch_conditions.append(condition) for writer in _listify(writers): - must.append( - models.FieldCondition(key="writers", match=models.MatchValue(value=writer)) + condition = models.FieldCondition( + key="writers", match=models.MatchValue(value=writer) ) + must.append(condition) + keyword_prefetch_conditions.append(condition) for genre in _listify(genres): - must.append(models.FieldCondition(key="genres", match=models.MatchValue(value=genre))) + condition = models.FieldCondition( + key="genres", match=models.MatchValue(value=genre) + ) + must.append(condition) + keyword_prefetch_conditions.append(condition) for collection in _listify(collections): - must.append( - models.FieldCondition( - key="collections", match=models.MatchValue(value=collection) - ) + condition = models.FieldCondition( + key="collections", match=models.MatchValue(value=collection) ) + must.append(condition) + keyword_prefetch_conditions.append(condition) if show_title: - must.append( - models.FieldCondition( - key="show_title", match=models.MatchValue(value=show_title) - ) + condition = models.FieldCondition( + key="show_title", match=models.MatchValue(value=show_title) ) + must.append(condition) + keyword_prefetch_conditions.append(condition) if season_number is not None: must.append( models.FieldCondition( @@ -924,18 +911,18 @@ def _listify(value: Sequence[str] | str | None) -> list[str]: must.append(models.FieldCondition(key="reviews", match=models.MatchText(text=reviews))) if plex_rating_key: - must.append( - models.FieldCondition( - key="data.plex.rating_key", - match=models.MatchValue(value=plex_rating_key), - ) + condition = models.FieldCondition( + key="data.plex.rating_key", + match=models.MatchValue(value=plex_rating_key), ) + must.append(condition) + keyword_prefetch_conditions.append(condition) if imdb_id: - must.append( - models.FieldCondition( - key="data.imdb.id", match=models.MatchValue(value=imdb_id) - ) + condition = models.FieldCondition( + key="data.imdb.id", match=models.MatchValue(value=imdb_id) ) + must.append(condition) + keyword_prefetch_conditions.append(condition) if tmdb_id is not None: must.append( models.FieldCondition( @@ -947,6 +934,36 @@ def _listify(value: Sequence[str] | str | None) -> list[str]: if must: filter_obj = models.Filter(must=must) + prefetch_filter: models.Filter | None = None + if keyword_prefetch_conditions: + prefetch_filter = models.Filter(must=keyword_prefetch_conditions) + if filter_obj is None: + filter_obj = models.Filter(must=keyword_prefetch_conditions) + + query_obj: models.Query | None = None + using_param: str | None = None + prefetch_param: Sequence[models.Prefetch] | None = None + if vector_queries: + candidate_limit = limit * 3 if len(vector_queries) > 1 else limit + prefetch_entries = [ + models.Prefetch( + query=models.NearestQuery(nearest=doc), + using=name, + limit=candidate_limit, + filter=prefetch_filter, + ) + for name, doc in vector_queries + ] + if len(prefetch_entries) > 1: + query_obj = models.FusionQuery(fusion=models.Fusion.RRF) + using_param = None + prefetch_param = prefetch_entries + else: + prefetch_entry = prefetch_entries[0] + query_obj = prefetch_entry.query + using_param = prefetch_entry.using + prefetch_param = None + if query_obj is None: query_obj = models.SampleQuery(sample=models.Sample.RANDOM) diff --git a/pyproject.toml b/pyproject.toml index 0785436..ab4f7b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "mcp-plex" -version = "1.0.20" +version = "1.0.22" 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 566f413..c5ce7e8 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -717,6 +717,52 @@ async def fake_query_points(*args, **kwargs): assert query_filter is not None assert len(query_filter.must) >= 10 assert isinstance(captured["query"], models.FusionQuery) + prefetch = captured["prefetch"] + assert prefetch is not None + expected_prefetch_keys = { + "type", + "actors", + "directors", + "writers", + "genres", + "collections", + "show_title", + "data.plex.rating_key", + "data.imdb.id", + } + for entry in prefetch: + assert entry.filter is not None + keys = {condition.key for condition in entry.filter.must} + assert keys == expected_prefetch_keys + + +def test_query_media_filters_without_vectors(monkeypatch): + with _load_server(monkeypatch) as module: + captured: dict[str, object] = {} + + async def fake_query_points(*args, **kwargs): + captured.update(kwargs) + payload = {"title": "Result", "plex": {"rating_key": "1"}} + return types.SimpleNamespace( + points=[types.SimpleNamespace(payload=payload, score=1.0)] + ) + + monkeypatch.setattr(module.server.qdrant_client, "query_points", fake_query_points) + + result = asyncio.run( + module.query_media.fn( + type="movie", + actors=["Actor"], + limit=1, + ) + ) + + assert result and result[0]["plex"]["rating_key"] == "1" + query_filter = captured["query_filter"] + assert query_filter is not None + keys = {condition.key for condition in query_filter.must} + assert keys == {"type", "actors"} + assert captured["prefetch"] is None def test_openapi_schema_tool_without_params(monkeypatch): diff --git a/uv.lock b/uv.lock index e2b9698..10127a4 100644 --- a/uv.lock +++ b/uv.lock @@ -730,7 +730,7 @@ wheels = [ [[package]] name = "mcp-plex" -version = "1.0.20" +version = "1.0.22" source = { editable = "." } dependencies = [ { name = "fastapi" },