feat: add Nextcloud Collectives app support#647
Conversation
Implement MCP tools for the Collectives wiki/documentation app, enabling agentic workflows for team knowledge base management. 16 tools covering collectives, pages, tags, search, and trash: - Read: list collectives, list/get pages (with WebDAV content), search, list tags, list trashed pages - Write: create/update collective, create/move/trash/restore pages, set emoji, create/assign/remove tags Includes Docker hook for app installation, OCS API client with envelope unwrapping, Pydantic models, unit tests (16), and integration tests (10). Closes #621 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Validate OCS envelope status before unwrapping data (raise OCSError on statuscode >= 400) - Fix test data: filePath should be "" for root-level pages, not filename - Catch specific exceptions (HTTPStatusError, OSError) instead of bare Exception in WebDAV content fetch, include error in log message - Return updated resource data from update_collective, move_page, and set_page_emoji instead of discarding API responses - Fix create_page docstring to mention collectivePath/filePath/fileName - Remove unused additional_headers parameter from _get_ocs_headers - Add unit test for OCS error status validation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add destructiveHint=True to collectives_remove_tag (matches "remove" keyword pattern in annotation tests) - Change collectives_update_collective to idempotentHint=False (update operations are non-idempotent per project convention) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bug fixes: - Catch OCSError/HTTPStatusError in all server tools, convert to McpError - Guard update_collective against empty body (raise ValueError) - Use restore_page response data in status message ADR-017 annotation fix: - Distinguish "remove" (reversible association) from "delete" (permanent): remove_tag and deck_remove_label_from_card no longer set destructiveHint - Update annotation test to exclude "remove" from destructive keywords Data model improvements: - Add trashTimestamp field to PageInfo - Create ListTrashedPagesResponse with is_trash context flag - Add collective_id to ListTagsResponse Test robustness: - Read NC credentials from environment variables (not hardcoded) - Filter landing page by parentId == 0 instead of assuming pages[0] Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bugs:
- assign_tag/remove_tag now call _unwrap_ocs to surface OCS-level errors
- trash_page changed to idempotentHint=False (trashing twice errors)
- WebDAV path parts stripped of slashes to prevent double-slash paths
Robustness:
- _unwrap_ocs uses ocs.get("data", {}) instead of ocs["data"]
- Unit test added for missing data key in OCS envelope
Minor:
- MCP error codes use -1 (project convention) instead of HTTP status codes
- update_collective docstring notes that emoji is required
- CollectiveTag.color validated as hex format via field_validator
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ound 4) Add collectives_trash_collective and collectives_delete_collective MCP tools with proper destructiveHint annotations. Refactor integration test fixture to use MCP tools for cleanup instead of direct httpx/OCS calls. Optimize _get_ocs_headers() to class-level constant. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Code Review: feat: add Nextcloud Collectives app supportOverall this is well-structured and follows the project conventions closely. The 16 tools are correctly annotated, scoped, and wired up. A few issues worth addressing before merging. Issues 1. Silent failure in Both methods call 2. The line 3. Docstring mismatch in The Args section says "emoji: New emoji for the collective (required)" but the signature has Minor Issues 4. Unnecessary
5.
6. Unrelated
7. Misleading test name
What's Good
Issues 1 and 2 are the most important as they can cause silent failures or confusing errors in production. The rest are low-priority cleanup items. |
- Validate OCS envelope in trash_collective, delete_collective, trash_page - Guard _unwrap_ocs against non-OCS responses with informative OCSError - Remove _get_ocs_headers() indirection, use class constants directly - Split headers: _OCS_HEADERS (GET) vs _OCS_HEADERS_JSON (with body) - Fix docstring claiming emoji param is required when it is optional - Rename misleading test, add test for non-OCS envelope handling Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Code ReviewThis is a solid, well-structured addition that follows the project's conventions closely. The OCS envelope abstraction, error handling, Pydantic models, tool annotations, and test coverage are all good. A few issues worth addressing: Bug: In Annotation: The current annotation is Robustness:
Minor:
Observation: This is intentional to clear an emoji, but worth a brief comment since null JSON handling varies across APIs. Worth adding an integration test for the clear-emoji case if not already present. Positives
|
- Fix assign_tag sending Content-Type header with no body - Mark collectives_update_collective as idempotent (no ETag involved) - Raise OCSError when 'data' key missing instead of silent fallback - Tighten color validator to 3 or 6 hex chars only - Add comment explaining null emoji semantics in set_page_emoji Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Nextcloud Webhook Testing FindingsDate: 2025-11-11 Executive SummarySuccessfully tested and validated Nextcloud webhook payloads for file/note events and calendar events. 5 out of 6 webhook types were captured and validated against expected schemas from ADR-010 and Nextcloud documentation. One calendar deletion webhook did not fire during testing (potential Nextcloud issue or configuration). Test Environment
Webhooks Registered
Captured Webhook Payloads1. NodeCreatedEvent (File/Note Creation)Test Action: Created note via Notes API Payload: {
"user": {
"uid": "admin",
"displayName": "admin"
},
"time": 1762850245,
"event": {
"class": "OCP\\Files\\Events\\Node\\NodeCreatedEvent",
"node": {
"id": 437,
"path": "/admin/files/Notes/Webhooks/Webhook Test Note.md"
}
}
}Validation:
Observations:
2. NodeWrittenEvent (File/Note Update)Test Action: Updated note content via Notes API Payload: {
"user": {
"uid": "admin",
"displayName": "admin"
},
"time": 1762850960,
"event": {
"class": "OCP\\Files\\Events\\Node\\NodeWrittenEvent",
"node": {
"id": 437,
"path": "/admin/files/Notes/Webhooks/Webhook Test Note.md"
}
}
}Validation:
Observations:
3. NodeDeletedEvent (File/Note Deletion)Test Action: Deleted note via Notes API Payload: {
"user": {
"uid": "admin",
"displayName": "admin"
},
"time": 1762851093,
"event": {
"class": "OCP\\Files\\Events\\Node\\NodeDeletedEvent",
"node": {
"path": "/admin/files/Notes/Webhooks/Webhook Test Note.md"
}
}
}Validation:
Observations:
4. CalendarObjectCreatedEvent (Calendar Event Creation)Test Action: Created calendar event via CalDAV PUT Payload (partial - calendarData omitted for brevity): {
"user": {
"uid": "admin",
"displayName": "admin"
},
"time": 1762851169,
"event": {
"calendarId": 1,
"class": "OCP\\Calendar\\Events\\CalendarObjectCreatedEvent",
"calendarData": {
"id": 1,
"uri": "personal",
"{http://calendarserver.org/ns/}getctag": "...",
"{http://sabredav.org/ns}sync-token": 21,
"{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set": [],
"{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp": [],
"{urn:ietf:params:xml:ns:caldav}calendar-timezone": null
},
"objectData": {
"id": 3,
"uri": "webhook-test-event-001.ics",
"lastmodified": 1762851169,
"etag": "\"2b937b7d77dc83c77329dfdb210ba9d0\"",
"calendarid": 1,
"size": 297,
"component": "vevent",
"classification": 0,
"uid": "webhook-test-event-001@nextcloud",
"calendardata": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\n...",
"{http://nextcloud.com/ns}deleted-at": null
},
"shares": []
}
}Validation:
Observations:
5. CalendarObjectUpdatedEvent (Calendar Event Update)Test Action: Updated calendar event via CalDAV PUT Payload (partial): {
"user": {
"uid": "admin",
"displayName": "admin"
},
"time": 1762851207,
"event": {
"calendarId": 1,
"class": "OCP\\Calendar\\Events\\CalendarObjectUpdatedEvent",
"calendarData": { /* same structure as creation */ },
"objectData": {
"id": 3,
"uri": "webhook-test-event-001.ics",
"lastmodified": 1762851207,
"etag": "\"2695a18013e0991e4212b07b61d5e1e2\"",
"calendarid": 1,
"size": 315,
"component": "vevent",
"classification": 0,
"uid": "webhook-test-event-001@nextcloud",
"calendardata": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\n...",
"{http://nextcloud.com/ns}deleted-at": null
},
"shares": []
}
}Validation:
Observations:
6. CalendarObjectDeletedEvent (Calendar Event Deletion)Test Action: Deleted calendar event via CalDAV DELETE Expected Payload (from Nextcloud docs): {
"user": {
"uid": "admin",
"displayName": "admin"
},
"time": <timestamp>,
"event": {
"calendarId": 1,
"class": "OCP\\Calendar\\Events\\CalendarObjectDeletedEvent",
"calendarData": { /* calendar metadata */ },
"objectData": {
"id": 3,
"uri": "webhook-test-event-001.ics",
/* ... other fields ... */
},
"shares": []
}
}Issue:
Possible Causes:
Recommended Actions:
Schema Comparison: Expected vs ActualFile Events
Type Discrepancy: Calendar Events
All calendar event fields match expected schemas. Key Findings for ADR-010 Implementation1. Deletion Events Have Different Schema
2. Multiple Webhooks Per Operation
3. Event-Specific ID Fields
4. Full State vs Delta
5. Calendar Data Richness
Recommendations for ADR-010 Implementation1. Webhook Event Parser (
|
|
test body for approval check |
|
Code Review - Part 1: Bugs / Correctness 1. Unhandled UnicodeDecodeError in collectives_get_page (server/collectives.py): The except clause catches (HTTPStatusError, OSError) but not UnicodeDecodeError. If a file contains non-UTF-8 bytes, this bubbles up as an unhandled exception rather than gracefully falling back to content = None. Add UnicodeDecodeError to the caught exceptions. 2. Non-standard error code in _handle_collectives_error (server/collectives.py): Using code=-1 in McpError is non-standard. Other tools use standard JSON-RPC codes (e.g. -32603 for INTERNAL_ERROR). Also, OCSError.status_code (403, 404, etc.) is silently dropped - exposing it in the message or mapping to a standard code would help callers diagnose problems. |
|
Code Review - Part 2: Unrelated Changes, Annotation Concern 3. pytest-otel added to dev dependencies (pyproject.toml + uv.lock): This OpenTelemetry pytest exporter appears unrelated to Collectives support and pulls in OTLP/gRPC transitive deps. If intentional, it deserves its own PR. 4. deck.py: destructiveHint=True removed from Remove Label from Deck Card: Correct fix (label removal is reversible), but unannounced in the PR description. Worth a brief mention. 5. collectives_update_collective naming: idempotentHint=True is correct for emoji-only updates. But consider renaming to collectives_set_collective_emoji - the Collectives API only exposes emoji as an updatable field, and 'Update Collective' implies broader scope. |
|
Code Review - Part 3: Test Concerns and Minor Notes 6. Brittle content assertions: assert 'Welcome' in data['content'] in both test_collectives_get_landing_page_content and test_collectives_search depends on the app default landing page template. Could break across Collectives app versions. Consider len(data['content']) > 0 instead. 7. Session-scoped fixture mutated by tests: test_collectives_update_emoji changes the emoji of the shared temporary_collective fixture. Matches patterns elsewhere in the test suite, but makes tests order-dependent. Minor notes:
Overall: architecture and approach are clean. The UnicodeDecodeError gap and unrelated pytest-otel dependency are the most concrete things to fix before merging. |
- Rename collectives_update_collective to collectives_set_collective_emoji (more precise since only emoji is settable) - Use standard JSON-RPC error code -32603 (INTERNAL_ERROR) instead of -1 - Handle UnicodeDecodeError when reading page content via WebDAV - Replace brittle 'Welcome' content assertion with length check Fixes CI: test_update_operations_not_idempotent no longer matches the renamed tool, which is correctly idempotent (no ETag involved). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Review: Nextcloud Collectives SupportGood addition overall — the code follows the project's existing patterns well (Pydantic response models, Bugs / CorrectnessInconsistent error code in # Bug: uses HTTP status code 400 instead of JSON-RPC error code
raise McpError(ErrorData(code=400, message=str(e))) from eThe raise McpError(ErrorData(code=-32603, message=str(e))) from e
Missing FeatureNo Annotation Concern
Minor Issues
WebDAV path construction edge case ( Test Coverage Gaps
Positive Notes
|
- Fix inconsistent error code in set_collective_emoji (400 → -32603) - Allow clearing emoji via set_collective_emoji(emoji=None) - Remove destructiveHint from trash operations (soft deletes are recoverable) - Change delete_collective to idempotentHint=False (requires trash precondition) - Add restore_collective and get_trashed_collectives tools - Add unit tests for ValueError guard, clear-emoji path, and new tools - Add integration test for full trash/restore/delete lifecycle - Verify move_page returns new title in response message Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Code ReviewOverall this is a well-structured addition that follows existing patterns faithfully (BaseNextcloudClient, Pydantic response models, ADR-017 annotations, scopes, instrument_tool). A few issues to address before merging: Bug: clearing a collective emoji silently raises In Calling Compare Accidental dependency: pytest-otel
Minor: The function carries Minor: The type name already conveys that these are trashed pages. The always-True What is working well
The emoji-clearing bug and accidental |
- Fix emoji clearing bug: use _UNSET sentinel in update_collective so
emoji=None sends {"emoji": null} instead of raising ValueError
- Move collectives_get_trashed_collectives to Read Tools section
- Remove redundant is_trash field from ListTrashedPagesResponse
- Add page lifecycle note to collectives_trash_page docstring
- Add unit test for clearing collective emoji via update_collective
- Add integration test for clearing collective emoji via MCP tool
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
PR Review: Nextcloud Collectives Support This is a high-quality, well-structured addition that follows the repository's established patterns. Here are my observations: Strengths
Issues and Questions
Minor Notes
Overall this is close to ready. The main open questions are item 3 (delete_tag gap) and item 4 (ValidationError handling). Great work on the comprehensive test coverage and clean separation of concerns. |
Summary
Tools
Read (8): list collectives, list/get pages (with WebDAV content), search pages, list tags, list trashed pages, list trashed collectives
Write (12): create/set-emoji collective, trash/delete/restore collective, create/move/trash/restore pages, set page emoji, create/assign/remove tags
Changes
app-hooks/post-installation/10-install-collectives-app.sh— Docker hook (enables Circles dep + installs Collectives)nextcloud_mcp_server/client/collectives.py— OCS API client with envelope unwrappingnextcloud_mcp_server/models/collectives.py— Domain + response Pydantic modelsnextcloud_mcp_server/server/collectives.py— 20 MCP tool definitions with annotations and scopesnextcloud_mcp_server/models/auth.py— Addedcollectives:read/collectives:writescopesclient/__init__.py,server/__init__.py,app.pyOther changes
nextcloud_mcp_server/server/deck.py— RemoveddestructiveHint=Truefrom "Remove Label from Deck Card" (reversible operation)tests/server/test_annotations.py— Updated destructive keyword list to exclude "remove"; addedcollectives_delete_collectiveexception for idempotency checkpyproject.toml— Addedpytest-otel>=2.0.1dev dependency (intentional: enables OpenTelemetry trace export from test runs for observability infrastructure)Test plan
tests/client/collectives/test_collectives_api.pytests/server/test_collectives_mcp.pyCloses #621
This PR was generated with the help of AI, and reviewed by a Human
🤖 Generated with Claude Code