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,112 @@
1
+ import { tsdavManager } from '../../tsdav-client.js';
2
+ import { validateInput, todoQuerySchema } from '../../validation.js';
3
+ import { formatTodoList } from '../../formatters.js';
4
+
5
+ /**
6
+ * Search and filter todos efficiently
7
+ */
8
+ export const todoQuery = {
9
+ name: 'todo_query',
10
+ description: '⭐ PREFERRED: Search and filter todos efficiently. Use instead of list_todos to conserve tokens. Omit calendar_url to search across ALL calendars automatically.',
11
+ inputSchema: {
12
+ type: 'object',
13
+ properties: {
14
+ calendar_url: {
15
+ type: 'string',
16
+ description: 'Optional: Specific calendar URL. Omit to search ALL calendars (recommended).',
17
+ },
18
+ summary_filter: {
19
+ type: 'string',
20
+ description: 'Search todo summaries/titles containing this text (case-insensitive). Example: "write report" or "review PR". Can be used alone as sufficient filter.',
21
+ },
22
+ status_filter: {
23
+ type: 'string',
24
+ enum: ['NEEDS-ACTION', 'IN-PROCESS', 'COMPLETED', 'CANCELLED'],
25
+ description: 'Filter by todo status. Use "NEEDS-ACTION" for pending tasks, "COMPLETED" for done tasks. Can be used alone as sufficient filter.',
26
+ },
27
+ time_range_start: {
28
+ type: 'string',
29
+ description: 'Start datetime for due date filtering (ISO 8601). If provided, time_range_end is REQUIRED. Both dates together form a complete filter.',
30
+ },
31
+ time_range_end: {
32
+ type: 'string',
33
+ description: 'End datetime for due date filtering (ISO 8601). If provided, time_range_start is REQUIRED. Both dates together form a complete filter.',
34
+ },
35
+ },
36
+ required: [],
37
+ },
38
+ handler: async (args) => {
39
+ const validated = validateInput(todoQuerySchema, args);
40
+ const client = tsdavManager.getCalDavClient();
41
+ const calendars = await client.fetchCalendars();
42
+
43
+ // If specific calendar requested, use it
44
+ let calendarsToSearch = calendars;
45
+ if (validated.calendar_url) {
46
+ const calendar = calendars.find(c => c.url === validated.calendar_url);
47
+ if (!calendar) {
48
+ const availableUrls = calendars.map(c => c.url).join('\n- ');
49
+ throw new Error(
50
+ `Calendar not found: ${validated.calendar_url}\n\n` +
51
+ `Available calendar URLs:\n- ${availableUrls}\n\n` +
52
+ `Tip: Omit calendar_url to search across all calendars automatically.`
53
+ );
54
+ }
55
+ calendarsToSearch = [calendar];
56
+ }
57
+
58
+ // Fetch todos from all selected calendars
59
+ let todos = [];
60
+ for (const calendar of calendarsToSearch) {
61
+ const calendarTodos = await client.fetchTodos({ calendar });
62
+ // Add calendar info to each todo
63
+ calendarTodos.forEach(todo => {
64
+ todo._calendarName = calendar.displayName || calendar.url;
65
+ });
66
+ todos = todos.concat(calendarTodos);
67
+ }
68
+
69
+ // Client-side filtering (tsdav doesn't support server-side VTODO filtering yet)
70
+ if (validated.summary_filter) {
71
+ const summaryLower = validated.summary_filter.toLowerCase();
72
+ todos = todos.filter(todo => {
73
+ const summary = todo.data?.match(/SUMMARY:(.+)/)?.[1] || '';
74
+ return summary.toLowerCase().includes(summaryLower);
75
+ });
76
+ }
77
+
78
+ if (validated.status_filter) {
79
+ todos = todos.filter(todo => {
80
+ const status = todo.data?.match(/STATUS:(.+)/)?.[1] || 'NEEDS-ACTION';
81
+ return status === validated.status_filter;
82
+ });
83
+ }
84
+
85
+ if (validated.time_range_start && validated.time_range_end) {
86
+ const startTime = new Date(validated.time_range_start).getTime();
87
+ const endTime = new Date(validated.time_range_end).getTime();
88
+
89
+ todos = todos.filter(todo => {
90
+ const dueMatch = todo.data?.match(/DUE:(\d{8}T\d{6}Z?)/);
91
+ if (!dueMatch) return false;
92
+
93
+ const dueStr = dueMatch[1];
94
+ const year = parseInt(dueStr.substr(0, 4));
95
+ const month = parseInt(dueStr.substr(4, 2)) - 1;
96
+ const day = parseInt(dueStr.substr(6, 2));
97
+ const hour = parseInt(dueStr.substr(9, 2));
98
+ const minute = parseInt(dueStr.substr(11, 2));
99
+ const dueTime = new Date(Date.UTC(year, month, day, hour, minute)).getTime();
100
+
101
+ return dueTime >= startTime && dueTime <= endTime;
102
+ });
103
+ }
104
+
105
+ // Determine calendar name for display
106
+ const calendarName = calendarsToSearch.length === 1
107
+ ? (calendarsToSearch[0].displayName || calendarsToSearch[0].url)
108
+ : `All Calendars (${calendarsToSearch.length})`;
109
+
110
+ return formatTodoList(todos, calendarName);
111
+ },
112
+ };
@@ -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 todo updates
9
+ * Supports all RFC 5545 VTODO properties via tsdav-utils
10
+ * Common fields: SUMMARY, DESCRIPTION, STATUS, PRIORITY, DUE, PERCENT-COMPLETE
11
+ * Custom properties: Any X-* property
12
+ */
13
+ const updateTodoFieldsSchema = z.object({
14
+ todo_url: z.string().url('Todo URL must be a valid URL'),
15
+ todo_etag: z.string().min(1, 'Todo etag is required'),
16
+ fields: z.record(z.string()).optional()
17
+ });
18
+
19
+ /**
20
+ * Field-agnostic todo update tool powered by tsdav-utils
21
+ * Supports all RFC 5545 VTODO properties without validation
22
+ *
23
+ * Features:
24
+ * - Any standard VTODO property (SUMMARY, DESCRIPTION, STATUS, PRIORITY, DUE, etc.)
25
+ * - Custom X-* properties for extensions
26
+ * - Field-agnostic: no pre-defined field list required
27
+ */
28
+ export const updateTodoFields = {
29
+ name: 'update_todo',
30
+ description: 'PREFERRED: Update todo fields without iCal formatting. Supports: SUMMARY (title), DESCRIPTION (details), STATUS (NEEDS-ACTION/IN-PROCESS/COMPLETED/CANCELLED), PRIORITY (0-9), DUE (due date), PERCENT-COMPLETE (0-100), and any RFC 5545 VTODO property including custom X-* properties.',
31
+ inputSchema: {
32
+ type: 'object',
33
+ properties: {
34
+ todo_url: {
35
+ type: 'string',
36
+ description: 'The URL of the todo to update'
37
+ },
38
+ todo_etag: {
39
+ type: 'string',
40
+ description: 'The etag of the todo (required for conflict detection)'
41
+ },
42
+ fields: {
43
+ type: 'object',
44
+ description: 'Fields to update - use UPPERCASE property names (e.g., SUMMARY, STATUS, PRIORITY). Any RFC 5545 VTODO property or custom X-* property is supported.',
45
+ additionalProperties: {
46
+ type: 'string'
47
+ },
48
+ properties: {
49
+ SUMMARY: {
50
+ type: 'string',
51
+ description: 'Todo title/summary'
52
+ },
53
+ DESCRIPTION: {
54
+ type: 'string',
55
+ description: 'Todo description/details'
56
+ },
57
+ STATUS: {
58
+ type: 'string',
59
+ description: 'Todo status: NEEDS-ACTION, IN-PROCESS, COMPLETED, or CANCELLED'
60
+ },
61
+ PRIORITY: {
62
+ type: 'string',
63
+ description: 'Priority level: 0 (undefined), 1 (highest) to 9 (lowest)'
64
+ },
65
+ DUE: {
66
+ type: 'string',
67
+ description: 'Due date (ISO 8601 or iCal format: 20250128T100000Z)'
68
+ },
69
+ 'PERCENT-COMPLETE': {
70
+ type: 'string',
71
+ description: 'Completion percentage: 0-100'
72
+ }
73
+ }
74
+ }
75
+ },
76
+ required: ['todo_url', 'todo_etag']
77
+ },
78
+ handler: async (args) => {
79
+ try {
80
+ const validated = validateInput(updateTodoFieldsSchema, args);
81
+ const client = tsdavManager.getCalDavClient();
82
+
83
+ // Step 1: Fetch the current todo from server
84
+ const calendarUrl = validated.todo_url.substring(0, validated.todo_url.lastIndexOf('/') + 1);
85
+ const currentTodos = await client.fetchTodos({
86
+ calendar: { url: calendarUrl },
87
+ objectUrls: [validated.todo_url]
88
+ });
89
+
90
+ if (!currentTodos || currentTodos.length === 0) {
91
+ throw new Error('Todo not found');
92
+ }
93
+
94
+ const todoObject = currentTodos[0];
95
+
96
+ // Step 2: Update fields using tsdav-utils (field-agnostic)
97
+ // Accepts any RFC 5545 VTODO property name (UPPERCASE)
98
+ const updatedData = updateFields(todoObject, validated.fields || {});
99
+
100
+ // Step 3: Send the updated todo back to server
101
+ const updateResponse = await client.updateTodo({
102
+ todo: {
103
+ url: validated.todo_url,
104
+ data: updatedData,
105
+ etag: validated.todo_etag
106
+ }
107
+ });
108
+
109
+ return formatSuccess('Todo 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_todo', error);
117
+ }
118
+ }
119
+ };
@@ -0,0 +1,46 @@
1
+ import { tsdavManager } from '../../tsdav-client.js';
2
+ import { validateInput, updateTodoSchema } from '../../validation.js';
3
+ import { formatSuccess } from '../../formatters.js';
4
+
5
+ /**
6
+ * Update an existing todo/task with raw VTODO iCal data
7
+ */
8
+ export const updateTodoRaw = {
9
+ name: 'update_todo_raw',
10
+ description: 'ADVANCED: Update todo with raw VTODO iCal data. Requires manual iCal formatting - use update_todo instead for simple field updates (summary, description, status). Only use this if you have complete pre-formatted VTODO data or need to update advanced iCal properties.',
11
+ inputSchema: {
12
+ type: 'object',
13
+ properties: {
14
+ todo_url: {
15
+ type: 'string',
16
+ description: 'The URL of the todo to update',
17
+ },
18
+ todo_etag: {
19
+ type: 'string',
20
+ description: 'The current ETag of the todo (required for conflict detection)',
21
+ },
22
+ updated_ical_data: {
23
+ type: 'string',
24
+ description: 'Complete updated VTODO iCalendar data',
25
+ },
26
+ },
27
+ required: ['todo_url', 'todo_etag', 'updated_ical_data'],
28
+ },
29
+ handler: async (args) => {
30
+ const validated = validateInput(updateTodoSchema, args);
31
+ const client = tsdavManager.getCalDavClient();
32
+
33
+ const result = await client.updateTodo({
34
+ todo: {
35
+ url: validated.todo_url,
36
+ data: validated.updated_ical_data,
37
+ etag: validated.todo_etag,
38
+ },
39
+ });
40
+
41
+ return formatSuccess('Todo updated successfully', {
42
+ url: result.url,
43
+ etag: result.etag,
44
+ });
45
+ },
46
+ };
@@ -0,0 +1,199 @@
1
+ import { DAVClient } from 'tsdav';
2
+ import { logger } from './logger.js';
3
+ import { CalDAVError, CardDAVError } from './error-handler.js';
4
+
5
+ /**
6
+ * Singleton CalDAV/CardDAV Client Manager
7
+ *
8
+ * Supports both Basic Auth and OAuth2 authentication:
9
+ * - Basic Auth: Standard CalDAV servers (Radicale, Baikal, Nextcloud)
10
+ * - OAuth2: Google Calendar and other OAuth2-enabled CalDAV servers
11
+ */
12
+ class TsdavClientManager {
13
+ constructor() {
14
+ this.calDavClient = null;
15
+ this.cardDavClient = null;
16
+ this.config = null;
17
+ this.authMethod = null;
18
+ }
19
+
20
+ /**
21
+ * Initialize clients with configuration
22
+ *
23
+ * @param {Object} config - Client configuration
24
+ * @param {string} config.serverUrl - CalDAV/CardDAV server URL
25
+ * @param {string} config.authMethod - 'Basic' or 'OAuth' (note: tsdav uses 'Oauth')
26
+ *
27
+ * For Basic Auth:
28
+ * @param {string} config.username - Username
29
+ * @param {string} config.password - Password
30
+ *
31
+ * For OAuth2:
32
+ * @param {string} config.username - User email (for OAuth2)
33
+ * @param {string} config.clientId - OAuth2 client ID
34
+ * @param {string} config.clientSecret - OAuth2 client secret
35
+ * @param {string} config.refreshToken - OAuth2 refresh token
36
+ * @param {string} config.tokenUrl - OAuth2 token endpoint (default: Google's)
37
+ */
38
+ async initialize(config) {
39
+ this.config = config;
40
+ this.authMethod = config.authMethod || 'Basic';
41
+
42
+ try {
43
+ // Determine authentication method
44
+ const useOAuth = this.authMethod === 'OAuth' || this.authMethod === 'Oauth';
45
+
46
+ if (useOAuth) {
47
+ logger.info({ serverUrl: config.serverUrl }, 'Initializing tsdav clients with OAuth2');
48
+ await this._initializeOAuth(config);
49
+ } else {
50
+ logger.info({ serverUrl: config.serverUrl }, 'Initializing tsdav clients with Basic Auth');
51
+ await this._initializeBasicAuth(config);
52
+ }
53
+
54
+ logger.info({
55
+ serverUrl: config.serverUrl,
56
+ authMethod: this.authMethod
57
+ }, 'tsdav clients initialized and logged in');
58
+ } catch (error) {
59
+ logger.error({
60
+ error: error.message,
61
+ serverUrl: config.serverUrl,
62
+ authMethod: this.authMethod
63
+ }, 'Failed to initialize tsdav clients');
64
+ throw error;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Initialize clients with Basic Authentication
70
+ * @private
71
+ */
72
+ async _initializeBasicAuth(config) {
73
+ // Validate required fields
74
+ if (!config.username || !config.password) {
75
+ throw new Error('Basic Auth requires username and password');
76
+ }
77
+
78
+ // CalDAV Client
79
+ this.calDavClient = new DAVClient({
80
+ serverUrl: config.serverUrl,
81
+ credentials: {
82
+ username: config.username,
83
+ password: config.password,
84
+ },
85
+ authMethod: 'Basic',
86
+ defaultAccountType: 'caldav',
87
+ });
88
+
89
+ // CardDAV Client
90
+ this.cardDavClient = new DAVClient({
91
+ serverUrl: config.serverUrl,
92
+ credentials: {
93
+ username: config.username,
94
+ password: config.password,
95
+ },
96
+ authMethod: 'Basic',
97
+ defaultAccountType: 'carddav',
98
+ });
99
+
100
+ // Login to both clients
101
+ await this.calDavClient.login();
102
+ logger.debug({ accountType: 'caldav' }, 'CalDAV client logged in (Basic Auth)');
103
+
104
+ await this.cardDavClient.login();
105
+ logger.debug({ accountType: 'carddav' }, 'CardDAV client logged in (Basic Auth)');
106
+ }
107
+
108
+ /**
109
+ * Initialize clients with OAuth2 Authentication
110
+ * @private
111
+ */
112
+ async _initializeOAuth(config) {
113
+ // Validate required OAuth fields
114
+ if (!config.username) {
115
+ throw new Error('OAuth requires username (user email)');
116
+ }
117
+ if (!config.clientId || !config.clientSecret || !config.refreshToken) {
118
+ throw new Error('OAuth requires clientId, clientSecret, and refreshToken');
119
+ }
120
+
121
+ // Default to Google's token endpoint if not specified
122
+ const tokenUrl = config.tokenUrl || 'https://accounts.google.com/o/oauth2/token';
123
+
124
+ const oauthCredentials = {
125
+ tokenUrl,
126
+ username: config.username,
127
+ refreshToken: config.refreshToken,
128
+ clientId: config.clientId,
129
+ clientSecret: config.clientSecret,
130
+ };
131
+
132
+ logger.debug({
133
+ username: config.username,
134
+ tokenUrl,
135
+ serverUrl: config.serverUrl
136
+ }, 'Configuring OAuth2 credentials');
137
+
138
+ // CalDAV Client with OAuth
139
+ this.calDavClient = new DAVClient({
140
+ serverUrl: config.serverUrl,
141
+ credentials: oauthCredentials,
142
+ authMethod: 'Oauth', // Note: tsdav expects 'Oauth' with capital O
143
+ defaultAccountType: 'caldav',
144
+ });
145
+
146
+ // CardDAV Client with OAuth
147
+ // Note: Google Calendar doesn't support CardDAV, but we initialize it anyway
148
+ // for compatibility with other OAuth2 CalDAV/CardDAV servers
149
+ this.cardDavClient = new DAVClient({
150
+ serverUrl: config.serverUrl,
151
+ credentials: oauthCredentials,
152
+ authMethod: 'Oauth',
153
+ defaultAccountType: 'carddav',
154
+ });
155
+
156
+ // Login to CalDAV client
157
+ await this.calDavClient.login();
158
+ logger.debug({ accountType: 'caldav' }, 'CalDAV client logged in (OAuth2)');
159
+
160
+ // Try to login to CardDAV client, but don't fail if it doesn't work
161
+ // (Google Calendar doesn't support CardDAV)
162
+ try {
163
+ await this.cardDavClient.login();
164
+ logger.debug({ accountType: 'carddav' }, 'CardDAV client logged in (OAuth2)');
165
+ } catch (error) {
166
+ logger.warn({
167
+ error: error.message
168
+ }, 'CardDAV login failed (expected for Google Calendar)');
169
+ // Don't throw - CardDAV is optional for OAuth2 providers like Google
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Get CalDAV client
175
+ */
176
+ getCalDavClient() {
177
+ if (!this.calDavClient) {
178
+ const error = new CalDAVError('CalDAV client not initialized. Call initialize() first.');
179
+ logger.error('CalDAV client not initialized');
180
+ throw error;
181
+ }
182
+ return this.calDavClient;
183
+ }
184
+
185
+ /**
186
+ * Get CardDAV client
187
+ */
188
+ getCardDavClient() {
189
+ if (!this.cardDavClient) {
190
+ const error = new CardDAVError('CardDAV client not initialized. Call initialize() first.');
191
+ logger.error('CardDAV client not initialized');
192
+ throw error;
193
+ }
194
+ return this.cardDavClient;
195
+ }
196
+ }
197
+
198
+ // Export singleton instance
199
+ export const tsdavManager = new TsdavClientManager();