Skip to content

Commit 3b88c59

Browse files
PhilflowIOclaude
andcommitted
feat(v2.2.1): Add calendar management tools (update/delete) + component-set fix
## New Features - `update_calendar` tool - Update calendar properties (name, color, description, timezone) - `delete_calendar` tool - Delete calendars with safety warnings ## Improvements - Fixed `supportedCalendarComponentSet` property format (camelCase, RFC 4791 compliant) - Added Radicale limitation documentation (component restrictions ignored) - 16 new Jest unit tests for calendar management - 7 new MCP integration tests ## Technical Details - Validation schemas for update/delete operations - Formatters: formatCalendarUpdateSuccess, formatCalendarDeleteSuccess - Calendar verification after updates/deletes - Multi-calendar search for operations ## Known Limitations - Radicale ignores component-set restrictions (server limitation, Issue #767) - Works correctly with Nextcloud/Baikal/other CalDAV servers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent e6fb8e2 commit 3b88c59

File tree

6 files changed

+501
-9
lines changed

6 files changed

+501
-9
lines changed
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import { describe, test, expect, beforeEach, jest } from '@jest/globals';
2+
import {
3+
validateInput,
4+
updateCalendarSchema,
5+
deleteCalendarSchema,
6+
} from '../src/validation.js';
7+
import {
8+
formatCalendarUpdateSuccess,
9+
formatCalendarDeleteSuccess,
10+
} from '../src/formatters.js';
11+
12+
describe('Calendar Management', () => {
13+
describe('updateCalendarSchema validation', () => {
14+
test('should accept valid calendar update with display_name', () => {
15+
const input = {
16+
calendar_url: 'https://dav.example.com/calendars/user/test',
17+
display_name: 'New Calendar Name',
18+
};
19+
const result = validateInput(updateCalendarSchema, input);
20+
expect(result.calendar_url).toBe(input.calendar_url);
21+
expect(result.display_name).toBe(input.display_name);
22+
});
23+
24+
test('should accept valid calendar update with color', () => {
25+
const input = {
26+
calendar_url: 'https://dav.example.com/calendars/user/test',
27+
color: '#FF5733',
28+
};
29+
const result = validateInput(updateCalendarSchema, input);
30+
expect(result.color).toBe(input.color);
31+
});
32+
33+
test('should accept valid calendar update with multiple fields', () => {
34+
const input = {
35+
calendar_url: 'https://dav.example.com/calendars/user/test',
36+
display_name: 'Updated Calendar',
37+
description: 'New description',
38+
color: '#00FF00',
39+
timezone: 'Europe/Berlin',
40+
};
41+
const result = validateInput(updateCalendarSchema, input);
42+
expect(result.display_name).toBe(input.display_name);
43+
expect(result.description).toBe(input.description);
44+
expect(result.color).toBe(input.color);
45+
expect(result.timezone).toBe(input.timezone);
46+
});
47+
48+
test('should reject invalid calendar URL', () => {
49+
const input = {
50+
calendar_url: 'not-a-url',
51+
display_name: 'Test',
52+
};
53+
expect(() => validateInput(updateCalendarSchema, input)).toThrow('Invalid calendar URL');
54+
});
55+
56+
test('should reject invalid color format', () => {
57+
const input = {
58+
calendar_url: 'https://dav.example.com/calendars/user/test',
59+
color: 'red',
60+
};
61+
expect(() => validateInput(updateCalendarSchema, input)).toThrow();
62+
});
63+
64+
test('should reject when no update fields provided', () => {
65+
const input = {
66+
calendar_url: 'https://dav.example.com/calendars/user/test',
67+
};
68+
expect(() => validateInput(updateCalendarSchema, input)).toThrow(
69+
'At least one field'
70+
);
71+
});
72+
73+
test('should accept valid hex color with lowercase', () => {
74+
const input = {
75+
calendar_url: 'https://dav.example.com/calendars/user/test',
76+
color: '#ff5733',
77+
};
78+
const result = validateInput(updateCalendarSchema, input);
79+
expect(result.color).toBe('#ff5733');
80+
});
81+
});
82+
83+
describe('deleteCalendarSchema validation', () => {
84+
test('should accept valid calendar URL', () => {
85+
const input = {
86+
calendar_url: 'https://dav.example.com/calendars/user/test',
87+
};
88+
const result = validateInput(deleteCalendarSchema, input);
89+
expect(result.calendar_url).toBe(input.calendar_url);
90+
});
91+
92+
test('should reject invalid calendar URL', () => {
93+
const input = {
94+
calendar_url: 'not-a-url',
95+
};
96+
expect(() => validateInput(deleteCalendarSchema, input)).toThrow('Invalid calendar URL');
97+
});
98+
99+
test('should reject missing calendar_url', () => {
100+
const input = {};
101+
expect(() => validateInput(deleteCalendarSchema, input)).toThrow();
102+
});
103+
});
104+
105+
describe('formatCalendarUpdateSuccess', () => {
106+
test('should format calendar update success with all fields', () => {
107+
const calendar = {
108+
displayName: 'Updated Calendar',
109+
url: 'https://dav.example.com/calendars/user/test',
110+
};
111+
const updatedFields = {
112+
display_name: 'Updated Calendar',
113+
description: 'New description',
114+
color: '#FF5733',
115+
timezone: 'Europe/Berlin',
116+
};
117+
118+
const result = formatCalendarUpdateSuccess(calendar, updatedFields);
119+
120+
expect(result.content).toBeDefined();
121+
expect(result.content[0].type).toBe('text');
122+
expect(result.content[0].text).toContain('Calendar updated successfully');
123+
expect(result.content[0].text).toContain('Updated Calendar');
124+
expect(result.content[0].text).toContain('Display name: Updated Calendar');
125+
expect(result.content[0].text).toContain('Description: New description');
126+
expect(result.content[0].text).toContain('Color: #FF5733');
127+
expect(result.content[0].text).toContain('Timezone: Europe/Berlin');
128+
});
129+
130+
test('should format calendar update success with single field', () => {
131+
const calendar = {
132+
displayName: 'My Calendar',
133+
url: 'https://dav.example.com/calendars/user/test',
134+
};
135+
const updatedFields = {
136+
display_name: 'My Calendar',
137+
};
138+
139+
const result = formatCalendarUpdateSuccess(calendar, updatedFields);
140+
141+
expect(result.content[0].text).toContain('Calendar updated successfully');
142+
expect(result.content[0].text).toContain('My Calendar');
143+
expect(result.content[0].text).not.toContain('Description:');
144+
});
145+
146+
test('should handle unnamed calendar', () => {
147+
const calendar = {
148+
url: 'https://dav.example.com/calendars/user/test',
149+
};
150+
const updatedFields = {
151+
color: '#00FF00',
152+
};
153+
154+
const result = formatCalendarUpdateSuccess(calendar, updatedFields);
155+
156+
expect(result.content[0].text).toContain('Unnamed Calendar');
157+
});
158+
});
159+
160+
describe('formatCalendarDeleteSuccess', () => {
161+
test('should format calendar deletion success with warning', () => {
162+
const calendarUrl = 'https://dav.example.com/calendars/user/test';
163+
164+
const result = formatCalendarDeleteSuccess(calendarUrl);
165+
166+
expect(result.content).toBeDefined();
167+
expect(result.content[0].type).toBe('text');
168+
expect(result.content[0].text).toContain('Calendar deleted successfully');
169+
expect(result.content[0].text).toContain('Warning');
170+
expect(result.content[0].text).toContain('permanently deleted');
171+
expect(result.content[0].text).toContain(calendarUrl);
172+
});
173+
174+
test('should include warning emoji', () => {
175+
const calendarUrl = 'https://dav.example.com/calendars/user/test';
176+
177+
const result = formatCalendarDeleteSuccess(calendarUrl);
178+
179+
expect(result.content[0].text).toMatch(//);
180+
});
181+
182+
test('should include success emoji', () => {
183+
const calendarUrl = 'https://dav.example.com/calendars/user/test';
184+
185+
const result = formatCalendarDeleteSuccess(calendarUrl);
186+
187+
expect(result.content[0].text).toMatch(//);
188+
});
189+
});
190+
});

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "tsdav-mcp-server",
3-
"version": "2.1.0",
3+
"version": "2.2.0",
44
"description": "Model Context Protocol (MCP) server for CalDAV/CardDAV/VTODO integration via tsdav - enables AI systems to manage calendars, contacts, and tasks",
55
"type": "module",
66
"main": "src/index.js",

src/formatters.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,58 @@ export function formatSuccess(operation, details = {}) {
500500
};
501501
}
502502

503+
export function formatCalendarUpdateSuccess(calendar, updatedFields) {
504+
let output = `✅ **Calendar updated successfully**\n\n`;
505+
506+
output += `- **Calendar**: ${calendar.displayName || 'Unnamed Calendar'}\n`;
507+
output += `- **URL**: ${calendar.url}\n`;
508+
509+
if (updatedFields && Object.keys(updatedFields).length > 0) {
510+
output += `\n**Updated fields:**\n`;
511+
if (updatedFields.display_name) {
512+
output += `- Display name: ${updatedFields.display_name}\n`;
513+
}
514+
if (updatedFields.description) {
515+
output += `- Description: ${updatedFields.description}\n`;
516+
}
517+
if (updatedFields.color) {
518+
output += `- Color: ${updatedFields.color}\n`;
519+
}
520+
if (updatedFields.timezone) {
521+
output += `- Timezone: ${updatedFields.timezone}\n`;
522+
}
523+
}
524+
525+
output += `\n---\n<details>\n<summary>Raw Data (JSON)</summary>\n\n\`\`\`json\n`;
526+
output += JSON.stringify({ success: true, calendar, updatedFields }, null, 2);
527+
output += '\n```\n</details>';
528+
529+
return {
530+
content: [{
531+
type: 'text',
532+
text: output
533+
}]
534+
};
535+
}
536+
537+
export function formatCalendarDeleteSuccess(calendarUrl) {
538+
let output = `✅ **Calendar deleted successfully**\n\n`;
539+
540+
output += `⚠️ **Warning**: The calendar and all its events have been permanently deleted.\n\n`;
541+
output += `- **Deleted URL**: ${calendarUrl}\n`;
542+
543+
output += `\n---\n<details>\n<summary>Raw Data (JSON)</summary>\n\n\`\`\`json\n`;
544+
output += JSON.stringify({ success: true, deleted: true, url: calendarUrl }, null, 2);
545+
output += '\n```\n</details>';
546+
547+
return {
548+
content: [{
549+
type: 'text',
550+
text: output
551+
}]
552+
};
553+
}
554+
503555
/**
504556
* Parse VTODO (task) from iCal data
505557
*/

0 commit comments

Comments
 (0)