Skip to content

Add Collectives tools#31

Merged
oleksandr-nc merged 4 commits into
mainfrom
feature/p4-collectives-tools
Mar 29, 2026
Merged

Add Collectives tools#31
oleksandr-nc merged 4 commits into
mainfrom
feature/p4-collectives-tools

Conversation

@bigcat88
Copy link
Copy Markdown
Contributor

@bigcat88 bigcat88 commented Mar 29, 2026

Summary

Adds 7 Collectives tools for managing shared knowledge bases and wiki pages via OCS API.

Tools:

Tool Permission Description
list_collectives read List all collectives the user has access to
get_collective_pages read List all pages in a collective
get_collective_page read Get a single page with content
create_collective write Create a new collective with optional emoji
create_collective_page write Create a page under a parent page
delete_collective destructive Permanently delete a collective and all pages
delete_collective_page destructive Permanently delete a page

Also:

  • Added ocs_patch method to client (needed for future restore-from-trash support)
  • Collectives app added to CI configuration
  • 21 integration tests covering CRUD, permissions, subpages, error cases

Tested locally on: NC 33 (stable33) and NC 34 (master) — 21/21 pass on both.

Test plan

  • 21 integration tests pass on NC 33
  • 21 integration tests pass on NC 34
  • CI passes on NC 32 + NC 33

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Add Collectives support: list, view pages, view individual pages, create collectives and pages, trash/restore, and permanent delete.
  • Tests

    • New comprehensive integration tests for Collectives covering creation, listing, pages, trash/restore, deletion, and permission scopes.
  • Chores

    • CI: ensure Nextcloud Collectives app is installed during integration test setup.

…llective_page, create_collective, create_collective_page, delete_collective, delete_collective_page
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 29, 2026

Warning

Rate limit exceeded

@bigcat88 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 15 minutes and 15 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 15 minutes and 15 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3156d97b-c2a7-4adc-a0ce-04580d1605bb

📥 Commits

Reviewing files that changed from the base of the PR and between 61a9168 and fea68ef.

📒 Files selected for processing (1)
  • tests/integration/test_collectives.py
📝 Walkthrough

Walkthrough

Adds Nextcloud Collectives support: new collectives MCP tools module (read/write/delete), registers it on the server, extends NextcloudClient with ocs_patch, adds integration tests, and ensures the Nextcloud test instance installs the Collectives app.

Changes

Cohort / File(s) Summary
CI / Workflow
​.github/workflows/tests-integration.yml
Adds php occ app:install collectives to Nextcloud test setup.
HTTP Client
src/nc_mcp_server/client.py
Adds NextcloudClient.ocs_patch(path, data=None) coroutine to send OCS PATCH requests, validate responses, and return parsed ocs.data.
Server Registration
src/nc_mcp_server/server.py
Imports and registers the collectives tool set in create_server.
Collectives Tools
src/nc_mcp_server/tools/collectives.py
New module registering multiple MCP tools (list/create/get/create-page/trash/restore/delete for collectives and pages) that call OCS endpoints under apps/collectives/api/v1.0, validate inputs, and return JSON-serialized results.
Integration Tests
tests/integration/test_collectives.py
New comprehensive integration test suite with helpers and multiple test classes covering listing, creation, pages, trash/restore, deletion, and permission-scoped behavior.
Test Expectations
tests/integration/test_server.py
Expanded EXPECTED_TOOLS to include the new collectives tool names to validate registration and tool count.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant FastMCP as FastMCP Server
  participant Tool as Collectives Tool
  participant Client as NextcloudClient
  participant NC as Nextcloud OCS

  User->>FastMCP: invoke collectives tool (e.g., create_collective)
  FastMCP->>Tool: dispatch request & params
  Tool->>Client: ocs_post / ocs_patch / ocs_get / ocs_delete (API path, data)
  Client->>NC: HTTP OCS request
  NC-->>Client: OCS JSON response
  Client-->>Tool: parsed ocs.data
  Tool-->>FastMCP: formatted JSON result or confirmation
  FastMCP-->>User: tool response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped to code where collectives bloom,
Paged and patched within the testing room,
OCS replies with tidy threads,
Tools assemble, tests make beds—
A rabbit cheers: "Deploy with room to zoom!" 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add Collectives tools' directly and clearly summarizes the main change—introducing new Collectives tools for managing Nextcloud collectives and pages.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/p4-collectives-tools

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 29, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.80%. Comparing base (ef52177) to head (fea68ef).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #31      +/-   ##
==========================================
+ Coverage   95.48%   95.80%   +0.31%     
==========================================
  Files          21       22       +1     
  Lines        1396     1501     +105     
==========================================
+ Hits         1333     1438     +105     
  Misses         63       63              
Flag Coverage Δ
integration 94.13% <100.00%> (+0.44%) ⬆️
nc32 94.13% <100.00%> (+0.44%) ⬆️
nc33 94.13% <100.00%> (+0.44%) ⬆️
py3.12 12.19% <0.95%> (-0.85%) ⬇️
py3.13 12.19% <0.95%> (-0.85%) ⬇️
py3.14 12.19% <0.95%> (-0.85%) ⬇️
session-cache 28.78% <13.33%> (-1.17%) ⬇️
unit 12.19% <0.95%> (-0.85%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@oleksandr-nc oleksandr-nc marked this pull request as ready for review March 29, 2026 18:15
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/integration/test_collectives.py (1)

23-27: Avoid index-based landing page selection in helper.

Picking pages[0] makes tests brittle to ordering changes. Resolve landing page by explicit predicate (and fail with a clear assertion if missing).

🔍 More robust helper
 async def _get_landing_page_id(nc_mcp: McpTestHelper, collective_id: int) -> int:
     result = await nc_mcp.call("get_collective_pages", collective_id=collective_id)
     pages = json.loads(result)
-    return pages[0]["id"]
+    for page in pages:
+        if page.get("title") == "Landing page":
+            return int(page["id"])
+    raise AssertionError(f"Landing page not found for collective {collective_id}")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/integration/test_collectives.py` around lines 23 - 27, The helper
_get_landing_page_id currently returns pages[0], which is brittle; instead load
the pages from get_collective_pages into the pages list and search for the
landing page by an explicit predicate (e.g., check page.get("is_landing") or
page.get("isLanding") or page.get("type") == "landing" or other project-specific
landing marker) and return that page's "id"; if no page matches, raise/assert
with a clear message indicating no landing page found for the given
collective_id so tests fail fast and clearly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/tests-integration.yml:
- Line 73: The CI step currently swallows failures by appending `|| echo
"collectives already installed"` to the install command; replace that fallback
so the job fails on real errors — remove the `|| echo ...` (or replace it with
an explicit failure like `|| exit 1`) for the `php occ app:install collectives`
invocation (the `$OCC "php occ app:install collectives"` command) so install
errors surface and break the CI run instead of being masked.

In `@src/nc_mcp_server/tools/collectives.py`:
- Line 60: The code calls data.get(...) unconditionally which crashes when data
is a list; update the places that build collectives, pages, and page to guard
the shape first: when constructing collectives use something like source = data
if isinstance(data, list) else data.get("collectives", []) (or source = [] if
not dict/list) and pass items to _format_collective; similarly for pages use
pages_source = data.get("pages", data) only after confirming isinstance(data,
dict) (otherwise treat data as the pages list), and for page read page_value =
data.get("page") only if isinstance(data, dict) else set a sensible default;
replace the three unconditional data.get(...) uses (the collectives list
comprehension, the pages handling, and the page access) with these isinstance
checks so list responses no longer call .get().

---

Nitpick comments:
In `@tests/integration/test_collectives.py`:
- Around line 23-27: The helper _get_landing_page_id currently returns pages[0],
which is brittle; instead load the pages from get_collective_pages into the
pages list and search for the landing page by an explicit predicate (e.g., check
page.get("is_landing") or page.get("isLanding") or page.get("type") == "landing"
or other project-specific landing marker) and return that page's "id"; if no
page matches, raise/assert with a clear message indicating no landing page found
for the given collective_id so tests fail fast and clearly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5d600d8a-62f7-428f-9dfd-ff8f60044f6d

📥 Commits

Reviewing files that changed from the base of the PR and between ef52177 and d86dd6f.

📒 Files selected for processing (6)
  • .github/workflows/tests-integration.yml
  • src/nc_mcp_server/client.py
  • src/nc_mcp_server/server.py
  • src/nc_mcp_server/tools/collectives.py
  • tests/integration/test_collectives.py
  • tests/integration/test_server.py

Comment thread .github/workflows/tests-integration.yml
Comment thread src/nc_mcp_server/tools/collectives.py Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
src/nc_mcp_server/tools/collectives.py (1)

55-56: ⚠️ Potential issue | 🟠 Major

Guard raw-list responses before accessing .get().

These two handlers try to support both wrapped dict payloads and bare list payloads, but the current expression still calls .get() first. If Nextcloud returns a raw list here, both tools fail with AttributeError instead of using the fallback path.

🛠️ Suggested fix
-        collectives = [_format_collective(c) for c in data.get("collectives", data if isinstance(data, list) else [])]
+        raw_collectives = (
+            data.get("collectives", [])
+            if isinstance(data, dict)
+            else data
+            if isinstance(data, list)
+            else []
+        )
+        collectives = [_format_collective(c) for c in raw_collectives]
         return json.dumps(collectives, indent=2, default=str)
...
-        pages = [_format_page(p) for p in data.get("pages", data if isinstance(data, list) else [])]
+        raw_pages = (
+            data.get("pages", [])
+            if isinstance(data, dict)
+            else data
+            if isinstance(data, list)
+            else []
+        )
+        pages = [_format_page(p) for p in raw_pages]
         return json.dumps(pages, indent=2, default=str)

Also applies to: 74-75

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/nc_mcp_server/tools/collectives.py` around lines 55 - 56, The current
expression calls .get() on data which breaks if Nextcloud returns a bare list;
update the handling after the client.ocs_get(...) call to first check the type
of data: if data is a dict, read items = data.get("collectives", []); else if
data is a list, set items = data; otherwise items = []. Then build collectives =
[_format_collective(c) for c in items]. Apply the same defensive check to the
similar block around lines 74-75 that handles other collectives responses.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/nc_mcp_server/tools/collectives.py`:
- Around line 28-39: _format_page currently strips the page body and only
returns metadata, causing get_collective_page() to lose the page content; add a
page-detail serializer (or extend _format_page with an include_body flag) that
preserves the body/content field (e.g., "body" or "content" from the OCS
payload) and use that serializer in get_collective_page() instead of the
metadata-only _format_page; also replace the other single-page return sites (the
code referenced around the second occurrence) to use the detail serializer so
single-page reads include the full content.

In `@tests/integration/test_collectives.py`:
- Around line 138-157: Both tests only assert metadata and miss verifying the
page content, so update test_get_landing_page and test_get_page_includes_tags
(which call _get_landing_page_id and get_collective_page) to assert the returned
page includes the normalized body; specifically add assertions that "body" is
present in page and that page["body"].get("normalized") (or otherwise non-empty
normalized content) is truthy to ensure the API returns the page content, not
just metadata.
- Around line 323-330: The test function
test_write_allows_create_but_blocks_trash should guarantee cleanup even if the
pytest.raises assertion fails: wrap the create + assert block in try/except or
try/finally, set coll = None before calling nc_mcp_write.call, then in a finally
block check if coll is set and call client.ocs_delete for both the normal and
trash endpoints (using coll["id"]) via nc_mcp_write.client; this ensures the
manual ocs_delete cleanup always runs regardless of assertion outcome.

---

Duplicate comments:
In `@src/nc_mcp_server/tools/collectives.py`:
- Around line 55-56: The current expression calls .get() on data which breaks if
Nextcloud returns a bare list; update the handling after the client.ocs_get(...)
call to first check the type of data: if data is a dict, read items =
data.get("collectives", []); else if data is a list, set items = data; otherwise
items = []. Then build collectives = [_format_collective(c) for c in items].
Apply the same defensive check to the similar block around lines 74-75 that
handles other collectives responses.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 191ce5a7-3f3f-4d00-808f-726c42ec2566

📥 Commits

Reviewing files that changed from the base of the PR and between d86dd6f and f587835.

📒 Files selected for processing (3)
  • src/nc_mcp_server/tools/collectives.py
  • tests/integration/test_collectives.py
  • tests/integration/test_server.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/integration/test_server.py

Comment on lines +28 to +39
def _format_page(p: dict[str, Any]) -> dict[str, Any]:
return {
"id": p["id"],
"title": p.get("title", ""),
"emoji": p.get("emoji"),
"timestamp": p.get("timestamp"),
"size": p.get("size"),
"file_name": p.get("fileName"),
"file_path": p.get("filePath"),
"last_user_id": p.get("lastUserId"),
"tags": p.get("tags", []),
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use a page-detail serializer for get_collective_page().

get_collective_page() promises the page content, but _format_page() only keeps metadata. As written, single-page reads always strip the body from the OCS payload.

🛠️ Suggested fix
 def _format_page(p: dict[str, Any]) -> dict[str, Any]:
     return {
         "id": p["id"],
         "title": p.get("title", ""),
         "emoji": p.get("emoji"),
         "timestamp": p.get("timestamp"),
         "size": p.get("size"),
         "file_name": p.get("fileName"),
         "file_path": p.get("filePath"),
         "last_user_id": p.get("lastUserId"),
         "tags": p.get("tags", []),
     }
+
+
+def _format_page_detail(p: dict[str, Any]) -> dict[str, Any]:
+    detail = _format_page(p)
+    for key, value in p.items():
+        if key not in {"id", "title", "emoji", "timestamp", "size", "fileName", "filePath", "lastUserId", "tags"}:
+            detail[key] = value
+    return detail
...
-        page = data.get("page", data)
-        return json.dumps(_format_page(page), indent=2, default=str)
+        page = data.get("page", data)
+        return json.dumps(_format_page_detail(page), indent=2, default=str)

Also applies to: 92-95

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/nc_mcp_server/tools/collectives.py` around lines 28 - 39, _format_page
currently strips the page body and only returns metadata, causing
get_collective_page() to lose the page content; add a page-detail serializer (or
extend _format_page with an include_body flag) that preserves the body/content
field (e.g., "body" or "content" from the OCS payload) and use that serializer
in get_collective_page() instead of the metadata-only _format_page; also replace
the other single-page return sites (the code referenced around the second
occurrence) to use the detail serializer so single-page reads include the full
content.

Comment on lines +138 to +157
async def test_get_landing_page(self, nc_mcp: McpTestHelper) -> None:
coll = await _create_collective(nc_mcp, "getpg")
try:
landing_id = await _get_landing_page_id(nc_mcp, coll["id"])
result = await nc_mcp.call("get_collective_page", collective_id=coll["id"], page_id=landing_id)
page = json.loads(result)
assert page["id"] == landing_id
assert page["title"] == "Landing page"
finally:
await _destroy_collective(nc_mcp, coll["id"])

@pytest.mark.asyncio
async def test_get_page_includes_tags(self, nc_mcp: McpTestHelper) -> None:
coll = await _create_collective(nc_mcp, "tags")
try:
landing_id = await _get_landing_page_id(nc_mcp, coll["id"])
result = await nc_mcp.call("get_collective_page", collective_id=coll["id"], page_id=landing_id)
page = json.loads(result)
assert "tags" in page
assert isinstance(page["tags"], list)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Assert the page body in the single-page tests.

These cases only validate metadata, so get_collective_page() can drop the page content and still pass. Please add an assertion for the normalized body field here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/integration/test_collectives.py` around lines 138 - 157, Both tests
only assert metadata and miss verifying the page content, so update
test_get_landing_page and test_get_page_includes_tags (which call
_get_landing_page_id and get_collective_page) to assert the returned page
includes the normalized body; specifically add assertions that "body" is present
in page and that page["body"].get("normalized") (or otherwise non-empty
normalized content) is truthy to ensure the API returns the page content, not
just metadata.

Comment thread tests/integration/test_collectives.py Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/nc_mcp_server/tools/collectives.py (1)

28-39: ⚠️ Potential issue | 🟠 Major

Preserve full page payload for single-page reads.

get_collective_page() promises content, but Line 95 serializes through _format_page() (Line 28), which drops non-metadata fields (including body/content). This breaks the tool contract for page reads.

💡 Proposed fix
 def _format_page(p: dict[str, Any]) -> dict[str, Any]:
     return {
         "id": p["id"],
         "title": p.get("title", ""),
         "emoji": p.get("emoji"),
         "timestamp": p.get("timestamp"),
         "size": p.get("size"),
         "file_name": p.get("fileName"),
         "file_path": p.get("filePath"),
         "last_user_id": p.get("lastUserId"),
         "tags": p.get("tags", []),
     }

+def _format_page_detail(p: dict[str, Any]) -> dict[str, Any]:
+    detail = _format_page(p)
+    for key, value in p.items():
+        if key not in {"id", "title", "emoji", "timestamp", "size", "fileName", "filePath", "lastUserId", "tags"}:
+            detail[key] = value
+    return detail
...
-        return json.dumps(_format_page(page), indent=2, default=str)
+        return json.dumps(_format_page_detail(page), indent=2, default=str)

Also applies to: 80-95

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/nc_mcp_server/tools/collectives.py` around lines 28 - 39, The
_format_page helper currently strips out non-metadata fields (e.g.,
body/content) causing get_collective_page to return incomplete page payloads;
change the behavior so single-page reads preserve the full page: either adjust
get_collective_page to return the raw page dict before calling _format_page or
modify _format_page to detect a full-page read (presence of content/body field
or a flag) and include all original keys (or at least include body/content,
fileName/filePath and any non-metadata fields) instead of dropping them; update
callers (get_collective_page and any list endpoints) so list responses still use
the trimmed metadata shape while get_collective_page returns the complete page.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/nc_mcp_server/tools/collectives.py`:
- Around line 28-39: The _format_page helper currently strips out non-metadata
fields (e.g., body/content) causing get_collective_page to return incomplete
page payloads; change the behavior so single-page reads preserve the full page:
either adjust get_collective_page to return the raw page dict before calling
_format_page or modify _format_page to detect a full-page read (presence of
content/body field or a flag) and include all original keys (or at least include
body/content, fileName/filePath and any non-metadata fields) instead of dropping
them; update callers (get_collective_page and any list endpoints) so list
responses still use the trimmed metadata shape while get_collective_page returns
the complete page.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f50b65e2-4c86-4540-aee8-32c0d56f8cd6

📥 Commits

Reviewing files that changed from the base of the PR and between f587835 and 61a9168.

📒 Files selected for processing (1)
  • src/nc_mcp_server/tools/collectives.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants