Addresses all 8 items in the round-4 bot review plus 4 remaining SonarQube OPEN issues that were silently broken by round 3's malformed NOSONAR markers. NOSONAR syntax fix (clears the remaining 4 OPEN SQ issues) ---------------------------------------------------------- Round 3 used ``# NOSONAR S<rule_key>`` form. SonarQube Python doesn't recognize the rule-key suffix — it treats the whole thing as a malformed suppression directive (S7632) AND lets the underlying rule keep firing (S7503 on ``_Cursor.__aenter__/__aexit__``). Switch every marker to bare ``# NOSONAR``, with the rationale moved into a preceding comment block. Affected sites: - storage.py: ``_Cursor.__aenter__``, ``_Cursor.__aexit__`` - config.py: ``get_database_ssl()`` ``return False`` + ``ssl.create_default_context()`` - test_storage_logging.py: ``SENTINEL_PASSWORD_FRAGMENT`` constant - test_storage_postgres.py: three ``bob_pw_v1`` / ``bob_pw_v2`` / ``carol_pw`` literals Bot 🔴#1 — defensive NOSONAR on get_database_ssl `return False` -------------------------------------------------------------- Bot predicted S4830 fires on the operator-opt-out path. SQ output shows it doesn't currently fire, but bare NOSONAR added defensively with rationale comment. Bot 🔴#2 — defensive NOSONAR on f-string SQL -------------------------------------------- ``update_oauth_session`` builds its SET clause via ``f"{', '.join(update_fields)}"``; ``get_audit_logs`` builds its WHERE clause via string concatenation. Both are safe (the fragments only come from this function's own branches, no user input), but the patterns trip taint analysers. Annotated both with bare NOSONAR + safety comment explaining the hardcoded-fragments invariant. Note: S2077 doesn't currently fire on these; defensive. Bot 🟡#3 — pg_advisory_lock for concurrent migrations ----------------------------------------------------- Without coordination, two pods rolling-updating simultaneously can both observe ``has_alembic=False`` and both try to apply migrations from scratch — the second crashes with "relation already exists". New ``_migration_lock()`` async context manager: - On Postgres: ``SELECT pg_advisory_lock(:lock_id)`` on a fresh connection (separate from the engine pool so it survives the ``to_thread.run_sync`` worker), held across BOTH the schema-inspect AND the migration call. Without that span, two pods could each observe "no alembic_version" before either started migrating, defeating the lock. - On SQLite: yields immediately (file-level locking serializes writes natively). Lock ID derived from ``sha256(b"nextcloud-mcp-server:migrations")[:8]`` as a stable signed int64 so we can't collide with other apps sharing the same Postgres. Bot 🟡#4 — RefreshTokenStorage.close() + lifespan wiring -------------------------------------------------------- New idempotent ``close()`` method calls ``await engine.dispose()``, nulls the engine, resets ``_initialized``. Wired into both ``app_lifespan_basic`` (BasicAuth) and the OAuth lifespan teardown, each wrapped in ``try/except Exception`` with ``logger.warning`` so a buggy dispose can't block SIGTERM. Without this, pooled asyncpg connections leak server-side slots until ``idle_in_transaction_session_timeout`` reaps them — with small pool defaults and frequent k8s rolling restarts this can starve ``max_connections``. Bot 🟢#5 — is_sqlite_url docstring on :memory: ---------------------------------------------- Updated docstring to note both file-backed and in-memory forms are recognized; caller is responsible for ``:memory:`` magic. Bot 🟢#6 — db_path via make_url(...).database --------------------------------------------- Replaced ``database_url.split("///", 1)[1]`` hack with SQLAlchemy's own URL parsing. Naturally handles in-memory (``.database is None`` → falls back to ``""``). Same lazy-import pattern as the existing ``mask_db_password`` to avoid module-import-time cost. Bot 🟢#7 — _to_sync_url unrecognized-driver guard ------------------------------------------------- Pulled ``_KNOWN_ASYNC_DRIVERS = ("aiosqlite", "asyncpg")`` into a module constant. When an unrecognized ``+<driver>`` token survives the strip, emits ``logger.warning`` with the known-supported list. Behavior unchanged for valid URLs. Bot 🟢#8 — get_audit_logs SELECT * → explicit columns ----------------------------------------------------- Replaced ``SELECT *`` with explicit column list. Future schema additions stay out of the dict return. New tests --------- - ``test_close_disposes_engine``: pins the public contract — engine nulled, state reset, second call is a no-op. - ``test_concurrent_initialize_serialized_by_advisory_lock``: spawns 3 concurrent inits against a fresh schema; asserts no "relation already exists" and exactly one ``alembic_version`` row at the end. Without the lock, this reliably fails on the second concurrent task. Docs ---- - ADR-026: new "Concurrent migrations across pods" subsection documents the advisory-lock approach + lock-ID derivation. Verification ------------ - ``uv run pytest tests/unit/`` — 1025 passed. - ``TEST_DATABASE_URL=… uv run pytest tests/integration/test_storage_postgres.py -m postgres`` — 9 passed (was 7). - ``ruff check && ruff format --check && ty check`` — clean. Expected post-push: SQ scan reports 0 OPEN issues (was 4). Tracked on Astrolabe Cloud POC board, card #99. --- _This PR was generated with the help of AI, and reviewed by a Human_ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Nextcloud MCP Server
A production-ready MCP server that connects AI assistants to your Nextcloud instance.
Enable Large Language Models like Claude, GPT, and Gemini to interact with your Nextcloud data through a secure API. Create notes, manage calendars, organize contacts, work with files, and more - all through natural language conversations.
This is a dedicated standalone MCP server designed for external MCP clients like Claude Code and IDEs. It runs independently of Nextcloud (Docker, VM, Kubernetes, or local) and provides deep CRUD operations across Nextcloud apps.
Note
Looking for AI features inside Nextcloud? Nextcloud also provides Context Agent, which powers the Assistant app and runs as an ExApp inside Nextcloud. See docs/comparison-context-agent.md for a detailed comparison of use cases.
Tip
Don't want to self-host? Astrolabe Cloud is a managed hosting service for this MCP server, aimed at users and teams who want advanced features like background sync and semantic search without operating the infrastructure themselves. The service is currently under development — sign up on the landing page to join the early-adopter list.
Quick Start
Run the server locally with uvx (no installation required):
NEXTCLOUD_HOST=https://your.nextcloud.instance.com \
NEXTCLOUD_USERNAME=your_username \
NEXTCLOUD_PASSWORD=your_app_password \
uvx nextcloud-mcp-server run --transport stdio
Or add it directly to your MCP client configuration (e.g. claude_desktop_config.json or .claude/settings.json):
{
"mcpServers": {
"nextcloud": {
"command": "uvx",
"args": ["nextcloud-mcp-server", "run", "--transport", "stdio"],
"env": {
"NEXTCLOUD_HOST": "https://your.nextcloud.instance.com",
"NEXTCLOUD_USERNAME": "your_username",
"NEXTCLOUD_PASSWORD": "your_app_password"
}
}
}
}
Tip
Generate an app password in Nextcloud under Settings > Security > Devices & sessions instead of using your login password.
Docker
For full features including semantic search, run with Docker:
docker run -p 127.0.0.1:8000:8000 --rm \
-e NEXTCLOUD_HOST=https://your.nextcloud.instance.com \
-e NEXTCLOUD_USERNAME=your_username \
-e NEXTCLOUD_PASSWORD=your_app_password \
ghcr.io/cbcoutinho/nextcloud-mcp-server:latest
Then connect your MCP client (Claude Desktop, IDEs, mcp dev, etc.) to http://127.0.0.1:8000/mcp.
For Kubernetes, see cbcoutinho/helm-charts. For other deployment options and Compose profiles, see docs/installation.md.
Key Features
- 110+ MCP Tools - Comprehensive API coverage across 10 Nextcloud apps
- MCP Resources - Structured data URIs for browsing Nextcloud data
- Semantic Search (Experimental) - Optional vector-powered search for Notes, Files, News items, and Deck cards (requires Qdrant + Ollama)
- Document Processing - OCR and text extraction from PDFs, DOCX, images with progress notifications
- Flexible Deployment - Docker, Kubernetes (Helm chart), VM, or local installation
- Production-Ready Auth - Basic Auth with app passwords; multi-user via Login Flow v2 — MCP clients authenticate via OAuth, the server handles Nextcloud app passwords transparently
- Tag-Based File Exclusion - Hide sensitive files/folders from MCP file tools by tagging them with a configured Nextcloud system tag (
EXCLUDED_TAGS). See docs/configuration.md - Multiple Transports - streamable-http (default) and stdio
Supported Apps
| App | Tools | Capabilities |
|---|---|---|
| Notes | 7 | Full CRUD, keyword search, semantic search |
| Calendar | 20+ | Events, todos (tasks), recurring events, attendees, availability |
| Contacts | 8 | Full CardDAV support, address books |
| Files (WebDAV) | 12 | Filesystem access, OCR/document processing |
| Deck | 15 | Boards, stacks, cards, labels, assignments |
| Cookbook | 13 | Recipe management, URL import (schema.org) |
| Tables | 5 | Row operations on Nextcloud Tables |
| Sharing | 10+ | Create and manage shares |
| News | 8 | Feeds, folders, items, feed health monitoring |
| Collectives | 16 | Full CRUD on collectives, pages, and tags |
| Talk (spreed) | 6 | List conversations, read/post messages, mark as read, list participants |
| Semantic Search | 2+ | Vector search for Notes, Files, News items, and Deck cards (experimental, opt-in, requires infrastructure) |
Want to see another Nextcloud app supported? Open an issue or contribute a pull request!
Authentication
The MCP server authenticates to Nextcloud using app-specific passwords (Basic Auth). Three deployment modes are supported:
| Mode | Best for |
|---|---|
| Single-User (BasicAuth) | Personal use, development, single-user deployments |
| Multi-User (BasicAuth pass-through) | Multi-user setups where clients send credentials via Authorization header |
| Multi-User (Login Flow v2) | Multi-user / hosted deployments — clients authenticate to the MCP server via OAuth, and the server obtains a per-user app password from Nextcloud and uses it transparently |
OAuth-direct-to-Nextcloud is no longer supported (it required upstream patches to user_oidc that were never merged). Login Flow v2 replaces it for multi-user deployments and works with stock Nextcloud.
See docs/authentication.md for setup instructions.
Semantic Search
An experimental RAG pipeline that lets MCP clients find Nextcloud content by meaning rather than keywords — a query for "car" also surfaces notes about "vehicle" or "transportation". Disabled by default (ENABLE_SEMANTIC_SEARCH=false); requires a vector database and embedding service. See docs/semantic-search-architecture.md and docs/configuration.md.
Tip
Don't want to run Qdrant and an embedding service? Astrolabe Cloud (under development) provides semantic search and background sync as a managed service.
Documentation
- Installation — Docker, Compose profiles, local, VM
- Configuration — Environment variables, document processing, semantic search setup
- Authentication — Basic Auth, Login Flow v2
- Running the Server — Start, manage, troubleshoot
- App Documentation — Per-app guides (Notes, Calendar, Contacts, WebDAV, Deck, Cookbook, Tables)
- Semantic Search Architecture + Vector Sync UI
- Login Flow v2 — recommended multi-user setup (architecture, env vars, scope reference, troubleshooting)
- Troubleshooting · Comparison with Context Agent
Contributing
Contributions are welcome!
- Report bugs or request features: GitHub Issues
- Submit improvements: Pull Requests
- Development guidelines: CLAUDE.md
Security
Found a security issue? Do not open a public GitHub issue. Use GitHub's private vulnerability reporting, or email security@astrolabecloud.com if you can't use GitHub. See SECURITY.md for details.
License
This project is licensed under the AGPL-3.0 License. See LICENSE for details.
