feat: CardDAV contact tools #2
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "carddav-contacts"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Adds CardDAV contact tools to the existing calendar MCP, completing the second half of the dav-mcp scope.
What's new
Five contact tools — opt-in via
CARDDAV_ADDRESSBOOK_URL:search-contacts(query?, category?, limit?)— substring search across name, email, phone, org, role, notes, address; CATEGORIES filterget-contact(uid)— single-contact lookupcreate-contact({ name, ... })— flat input schema, tool generates the UUIDupdate-contact(uid, ...)— partial update; preserves unmodeled vCard properties (PHOTO, X-* extensions, additional phones/emails) via line-level patchingdelete-contact(uid)Foundation:
src/carddav.ts— tsdav client with env-var fallback (CARDDAV_* → CALDAV_* for shared-server setups like Baikal)src/vcard.ts— hand-rolled vCard 4.0 parse/build/patch helpers (RFC 6350)src/index.ts— registers contact tools only when CARDDAV_ADDRESSBOOK_URL is setDesign choices
upsert-contacttool. Name-matching ambiguity ("which Jacob?") is better resolved in conversation than in the server. The composition pattern is documented in CLAUDE.md:search-contacts→ branch on result count → callcreateorupdate.patchVCard, which rewrites only touched property lines and preserves the rest of the vCard verbatim.import { createDAVClient } from "tsdav". Default-import + destructure works in both tsx and plain Node ESM.Test plan
npm test— 44 tests pass (18 vCard tests + 5 per-tool tests + existing calendar coverage)npm run check— Biome clean🤖 Generated with Claude Code
Update — adversarial review pass
Spawned four parallel reviewers (security, vCard/CardDAV protocol correctness, general correctness, design quality) against this branch and applied the actionable findings in commit
6dc31f4.Critical bugs fixed (data-loss / corruption)
\r—escapeValuewas missing CR. An LLM-supplied string with embedded CRLF could splice in extraBEGIN:VCARD/END:VCARDlines and corrupt the address book.unescapeValuemangled values likeC:\folder\new(treated\\nas escaped LF before resolving\\\\). Rewritten as a single-pass state machine.phonewas deleting allTELlines includingTYPE=WORK/TYPE=HOME, contradicting the README promise. Now uses TYPE-aware drop predicate; same fix foremailandaddress.N— patchingnamewas rewritingN:Smith;Jane;;;toN:Jane Smith;;;;. Nownamepatch only touches FN; existing N is preserved.item1.TELgroup prefix — patches were leaving original grouped TEL alongside new ungrouped TEL → duplicate phones. Group prefix is now stripped on parse.Defensive improvements
foldLinenow counts bytes, not chars, so multibyte names/notes never bisect mid-sequence.Mary Ann Smith→N:Smith;Mary Ann;;;instead of putting the whole name in family slot.CALDAV_*toCARDDAV_*creds is now blocked when the URL hosts differ. Set explicitCARDDAV_USERNAME/PASSWORDif you really mean to send CalDAV creds to a different server.findVCardByUidhelper; throws on >1 hit instead of silently picking one.safeTextschema rejects C0/DEL control chars;.min(1)on name and uid; YYYY-MM-DD regex on birthday.error.messageonly.Design improvements
src/schemas/contact-input.tsso create/update share field definitions.CARDDAV_ADDRESSBOOK_URLis configured server-side;update-contactnudges towardsearch-contactsfirst;search-contactsnotes server-defined ordering.Tests
58 passing (up from 44). New coverage:
findVCardByUidmatch / not-found / ambiguous-match.Deferred (not blockers)