diff --git a/nextcloud_mcp_server/api/visualization.py b/nextcloud_mcp_server/api/visualization.py index 6bca433c..736d9858 100644 --- a/nextcloud_mcp_server/api/visualization.py +++ b/nextcloud_mcp_server/api/visualization.py @@ -247,7 +247,15 @@ async def unified_search(request: Request) -> JSONResponse: accessible_owners=owners, ) ) + # Sort, then cap to a fixed over-fetch budget before the result + # reaches verify-on-read. Without this, N doc_types each fetched + # at search_limit would send N*search_limit candidates into + # verification — one Nextcloud round-trip each — scaling the cost + # with len(doc_types). 2x leaves headroom for verify-on-read + # drops before pagination, matching the nc_semantic_search and + # viz_routes pattern. results.sort(key=lambda r: r.score, reverse=True) + results = results[: search_limit * 2] else: results = await search_algo.search( query=query, diff --git a/nextcloud_mcp_server/search/context.py b/nextcloud_mcp_server/search/context.py index 2bec1105..42eb8813 100644 --- a/nextcloud_mcp_server/search/context.py +++ b/nextcloud_mcp_server/search/context.py @@ -190,7 +190,13 @@ async def _get_deck_metadata_from_qdrant( qdrant_client = await get_qdrant_client() settings = get_settings() - # Query for any chunk of this card (we just need metadata) + # Query for any chunk of this card (we just need metadata). + # Intentionally self-only (raw user_id, not build_ownership_filter): + # deck cards are a documented cross-user gap — the Deck API is per-user, + # so cross-user deck context can't be fetched with the caller's + # credentials anyway (see the doc_type=="file"-only gate in + # get_chunk_with_context). Every other internal Qdrant lookup here is + # ACL-aware; this one is the deliberate exception. scroll_result = await qdrant_client.scroll( collection_name=settings.get_collection_name(), scroll_filter=Filter(