calendar-mcp 1.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/README.md +159 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +261 -0
- package/dist/tools/create-event.d.ts +7 -0
- package/dist/tools/create-event.js +57 -0
- package/dist/tools/delete-event.d.ts +7 -0
- package/dist/tools/delete-event.js +29 -0
- package/dist/tools/find-free.d.ts +7 -0
- package/dist/tools/find-free.js +119 -0
- package/dist/tools/list-events.d.ts +11 -0
- package/dist/tools/list-events.js +46 -0
- package/dist/tools/search-events.d.ts +7 -0
- package/dist/tools/search-events.js +43 -0
- package/dist/tools/update-event.d.ts +7 -0
- package/dist/tools/update-event.js +64 -0
- package/dist/types.d.ts +134 -0
- package/dist/types.js +1 -0
- package/dist/utils/dates.d.ts +58 -0
- package/dist/utils/dates.js +145 -0
- package/dist/utils/git.d.ts +35 -0
- package/dist/utils/git.js +88 -0
- package/dist/utils/ical.d.ts +47 -0
- package/dist/utils/ical.js +249 -0
- package/dist/utils/paths.d.ts +21 -0
- package/dist/utils/paths.js +50 -0
- package/package.json +60 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
+
import { getCalendarPath, ensureCalendarDir, pathExists } from './paths.js';
|
|
4
|
+
import { formatICalDateTime, formatICalDate, parseICalDate, toISO } from './dates.js';
|
|
5
|
+
const PRODID = '-//Calendar MCP//EN';
|
|
6
|
+
const VERSION = '2.0';
|
|
7
|
+
const CALNAME = 'My Calendar';
|
|
8
|
+
const TIMEZONE = 'UTC';
|
|
9
|
+
const UID_DOMAIN = 'calendar.local';
|
|
10
|
+
/**
|
|
11
|
+
* Generate a unique event ID
|
|
12
|
+
*/
|
|
13
|
+
export function generateUID() {
|
|
14
|
+
return `${uuidv4()}@${UID_DOMAIN}`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Extract the ID part from a UID (without domain)
|
|
18
|
+
*/
|
|
19
|
+
export function extractId(uid) {
|
|
20
|
+
const atIndex = uid.indexOf('@');
|
|
21
|
+
return atIndex > 0 ? uid.slice(0, atIndex) : uid;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Create a full UID from an ID
|
|
25
|
+
*/
|
|
26
|
+
export function createUID(id) {
|
|
27
|
+
if (id.includes('@')) {
|
|
28
|
+
return id;
|
|
29
|
+
}
|
|
30
|
+
return `${id}@${UID_DOMAIN}`;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Escape special characters for iCal text fields
|
|
34
|
+
*/
|
|
35
|
+
export function escapeICalText(text) {
|
|
36
|
+
return text
|
|
37
|
+
.replace(/\\/g, '\\\\')
|
|
38
|
+
.replace(/;/g, '\\;')
|
|
39
|
+
.replace(/,/g, '\\,')
|
|
40
|
+
.replace(/\n/g, '\\n');
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Unescape special characters from iCal text fields
|
|
44
|
+
*/
|
|
45
|
+
export function unescapeICalText(text) {
|
|
46
|
+
return text
|
|
47
|
+
.replace(/\\n/g, '\n')
|
|
48
|
+
.replace(/\\,/g, ',')
|
|
49
|
+
.replace(/\\;/g, ';')
|
|
50
|
+
.replace(/\\\\/g, '\\');
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Fold long lines according to iCal spec (max 75 chars per line)
|
|
54
|
+
*/
|
|
55
|
+
export function foldLine(line) {
|
|
56
|
+
if (line.length <= 75) {
|
|
57
|
+
return line;
|
|
58
|
+
}
|
|
59
|
+
const result = [];
|
|
60
|
+
let remaining = line;
|
|
61
|
+
// First line can be 75 chars
|
|
62
|
+
result.push(remaining.slice(0, 75));
|
|
63
|
+
remaining = remaining.slice(75);
|
|
64
|
+
// Continuation lines start with space and can be 74 chars of content
|
|
65
|
+
while (remaining.length > 0) {
|
|
66
|
+
result.push(' ' + remaining.slice(0, 74));
|
|
67
|
+
remaining = remaining.slice(74);
|
|
68
|
+
}
|
|
69
|
+
return result.join('\r\n');
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Unfold lines that were folded according to iCal spec
|
|
73
|
+
*/
|
|
74
|
+
export function unfoldLines(content) {
|
|
75
|
+
// Replace CRLF + space/tab with nothing (unfold)
|
|
76
|
+
return content.replace(/\r?\n[ \t]/g, '');
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Parse iCalendar content to events
|
|
80
|
+
*/
|
|
81
|
+
export function parseICalendar(content) {
|
|
82
|
+
const unfolded = unfoldLines(content);
|
|
83
|
+
const lines = unfolded.split(/\r?\n/);
|
|
84
|
+
const events = [];
|
|
85
|
+
let currentEvent = null;
|
|
86
|
+
for (const line of lines) {
|
|
87
|
+
const trimmed = line.trim();
|
|
88
|
+
if (!trimmed)
|
|
89
|
+
continue;
|
|
90
|
+
if (trimmed === 'BEGIN:VEVENT') {
|
|
91
|
+
currentEvent = { allDay: false };
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (trimmed === 'END:VEVENT') {
|
|
95
|
+
if (currentEvent && currentEvent.uid && currentEvent.dtstart && currentEvent.summary) {
|
|
96
|
+
events.push(icalEventToCalendarEvent(currentEvent));
|
|
97
|
+
}
|
|
98
|
+
currentEvent = null;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (!currentEvent)
|
|
102
|
+
continue;
|
|
103
|
+
// Parse property
|
|
104
|
+
const colonIndex = trimmed.indexOf(':');
|
|
105
|
+
if (colonIndex === -1)
|
|
106
|
+
continue;
|
|
107
|
+
const propertyPart = trimmed.slice(0, colonIndex);
|
|
108
|
+
const valuePart = trimmed.slice(colonIndex + 1);
|
|
109
|
+
// Handle properties with parameters (e.g., DTSTART;VALUE=DATE:20250125)
|
|
110
|
+
const semiIndex = propertyPart.indexOf(';');
|
|
111
|
+
const propertyName = semiIndex > 0 ? propertyPart.slice(0, semiIndex) : propertyPart;
|
|
112
|
+
const params = semiIndex > 0 ? propertyPart.slice(semiIndex + 1) : '';
|
|
113
|
+
switch (propertyName.toUpperCase()) {
|
|
114
|
+
case 'UID':
|
|
115
|
+
currentEvent.uid = valuePart;
|
|
116
|
+
break;
|
|
117
|
+
case 'SUMMARY':
|
|
118
|
+
currentEvent.summary = unescapeICalText(valuePart);
|
|
119
|
+
break;
|
|
120
|
+
case 'DESCRIPTION':
|
|
121
|
+
currentEvent.description = unescapeICalText(valuePart);
|
|
122
|
+
break;
|
|
123
|
+
case 'LOCATION':
|
|
124
|
+
currentEvent.location = unescapeICalText(valuePart);
|
|
125
|
+
break;
|
|
126
|
+
case 'DTSTART': {
|
|
127
|
+
const isAllDay = params.toUpperCase().includes('VALUE=DATE');
|
|
128
|
+
const parsed = parseICalDate(valuePart);
|
|
129
|
+
currentEvent.dtstart = parsed.date;
|
|
130
|
+
currentEvent.allDay = isAllDay || parsed.allDay;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
case 'DTEND': {
|
|
134
|
+
const parsed = parseICalDate(valuePart);
|
|
135
|
+
currentEvent.dtend = parsed.date;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
case 'DTSTAMP': {
|
|
139
|
+
const parsed = parseICalDate(valuePart);
|
|
140
|
+
currentEvent.dtstamp = parsed.date;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case 'CREATED': {
|
|
144
|
+
const parsed = parseICalDate(valuePart);
|
|
145
|
+
currentEvent.created = parsed.date;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
case 'LAST-MODIFIED': {
|
|
149
|
+
const parsed = parseICalDate(valuePart);
|
|
150
|
+
currentEvent.lastModified = parsed.date;
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
case 'STATUS':
|
|
154
|
+
currentEvent.status = valuePart.toUpperCase();
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return events;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Convert internal ICalEvent to CalendarEvent format
|
|
162
|
+
*/
|
|
163
|
+
function icalEventToCalendarEvent(event) {
|
|
164
|
+
return {
|
|
165
|
+
id: extractId(event.uid),
|
|
166
|
+
title: event.summary,
|
|
167
|
+
start: toISO(event.dtstart),
|
|
168
|
+
end: event.dtend ? toISO(event.dtend) : undefined,
|
|
169
|
+
location: event.location,
|
|
170
|
+
description: event.description,
|
|
171
|
+
allDay: event.allDay,
|
|
172
|
+
status: event.status ?? 'CONFIRMED',
|
|
173
|
+
created: event.created ? toISO(event.created) : undefined,
|
|
174
|
+
lastModified: event.lastModified ? toISO(event.lastModified) : undefined,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Generate iCalendar content from events
|
|
179
|
+
*/
|
|
180
|
+
export function generateICalendar(events) {
|
|
181
|
+
const lines = [
|
|
182
|
+
'BEGIN:VCALENDAR',
|
|
183
|
+
`VERSION:${VERSION}`,
|
|
184
|
+
`PRODID:${PRODID}`,
|
|
185
|
+
'CALSCALE:GREGORIAN',
|
|
186
|
+
'METHOD:PUBLISH',
|
|
187
|
+
`X-WR-CALNAME:${CALNAME}`,
|
|
188
|
+
`X-WR-TIMEZONE:${TIMEZONE}`,
|
|
189
|
+
];
|
|
190
|
+
for (const event of events) {
|
|
191
|
+
lines.push('BEGIN:VEVENT');
|
|
192
|
+
lines.push(`UID:${createUID(event.id)}`);
|
|
193
|
+
lines.push(`DTSTAMP:${formatICalDateTime(new Date())}`);
|
|
194
|
+
const startDate = new Date(event.start);
|
|
195
|
+
if (event.allDay) {
|
|
196
|
+
lines.push(`DTSTART;VALUE=DATE:${formatICalDate(startDate)}`);
|
|
197
|
+
if (event.end) {
|
|
198
|
+
lines.push(`DTEND;VALUE=DATE:${formatICalDate(new Date(event.end))}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
lines.push(`DTSTART:${formatICalDateTime(startDate)}`);
|
|
203
|
+
if (event.end) {
|
|
204
|
+
lines.push(`DTEND:${formatICalDateTime(new Date(event.end))}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
lines.push(`SUMMARY:${escapeICalText(event.title)}`);
|
|
208
|
+
if (event.description) {
|
|
209
|
+
lines.push(foldLine(`DESCRIPTION:${escapeICalText(event.description)}`));
|
|
210
|
+
}
|
|
211
|
+
if (event.location) {
|
|
212
|
+
lines.push(foldLine(`LOCATION:${escapeICalText(event.location)}`));
|
|
213
|
+
}
|
|
214
|
+
lines.push(`STATUS:${event.status}`);
|
|
215
|
+
if (event.created) {
|
|
216
|
+
lines.push(`CREATED:${formatICalDateTime(new Date(event.created))}`);
|
|
217
|
+
}
|
|
218
|
+
if (event.lastModified) {
|
|
219
|
+
lines.push(`LAST-MODIFIED:${formatICalDateTime(new Date(event.lastModified))}`);
|
|
220
|
+
}
|
|
221
|
+
lines.push('END:VEVENT');
|
|
222
|
+
}
|
|
223
|
+
lines.push('END:VCALENDAR');
|
|
224
|
+
// iCal uses CRLF line endings
|
|
225
|
+
return lines.join('\r\n') + '\r\n';
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Read the calendar file and return parsed events
|
|
229
|
+
* Returns empty array if file doesn't exist
|
|
230
|
+
*/
|
|
231
|
+
export async function readCalendarFile() {
|
|
232
|
+
const calendarPath = getCalendarPath();
|
|
233
|
+
const exists = await pathExists(calendarPath);
|
|
234
|
+
if (!exists) {
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
const content = await readFile(calendarPath, 'utf-8');
|
|
238
|
+
return parseICalendar(content);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Write events to the calendar file
|
|
242
|
+
* Creates the Calendar directory if it doesn't exist
|
|
243
|
+
*/
|
|
244
|
+
export async function writeCalendarFile(events) {
|
|
245
|
+
await ensureCalendarDir();
|
|
246
|
+
const calendarPath = getCalendarPath();
|
|
247
|
+
const content = generateICalendar(events);
|
|
248
|
+
await writeFile(calendarPath, content, 'utf-8');
|
|
249
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get the vault path from environment variable
|
|
3
|
+
* @throws Error if VAULT_PATH is not set
|
|
4
|
+
*/
|
|
5
|
+
export declare function getVaultPath(): string;
|
|
6
|
+
/**
|
|
7
|
+
* Get the Calendar directory path within the vault
|
|
8
|
+
*/
|
|
9
|
+
export declare function getCalendarDir(): string;
|
|
10
|
+
/**
|
|
11
|
+
* Get the full path to the events.ics file
|
|
12
|
+
*/
|
|
13
|
+
export declare function getCalendarPath(): string;
|
|
14
|
+
/**
|
|
15
|
+
* Check if a path exists
|
|
16
|
+
*/
|
|
17
|
+
export declare function pathExists(path: string): Promise<boolean>;
|
|
18
|
+
/**
|
|
19
|
+
* Ensure the Calendar directory exists, creating it if necessary
|
|
20
|
+
*/
|
|
21
|
+
export declare function ensureCalendarDir(): Promise<void>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { mkdir, access } from 'fs/promises';
|
|
3
|
+
import { constants } from 'fs';
|
|
4
|
+
const CALENDAR_FOLDER = 'Calendar';
|
|
5
|
+
const CALENDAR_FILE = 'events.ics';
|
|
6
|
+
/**
|
|
7
|
+
* Get the vault path from environment variable
|
|
8
|
+
* @throws Error if VAULT_PATH is not set
|
|
9
|
+
*/
|
|
10
|
+
export function getVaultPath() {
|
|
11
|
+
const vaultPath = process.env.VAULT_PATH;
|
|
12
|
+
if (!vaultPath) {
|
|
13
|
+
throw new Error('VAULT_PATH environment variable is not set');
|
|
14
|
+
}
|
|
15
|
+
return vaultPath;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Get the Calendar directory path within the vault
|
|
19
|
+
*/
|
|
20
|
+
export function getCalendarDir() {
|
|
21
|
+
return join(getVaultPath(), CALENDAR_FOLDER);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Get the full path to the events.ics file
|
|
25
|
+
*/
|
|
26
|
+
export function getCalendarPath() {
|
|
27
|
+
return join(getCalendarDir(), CALENDAR_FILE);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Check if a path exists
|
|
31
|
+
*/
|
|
32
|
+
export async function pathExists(path) {
|
|
33
|
+
try {
|
|
34
|
+
await access(path, constants.F_OK);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Ensure the Calendar directory exists, creating it if necessary
|
|
43
|
+
*/
|
|
44
|
+
export async function ensureCalendarDir() {
|
|
45
|
+
const calendarDir = getCalendarDir();
|
|
46
|
+
const exists = await pathExists(calendarDir);
|
|
47
|
+
if (!exists) {
|
|
48
|
+
await mkdir(calendarDir, { recursive: true });
|
|
49
|
+
}
|
|
50
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "calendar-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for calendar management via Claude Code - uses iCalendar format",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Masbuc53/PersonalCalendar.git",
|
|
10
|
+
"directory": "calendar-mcp"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/Masbuc53/PersonalCalendar/tree/main/calendar-mcp#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/Masbuc53/PersonalCalendar/issues"
|
|
15
|
+
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"calendar-mcp": "dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc",
|
|
25
|
+
"start": "node dist/index.js",
|
|
26
|
+
"dev": "tsc --watch",
|
|
27
|
+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
28
|
+
"test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
|
|
29
|
+
"prepublishOnly": "npm run build"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"mcp",
|
|
33
|
+
"model-context-protocol",
|
|
34
|
+
"claude",
|
|
35
|
+
"claude-code",
|
|
36
|
+
"calendar",
|
|
37
|
+
"icalendar",
|
|
38
|
+
"obsidian"
|
|
39
|
+
],
|
|
40
|
+
"author": "Mason Buchanan",
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
44
|
+
"chrono-node": "^2.7.0",
|
|
45
|
+
"ical.js": "^2.0.0",
|
|
46
|
+
"simple-git": "^3.22.0",
|
|
47
|
+
"uuid": "^9.0.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/jest": "^29.5.0",
|
|
51
|
+
"@types/node": "^20.10.0",
|
|
52
|
+
"@types/uuid": "^9.0.0",
|
|
53
|
+
"jest": "^29.7.0",
|
|
54
|
+
"ts-jest": "^29.1.0",
|
|
55
|
+
"typescript": "^5.3.0"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=18.0.0"
|
|
59
|
+
}
|
|
60
|
+
}
|