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,225 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * dav-mcp STDIO Server
4
+ *
5
+ * Entry point for local MCP clients: Claude Desktop, Cursor, VS Code, npx usage
6
+ *
7
+ * CRITICAL: This server uses STDIO transport. All logging MUST go to stderr
8
+ * to avoid corrupting JSON-RPC messages on stdout.
9
+ *
10
+ * Usage:
11
+ * MCP_TRANSPORT=stdio node src/server-stdio.js
12
+ *
13
+ * Or via npx (after npm publish):
14
+ * npx dav-mcp
15
+ *
16
+ * Configuration via environment variables:
17
+ * - CALDAV_SERVER_URL: CalDAV server URL
18
+ * - CALDAV_USERNAME: Username for Basic Auth
19
+ * - CALDAV_PASSWORD: Password for Basic Auth
20
+ * - AUTH_METHOD: 'Basic' (default) or 'OAuth'
21
+ *
22
+ * For OAuth2 (Google Calendar):
23
+ * - GOOGLE_SERVER_URL: Google CalDAV URL
24
+ * - GOOGLE_USER: Google account email
25
+ * - GOOGLE_CLIENT_ID: OAuth2 client ID
26
+ * - GOOGLE_CLIENT_SECRET: OAuth2 client secret
27
+ * - GOOGLE_REFRESH_TOKEN: OAuth2 refresh token
28
+ */
29
+
30
+ // Set STDIO mode BEFORE importing logger
31
+ process.env.MCP_TRANSPORT = 'stdio';
32
+
33
+ import dotenv from 'dotenv';
34
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
35
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
36
+ import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
37
+
38
+ import { tsdavManager } from './tsdav-client.js';
39
+ import { tools } from './tools/index.js';
40
+ import { createToolErrorResponse, MCP_ERROR_CODES } from './error-handler.js';
41
+ import { logger } from './logger.js';
42
+ import { initializeToolCallLogger, getToolCallLogger } from './tool-call-logger.js';
43
+
44
+ // Load environment variables
45
+ dotenv.config();
46
+
47
+ /**
48
+ * Initialize tsdav clients based on auth method
49
+ */
50
+ async function initializeTsdav() {
51
+ const authMethod = process.env.AUTH_METHOD || 'Basic';
52
+
53
+ if (authMethod === 'OAuth' || authMethod === 'Oauth') {
54
+ // OAuth2 Configuration (e.g., Google Calendar)
55
+ logger.info('Initializing with OAuth2 authentication');
56
+
57
+ if (!process.env.GOOGLE_CLIENT_ID || !process.env.GOOGLE_CLIENT_SECRET || !process.env.GOOGLE_REFRESH_TOKEN) {
58
+ throw new Error('OAuth2 requires GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and GOOGLE_REFRESH_TOKEN');
59
+ }
60
+
61
+ await tsdavManager.initialize({
62
+ serverUrl: process.env.GOOGLE_SERVER_URL || 'https://apidata.googleusercontent.com/caldav/v2/',
63
+ authMethod: 'OAuth',
64
+ username: process.env.GOOGLE_USER,
65
+ clientId: process.env.GOOGLE_CLIENT_ID,
66
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
67
+ refreshToken: process.env.GOOGLE_REFRESH_TOKEN,
68
+ tokenUrl: process.env.GOOGLE_TOKEN_URL || 'https://accounts.google.com/o/oauth2/token',
69
+ });
70
+
71
+ logger.info('OAuth2 clients initialized successfully');
72
+ } else {
73
+ // Basic Auth Configuration (standard CalDAV servers)
74
+ logger.info('Initializing with Basic authentication');
75
+
76
+ if (!process.env.CALDAV_SERVER_URL || !process.env.CALDAV_USERNAME || !process.env.CALDAV_PASSWORD) {
77
+ throw new Error('Basic Auth requires CALDAV_SERVER_URL, CALDAV_USERNAME, and CALDAV_PASSWORD');
78
+ }
79
+
80
+ await tsdavManager.initialize({
81
+ serverUrl: process.env.CALDAV_SERVER_URL,
82
+ authMethod: 'Basic',
83
+ username: process.env.CALDAV_USERNAME,
84
+ password: process.env.CALDAV_PASSWORD,
85
+ });
86
+
87
+ logger.info('Basic Auth clients initialized successfully');
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Create MCP Server with tool handlers
93
+ */
94
+ function createMCPServer() {
95
+ const server = new Server(
96
+ {
97
+ name: process.env.MCP_SERVER_NAME || 'dav-mcp',
98
+ version: process.env.MCP_SERVER_VERSION || '3.0.0',
99
+ },
100
+ {
101
+ capabilities: {
102
+ tools: {},
103
+ },
104
+ }
105
+ );
106
+
107
+ // Register tools/list handler
108
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
109
+ logger.debug({ count: tools.length }, 'tools/list request received');
110
+ return {
111
+ tools: tools.map(t => ({
112
+ name: t.name,
113
+ description: t.description,
114
+ inputSchema: t.inputSchema,
115
+ })),
116
+ };
117
+ });
118
+
119
+ // Register tools/call handler
120
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
121
+ const toolName = request.params.name;
122
+ const args = request.params.arguments || {};
123
+ const toolCallLogger = getToolCallLogger();
124
+
125
+ logger.info({ tool: toolName }, 'tools/call request received');
126
+
127
+ const tool = tools.find(t => t.name === toolName);
128
+ if (!tool) {
129
+ logger.error({ tool: toolName }, 'Tool not found');
130
+ const error = new Error(`Unknown tool: ${toolName}`);
131
+ error.code = MCP_ERROR_CODES.METHOD_NOT_FOUND;
132
+ throw error;
133
+ }
134
+
135
+ const startTime = Date.now();
136
+ toolCallLogger.logToolCallStart(toolName, args, { transport: 'stdio' });
137
+
138
+ try {
139
+ logger.debug({ tool: toolName }, 'Executing tool');
140
+ const result = await tool.handler(args);
141
+ const duration = Date.now() - startTime;
142
+
143
+ logger.info({ tool: toolName, duration }, 'Tool executed successfully');
144
+ toolCallLogger.logToolCallSuccess(toolName, args, result, {
145
+ transport: 'stdio',
146
+ duration,
147
+ });
148
+
149
+ return result;
150
+ } catch (error) {
151
+ const duration = Date.now() - startTime;
152
+
153
+ logger.error({ tool: toolName, error: error.message }, 'Tool execution error');
154
+ toolCallLogger.logToolCallError(toolName, args, error, {
155
+ transport: 'stdio',
156
+ duration,
157
+ });
158
+
159
+ return createToolErrorResponse(error, process.env.NODE_ENV === 'development');
160
+ }
161
+ });
162
+
163
+ return server;
164
+ }
165
+
166
+ /**
167
+ * Main entry point
168
+ */
169
+ async function main() {
170
+ try {
171
+ logger.info('Starting dav-mcp STDIO server...');
172
+
173
+ // Initialize tsdav clients
174
+ await initializeTsdav();
175
+
176
+ // Initialize tool call logger
177
+ initializeToolCallLogger();
178
+ logger.info('Tool call logger initialized');
179
+
180
+ // Create MCP server
181
+ const server = createMCPServer();
182
+ logger.debug({ count: tools.length }, 'MCP server created with tools');
183
+
184
+ // Create STDIO transport
185
+ const transport = new StdioServerTransport();
186
+
187
+ // Connect server to transport
188
+ await server.connect(transport);
189
+
190
+ logger.info({
191
+ name: process.env.MCP_SERVER_NAME || 'dav-mcp',
192
+ version: process.env.MCP_SERVER_VERSION || '3.0.0',
193
+ tools: tools.length,
194
+ }, 'dav-mcp STDIO server ready');
195
+
196
+ } catch (error) {
197
+ logger.error({ error: error.message, stack: error.stack }, 'Fatal error starting server');
198
+ process.exit(1);
199
+ }
200
+ }
201
+
202
+ // Graceful shutdown handlers
203
+ process.on('SIGTERM', () => {
204
+ logger.info('Received SIGTERM, shutting down...');
205
+ process.exit(0);
206
+ });
207
+
208
+ process.on('SIGINT', () => {
209
+ logger.info('Received SIGINT, shutting down...');
210
+ process.exit(0);
211
+ });
212
+
213
+ // Handle uncaught errors gracefully
214
+ process.on('uncaughtException', (error) => {
215
+ logger.error({ error: error.message, stack: error.stack }, 'Uncaught exception');
216
+ process.exit(1);
217
+ });
218
+
219
+ process.on('unhandledRejection', (reason) => {
220
+ logger.error({ reason }, 'Unhandled promise rejection');
221
+ process.exit(1);
222
+ });
223
+
224
+ // Start the server
225
+ main();
@@ -0,0 +1,148 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ /**
5
+ * Tool Call Logger - Logs all MCP tool calls to JSON Lines format
6
+ * Enables real-time monitoring of LLM tool selection behavior
7
+ */
8
+
9
+ class ToolCallLogger {
10
+ constructor(options = {}) {
11
+ this.enabled = options.enabled !== false;
12
+ this.outputMode = options.outputMode || 'file'; // 'file' | 'console' | 'both'
13
+ this.logFile = options.logFile || '/tmp/mcp-tool-calls.jsonl';
14
+
15
+ if (this.enabled && this.outputMode.includes('file')) {
16
+ this.ensureLogFileExists();
17
+ }
18
+ }
19
+
20
+ ensureLogFileExists() {
21
+ const dir = path.dirname(this.logFile);
22
+ if (!fs.existsSync(dir)) {
23
+ fs.mkdirSync(dir, { recursive: true });
24
+ }
25
+ }
26
+
27
+ log(entry) {
28
+ if (!this.enabled) return;
29
+
30
+ const line = JSON.stringify(entry) + '\n';
31
+
32
+ if (this.outputMode === 'console' || this.outputMode === 'both') {
33
+ console.log(line.trim());
34
+ }
35
+
36
+ if (this.outputMode === 'file' || this.outputMode === 'both') {
37
+ try {
38
+ fs.appendFileSync(this.logFile, line);
39
+ } catch (error) {
40
+ console.error('Failed to write to tool call log:', error.message);
41
+ }
42
+ }
43
+ }
44
+
45
+ logToolCallStart(toolName, args, metadata = {}) {
46
+ this.log({
47
+ type: 'tool_call_start',
48
+ timestamp: new Date().toISOString(),
49
+ tool: toolName,
50
+ args: args,
51
+ ...metadata
52
+ });
53
+ }
54
+
55
+ logToolCallSuccess(toolName, args, result, metadata = {}) {
56
+ // Summarize result to avoid huge logs
57
+ const resultSummary = this.summarizeResult(result);
58
+
59
+ this.log({
60
+ type: 'tool_call_success',
61
+ timestamp: new Date().toISOString(),
62
+ tool: toolName,
63
+ args: args,
64
+ result_summary: resultSummary,
65
+ duration_ms: metadata.duration,
66
+ ...metadata
67
+ });
68
+ }
69
+
70
+ logToolCallError(toolName, args, error, metadata = {}) {
71
+ this.log({
72
+ type: 'tool_call_error',
73
+ timestamp: new Date().toISOString(),
74
+ tool: toolName,
75
+ args: args,
76
+ error: {
77
+ message: error.message,
78
+ code: error.code,
79
+ stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
80
+ },
81
+ duration_ms: metadata.duration,
82
+ ...metadata
83
+ });
84
+ }
85
+
86
+ summarizeResult(result) {
87
+ if (!result) return null;
88
+
89
+ // Handle MCP result format
90
+ if (result.content && Array.isArray(result.content)) {
91
+ return {
92
+ type: 'mcp_result',
93
+ content_count: result.content.length,
94
+ content_types: result.content.map(c => c.type),
95
+ has_text: result.content.some(c => c.type === 'text'),
96
+ text_length: result.content
97
+ .filter(c => c.type === 'text')
98
+ .reduce((sum, c) => sum + (c.text?.length || 0), 0)
99
+ };
100
+ }
101
+
102
+ // Handle plain objects/strings
103
+ if (typeof result === 'string') {
104
+ return { type: 'string', length: result.length };
105
+ }
106
+
107
+ if (typeof result === 'object') {
108
+ return {
109
+ type: 'object',
110
+ keys: Object.keys(result).slice(0, 5),
111
+ key_count: Object.keys(result).length
112
+ };
113
+ }
114
+
115
+ return { type: typeof result };
116
+ }
117
+
118
+ clearLog() {
119
+ if (this.enabled && fs.existsSync(this.logFile)) {
120
+ fs.unlinkSync(this.logFile);
121
+ }
122
+ }
123
+ }
124
+
125
+ // Singleton instance
126
+ let instance = null;
127
+
128
+ export function initializeToolCallLogger(options = {}) {
129
+ const enabled = process.env.LOG_TOOL_CALLS !== 'false';
130
+ const outputMode = process.env.TOOL_CALL_LOG_MODE || 'file';
131
+ const logFile = process.env.TOOL_CALL_LOG_FILE || '/tmp/mcp-tool-calls.jsonl';
132
+
133
+ instance = new ToolCallLogger({
134
+ enabled,
135
+ outputMode,
136
+ logFile,
137
+ ...options
138
+ });
139
+
140
+ return instance;
141
+ }
142
+
143
+ export function getToolCallLogger() {
144
+ if (!instance) {
145
+ instance = initializeToolCallLogger();
146
+ }
147
+ return instance;
148
+ }
@@ -0,0 +1,38 @@
1
+ import { tsdavManager } from '../../tsdav-client.js';
2
+ import { validateInput, calendarMultiGetSchema } from '../../validation.js';
3
+ import { formatEventList } from '../../formatters.js';
4
+
5
+ /**
6
+ * Batch fetch multiple specific calendar events by their URLs
7
+ */
8
+ export const calendarMultiGet = {
9
+ name: 'calendar_multi_get',
10
+ description: 'Batch fetch multiple specific calendar events by their URLs. Use when you have exact event URLs and want to retrieve their details',
11
+ inputSchema: {
12
+ type: 'object',
13
+ properties: {
14
+ calendar_url: {
15
+ type: 'string',
16
+ description: 'The URL of the calendar',
17
+ },
18
+ event_urls: {
19
+ type: 'array',
20
+ items: { type: 'string' },
21
+ description: 'Array of event URLs to fetch',
22
+ },
23
+ },
24
+ required: ['calendar_url', 'event_urls'],
25
+ },
26
+ handler: async (args) => {
27
+ const validated = validateInput(calendarMultiGetSchema, args);
28
+ const client = tsdavManager.getCalDavClient();
29
+
30
+ const events = await client.calendarMultiGet({
31
+ url: validated.calendar_url,
32
+ props: [{ name: 'getetag', namespace: 'DAV:' }, 'calendar-data'],
33
+ objectUrls: validated.event_urls,
34
+ });
35
+
36
+ return formatEventList(events, { url: validated.calendar_url });
37
+ },
38
+ };
@@ -0,0 +1,98 @@
1
+ import { tsdavManager } from '../../tsdav-client.js';
2
+ import { validateInput, calendarQuerySchema } from '../../validation.js';
3
+ import { formatEventList } from '../../formatters.js';
4
+ import { buildTimeRangeOptions } from '../shared/helpers.js';
5
+
6
+ /**
7
+ * Search and filter calendar events efficiently
8
+ */
9
+ export const calendarQuery = {
10
+ name: 'calendar_query',
11
+ description: '⭐ PREFERRED: Search and filter calendar events efficiently. Use instead of list_events to avoid loading thousands of entries. Omit calendar_url to search across ALL calendars automatically.',
12
+ inputSchema: {
13
+ type: 'object',
14
+ properties: {
15
+ calendar_url: {
16
+ type: 'string',
17
+ description: 'Optional: Specific calendar URL. Omit to search ALL calendars (recommended for "find events with X" queries). Only provide if user explicitly names a calendar. DO NOT use list_calendars first - that defeats cross-calendar search.',
18
+ },
19
+ time_range_start: {
20
+ type: 'string',
21
+ description: 'Start datetime (ISO 8601, e.g., 2025-10-30T00:00:00Z). If provided, time_range_end is REQUIRED. Calculate dates for "today", "this week", etc. Can be used alone (with end date) as sufficient filter.',
22
+ },
23
+ time_range_end: {
24
+ type: 'string',
25
+ description: 'End datetime (ISO 8601). If provided, time_range_start is REQUIRED. Both dates together form a complete filter. Do not omit if start is provided.',
26
+ },
27
+ summary_filter: {
28
+ type: 'string',
29
+ description: 'Search event titles/summaries containing this text (case-insensitive). Example: "meeting with Elena" or "standup". Can be used alone as sufficient filter.',
30
+ },
31
+ location_filter: {
32
+ type: 'string',
33
+ description: 'Search event locations containing this text. Example: "Berlin", "Office", "Zoom". Can be used alone as sufficient filter.',
34
+ },
35
+ },
36
+ required: [],
37
+ },
38
+ handler: async (args) => {
39
+ const validated = validateInput(calendarQuerySchema, 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
+ // Build timeRange options
59
+ const timeRangeOptions = buildTimeRangeOptions(validated.time_range_start, validated.time_range_end);
60
+
61
+ // Search across all selected calendars
62
+ let allEvents = [];
63
+ for (const calendar of calendarsToSearch) {
64
+ const options = { calendar, ...timeRangeOptions };
65
+ const events = await client.fetchCalendarObjects(options);
66
+ // Add calendar info to each event
67
+ events.forEach(event => {
68
+ event._calendarName = calendar.displayName || calendar.url;
69
+ });
70
+ allEvents = allEvents.concat(events);
71
+ }
72
+
73
+ let filteredEvents = allEvents;
74
+
75
+ if (validated.summary_filter) {
76
+ const summaryLower = validated.summary_filter.toLowerCase();
77
+ filteredEvents = filteredEvents.filter(event => {
78
+ const summary = event.data?.match(/SUMMARY:(.+)/)?.[1] || '';
79
+ return summary.toLowerCase().includes(summaryLower);
80
+ });
81
+ }
82
+
83
+ if (validated.location_filter) {
84
+ const locationLower = validated.location_filter.toLowerCase();
85
+ filteredEvents = filteredEvents.filter(event => {
86
+ const location = event.data?.match(/LOCATION:(.+)/)?.[1] || '';
87
+ return location.toLowerCase().includes(locationLower);
88
+ });
89
+ }
90
+
91
+ // Determine calendar name for display
92
+ const calendarName = calendarsToSearch.length === 1
93
+ ? (calendarsToSearch[0].displayName || calendarsToSearch[0].url)
94
+ : `All Calendars (${calendarsToSearch.length})`;
95
+
96
+ return formatEventList(filteredEvents, calendarName);
97
+ },
98
+ };
@@ -0,0 +1,79 @@
1
+ import { tsdavManager } from '../../tsdav-client.js';
2
+ import { validateInput, createEventSchema, sanitizeICalString } from '../../validation.js';
3
+ import { formatSuccess } from '../../formatters.js';
4
+ import { formatICalDate, generateUID, findCalendarOrThrow } from '../shared/helpers.js';
5
+
6
+ /**
7
+ * Create a new calendar event
8
+ */
9
+ export const createEvent = {
10
+ name: 'create_event',
11
+ description: 'Create a new calendar event with title, date, time, optional description and location',
12
+ inputSchema: {
13
+ type: 'object',
14
+ properties: {
15
+ calendar_url: {
16
+ type: 'string',
17
+ description: 'The URL of the calendar to create the event in',
18
+ },
19
+ summary: {
20
+ type: 'string',
21
+ description: 'Event title/summary',
22
+ },
23
+ start_date: {
24
+ type: 'string',
25
+ description: 'Start date in ISO 8601 format',
26
+ },
27
+ end_date: {
28
+ type: 'string',
29
+ description: 'End date in ISO 8601 format',
30
+ },
31
+ description: {
32
+ type: 'string',
33
+ description: 'Event description (optional)',
34
+ },
35
+ location: {
36
+ type: 'string',
37
+ description: 'Event location (optional)',
38
+ },
39
+ },
40
+ required: ['calendar_url', 'summary', 'start_date', 'end_date'],
41
+ },
42
+ handler: async (args) => {
43
+ const validated = validateInput(createEventSchema, args);
44
+ const client = tsdavManager.getCalDavClient();
45
+ const calendars = await client.fetchCalendars();
46
+ const calendar = findCalendarOrThrow(calendars, validated.calendar_url);
47
+
48
+ const now = new Date();
49
+ const uid = generateUID('event');
50
+
51
+ const summary = sanitizeICalString(validated.summary);
52
+ const description = validated.description ? sanitizeICalString(validated.description) : '';
53
+ const location = validated.location ? sanitizeICalString(validated.location) : '';
54
+
55
+ const iCalString = `BEGIN:VCALENDAR
56
+ VERSION:2.0
57
+ PRODID:-//tsdav-mcp-server//EN
58
+ BEGIN:VEVENT
59
+ UID:${uid}
60
+ DTSTAMP:${formatICalDate(now)}
61
+ DTSTART:${formatICalDate(new Date(validated.start_date))}
62
+ DTEND:${formatICalDate(new Date(validated.end_date))}
63
+ SUMMARY:${summary}${description ? `\nDESCRIPTION:${description}` : ''}${location ? `\nLOCATION:${location}` : ''}
64
+ END:VEVENT
65
+ END:VCALENDAR`;
66
+
67
+ const response = await client.createCalendarObject({
68
+ calendar,
69
+ filename: `${uid}.ics`,
70
+ iCalString,
71
+ });
72
+
73
+ return formatSuccess('Event created successfully', {
74
+ url: response.url,
75
+ etag: response.etag,
76
+ summary: validated.summary,
77
+ });
78
+ },
79
+ };
@@ -0,0 +1,36 @@
1
+ import { tsdavManager } from '../../tsdav-client.js';
2
+ import { validateInput, deleteCalendarSchema } from '../../validation.js';
3
+ import { formatCalendarDeleteSuccess } from '../../formatters.js';
4
+
5
+ /**
6
+ * Permanently delete a calendar and all its events
7
+ */
8
+ export const deleteCalendar = {
9
+ name: 'delete_calendar',
10
+ description: 'Permanently delete a calendar and all its events. WARNING: This action cannot be undone! Use this when user explicitly asks to "delete calendar" or "remove calendar"',
11
+ inputSchema: {
12
+ type: 'object',
13
+ properties: {
14
+ calendar_url: {
15
+ type: 'string',
16
+ description: 'The URL of the calendar to delete (get from list_calendars)',
17
+ },
18
+ },
19
+ required: ['calendar_url'],
20
+ },
21
+ handler: async (args) => {
22
+ const validated = validateInput(deleteCalendarSchema, args);
23
+ const client = tsdavManager.getCalDavClient();
24
+
25
+ // Use deleteObject to send DELETE request
26
+ await client.deleteObject({
27
+ url: validated.calendar_url,
28
+ headers: {
29
+ 'Content-Type': 'text/calendar; charset=utf-8',
30
+ },
31
+ });
32
+
33
+ // Return formatted success with warning
34
+ return formatCalendarDeleteSuccess(validated.calendar_url);
35
+ },
36
+ };
@@ -0,0 +1,38 @@
1
+ import { tsdavManager } from '../../tsdav-client.js';
2
+ import { validateInput, deleteEventSchema } from '../../validation.js';
3
+ import { formatSuccess } from '../../formatters.js';
4
+
5
+ /**
6
+ * Delete a calendar event permanently
7
+ */
8
+ export const deleteEvent = {
9
+ name: 'delete_event',
10
+ description: 'Delete a calendar event permanently. Requires event URL and etag',
11
+ inputSchema: {
12
+ type: 'object',
13
+ properties: {
14
+ event_url: {
15
+ type: 'string',
16
+ description: 'The URL of the event to delete',
17
+ },
18
+ event_etag: {
19
+ type: 'string',
20
+ description: 'The etag of the event',
21
+ },
22
+ },
23
+ required: ['event_url', 'event_etag'],
24
+ },
25
+ handler: async (args) => {
26
+ const validated = validateInput(deleteEventSchema, args);
27
+ const client = tsdavManager.getCalDavClient();
28
+
29
+ await client.deleteCalendarObject({
30
+ calendarObject: {
31
+ url: validated.event_url,
32
+ etag: validated.event_etag,
33
+ },
34
+ });
35
+
36
+ return formatSuccess('Event deleted successfully');
37
+ },
38
+ };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Calendar Tools - CalDAV operations
3
+ * Exports all calendar-related tools
4
+ */
5
+
6
+ export { listCalendars } from './list-calendars.js';
7
+ export { listEvents } from './list-events.js';
8
+ export { createEvent } from './create-event.js';
9
+ export { updateEventFields } from './update-event-fields.js';
10
+ export { updateEventRaw } from './update-event-raw.js';
11
+ export { deleteEvent } from './delete-event.js';
12
+ export { calendarQuery } from './calendar-query.js';
13
+ export { makeCalendar } from './make-calendar.js';
14
+ export { updateCalendar } from './update-calendar.js';
15
+ export { deleteCalendar } from './delete-calendar.js';
16
+ export { calendarMultiGet } from './calendar-multi-get.js';