Field-agnostic utility layer for tsdav CalDAV/CardDAV operations.
tsdav-utils is a dumb glue library that bridges the gap between tsdav (network transport) and your application logic. It provides a single, field-agnostic function to update any property on calendar events, todos, or contacts without business logic, validation, or opinions.
Your Application / MCP Server
↓
tsdav-utils (field manipulation) ← This library
↓
tsdav (transport) + ical.js (parsing)
↓
CalDAV/CardDAV Server
- Zero business logic - No semantic understanding of fields
- Zero validation - Accepts any property name and value
- Maximum flexibility - Works with standard and custom (X-*) properties
- No hardcoded field lists - Truly field-agnostic
This is intentional. All intelligence belongs in your application layer or MCP server.
npm install tsdav tsdav-utilsimport { createDAVClient } from 'tsdav';
import { updateFields } from 'tsdav-utils';
// 1. Create tsdav client
const client = await createDAVClient({
serverUrl: 'https://caldav.example.com',
credentials: {
username: 'user',
password: 'password',
},
authMethod: 'Basic',
defaultAccountType: 'caldav',
});
// 2. Fetch event
const calendars = await client.fetchCalendars();
const events = await client.fetchCalendarObjects({
calendar: calendars[0],
});
const event = events[0];
// 3. Update any fields
const updated = updateFields(event.data, {
'SUMMARY': 'Updated Meeting Title',
'LOCATION': 'Berlin Office',
'X-ZOOM-LINK': 'https://zoom.us/j/123456789',
});
// 4. Save back to server
await client.updateCalendarObject({
calendarObject: {
...event,
data: updated,
},
});const updated = updateFields(event.data, {
'SUMMARY': 'Team Standup',
'LOCATION': 'Conference Room A',
'DESCRIPTION': 'Daily team sync',
'STATUS': 'CONFIRMED',
'X-MEETING-ROOM': 'BLDG-A-301',
});// Calendar Event (VEVENT)
const updatedEvent = updateFields(event.data, {
'SUMMARY': 'New Event Title',
'LOCATION': 'Office',
});
// Todo/Task (VTODO)
const updatedTodo = updateFields(todo.data, {
'SUMMARY': 'Finish documentation',
'STATUS': 'IN-PROCESS',
'PRIORITY': '1',
});
// Contact (VCARD)
const updatedContact = updateFields(vcard.data, {
'FN': 'Jane Doe',
'EMAIL': 'jane@example.com',
'TEL': '+1234567890',
});// Add custom fields for integration with other systems
const updated = updateFields(event.data, {
'SUMMARY': 'Client Meeting',
'X-CRM-ID': 'SF-12345',
'X-PROJECT-CODE': 'PROJ-2025-001',
'X-ZOOM-LINK': 'https://zoom.us/j/987654321',
'X-SLACK-CHANNEL': '#project-alpha',
});Updates arbitrary properties on a calendar/todo/contact object.
-
calendarObject:
string | { data: string }- Raw iCal string, OR
- tsdav
DAVCalendarObjectwithdatafield
-
fields:
Record<string, string>- Key-value pairs of iCal properties to update
- Keys: iCal property names (e.g.,
'SUMMARY','LOCATION','X-CUSTOM') - Values: Property values as strings
string: Updated iCal string ready fortsdav.updateCalendarObject()
const updated: string = updateFields(calendarObject, {
'SUMMARY': 'New Title',
'X-CUSTOM-FIELD': 'custom value',
});// ❌ We don't provide convenience methods
rescheduleEvent(event, newDate); // Doesn't exist
markComplete(todo); // Doesn't exist
findMeetingsByAttendee(email); // Doesn't exist// ❌ We don't validate property names
updateFields(event, {
'SUMMMARY': 'Typo in field name', // ✅ Accepted (user's responsibility)
});
// ❌ We don't validate values
updateFields(event, {
'DTSTART': 'invalid-date', // ✅ Accepted (ical.js may throw later)
});// ❌ We don't convert datetime formats
updateFields(event, {
'DTSTART': '2025-01-28', // ❌ Wrong format - user must provide iCal format
});
// ✅ User provides correctly formatted iCal datetime
updateFields(event, {
'DTSTART': '20250128T100000Z', // ✅ Correct iCal format
});These limitations are intentional and documented in GitHub issues:
-
Datetime properties (#1)
- ical.js validates datetime values
- Complex datetime handling requires special consideration
- Workaround: Update other properties, handle datetimes separately
-
Multi-value properties (#2)
ATTENDEEwith multiple peopleCATEGORIESwith multiple values- Current: Treats as string (first value only)
-
Structured properties (#3)
VCARD.Nhas 5 components (Family;Given;Additional;Prefix;Suffix)VCARD.ADRhas 7 components- Current: May not handle component structure correctly
-
Timezone handling (#4)
- Complex timezone conversions
- All-day vs timed events
- Workaround: Store in UTC, handle conversion in application layer
-
Recurrence rules (RRULE) (#5)
- Complex recurrence patterns
- Expanding recurring events
- Workaround: Use ical.js directly for recurrence logic
- Building an MCP server for calendar/contact management
- Syncing calendar data between systems
- Bulk updating calendar properties
- Adding custom X-* fields for integration
- Simple CRUD operations on calendar data
- You need high-level scheduling logic → Use a full calendar library
- You need complex timezone handling → Use a datetime library + ical.js
- You need recurrence expansion → Use ical.js directly
- You need validation → Add validation in your application layer
// MCP tool to update calendar event
async function updateEvent(eventId: string, updates: Record<string, string>) {
const client = await createDAVClient(config);
const calendars = await client.fetchCalendars();
const events = await client.fetchCalendarObjects({ calendar: calendars[0] });
const event = events.find(e => e.url.includes(eventId));
if (!event) throw new Error('Event not found');
// Update with field-agnostic approach
const updated = updateFields(event.data, updates);
await client.updateCalendarObject({
calendarObject: { ...event, data: updated },
});
return { success: true, updated: updates };
}// Your MCP server handles the mapping
function llmToIcalFields(llmUpdate: any): Record<string, string> {
return {
'SUMMARY': llmUpdate.title || llmUpdate.summary,
'LOCATION': llmUpdate.location || llmUpdate.where,
'DESCRIPTION': llmUpdate.description || llmUpdate.notes,
'STATUS': llmUpdate.status?.toUpperCase(),
};
}
const icalFields = llmToIcalFields(llmResponse);
const updated = updateFields(event.data, icalFields);npm test # Run all tests
npm run test:watch # Watch mode
npm run test:coverage # Coverage reportThis library intentionally does very little. Before adding features, ask:
- Does this add business logic? → Don't add it
- Could this be done in user code? → Don't add it
- Does this restrict property usage? → Don't add it
Valid contributions:
- Bug fixes (properties not preserved, etc.)
- Performance improvements
- Better error messages
MIT