From f2d4982b2ff8327bd5e6d9eb36944439a02aa1be Mon Sep 17 00:00:00 2001 From: Chris Coutinho Date: Sun, 10 May 2026 14:03:48 +0200 Subject: [PATCH] fix(qdrant): use collection_exists for startup probe (multi-tenant safe) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The startup path in `get_qdrant_client()` calls `get_collections()` to check whether the configured collection already exists. That's a cluster-wide list operation; in managed multi-tenant Qdrant Cloud deployments where each tenant's JWT is scoped to a single collection (by design — `access: [{"collection": "tenant_", "access": "rw"}]`), the call returns `403 Forbidden` and the FastAPI lifespan crashes: qdrant_client.http.exceptions.UnexpectedResponse: 403 (Forbidden) raw response: {"error":"forbidden"} RuntimeError: Cannot start vector sync - Qdrant initialization failed Switching to `collection_exists(collection_name)` (per-collection HEAD-style probe) only requires access to the named collection, which the tenant JWT has. Single-tenant deployments using an admin/master key are unaffected — they had access to both forms; this picks the narrower one. Doesn't change creation semantics: when the collection isn't present the code path still calls `create_collection`. In a managed setup where the collection is pre-provisioned by an external admin (e.g., the Astrolabe Cloud control plane's create-tenant workflow), that branch never fires for an existing tenant; cold-start tenants get their collection created by the workflow before the Pod boots. Co-Authored-By: Claude Opus 4.7 (1M context) --- nextcloud_mcp_server/vector/qdrant_client.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/nextcloud_mcp_server/vector/qdrant_client.py b/nextcloud_mcp_server/vector/qdrant_client.py index b2aa0e65..77ccf26a 100644 --- a/nextcloud_mcp_server/vector/qdrant_client.py +++ b/nextcloud_mcp_server/vector/qdrant_client.py @@ -71,12 +71,21 @@ async def get_qdrant_client() -> AsyncQdrantClient: expected_dimension = embedding_service.get_dimension() - # Explicitly check if collection exists + # Explicitly check if collection exists. + # + # Use `collection_exists(name)` (per-collection HEAD-style probe) + # rather than `get_collections()` (cluster-wide list). In managed + # multi-tenant Qdrant Cloud setups, per-tenant JWTs are scoped + # to a single collection and `get_collections()` returns 403 + # Forbidden by design — listing other tenants' collections would + # be a security regression. `collection_exists` only requires + # access to the named collection, which the tenant JWT has. logger.debug(f"Checking if collection '{collection_name}' exists...") - collections = await _qdrant_client.get_collections() - collection_names = [c.name for c in collections.collections] + collection_present = await _qdrant_client.collection_exists( + collection_name=collection_name + ) - if collection_name in collection_names: + if collection_present: # Collection exists - validate dimensions logger.debug( f"Collection '{collection_name}' found, validating dimensions..."