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,21 @@
|
|
|
1
|
+
import { tsdavManager } from '../../tsdav-client.js';
|
|
2
|
+
import { formatAddressBookList } from '../../formatters.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* List all available address books from the CardDAV server
|
|
6
|
+
*/
|
|
7
|
+
export const listAddressbooks = {
|
|
8
|
+
name: 'list_addressbooks',
|
|
9
|
+
description: 'List all available address books from the CardDAV server. Use this to get address book URLs needed for other contact operations',
|
|
10
|
+
inputSchema: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {},
|
|
13
|
+
required: [],
|
|
14
|
+
},
|
|
15
|
+
handler: async () => {
|
|
16
|
+
const client = tsdavManager.getCardDavClient();
|
|
17
|
+
const addressBooks = await client.fetchAddressBooks();
|
|
18
|
+
|
|
19
|
+
return formatAddressBookList(addressBooks);
|
|
20
|
+
},
|
|
21
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { tsdavManager } from '../../tsdav-client.js';
|
|
2
|
+
import { validateInput, listContactsSchema } from '../../validation.js';
|
|
3
|
+
import { formatContactList } from '../../formatters.js';
|
|
4
|
+
import { findAddressbookOrThrow } from '../shared/helpers.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* List ALL contacts from an address book without filtering
|
|
8
|
+
*/
|
|
9
|
+
export const listContacts = {
|
|
10
|
+
name: 'list_contacts',
|
|
11
|
+
description: 'List ALL contacts from an address book without filtering. WARNING: Returns all contacts which can be thousands - use addressbook_query instead when searching for specific contacts by name, email, or organization to save tokens',
|
|
12
|
+
inputSchema: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
addressbook_url: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
description: 'The URL of the address book to fetch contacts from',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
required: ['addressbook_url'],
|
|
21
|
+
},
|
|
22
|
+
handler: async (args) => {
|
|
23
|
+
const validated = validateInput(listContactsSchema, args);
|
|
24
|
+
const client = tsdavManager.getCardDavClient();
|
|
25
|
+
const addressBooks = await client.fetchAddressBooks();
|
|
26
|
+
const addressBook = findAddressbookOrThrow(addressBooks, validated.addressbook_url);
|
|
27
|
+
|
|
28
|
+
const vcards = await client.fetchVCards({ addressBook });
|
|
29
|
+
|
|
30
|
+
return formatContactList(vcards, addressBook);
|
|
31
|
+
},
|
|
32
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
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 vCard updates
|
|
9
|
+
* Supports all RFC 6350 vCard properties via tsdav-utils
|
|
10
|
+
* Common fields: FN, N, EMAIL, TEL, ORG, TITLE, NOTE, URL, ADR
|
|
11
|
+
* Custom properties: Any X-* property
|
|
12
|
+
*/
|
|
13
|
+
const updateContactFieldsSchema = z.object({
|
|
14
|
+
vcard_url: z.string().url('vCard URL must be a valid URL'),
|
|
15
|
+
vcard_etag: z.string().min(1, 'vCard etag is required'),
|
|
16
|
+
fields: z.record(z.string()).optional()
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Field-agnostic contact update tool powered by tsdav-utils
|
|
21
|
+
* Supports all RFC 6350 vCard properties without validation
|
|
22
|
+
*
|
|
23
|
+
* Features:
|
|
24
|
+
* - Any standard vCard property (FN, N, EMAIL, TEL, ORG, TITLE, ADR, etc.)
|
|
25
|
+
* - Custom X-* properties for extensions
|
|
26
|
+
* - Field-agnostic: no pre-defined field list required
|
|
27
|
+
*/
|
|
28
|
+
export const updateContactFields = {
|
|
29
|
+
name: 'update_contact',
|
|
30
|
+
description: 'PREFERRED: Update contact fields without vCard formatting. Supports: FN (full name), N (structured name), EMAIL, TEL (phone), ORG (organization), TITLE (job title), NOTE, URL, ADR (address), BDAY (birthday), and any RFC 6350 vCard property including custom X-* properties.',
|
|
31
|
+
inputSchema: {
|
|
32
|
+
type: 'object',
|
|
33
|
+
properties: {
|
|
34
|
+
vcard_url: {
|
|
35
|
+
type: 'string',
|
|
36
|
+
description: 'The URL of the vCard to update'
|
|
37
|
+
},
|
|
38
|
+
vcard_etag: {
|
|
39
|
+
type: 'string',
|
|
40
|
+
description: 'The etag of the vCard (required for conflict detection)'
|
|
41
|
+
},
|
|
42
|
+
fields: {
|
|
43
|
+
type: 'object',
|
|
44
|
+
description: 'Fields to update - use UPPERCASE property names (e.g., FN, EMAIL, TEL). Any RFC 6350 vCard property or custom X-* property is supported.',
|
|
45
|
+
additionalProperties: {
|
|
46
|
+
type: 'string'
|
|
47
|
+
},
|
|
48
|
+
properties: {
|
|
49
|
+
FN: {
|
|
50
|
+
type: 'string',
|
|
51
|
+
description: 'Full formatted name (e.g., "John Doe")'
|
|
52
|
+
},
|
|
53
|
+
N: {
|
|
54
|
+
type: 'string',
|
|
55
|
+
description: 'Structured name (format: "Family;Given;Additional;Prefix;Suffix")'
|
|
56
|
+
},
|
|
57
|
+
EMAIL: {
|
|
58
|
+
type: 'string',
|
|
59
|
+
description: 'Email address'
|
|
60
|
+
},
|
|
61
|
+
TEL: {
|
|
62
|
+
type: 'string',
|
|
63
|
+
description: 'Phone number'
|
|
64
|
+
},
|
|
65
|
+
ORG: {
|
|
66
|
+
type: 'string',
|
|
67
|
+
description: 'Organization/company name'
|
|
68
|
+
},
|
|
69
|
+
TITLE: {
|
|
70
|
+
type: 'string',
|
|
71
|
+
description: 'Job title'
|
|
72
|
+
},
|
|
73
|
+
NOTE: {
|
|
74
|
+
type: 'string',
|
|
75
|
+
description: 'Notes/comments'
|
|
76
|
+
},
|
|
77
|
+
URL: {
|
|
78
|
+
type: 'string',
|
|
79
|
+
description: 'Web URL'
|
|
80
|
+
},
|
|
81
|
+
ADR: {
|
|
82
|
+
type: 'string',
|
|
83
|
+
description: 'Address (format: "POBox;Extended;Street;City;Region;PostalCode;Country")'
|
|
84
|
+
},
|
|
85
|
+
BDAY: {
|
|
86
|
+
type: 'string',
|
|
87
|
+
description: 'Birthday (ISO 8601 format: 1990-01-01)'
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
required: ['vcard_url', 'vcard_etag']
|
|
93
|
+
},
|
|
94
|
+
handler: async (args) => {
|
|
95
|
+
try {
|
|
96
|
+
const validated = validateInput(updateContactFieldsSchema, args);
|
|
97
|
+
const client = tsdavManager.getCardDavClient();
|
|
98
|
+
|
|
99
|
+
// Step 1: Fetch the current vCard from server
|
|
100
|
+
const addressBookUrl = validated.vcard_url.substring(0, validated.vcard_url.lastIndexOf('/') + 1);
|
|
101
|
+
const currentVCards = await client.fetchVCards({
|
|
102
|
+
addressBook: { url: addressBookUrl },
|
|
103
|
+
objectUrls: [validated.vcard_url]
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (!currentVCards || currentVCards.length === 0) {
|
|
107
|
+
throw new Error('Contact not found');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const vCardObject = currentVCards[0];
|
|
111
|
+
|
|
112
|
+
// Step 2: Update fields using tsdav-utils (field-agnostic)
|
|
113
|
+
// Accepts any RFC 6350 vCard property name (UPPERCASE)
|
|
114
|
+
const updatedData = updateFields(vCardObject, validated.fields || {});
|
|
115
|
+
|
|
116
|
+
// Step 3: Send the updated vCard back to server
|
|
117
|
+
const updateResponse = await client.updateVCard({
|
|
118
|
+
vCard: {
|
|
119
|
+
url: validated.vcard_url,
|
|
120
|
+
data: updatedData,
|
|
121
|
+
etag: validated.vcard_etag
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return formatSuccess('Contact updated successfully', {
|
|
126
|
+
etag: updateResponse.etag,
|
|
127
|
+
updated_fields: Object.keys(validated.fields || {}),
|
|
128
|
+
message: `Updated ${Object.keys(validated.fields || {}).length} field(s): ${Object.keys(validated.fields || {}).join(', ')}`
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
} catch (error) {
|
|
132
|
+
return formatError('update_contact', error);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { tsdavManager } from '../../tsdav-client.js';
|
|
2
|
+
import { validateInput, updateContactSchema } from '../../validation.js';
|
|
3
|
+
import { formatSuccess } from '../../formatters.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Update an existing contact with raw vCard data
|
|
7
|
+
*/
|
|
8
|
+
export const updateContactRaw = {
|
|
9
|
+
name: 'update_contact_raw',
|
|
10
|
+
description: 'ADVANCED: Update contact with raw vCard data. Requires manual vCard formatting - use update_contact instead for simple field updates (name, email, phone). Only use this if you have complete pre-formatted vCard data or need to update advanced vCard properties.',
|
|
11
|
+
inputSchema: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
vcard_url: {
|
|
15
|
+
type: 'string',
|
|
16
|
+
description: 'The URL of the vCard to update',
|
|
17
|
+
},
|
|
18
|
+
vcard_etag: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
description: 'The etag of the vCard',
|
|
21
|
+
},
|
|
22
|
+
updated_vcard_data: {
|
|
23
|
+
type: 'string',
|
|
24
|
+
description: 'The complete updated vCard data',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
required: ['vcard_url', 'vcard_etag', 'updated_vcard_data'],
|
|
28
|
+
},
|
|
29
|
+
handler: async (args) => {
|
|
30
|
+
const validated = validateInput(updateContactSchema, args);
|
|
31
|
+
const client = tsdavManager.getCardDavClient();
|
|
32
|
+
|
|
33
|
+
const response = await client.updateVCard({
|
|
34
|
+
vCard: {
|
|
35
|
+
url: validated.vcard_url,
|
|
36
|
+
data: validated.updated_vcard_data,
|
|
37
|
+
etag: validated.vcard_etag,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return formatSuccess('Contact updated successfully', {
|
|
42
|
+
etag: response.etag,
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tools Main Index
|
|
3
|
+
* Combines all tool modules into a single exportable array
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Calendar Tools (CalDAV)
|
|
7
|
+
import * as calendarTools from './calendar/index.js';
|
|
8
|
+
|
|
9
|
+
// Contact Tools (CardDAV)
|
|
10
|
+
import * as contactTools from './contacts/index.js';
|
|
11
|
+
|
|
12
|
+
// Todo Tools (VTODO)
|
|
13
|
+
import * as todoTools from './todos/index.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* All available MCP tools
|
|
17
|
+
* Total: 26 tools organized in 3 categories
|
|
18
|
+
*/
|
|
19
|
+
export const tools = [
|
|
20
|
+
// ================================
|
|
21
|
+
// CALENDAR TOOLS (11 tools)
|
|
22
|
+
// ================================
|
|
23
|
+
calendarTools.listCalendars,
|
|
24
|
+
calendarTools.listEvents,
|
|
25
|
+
calendarTools.createEvent,
|
|
26
|
+
calendarTools.updateEventFields,
|
|
27
|
+
calendarTools.updateEventRaw,
|
|
28
|
+
calendarTools.deleteEvent,
|
|
29
|
+
calendarTools.calendarQuery,
|
|
30
|
+
calendarTools.makeCalendar,
|
|
31
|
+
calendarTools.updateCalendar,
|
|
32
|
+
calendarTools.deleteCalendar,
|
|
33
|
+
calendarTools.calendarMultiGet,
|
|
34
|
+
|
|
35
|
+
// ================================
|
|
36
|
+
// CONTACT TOOLS (8 tools)
|
|
37
|
+
// ================================
|
|
38
|
+
contactTools.listAddressbooks,
|
|
39
|
+
contactTools.listContacts,
|
|
40
|
+
contactTools.createContact,
|
|
41
|
+
contactTools.updateContactFields,
|
|
42
|
+
contactTools.updateContactRaw,
|
|
43
|
+
contactTools.deleteContact,
|
|
44
|
+
contactTools.addressbookQuery,
|
|
45
|
+
contactTools.addressbookMultiGet,
|
|
46
|
+
|
|
47
|
+
// ================================
|
|
48
|
+
// TODO TOOLS (7 tools)
|
|
49
|
+
// ================================
|
|
50
|
+
todoTools.listTodos,
|
|
51
|
+
todoTools.createTodo,
|
|
52
|
+
todoTools.updateTodoFields,
|
|
53
|
+
todoTools.updateTodoRaw,
|
|
54
|
+
todoTools.deleteTodo,
|
|
55
|
+
todoTools.todoQuery,
|
|
56
|
+
todoTools.todoMultiGet,
|
|
57
|
+
];
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helper functions for tool implementations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Format iCal date (ISO 8601 to iCal format)
|
|
7
|
+
* @param {string|Date} date - Date to format
|
|
8
|
+
* @returns {string} Formatted iCal date string (YYYYMMDDTHHmmssZ)
|
|
9
|
+
*/
|
|
10
|
+
export function formatICalDate(date) {
|
|
11
|
+
return new Date(date).toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generate unique UID for calendar objects
|
|
16
|
+
* @param {string} prefix - Prefix for the UID (e.g., 'event', 'todo', 'contact')
|
|
17
|
+
* @returns {string} Unique identifier
|
|
18
|
+
*/
|
|
19
|
+
export function generateUID(prefix = 'object') {
|
|
20
|
+
return `${prefix}-${Date.now()}@tsdav-mcp`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Extract calendar home URL from existing calendar URL or account
|
|
25
|
+
* @param {Object} client - CalDAV client instance
|
|
26
|
+
* @returns {Promise<string>} Calendar home URL
|
|
27
|
+
*/
|
|
28
|
+
export async function getCalendarHome(client) {
|
|
29
|
+
// Try to get from account first
|
|
30
|
+
let calendarHome = client.account?.homeUrl;
|
|
31
|
+
|
|
32
|
+
// Fallback: Extract from existing calendar
|
|
33
|
+
if (!calendarHome) {
|
|
34
|
+
const calendars = await client.fetchCalendars();
|
|
35
|
+
|
|
36
|
+
if (!calendars || calendars.length === 0) {
|
|
37
|
+
throw new Error('Cannot determine calendar home: No calendar home found and no existing calendars available.');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Extract calendar home from an existing calendar URL
|
|
41
|
+
// Example: https://dav.example.com/calendars/user/calendar-name/ -> https://dav.example.com/calendars/user/
|
|
42
|
+
const existingCalendarUrl = calendars[0].url;
|
|
43
|
+
calendarHome = existingCalendarUrl.substring(0, existingCalendarUrl.lastIndexOf('/', existingCalendarUrl.length - 2) + 1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return calendarHome;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Sanitize calendar/event name for URL usage
|
|
51
|
+
* @param {string} name - Display name
|
|
52
|
+
* @returns {string} Sanitized name suitable for URLs
|
|
53
|
+
*/
|
|
54
|
+
export function sanitizeNameForUrl(name) {
|
|
55
|
+
return name
|
|
56
|
+
.toLowerCase()
|
|
57
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
58
|
+
.replace(/-+/g, '-')
|
|
59
|
+
.replace(/^-|-$/g, '');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Find calendar by URL and provide helpful error if not found
|
|
64
|
+
* @param {Array} calendars - List of calendars
|
|
65
|
+
* @param {string} calendarUrl - URL to search for
|
|
66
|
+
* @returns {Object} Calendar object
|
|
67
|
+
* @throws {Error} If calendar not found
|
|
68
|
+
*/
|
|
69
|
+
export function findCalendarOrThrow(calendars, calendarUrl) {
|
|
70
|
+
const calendar = calendars.find(c => c.url === calendarUrl);
|
|
71
|
+
|
|
72
|
+
if (!calendar) {
|
|
73
|
+
const availableUrls = calendars.map(c => c.url).join('\n- ');
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Calendar not found: ${calendarUrl}\n\n` +
|
|
76
|
+
`Available calendar URLs:\n- ${availableUrls}\n\n` +
|
|
77
|
+
`Please use list_calendars first to get the correct calendar URLs.`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return calendar;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Find addressbook by URL and provide helpful error if not found
|
|
86
|
+
* @param {Array} addressbooks - List of addressbooks
|
|
87
|
+
* @param {string} addressbookUrl - URL to search for
|
|
88
|
+
* @returns {Object} Addressbook object
|
|
89
|
+
* @throws {Error} If addressbook not found
|
|
90
|
+
*/
|
|
91
|
+
export function findAddressbookOrThrow(addressbooks, addressbookUrl) {
|
|
92
|
+
const addressbook = addressbooks.find(ab => ab.url === addressbookUrl);
|
|
93
|
+
|
|
94
|
+
if (!addressbook) {
|
|
95
|
+
const availableUrls = addressbooks.map(ab => ab.url).join('\n- ');
|
|
96
|
+
throw new Error(
|
|
97
|
+
`Address book not found: ${addressbookUrl}\n\n` +
|
|
98
|
+
`Available address book URLs:\n- ${availableUrls}\n\n` +
|
|
99
|
+
`Please use list_addressbooks first to get the correct URLs.`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return addressbook;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Build time range options for queries
|
|
108
|
+
* @param {string} timeRangeStart - Start date (ISO 8601)
|
|
109
|
+
* @param {string} timeRangeEnd - End date (ISO 8601)
|
|
110
|
+
* @returns {Object} Time range options object
|
|
111
|
+
*/
|
|
112
|
+
export function buildTimeRangeOptions(timeRangeStart, timeRangeEnd) {
|
|
113
|
+
const options = {};
|
|
114
|
+
|
|
115
|
+
if (timeRangeStart && !timeRangeEnd) {
|
|
116
|
+
// Default to 1 year from start if only start provided
|
|
117
|
+
const startDate = new Date(timeRangeStart);
|
|
118
|
+
const endDate = new Date(startDate);
|
|
119
|
+
endDate.setFullYear(endDate.getFullYear() + 1);
|
|
120
|
+
options.timeRange = {
|
|
121
|
+
start: timeRangeStart,
|
|
122
|
+
end: endDate.toISOString(),
|
|
123
|
+
};
|
|
124
|
+
} else if (timeRangeStart && timeRangeEnd) {
|
|
125
|
+
options.timeRange = {
|
|
126
|
+
start: timeRangeStart,
|
|
127
|
+
end: timeRangeEnd,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return options;
|
|
132
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { tsdavManager } from '../../tsdav-client.js';
|
|
2
|
+
import { validateInput, createTodoSchema, sanitizeICalString } from '../../validation.js';
|
|
3
|
+
import { formatSuccess } from '../../formatters.js';
|
|
4
|
+
import { formatICalDate } from '../shared/helpers.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create a new todo/task in a calendar
|
|
8
|
+
*/
|
|
9
|
+
export const createTodo = {
|
|
10
|
+
name: 'create_todo',
|
|
11
|
+
description: 'Create a new todo/task in a calendar. Use this when user wants to add a task, todo item, or reminder with optional due date, priority, and status.',
|
|
12
|
+
inputSchema: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
calendar_url: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
description: 'The URL of the calendar to create the todo in',
|
|
18
|
+
},
|
|
19
|
+
summary: {
|
|
20
|
+
type: 'string',
|
|
21
|
+
description: 'The title/summary of the todo (required)',
|
|
22
|
+
},
|
|
23
|
+
description: {
|
|
24
|
+
type: 'string',
|
|
25
|
+
description: 'Optional detailed description',
|
|
26
|
+
},
|
|
27
|
+
due_date: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
description: 'Optional due date in ISO 8601 format (e.g., 2025-12-31T23:59:59+02:00)',
|
|
30
|
+
},
|
|
31
|
+
priority: {
|
|
32
|
+
type: 'number',
|
|
33
|
+
description: 'Optional priority: 0=none, 1-3=high, 4-6=medium, 7-9=low',
|
|
34
|
+
},
|
|
35
|
+
status: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
enum: ['NEEDS-ACTION', 'IN-PROCESS', 'COMPLETED', 'CANCELLED'],
|
|
38
|
+
description: 'Optional status (default: NEEDS-ACTION)',
|
|
39
|
+
},
|
|
40
|
+
percent_complete: {
|
|
41
|
+
type: 'number',
|
|
42
|
+
description: 'Optional completion percentage (0-100)',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
required: ['calendar_url', 'summary'],
|
|
46
|
+
},
|
|
47
|
+
handler: async (args) => {
|
|
48
|
+
const validated = validateInput(createTodoSchema, args);
|
|
49
|
+
const client = tsdavManager.getCalDavClient();
|
|
50
|
+
|
|
51
|
+
// Build VTODO iCalendar string
|
|
52
|
+
const uid = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}@tsdav-mcp`;
|
|
53
|
+
const dtstamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
|
|
54
|
+
|
|
55
|
+
let vtodo = 'BEGIN:VCALENDAR\r\n';
|
|
56
|
+
vtodo += 'VERSION:2.0\r\n';
|
|
57
|
+
vtodo += 'PRODID:-//tsdav-mcp-server//NONSGML v1.2.0//EN\r\n';
|
|
58
|
+
vtodo += 'BEGIN:VTODO\r\n';
|
|
59
|
+
vtodo += `UID:${uid}\r\n`;
|
|
60
|
+
vtodo += `DTSTAMP:${dtstamp}\r\n`;
|
|
61
|
+
vtodo += `SUMMARY:${sanitizeICalString(validated.summary)}\r\n`;
|
|
62
|
+
|
|
63
|
+
if (validated.description) {
|
|
64
|
+
vtodo += `DESCRIPTION:${sanitizeICalString(validated.description)}\r\n`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (validated.status) {
|
|
68
|
+
vtodo += `STATUS:${validated.status}\r\n`;
|
|
69
|
+
} else {
|
|
70
|
+
vtodo += 'STATUS:NEEDS-ACTION\r\n';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (validated.priority !== undefined) {
|
|
74
|
+
vtodo += `PRIORITY:${validated.priority}\r\n`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (validated.due_date) {
|
|
78
|
+
const dueDate = new Date(validated.due_date).toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
|
|
79
|
+
vtodo += `DUE:${dueDate}\r\n`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (validated.percent_complete !== undefined) {
|
|
83
|
+
vtodo += `PERCENT-COMPLETE:${validated.percent_complete}\r\n`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
vtodo += 'END:VTODO\r\n';
|
|
87
|
+
vtodo += 'END:VCALENDAR\r\n';
|
|
88
|
+
|
|
89
|
+
const result = await client.createTodo({
|
|
90
|
+
calendar: { url: validated.calendar_url },
|
|
91
|
+
filename: `${Date.now()}.ics`,
|
|
92
|
+
iCalString: vtodo,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return formatSuccess('Todo created successfully', {
|
|
96
|
+
url: result.url,
|
|
97
|
+
etag: result.etag,
|
|
98
|
+
summary: validated.summary,
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { tsdavManager } from '../../tsdav-client.js';
|
|
2
|
+
import { validateInput, deleteTodoSchema } from '../../validation.js';
|
|
3
|
+
import { formatSuccess } from '../../formatters.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Delete a todo/task permanently
|
|
7
|
+
*/
|
|
8
|
+
export const deleteTodo = {
|
|
9
|
+
name: 'delete_todo',
|
|
10
|
+
description: 'Delete a todo/task permanently from the calendar. Cannot be undone. Requires todo URL and etag.',
|
|
11
|
+
inputSchema: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
todo_url: {
|
|
15
|
+
type: 'string',
|
|
16
|
+
description: 'The URL of the todo to delete',
|
|
17
|
+
},
|
|
18
|
+
todo_etag: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
description: 'The current ETag of the todo',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
required: ['todo_url', 'todo_etag'],
|
|
24
|
+
},
|
|
25
|
+
handler: async (args) => {
|
|
26
|
+
const validated = validateInput(deleteTodoSchema, args);
|
|
27
|
+
const client = tsdavManager.getCalDavClient();
|
|
28
|
+
|
|
29
|
+
await client.deleteTodo({
|
|
30
|
+
todo: {
|
|
31
|
+
url: validated.todo_url,
|
|
32
|
+
etag: validated.todo_etag,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return formatSuccess('Todo deleted successfully');
|
|
37
|
+
},
|
|
38
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Todo Tools - VTODO operations
|
|
3
|
+
* Exports all todo/task-related tools
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { listTodos } from './list-todos.js';
|
|
7
|
+
export { createTodo } from './create-todo.js';
|
|
8
|
+
export { updateTodoFields } from './update-todo-fields.js';
|
|
9
|
+
export { updateTodoRaw } from './update-todo-raw.js';
|
|
10
|
+
export { deleteTodo } from './delete-todo.js';
|
|
11
|
+
export { todoQuery } from './todo-query.js';
|
|
12
|
+
export { todoMultiGet } from './todo-multi-get.js';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { tsdavManager } from '../../tsdav-client.js';
|
|
2
|
+
import { validateInput, listTodosSchema } from '../../validation.js';
|
|
3
|
+
import { formatTodoList } from '../../formatters.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* List ALL todos/tasks from a calendar
|
|
7
|
+
*/
|
|
8
|
+
export const listTodos = {
|
|
9
|
+
name: 'list_todos',
|
|
10
|
+
description: 'List ALL todos/tasks from a calendar. WARNING: Returns all todos without filtering - use todo_query for searches with filters by status, summary, or due date.',
|
|
11
|
+
inputSchema: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
calendar_url: {
|
|
15
|
+
type: 'string',
|
|
16
|
+
description: 'The URL of the calendar containing todos',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
required: ['calendar_url'],
|
|
20
|
+
},
|
|
21
|
+
handler: async (args) => {
|
|
22
|
+
const validated = validateInput(listTodosSchema, args);
|
|
23
|
+
const client = tsdavManager.getCalDavClient();
|
|
24
|
+
|
|
25
|
+
const calendar = { url: validated.calendar_url };
|
|
26
|
+
const todos = await client.fetchTodos({ calendar });
|
|
27
|
+
|
|
28
|
+
return formatTodoList(todos, validated.calendar_url);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { tsdavManager } from '../../tsdav-client.js';
|
|
2
|
+
import { validateInput, todoMultiGetSchema } from '../../validation.js';
|
|
3
|
+
import { formatTodoList } from '../../formatters.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Batch fetch multiple specific todos by their URLs
|
|
7
|
+
*/
|
|
8
|
+
export const todoMultiGet = {
|
|
9
|
+
name: 'todo_multi_get',
|
|
10
|
+
description: 'Batch fetch multiple specific todos by their URLs. More efficient than fetching one by one when you have exact todo URLs.',
|
|
11
|
+
inputSchema: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
todo_urls: {
|
|
15
|
+
type: 'array',
|
|
16
|
+
items: { type: 'string' },
|
|
17
|
+
description: 'Array of todo URLs to fetch',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
required: ['todo_urls'],
|
|
21
|
+
},
|
|
22
|
+
handler: async (args) => {
|
|
23
|
+
const validated = validateInput(todoMultiGetSchema, args);
|
|
24
|
+
const client = tsdavManager.getCalDavClient();
|
|
25
|
+
|
|
26
|
+
// Extract calendar URL from first todo URL
|
|
27
|
+
const calendarUrl = validated.todo_urls[0].split('/').slice(0, -1).join('/');
|
|
28
|
+
|
|
29
|
+
const todos = await client.todoMultiGet({
|
|
30
|
+
url: calendarUrl,
|
|
31
|
+
props: [{ name: 'getetag', namespace: 'DAV:' }, { name: 'calendar-data', namespace: 'urn:ietf:params:xml:ns:caldav' }],
|
|
32
|
+
objectUrls: validated.todo_urls,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return formatTodoList(todos, calendarUrl);
|
|
36
|
+
},
|
|
37
|
+
};
|