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 { formatAddressBookList } from '../../formatters.js';
3
+
4
+ /**
5
+ * List all available address books from the CardDAV server
6
+ */
7
+ export const listAddressbooks = {
8
+ name: 'list_addressbooks',
9
+ description: 'List all available address books from the CardDAV server. Use this to get address book URLs needed for other contact operations',
10
+ inputSchema: {
11
+ type: 'object',
12
+ properties: {},
13
+ required: [],
14
+ },
15
+ handler: async () => {
16
+ const client = tsdavManager.getCardDavClient();
17
+ const addressBooks = await client.fetchAddressBooks();
18
+
19
+ return formatAddressBookList(addressBooks);
20
+ },
21
+ };
@@ -0,0 +1,32 @@
1
+ import { tsdavManager } from '../../tsdav-client.js';
2
+ import { validateInput, listContactsSchema } from '../../validation.js';
3
+ import { formatContactList } from '../../formatters.js';
4
+ import { findAddressbookOrThrow } from '../shared/helpers.js';
5
+
6
+ /**
7
+ * List ALL contacts from an address book without filtering
8
+ */
9
+ export const listContacts = {
10
+ name: 'list_contacts',
11
+ description: 'List ALL contacts from an address book without filtering. WARNING: Returns all contacts which can be thousands - use addressbook_query instead when searching for specific contacts by name, email, or organization to save tokens',
12
+ inputSchema: {
13
+ type: 'object',
14
+ properties: {
15
+ addressbook_url: {
16
+ type: 'string',
17
+ description: 'The URL of the address book to fetch contacts from',
18
+ },
19
+ },
20
+ required: ['addressbook_url'],
21
+ },
22
+ handler: async (args) => {
23
+ const validated = validateInput(listContactsSchema, args);
24
+ const client = tsdavManager.getCardDavClient();
25
+ const addressBooks = await client.fetchAddressBooks();
26
+ const addressBook = findAddressbookOrThrow(addressBooks, validated.addressbook_url);
27
+
28
+ const vcards = await client.fetchVCards({ addressBook });
29
+
30
+ return formatContactList(vcards, addressBook);
31
+ },
32
+ };
@@ -0,0 +1,135 @@
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 vCard updates
9
+ * Supports all RFC 6350 vCard properties via tsdav-utils
10
+ * Common fields: FN, N, EMAIL, TEL, ORG, TITLE, NOTE, URL, ADR
11
+ * Custom properties: Any X-* property
12
+ */
13
+ const updateContactFieldsSchema = z.object({
14
+ vcard_url: z.string().url('vCard URL must be a valid URL'),
15
+ vcard_etag: z.string().min(1, 'vCard etag is required'),
16
+ fields: z.record(z.string()).optional()
17
+ });
18
+
19
+ /**
20
+ * Field-agnostic contact update tool powered by tsdav-utils
21
+ * Supports all RFC 6350 vCard properties without validation
22
+ *
23
+ * Features:
24
+ * - Any standard vCard property (FN, N, EMAIL, TEL, ORG, TITLE, ADR, etc.)
25
+ * - Custom X-* properties for extensions
26
+ * - Field-agnostic: no pre-defined field list required
27
+ */
28
+ export const updateContactFields = {
29
+ name: 'update_contact',
30
+ description: 'PREFERRED: Update contact fields without vCard formatting. Supports: FN (full name), N (structured name), EMAIL, TEL (phone), ORG (organization), TITLE (job title), NOTE, URL, ADR (address), BDAY (birthday), and any RFC 6350 vCard property including custom X-* properties.',
31
+ inputSchema: {
32
+ type: 'object',
33
+ properties: {
34
+ vcard_url: {
35
+ type: 'string',
36
+ description: 'The URL of the vCard to update'
37
+ },
38
+ vcard_etag: {
39
+ type: 'string',
40
+ description: 'The etag of the vCard (required for conflict detection)'
41
+ },
42
+ fields: {
43
+ type: 'object',
44
+ description: 'Fields to update - use UPPERCASE property names (e.g., FN, EMAIL, TEL). Any RFC 6350 vCard property or custom X-* property is supported.',
45
+ additionalProperties: {
46
+ type: 'string'
47
+ },
48
+ properties: {
49
+ FN: {
50
+ type: 'string',
51
+ description: 'Full formatted name (e.g., "John Doe")'
52
+ },
53
+ N: {
54
+ type: 'string',
55
+ description: 'Structured name (format: "Family;Given;Additional;Prefix;Suffix")'
56
+ },
57
+ EMAIL: {
58
+ type: 'string',
59
+ description: 'Email address'
60
+ },
61
+ TEL: {
62
+ type: 'string',
63
+ description: 'Phone number'
64
+ },
65
+ ORG: {
66
+ type: 'string',
67
+ description: 'Organization/company name'
68
+ },
69
+ TITLE: {
70
+ type: 'string',
71
+ description: 'Job title'
72
+ },
73
+ NOTE: {
74
+ type: 'string',
75
+ description: 'Notes/comments'
76
+ },
77
+ URL: {
78
+ type: 'string',
79
+ description: 'Web URL'
80
+ },
81
+ ADR: {
82
+ type: 'string',
83
+ description: 'Address (format: "POBox;Extended;Street;City;Region;PostalCode;Country")'
84
+ },
85
+ BDAY: {
86
+ type: 'string',
87
+ description: 'Birthday (ISO 8601 format: 1990-01-01)'
88
+ }
89
+ }
90
+ }
91
+ },
92
+ required: ['vcard_url', 'vcard_etag']
93
+ },
94
+ handler: async (args) => {
95
+ try {
96
+ const validated = validateInput(updateContactFieldsSchema, args);
97
+ const client = tsdavManager.getCardDavClient();
98
+
99
+ // Step 1: Fetch the current vCard from server
100
+ const addressBookUrl = validated.vcard_url.substring(0, validated.vcard_url.lastIndexOf('/') + 1);
101
+ const currentVCards = await client.fetchVCards({
102
+ addressBook: { url: addressBookUrl },
103
+ objectUrls: [validated.vcard_url]
104
+ });
105
+
106
+ if (!currentVCards || currentVCards.length === 0) {
107
+ throw new Error('Contact not found');
108
+ }
109
+
110
+ const vCardObject = currentVCards[0];
111
+
112
+ // Step 2: Update fields using tsdav-utils (field-agnostic)
113
+ // Accepts any RFC 6350 vCard property name (UPPERCASE)
114
+ const updatedData = updateFields(vCardObject, validated.fields || {});
115
+
116
+ // Step 3: Send the updated vCard back to server
117
+ const updateResponse = await client.updateVCard({
118
+ vCard: {
119
+ url: validated.vcard_url,
120
+ data: updatedData,
121
+ etag: validated.vcard_etag
122
+ }
123
+ });
124
+
125
+ return formatSuccess('Contact updated successfully', {
126
+ etag: updateResponse.etag,
127
+ updated_fields: Object.keys(validated.fields || {}),
128
+ message: `Updated ${Object.keys(validated.fields || {}).length} field(s): ${Object.keys(validated.fields || {}).join(', ')}`
129
+ });
130
+
131
+ } catch (error) {
132
+ return formatError('update_contact', error);
133
+ }
134
+ }
135
+ };
@@ -0,0 +1,45 @@
1
+ import { tsdavManager } from '../../tsdav-client.js';
2
+ import { validateInput, updateContactSchema } from '../../validation.js';
3
+ import { formatSuccess } from '../../formatters.js';
4
+
5
+ /**
6
+ * Update an existing contact with raw vCard data
7
+ */
8
+ export const updateContactRaw = {
9
+ name: 'update_contact_raw',
10
+ description: 'ADVANCED: Update contact with raw vCard data. Requires manual vCard formatting - use update_contact instead for simple field updates (name, email, phone). Only use this if you have complete pre-formatted vCard data or need to update advanced vCard properties.',
11
+ inputSchema: {
12
+ type: 'object',
13
+ properties: {
14
+ vcard_url: {
15
+ type: 'string',
16
+ description: 'The URL of the vCard to update',
17
+ },
18
+ vcard_etag: {
19
+ type: 'string',
20
+ description: 'The etag of the vCard',
21
+ },
22
+ updated_vcard_data: {
23
+ type: 'string',
24
+ description: 'The complete updated vCard data',
25
+ },
26
+ },
27
+ required: ['vcard_url', 'vcard_etag', 'updated_vcard_data'],
28
+ },
29
+ handler: async (args) => {
30
+ const validated = validateInput(updateContactSchema, args);
31
+ const client = tsdavManager.getCardDavClient();
32
+
33
+ const response = await client.updateVCard({
34
+ vCard: {
35
+ url: validated.vcard_url,
36
+ data: validated.updated_vcard_data,
37
+ etag: validated.vcard_etag,
38
+ },
39
+ });
40
+
41
+ return formatSuccess('Contact updated successfully', {
42
+ etag: response.etag,
43
+ });
44
+ },
45
+ };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * MCP Tools Main Index
3
+ * Combines all tool modules into a single exportable array
4
+ */
5
+
6
+ // Calendar Tools (CalDAV)
7
+ import * as calendarTools from './calendar/index.js';
8
+
9
+ // Contact Tools (CardDAV)
10
+ import * as contactTools from './contacts/index.js';
11
+
12
+ // Todo Tools (VTODO)
13
+ import * as todoTools from './todos/index.js';
14
+
15
+ /**
16
+ * All available MCP tools
17
+ * Total: 26 tools organized in 3 categories
18
+ */
19
+ export const tools = [
20
+ // ================================
21
+ // CALENDAR TOOLS (11 tools)
22
+ // ================================
23
+ calendarTools.listCalendars,
24
+ calendarTools.listEvents,
25
+ calendarTools.createEvent,
26
+ calendarTools.updateEventFields,
27
+ calendarTools.updateEventRaw,
28
+ calendarTools.deleteEvent,
29
+ calendarTools.calendarQuery,
30
+ calendarTools.makeCalendar,
31
+ calendarTools.updateCalendar,
32
+ calendarTools.deleteCalendar,
33
+ calendarTools.calendarMultiGet,
34
+
35
+ // ================================
36
+ // CONTACT TOOLS (8 tools)
37
+ // ================================
38
+ contactTools.listAddressbooks,
39
+ contactTools.listContacts,
40
+ contactTools.createContact,
41
+ contactTools.updateContactFields,
42
+ contactTools.updateContactRaw,
43
+ contactTools.deleteContact,
44
+ contactTools.addressbookQuery,
45
+ contactTools.addressbookMultiGet,
46
+
47
+ // ================================
48
+ // TODO TOOLS (7 tools)
49
+ // ================================
50
+ todoTools.listTodos,
51
+ todoTools.createTodo,
52
+ todoTools.updateTodoFields,
53
+ todoTools.updateTodoRaw,
54
+ todoTools.deleteTodo,
55
+ todoTools.todoQuery,
56
+ todoTools.todoMultiGet,
57
+ ];
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Shared helper functions for tool implementations
3
+ */
4
+
5
+ /**
6
+ * Format iCal date (ISO 8601 to iCal format)
7
+ * @param {string|Date} date - Date to format
8
+ * @returns {string} Formatted iCal date string (YYYYMMDDTHHmmssZ)
9
+ */
10
+ export function formatICalDate(date) {
11
+ return new Date(date).toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
12
+ }
13
+
14
+ /**
15
+ * Generate unique UID for calendar objects
16
+ * @param {string} prefix - Prefix for the UID (e.g., 'event', 'todo', 'contact')
17
+ * @returns {string} Unique identifier
18
+ */
19
+ export function generateUID(prefix = 'object') {
20
+ return `${prefix}-${Date.now()}@tsdav-mcp`;
21
+ }
22
+
23
+ /**
24
+ * Extract calendar home URL from existing calendar URL or account
25
+ * @param {Object} client - CalDAV client instance
26
+ * @returns {Promise<string>} Calendar home URL
27
+ */
28
+ export async function getCalendarHome(client) {
29
+ // Try to get from account first
30
+ let calendarHome = client.account?.homeUrl;
31
+
32
+ // Fallback: Extract from existing calendar
33
+ if (!calendarHome) {
34
+ const calendars = await client.fetchCalendars();
35
+
36
+ if (!calendars || calendars.length === 0) {
37
+ throw new Error('Cannot determine calendar home: No calendar home found and no existing calendars available.');
38
+ }
39
+
40
+ // Extract calendar home from an existing calendar URL
41
+ // Example: https://dav.example.com/calendars/user/calendar-name/ -> https://dav.example.com/calendars/user/
42
+ const existingCalendarUrl = calendars[0].url;
43
+ calendarHome = existingCalendarUrl.substring(0, existingCalendarUrl.lastIndexOf('/', existingCalendarUrl.length - 2) + 1);
44
+ }
45
+
46
+ return calendarHome;
47
+ }
48
+
49
+ /**
50
+ * Sanitize calendar/event name for URL usage
51
+ * @param {string} name - Display name
52
+ * @returns {string} Sanitized name suitable for URLs
53
+ */
54
+ export function sanitizeNameForUrl(name) {
55
+ return name
56
+ .toLowerCase()
57
+ .replace(/[^a-z0-9-]/g, '-')
58
+ .replace(/-+/g, '-')
59
+ .replace(/^-|-$/g, '');
60
+ }
61
+
62
+ /**
63
+ * Find calendar by URL and provide helpful error if not found
64
+ * @param {Array} calendars - List of calendars
65
+ * @param {string} calendarUrl - URL to search for
66
+ * @returns {Object} Calendar object
67
+ * @throws {Error} If calendar not found
68
+ */
69
+ export function findCalendarOrThrow(calendars, calendarUrl) {
70
+ const calendar = calendars.find(c => c.url === calendarUrl);
71
+
72
+ if (!calendar) {
73
+ const availableUrls = calendars.map(c => c.url).join('\n- ');
74
+ throw new Error(
75
+ `Calendar not found: ${calendarUrl}\n\n` +
76
+ `Available calendar URLs:\n- ${availableUrls}\n\n` +
77
+ `Please use list_calendars first to get the correct calendar URLs.`
78
+ );
79
+ }
80
+
81
+ return calendar;
82
+ }
83
+
84
+ /**
85
+ * Find addressbook by URL and provide helpful error if not found
86
+ * @param {Array} addressbooks - List of addressbooks
87
+ * @param {string} addressbookUrl - URL to search for
88
+ * @returns {Object} Addressbook object
89
+ * @throws {Error} If addressbook not found
90
+ */
91
+ export function findAddressbookOrThrow(addressbooks, addressbookUrl) {
92
+ const addressbook = addressbooks.find(ab => ab.url === addressbookUrl);
93
+
94
+ if (!addressbook) {
95
+ const availableUrls = addressbooks.map(ab => ab.url).join('\n- ');
96
+ throw new Error(
97
+ `Address book not found: ${addressbookUrl}\n\n` +
98
+ `Available address book URLs:\n- ${availableUrls}\n\n` +
99
+ `Please use list_addressbooks first to get the correct URLs.`
100
+ );
101
+ }
102
+
103
+ return addressbook;
104
+ }
105
+
106
+ /**
107
+ * Build time range options for queries
108
+ * @param {string} timeRangeStart - Start date (ISO 8601)
109
+ * @param {string} timeRangeEnd - End date (ISO 8601)
110
+ * @returns {Object} Time range options object
111
+ */
112
+ export function buildTimeRangeOptions(timeRangeStart, timeRangeEnd) {
113
+ const options = {};
114
+
115
+ if (timeRangeStart && !timeRangeEnd) {
116
+ // Default to 1 year from start if only start provided
117
+ const startDate = new Date(timeRangeStart);
118
+ const endDate = new Date(startDate);
119
+ endDate.setFullYear(endDate.getFullYear() + 1);
120
+ options.timeRange = {
121
+ start: timeRangeStart,
122
+ end: endDate.toISOString(),
123
+ };
124
+ } else if (timeRangeStart && timeRangeEnd) {
125
+ options.timeRange = {
126
+ start: timeRangeStart,
127
+ end: timeRangeEnd,
128
+ };
129
+ }
130
+
131
+ return options;
132
+ }
@@ -0,0 +1,101 @@
1
+ import { tsdavManager } from '../../tsdav-client.js';
2
+ import { validateInput, createTodoSchema, sanitizeICalString } from '../../validation.js';
3
+ import { formatSuccess } from '../../formatters.js';
4
+ import { formatICalDate } from '../shared/helpers.js';
5
+
6
+ /**
7
+ * Create a new todo/task in a calendar
8
+ */
9
+ export const createTodo = {
10
+ name: 'create_todo',
11
+ description: 'Create a new todo/task in a calendar. Use this when user wants to add a task, todo item, or reminder with optional due date, priority, and status.',
12
+ inputSchema: {
13
+ type: 'object',
14
+ properties: {
15
+ calendar_url: {
16
+ type: 'string',
17
+ description: 'The URL of the calendar to create the todo in',
18
+ },
19
+ summary: {
20
+ type: 'string',
21
+ description: 'The title/summary of the todo (required)',
22
+ },
23
+ description: {
24
+ type: 'string',
25
+ description: 'Optional detailed description',
26
+ },
27
+ due_date: {
28
+ type: 'string',
29
+ description: 'Optional due date in ISO 8601 format (e.g., 2025-12-31T23:59:59+02:00)',
30
+ },
31
+ priority: {
32
+ type: 'number',
33
+ description: 'Optional priority: 0=none, 1-3=high, 4-6=medium, 7-9=low',
34
+ },
35
+ status: {
36
+ type: 'string',
37
+ enum: ['NEEDS-ACTION', 'IN-PROCESS', 'COMPLETED', 'CANCELLED'],
38
+ description: 'Optional status (default: NEEDS-ACTION)',
39
+ },
40
+ percent_complete: {
41
+ type: 'number',
42
+ description: 'Optional completion percentage (0-100)',
43
+ },
44
+ },
45
+ required: ['calendar_url', 'summary'],
46
+ },
47
+ handler: async (args) => {
48
+ const validated = validateInput(createTodoSchema, args);
49
+ const client = tsdavManager.getCalDavClient();
50
+
51
+ // Build VTODO iCalendar string
52
+ const uid = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}@tsdav-mcp`;
53
+ const dtstamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
54
+
55
+ let vtodo = 'BEGIN:VCALENDAR\r\n';
56
+ vtodo += 'VERSION:2.0\r\n';
57
+ vtodo += 'PRODID:-//tsdav-mcp-server//NONSGML v1.2.0//EN\r\n';
58
+ vtodo += 'BEGIN:VTODO\r\n';
59
+ vtodo += `UID:${uid}\r\n`;
60
+ vtodo += `DTSTAMP:${dtstamp}\r\n`;
61
+ vtodo += `SUMMARY:${sanitizeICalString(validated.summary)}\r\n`;
62
+
63
+ if (validated.description) {
64
+ vtodo += `DESCRIPTION:${sanitizeICalString(validated.description)}\r\n`;
65
+ }
66
+
67
+ if (validated.status) {
68
+ vtodo += `STATUS:${validated.status}\r\n`;
69
+ } else {
70
+ vtodo += 'STATUS:NEEDS-ACTION\r\n';
71
+ }
72
+
73
+ if (validated.priority !== undefined) {
74
+ vtodo += `PRIORITY:${validated.priority}\r\n`;
75
+ }
76
+
77
+ if (validated.due_date) {
78
+ const dueDate = new Date(validated.due_date).toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
79
+ vtodo += `DUE:${dueDate}\r\n`;
80
+ }
81
+
82
+ if (validated.percent_complete !== undefined) {
83
+ vtodo += `PERCENT-COMPLETE:${validated.percent_complete}\r\n`;
84
+ }
85
+
86
+ vtodo += 'END:VTODO\r\n';
87
+ vtodo += 'END:VCALENDAR\r\n';
88
+
89
+ const result = await client.createTodo({
90
+ calendar: { url: validated.calendar_url },
91
+ filename: `${Date.now()}.ics`,
92
+ iCalString: vtodo,
93
+ });
94
+
95
+ return formatSuccess('Todo created successfully', {
96
+ url: result.url,
97
+ etag: result.etag,
98
+ summary: validated.summary,
99
+ });
100
+ },
101
+ };
@@ -0,0 +1,38 @@
1
+ import { tsdavManager } from '../../tsdav-client.js';
2
+ import { validateInput, deleteTodoSchema } from '../../validation.js';
3
+ import { formatSuccess } from '../../formatters.js';
4
+
5
+ /**
6
+ * Delete a todo/task permanently
7
+ */
8
+ export const deleteTodo = {
9
+ name: 'delete_todo',
10
+ description: 'Delete a todo/task permanently from the calendar. Cannot be undone. Requires todo URL and etag.',
11
+ inputSchema: {
12
+ type: 'object',
13
+ properties: {
14
+ todo_url: {
15
+ type: 'string',
16
+ description: 'The URL of the todo to delete',
17
+ },
18
+ todo_etag: {
19
+ type: 'string',
20
+ description: 'The current ETag of the todo',
21
+ },
22
+ },
23
+ required: ['todo_url', 'todo_etag'],
24
+ },
25
+ handler: async (args) => {
26
+ const validated = validateInput(deleteTodoSchema, args);
27
+ const client = tsdavManager.getCalDavClient();
28
+
29
+ await client.deleteTodo({
30
+ todo: {
31
+ url: validated.todo_url,
32
+ etag: validated.todo_etag,
33
+ },
34
+ });
35
+
36
+ return formatSuccess('Todo deleted successfully');
37
+ },
38
+ };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Todo Tools - VTODO operations
3
+ * Exports all todo/task-related tools
4
+ */
5
+
6
+ export { listTodos } from './list-todos.js';
7
+ export { createTodo } from './create-todo.js';
8
+ export { updateTodoFields } from './update-todo-fields.js';
9
+ export { updateTodoRaw } from './update-todo-raw.js';
10
+ export { deleteTodo } from './delete-todo.js';
11
+ export { todoQuery } from './todo-query.js';
12
+ export { todoMultiGet } from './todo-multi-get.js';
@@ -0,0 +1,30 @@
1
+ import { tsdavManager } from '../../tsdav-client.js';
2
+ import { validateInput, listTodosSchema } from '../../validation.js';
3
+ import { formatTodoList } from '../../formatters.js';
4
+
5
+ /**
6
+ * List ALL todos/tasks from a calendar
7
+ */
8
+ export const listTodos = {
9
+ name: 'list_todos',
10
+ description: 'List ALL todos/tasks from a calendar. WARNING: Returns all todos without filtering - use todo_query for searches with filters by status, summary, or due date.',
11
+ inputSchema: {
12
+ type: 'object',
13
+ properties: {
14
+ calendar_url: {
15
+ type: 'string',
16
+ description: 'The URL of the calendar containing todos',
17
+ },
18
+ },
19
+ required: ['calendar_url'],
20
+ },
21
+ handler: async (args) => {
22
+ const validated = validateInput(listTodosSchema, args);
23
+ const client = tsdavManager.getCalDavClient();
24
+
25
+ const calendar = { url: validated.calendar_url };
26
+ const todos = await client.fetchTodos({ calendar });
27
+
28
+ return formatTodoList(todos, validated.calendar_url);
29
+ },
30
+ };
@@ -0,0 +1,37 @@
1
+ import { tsdavManager } from '../../tsdav-client.js';
2
+ import { validateInput, todoMultiGetSchema } from '../../validation.js';
3
+ import { formatTodoList } from '../../formatters.js';
4
+
5
+ /**
6
+ * Batch fetch multiple specific todos by their URLs
7
+ */
8
+ export const todoMultiGet = {
9
+ name: 'todo_multi_get',
10
+ description: 'Batch fetch multiple specific todos by their URLs. More efficient than fetching one by one when you have exact todo URLs.',
11
+ inputSchema: {
12
+ type: 'object',
13
+ properties: {
14
+ todo_urls: {
15
+ type: 'array',
16
+ items: { type: 'string' },
17
+ description: 'Array of todo URLs to fetch',
18
+ },
19
+ },
20
+ required: ['todo_urls'],
21
+ },
22
+ handler: async (args) => {
23
+ const validated = validateInput(todoMultiGetSchema, args);
24
+ const client = tsdavManager.getCalDavClient();
25
+
26
+ // Extract calendar URL from first todo URL
27
+ const calendarUrl = validated.todo_urls[0].split('/').slice(0, -1).join('/');
28
+
29
+ const todos = await client.todoMultiGet({
30
+ url: calendarUrl,
31
+ props: [{ name: 'getetag', namespace: 'DAV:' }, { name: 'calendar-data', namespace: 'urn:ietf:params:xml:ns:caldav' }],
32
+ objectUrls: validated.todo_urls,
33
+ });
34
+
35
+ return formatTodoList(todos, calendarUrl);
36
+ },
37
+ };