Skip to content

Add Contacts tools with CardDAV support#37

Merged
oleksandr-nc merged 11 commits into
mainfrom
feature/p3-contacts-tools
Apr 7, 2026
Merged

Add Contacts tools with CardDAV support#37
oleksandr-nc merged 11 commits into
mainfrom
feature/p3-contacts-tools

Conversation

@bigcat88
Copy link
Copy Markdown
Contributor

@bigcat88 bigcat88 commented Mar 31, 2026

Summary

Add 6 Contacts tools via CardDAV protocol, completing another Phase 3 item:

Tool Permission Description
list_addressbooks READ List user's address books (excludes system-generated)
get_contacts READ Get contacts from a book with pagination
get_contact READ Get single contact by UID
create_contact WRITE Create contact with name, email, phone, org, title, note
update_contact WRITE Partial update with ETag concurrency control
delete_contact DESTRUCTIVE Delete contact by UID

Uses icalendar library for vCard parsing/generation (already a dependency from Calendar).
Strips PHOTO data from responses to keep context compact.
Parses multi-value fields (EMAIL, TEL, ADR) into typed arrays.

Test plan

  • 36 integration tests covering all 6 tools
  • Permission enforcement (read-only blocks create/delete, write allows create)
  • Unicode names, all-fields creation, ETag conflict detection
  • Server registration tests updated (79 tools total)
  • Lint + pyright clean
  • CI

Summary by CodeRabbit

  • New Features

    • Contacts management via CardDAV: list addressbooks and create/retrieve/update/delete contacts with ETag concurrency and permission enforcement.
  • Tests

    • Added comprehensive integration tests for listing, pagination, creation, updates (structured names, multi-value emails/phones, categories), deletion, and permission scenarios.
  • Chores

    • Updated project progress and type-check config to include Contacts; totals now 79 tools and 622 tests.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 31, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a CardDAV-based Contacts MCP toolset (six tools) with vCard parsing and ETag-aware write operations, registers it in the server, seeds contact test data, updates Pyright and progress docs, and adds comprehensive integration tests and test-server expectations. (29 words)

Changes

Cohort / File(s) Summary
Documentation & Config
PROGRESS.md, pyproject.toml
Marked Contacts complete and updated totals/coverage in PROGRESS.md; added src/nc_mcp_server/tools/contacts.py to Pyright executionEnvironments and relaxed unknown-type reports for that file.
Seed Data Script
scripts/seed_pagination_data.py
Added deterministic contact seeding constants and seed_contacts(...); integrated into main() to PUT .vcf CardDAV resources for test data.
New Contacts Tool
src/nc_mcp_server/tools/contacts.py
New MCP module implementing CardDAV PROPFIND/REPORT templates, path helpers, XML/vCard parsing/serialization, JSON mapping, and six tools: list_addressbooks, get_contacts, get_contact, create_contact, update_contact, delete_contact (includes pagination, ETag handling and conditional PUT/DELETE flows).
Server Registration
src/nc_mcp_server/server.py
Imported and registered the contacts tool via contacts.register(mcp) in server creation.
Integration Tests
tests/integration/test_contacts.py, tests/integration/test_server.py
Added extensive integration tests for listing, pagination, CRUD, structured names, categories, multi-value email/phone, permission scenarios; updated EXPECTED_TOOLS to include contact-related tool names.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant MCP as FastMCP / contacts tool
    participant DAV as Nextcloud CardDAV

    Client->>MCP: create_contact(payload)
    MCP->>DAV: PUT /addressbooks/.../{uid}.vcf (Content-Type: text/vcard)
    DAV-->>MCP: 201 Created + ETag
    MCP->>DAV: REPORT (retrieve stored vCard + etag)
    DAV-->>MCP: vCard + ETag
    MCP-->>Client: contact JSON + etag

    Client->>MCP: get_contacts(book_id, limit, offset)
    MCP->>DAV: REPORT (query contacts)
    DAV-->>MCP: list of hrefs + vCards + etags
    MCP-->>Client: paginated contact list (has_more, items)

    Client->>MCP: update_contact(uid, etag, changes)
    MCP->>DAV: REPORT (fetch current vCard)
    DAV-->>MCP: vCard + current-etag
    MCP->>DAV: PUT updated .vcf with If-Match: etag
    DAV-->>MCP: 204 No Content / new ETag
    MCP-->>Client: updated contact JSON

    Client->>MCP: delete_contact(uid)
    MCP->>DAV: REPORT (locate href)
    DAV-->>MCP: href
    MCP->>DAV: DELETE /addressbooks/.../{uid}.vcf
    DAV-->>MCP: 204 No Content
    MCP-->>Client: deletion confirmation
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐇 I hopped through vCards beneath the moonlit logs,
stitched UIDs and names and chased folded long lines,
ETags clutched close as I PUT and REPORT,
six tools now bouncing in addressbook thickets —
nibble a carrot, tests pass in moonlit shines.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.37% 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 accurately summarizes the main change: adding Contacts tools with CardDAV support, which is the primary feature across all modified files.

✏️ 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/p3-contacts-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 31, 2026

Codecov Report

❌ Patch coverage is 96.76550% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.09%. Comparing base (9e7b1a0) to head (2f4d28e).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/nc_mcp_server/tools/contacts.py 96.75% 12 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #37      +/-   ##
==========================================
+ Coverage   95.97%   96.09%   +0.12%     
==========================================
  Files          24       25       +1     
  Lines        1911     2282     +371     
==========================================
+ Hits         1834     2193     +359     
- Misses         77       89      +12     
Flag Coverage Δ
integration 95.00% <96.76%> (+0.34%) ⬆️
nc32 95.00% <96.76%> (+0.34%) ⬆️
nc33 94.96% <96.76%> (+0.35%) ⬆️
py3.12 8.01% <0.00%> (-1.56%) ⬇️
py3.13 8.01% <0.00%> (-1.56%) ⬇️
py3.14 8.01% <0.00%> (-1.56%) ⬇️
session-cache 23.00% <10.51%> (-2.43%) ⬇️
unit 8.01% <0.00%> (-1.56%) ⬇️
user-permissions 42.68% <16.44%> (-5.10%) ⬇️

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 April 1, 2026 07:20
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: 4

🧹 Nitpick comments (1)
src/nc_mcp_server/tools/contacts.py (1)

348-355: Sort before slicing so offset pagination is stable.

get_contacts paginates in raw REPORT order. If the server changes response order between calls, contacts can jump between pages or be skipped/duplicated. Sorting by a stable key like UID or href before applying offset/limit makes the pagination contract deterministic.

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

In `@src/nc_mcp_server/tools/contacts.py` around lines 348 - 355, The pagination
is unstable because results are sliced in REPORT order; modify the loop in
get_contacts (the block using results = _parse_report_xml(...) and calling
_format_contact) to attach a stable sort key (e.g., set contact["href"] = _href
and preserve contact["uid"] if available from _format_contact) and then sort
all_contacts by that stable key (prefer contact.get("uid") with a fallback to
contact["href"]) before computing page = all_contacts[offset:offset+limit] and
has_more. This ensures deterministic ordering regardless of server response
order.
🤖 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/contacts.py`:
- Around line 471-483: The update_contact function currently uses empty-string
defaults and filters updates by truthiness, preventing clearing fields; change
optional parameters (full_name, given_name, family_name, email, phone,
organization, title, note, book_id) to default to None, build the updates dict
using "is not None" for each parameter (so None means "not provided"), and
update _apply_contact_updates to treat an explicit empty string as a removal
(strip the field and do not reinsert it when the new value is empty); apply the
same None-default + "is not None" pattern to the other similar contact-update
function(s) in this file.
- Around line 202-232: The _build_vcard function is incorrectly using xml_escape
for vCard text causing characters like "&" and newlines to be mangled; replace
xml_escape calls in _build_vcard (for UID, FN, N, EMAIL, TEL, ORG, TITLE, NOTE
and any constructed FN/N fields) with a vCard-safe text escaper/serializer
(escape backslashes, semicolons, commas, and newlines per vCard 3.0 rules and
handle line folding) and reuse the same helper used by _apply_contact_updates
(or add a single helper named e.g. vcard_escape_text and call it from both
_build_vcard and _apply_contact_updates) so all contact fields are serialized
consistently. Ensure UID remains unchanged if provided and that the function
still joins lines with "\r\n" and appends the final "\r\n".
- Around line 284-291: The current partial-name update in the block using
ICal.from_ical and card.get("N") overwrites the entire N component as
"family;given;;;" and thus drops existing additional, prefix, and suffix fields;
fix it by reading old_n.fields.additional, old_n.fields.prefix, and
old_n.fields.suffix (falling back to empty strings if old_n is None or missing
attributes) and construct the new N as "family;given;additional;prefix;suffix"
using xml_escape, so new_lines.insert(insert_before, f"N:{...}") preserves any
existing components when only given_name or family_name are updated (keep using
ICal.from_ical, card.get("N"), new_lines.insert, xml_escape and the updates dict
to locate values).

In `@tests/integration/test_contacts.py`:
- Around line 20-29: The autouse fixture _cleanup_test_contacts currently
depends on the shared nc_mcp test helper (which bootstraps module-level
client/config/permission state) and thus makes permission tests order-dependent;
change the fixture to avoid using nc_mcp and instead perform cleanup via a
direct DAV client or the same isolated fixture under test (e.g., replace the
nc_mcp parameter with a raw dav client fixture or an isolated helper) and call
the remote endpoints directly (use the equivalent of get_contacts and
delete_contact via the DAV client) so you don't mutate the shared singleton;
ensure you still filter by uid.startswith("mcp-") and PREFIX in full_name and
suppress ToolError during delete.

---

Nitpick comments:
In `@src/nc_mcp_server/tools/contacts.py`:
- Around line 348-355: The pagination is unstable because results are sliced in
REPORT order; modify the loop in get_contacts (the block using results =
_parse_report_xml(...) and calling _format_contact) to attach a stable sort key
(e.g., set contact["href"] = _href and preserve contact["uid"] if available from
_format_contact) and then sort all_contacts by that stable key (prefer
contact.get("uid") with a fallback to contact["href"]) before computing page =
all_contacts[offset:offset+limit] and has_more. This ensures deterministic
ordering regardless of server response order.
🪄 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: 17a4ec9d-7cb1-4690-9714-09f65e35a379

📥 Commits

Reviewing files that changed from the base of the PR and between 9e7b1a0 and 6333f60.

📒 Files selected for processing (7)
  • PROGRESS.md
  • pyproject.toml
  • scripts/seed_pagination_data.py
  • src/nc_mcp_server/server.py
  • src/nc_mcp_server/tools/contacts.py
  • tests/integration/test_contacts.py
  • tests/integration/test_server.py

Comment thread src/nc_mcp_server/tools/contacts.py
Comment thread src/nc_mcp_server/tools/contacts.py Outdated
Comment thread src/nc_mcp_server/tools/contacts.py
Comment thread tests/integration/test_contacts.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.

Actionable comments posted: 2

♻️ Duplicate comments (3)
src/nc_mcp_server/tools/contacts.py (2)

480-529: ⚠️ Potential issue | 🟠 Major

update_contact still can't clear a populated field.

This matches the earlier unresolved concern: every optional arg defaults to "", and updates keeps only truthy values. note="", email="", etc. are therefore indistinguishable from “not provided”, so once a field is set there is no tool path to remove it.

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

In `@src/nc_mcp_server/tools/contacts.py` around lines 480 - 529, The
update_contact function uses empty-string defaults and filters falsy values, so
callers cannot clear existing fields; change each optional parameter (full_name,
given_name, family_name, email, phone, organization, title, note, and book_id if
applicable) to default to None and update the construction of updates to include
keys when the value is not None (i.e., differentiate None == not provided vs ""
== explicit clear). Update the function signature and any type hints to accept
Optional[str], and ensure the rest of update_contact (including the updates dict
logic and the ValueError check) treats None as absence and empty string as an
explicit value to send to the backend.

268-304: ⚠️ Potential issue | 🟠 Major

Unfold logical vCard lines before stripping fields.

This still has the earlier line-folding problem in a different form: _strip_updated_fields() works on raw physical lines, so updating a folded NOTE/ORG/ADR removes only the first line and leaves the continuation behind. When the card is reassembled, that orphaned continuation line gets attached to the previous property and corrupts the contact.

Suggested fix
+def _unfold_vcard_lines(vcard_data: str) -> list[str]:
+    lines: list[str] = []
+    for raw_line in vcard_data.splitlines():
+        clean = raw_line.rstrip("\r")
+        if clean[:1] in {" ", "\t"} and lines:
+            lines[-1] += clean[1:]
+        else:
+            lines.append(clean)
+    return lines
+
 def _apply_contact_updates(vcard_data: str, updates: dict[str, Any]) -> str:
     """Apply partial field updates to existing vCard data, return new vCard string."""
     skip_fields = {field for key, field in _UPDATE_FIELD_MAP if key in updates}
     if ("given_name" in updates or "family_name" in updates) and "full_name" not in updates:
         skip_fields.add("FN")
-    new_lines = _strip_updated_fields(vcard_data.strip().split("\n"), skip_fields)
+    new_lines = _strip_updated_fields(_unfold_vcard_lines(vcard_data), skip_fields)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/nc_mcp_server/tools/contacts.py` around lines 268 - 304, The bug is that
folded vCard logical lines (continuations starting with space/tab) are not
unfolded before _strip_updated_fields runs, so only the first physical line is
removed and orphaned continuations remain; to fix, unfold logical lines first
inside _apply_contact_updates (before calling _strip_updated_fields) or update
_strip_updated_fields to accept already-unfolded lines: implement an unfold step
that joins any line that begins with a space or tab to the previous line
(preserving exact whitespace rules and handling CR/LF), then pass the unfolded
lines to _strip_updated_fields (or operate on them in-place), ensuring all
mentions of vCard line splitting use this unfolded representation (reference
functions: _apply_contact_updates, _strip_updated_fields and keep using
_vcard_escape for inserted lines).
tests/integration/test_contacts.py (1)

20-21: ⚠️ Potential issue | 🟠 Major

This autouse cleanup fixture still makes the permission tests order-dependent.

_cleanup_test_contacts() bootstraps nc_mcp before every test, while the permission cases also request nc_mcp_read_only / nc_mcp_write. Because the tools read client/config/permission from module state at call time, whichever fixture initializes last controls the effective permission level, so those assertions are not actually isolated.

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

In `@tests/integration/test_contacts.py` around lines 20 - 21, The autouse fixture
_cleanup_test_contacts is initializing nc_mcp for every test and clobbering
permission-specific fixtures (nc_mcp_read_only / nc_mcp_write), making tests
order-dependent; change the fixture so it does not forcibly create nc_mcp for
every test—either remove autouse=True and require tests that need cleanup to
explicitly use the fixture, or rewrite _cleanup_test_contacts to accept the
pytest request fixture and call request.getfixturevalue to obtain the
appropriate helper (e.g., request.getfixturevalue("nc_mcp_read_only") /
"nc_mcp_write" / "nc_mcp") before performing cleanup so you never instantiate
the wrong nc_mcp variant and preserve isolation for permission tests.
🤖 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/contacts.py`:
- Around line 248-265: The current update mapping (_UPDATE_FIELD_MAP and
_SIMPLE_UPDATE_FIELDS) causes all existing EMAIL/TEL vCard properties to be
removed and replaced with a single entry (e.g., "EMAIL;TYPE=WORK" or
"TEL;TYPE=CELL"); change the update logic so that when updating email or phone
you merge with existing properties instead of stripping them: locate the code
that uses _SIMPLE_UPDATE_FIELDS/_UPDATE_FIELD_MAP to write vCard properties and
modify it to (1) read existing EMAIL/TEL entries into a list, (2) update the
intended entry by matching an identifier (type, value, or a “primary” flag) or
append a new entry if no match, and (3) rewrite all EMAIL/TEL entries preserving
their types/metadata; keep the other simple fields behavior the same and ensure
updates of "email" and "phone" operate on arrays rather than replacing the
entire set.

In `@tests/integration/test_contacts.py`:
- Around line 23-28: The cleanup currently checks PREFIX in
contact.get("full_name", "") which is mutable and causes test-created mcp-*
contacts to be missed; change the condition to rely only on the immutable UID
prefix (e.g., uid.startswith("mcp-")) when deciding to delete contacts. In the
loop that calls nc_mcp.call("get_contacts", ...) and later
nc_mcp.call("delete_contact", ...), remove the PREFIX/full_name check and delete
any contact where contact["uid"].startswith("mcp-") (optionally also verify
BOOK_ID if needed) so cleanup no longer depends on display name mutations.

---

Duplicate comments:
In `@src/nc_mcp_server/tools/contacts.py`:
- Around line 480-529: The update_contact function uses empty-string defaults
and filters falsy values, so callers cannot clear existing fields; change each
optional parameter (full_name, given_name, family_name, email, phone,
organization, title, note, and book_id if applicable) to default to None and
update the construction of updates to include keys when the value is not None
(i.e., differentiate None == not provided vs "" == explicit clear). Update the
function signature and any type hints to accept Optional[str], and ensure the
rest of update_contact (including the updates dict logic and the ValueError
check) treats None as absence and empty string as an explicit value to send to
the backend.
- Around line 268-304: The bug is that folded vCard logical lines (continuations
starting with space/tab) are not unfolded before _strip_updated_fields runs, so
only the first physical line is removed and orphaned continuations remain; to
fix, unfold logical lines first inside _apply_contact_updates (before calling
_strip_updated_fields) or update _strip_updated_fields to accept
already-unfolded lines: implement an unfold step that joins any line that begins
with a space or tab to the previous line (preserving exact whitespace rules and
handling CR/LF), then pass the unfolded lines to _strip_updated_fields (or
operate on them in-place), ensuring all mentions of vCard line splitting use
this unfolded representation (reference functions: _apply_contact_updates,
_strip_updated_fields and keep using _vcard_escape for inserted lines).

In `@tests/integration/test_contacts.py`:
- Around line 20-21: The autouse fixture _cleanup_test_contacts is initializing
nc_mcp for every test and clobbering permission-specific fixtures
(nc_mcp_read_only / nc_mcp_write), making tests order-dependent; change the
fixture so it does not forcibly create nc_mcp for every test—either remove
autouse=True and require tests that need cleanup to explicitly use the fixture,
or rewrite _cleanup_test_contacts to accept the pytest request fixture and call
request.getfixturevalue to obtain the appropriate helper (e.g.,
request.getfixturevalue("nc_mcp_read_only") / "nc_mcp_write" / "nc_mcp") before
performing cleanup so you never instantiate the wrong nc_mcp variant and
preserve isolation for permission tests.
🪄 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: 34c36fb9-6d4e-4939-a9ef-36351a197ae5

📥 Commits

Reviewing files that changed from the base of the PR and between 6333f60 and b55a58d.

📒 Files selected for processing (2)
  • src/nc_mcp_server/tools/contacts.py
  • tests/integration/test_contacts.py

Comment thread src/nc_mcp_server/tools/contacts.py
Comment thread tests/integration/test_contacts.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: 2

♻️ Duplicate comments (2)
tests/integration/test_contacts.py (1)

633-636: ⚠️ Potential issue | 🟠 Major

Drop the extra nc_mcp fixture from this permission test.

Requesting both fixtures mutates the same global server state twice, so this assertion can pass under the wrong permission context. Keeping only nc_mcp_write preserves the isolation you just restored elsewhere.

🧪 Proposed fix
-    async def test_write_allows_create(self, nc_mcp: McpTestHelper, nc_mcp_write: McpTestHelper) -> None:
+    async def test_write_allows_create(self, nc_mcp_write: McpTestHelper) -> None:
         result = await nc_mcp_write.call("create_contact", full_name=f"{PREFIX}-write-ok", book_id=BOOK_ID)
         contact = json.loads(result)
         assert contact["uid"]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/integration/test_contacts.py` around lines 633 - 636, The test
test_write_allows_create currently requests both fixtures nc_mcp and
nc_mcp_write which mutates shared server state; remove the extra fixture by
deleting nc_mcp from the test signature so the function only accepts
nc_mcp_write, and keep the rest of the body (the call to
nc_mcp_write.call("create_contact", ...), JSON decode and assert) unchanged to
ensure the test runs under the intended write-only permission context.
src/nc_mcp_server/tools/contacts.py (1)

248-265: ⚠️ Potential issue | 🟠 Major

Updating email/phone still wipes every other value on the card.

This path strips all existing EMAIL/TEL lines and then writes back exactly one WORK/CELL property. Any secondary addresses/numbers and their type metadata are lost on a partial update.

Also applies to: 295-317

🤖 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/contacts.py`:
- Around line 295-315: The name-write path in _apply_contact_updates is keyed on
truthiness so passing full_name="" or clearing given/family can strip FN or
leave an empty FN; change the logic to treat presence in updates (use "if
'full_name' in updates" and check for keys for given_name/family_name) and
ensure a valid FN is always inserted before returning: after computing
family/given/additional/prefix/suffix (using ICal.from_ical and old_n as today),
synthesize a fallback FN when updates result in an empty full name by deriving
one from the new structured parts (e.g., f"{given} {family}".strip()) or, if
those are empty, fall back to the existing FN value from the parsed vcard;
always insert that FN line (using _vcard_escape) rather than skipping when falsy
so the vCard never ends up with a missing or empty FN; update the same logic in
the other occurrence mentioned (around lines 529-542).
- Around line 45-47: _vcard_escape currently only replaces "\n" so inputs with
Windows CRLF or lone CR leave "\r" characters that can break vCard lines; update
the _vcard_escape function to normalize CRLF and CR to plain "\n" first (e.g.,
normalize "\r\n" and "\r" to "\n") and then perform the existing escaping of
backslashes, semicolons, commas and newlines so the returned string contains no
raw "\r" characters that could corrupt multiline properties like NOTE.

---

Duplicate comments:
In `@tests/integration/test_contacts.py`:
- Around line 633-636: The test test_write_allows_create currently requests both
fixtures nc_mcp and nc_mcp_write which mutates shared server state; remove the
extra fixture by deleting nc_mcp from the test signature so the function only
accepts nc_mcp_write, and keep the rest of the body (the call to
nc_mcp_write.call("create_contact", ...), JSON decode and assert) unchanged to
ensure the test runs under the intended write-only permission context.
🪄 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: 432e5a56-3439-4603-a0dc-caf26290333d

📥 Commits

Reviewing files that changed from the base of the PR and between 52a67a5 and 3f84152.

📒 Files selected for processing (2)
  • src/nc_mcp_server/tools/contacts.py
  • tests/integration/test_contacts.py

Comment thread src/nc_mcp_server/tools/contacts.py Outdated
Comment thread src/nc_mcp_server/tools/contacts.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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/integration/test_contacts.py`:
- Around line 801-802: The test function test_write_allows_create currently
declares two fixtures (nc_mcp and nc_mcp_write) which couples tests via shared
server state; remove the unused nc_mcp fixture from the function signature so
the test only depends on nc_mcp_write, leaving the body calling
nc_mcp_write.call("create_contact", ...). Update the function definition to
accept only nc_mcp_write (preserving its type annotation McpTestHelper) and run
the same assertion to avoid the extra fixture setup.
- Around line 52-54: The cleanup NextcloudClient is not guaranteed to be closed
if _delete_mcp_contacts raises or the task is cancelled; wrap the client usage
in a try/finally so client.close() is always awaited in the finally block.
Locate the client creation (NextcloudClient(_cleanup_config)), the call to
_delete_mcp_contacts(client, _cleanup_config.user), and the await client.close()
and change the flow to create the client, call _delete_mcp_contacts inside try,
and await client.close() inside finally to ensure closure on all failures or
cancellations.
- Around line 33-37: The cleanup loop currently wraps parsing/formatting and
deletion in a single contextlib.suppress(Exception) which lets a failure in
_parse_report_xml or _format_contact (or a missing contact["uid"]) abort the
whole loop; instead, limit the suppression to only the deletion step: call
_parse_report_xml and _format_contact outside the suppress block, validate
contact.get("uid") and compute resource (using BOOK_ID and contact['uid']) and
skip/log the bad contact on failure, then use contextlib.suppress(Exception)
only around the code that performs the remote delete so one bad payload won’t
stop cleaning remaining mcp-* contacts.
🪄 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: ea8e52a1-031e-4109-a1ab-03aa89a18096

📥 Commits

Reviewing files that changed from the base of the PR and between 3f84152 and fa9eb0f.

📒 Files selected for processing (2)
  • src/nc_mcp_server/tools/contacts.py
  • tests/integration/test_contacts.py
✅ Files skipped from review due to trivial changes (1)
  • src/nc_mcp_server/tools/contacts.py

Comment thread tests/integration/test_contacts.py Outdated
Comment thread tests/integration/test_contacts.py Outdated
Comment thread tests/integration/test_contacts.py Outdated
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