diff --git a/CHANGELOG.md b/CHANGELOG.md index 14e2eab2..72d17275 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to the Nextcloud MCP Server will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [PEP 440](https://peps.python.org/pep-0440/). +## v0.83.4 (2026-05-10) + +### Fix + +- **qdrant**: use get_collection for startup probe (multi-tenant safe, take 2) + +## v0.83.3 (2026-05-10) + +### Fix + +- **qdrant**: use collection_exists for startup probe (multi-tenant safe) + ## v0.83.2 (2026-05-09) ### Fix diff --git a/nextcloud_mcp_server/vector/qdrant_client.py b/nextcloud_mcp_server/vector/qdrant_client.py index b205cf46..2c82bafb 100644 --- a/nextcloud_mcp_server/vector/qdrant_client.py +++ b/nextcloud_mcp_server/vector/qdrant_client.py @@ -559,17 +559,33 @@ async def get_qdrant_client() -> AsyncQdrantClient: expected_dimension = embedding_service.get_dimension() - # Explicitly check if collection exists - logger.debug(f"Checking if collection '{collection_name}' exists...") - collections = await provisional.get_collections() - collection_names = [c.name for c in collections.collections] + # Existence check folded into the get_collection() call. + # + # In managed multi-tenant Qdrant Cloud setups, per-tenant JWTs are + # scoped to a single collection (`access: [{"collection": "...", + # "access": "rw"}]`) and Qdrant denies the cluster-level meta + # endpoints `GET /collections` (used by `get_collections()`) and + # `GET /collections/{name}/exists` (used by `collection_exists()`) + # with 403 Forbidden — by design, since listing or probing + # collections cluster-wide is a tenant-isolation boundary. + # `GET /collections/{name}` (the underlying call for + # `get_collection()`) is the only existence-probe permitted on a + # collection-scoped JWT — it returns 200 with the collection + # detail on hit and 404 on miss. + logger.debug(f"Fetching collection '{collection_name}' details...") + collection_info = None + try: + collection_info = await provisional.get_collection(collection_name) + except UnexpectedResponse as exc: + if exc.status_code != 404: + raise + logger.debug(f"Collection '{collection_name}' not found (404).") - if collection_name in collection_names: + if collection_info is not None: # Collection exists - validate dimensions logger.debug( f"Collection '{collection_name}' found, validating dimensions..." ) - collection_info = await provisional.get_collection(collection_name) # Handle both named vectors (dict) and legacy single vector vectors = collection_info.config.params.vectors if isinstance(vectors, dict): diff --git a/pyproject.toml b/pyproject.toml index 5a8101de..f813b5ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "nextcloud-mcp-server" -version = "0.83.2" +version = "0.83.4" description = "Model Context Protocol (MCP) server for Nextcloud integration - enables AI assistants to interact with Nextcloud data" authors = [ {name = "Chris Coutinho", email = "chris@coutinho.io"} diff --git a/uv.lock b/uv.lock index 9082741f..abcf9bd9 100644 --- a/uv.lock +++ b/uv.lock @@ -2123,7 +2123,7 @@ wheels = [ [[package]] name = "nextcloud-mcp-server" -version = "0.83.2" +version = "0.83.4" source = { editable = "." } dependencies = [ { name = "aiosqlite" },