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.
- package/CHANGELOG.md +45 -0
- package/LICENSE +21 -0
- package/README.md +260 -0
- package/package.json +80 -0
- package/src/error-handler.js +215 -0
- package/src/formatters.js +754 -0
- package/src/logger.js +144 -0
- package/src/server-http.js +402 -0
- package/src/server-stdio.js +225 -0
- package/src/tool-call-logger.js +148 -0
- package/src/tools/calendar/calendar-multi-get.js +38 -0
- package/src/tools/calendar/calendar-query.js +98 -0
- package/src/tools/calendar/create-event.js +79 -0
- package/src/tools/calendar/delete-calendar.js +36 -0
- package/src/tools/calendar/delete-event.js +38 -0
- package/src/tools/calendar/index.js +16 -0
- package/src/tools/calendar/list-calendars.js +21 -0
- package/src/tools/calendar/list-events.js +43 -0
- package/src/tools/calendar/make-calendar.js +80 -0
- package/src/tools/calendar/update-calendar.js +106 -0
- package/src/tools/calendar/update-event-fields.js +119 -0
- package/src/tools/calendar/update-event-raw.js +45 -0
- package/src/tools/contacts/addressbook-multi-get.js +38 -0
- package/src/tools/contacts/addressbook-query.js +85 -0
- package/src/tools/contacts/create-contact.js +84 -0
- package/src/tools/contacts/delete-contact.js +38 -0
- package/src/tools/contacts/index.js +13 -0
- package/src/tools/contacts/list-addressbooks.js +21 -0
- package/src/tools/contacts/list-contacts.js +32 -0
- package/src/tools/contacts/update-contact-fields.js +135 -0
- package/src/tools/contacts/update-contact-raw.js +45 -0
- package/src/tools/index.js +57 -0
- package/src/tools/shared/helpers.js +132 -0
- package/src/tools/todos/create-todo.js +101 -0
- package/src/tools/todos/delete-todo.js +38 -0
- package/src/tools/todos/index.js +12 -0
- package/src/tools/todos/list-todos.js +30 -0
- package/src/tools/todos/todo-multi-get.js +37 -0
- package/src/tools/todos/todo-query.js +112 -0
- package/src/tools/todos/update-todo-fields.js +119 -0
- package/src/tools/todos/update-todo-raw.js +46 -0
- package/src/tsdav-client.js +199 -0
- package/src/utils/tool-helpers.js +388 -0
- package/src/validation.js +245 -0
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM-Friendly Output Formatters for tsdav-mcp
|
|
3
|
+
*
|
|
4
|
+
* This module provides formatters that convert raw CalDAV/CardDAV data
|
|
5
|
+
* into human-readable Markdown format optimized for LLM consumption.
|
|
6
|
+
*
|
|
7
|
+
* Uses RFC-compliant parsing:
|
|
8
|
+
* - ical.js for RFC 5545 (iCalendar) compliance
|
|
9
|
+
* - ical.js for RFC 6350 (vCard) compliance (supports v3.0 and v4.0)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import ICAL from 'ical.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse iCal data string to extract event properties (RFC 5545 compliant)
|
|
16
|
+
*/
|
|
17
|
+
function parseICalEvent(icalData) {
|
|
18
|
+
try {
|
|
19
|
+
const jcalData = ICAL.parse(icalData);
|
|
20
|
+
const comp = new ICAL.Component(jcalData);
|
|
21
|
+
const vevent = comp.getFirstSubcomponent('vevent');
|
|
22
|
+
|
|
23
|
+
if (!vevent) {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const event = new ICAL.Event(vevent);
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
summary: event.summary || '',
|
|
31
|
+
description: event.description || '',
|
|
32
|
+
location: event.location || '',
|
|
33
|
+
uid: event.uid || '',
|
|
34
|
+
dtstart: event.startDate,
|
|
35
|
+
dtend: event.endDate,
|
|
36
|
+
isRecurring: event.isRecurring(),
|
|
37
|
+
rrule: event.isRecurring() ? vevent.getFirstPropertyValue('rrule') : null,
|
|
38
|
+
organizer: vevent.getFirstPropertyValue('organizer'),
|
|
39
|
+
attendees: vevent.getAllProperties('attendee').map(att => ({
|
|
40
|
+
email: att.getFirstValue(),
|
|
41
|
+
role: att.getParameter('role'),
|
|
42
|
+
partstat: att.getParameter('partstat'),
|
|
43
|
+
cn: att.getParameter('cn'),
|
|
44
|
+
})),
|
|
45
|
+
alarms: vevent.getAllSubcomponents('valarm').map(valarm => ({
|
|
46
|
+
action: valarm.getFirstPropertyValue('action'),
|
|
47
|
+
trigger: valarm.getFirstPropertyValue('trigger'),
|
|
48
|
+
description: valarm.getFirstPropertyValue('description'),
|
|
49
|
+
})),
|
|
50
|
+
};
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('Error parsing iCal event:', error);
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Parse vCard data string to extract contact properties (RFC 6350 compliant)
|
|
59
|
+
*/
|
|
60
|
+
function parseVCard(vcardData) {
|
|
61
|
+
try {
|
|
62
|
+
const jcard = ICAL.parse(vcardData);
|
|
63
|
+
const vcard = new ICAL.Component(jcard);
|
|
64
|
+
|
|
65
|
+
const contact = {
|
|
66
|
+
fullName: vcard.getFirstPropertyValue('fn') || '',
|
|
67
|
+
uid: vcard.getFirstPropertyValue('uid') || '',
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Parse structured name (N property)
|
|
71
|
+
const n = vcard.getFirstProperty('n');
|
|
72
|
+
if (n) {
|
|
73
|
+
const nameValue = n.getFirstValue();
|
|
74
|
+
contact.familyName = nameValue[0] || '';
|
|
75
|
+
contact.givenName = nameValue[1] || '';
|
|
76
|
+
contact.additionalNames = nameValue[2] || '';
|
|
77
|
+
contact.honorificPrefixes = nameValue[3] || '';
|
|
78
|
+
contact.honorificSuffixes = nameValue[4] || '';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Parse all emails
|
|
82
|
+
const emails = vcard.getAllProperties('email');
|
|
83
|
+
if (emails && emails.length > 0) {
|
|
84
|
+
contact.emails = emails.map(e => ({
|
|
85
|
+
value: e.getFirstValue(),
|
|
86
|
+
type: e.getParameter('type') ? [e.getParameter('type')] : [],
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Parse all phone numbers
|
|
91
|
+
const tels = vcard.getAllProperties('tel');
|
|
92
|
+
if (tels && tels.length > 0) {
|
|
93
|
+
contact.phones = tels.map(t => ({
|
|
94
|
+
value: t.getFirstValue(),
|
|
95
|
+
type: t.getParameter('type') ? [t.getParameter('type')] : [],
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Parse all addresses
|
|
100
|
+
const adrs = vcard.getAllProperties('adr');
|
|
101
|
+
if (adrs && adrs.length > 0) {
|
|
102
|
+
contact.addresses = adrs.map(a => {
|
|
103
|
+
const adrValue = a.getFirstValue();
|
|
104
|
+
return {
|
|
105
|
+
poBox: adrValue[0] || '',
|
|
106
|
+
extendedAddress: adrValue[1] || '',
|
|
107
|
+
streetAddress: adrValue[2] || '',
|
|
108
|
+
locality: adrValue[3] || '',
|
|
109
|
+
region: adrValue[4] || '',
|
|
110
|
+
postalCode: adrValue[5] || '',
|
|
111
|
+
country: adrValue[6] || '',
|
|
112
|
+
type: a.getParameter('type') ? [a.getParameter('type')] : [],
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Parse organization
|
|
118
|
+
const org = vcard.getFirstProperty('org');
|
|
119
|
+
if (org) {
|
|
120
|
+
const orgValue = org.getFirstValue();
|
|
121
|
+
contact.organization = Array.isArray(orgValue) ? orgValue.join(', ') : orgValue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Parse note
|
|
125
|
+
const note = vcard.getFirstPropertyValue('note');
|
|
126
|
+
if (note) {
|
|
127
|
+
contact.note = note;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return contact;
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.error('Error parsing vCard:', error);
|
|
133
|
+
return {};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Format ICAL.Time to human-readable format with proper timezone support
|
|
139
|
+
*/
|
|
140
|
+
function formatDateTime(icalTime) {
|
|
141
|
+
if (!icalTime) return '';
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
// Convert ICAL.Time to JavaScript Date
|
|
145
|
+
const jsDate = icalTime.toJSDate();
|
|
146
|
+
|
|
147
|
+
const dateStr = jsDate.toLocaleDateString('en-US', {
|
|
148
|
+
year: 'numeric',
|
|
149
|
+
month: 'long',
|
|
150
|
+
day: 'numeric',
|
|
151
|
+
timeZone: icalTime.timezone === 'UTC' ? 'UTC' : undefined,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const timeStr = jsDate.toLocaleTimeString('en-US', {
|
|
155
|
+
hour: '2-digit',
|
|
156
|
+
minute: '2-digit',
|
|
157
|
+
timeZoneName: 'short',
|
|
158
|
+
timeZone: icalTime.timezone === 'UTC' ? 'UTC' : undefined,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return `${dateStr}, ${timeStr}`;
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.error('Error formatting datetime:', error);
|
|
164
|
+
return '';
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Format a single calendar event to Markdown
|
|
170
|
+
*/
|
|
171
|
+
export function formatEvent(event, calendarName = 'Unknown Calendar') {
|
|
172
|
+
const parsed = parseICalEvent(event.data);
|
|
173
|
+
|
|
174
|
+
const startDate = formatDateTime(parsed.dtstart);
|
|
175
|
+
const endDate = formatDateTime(parsed.dtend);
|
|
176
|
+
|
|
177
|
+
let output = `## ${parsed.summary || 'Untitled Event'}\n\n`;
|
|
178
|
+
output += `- **When**: ${startDate}`;
|
|
179
|
+
|
|
180
|
+
if (endDate && endDate !== startDate) {
|
|
181
|
+
output += ` to ${endDate}`;
|
|
182
|
+
}
|
|
183
|
+
output += '\n';
|
|
184
|
+
|
|
185
|
+
if (parsed.location) {
|
|
186
|
+
output += `- **Where**: ${parsed.location}\n`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (parsed.description) {
|
|
190
|
+
output += `- **Description**: ${parsed.description}\n`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Show recurrence info if event is recurring
|
|
194
|
+
if (parsed.isRecurring && parsed.rrule) {
|
|
195
|
+
output += `- **Recurring**: ${parsed.rrule.toString()}\n`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Show organizer if present
|
|
199
|
+
if (parsed.organizer) {
|
|
200
|
+
const organizerEmail = parsed.organizer.replace('mailto:', '');
|
|
201
|
+
output += `- **Organizer**: ${organizerEmail}\n`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Show attendees if present
|
|
205
|
+
if (parsed.attendees && parsed.attendees.length > 0) {
|
|
206
|
+
output += `- **Attendees**: ${parsed.attendees.length} person(s)\n`;
|
|
207
|
+
parsed.attendees.forEach(att => {
|
|
208
|
+
const email = att.email ? att.email.replace('mailto:', '') : '';
|
|
209
|
+
const name = att.cn || email;
|
|
210
|
+
const status = att.partstat ? ` (${att.partstat})` : '';
|
|
211
|
+
output += ` - ${name}${status}\n`;
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Show alarms if present
|
|
216
|
+
if (parsed.alarms && parsed.alarms.length > 0) {
|
|
217
|
+
output += `- **Reminders**: ${parsed.alarms.length} alarm(s)\n`;
|
|
218
|
+
parsed.alarms.forEach(alarm => {
|
|
219
|
+
output += ` - ${alarm.action}: ${alarm.trigger ? alarm.trigger.toString() : 'Unknown trigger'}\n`;
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
output += `- **Calendar**: ${calendarName}\n`;
|
|
224
|
+
output += `- **URL**: ${event.url}\n`;
|
|
225
|
+
|
|
226
|
+
return output;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Format a list of calendar events to LLM-friendly Markdown
|
|
231
|
+
*/
|
|
232
|
+
export function formatEventList(events, calendarName = 'Unknown Calendar') {
|
|
233
|
+
if (!events || events.length === 0) {
|
|
234
|
+
return {
|
|
235
|
+
content: [{
|
|
236
|
+
type: 'text',
|
|
237
|
+
text: 'No events found.'
|
|
238
|
+
}]
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let output = `Found events: **${events.length}**\n\n`;
|
|
243
|
+
|
|
244
|
+
events.forEach((event, index) => {
|
|
245
|
+
output += `### ${index + 1}. `;
|
|
246
|
+
output += formatEvent(event, calendarName).replace(/^## /, '') + '\n';
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
output += `---\n<details>\n<summary>Raw Data (JSON)</summary>\n\n\`\`\`json\n`;
|
|
250
|
+
output += JSON.stringify(events.map(e => ({
|
|
251
|
+
url: e.url,
|
|
252
|
+
etag: e.etag,
|
|
253
|
+
data: e.data
|
|
254
|
+
})), null, 2);
|
|
255
|
+
output += '\n```\n</details>';
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
content: [{
|
|
259
|
+
type: 'text',
|
|
260
|
+
text: output
|
|
261
|
+
}]
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Format a single contact to Markdown
|
|
267
|
+
*/
|
|
268
|
+
export function formatContact(contact, addressBookName = 'Unknown Address Book') {
|
|
269
|
+
const parsed = parseVCard(contact.data);
|
|
270
|
+
|
|
271
|
+
let output = `## ${parsed.fullName || 'Unnamed Contact'}\n\n`;
|
|
272
|
+
|
|
273
|
+
// Show structured name if available
|
|
274
|
+
if (parsed.givenName || parsed.familyName) {
|
|
275
|
+
const nameParts = [];
|
|
276
|
+
if (parsed.honorificPrefixes) nameParts.push(parsed.honorificPrefixes);
|
|
277
|
+
if (parsed.givenName) nameParts.push(parsed.givenName);
|
|
278
|
+
if (parsed.additionalNames) nameParts.push(parsed.additionalNames);
|
|
279
|
+
if (parsed.familyName) nameParts.push(parsed.familyName);
|
|
280
|
+
if (parsed.honorificSuffixes) nameParts.push(parsed.honorificSuffixes);
|
|
281
|
+
if (nameParts.length > 0) {
|
|
282
|
+
output += `- **Full Name**: ${nameParts.join(' ')}\n`;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (parsed.organization) {
|
|
287
|
+
output += `- **Organization**: ${parsed.organization}\n`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Show all emails
|
|
291
|
+
if (parsed.emails && parsed.emails.length > 0) {
|
|
292
|
+
if (parsed.emails.length === 1) {
|
|
293
|
+
const emailType = parsed.emails[0].type.length > 0 ? ` (${parsed.emails[0].type.join(', ')})` : '';
|
|
294
|
+
output += `- **Email**: ${parsed.emails[0].value}${emailType}\n`;
|
|
295
|
+
} else {
|
|
296
|
+
output += `- **Emails**: ${parsed.emails.length} email(s)\n`;
|
|
297
|
+
parsed.emails.forEach(email => {
|
|
298
|
+
const emailType = email.type.length > 0 ? ` (${email.type.join(', ')})` : '';
|
|
299
|
+
output += ` - ${email.value}${emailType}\n`;
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Show all phones
|
|
305
|
+
if (parsed.phones && parsed.phones.length > 0) {
|
|
306
|
+
if (parsed.phones.length === 1) {
|
|
307
|
+
const phoneType = parsed.phones[0].type.length > 0 ? ` (${parsed.phones[0].type.join(', ')})` : '';
|
|
308
|
+
output += `- **Phone**: ${parsed.phones[0].value}${phoneType}\n`;
|
|
309
|
+
} else {
|
|
310
|
+
output += `- **Phones**: ${parsed.phones.length} phone(s)\n`;
|
|
311
|
+
parsed.phones.forEach(phone => {
|
|
312
|
+
const phoneType = phone.type.length > 0 ? ` (${phone.type.join(', ')})` : '';
|
|
313
|
+
output += ` - ${phone.value}${phoneType}\n`;
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Show all addresses
|
|
319
|
+
if (parsed.addresses && parsed.addresses.length > 0) {
|
|
320
|
+
output += `- **Addresses**: ${parsed.addresses.length} address(es)\n`;
|
|
321
|
+
parsed.addresses.forEach(addr => {
|
|
322
|
+
const addrParts = [];
|
|
323
|
+
if (addr.streetAddress) addrParts.push(addr.streetAddress);
|
|
324
|
+
if (addr.locality) addrParts.push(addr.locality);
|
|
325
|
+
if (addr.region) addrParts.push(addr.region);
|
|
326
|
+
if (addr.postalCode) addrParts.push(addr.postalCode);
|
|
327
|
+
if (addr.country) addrParts.push(addr.country);
|
|
328
|
+
const addrType = addr.type.length > 0 ? ` (${addr.type.join(', ')})` : '';
|
|
329
|
+
if (addrParts.length > 0) {
|
|
330
|
+
output += ` - ${addrParts.join(', ')}${addrType}\n`;
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (parsed.note) {
|
|
336
|
+
output += `- **Note**: ${parsed.note}\n`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
output += `- **Address Book**: ${addressBookName}\n`;
|
|
340
|
+
output += `- **URL**: ${contact.url}\n`;
|
|
341
|
+
|
|
342
|
+
return output;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Format a list of contacts to LLM-friendly Markdown
|
|
347
|
+
*/
|
|
348
|
+
export function formatContactList(contacts, addressBookName = 'Unknown Address Book') {
|
|
349
|
+
if (!contacts || contacts.length === 0) {
|
|
350
|
+
return {
|
|
351
|
+
content: [{
|
|
352
|
+
type: 'text',
|
|
353
|
+
text: `No contacts found in ${addressBookName}.
|
|
354
|
+
|
|
355
|
+
💡 **Next steps**:
|
|
356
|
+
- Try broader search: use addressbook_query with partial name
|
|
357
|
+
- List all contacts: use list_contacts to see available names
|
|
358
|
+
- Create new contact: use create_contact if contact doesn't exist yet
|
|
359
|
+
|
|
360
|
+
📝 **Available address books**: Use list_addressbooks to see all address books`
|
|
361
|
+
}]
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
let output = `Found contacts: **${contacts.length}**\n\n`;
|
|
366
|
+
|
|
367
|
+
contacts.forEach((contact, index) => {
|
|
368
|
+
output += `### ${index + 1}. `;
|
|
369
|
+
output += formatContact(contact, addressBookName).replace(/^## /, '') + '\n';
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
output += `---\n<details>\n<summary>Raw Data (JSON)</summary>\n\n\`\`\`json\n`;
|
|
373
|
+
output += JSON.stringify(contacts.map(c => ({
|
|
374
|
+
url: c.url,
|
|
375
|
+
etag: c.etag,
|
|
376
|
+
data: c.data
|
|
377
|
+
})), null, 2);
|
|
378
|
+
output += '\n```\n</details>';
|
|
379
|
+
|
|
380
|
+
// Add next action hints
|
|
381
|
+
output += `\n💡 **What you can do next**:
|
|
382
|
+
- Update contact: use update_contact with URL and ETAG from above
|
|
383
|
+
- Delete contact: use delete_contact with URL and ETAG from above
|
|
384
|
+
- Get full details: Contact data already complete above`;
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
content: [{
|
|
388
|
+
type: 'text',
|
|
389
|
+
text: output
|
|
390
|
+
}]
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Helper: Extract string value from property (handles both string and object)
|
|
396
|
+
* tsdav sometimes returns { _text: "value" } instead of "value"
|
|
397
|
+
*/
|
|
398
|
+
function extractPropertyValue(prop) {
|
|
399
|
+
if (!prop) return '';
|
|
400
|
+
if (typeof prop === 'string') return prop;
|
|
401
|
+
if (typeof prop === 'object') {
|
|
402
|
+
return prop._text || prop.value || String(prop);
|
|
403
|
+
}
|
|
404
|
+
return String(prop);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Format calendar list to LLM-friendly Markdown
|
|
409
|
+
*/
|
|
410
|
+
export function formatCalendarList(calendars) {
|
|
411
|
+
if (!calendars || calendars.length === 0) {
|
|
412
|
+
return {
|
|
413
|
+
content: [{
|
|
414
|
+
type: 'text',
|
|
415
|
+
text: 'No calendars found.'
|
|
416
|
+
}]
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
let output = `Available calendars: **${calendars.length}**\n\n`;
|
|
421
|
+
|
|
422
|
+
calendars.forEach((cal, index) => {
|
|
423
|
+
const displayName = extractPropertyValue(cal.displayName) || 'Unnamed Calendar';
|
|
424
|
+
output += `### ${index + 1}. ${displayName}\n\n`;
|
|
425
|
+
|
|
426
|
+
if (cal.description) {
|
|
427
|
+
output += `- **Description**: ${cal.description}\n`;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (cal.components) {
|
|
431
|
+
output += `- **Components**: ${cal.components.join(', ')}\n`;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (cal.calendarColor) {
|
|
435
|
+
output += `- **Color**: ${cal.calendarColor}\n`;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
output += `- **URL**: ${cal.url}\n\n`;
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
output += `---\n<details>\n<summary>Raw Data (JSON)</summary>\n\n\`\`\`json\n`;
|
|
442
|
+
output += JSON.stringify(calendars.map(cal => ({
|
|
443
|
+
displayName: cal.displayName,
|
|
444
|
+
url: cal.url,
|
|
445
|
+
components: cal.components,
|
|
446
|
+
calendarColor: cal.calendarColor,
|
|
447
|
+
description: cal.description,
|
|
448
|
+
})), null, 2);
|
|
449
|
+
output += '\n```\n</details>';
|
|
450
|
+
|
|
451
|
+
return {
|
|
452
|
+
content: [{
|
|
453
|
+
type: 'text',
|
|
454
|
+
text: output
|
|
455
|
+
}]
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Format address book list to LLM-friendly Markdown
|
|
461
|
+
*/
|
|
462
|
+
export function formatAddressBookList(addressBooks) {
|
|
463
|
+
if (!addressBooks || addressBooks.length === 0) {
|
|
464
|
+
return {
|
|
465
|
+
content: [{
|
|
466
|
+
type: 'text',
|
|
467
|
+
text: 'No address books found.'
|
|
468
|
+
}]
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
let output = `Available address books: **${addressBooks.length}**\n\n`;
|
|
473
|
+
|
|
474
|
+
addressBooks.forEach((ab, index) => {
|
|
475
|
+
output += `### ${index + 1}. ${ab.displayName || 'Unnamed Address Book'}\n\n`;
|
|
476
|
+
|
|
477
|
+
if (ab.description) {
|
|
478
|
+
output += `- **Description**: ${ab.description}\n`;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
output += `- **URL**: ${ab.url}\n\n`;
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
output += `---\n<details>\n<summary>Raw Data (JSON)</summary>\n\n\`\`\`json\n`;
|
|
485
|
+
output += JSON.stringify(addressBooks.map(ab => ({
|
|
486
|
+
displayName: ab.displayName,
|
|
487
|
+
url: ab.url,
|
|
488
|
+
description: ab.description,
|
|
489
|
+
})), null, 2);
|
|
490
|
+
output += '\n```\n</details>';
|
|
491
|
+
|
|
492
|
+
return {
|
|
493
|
+
content: [{
|
|
494
|
+
type: 'text',
|
|
495
|
+
text: output
|
|
496
|
+
}]
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Format success message for create/update/delete operations
|
|
502
|
+
*/
|
|
503
|
+
export function formatSuccess(operation, details = {}) {
|
|
504
|
+
let output = `✅ **${operation} successful**\n\n`;
|
|
505
|
+
|
|
506
|
+
if (details.url) {
|
|
507
|
+
output += `- **URL**: ${details.url}\n`;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (details.etag) {
|
|
511
|
+
output += `- **ETag**: ${details.etag}\n`;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (details.message) {
|
|
515
|
+
output += `- **Message**: ${details.message}\n`;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
output += `\n---\n<details>\n<summary>Rohdaten (JSON)</summary>\n\n\`\`\`json\n`;
|
|
519
|
+
output += JSON.stringify({ success: true, ...details }, null, 2);
|
|
520
|
+
output += '\n```\n</details>';
|
|
521
|
+
|
|
522
|
+
return {
|
|
523
|
+
content: [{
|
|
524
|
+
type: 'text',
|
|
525
|
+
text: output
|
|
526
|
+
}]
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
export function formatCalendarUpdateSuccess(calendar, updatedFields) {
|
|
531
|
+
let output = `✅ **Calendar updated successfully**\n\n`;
|
|
532
|
+
|
|
533
|
+
const displayName = extractPropertyValue(calendar.displayName) || 'Unnamed Calendar';
|
|
534
|
+
output += `- **Calendar**: ${displayName}\n`;
|
|
535
|
+
output += `- **URL**: ${calendar.url}\n`;
|
|
536
|
+
|
|
537
|
+
if (updatedFields && Object.keys(updatedFields).length > 0) {
|
|
538
|
+
output += `\n**Updated fields:**\n`;
|
|
539
|
+
if (updatedFields.display_name) {
|
|
540
|
+
output += `- Display name: ${updatedFields.display_name}\n`;
|
|
541
|
+
}
|
|
542
|
+
if (updatedFields.description) {
|
|
543
|
+
output += `- Description: ${updatedFields.description}\n`;
|
|
544
|
+
}
|
|
545
|
+
if (updatedFields.color) {
|
|
546
|
+
output += `- Color: ${updatedFields.color}\n`;
|
|
547
|
+
}
|
|
548
|
+
if (updatedFields.timezone) {
|
|
549
|
+
output += `- Timezone: ${updatedFields.timezone}\n`;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
output += `\n---\n<details>\n<summary>Raw Data (JSON)</summary>\n\n\`\`\`json\n`;
|
|
554
|
+
output += JSON.stringify({ success: true, calendar, updatedFields }, null, 2);
|
|
555
|
+
output += '\n```\n</details>';
|
|
556
|
+
|
|
557
|
+
return {
|
|
558
|
+
content: [{
|
|
559
|
+
type: 'text',
|
|
560
|
+
text: output
|
|
561
|
+
}]
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
export function formatCalendarDeleteSuccess(calendarUrl) {
|
|
566
|
+
let output = `✅ **Calendar deleted successfully**\n\n`;
|
|
567
|
+
|
|
568
|
+
output += `⚠️ **Warning**: The calendar and all its events have been permanently deleted.\n\n`;
|
|
569
|
+
output += `- **Deleted URL**: ${calendarUrl}\n`;
|
|
570
|
+
|
|
571
|
+
output += `\n---\n<details>\n<summary>Raw Data (JSON)</summary>\n\n\`\`\`json\n`;
|
|
572
|
+
output += JSON.stringify({ success: true, deleted: true, url: calendarUrl }, null, 2);
|
|
573
|
+
output += '\n```\n</details>';
|
|
574
|
+
|
|
575
|
+
return {
|
|
576
|
+
content: [{
|
|
577
|
+
type: 'text',
|
|
578
|
+
text: output
|
|
579
|
+
}]
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Parse VTODO (task) from iCal data
|
|
585
|
+
*/
|
|
586
|
+
function parseVTodo(icalData) {
|
|
587
|
+
try {
|
|
588
|
+
const jcalData = ICAL.parse(icalData);
|
|
589
|
+
const comp = new ICAL.Component(jcalData);
|
|
590
|
+
const vtodo = comp.getFirstSubcomponent('vtodo');
|
|
591
|
+
|
|
592
|
+
if (!vtodo) {
|
|
593
|
+
return {};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return {
|
|
597
|
+
uid: vtodo.getFirstPropertyValue('uid') || '',
|
|
598
|
+
summary: vtodo.getFirstPropertyValue('summary') || '',
|
|
599
|
+
description: vtodo.getFirstPropertyValue('description') || '',
|
|
600
|
+
status: vtodo.getFirstPropertyValue('status') || 'NEEDS-ACTION',
|
|
601
|
+
priority: vtodo.getFirstPropertyValue('priority') || 0,
|
|
602
|
+
percentComplete: vtodo.getFirstPropertyValue('percent-complete') || 0,
|
|
603
|
+
due: vtodo.getFirstPropertyValue('due'),
|
|
604
|
+
completed: vtodo.getFirstPropertyValue('completed'),
|
|
605
|
+
dtstart: vtodo.getFirstPropertyValue('dtstart'),
|
|
606
|
+
};
|
|
607
|
+
} catch (error) {
|
|
608
|
+
console.error('Error parsing VTODO:', error);
|
|
609
|
+
return {};
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Get emoji for todo status
|
|
615
|
+
*/
|
|
616
|
+
function getStatusEmoji(status) {
|
|
617
|
+
const statusMap = {
|
|
618
|
+
'NEEDS-ACTION': '📋',
|
|
619
|
+
'IN-PROCESS': '🔄',
|
|
620
|
+
'COMPLETED': '✅',
|
|
621
|
+
'CANCELLED': '❌',
|
|
622
|
+
};
|
|
623
|
+
return statusMap[status] || '📋';
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Format priority (0-9 where 0=undefined, 1=highest, 9=lowest)
|
|
628
|
+
*/
|
|
629
|
+
function formatPriority(priority) {
|
|
630
|
+
if (priority === 0 || priority === undefined) return 'None';
|
|
631
|
+
if (priority >= 1 && priority <= 3) return `🔴 High (${priority})`;
|
|
632
|
+
if (priority >= 4 && priority <= 6) return `🟡 Medium (${priority})`;
|
|
633
|
+
return `🟢 Low (${priority})`;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Format a single todo to Markdown
|
|
638
|
+
*/
|
|
639
|
+
export function formatTodo(todo, calendarName = 'Unknown Calendar') {
|
|
640
|
+
const parsed = parseVTodo(todo.data);
|
|
641
|
+
const statusEmoji = getStatusEmoji(parsed.status);
|
|
642
|
+
|
|
643
|
+
let output = `## ${statusEmoji} ${parsed.summary || 'Untitled Task'}\n\n`;
|
|
644
|
+
|
|
645
|
+
output += `- **Status**: ${parsed.status}\n`;
|
|
646
|
+
|
|
647
|
+
if (parsed.due) {
|
|
648
|
+
output += `- **Due**: ${formatDateTime(parsed.due)}\n`;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (parsed.priority && parsed.priority !== 0) {
|
|
652
|
+
output += `- **Priority**: ${formatPriority(parsed.priority)}\n`;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (parsed.percentComplete > 0) {
|
|
656
|
+
output += `- **Progress**: ${parsed.percentComplete}%\n`;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (parsed.description) {
|
|
660
|
+
output += `- **Description**: ${parsed.description}\n`;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (parsed.dtstart) {
|
|
664
|
+
output += `- **Start**: ${formatDateTime(parsed.dtstart)}\n`;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (parsed.completed) {
|
|
668
|
+
output += `- **Completed**: ${formatDateTime(parsed.completed)}\n`;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
output += `- **Calendar**: ${calendarName}\n`;
|
|
672
|
+
output += `- **URL**: ${todo.url}\n`;
|
|
673
|
+
output += `- **ETag**: ${todo.etag} *(required for updates)*\n`;
|
|
674
|
+
|
|
675
|
+
return output;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Format a list of todos to LLM-friendly Markdown
|
|
680
|
+
*/
|
|
681
|
+
export function formatTodoList(todos, calendarName = 'Unknown Calendar') {
|
|
682
|
+
if (!todos || todos.length === 0) {
|
|
683
|
+
return {
|
|
684
|
+
content: [{
|
|
685
|
+
type: 'text',
|
|
686
|
+
text: 'No todos found.'
|
|
687
|
+
}]
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
let output = `Found todos: **${todos.length}**\n\n`;
|
|
692
|
+
|
|
693
|
+
todos.forEach((todo, index) => {
|
|
694
|
+
output += `### ${index + 1}. `;
|
|
695
|
+
output += formatTodo(todo, calendarName).replace(/^## /, '') + '\n';
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
output += `---\n<details>\n<summary>Raw Data (JSON)</summary>\n\n\`\`\`json\n`;
|
|
699
|
+
output += JSON.stringify(todos.map(t => ({
|
|
700
|
+
url: t.url,
|
|
701
|
+
etag: t.etag,
|
|
702
|
+
data: t.data
|
|
703
|
+
})), null, 2);
|
|
704
|
+
output += '\n```\n</details>';
|
|
705
|
+
|
|
706
|
+
return {
|
|
707
|
+
content: [{
|
|
708
|
+
type: 'text',
|
|
709
|
+
text: output
|
|
710
|
+
}]
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Format error message in a user-friendly way
|
|
716
|
+
*/
|
|
717
|
+
export function formatError(error, context = '') {
|
|
718
|
+
let output = `❌ **Error${context ? ` in ${context}` : ''}**\n\n`;
|
|
719
|
+
|
|
720
|
+
// Provide actionable error messages
|
|
721
|
+
const errorMsg = error.message || String(error);
|
|
722
|
+
|
|
723
|
+
if (errorMsg.includes('not found')) {
|
|
724
|
+
output += `The specified resource was not found.\n\n`;
|
|
725
|
+
output += `**Possible solutions:**\n`;
|
|
726
|
+
output += `- Check the URL\n`;
|
|
727
|
+
output += `- Ensure the resource exists\n`;
|
|
728
|
+
output += `- Refresh the resource list\n`;
|
|
729
|
+
} else if (errorMsg.includes('auth') || errorMsg.includes('401')) {
|
|
730
|
+
output += `Authentication failed.\n\n`;
|
|
731
|
+
output += `**Possible solutions:**\n`;
|
|
732
|
+
output += `- Check username and password\n`;
|
|
733
|
+
output += `- Ensure the server is reachable\n`;
|
|
734
|
+
output += `- Verify server settings in .env file\n`;
|
|
735
|
+
} else if (errorMsg.includes('etag') || errorMsg.includes('412')) {
|
|
736
|
+
output += `The resource was modified in the meantime.\n\n`;
|
|
737
|
+
output += `**Possible solutions:**\n`;
|
|
738
|
+
output += `- Reload the current version of the resource\n`;
|
|
739
|
+
output += `- Use the current ETag\n`;
|
|
740
|
+
} else {
|
|
741
|
+
output += `${errorMsg}\n`;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
output += `\n---\n<details>\n<summary>Technical Details</summary>\n\n\`\`\`\n`;
|
|
745
|
+
output += error.stack || errorMsg;
|
|
746
|
+
output += '\n```\n</details>';
|
|
747
|
+
|
|
748
|
+
return {
|
|
749
|
+
content: [{
|
|
750
|
+
type: 'text',
|
|
751
|
+
text: output
|
|
752
|
+
}]
|
|
753
|
+
};
|
|
754
|
+
}
|