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
package/src/logger.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple JSON logger with millisecond precision
|
|
3
|
+
* Outputs structured JSON logs to stdout (HTTP) or stderr (STDIO)
|
|
4
|
+
*
|
|
5
|
+
* CRITICAL: In STDIO mode, all output MUST go to stderr to avoid
|
|
6
|
+
* corrupting JSON-RPC messages on stdout.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Detect STDIO transport mode - must write to stderr to preserve stdout for JSON-RPC
|
|
10
|
+
const isStdioMode = process.env.MCP_TRANSPORT === 'stdio';
|
|
11
|
+
|
|
12
|
+
class JSONLogger {
|
|
13
|
+
constructor(context = {}, level = 'info') {
|
|
14
|
+
this.context = context;
|
|
15
|
+
this.level = level;
|
|
16
|
+
this.levels = { error: 0, warn: 1, info: 2, debug: 3 };
|
|
17
|
+
this.minLevel = this.levels[process.env.LOG_LEVEL?.toLowerCase() || 'info'] || 2;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Format timestamp with milliseconds (HH:MM:ss.mmm)
|
|
22
|
+
*/
|
|
23
|
+
formatTimestamp() {
|
|
24
|
+
const now = new Date();
|
|
25
|
+
const hours = String(now.getHours()).padStart(2, '0');
|
|
26
|
+
const minutes = String(now.getMinutes()).padStart(2, '0');
|
|
27
|
+
const seconds = String(now.getSeconds()).padStart(2, '0');
|
|
28
|
+
const milliseconds = String(now.getMilliseconds()).padStart(3, '0');
|
|
29
|
+
return `${hours}:${minutes}:${seconds}.${milliseconds}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Log a message at the specified level
|
|
34
|
+
*/
|
|
35
|
+
log(level, objOrMsg, msg) {
|
|
36
|
+
// Check if level should be logged
|
|
37
|
+
if (this.levels[level] > this.minLevel) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const timestamp = this.formatTimestamp();
|
|
42
|
+
let logData = {
|
|
43
|
+
time: timestamp,
|
|
44
|
+
level: level.toUpperCase(),
|
|
45
|
+
...this.context,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Handle both log(level, obj, msg) and log(level, msg) patterns
|
|
49
|
+
if (typeof objOrMsg === 'string') {
|
|
50
|
+
logData.msg = objOrMsg;
|
|
51
|
+
} else if (typeof objOrMsg === 'object' && objOrMsg !== null) {
|
|
52
|
+
logData = { ...logData, ...objOrMsg };
|
|
53
|
+
if (msg) {
|
|
54
|
+
logData.msg = msg;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Output function: stderr for STDIO mode, stdout for HTTP mode
|
|
59
|
+
const output = isStdioMode
|
|
60
|
+
? (msg) => process.stderr.write(msg + '\n')
|
|
61
|
+
: (msg) => console.log(msg);
|
|
62
|
+
|
|
63
|
+
// Pretty-print in development, single-line JSON in production
|
|
64
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
65
|
+
// Development: colored output with readable format
|
|
66
|
+
const colorMap = {
|
|
67
|
+
error: '\x1b[31m', // Red
|
|
68
|
+
warn: '\x1b[33m', // Yellow
|
|
69
|
+
info: '\x1b[32m', // Green
|
|
70
|
+
debug: '\x1b[36m', // Cyan
|
|
71
|
+
};
|
|
72
|
+
const reset = '\x1b[0m';
|
|
73
|
+
const color = colorMap[level] || '';
|
|
74
|
+
|
|
75
|
+
const { time, level: lvl, msg: message, ...rest } = logData;
|
|
76
|
+
const extraFields = Object.keys(rest).length > 0
|
|
77
|
+
? ' ' + JSON.stringify(rest)
|
|
78
|
+
: '';
|
|
79
|
+
|
|
80
|
+
output(`[${time}] ${color}${lvl}${reset}: ${message || ''}${extraFields}`);
|
|
81
|
+
} else {
|
|
82
|
+
// Production: single-line JSON
|
|
83
|
+
output(JSON.stringify(logData));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
info(objOrMsg, msg) {
|
|
88
|
+
this.log('info', objOrMsg, msg);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
warn(objOrMsg, msg) {
|
|
92
|
+
this.log('warn', objOrMsg, msg);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
error(objOrMsg, msg) {
|
|
96
|
+
this.log('error', objOrMsg, msg);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
debug(objOrMsg, msg) {
|
|
100
|
+
this.log('debug', objOrMsg, msg);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Create child logger with additional context
|
|
105
|
+
*/
|
|
106
|
+
child(additionalContext) {
|
|
107
|
+
return new JSONLogger(
|
|
108
|
+
{ ...this.context, ...additionalContext },
|
|
109
|
+
this.level
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Main logger instance
|
|
116
|
+
*/
|
|
117
|
+
export const logger = new JSONLogger();
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Create child logger with context
|
|
121
|
+
*/
|
|
122
|
+
export function createContextLogger(context) {
|
|
123
|
+
return logger.child(context);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Create logger with request ID for tracing
|
|
128
|
+
*/
|
|
129
|
+
export function createRequestLogger(requestId, additionalContext = {}) {
|
|
130
|
+
return logger.child({
|
|
131
|
+
requestId,
|
|
132
|
+
...additionalContext,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Create logger with session ID for tracing
|
|
138
|
+
*/
|
|
139
|
+
export function createSessionLogger(sessionId, additionalContext = {}) {
|
|
140
|
+
return logger.child({
|
|
141
|
+
sessionId,
|
|
142
|
+
...additionalContext,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dav-mcp Streamable HTTP Server (Stateless)
|
|
3
|
+
*
|
|
4
|
+
* Modern MCP transport for remote clients (n8n, cloud deployments)
|
|
5
|
+
* Implements the MCP Streamable HTTP specification in stateless mode.
|
|
6
|
+
*
|
|
7
|
+
* Each request is independent - no session state maintained.
|
|
8
|
+
* Suitable for horizontal scaling and multi-node deployments.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* node src/server-http.js
|
|
12
|
+
*
|
|
13
|
+
* Configuration via environment variables:
|
|
14
|
+
* - PORT: Server port (default: 3000)
|
|
15
|
+
* - BEARER_TOKEN: Required for authentication
|
|
16
|
+
* - CORS_ALLOWED_ORIGINS: Comma-separated list of allowed origins
|
|
17
|
+
* - CALDAV_SERVER_URL, CALDAV_USERNAME, CALDAV_PASSWORD: CalDAV credentials
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import express from 'express';
|
|
21
|
+
import cors from 'cors';
|
|
22
|
+
import crypto from 'crypto';
|
|
23
|
+
import dotenv from 'dotenv';
|
|
24
|
+
import rateLimit from 'express-rate-limit';
|
|
25
|
+
|
|
26
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
27
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
28
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
29
|
+
|
|
30
|
+
import { tsdavManager } from './tsdav-client.js';
|
|
31
|
+
import { tools } from './tools/index.js';
|
|
32
|
+
import { createToolErrorResponse, MCP_ERROR_CODES } from './error-handler.js';
|
|
33
|
+
import { logger, createRequestLogger } from './logger.js';
|
|
34
|
+
import { initializeToolCallLogger, getToolCallLogger } from './tool-call-logger.js';
|
|
35
|
+
|
|
36
|
+
// Load environment variables
|
|
37
|
+
dotenv.config();
|
|
38
|
+
|
|
39
|
+
const app = express();
|
|
40
|
+
const PORT = process.env.PORT || 3000;
|
|
41
|
+
|
|
42
|
+
// CORS Configuration
|
|
43
|
+
const allowedOrigins = process.env.CORS_ALLOWED_ORIGINS
|
|
44
|
+
? process.env.CORS_ALLOWED_ORIGINS.split(',')
|
|
45
|
+
: ['http://localhost:5678', 'http://localhost:3000'];
|
|
46
|
+
|
|
47
|
+
app.use(cors({
|
|
48
|
+
origin: (origin, callback) => {
|
|
49
|
+
if (!origin) return callback(null, true);
|
|
50
|
+
if (allowedOrigins.indexOf(origin) !== -1 || allowedOrigins.includes('*')) {
|
|
51
|
+
callback(null, true);
|
|
52
|
+
} else {
|
|
53
|
+
callback(new Error('Not allowed by CORS'));
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
credentials: true,
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
// Body parser
|
|
60
|
+
app.use(express.json());
|
|
61
|
+
|
|
62
|
+
// Rate Limiting
|
|
63
|
+
const limiter = rateLimit({
|
|
64
|
+
windowMs: 15 * 60 * 1000,
|
|
65
|
+
max: (req) => {
|
|
66
|
+
const ip = req.ip || req.connection.remoteAddress;
|
|
67
|
+
if (ip === '127.0.0.1' || ip === '::1' || ip === '::ffff:127.0.0.1' || ip?.startsWith('::ffff:172.')) {
|
|
68
|
+
return 10000;
|
|
69
|
+
}
|
|
70
|
+
return 100;
|
|
71
|
+
},
|
|
72
|
+
message: 'Too many requests from this IP, please try again later.',
|
|
73
|
+
standardHeaders: true,
|
|
74
|
+
legacyHeaders: false,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
app.use('/mcp', limiter);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Bearer token authentication middleware
|
|
81
|
+
*/
|
|
82
|
+
function authenticateBearer(req, res, next) {
|
|
83
|
+
const bearerToken = process.env.BEARER_TOKEN;
|
|
84
|
+
|
|
85
|
+
if (!bearerToken) {
|
|
86
|
+
logger.error('Server misconfiguration: BEARER_TOKEN not set');
|
|
87
|
+
return res.status(500).json({
|
|
88
|
+
jsonrpc: '2.0',
|
|
89
|
+
error: { code: -32603, message: 'Server misconfiguration: BEARER_TOKEN not set' },
|
|
90
|
+
id: null,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const authHeader = req.headers.authorization;
|
|
95
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
96
|
+
logger.warn({ ip: req.ip }, 'Unauthorized: Bearer token required');
|
|
97
|
+
return res.status(401).json({
|
|
98
|
+
jsonrpc: '2.0',
|
|
99
|
+
error: { code: -32001, message: 'Unauthorized: Bearer token required' },
|
|
100
|
+
id: null,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const token = authHeader.substring(7);
|
|
105
|
+
|
|
106
|
+
// Timing-safe comparison
|
|
107
|
+
const tokenBuffer = Buffer.from(token);
|
|
108
|
+
const secretBuffer = Buffer.from(bearerToken);
|
|
109
|
+
|
|
110
|
+
if (tokenBuffer.length !== secretBuffer.length || !crypto.timingSafeEqual(tokenBuffer, secretBuffer)) {
|
|
111
|
+
logger.warn({ ip: req.ip }, 'Unauthorized: Invalid token');
|
|
112
|
+
return res.status(401).json({
|
|
113
|
+
jsonrpc: '2.0',
|
|
114
|
+
error: { code: -32001, message: 'Unauthorized: Invalid token' },
|
|
115
|
+
id: null,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
next();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Initialize tsdav clients
|
|
124
|
+
*/
|
|
125
|
+
async function initializeTsdav() {
|
|
126
|
+
try {
|
|
127
|
+
const authMethod = process.env.AUTH_METHOD || 'Basic';
|
|
128
|
+
|
|
129
|
+
if (authMethod === 'OAuth' || authMethod === 'Oauth') {
|
|
130
|
+
logger.info('Initializing with OAuth2 authentication');
|
|
131
|
+
|
|
132
|
+
if (!process.env.GOOGLE_CLIENT_ID || !process.env.GOOGLE_CLIENT_SECRET || !process.env.GOOGLE_REFRESH_TOKEN) {
|
|
133
|
+
throw new Error('OAuth2 requires GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and GOOGLE_REFRESH_TOKEN');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
await tsdavManager.initialize({
|
|
137
|
+
serverUrl: process.env.GOOGLE_SERVER_URL || 'https://apidata.googleusercontent.com/caldav/v2/',
|
|
138
|
+
authMethod: 'OAuth',
|
|
139
|
+
username: process.env.GOOGLE_USER,
|
|
140
|
+
clientId: process.env.GOOGLE_CLIENT_ID,
|
|
141
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
142
|
+
refreshToken: process.env.GOOGLE_REFRESH_TOKEN,
|
|
143
|
+
tokenUrl: process.env.GOOGLE_TOKEN_URL || 'https://accounts.google.com/o/oauth2/token',
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
logger.info('OAuth2 clients initialized successfully');
|
|
147
|
+
} else {
|
|
148
|
+
logger.info('Initializing with Basic authentication');
|
|
149
|
+
|
|
150
|
+
if (!process.env.CALDAV_SERVER_URL || !process.env.CALDAV_USERNAME || !process.env.CALDAV_PASSWORD) {
|
|
151
|
+
throw new Error('Basic Auth requires CALDAV_SERVER_URL, CALDAV_USERNAME, and CALDAV_PASSWORD');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
await tsdavManager.initialize({
|
|
155
|
+
serverUrl: process.env.CALDAV_SERVER_URL,
|
|
156
|
+
authMethod: 'Basic',
|
|
157
|
+
username: process.env.CALDAV_USERNAME,
|
|
158
|
+
password: process.env.CALDAV_PASSWORD,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
logger.info('Basic Auth clients initialized successfully');
|
|
162
|
+
}
|
|
163
|
+
} catch (error) {
|
|
164
|
+
logger.error({ error: error.message }, 'Failed to initialize tsdav clients');
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Create MCP Server instance for a request
|
|
171
|
+
*/
|
|
172
|
+
function createMCPServer(requestId) {
|
|
173
|
+
const requestLogger = createRequestLogger(requestId);
|
|
174
|
+
|
|
175
|
+
const server = new Server(
|
|
176
|
+
{
|
|
177
|
+
name: process.env.MCP_SERVER_NAME || 'dav-mcp',
|
|
178
|
+
version: process.env.MCP_SERVER_VERSION || '3.0.0',
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
capabilities: {
|
|
182
|
+
tools: {},
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
// Register tools/list handler
|
|
188
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
189
|
+
requestLogger.debug({ count: tools.length }, 'tools/list request');
|
|
190
|
+
return {
|
|
191
|
+
tools: tools.map(t => ({
|
|
192
|
+
name: t.name,
|
|
193
|
+
description: t.description,
|
|
194
|
+
inputSchema: t.inputSchema,
|
|
195
|
+
})),
|
|
196
|
+
};
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Register tools/call handler
|
|
200
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
201
|
+
const toolCallLogger = getToolCallLogger();
|
|
202
|
+
const toolName = request.params.name;
|
|
203
|
+
const args = request.params.arguments || {};
|
|
204
|
+
|
|
205
|
+
requestLogger.info({ tool: toolName }, 'tools/call request');
|
|
206
|
+
|
|
207
|
+
const tool = tools.find(t => t.name === toolName);
|
|
208
|
+
if (!tool) {
|
|
209
|
+
requestLogger.error({ tool: toolName }, 'Tool not found');
|
|
210
|
+
const error = new Error(`Unknown tool: ${toolName}`);
|
|
211
|
+
error.code = MCP_ERROR_CODES.METHOD_NOT_FOUND;
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const startTime = Date.now();
|
|
216
|
+
toolCallLogger.logToolCallStart(toolName, args, {
|
|
217
|
+
requestId,
|
|
218
|
+
transport: 'http',
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const result = await tool.handler(args);
|
|
223
|
+
const duration = Date.now() - startTime;
|
|
224
|
+
|
|
225
|
+
requestLogger.info({ tool: toolName, duration }, 'Tool executed successfully');
|
|
226
|
+
toolCallLogger.logToolCallSuccess(toolName, args, result, {
|
|
227
|
+
requestId,
|
|
228
|
+
transport: 'http',
|
|
229
|
+
duration,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
return result;
|
|
233
|
+
} catch (error) {
|
|
234
|
+
const duration = Date.now() - startTime;
|
|
235
|
+
|
|
236
|
+
requestLogger.error({ tool: toolName, error: error.message }, 'Tool execution error');
|
|
237
|
+
toolCallLogger.logToolCallError(toolName, args, error, {
|
|
238
|
+
requestId,
|
|
239
|
+
transport: 'http',
|
|
240
|
+
duration,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
return createToolErrorResponse(error, process.env.NODE_ENV === 'development');
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
return server;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* POST /mcp - Handle MCP requests (stateless)
|
|
252
|
+
*/
|
|
253
|
+
app.post('/mcp', authenticateBearer, async (req, res) => {
|
|
254
|
+
const requestId = crypto.randomUUID();
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
// Stateless: create new transport and server for each request
|
|
258
|
+
const transport = new StreamableHTTPServerTransport({
|
|
259
|
+
sessionIdGenerator: undefined, // Stateless mode
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const server = createMCPServer(requestId);
|
|
263
|
+
|
|
264
|
+
// Cleanup on request close
|
|
265
|
+
res.on('close', () => {
|
|
266
|
+
transport.close();
|
|
267
|
+
server.close();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
await server.connect(transport);
|
|
271
|
+
await transport.handleRequest(req, res, req.body);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
logger.error({ requestId, error: error.message }, 'Error handling MCP request');
|
|
274
|
+
if (!res.headersSent) {
|
|
275
|
+
res.status(500).json({
|
|
276
|
+
jsonrpc: '2.0',
|
|
277
|
+
error: { code: -32603, message: 'Internal server error' },
|
|
278
|
+
id: null,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* GET /mcp - Not supported in stateless mode
|
|
286
|
+
*/
|
|
287
|
+
app.get('/mcp', (req, res) => {
|
|
288
|
+
res.status(405).json({
|
|
289
|
+
jsonrpc: '2.0',
|
|
290
|
+
error: { code: -32000, message: 'Method not allowed. Use POST for MCP requests.' },
|
|
291
|
+
id: null,
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* DELETE /mcp - Not supported in stateless mode
|
|
297
|
+
*/
|
|
298
|
+
app.delete('/mcp', (req, res) => {
|
|
299
|
+
res.status(405).json({
|
|
300
|
+
jsonrpc: '2.0',
|
|
301
|
+
error: { code: -32000, message: 'Method not allowed. Use POST for MCP requests.' },
|
|
302
|
+
id: null,
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Health check endpoint
|
|
308
|
+
*/
|
|
309
|
+
app.get('/health', (req, res) => {
|
|
310
|
+
res.json({
|
|
311
|
+
status: 'healthy',
|
|
312
|
+
server: process.env.MCP_SERVER_NAME || 'dav-mcp',
|
|
313
|
+
version: process.env.MCP_SERVER_VERSION || '3.0.0',
|
|
314
|
+
transport: 'http-stateless',
|
|
315
|
+
timestamp: new Date().toISOString(),
|
|
316
|
+
tools: tools.length,
|
|
317
|
+
uptime: process.uptime(),
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Info endpoint
|
|
323
|
+
*/
|
|
324
|
+
app.get('/', (req, res) => {
|
|
325
|
+
res.json({
|
|
326
|
+
name: process.env.MCP_SERVER_NAME || 'dav-mcp',
|
|
327
|
+
version: process.env.MCP_SERVER_VERSION || '3.0.0',
|
|
328
|
+
transport: 'http-stateless',
|
|
329
|
+
description: 'MCP Streamable HTTP Server for CalDAV/CardDAV integration (stateless)',
|
|
330
|
+
endpoints: {
|
|
331
|
+
mcp: '/mcp (POST only)',
|
|
332
|
+
health: '/health (GET)',
|
|
333
|
+
},
|
|
334
|
+
tools: tools.map(t => ({
|
|
335
|
+
name: t.name,
|
|
336
|
+
description: t.description,
|
|
337
|
+
})),
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Graceful shutdown handler
|
|
343
|
+
*/
|
|
344
|
+
async function gracefulShutdown(signal) {
|
|
345
|
+
logger.info({ signal }, 'Received shutdown signal, shutting down...');
|
|
346
|
+
|
|
347
|
+
if (httpServer) {
|
|
348
|
+
httpServer.close(() => {
|
|
349
|
+
logger.info('HTTP server closed');
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
logger.info('Shutdown completed');
|
|
354
|
+
process.exit(0);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Register shutdown handlers
|
|
358
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
359
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
360
|
+
|
|
361
|
+
// Handle uncaught errors
|
|
362
|
+
process.on('uncaughtException', (error) => {
|
|
363
|
+
logger.error({ error: error.message, stack: error.stack }, 'Uncaught exception');
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
process.on('unhandledRejection', (reason) => {
|
|
367
|
+
logger.error({ reason }, 'Unhandled promise rejection');
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
let httpServer;
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Start server
|
|
374
|
+
*/
|
|
375
|
+
async function start() {
|
|
376
|
+
logger.info('Starting dav-mcp HTTP Server (stateless)...');
|
|
377
|
+
|
|
378
|
+
// Initialize tsdav clients
|
|
379
|
+
await initializeTsdav();
|
|
380
|
+
|
|
381
|
+
// Initialize tool call logger
|
|
382
|
+
initializeToolCallLogger();
|
|
383
|
+
logger.info('Tool call logger initialized');
|
|
384
|
+
|
|
385
|
+
// Start Express server
|
|
386
|
+
httpServer = app.listen(PORT, () => {
|
|
387
|
+
logger.info({
|
|
388
|
+
port: PORT,
|
|
389
|
+
url: `http://localhost:${PORT}`,
|
|
390
|
+
mcpEndpoint: `http://localhost:${PORT}/mcp`,
|
|
391
|
+
mode: 'stateless',
|
|
392
|
+
}, 'HTTP Server running');
|
|
393
|
+
|
|
394
|
+
logger.info({ count: tools.length }, 'Available tools');
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Start the server
|
|
399
|
+
start().catch(error => {
|
|
400
|
+
logger.error({ error: error.message, stack: error.stack }, 'Failed to start server');
|
|
401
|
+
process.exit(1);
|
|
402
|
+
});
|