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,225 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* dav-mcp STDIO Server
|
|
4
|
+
*
|
|
5
|
+
* Entry point for local MCP clients: Claude Desktop, Cursor, VS Code, npx usage
|
|
6
|
+
*
|
|
7
|
+
* CRITICAL: This server uses STDIO transport. All logging MUST go to stderr
|
|
8
|
+
* to avoid corrupting JSON-RPC messages on stdout.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* MCP_TRANSPORT=stdio node src/server-stdio.js
|
|
12
|
+
*
|
|
13
|
+
* Or via npx (after npm publish):
|
|
14
|
+
* npx dav-mcp
|
|
15
|
+
*
|
|
16
|
+
* Configuration via environment variables:
|
|
17
|
+
* - CALDAV_SERVER_URL: CalDAV server URL
|
|
18
|
+
* - CALDAV_USERNAME: Username for Basic Auth
|
|
19
|
+
* - CALDAV_PASSWORD: Password for Basic Auth
|
|
20
|
+
* - AUTH_METHOD: 'Basic' (default) or 'OAuth'
|
|
21
|
+
*
|
|
22
|
+
* For OAuth2 (Google Calendar):
|
|
23
|
+
* - GOOGLE_SERVER_URL: Google CalDAV URL
|
|
24
|
+
* - GOOGLE_USER: Google account email
|
|
25
|
+
* - GOOGLE_CLIENT_ID: OAuth2 client ID
|
|
26
|
+
* - GOOGLE_CLIENT_SECRET: OAuth2 client secret
|
|
27
|
+
* - GOOGLE_REFRESH_TOKEN: OAuth2 refresh token
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
// Set STDIO mode BEFORE importing logger
|
|
31
|
+
process.env.MCP_TRANSPORT = 'stdio';
|
|
32
|
+
|
|
33
|
+
import dotenv from 'dotenv';
|
|
34
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
35
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
36
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
37
|
+
|
|
38
|
+
import { tsdavManager } from './tsdav-client.js';
|
|
39
|
+
import { tools } from './tools/index.js';
|
|
40
|
+
import { createToolErrorResponse, MCP_ERROR_CODES } from './error-handler.js';
|
|
41
|
+
import { logger } from './logger.js';
|
|
42
|
+
import { initializeToolCallLogger, getToolCallLogger } from './tool-call-logger.js';
|
|
43
|
+
|
|
44
|
+
// Load environment variables
|
|
45
|
+
dotenv.config();
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Initialize tsdav clients based on auth method
|
|
49
|
+
*/
|
|
50
|
+
async function initializeTsdav() {
|
|
51
|
+
const authMethod = process.env.AUTH_METHOD || 'Basic';
|
|
52
|
+
|
|
53
|
+
if (authMethod === 'OAuth' || authMethod === 'Oauth') {
|
|
54
|
+
// OAuth2 Configuration (e.g., Google Calendar)
|
|
55
|
+
logger.info('Initializing with OAuth2 authentication');
|
|
56
|
+
|
|
57
|
+
if (!process.env.GOOGLE_CLIENT_ID || !process.env.GOOGLE_CLIENT_SECRET || !process.env.GOOGLE_REFRESH_TOKEN) {
|
|
58
|
+
throw new Error('OAuth2 requires GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and GOOGLE_REFRESH_TOKEN');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await tsdavManager.initialize({
|
|
62
|
+
serverUrl: process.env.GOOGLE_SERVER_URL || 'https://apidata.googleusercontent.com/caldav/v2/',
|
|
63
|
+
authMethod: 'OAuth',
|
|
64
|
+
username: process.env.GOOGLE_USER,
|
|
65
|
+
clientId: process.env.GOOGLE_CLIENT_ID,
|
|
66
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
67
|
+
refreshToken: process.env.GOOGLE_REFRESH_TOKEN,
|
|
68
|
+
tokenUrl: process.env.GOOGLE_TOKEN_URL || 'https://accounts.google.com/o/oauth2/token',
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
logger.info('OAuth2 clients initialized successfully');
|
|
72
|
+
} else {
|
|
73
|
+
// Basic Auth Configuration (standard CalDAV servers)
|
|
74
|
+
logger.info('Initializing with Basic authentication');
|
|
75
|
+
|
|
76
|
+
if (!process.env.CALDAV_SERVER_URL || !process.env.CALDAV_USERNAME || !process.env.CALDAV_PASSWORD) {
|
|
77
|
+
throw new Error('Basic Auth requires CALDAV_SERVER_URL, CALDAV_USERNAME, and CALDAV_PASSWORD');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await tsdavManager.initialize({
|
|
81
|
+
serverUrl: process.env.CALDAV_SERVER_URL,
|
|
82
|
+
authMethod: 'Basic',
|
|
83
|
+
username: process.env.CALDAV_USERNAME,
|
|
84
|
+
password: process.env.CALDAV_PASSWORD,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
logger.info('Basic Auth clients initialized successfully');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Create MCP Server with tool handlers
|
|
93
|
+
*/
|
|
94
|
+
function createMCPServer() {
|
|
95
|
+
const server = new Server(
|
|
96
|
+
{
|
|
97
|
+
name: process.env.MCP_SERVER_NAME || 'dav-mcp',
|
|
98
|
+
version: process.env.MCP_SERVER_VERSION || '3.0.0',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
capabilities: {
|
|
102
|
+
tools: {},
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// Register tools/list handler
|
|
108
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
109
|
+
logger.debug({ count: tools.length }, 'tools/list request received');
|
|
110
|
+
return {
|
|
111
|
+
tools: tools.map(t => ({
|
|
112
|
+
name: t.name,
|
|
113
|
+
description: t.description,
|
|
114
|
+
inputSchema: t.inputSchema,
|
|
115
|
+
})),
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Register tools/call handler
|
|
120
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
121
|
+
const toolName = request.params.name;
|
|
122
|
+
const args = request.params.arguments || {};
|
|
123
|
+
const toolCallLogger = getToolCallLogger();
|
|
124
|
+
|
|
125
|
+
logger.info({ tool: toolName }, 'tools/call request received');
|
|
126
|
+
|
|
127
|
+
const tool = tools.find(t => t.name === toolName);
|
|
128
|
+
if (!tool) {
|
|
129
|
+
logger.error({ tool: toolName }, 'Tool not found');
|
|
130
|
+
const error = new Error(`Unknown tool: ${toolName}`);
|
|
131
|
+
error.code = MCP_ERROR_CODES.METHOD_NOT_FOUND;
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const startTime = Date.now();
|
|
136
|
+
toolCallLogger.logToolCallStart(toolName, args, { transport: 'stdio' });
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
logger.debug({ tool: toolName }, 'Executing tool');
|
|
140
|
+
const result = await tool.handler(args);
|
|
141
|
+
const duration = Date.now() - startTime;
|
|
142
|
+
|
|
143
|
+
logger.info({ tool: toolName, duration }, 'Tool executed successfully');
|
|
144
|
+
toolCallLogger.logToolCallSuccess(toolName, args, result, {
|
|
145
|
+
transport: 'stdio',
|
|
146
|
+
duration,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
return result;
|
|
150
|
+
} catch (error) {
|
|
151
|
+
const duration = Date.now() - startTime;
|
|
152
|
+
|
|
153
|
+
logger.error({ tool: toolName, error: error.message }, 'Tool execution error');
|
|
154
|
+
toolCallLogger.logToolCallError(toolName, args, error, {
|
|
155
|
+
transport: 'stdio',
|
|
156
|
+
duration,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return createToolErrorResponse(error, process.env.NODE_ENV === 'development');
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return server;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Main entry point
|
|
168
|
+
*/
|
|
169
|
+
async function main() {
|
|
170
|
+
try {
|
|
171
|
+
logger.info('Starting dav-mcp STDIO server...');
|
|
172
|
+
|
|
173
|
+
// Initialize tsdav clients
|
|
174
|
+
await initializeTsdav();
|
|
175
|
+
|
|
176
|
+
// Initialize tool call logger
|
|
177
|
+
initializeToolCallLogger();
|
|
178
|
+
logger.info('Tool call logger initialized');
|
|
179
|
+
|
|
180
|
+
// Create MCP server
|
|
181
|
+
const server = createMCPServer();
|
|
182
|
+
logger.debug({ count: tools.length }, 'MCP server created with tools');
|
|
183
|
+
|
|
184
|
+
// Create STDIO transport
|
|
185
|
+
const transport = new StdioServerTransport();
|
|
186
|
+
|
|
187
|
+
// Connect server to transport
|
|
188
|
+
await server.connect(transport);
|
|
189
|
+
|
|
190
|
+
logger.info({
|
|
191
|
+
name: process.env.MCP_SERVER_NAME || 'dav-mcp',
|
|
192
|
+
version: process.env.MCP_SERVER_VERSION || '3.0.0',
|
|
193
|
+
tools: tools.length,
|
|
194
|
+
}, 'dav-mcp STDIO server ready');
|
|
195
|
+
|
|
196
|
+
} catch (error) {
|
|
197
|
+
logger.error({ error: error.message, stack: error.stack }, 'Fatal error starting server');
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Graceful shutdown handlers
|
|
203
|
+
process.on('SIGTERM', () => {
|
|
204
|
+
logger.info('Received SIGTERM, shutting down...');
|
|
205
|
+
process.exit(0);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
process.on('SIGINT', () => {
|
|
209
|
+
logger.info('Received SIGINT, shutting down...');
|
|
210
|
+
process.exit(0);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Handle uncaught errors gracefully
|
|
214
|
+
process.on('uncaughtException', (error) => {
|
|
215
|
+
logger.error({ error: error.message, stack: error.stack }, 'Uncaught exception');
|
|
216
|
+
process.exit(1);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
process.on('unhandledRejection', (reason) => {
|
|
220
|
+
logger.error({ reason }, 'Unhandled promise rejection');
|
|
221
|
+
process.exit(1);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Start the server
|
|
225
|
+
main();
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tool Call Logger - Logs all MCP tool calls to JSON Lines format
|
|
6
|
+
* Enables real-time monitoring of LLM tool selection behavior
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
class ToolCallLogger {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.enabled = options.enabled !== false;
|
|
12
|
+
this.outputMode = options.outputMode || 'file'; // 'file' | 'console' | 'both'
|
|
13
|
+
this.logFile = options.logFile || '/tmp/mcp-tool-calls.jsonl';
|
|
14
|
+
|
|
15
|
+
if (this.enabled && this.outputMode.includes('file')) {
|
|
16
|
+
this.ensureLogFileExists();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
ensureLogFileExists() {
|
|
21
|
+
const dir = path.dirname(this.logFile);
|
|
22
|
+
if (!fs.existsSync(dir)) {
|
|
23
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
log(entry) {
|
|
28
|
+
if (!this.enabled) return;
|
|
29
|
+
|
|
30
|
+
const line = JSON.stringify(entry) + '\n';
|
|
31
|
+
|
|
32
|
+
if (this.outputMode === 'console' || this.outputMode === 'both') {
|
|
33
|
+
console.log(line.trim());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (this.outputMode === 'file' || this.outputMode === 'both') {
|
|
37
|
+
try {
|
|
38
|
+
fs.appendFileSync(this.logFile, line);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error('Failed to write to tool call log:', error.message);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
logToolCallStart(toolName, args, metadata = {}) {
|
|
46
|
+
this.log({
|
|
47
|
+
type: 'tool_call_start',
|
|
48
|
+
timestamp: new Date().toISOString(),
|
|
49
|
+
tool: toolName,
|
|
50
|
+
args: args,
|
|
51
|
+
...metadata
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
logToolCallSuccess(toolName, args, result, metadata = {}) {
|
|
56
|
+
// Summarize result to avoid huge logs
|
|
57
|
+
const resultSummary = this.summarizeResult(result);
|
|
58
|
+
|
|
59
|
+
this.log({
|
|
60
|
+
type: 'tool_call_success',
|
|
61
|
+
timestamp: new Date().toISOString(),
|
|
62
|
+
tool: toolName,
|
|
63
|
+
args: args,
|
|
64
|
+
result_summary: resultSummary,
|
|
65
|
+
duration_ms: metadata.duration,
|
|
66
|
+
...metadata
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
logToolCallError(toolName, args, error, metadata = {}) {
|
|
71
|
+
this.log({
|
|
72
|
+
type: 'tool_call_error',
|
|
73
|
+
timestamp: new Date().toISOString(),
|
|
74
|
+
tool: toolName,
|
|
75
|
+
args: args,
|
|
76
|
+
error: {
|
|
77
|
+
message: error.message,
|
|
78
|
+
code: error.code,
|
|
79
|
+
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
|
|
80
|
+
},
|
|
81
|
+
duration_ms: metadata.duration,
|
|
82
|
+
...metadata
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
summarizeResult(result) {
|
|
87
|
+
if (!result) return null;
|
|
88
|
+
|
|
89
|
+
// Handle MCP result format
|
|
90
|
+
if (result.content && Array.isArray(result.content)) {
|
|
91
|
+
return {
|
|
92
|
+
type: 'mcp_result',
|
|
93
|
+
content_count: result.content.length,
|
|
94
|
+
content_types: result.content.map(c => c.type),
|
|
95
|
+
has_text: result.content.some(c => c.type === 'text'),
|
|
96
|
+
text_length: result.content
|
|
97
|
+
.filter(c => c.type === 'text')
|
|
98
|
+
.reduce((sum, c) => sum + (c.text?.length || 0), 0)
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Handle plain objects/strings
|
|
103
|
+
if (typeof result === 'string') {
|
|
104
|
+
return { type: 'string', length: result.length };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (typeof result === 'object') {
|
|
108
|
+
return {
|
|
109
|
+
type: 'object',
|
|
110
|
+
keys: Object.keys(result).slice(0, 5),
|
|
111
|
+
key_count: Object.keys(result).length
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { type: typeof result };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
clearLog() {
|
|
119
|
+
if (this.enabled && fs.existsSync(this.logFile)) {
|
|
120
|
+
fs.unlinkSync(this.logFile);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Singleton instance
|
|
126
|
+
let instance = null;
|
|
127
|
+
|
|
128
|
+
export function initializeToolCallLogger(options = {}) {
|
|
129
|
+
const enabled = process.env.LOG_TOOL_CALLS !== 'false';
|
|
130
|
+
const outputMode = process.env.TOOL_CALL_LOG_MODE || 'file';
|
|
131
|
+
const logFile = process.env.TOOL_CALL_LOG_FILE || '/tmp/mcp-tool-calls.jsonl';
|
|
132
|
+
|
|
133
|
+
instance = new ToolCallLogger({
|
|
134
|
+
enabled,
|
|
135
|
+
outputMode,
|
|
136
|
+
logFile,
|
|
137
|
+
...options
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return instance;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function getToolCallLogger() {
|
|
144
|
+
if (!instance) {
|
|
145
|
+
instance = initializeToolCallLogger();
|
|
146
|
+
}
|
|
147
|
+
return instance;
|
|
148
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { tsdavManager } from '../../tsdav-client.js';
|
|
2
|
+
import { validateInput, calendarMultiGetSchema } from '../../validation.js';
|
|
3
|
+
import { formatEventList } from '../../formatters.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Batch fetch multiple specific calendar events by their URLs
|
|
7
|
+
*/
|
|
8
|
+
export const calendarMultiGet = {
|
|
9
|
+
name: 'calendar_multi_get',
|
|
10
|
+
description: 'Batch fetch multiple specific calendar events by their URLs. Use when you have exact event URLs and want to retrieve their details',
|
|
11
|
+
inputSchema: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
calendar_url: {
|
|
15
|
+
type: 'string',
|
|
16
|
+
description: 'The URL of the calendar',
|
|
17
|
+
},
|
|
18
|
+
event_urls: {
|
|
19
|
+
type: 'array',
|
|
20
|
+
items: { type: 'string' },
|
|
21
|
+
description: 'Array of event URLs to fetch',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
required: ['calendar_url', 'event_urls'],
|
|
25
|
+
},
|
|
26
|
+
handler: async (args) => {
|
|
27
|
+
const validated = validateInput(calendarMultiGetSchema, args);
|
|
28
|
+
const client = tsdavManager.getCalDavClient();
|
|
29
|
+
|
|
30
|
+
const events = await client.calendarMultiGet({
|
|
31
|
+
url: validated.calendar_url,
|
|
32
|
+
props: [{ name: 'getetag', namespace: 'DAV:' }, 'calendar-data'],
|
|
33
|
+
objectUrls: validated.event_urls,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return formatEventList(events, { url: validated.calendar_url });
|
|
37
|
+
},
|
|
38
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { tsdavManager } from '../../tsdav-client.js';
|
|
2
|
+
import { validateInput, calendarQuerySchema } from '../../validation.js';
|
|
3
|
+
import { formatEventList } from '../../formatters.js';
|
|
4
|
+
import { buildTimeRangeOptions } from '../shared/helpers.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Search and filter calendar events efficiently
|
|
8
|
+
*/
|
|
9
|
+
export const calendarQuery = {
|
|
10
|
+
name: 'calendar_query',
|
|
11
|
+
description: '⭐ PREFERRED: Search and filter calendar events efficiently. Use instead of list_events to avoid loading thousands of entries. Omit calendar_url to search across ALL calendars automatically.',
|
|
12
|
+
inputSchema: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
calendar_url: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
description: 'Optional: Specific calendar URL. Omit to search ALL calendars (recommended for "find events with X" queries). Only provide if user explicitly names a calendar. DO NOT use list_calendars first - that defeats cross-calendar search.',
|
|
18
|
+
},
|
|
19
|
+
time_range_start: {
|
|
20
|
+
type: 'string',
|
|
21
|
+
description: 'Start datetime (ISO 8601, e.g., 2025-10-30T00:00:00Z). If provided, time_range_end is REQUIRED. Calculate dates for "today", "this week", etc. Can be used alone (with end date) as sufficient filter.',
|
|
22
|
+
},
|
|
23
|
+
time_range_end: {
|
|
24
|
+
type: 'string',
|
|
25
|
+
description: 'End datetime (ISO 8601). If provided, time_range_start is REQUIRED. Both dates together form a complete filter. Do not omit if start is provided.',
|
|
26
|
+
},
|
|
27
|
+
summary_filter: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
description: 'Search event titles/summaries containing this text (case-insensitive). Example: "meeting with Elena" or "standup". Can be used alone as sufficient filter.',
|
|
30
|
+
},
|
|
31
|
+
location_filter: {
|
|
32
|
+
type: 'string',
|
|
33
|
+
description: 'Search event locations containing this text. Example: "Berlin", "Office", "Zoom". Can be used alone as sufficient filter.',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
required: [],
|
|
37
|
+
},
|
|
38
|
+
handler: async (args) => {
|
|
39
|
+
const validated = validateInput(calendarQuerySchema, args);
|
|
40
|
+
const client = tsdavManager.getCalDavClient();
|
|
41
|
+
const calendars = await client.fetchCalendars();
|
|
42
|
+
|
|
43
|
+
// If specific calendar requested, use it
|
|
44
|
+
let calendarsToSearch = calendars;
|
|
45
|
+
if (validated.calendar_url) {
|
|
46
|
+
const calendar = calendars.find(c => c.url === validated.calendar_url);
|
|
47
|
+
if (!calendar) {
|
|
48
|
+
const availableUrls = calendars.map(c => c.url).join('\n- ');
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Calendar not found: ${validated.calendar_url}\n\n` +
|
|
51
|
+
`Available calendar URLs:\n- ${availableUrls}\n\n` +
|
|
52
|
+
`Tip: Omit calendar_url to search across all calendars automatically.`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
calendarsToSearch = [calendar];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Build timeRange options
|
|
59
|
+
const timeRangeOptions = buildTimeRangeOptions(validated.time_range_start, validated.time_range_end);
|
|
60
|
+
|
|
61
|
+
// Search across all selected calendars
|
|
62
|
+
let allEvents = [];
|
|
63
|
+
for (const calendar of calendarsToSearch) {
|
|
64
|
+
const options = { calendar, ...timeRangeOptions };
|
|
65
|
+
const events = await client.fetchCalendarObjects(options);
|
|
66
|
+
// Add calendar info to each event
|
|
67
|
+
events.forEach(event => {
|
|
68
|
+
event._calendarName = calendar.displayName || calendar.url;
|
|
69
|
+
});
|
|
70
|
+
allEvents = allEvents.concat(events);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let filteredEvents = allEvents;
|
|
74
|
+
|
|
75
|
+
if (validated.summary_filter) {
|
|
76
|
+
const summaryLower = validated.summary_filter.toLowerCase();
|
|
77
|
+
filteredEvents = filteredEvents.filter(event => {
|
|
78
|
+
const summary = event.data?.match(/SUMMARY:(.+)/)?.[1] || '';
|
|
79
|
+
return summary.toLowerCase().includes(summaryLower);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (validated.location_filter) {
|
|
84
|
+
const locationLower = validated.location_filter.toLowerCase();
|
|
85
|
+
filteredEvents = filteredEvents.filter(event => {
|
|
86
|
+
const location = event.data?.match(/LOCATION:(.+)/)?.[1] || '';
|
|
87
|
+
return location.toLowerCase().includes(locationLower);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Determine calendar name for display
|
|
92
|
+
const calendarName = calendarsToSearch.length === 1
|
|
93
|
+
? (calendarsToSearch[0].displayName || calendarsToSearch[0].url)
|
|
94
|
+
: `All Calendars (${calendarsToSearch.length})`;
|
|
95
|
+
|
|
96
|
+
return formatEventList(filteredEvents, calendarName);
|
|
97
|
+
},
|
|
98
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { tsdavManager } from '../../tsdav-client.js';
|
|
2
|
+
import { validateInput, createEventSchema, sanitizeICalString } from '../../validation.js';
|
|
3
|
+
import { formatSuccess } from '../../formatters.js';
|
|
4
|
+
import { formatICalDate, generateUID, findCalendarOrThrow } from '../shared/helpers.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create a new calendar event
|
|
8
|
+
*/
|
|
9
|
+
export const createEvent = {
|
|
10
|
+
name: 'create_event',
|
|
11
|
+
description: 'Create a new calendar event with title, date, time, optional description and location',
|
|
12
|
+
inputSchema: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
calendar_url: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
description: 'The URL of the calendar to create the event in',
|
|
18
|
+
},
|
|
19
|
+
summary: {
|
|
20
|
+
type: 'string',
|
|
21
|
+
description: 'Event title/summary',
|
|
22
|
+
},
|
|
23
|
+
start_date: {
|
|
24
|
+
type: 'string',
|
|
25
|
+
description: 'Start date in ISO 8601 format',
|
|
26
|
+
},
|
|
27
|
+
end_date: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
description: 'End date in ISO 8601 format',
|
|
30
|
+
},
|
|
31
|
+
description: {
|
|
32
|
+
type: 'string',
|
|
33
|
+
description: 'Event description (optional)',
|
|
34
|
+
},
|
|
35
|
+
location: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
description: 'Event location (optional)',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
required: ['calendar_url', 'summary', 'start_date', 'end_date'],
|
|
41
|
+
},
|
|
42
|
+
handler: async (args) => {
|
|
43
|
+
const validated = validateInput(createEventSchema, args);
|
|
44
|
+
const client = tsdavManager.getCalDavClient();
|
|
45
|
+
const calendars = await client.fetchCalendars();
|
|
46
|
+
const calendar = findCalendarOrThrow(calendars, validated.calendar_url);
|
|
47
|
+
|
|
48
|
+
const now = new Date();
|
|
49
|
+
const uid = generateUID('event');
|
|
50
|
+
|
|
51
|
+
const summary = sanitizeICalString(validated.summary);
|
|
52
|
+
const description = validated.description ? sanitizeICalString(validated.description) : '';
|
|
53
|
+
const location = validated.location ? sanitizeICalString(validated.location) : '';
|
|
54
|
+
|
|
55
|
+
const iCalString = `BEGIN:VCALENDAR
|
|
56
|
+
VERSION:2.0
|
|
57
|
+
PRODID:-//tsdav-mcp-server//EN
|
|
58
|
+
BEGIN:VEVENT
|
|
59
|
+
UID:${uid}
|
|
60
|
+
DTSTAMP:${formatICalDate(now)}
|
|
61
|
+
DTSTART:${formatICalDate(new Date(validated.start_date))}
|
|
62
|
+
DTEND:${formatICalDate(new Date(validated.end_date))}
|
|
63
|
+
SUMMARY:${summary}${description ? `\nDESCRIPTION:${description}` : ''}${location ? `\nLOCATION:${location}` : ''}
|
|
64
|
+
END:VEVENT
|
|
65
|
+
END:VCALENDAR`;
|
|
66
|
+
|
|
67
|
+
const response = await client.createCalendarObject({
|
|
68
|
+
calendar,
|
|
69
|
+
filename: `${uid}.ics`,
|
|
70
|
+
iCalString,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return formatSuccess('Event created successfully', {
|
|
74
|
+
url: response.url,
|
|
75
|
+
etag: response.etag,
|
|
76
|
+
summary: validated.summary,
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { tsdavManager } from '../../tsdav-client.js';
|
|
2
|
+
import { validateInput, deleteCalendarSchema } from '../../validation.js';
|
|
3
|
+
import { formatCalendarDeleteSuccess } from '../../formatters.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Permanently delete a calendar and all its events
|
|
7
|
+
*/
|
|
8
|
+
export const deleteCalendar = {
|
|
9
|
+
name: 'delete_calendar',
|
|
10
|
+
description: 'Permanently delete a calendar and all its events. WARNING: This action cannot be undone! Use this when user explicitly asks to "delete calendar" or "remove calendar"',
|
|
11
|
+
inputSchema: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
calendar_url: {
|
|
15
|
+
type: 'string',
|
|
16
|
+
description: 'The URL of the calendar to delete (get from list_calendars)',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
required: ['calendar_url'],
|
|
20
|
+
},
|
|
21
|
+
handler: async (args) => {
|
|
22
|
+
const validated = validateInput(deleteCalendarSchema, args);
|
|
23
|
+
const client = tsdavManager.getCalDavClient();
|
|
24
|
+
|
|
25
|
+
// Use deleteObject to send DELETE request
|
|
26
|
+
await client.deleteObject({
|
|
27
|
+
url: validated.calendar_url,
|
|
28
|
+
headers: {
|
|
29
|
+
'Content-Type': 'text/calendar; charset=utf-8',
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Return formatted success with warning
|
|
34
|
+
return formatCalendarDeleteSuccess(validated.calendar_url);
|
|
35
|
+
},
|
|
36
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { tsdavManager } from '../../tsdav-client.js';
|
|
2
|
+
import { validateInput, deleteEventSchema } from '../../validation.js';
|
|
3
|
+
import { formatSuccess } from '../../formatters.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Delete a calendar event permanently
|
|
7
|
+
*/
|
|
8
|
+
export const deleteEvent = {
|
|
9
|
+
name: 'delete_event',
|
|
10
|
+
description: 'Delete a calendar event permanently. Requires event URL and etag',
|
|
11
|
+
inputSchema: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
event_url: {
|
|
15
|
+
type: 'string',
|
|
16
|
+
description: 'The URL of the event to delete',
|
|
17
|
+
},
|
|
18
|
+
event_etag: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
description: 'The etag of the event',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
required: ['event_url', 'event_etag'],
|
|
24
|
+
},
|
|
25
|
+
handler: async (args) => {
|
|
26
|
+
const validated = validateInput(deleteEventSchema, args);
|
|
27
|
+
const client = tsdavManager.getCalDavClient();
|
|
28
|
+
|
|
29
|
+
await client.deleteCalendarObject({
|
|
30
|
+
calendarObject: {
|
|
31
|
+
url: validated.event_url,
|
|
32
|
+
etag: validated.event_etag,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return formatSuccess('Event deleted successfully');
|
|
37
|
+
},
|
|
38
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calendar Tools - CalDAV operations
|
|
3
|
+
* Exports all calendar-related tools
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { listCalendars } from './list-calendars.js';
|
|
7
|
+
export { listEvents } from './list-events.js';
|
|
8
|
+
export { createEvent } from './create-event.js';
|
|
9
|
+
export { updateEventFields } from './update-event-fields.js';
|
|
10
|
+
export { updateEventRaw } from './update-event-raw.js';
|
|
11
|
+
export { deleteEvent } from './delete-event.js';
|
|
12
|
+
export { calendarQuery } from './calendar-query.js';
|
|
13
|
+
export { makeCalendar } from './make-calendar.js';
|
|
14
|
+
export { updateCalendar } from './update-calendar.js';
|
|
15
|
+
export { deleteCalendar } from './delete-calendar.js';
|
|
16
|
+
export { calendarMultiGet } from './calendar-multi-get.js';
|