dav-mcp 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/LICENSE +21 -0
  3. package/README.md +260 -0
  4. package/package.json +80 -0
  5. package/src/error-handler.js +215 -0
  6. package/src/formatters.js +754 -0
  7. package/src/logger.js +144 -0
  8. package/src/server-http.js +402 -0
  9. package/src/server-stdio.js +225 -0
  10. package/src/tool-call-logger.js +148 -0
  11. package/src/tools/calendar/calendar-multi-get.js +38 -0
  12. package/src/tools/calendar/calendar-query.js +98 -0
  13. package/src/tools/calendar/create-event.js +79 -0
  14. package/src/tools/calendar/delete-calendar.js +36 -0
  15. package/src/tools/calendar/delete-event.js +38 -0
  16. package/src/tools/calendar/index.js +16 -0
  17. package/src/tools/calendar/list-calendars.js +21 -0
  18. package/src/tools/calendar/list-events.js +43 -0
  19. package/src/tools/calendar/make-calendar.js +80 -0
  20. package/src/tools/calendar/update-calendar.js +106 -0
  21. package/src/tools/calendar/update-event-fields.js +119 -0
  22. package/src/tools/calendar/update-event-raw.js +45 -0
  23. package/src/tools/contacts/addressbook-multi-get.js +38 -0
  24. package/src/tools/contacts/addressbook-query.js +85 -0
  25. package/src/tools/contacts/create-contact.js +84 -0
  26. package/src/tools/contacts/delete-contact.js +38 -0
  27. package/src/tools/contacts/index.js +13 -0
  28. package/src/tools/contacts/list-addressbooks.js +21 -0
  29. package/src/tools/contacts/list-contacts.js +32 -0
  30. package/src/tools/contacts/update-contact-fields.js +135 -0
  31. package/src/tools/contacts/update-contact-raw.js +45 -0
  32. package/src/tools/index.js +57 -0
  33. package/src/tools/shared/helpers.js +132 -0
  34. package/src/tools/todos/create-todo.js +101 -0
  35. package/src/tools/todos/delete-todo.js +38 -0
  36. package/src/tools/todos/index.js +12 -0
  37. package/src/tools/todos/list-todos.js +30 -0
  38. package/src/tools/todos/todo-multi-get.js +37 -0
  39. package/src/tools/todos/todo-query.js +112 -0
  40. package/src/tools/todos/update-todo-fields.js +119 -0
  41. package/src/tools/todos/update-todo-raw.js +46 -0
  42. package/src/tsdav-client.js +199 -0
  43. package/src/utils/tool-helpers.js +388 -0
  44. package/src/validation.js +245 -0
@@ -0,0 +1,21 @@
1
+ import { tsdavManager } from '../../tsdav-client.js';
2
+ import { formatCalendarList } from '../../formatters.js';
3
+
4
+ /**
5
+ * List all available calendars from the CalDAV server
6
+ */
7
+ export const listCalendars = {
8
+ name: 'list_calendars',
9
+ description: 'List all available calendars from the CalDAV server. Use this to get calendar URLs needed for other operations',
10
+ inputSchema: {
11
+ type: 'object',
12
+ properties: {},
13
+ required: [],
14
+ },
15
+ handler: async () => {
16
+ const client = tsdavManager.getCalDavClient();
17
+ const calendars = await client.fetchCalendars();
18
+
19
+ return formatCalendarList(calendars);
20
+ },
21
+ };
@@ -0,0 +1,43 @@
1
+ import { tsdavManager } from '../../tsdav-client.js';
2
+ import { validateInput, listEventsSchema } from '../../validation.js';
3
+ import { formatEventList } from '../../formatters.js';
4
+ import { findCalendarOrThrow, buildTimeRangeOptions } from '../shared/helpers.js';
5
+
6
+ /**
7
+ * List ALL events from a single calendar without filtering
8
+ */
9
+ export const listEvents = {
10
+ name: 'list_events',
11
+ description: 'List ALL events from a single calendar without filtering. WARNING: Returns all events which can be many thousands - use calendar_query instead for searching with filters (supports multi-calendar search).',
12
+ inputSchema: {
13
+ type: 'object',
14
+ properties: {
15
+ calendar_url: {
16
+ type: 'string',
17
+ description: 'The URL of the calendar to fetch events from. Use list_calendars first to get available URLs.',
18
+ },
19
+ time_range_start: {
20
+ type: 'string',
21
+ description: 'Optional: Start date in ISO 8601 format (e.g., 2025-01-01T00:00:00.000Z)',
22
+ },
23
+ time_range_end: {
24
+ type: 'string',
25
+ description: 'Optional: End date in ISO 8601 format',
26
+ },
27
+ },
28
+ required: ['calendar_url'],
29
+ },
30
+ handler: async (args) => {
31
+ const validated = validateInput(listEventsSchema, args);
32
+ const client = tsdavManager.getCalDavClient();
33
+ const calendars = await client.fetchCalendars();
34
+ const calendar = findCalendarOrThrow(calendars, validated.calendar_url);
35
+
36
+ const timeRangeOptions = buildTimeRangeOptions(validated.time_range_start, validated.time_range_end);
37
+ const options = { calendar, ...timeRangeOptions };
38
+
39
+ const events = await client.fetchCalendarObjects(options);
40
+
41
+ return formatEventList(events, calendar);
42
+ },
43
+ };
@@ -0,0 +1,80 @@
1
+ import { tsdavManager } from '../../tsdav-client.js';
2
+ import { validateInput, makeCalendarSchema } from '../../validation.js';
3
+ import { formatSuccess } from '../../formatters.js';
4
+ import { getCalendarHome, sanitizeNameForUrl } from '../shared/helpers.js';
5
+
6
+ /**
7
+ * Create a new calendar collection
8
+ */
9
+ export const makeCalendar = {
10
+ name: 'make_calendar',
11
+ description: 'Create a new calendar collection on the CalDAV server with optional color, description, timezone, and component types',
12
+ inputSchema: {
13
+ type: 'object',
14
+ properties: {
15
+ display_name: {
16
+ type: 'string',
17
+ description: 'Display name for the new calendar',
18
+ },
19
+ description: {
20
+ type: 'string',
21
+ description: 'Optional: Calendar description',
22
+ },
23
+ color: {
24
+ type: 'string',
25
+ description: 'Optional: Calendar color in hex format (e.g., #FF5733)',
26
+ },
27
+ timezone: {
28
+ type: 'string',
29
+ description: 'Optional: Timezone ID (e.g., Europe/Berlin)',
30
+ },
31
+ components: {
32
+ type: 'array',
33
+ items: {
34
+ type: 'string',
35
+ enum: ['VEVENT', 'VTODO', 'VJOURNAL']
36
+ },
37
+ description: 'Optional: Supported component types. Default: ["VEVENT", "VTODO"]. Use ["VEVENT"] for events only, ["VTODO"] for tasks only.',
38
+ },
39
+ },
40
+ required: ['display_name'],
41
+ },
42
+ handler: async (args) => {
43
+ const validated = validateInput(makeCalendarSchema, args);
44
+ const client = tsdavManager.getCalDavClient();
45
+
46
+ // Get calendar home URL
47
+ const calendarHome = await getCalendarHome(client);
48
+
49
+ // Generate new calendar URL with sanitized name
50
+ const sanitizedName = sanitizeNameForUrl(validated.display_name);
51
+ const newCalendarUrl = `${calendarHome}${sanitizedName}/`;
52
+
53
+ // Prepare calendar props
54
+ const calendarProps = {
55
+ displayName: validated.display_name,
56
+ description: validated.description,
57
+ calendarColor: validated.color,
58
+ timezone: validated.timezone,
59
+ };
60
+
61
+ // Add supported component set if specified
62
+ // NOTE: Radicale ignores this property (known limitation), but works with Nextcloud/Baikal
63
+ // Format: supportedCalendarComponentSet.comp[{_attributes: {name: 'VEVENT'}}]
64
+ if (validated.components && validated.components.length > 0) {
65
+ calendarProps.supportedCalendarComponentSet = {
66
+ comp: validated.components.map(comp => ({ _attributes: { name: comp } }))
67
+ };
68
+ }
69
+
70
+ const calendar = await client.makeCalendar({
71
+ url: newCalendarUrl,
72
+ props: calendarProps
73
+ });
74
+
75
+ return formatSuccess('Calendar created successfully', {
76
+ displayName: validated.display_name,
77
+ url: newCalendarUrl,
78
+ });
79
+ },
80
+ };
@@ -0,0 +1,106 @@
1
+ import { tsdavManager } from '../../tsdav-client.js';
2
+ import { validateInput, updateCalendarSchema } from '../../validation.js';
3
+ import { formatCalendarUpdateSuccess } from '../../formatters.js';
4
+
5
+ /**
6
+ * Update an existing calendar's properties
7
+ */
8
+ export const updateCalendar = {
9
+ name: 'update_calendar',
10
+ description: 'Update an existing calendar\'s properties (display name, description, color, timezone). Use this when user asks to "rename calendar", "change calendar color", or "update calendar properties"',
11
+ inputSchema: {
12
+ type: 'object',
13
+ properties: {
14
+ calendar_url: {
15
+ type: 'string',
16
+ description: 'The URL of the calendar to update (get from list_calendars)',
17
+ },
18
+ display_name: {
19
+ type: 'string',
20
+ description: 'Optional: New display name for the calendar',
21
+ },
22
+ description: {
23
+ type: 'string',
24
+ description: 'Optional: New description for the calendar',
25
+ },
26
+ color: {
27
+ type: 'string',
28
+ description: 'Optional: New calendar color in hex format (e.g., #FF5733)',
29
+ },
30
+ timezone: {
31
+ type: 'string',
32
+ description: 'Optional: New timezone ID (e.g., Europe/Berlin)',
33
+ },
34
+ },
35
+ required: ['calendar_url'],
36
+ },
37
+ handler: async (args) => {
38
+ const validated = validateInput(updateCalendarSchema, args);
39
+ const client = tsdavManager.getCalDavClient();
40
+
41
+ // Build WebDAV PROPPATCH XML
42
+ let proppatchXml = '<?xml version="1.0" encoding="UTF-8"?>\n';
43
+ proppatchXml += '<d:propertyupdate xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:x="http://apple.com/ns/ical/">\n';
44
+ proppatchXml += ' <d:set>\n';
45
+ proppatchXml += ' <d:prop>\n';
46
+
47
+ if (validated.display_name) {
48
+ proppatchXml += ` <d:displayname>${validated.display_name}</d:displayname>\n`;
49
+ }
50
+ if (validated.description) {
51
+ proppatchXml += ` <c:calendar-description>${validated.description}</c:calendar-description>\n`;
52
+ }
53
+ if (validated.color) {
54
+ proppatchXml += ` <x:calendar-color>${validated.color}</x:calendar-color>\n`;
55
+ }
56
+ if (validated.timezone) {
57
+ // Validate timezone format (basic check)
58
+ if (!validated.timezone.includes('/')) {
59
+ throw new Error(`Invalid timezone format: ${validated.timezone}. Expected format: "Europe/Berlin", "America/New_York", etc.`);
60
+ }
61
+ proppatchXml += ` <c:calendar-timezone>${validated.timezone}</c:calendar-timezone>\n`;
62
+ }
63
+
64
+ proppatchXml += ' </d:prop>\n';
65
+ proppatchXml += ' </d:set>\n';
66
+ proppatchXml += '</d:propertyupdate>';
67
+
68
+ // Use raw fetch with HTTP PROPPATCH method
69
+ const response = await fetch(validated.calendar_url, {
70
+ method: 'PROPPATCH',
71
+ headers: {
72
+ 'Content-Type': 'text/xml; charset=utf-8',
73
+ ...client.authHeaders,
74
+ },
75
+ body: proppatchXml,
76
+ });
77
+
78
+ if (!response.ok) {
79
+ const errorText = await response.text();
80
+ throw new Error(
81
+ `PROPPATCH failed with status ${response.status} ${response.statusText}\n` +
82
+ `Response: ${errorText}\n\n` +
83
+ `This may indicate:\n` +
84
+ `- Invalid property value (check timezone format if specified)\n` +
85
+ `- Server does not support calendar property updates\n` +
86
+ `- Permission denied for this calendar`
87
+ );
88
+ }
89
+
90
+ // Fetch updated calendar to confirm
91
+ const calendars = await client.fetchCalendars();
92
+ const updatedCalendar = calendars.find(c => c.url === validated.calendar_url);
93
+
94
+ if (!updatedCalendar) {
95
+ throw new Error(`Calendar not found after update: ${validated.calendar_url}`);
96
+ }
97
+
98
+ // Return formatted success
99
+ return formatCalendarUpdateSuccess(updatedCalendar, {
100
+ display_name: validated.display_name,
101
+ description: validated.description,
102
+ color: validated.color,
103
+ timezone: validated.timezone,
104
+ });
105
+ },
106
+ };
@@ -0,0 +1,119 @@
1
+ import { tsdavManager } from '../../tsdav-client.js';
2
+ import { validateInput } from '../../validation.js';
3
+ import { formatSuccess, formatError } from '../../formatters.js';
4
+ import { z } from 'zod';
5
+ import { updateFields } from 'tsdav-utils';
6
+
7
+ /**
8
+ * Schema for field-based event updates
9
+ * Supports all RFC 5545 iCalendar properties via tsdav-utils
10
+ * Common fields: SUMMARY, DESCRIPTION, LOCATION, DTSTART, DTEND, STATUS
11
+ * Custom properties: Any X-* property
12
+ */
13
+ const updateEventFieldsSchema = z.object({
14
+ event_url: z.string().url('Event URL must be a valid URL'),
15
+ event_etag: z.string().min(1, 'Event etag is required'),
16
+ fields: z.record(z.string()).optional()
17
+ });
18
+
19
+ /**
20
+ * Field-agnostic event update tool powered by tsdav-utils
21
+ * Supports all RFC 5545 iCalendar properties without validation
22
+ *
23
+ * Features:
24
+ * - Any standard VEVENT property (SUMMARY, DESCRIPTION, LOCATION, DTSTART, etc.)
25
+ * - Custom X-* properties for extensions
26
+ * - Field-agnostic: no pre-defined field list required
27
+ */
28
+ export const updateEventFields = {
29
+ name: 'update_event',
30
+ description: 'PREFERRED: Update event fields without iCal formatting. Supports: SUMMARY (title), DESCRIPTION (details), LOCATION (place), DTSTART (start time), DTEND (end time), STATUS (TENTATIVE/CONFIRMED/CANCELLED), and any RFC 5545 property including custom X-* properties (e.g., X-ZOOM-LINK, X-MEETING-ROOM).',
31
+ inputSchema: {
32
+ type: 'object',
33
+ properties: {
34
+ event_url: {
35
+ type: 'string',
36
+ description: 'The URL of the event to update'
37
+ },
38
+ event_etag: {
39
+ type: 'string',
40
+ description: 'The etag of the event (required for conflict detection)'
41
+ },
42
+ fields: {
43
+ type: 'object',
44
+ description: 'Fields to update - use UPPERCASE property names (e.g., SUMMARY, LOCATION, DTSTART). Any RFC 5545 property or custom X-* property is supported.',
45
+ additionalProperties: {
46
+ type: 'string'
47
+ },
48
+ properties: {
49
+ SUMMARY: {
50
+ type: 'string',
51
+ description: 'Event title/summary'
52
+ },
53
+ DESCRIPTION: {
54
+ type: 'string',
55
+ description: 'Event description/details'
56
+ },
57
+ LOCATION: {
58
+ type: 'string',
59
+ description: 'Physical or virtual meeting location'
60
+ },
61
+ DTSTART: {
62
+ type: 'string',
63
+ description: 'Start datetime (ISO 8601 or iCal format: 20250128T100000Z)'
64
+ },
65
+ DTEND: {
66
+ type: 'string',
67
+ description: 'End datetime (ISO 8601 or iCal format)'
68
+ },
69
+ STATUS: {
70
+ type: 'string',
71
+ description: 'Event status: TENTATIVE, CONFIRMED, or CANCELLED'
72
+ }
73
+ }
74
+ }
75
+ },
76
+ required: ['event_url', 'event_etag']
77
+ },
78
+ handler: async (args) => {
79
+ try {
80
+ const validated = validateInput(updateEventFieldsSchema, args);
81
+ const client = tsdavManager.getCalDavClient();
82
+
83
+ // Step 1: Fetch the current event from server
84
+ const calendarUrl = validated.event_url.substring(0, validated.event_url.lastIndexOf('/') + 1);
85
+ const currentEvents = await client.fetchCalendarObjects({
86
+ calendar: { url: calendarUrl },
87
+ objectUrls: [validated.event_url]
88
+ });
89
+
90
+ if (!currentEvents || currentEvents.length === 0) {
91
+ throw new Error('Event not found');
92
+ }
93
+
94
+ const calendarObject = currentEvents[0];
95
+
96
+ // Step 2: Update fields using tsdav-utils (field-agnostic)
97
+ // Accepts any RFC 5545 property name (UPPERCASE)
98
+ const updatedData = updateFields(calendarObject, validated.fields || {});
99
+
100
+ // Step 3: Send the updated event back to server
101
+ const updateResponse = await client.updateCalendarObject({
102
+ calendarObject: {
103
+ url: validated.event_url,
104
+ data: updatedData,
105
+ etag: validated.event_etag
106
+ }
107
+ });
108
+
109
+ return formatSuccess('Event updated successfully', {
110
+ etag: updateResponse.etag,
111
+ updated_fields: Object.keys(validated.fields || {}),
112
+ message: `Updated ${Object.keys(validated.fields || {}).length} field(s): ${Object.keys(validated.fields || {}).join(', ')}`
113
+ });
114
+
115
+ } catch (error) {
116
+ return formatError('update_event_fields', error);
117
+ }
118
+ }
119
+ };
@@ -0,0 +1,45 @@
1
+ import { tsdavManager } from '../../tsdav-client.js';
2
+ import { validateInput, updateEventSchema } from '../../validation.js';
3
+ import { formatSuccess } from '../../formatters.js';
4
+
5
+ /**
6
+ * Update an existing calendar event with raw iCal data
7
+ */
8
+ export const updateEventRaw = {
9
+ name: 'update_event_raw',
10
+ description: 'ADVANCED: Update event with raw iCal data. Requires manual iCal formatting - use update_event instead for simple field updates (summary, description). Only use this if you have complete pre-formatted iCal data or need to update advanced iCal properties.',
11
+ inputSchema: {
12
+ type: 'object',
13
+ properties: {
14
+ event_url: {
15
+ type: 'string',
16
+ description: 'The URL of the event to update',
17
+ },
18
+ event_etag: {
19
+ type: 'string',
20
+ description: 'The etag of the event',
21
+ },
22
+ updated_ical_data: {
23
+ type: 'string',
24
+ description: 'The complete updated iCal data',
25
+ },
26
+ },
27
+ required: ['event_url', 'event_etag', 'updated_ical_data'],
28
+ },
29
+ handler: async (args) => {
30
+ const validated = validateInput(updateEventSchema, args);
31
+ const client = tsdavManager.getCalDavClient();
32
+
33
+ const response = await client.updateCalendarObject({
34
+ calendarObject: {
35
+ url: validated.event_url,
36
+ data: validated.updated_ical_data,
37
+ etag: validated.event_etag,
38
+ },
39
+ });
40
+
41
+ return formatSuccess('Event updated successfully', {
42
+ etag: response.etag,
43
+ });
44
+ },
45
+ };
@@ -0,0 +1,38 @@
1
+ import { tsdavManager } from '../../tsdav-client.js';
2
+ import { validateInput, addressBookMultiGetSchema } from '../../validation.js';
3
+ import { formatContactList } from '../../formatters.js';
4
+
5
+ /**
6
+ * Batch fetch multiple specific contacts by their URLs
7
+ */
8
+ export const addressbookMultiGet = {
9
+ name: 'addressbook_multi_get',
10
+ description: 'Batch fetch multiple specific contacts by their URLs. Use when you have exact contact URLs and want to retrieve their details',
11
+ inputSchema: {
12
+ type: 'object',
13
+ properties: {
14
+ addressbook_url: {
15
+ type: 'string',
16
+ description: 'The URL of the address book',
17
+ },
18
+ contact_urls: {
19
+ type: 'array',
20
+ items: { type: 'string' },
21
+ description: 'Array of contact URLs to fetch',
22
+ },
23
+ },
24
+ required: ['addressbook_url', 'contact_urls'],
25
+ },
26
+ handler: async (args) => {
27
+ const validated = validateInput(addressBookMultiGetSchema, args);
28
+ const client = tsdavManager.getCardDavClient();
29
+
30
+ const vcards = await client.addressBookMultiGet({
31
+ url: validated.addressbook_url,
32
+ props: [{ name: 'getetag', namespace: 'DAV:' }, { name: 'address-data', namespace: 'urn:ietf:params:xml:ns:carddav' }],
33
+ objectUrls: validated.contact_urls,
34
+ });
35
+
36
+ return formatContactList(vcards, { url: validated.addressbook_url });
37
+ },
38
+ };
@@ -0,0 +1,85 @@
1
+ import { tsdavManager } from '../../tsdav-client.js';
2
+ import { validateInput, addressBookQuerySchema } from '../../validation.js';
3
+ import { formatContactList } from '../../formatters.js';
4
+
5
+ /**
6
+ * Search and filter contacts efficiently
7
+ */
8
+ export const addressbookQuery = {
9
+ name: 'addressbook_query',
10
+ description: '⭐ PREFERRED: Search and filter contacts efficiently (name, email, organization). Use for "find contacts with...", "search for email...", "contacts at company..." queries. Use instead of list_contacts when ANY filter is specified. Omit addressbook_url to search across ALL addressbooks automatically.',
11
+ inputSchema: {
12
+ type: 'object',
13
+ properties: {
14
+ addressbook_url: {
15
+ type: 'string',
16
+ description: 'Optional: Specific addressbook URL. Omit to search ALL addressbooks (recommended for "find contact X" queries). Only provide if user explicitly names an addressbook.',
17
+ },
18
+ name_filter: {
19
+ type: 'string',
20
+ description: 'Search contact names (full/given/family name). Example: "John Smith" or "Smith". At least one filter (name, email, or org) is required.',
21
+ },
22
+ email_filter: {
23
+ type: 'string',
24
+ description: 'Search contact email addresses. Use for queries like "Gmail contacts" → "@gmail.com", "work emails" → "@company.com", or specific addresses. At least one filter (name, email, or org) is required.',
25
+ },
26
+ organization_filter: {
27
+ type: 'string',
28
+ description: 'Search contact organizations/companies. Example: "Google" or "Acme Corp". At least one filter (name, email, or org) is required.',
29
+ },
30
+ },
31
+ required: [],
32
+ },
33
+ handler: async (args) => {
34
+ const validated = validateInput(addressBookQuerySchema, args);
35
+ const client = tsdavManager.getCardDavClient();
36
+ const addressBooks = await client.fetchAddressBooks();
37
+
38
+ // Resolve which addressbooks to search (all or specific)
39
+ const addressbooksToSearch = validated.addressbook_url
40
+ ? addressBooks.filter(ab => ab.url === validated.addressbook_url)
41
+ : addressBooks;
42
+
43
+ // Collect all vcards from all selected addressbooks
44
+ let allVCards = [];
45
+ for (const addressBook of addressbooksToSearch) {
46
+ const vcards = await client.fetchVCards({ addressBook });
47
+ // Add addressbook context for display
48
+ vcards.forEach(vcard => {
49
+ vcard._addressbookName = addressBook.displayName || addressBook.url;
50
+ });
51
+ allVCards = allVCards.concat(vcards);
52
+ }
53
+
54
+ let filteredContacts = allVCards;
55
+
56
+ if (validated.name_filter) {
57
+ const nameLower = validated.name_filter.toLowerCase();
58
+ filteredContacts = filteredContacts.filter(vcard => {
59
+ const fn = vcard.data?.match(/FN:(.+)/)?.[1] || '';
60
+ const n = vcard.data?.match(/N:(.+)/)?.[1] || '';
61
+ return fn.toLowerCase().includes(nameLower) || n.toLowerCase().includes(nameLower);
62
+ });
63
+ }
64
+
65
+ if (validated.email_filter) {
66
+ const emailLower = validated.email_filter.toLowerCase();
67
+ filteredContacts = filteredContacts.filter(vcard => {
68
+ const email = vcard.data?.match(/EMAIL[^:]*:(.+)/)?.[1] || '';
69
+ return email.toLowerCase().includes(emailLower);
70
+ });
71
+ }
72
+
73
+ if (validated.organization_filter) {
74
+ const orgLower = validated.organization_filter.toLowerCase();
75
+ filteredContacts = filteredContacts.filter(vcard => {
76
+ const org = vcard.data?.match(/ORG:(.+)/)?.[1] || '';
77
+ return org.toLowerCase().includes(orgLower);
78
+ });
79
+ }
80
+
81
+ // Format and return results (pass first addressbook for context, or null if multiple)
82
+ const singleAddressbook = addressbooksToSearch.length === 1 ? addressbooksToSearch[0] : null;
83
+ return formatContactList(filteredContacts, singleAddressbook);
84
+ },
85
+ };
@@ -0,0 +1,84 @@
1
+ import { tsdavManager } from '../../tsdav-client.js';
2
+ import { validateInput, createContactSchema, sanitizeVCardString } from '../../validation.js';
3
+ import { formatSuccess } from '../../formatters.js';
4
+ import { findAddressbookOrThrow } from '../shared/helpers.js';
5
+
6
+ /**
7
+ * Create a new contact (vCard)
8
+ */
9
+ export const createContact = {
10
+ name: 'create_contact',
11
+ description: 'Create a new contact (vCard) with name, email, phone, organization, and other details',
12
+ inputSchema: {
13
+ type: 'object',
14
+ properties: {
15
+ addressbook_url: {
16
+ type: 'string',
17
+ description: 'The URL of the address book to create the contact in',
18
+ },
19
+ full_name: {
20
+ type: 'string',
21
+ description: 'Full name of the contact',
22
+ },
23
+ family_name: {
24
+ type: 'string',
25
+ description: 'Family/last name',
26
+ },
27
+ given_name: {
28
+ type: 'string',
29
+ description: 'Given/first name',
30
+ },
31
+ email: {
32
+ type: 'string',
33
+ description: 'Email address (optional)',
34
+ },
35
+ phone: {
36
+ type: 'string',
37
+ description: 'Phone number (optional)',
38
+ },
39
+ organization: {
40
+ type: 'string',
41
+ description: 'Organization/company (optional)',
42
+ },
43
+ note: {
44
+ type: 'string',
45
+ description: 'Additional notes (optional)',
46
+ },
47
+ },
48
+ required: ['addressbook_url', 'full_name'],
49
+ },
50
+ handler: async (args) => {
51
+ const validated = validateInput(createContactSchema, args);
52
+ const client = tsdavManager.getCardDavClient();
53
+ const addressBooks = await client.fetchAddressBooks();
54
+ const addressBook = findAddressbookOrThrow(addressBooks, validated.addressbook_url);
55
+
56
+ const uid = `contact-${Date.now()}`;
57
+ const fullName = sanitizeVCardString(validated.full_name);
58
+ const familyName = validated.family_name ? sanitizeVCardString(validated.family_name) : '';
59
+ const givenName = validated.given_name ? sanitizeVCardString(validated.given_name) : '';
60
+ const email = validated.email ? sanitizeVCardString(validated.email) : '';
61
+ const phone = validated.phone ? sanitizeVCardString(validated.phone) : '';
62
+ const organization = validated.organization ? sanitizeVCardString(validated.organization) : '';
63
+ const note = validated.note ? sanitizeVCardString(validated.note) : '';
64
+
65
+ const vCardString = `BEGIN:VCARD
66
+ VERSION:3.0
67
+ UID:${uid}
68
+ FN:${fullName}${familyName || givenName ? `\nN:${familyName};${givenName};;;` : ''}${email ? `\nEMAIL;TYPE=INTERNET:${email}` : ''}${phone ? `\nTEL;TYPE=CELL:${phone}` : ''}${organization ? `\nORG:${organization}` : ''}${note ? `\nNOTE:${note}` : ''}
69
+ REV:${new Date().toISOString()}
70
+ END:VCARD`;
71
+
72
+ const response = await client.createVCard({
73
+ addressBook,
74
+ filename: `${uid}.vcf`,
75
+ vCardString,
76
+ });
77
+
78
+ return formatSuccess('Contact created successfully', {
79
+ url: response.url,
80
+ etag: response.etag,
81
+ fullName: validated.full_name,
82
+ });
83
+ },
84
+ };
@@ -0,0 +1,38 @@
1
+ import { tsdavManager } from '../../tsdav-client.js';
2
+ import { validateInput, deleteContactSchema } from '../../validation.js';
3
+ import { formatSuccess } from '../../formatters.js';
4
+
5
+ /**
6
+ * Delete a contact (vCard) permanently
7
+ */
8
+ export const deleteContact = {
9
+ name: 'delete_contact',
10
+ description: 'Delete a contact (vCard) permanently. Requires contact URL and etag',
11
+ inputSchema: {
12
+ type: 'object',
13
+ properties: {
14
+ vcard_url: {
15
+ type: 'string',
16
+ description: 'The URL of the vCard to delete',
17
+ },
18
+ vcard_etag: {
19
+ type: 'string',
20
+ description: 'The etag of the vCard',
21
+ },
22
+ },
23
+ required: ['vcard_url', 'vcard_etag'],
24
+ },
25
+ handler: async (args) => {
26
+ const validated = validateInput(deleteContactSchema, args);
27
+ const client = tsdavManager.getCardDavClient();
28
+
29
+ await client.deleteVCard({
30
+ vCard: {
31
+ url: validated.vcard_url,
32
+ etag: validated.vcard_etag,
33
+ },
34
+ });
35
+
36
+ return formatSuccess('Contact deleted successfully');
37
+ },
38
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Contact Tools - CardDAV operations
3
+ * Exports all contact-related tools
4
+ */
5
+
6
+ export { listAddressbooks } from './list-addressbooks.js';
7
+ export { listContacts } from './list-contacts.js';
8
+ export { createContact } from './create-contact.js';
9
+ export { updateContactFields } from './update-contact-fields.js';
10
+ export { updateContactRaw } from './update-contact-raw.js';
11
+ export { deleteContact } from './delete-contact.js';
12
+ export { addressbookQuery } from './addressbook-query.js';
13
+ export { addressbookMultiGet } from './addressbook-multi-get.js';