Skip to content

Contacts: read and write postal addresses (vCard ADR) - #13

Merged
rutgerhofste merged 1 commit into
mainfrom
claude/epic-lamport-afixon
Sep 11, 2026
Merged

rutgerhofste merged 1 commit into
mainfrom
claude/epic-lamport-afixon

Conversation

@rutgerhofste

Copy link
Copy Markdown
Member

What

contacts_read returns addresses; contacts_create / contacts_update take it. One address is an object mapping 1:1 onto vCard's seven ADR components:

{ type: "home"|"work", street, extended, po_box, city, region, postal_code, country, preferred }

Same semantics as emails / phones: the list replaces, [] clears, omitted leaves the card alone. Object form only (no flat-string shorthand). contacts_search is unchanged on purpose.

How

  • providers/protocol.py: ContactAddress dataclass, ContactDetail.addresses (default []), addresses kwarg on create_contact / update_contact.
  • providers/soverin/contacts.py: reads ADR from 3.0 and 4.0 cards (TYPE=HOME,PREF / TYPE=home;PREF=1), writes in the dialect the card already speaks, drops a 3.0 LABEL when replacing the ADRs it described. vobject handles ; , \ escaping. Update stays read-modify-write on the whole card, so PHOTO, CATEGORIES, X-*, NOTE survive.
  • ETag: the REPORT already returned getetag; it is now carried through _find_card and sent as If-Match on PUT and DELETE. A 412 becomes a clear "changed on the server, read it again".
  • schemas.py / tools/contacts.py: ContactAddressOut is both the output shape and the input argument, so the MCP schema names every field with a description. An address with nothing filled in is refused.
  • knowledge.py / CLAUDE.md: documented.

Tests

  • tests/test_contacts_addresses.py: 3.0 and 4.0 parsing with two addresses, round-trip without addresses (PUT body equals the parsed card, all unknown properties present), comma + semicolon escaping, [] clears, create, If-Match echoed and 412 surfaced, object-only tool argument.
  • Driven end to end against a local Radicale CardDAV server: update, read back, two addresses with escaping, clear, create, stale If-Match → 412.
  • make lint and make test green (248 passed).

Acceptance still open

scripts/e2e_contact_address.py <uid> runs the Soverin acceptance (writes the Hoogstraat address onto the contact, verifies emails/phones/note unchanged). Needs .env with the Soverin credentials and SQUIRREL_CARDDAV_URL; not available in this session.

Note: the admin package's Graph contacts provider does not accept addresses yet, so bumping SQUIRREL_MCP_REF past this needs that first (issue filed there).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Q9DRvTe1KGW48cAw3CXgvr


Generated by Claude Code

contacts_read returns `addresses`, contacts_create / contacts_update take
it, as a list of objects mapping 1:1 onto the seven ADR components
(po_box;extended;street;city;region;postal_code;country) plus type and
preferred. Same semantics as emails/phones: the list replaces, [] clears,
omitted leaves the card alone. Object form only; contacts_search unchanged.

The CardDAV provider writes the params in the dialect the card already
speaks (TYPE=HOME,PREF on 3.0, TYPE=home;PREF=1 on 4.0), reads both,
leaves every property it does not model in place, and now echoes the
REPORT's ETag as If-Match on PUT and DELETE so an update cannot overwrite
what another client saved in between.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q9DRvTe1KGW48cAw3CXgvr
@rutgerhofste
rutgerhofste marked this pull request as ready for review September 11, 2026 14:48
@rutgerhofste
rutgerhofste merged commit 96390fa into main Sep 11, 2026
3 checks passed
@rutgerhofste
rutgerhofste deleted the claude/epic-lamport-afixon branch September 11, 2026 15:01
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