@atlisp/mcp 1.0.19 → 1.0.20
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 +116 -1
- package/package.json +2 -1
- package/src/atlisp-mcp.js +284 -200
- package/src/cad-worker.js +11 -4
- package/src/cad.js +31 -24
- package/src/config.js +28 -0
- package/src/constants.js +2 -1
- package/src/errors.js +55 -0
- package/src/handlers/function-handlers.js +25 -13
- package/src/handlers/prompt-handlers.js +268 -0
- package/src/handlers/resource-handlers.js +82 -35
- package/src/sse-session-manager.js +1 -3
- package/src/sse-session.js +125 -15
- package/src/subscription-manager.js +11 -0
package/src/atlisp-mcp.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import rawBody from 'raw-body';
|
|
3
3
|
import iconv from 'iconv-lite';
|
|
4
|
+
import rateLimit from 'express-rate-limit';
|
|
4
5
|
const { decode: charsetDecode, encodingExists } = iconv;
|
|
5
6
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
6
7
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
@@ -11,28 +12,58 @@ import {
|
|
|
11
12
|
ReadResourceRequestSchema,
|
|
12
13
|
SubscribeRequestSchema,
|
|
13
14
|
UnsubscribeRequestSchema,
|
|
15
|
+
ListPromptsRequestSchema,
|
|
16
|
+
GetPromptRequestSchema,
|
|
14
17
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
15
18
|
import { cad } from './cad.js';
|
|
16
19
|
import { CAD_PLATFORMS, FILE_EXTENSIONS, PROTOCOL_VERSION, SERVER_NAME, SERVER_VERSION } from './constants.js';
|
|
17
|
-
import { log } from './logger.js';
|
|
20
|
+
import { log, closeLog } from './logger.js';
|
|
18
21
|
import * as cadHandlers from './handlers/cad-handlers.js';
|
|
19
22
|
import * as packageHandlers from './handlers/package-handlers.js';
|
|
20
23
|
import * as functionHandlers from './handlers/function-handlers.js';
|
|
21
24
|
import { listResources, readResource } from './handlers/resource-handlers.js';
|
|
25
|
+
import { listPrompts, getPrompt } from './handlers/prompt-handlers.js';
|
|
22
26
|
import * as subMgr from './subscription-manager.js';
|
|
23
27
|
import { SseSession, SSE_EVENTS, createSseResponseHandler, parseLastEventId } from './sse-session.js';
|
|
24
28
|
import { SseSessionManager } from './sse-session-manager.js';
|
|
29
|
+
import { createError, toMcpError } from './errors.js';
|
|
30
|
+
import config from './config.js';
|
|
25
31
|
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
const SERVER_CAPABILITIES = {
|
|
33
|
+
tools: {},
|
|
34
|
+
resources: { subscribe: true, listChanged: true },
|
|
35
|
+
prompts: {}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const SERVER_INFO = {
|
|
39
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
40
|
+
capabilities: SERVER_CAPABILITIES,
|
|
41
|
+
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const { port, host, transport, apiKey, enableSse, rateLimitWindow, rateLimitMax } = config;
|
|
45
|
+
|
|
46
|
+
if (apiKey) log('API Key authentication enabled');
|
|
47
|
+
if (enableSse) log('SSE mode enabled');
|
|
48
|
+
|
|
49
|
+
const mcpLimiter = rateLimit({
|
|
50
|
+
windowMs: rateLimitWindow,
|
|
51
|
+
max: rateLimitMax,
|
|
52
|
+
standardHeaders: true,
|
|
53
|
+
legacyHeaders: false,
|
|
54
|
+
message: { error: '请求过于频繁,请稍后再试' }
|
|
55
|
+
});
|
|
33
56
|
|
|
34
57
|
const sseSessionManager = new SseSessionManager();
|
|
35
58
|
|
|
59
|
+
subMgr.setNotify((uri, data) => {
|
|
60
|
+
sseSessionManager.broadcast(SSE_EVENTS.MESSAGE, {
|
|
61
|
+
jsonrpc: '2.0',
|
|
62
|
+
method: 'notifications/resources/updated',
|
|
63
|
+
params: { uri, data }
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
36
67
|
export { CAD_PLATFORMS, FILE_EXTENSIONS, MOCK_PACKAGES } from './constants.js';
|
|
37
68
|
|
|
38
69
|
const TOOL_HANDLERS = {
|
|
@@ -183,7 +214,31 @@ export const tools = [
|
|
|
183
214
|
export async function handleToolCall(name, args) {
|
|
184
215
|
const handler = TOOL_HANDLERS[name];
|
|
185
216
|
if (!handler) return { content: [{ type: 'text', text: `未知工具: ${name}` }], isError: true };
|
|
186
|
-
|
|
217
|
+
try {
|
|
218
|
+
const result = await handler(args || {});
|
|
219
|
+
notifyResourceChanges(name);
|
|
220
|
+
return result;
|
|
221
|
+
} catch (e) {
|
|
222
|
+
log(`Tool call error [${name}]: ${e.message}`);
|
|
223
|
+
return { content: [{ type: 'text', text: `工具执行错误: ${e.message}` }], isError: true };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function notifyResourceChanges(toolName) {
|
|
228
|
+
const resourceMap = {
|
|
229
|
+
connect_cad: ['atlisp://cad/info'],
|
|
230
|
+
new_document: ['atlisp://dwg/name', 'atlisp://dwg/path', 'atlisp://cad/layers', 'atlisp://cad/entities'],
|
|
231
|
+
install_package: ['atlisp://packages'],
|
|
232
|
+
install_atlisp: ['atlisp://packages'],
|
|
233
|
+
};
|
|
234
|
+
const uris = resourceMap[toolName];
|
|
235
|
+
if (uris) {
|
|
236
|
+
for (const uri of uris) {
|
|
237
|
+
if (subMgr.isSubscribed(uri)) {
|
|
238
|
+
subMgr.notify(uri, null);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
187
242
|
}
|
|
188
243
|
|
|
189
244
|
async function initCadConnection() {
|
|
@@ -206,7 +261,7 @@ let server = null;
|
|
|
206
261
|
export function createMcpServer() {
|
|
207
262
|
server = new Server(
|
|
208
263
|
{ name: SERVER_NAME, version: SERVER_VERSION },
|
|
209
|
-
{ capabilities:
|
|
264
|
+
{ capabilities: SERVER_CAPABILITIES }
|
|
210
265
|
);
|
|
211
266
|
|
|
212
267
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
|
|
@@ -252,88 +307,82 @@ export function createMcpServer() {
|
|
|
252
307
|
return { success: true };
|
|
253
308
|
});
|
|
254
309
|
|
|
310
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
311
|
+
prompts: await listPrompts()
|
|
312
|
+
}));
|
|
313
|
+
|
|
314
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
315
|
+
const { name, arguments: args } = request.params;
|
|
316
|
+
return await getPrompt(name, args || {});
|
|
317
|
+
});
|
|
318
|
+
|
|
255
319
|
return server;
|
|
256
320
|
}
|
|
257
321
|
|
|
258
|
-
async function
|
|
322
|
+
async function dispatchMcpMethod(responder, method, params, id) {
|
|
259
323
|
try {
|
|
260
324
|
if (method === 'initialize') {
|
|
261
|
-
|
|
262
|
-
protocolVersion: PROTOCOL_VERSION,
|
|
263
|
-
capabilities: { tools: {}, resources: { subscribe: true, listChanged: true } },
|
|
264
|
-
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
|
|
265
|
-
} });
|
|
325
|
+
responder({ jsonrpc: '2.0', id, result: SERVER_INFO });
|
|
266
326
|
return;
|
|
267
327
|
}
|
|
268
328
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const resources = await listResources(subMgr.list());
|
|
276
|
-
session.sendResponse({ jsonrpc: '2.0', id, result: { resources } });
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (method === 'resources/read') {
|
|
281
|
-
const uri = params?.uri;
|
|
282
|
-
if (!uri) {
|
|
283
|
-
session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } });
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
try {
|
|
329
|
+
const methodHandlers = {
|
|
330
|
+
'tools/list': async () => ({ tools }),
|
|
331
|
+
'resources/list': async () => ({ resources: await listResources(subMgr.list()) }),
|
|
332
|
+
'resources/read': async () => {
|
|
333
|
+
const uri = params?.uri;
|
|
334
|
+
if (!uri) throw createError('INVALID_PARAMS', { detail: '缺少 uri 参数' });
|
|
287
335
|
const data = await readResource(uri);
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
336
|
+
return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(data) }] };
|
|
337
|
+
},
|
|
338
|
+
'resources/subscribe': async () => {
|
|
339
|
+
const uri = params?.uri;
|
|
340
|
+
if (!uri) throw createError('INVALID_PARAMS', { detail: '缺少 uri 参数' });
|
|
341
|
+
subMgr.subscribe(uri);
|
|
342
|
+
return { success: true };
|
|
343
|
+
},
|
|
344
|
+
'resources/unsubscribe': async () => {
|
|
345
|
+
const uri = params?.uri;
|
|
346
|
+
if (!uri) throw createError('INVALID_PARAMS', { detail: '缺少 uri 参数' });
|
|
347
|
+
subMgr.unsubscribe(uri);
|
|
348
|
+
return { success: true };
|
|
349
|
+
},
|
|
350
|
+
'tools/call': async () => {
|
|
351
|
+
const toolName = params?.name;
|
|
352
|
+
if (!toolName) throw createError('INVALID_PARAMS', { detail: 'missing tool name' });
|
|
353
|
+
return await handleToolCall(toolName, params?.arguments || {});
|
|
354
|
+
},
|
|
355
|
+
'prompts/list': async () => ({ prompts: await listPrompts() }),
|
|
356
|
+
'prompts/get': async () => {
|
|
357
|
+
const promptName = params?.name;
|
|
358
|
+
if (!promptName) throw createError('INVALID_PARAMS', { detail: '缺少 prompt name 参数' });
|
|
359
|
+
return await getPrompt(promptName, params?.arguments || {});
|
|
300
360
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const handler = methodHandlers[method];
|
|
364
|
+
if (handler) {
|
|
365
|
+
const result = await handler();
|
|
366
|
+
responder({ jsonrpc: '2.0', id, result });
|
|
367
|
+
} else {
|
|
368
|
+
responder({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } });
|
|
304
369
|
}
|
|
305
|
-
|
|
306
|
-
if (method === 'resources/unsubscribe') {
|
|
307
|
-
const uri = params?.uri;
|
|
308
|
-
if (!uri) {
|
|
309
|
-
session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } });
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
subMgr.unsubscribe(uri);
|
|
313
|
-
session.sendResponse({ jsonrpc: '2.0', id, result: { success: true } });
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
if (method === 'tools/call') {
|
|
318
|
-
const toolName = params?.name;
|
|
319
|
-
if (!toolName) {
|
|
320
|
-
session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32602, message: 'Invalid params: missing tool name' } });
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
const result = await handleToolCall(toolName, params?.arguments || {});
|
|
324
|
-
session.sendResponse({ jsonrpc: '2.0', id, result });
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } });
|
|
329
370
|
} catch (e) {
|
|
330
|
-
|
|
331
|
-
|
|
371
|
+
const mcpError = toMcpError(e);
|
|
372
|
+
log(`MCP error [${method}]: ${mcpError.message}`);
|
|
373
|
+
responder({ jsonrpc: '2.0', id, error: { code: -32603, message: mcpError.message } });
|
|
332
374
|
}
|
|
333
375
|
}
|
|
334
376
|
|
|
377
|
+
async function handleMcpRequest(session, method, params, id) {
|
|
378
|
+
await dispatchMcpMethod(
|
|
379
|
+
(response) => session.sendResponse(response),
|
|
380
|
+
method, params, id
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
335
384
|
export async function startServer() {
|
|
336
|
-
if (
|
|
385
|
+
if (transport === 'stdio') {
|
|
337
386
|
await initCadConnection();
|
|
338
387
|
const mcpServer = createMcpServer();
|
|
339
388
|
const transport = new StdioServerTransport();
|
|
@@ -366,11 +415,11 @@ export async function startServer() {
|
|
|
366
415
|
});
|
|
367
416
|
|
|
368
417
|
const PUBLIC_PATHS = ['/health'];
|
|
369
|
-
if (
|
|
418
|
+
if (apiKey) {
|
|
370
419
|
app.use((req, res, next) => {
|
|
371
420
|
if (PUBLIC_PATHS.includes(req.path)) return next();
|
|
372
421
|
const auth = req.get('Authorization');
|
|
373
|
-
if (auth === `Bearer ${
|
|
422
|
+
if (auth === `Bearer ${apiKey}`) return next();
|
|
374
423
|
res.status(401).json({ error: 'Unauthorized' });
|
|
375
424
|
});
|
|
376
425
|
}
|
|
@@ -379,11 +428,33 @@ export async function startServer() {
|
|
|
379
428
|
res.json({ status: 'ok', cad: cad.connected ? 'connected' : 'disconnected' });
|
|
380
429
|
});
|
|
381
430
|
|
|
382
|
-
|
|
431
|
+
app.use((req, res, next) => {
|
|
432
|
+
const path = req.path;
|
|
433
|
+
if (path.startsWith('/%7B') || path.startsWith('/{"')) {
|
|
434
|
+
try {
|
|
435
|
+
const decoded = decodeURIComponent(path.slice(1));
|
|
436
|
+
if (decoded.startsWith('{"path":')) {
|
|
437
|
+
const data = JSON.parse(decoded);
|
|
438
|
+
const endpoint = data.path;
|
|
439
|
+
log(`Client sent SSE endpoint in URL path, extracting: ${endpoint}`);
|
|
440
|
+
req.url = endpoint + ((req._parsedUrl && req._parsedUrl.search) || '');
|
|
441
|
+
try { req.path = endpoint; } catch (_) { /* Express 5: req.path is read-only, req.url is sufficient */ }
|
|
442
|
+
}
|
|
443
|
+
} catch (e) {
|
|
444
|
+
log(`Failed to parse malformed path: ${e.message}`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
next();
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
if (config.enableCors) {
|
|
383
451
|
app.use((req, res, next) => {
|
|
384
|
-
res.setHeader('Access-Control-Allow-Origin',
|
|
385
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
386
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Accept, mcp-session-id');
|
|
452
|
+
res.setHeader('Access-Control-Allow-Origin', config.corsOrigin);
|
|
453
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, CONNECT');
|
|
454
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Accept, mcp-session-id, last-event-id');
|
|
455
|
+
res.setHeader('Access-Control-Expose-Headers', 'mcp-session-id, content-type, x-accel-buffering');
|
|
456
|
+
res.setHeader('Access-Control-Max-Age', '86400');
|
|
457
|
+
res.setHeader('Vary', 'Accept');
|
|
387
458
|
if (req.method === 'OPTIONS') return res.status(204).end();
|
|
388
459
|
next();
|
|
389
460
|
});
|
|
@@ -391,47 +462,108 @@ export async function startServer() {
|
|
|
391
462
|
|
|
392
463
|
createMcpServer();
|
|
393
464
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
465
|
+
function setupSseConnection(req, res) {
|
|
466
|
+
log(`SSE connect: ${req.method} ${req.path} - session: ${req.get('mcp-session-id') || 'new'}`);
|
|
467
|
+
const sessionId = req.get('mcp-session-id');
|
|
468
|
+
const lastEventId = parseLastEventId(req);
|
|
469
|
+
const clientId = sessionId || crypto.randomUUID();
|
|
470
|
+
|
|
471
|
+
res.setHeader('mcp-session-id', clientId);
|
|
472
|
+
|
|
473
|
+
let session;
|
|
474
|
+
if (sessionId && sseSessionManager.has(sessionId)) {
|
|
475
|
+
session = sseSessionManager.get(sessionId);
|
|
476
|
+
} else {
|
|
477
|
+
session = new SseSession(res, { clientId, resumeFromId: lastEventId });
|
|
407
478
|
sseSessionManager.add(clientId, session);
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (lastEventId && lastEventId > 0) {
|
|
482
|
+
session.sendEvent(SSE_EVENTS.MESSAGE, { type: 'resume', lastEventId }, lastEventId + 1);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
session.sendPing();
|
|
486
|
+
|
|
487
|
+
res.on('close', () => {
|
|
488
|
+
log(`SSE close: session ${clientId}`);
|
|
489
|
+
sseSessionManager.remove(clientId);
|
|
412
490
|
});
|
|
491
|
+
}
|
|
413
492
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
493
|
+
if (enableSse) {
|
|
494
|
+
app.get('/sse', (req, res) => setupSseConnection(req, res));
|
|
495
|
+
app.get('/mcp/sse', (req, res) => setupSseConnection(req, res));
|
|
496
|
+
|
|
497
|
+
app.post('/mcp/message', mcpLimiter, async (req, res) => {
|
|
498
|
+
const sessionId = req.get('mcp-session-id') || '';
|
|
499
|
+
const accept = req.get('Accept') || '';
|
|
500
|
+
const wantsSse = accept.includes('text/event-stream');
|
|
501
|
+
log(`POST /mcp/message - session: ${sessionId || 'new'} - wantsSSE: ${wantsSse}`);
|
|
502
|
+
|
|
503
|
+
let session;
|
|
504
|
+
if (sessionId && sseSessionManager.has(sessionId)) {
|
|
505
|
+
session = sseSessionManager.get(sessionId);
|
|
506
|
+
} else if (wantsSse) {
|
|
507
|
+
const clientId = sessionId || crypto.randomUUID();
|
|
508
|
+
res.setHeader('mcp-session-id', clientId);
|
|
509
|
+
session = new SseSession(res, { clientId });
|
|
510
|
+
sseSessionManager.add(clientId, session);
|
|
511
|
+
log(`POST /mcp/message - created new session: ${clientId}`);
|
|
512
|
+
res.on('close', () => {
|
|
513
|
+
log(`POST SSE close: session ${clientId}`);
|
|
514
|
+
sseSessionManager.remove(clientId);
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const body = req.body || {};
|
|
519
|
+
const { method, params, id } = body;
|
|
520
|
+
|
|
521
|
+
if (!id) {
|
|
522
|
+
if (session) res.status(204).end();
|
|
523
|
+
else res.status(202).end();
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (session) {
|
|
528
|
+
try {
|
|
529
|
+
await handleMcpRequest(session, method, params, id);
|
|
530
|
+
} catch (e) {
|
|
531
|
+
log('SSE message error: ' + e.message);
|
|
532
|
+
session.sendError(-32603, e.message, id);
|
|
533
|
+
}
|
|
534
|
+
res.status(204).end();
|
|
535
|
+
} else {
|
|
536
|
+
await dispatchMcpMethod(
|
|
537
|
+
(response) => {
|
|
538
|
+
res.setHeader('Content-Type', 'application/json');
|
|
539
|
+
res.status(200).json(response);
|
|
540
|
+
},
|
|
541
|
+
method, params, id
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
});
|
|
432
545
|
}
|
|
433
546
|
|
|
434
|
-
app.
|
|
547
|
+
app.get('/mcp', (req, res) => {
|
|
548
|
+
const accept = req.get('Accept') || '';
|
|
549
|
+
if (accept.includes('text/event-stream')) {
|
|
550
|
+
if (!enableSse) {
|
|
551
|
+
res.status(400).json({ error: 'SSE mode is disabled' });
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
setupSseConnection(req, res);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
res.status(200).json({
|
|
558
|
+
name: SERVER_NAME,
|
|
559
|
+
version: SERVER_VERSION,
|
|
560
|
+
transport: 'streamable-http',
|
|
561
|
+
sseEndpoint: '/sse',
|
|
562
|
+
messageEndpoint: '/mcp/message'
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
app.post('/mcp', mcpLimiter, async (req, res) => {
|
|
435
567
|
const body = req.body || {};
|
|
436
568
|
const { method, params, id } = body;
|
|
437
569
|
const accept = req.get('Accept') || '';
|
|
@@ -440,110 +572,62 @@ export async function startServer() {
|
|
|
440
572
|
log(`${req.method} ${req.path} - session: ${sessionId} - method: ${method} - wantsSSE: ${wantsSse}`);
|
|
441
573
|
|
|
442
574
|
if (wantsSse) {
|
|
443
|
-
if (!
|
|
575
|
+
if (!enableSse) {
|
|
444
576
|
res.status(400).json({ error: 'SSE mode is disabled' });
|
|
445
577
|
return;
|
|
446
578
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
res.setHeader('
|
|
450
|
-
|
|
579
|
+
|
|
580
|
+
const clientId = sessionId || crypto.randomUUID();
|
|
581
|
+
res.setHeader('mcp-session-id', clientId);
|
|
582
|
+
|
|
583
|
+
let session;
|
|
451
584
|
if (sessionId && sseSessionManager.has(sessionId)) {
|
|
452
|
-
|
|
585
|
+
session = sseSessionManager.get(sessionId);
|
|
453
586
|
if (!id) {
|
|
454
587
|
res.status(204).end();
|
|
455
588
|
return;
|
|
456
589
|
}
|
|
457
590
|
await handleMcpRequest(session, method, params, id);
|
|
458
|
-
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
res.setHeader('mcp-session-id', sessionId || crypto.randomUUID());
|
|
463
|
-
const { send } = createSseResponseHandler(res, wantsSse);
|
|
464
|
-
|
|
465
|
-
if (!id) return res.status(204).end();
|
|
466
|
-
|
|
467
|
-
try {
|
|
468
|
-
if (method === 'initialize') {
|
|
469
|
-
send({ jsonrpc: '2.0', id, result: {
|
|
470
|
-
protocolVersion: PROTOCOL_VERSION,
|
|
471
|
-
capabilities: { tools: {}, resources: { subscribe: true, listChanged: true } },
|
|
472
|
-
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
|
|
473
|
-
} }, true);
|
|
591
|
+
res.status(204).end();
|
|
474
592
|
return;
|
|
475
593
|
}
|
|
476
594
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
if (method === 'resources/list') {
|
|
483
|
-
const resources = await listResources(subMgr.list());
|
|
484
|
-
send({ jsonrpc: '2.0', id, result: { resources } }, true);
|
|
485
|
-
return;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
if (method === 'resources/read') {
|
|
489
|
-
const uri = params?.uri;
|
|
490
|
-
if (!uri) {
|
|
491
|
-
send({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } }, true);
|
|
492
|
-
return;
|
|
493
|
-
}
|
|
494
|
-
try {
|
|
495
|
-
const data = await readResource(uri);
|
|
496
|
-
send({ jsonrpc: '2.0', id, result: { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(data) }] } }, true);
|
|
497
|
-
} catch (e) {
|
|
498
|
-
send({ jsonrpc: '2.0', id, error: { code: -32603, message: e.message } }, true);
|
|
499
|
-
}
|
|
500
|
-
return;
|
|
501
|
-
}
|
|
595
|
+
session = new SseSession(res, { clientId });
|
|
596
|
+
sseSessionManager.add(clientId, session);
|
|
502
597
|
|
|
503
|
-
if (
|
|
504
|
-
const uri = params?.uri;
|
|
505
|
-
if (!uri) {
|
|
506
|
-
send({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } }, true);
|
|
507
|
-
return;
|
|
508
|
-
}
|
|
509
|
-
subMgr.subscribe(uri);
|
|
510
|
-
send({ jsonrpc: '2.0', id, result: { success: true } }, true);
|
|
598
|
+
if (!id) {
|
|
511
599
|
return;
|
|
512
600
|
}
|
|
601
|
+
await handleMcpRequest(session, method, params, id);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
513
604
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
if (!uri) {
|
|
517
|
-
send({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } }, true);
|
|
518
|
-
return;
|
|
519
|
-
}
|
|
520
|
-
subMgr.unsubscribe(uri);
|
|
521
|
-
send({ jsonrpc: '2.0', id, result: { success: true } }, true);
|
|
522
|
-
return;
|
|
523
|
-
}
|
|
605
|
+
res.setHeader('mcp-session-id', sessionId || crypto.randomUUID());
|
|
606
|
+
const { send } = createSseResponseHandler(res, wantsSse);
|
|
524
607
|
|
|
525
|
-
|
|
526
|
-
const toolName = params?.name;
|
|
527
|
-
if (!toolName) {
|
|
528
|
-
send({ jsonrpc: '2.0', id, error: { code: -32602, message: 'Invalid params: missing tool name' } }, true);
|
|
529
|
-
return;
|
|
530
|
-
}
|
|
531
|
-
const result = await handleToolCall(toolName, params?.arguments || {});
|
|
532
|
-
send({ jsonrpc: '2.0', id, result }, true);
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
608
|
+
if (!id) return res.status(204).end();
|
|
535
609
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
}
|
|
610
|
+
await dispatchMcpMethod(
|
|
611
|
+
(response) => send(response, true),
|
|
612
|
+
method, params, id
|
|
613
|
+
);
|
|
541
614
|
});
|
|
542
615
|
|
|
543
|
-
app.listen(
|
|
616
|
+
const httpServer = app.listen(config.port, config.host, async () => {
|
|
544
617
|
await initCadConnection();
|
|
545
|
-
console.error(`@lisp MCP Server 启动 - http://${
|
|
618
|
+
console.error(`@lisp MCP Server 启动 - http://${config.host}:${config.port} - Streamable HTTP`);
|
|
546
619
|
});
|
|
620
|
+
|
|
621
|
+
async function shutdown(signal) {
|
|
622
|
+
console.error(`\n收到 ${signal},正在优雅关闭...`);
|
|
623
|
+
sseSessionManager.closeAll();
|
|
624
|
+
await cad.disconnect();
|
|
625
|
+
closeLog();
|
|
626
|
+
httpServer.close(() => process.exit(0));
|
|
627
|
+
setTimeout(() => process.exit(1), 5000);
|
|
628
|
+
}
|
|
629
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
630
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
547
631
|
}
|
|
548
632
|
}
|
|
549
633
|
|
package/src/cad-worker.js
CHANGED
|
@@ -11,7 +11,9 @@ const DEBUG_FILE = process.env.DEBUG_FILE || path.join(process.env.TEMP || '/tmp
|
|
|
11
11
|
|
|
12
12
|
function log(msg) {
|
|
13
13
|
const entry = `${new Date().toISOString()} ${msg}\n`;
|
|
14
|
-
|
|
14
|
+
if (process.env.DEBUG) {
|
|
15
|
+
fs.appendFile(DEBUG_FILE, entry, () => {});
|
|
16
|
+
}
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
const cadConnect = edge.func(function() {/*
|
|
@@ -221,10 +223,13 @@ const cadSendWithResult = edge.func(function() {/*
|
|
|
221
223
|
|
|
222
224
|
process.stdin.setEncoding('utf8');
|
|
223
225
|
|
|
226
|
+
let stdinBuffer = '';
|
|
224
227
|
process.stdin.on('data', (data) => {
|
|
225
|
-
|
|
228
|
+
stdinBuffer += data;
|
|
229
|
+
const lines = stdinBuffer.split('\n');
|
|
230
|
+
stdinBuffer = lines.pop() || '';
|
|
226
231
|
for (const line of lines) {
|
|
227
|
-
if (!line) continue;
|
|
232
|
+
if (!line.trim()) continue;
|
|
228
233
|
try {
|
|
229
234
|
const msg = JSON.parse(line);
|
|
230
235
|
handleMessage(msg).then(result => {
|
|
@@ -374,7 +379,9 @@ async function handleMessage(msg) {
|
|
|
374
379
|
});
|
|
375
380
|
});
|
|
376
381
|
if (result && result.success) return result;
|
|
377
|
-
} catch (e) {
|
|
382
|
+
} catch (e) {
|
|
383
|
+
log('cadConnect failed: ' + e.message);
|
|
384
|
+
}
|
|
378
385
|
return new Promise((resolve, reject) => {
|
|
379
386
|
cadLaunch({}, (e, r) => {
|
|
380
387
|
if (e) reject(e);
|