Skip to content

Add Unified Search tools with cross-app search support#43

Merged
oleksandr-nc merged 2 commits intomainfrom
feature/p5-unified-search
Apr 12, 2026
Merged

Add Unified Search tools with cross-app search support#43
oleksandr-nc merged 2 commits intomainfrom
feature/p5-unified-search

Conversation

@bigcat88
Copy link
Copy Markdown
Contributor

@bigcat88 bigcat88 commented Apr 12, 2026

Summary

  • Add 2 Unified Search tools: list_search_providers and unified_search
  • Searches across all Nextcloud apps (files, calendar, contacts, talk, mail, notes, etc.) via the built-in Unified Search OCS API
  • 18 search providers available out of the box
  • Cursor-based pagination support
  • Provider-specific filters (date range, person, mime type, etc.) via JSON parameter

Tools

Tool Permission Description
list_search_providers read List available search providers with their supported filters
unified_search read Search any provider with term, pagination, and filters

Test plan

  • 17 integration tests against live Nextcloud
  • Provider listing: returns providers, checks fields/filters
  • File search: create file, search, verify match
  • Pagination: cursor-based page navigation with no overlaps
  • Multiple providers: contacts, talk-conversations
  • Filters: mime type filter for files
  • Error handling: nonexistent provider
  • Permissions: read-only access works
  • ruff + pyright clean

Summary by CodeRabbit

  • New Features

    • Unified Search: query across multiple providers with filtering, cursor-based pagination, and result formatting.
    • Provider discovery: list available search providers.
  • Tests

    • Added integration tests validating provider listing, searches, pagination, filtering, permissions, and multiple providers against live Nextcloud instances.
  • Documentation

    • Updated progress tracker: search tools marked completed and Phase 5 set to In Progress; test coverage totals updated.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 12, 2026

📝 Walkthrough
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.39% 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 Unified Search tools with cross-app search support' directly and clearly summarizes the main change: adding two new search tools with cross-app functionality.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/p5-unified-search

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 Apr 12, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.22%. Comparing base (ac3d3a2) to head (42673fe).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #43      +/-   ##
==========================================
+ Coverage   96.16%   96.22%   +0.06%     
==========================================
  Files          26       27       +1     
  Lines        2658     2701      +43     
==========================================
+ Hits         2556     2599      +43     
  Misses        102      102              
Flag Coverage Δ
integration 95.29% <100.00%> (+0.07%) ⬆️
nc32 95.29% <100.00%> (+0.07%) ⬆️
nc33 95.26% <100.00%> (+0.07%) ⬆️
py3.12 6.81% <0.00%> (-0.12%) ⬇️
py3.13 6.81% <0.00%> (-0.12%) ⬇️
py3.14 6.81% <0.00%> (-0.12%) ⬇️
session-cache 21.73% <23.25%> (+0.02%) ⬆️
unit 6.81% <0.00%> (-0.12%) ⬇️
user-permissions 39.72% <39.53%> (-0.01%) ⬇️

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.

@bigcat88 bigcat88 marked this pull request as ready for review April 12, 2026 13:08
@bigcat88
Copy link
Copy Markdown
Contributor Author

(AI) Ready for review. All CI green. Two read-only Unified Search tools that search across all 18 Nextcloud app providers with cursor pagination and provider-specific filters.

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 (2)
tests/integration/test_search.py (2)

96-99: test_search_with_limit does not strongly validate limit enforcement.

Line 99 passes even when results are 0/1, so truncation behavior is not proven. Seed >2 matching fixtures and assert exactly 2 results.

Suggested test tightening
     async def test_search_with_limit(self, nc_mcp: McpTestHelper) -> None:
+        await nc_mcp.create_test_dir()
+        for i in range(3):
+            await nc_mcp.upload_test_file(f"mcp-test-suite/pagtest-limit-{i}.txt", "pagtest-limit")
-        result = await nc_mcp.call("unified_search", provider="files", term="pagtest", limit=2)
+        result = await nc_mcp.call("unified_search", provider="files", term="pagtest-limit", limit=2)
         data = json.loads(result)
-        assert len(data["entries"]) <= 2
+        assert len(data["entries"]) == 2
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/integration/test_search.py` around lines 96 - 99,
test_search_with_limit doesn't prove truncation because it allows 0/1 results;
update the test_search_with_limit to ensure there are more than two matching
fixtures before calling nc_mcp.call("unified_search", provider="files",
term="pagtest", limit=2) (e.g., create or seed at least 3 entries that match
"pagtest" using the existing test helper), then assert that
json.loads(result)["entries"] has length exactly 2 to validate the limit is
enforced.

122-130: Add negative-path filter tests.

Current coverage only validates valid JSON filters. Add cases for invalid JSON and reserved keys (limit, term, cursor) so request-shape constraints are regression-proof.

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

In `@tests/integration/test_search.py` around lines 122 - 130, Add negative-path
unit tests for the unified_search call to ensure filter validation: extend or
add tests alongside test_search_with_filter that call
nc_mcp.call("unified_search", ...) with filters set to invalid JSON (e.g.,
malformed string) and with filters containing reserved keys "limit", "term", and
"cursor"; assert that the response indicates a validation error (HTTP error code
or error payload consistent with the service) rather than success, and ensure
the tests reference the same test harness (test_search_with_filter, nc_mcp.call)
so they run in the same suite and protect against regressions in request-shape
handling.
🤖 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/search.py`:
- Around line 103-105: The current code directly does params.update(extra) after
parsing filters, allowing callers to override reserved pagination/query keys
(like "limit", "term", "cursor") and bypass the earlier clamping logic; change
the update to either whitelist allowed filter keys or strip reserved keys from
extra before merging (i.e., remove "limit", "term", "cursor" from extra or only
merge keys not in that reserved set) so that the existing clamp/pagination
enforcement on params remains in effect; update the logic around the filters
parsing in search.py where `extra = json.loads(filters)` and
`params.update(extra)` is used.
- Line 108: The code at the call to client.ocs_get is interpolating raw provider
into the path (SEARCH_API/{provider}/search); sanitize by URL-encoding the
provider before building the endpoint so characters like "/" cannot alter the
path. Use a standard encoder (e.g., urllib.parse.quote with an appropriate safe
parameter) to create provider_encoded and replace
f"{SEARCH_API}/{provider}/search" with
f"{SEARCH_API}/{provider_encoded}/search"; add the necessary import for the
encoder and ensure any tests or callers still pass encoded values where needed.

---

Nitpick comments:
In `@tests/integration/test_search.py`:
- Around line 96-99: test_search_with_limit doesn't prove truncation because it
allows 0/1 results; update the test_search_with_limit to ensure there are more
than two matching fixtures before calling nc_mcp.call("unified_search",
provider="files", term="pagtest", limit=2) (e.g., create or seed at least 3
entries that match "pagtest" using the existing test helper), then assert that
json.loads(result)["entries"] has length exactly 2 to validate the limit is
enforced.
- Around line 122-130: Add negative-path unit tests for the unified_search call
to ensure filter validation: extend or add tests alongside
test_search_with_filter that call nc_mcp.call("unified_search", ...) with
filters set to invalid JSON (e.g., malformed string) and with filters containing
reserved keys "limit", "term", and "cursor"; assert that the response indicates
a validation error (HTTP error code or error payload consistent with the
service) rather than success, and ensure the tests reference the same test
harness (test_search_with_filter, nc_mcp.call) so they run in the same suite and
protect against regressions in request-shape handling.
🪄 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: b88448b5-5326-4c1f-bbe6-bc5f7ddd5d27

📥 Commits

Reviewing files that changed from the base of the PR and between ac3d3a2 and 7e20984.

📒 Files selected for processing (5)
  • PROGRESS.md
  • src/nc_mcp_server/server.py
  • src/nc_mcp_server/tools/search.py
  • tests/integration/test_search.py
  • tests/integration/test_server.py

Comment thread src/nc_mcp_server/tools/search.py
Comment thread src/nc_mcp_server/tools/search.py
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 (2)
src/nc_mcp_server/tools/search.py (2)

103-107: ⚠️ Potential issue | 🟠 Major

Validate filters is a JSON object before merging.

At Line 104, valid JSON that is not an object (e.g., [], 1, "x") can make Line 105/Line 107 fail with runtime errors instead of a clean user-facing validation error.

Suggested fix
@@
         if filters:
-            extra = json.loads(filters)
+            try:
+                extra = json.loads(filters)
+            except json.JSONDecodeError as exc:
+                raise ValueError("filters must be valid JSON") from exc
+            if not isinstance(extra, dict):
+                raise ValueError("filters must be a JSON object")
             for key in ("term", "limit", "cursor"):
                 extra.pop(key, None)
             params.update(extra)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/nc_mcp_server/tools/search.py` around lines 103 - 107, The code parses
filters into `extra` and assumes it's a JSON object; if `filters` is valid JSON
but not an object (e.g., list, number, string) the subsequent `extra.pop(...)`
and `params.update(extra)` will raise runtime errors — change the logic in the
block that handles `filters` (the `filters` variable, `extra` and `params`) to
validate that `json.loads(filters)` returns a dict before mutating it; if it's
not a dict, return or raise a clear validation error (or convert/ignore
non-object types per API contract) so only objects are merged into `params`.

110-110: ⚠️ Potential issue | 🟠 Major

URL-encode provider before composing the endpoint path.

Line 110 currently interpolates raw user input into a path segment. Encode it first so / and path tokens cannot alter endpoint shape.

Suggested fix
@@
 import json
 from typing import Any
+from urllib.parse import quote
@@
-        data = await client.ocs_get(f"{SEARCH_API}/{provider}/search", params=params)
+        provider_path = quote(provider, safe="")
+        data = await client.ocs_get(f"{SEARCH_API}/{provider_path}/search", params=params)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/nc_mcp_server/tools/search.py` at line 110, The code interpolates raw
provider into the path (data = await
client.ocs_get(f"{SEARCH_API}/{provider}/search", ...)), so URL-encode provider
first to prevent path injection; modify the call to build the URL with an
encoded_provider produced via urllib.parse.quote(provider, safe='') (or
equivalent), update the f-string to use encoded_provider, and add the necessary
import of quote from urllib.parse so ocs_get receives a safe, encoded path
segment.
🤖 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/search.py`:
- Around line 103-107: The code parses filters into `extra` and assumes it's a
JSON object; if `filters` is valid JSON but not an object (e.g., list, number,
string) the subsequent `extra.pop(...)` and `params.update(extra)` will raise
runtime errors — change the logic in the block that handles `filters` (the
`filters` variable, `extra` and `params`) to validate that `json.loads(filters)`
returns a dict before mutating it; if it's not a dict, return or raise a clear
validation error (or convert/ignore non-object types per API contract) so only
objects are merged into `params`.
- Line 110: The code interpolates raw provider into the path (data = await
client.ocs_get(f"{SEARCH_API}/{provider}/search", ...)), so URL-encode provider
first to prevent path injection; modify the call to build the URL with an
encoded_provider produced via urllib.parse.quote(provider, safe='') (or
equivalent), update the f-string to use encoded_provider, and add the necessary
import of quote from urllib.parse so ocs_get receives a safe, encoded path
segment.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9bfdafe7-d8a6-405d-bfd2-57b1f9e36bad

📥 Commits

Reviewing files that changed from the base of the PR and between 7e20984 and 42673fe.

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

@oleksandr-nc oleksandr-nc merged commit b0287c4 into main Apr 12, 2026
12 checks passed
@oleksandr-nc oleksandr-nc deleted the feature/p5-unified-search branch April 12, 2026 13:45
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