@atlisp/mcp 1.0.18 → 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 -0
- package/package.json +2 -1
- package/src/atlisp-mcp.js +290 -195
- package/src/cad-worker.js +11 -4
- package/src/cad.js +33 -26
- package/src/config.js +28 -0
- package/src/constants.js +2 -1
- package/src/errors.js +55 -0
- package/src/handlers/cad-handlers.js +2 -2
- 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,32 +12,64 @@ 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
|
-
|
|
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
|
+
});
|
|
31
56
|
|
|
32
57
|
const sseSessionManager = new SseSessionManager();
|
|
33
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
|
+
|
|
34
67
|
export { CAD_PLATFORMS, FILE_EXTENSIONS, MOCK_PACKAGES } from './constants.js';
|
|
35
68
|
|
|
36
69
|
const TOOL_HANDLERS = {
|
|
37
70
|
connect_cad: () => cadHandlers.connectCad(),
|
|
38
71
|
eval_lisp: (a) => cadHandlers.evalLisp(a.code),
|
|
39
|
-
eval_lisp_with_result: (a) => cadHandlers.evalLisp(a.code, true),
|
|
72
|
+
eval_lisp_with_result: (a) => cadHandlers.evalLisp(a.code, true, a.encoding),
|
|
40
73
|
get_cad_info: () => cadHandlers.getCadInfo(),
|
|
41
74
|
list_packages: () => packageHandlers.listPackages(),
|
|
42
75
|
search_packages: (a) => packageHandlers.searchPackages(a.query),
|
|
@@ -76,7 +109,10 @@ export const tools = [
|
|
|
76
109
|
description: '在 CAD 中执行 AutoLISP 代码并返回结果',
|
|
77
110
|
inputSchema: {
|
|
78
111
|
type: 'object',
|
|
79
|
-
properties: {
|
|
112
|
+
properties: {
|
|
113
|
+
code: { type: 'string', description: '要执行的 LISP 代码' },
|
|
114
|
+
encoding: { type: 'string', description: '结果编码,如 utf-8, gbk, gb2312, gb18030(可选,默认自动检测)' }
|
|
115
|
+
},
|
|
80
116
|
required: ['code']
|
|
81
117
|
}
|
|
82
118
|
},
|
|
@@ -178,7 +214,31 @@ export const tools = [
|
|
|
178
214
|
export async function handleToolCall(name, args) {
|
|
179
215
|
const handler = TOOL_HANDLERS[name];
|
|
180
216
|
if (!handler) return { content: [{ type: 'text', text: `未知工具: ${name}` }], isError: true };
|
|
181
|
-
|
|
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
|
+
}
|
|
182
242
|
}
|
|
183
243
|
|
|
184
244
|
async function initCadConnection() {
|
|
@@ -201,7 +261,7 @@ let server = null;
|
|
|
201
261
|
export function createMcpServer() {
|
|
202
262
|
server = new Server(
|
|
203
263
|
{ name: SERVER_NAME, version: SERVER_VERSION },
|
|
204
|
-
{ capabilities:
|
|
264
|
+
{ capabilities: SERVER_CAPABILITIES }
|
|
205
265
|
);
|
|
206
266
|
|
|
207
267
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
|
|
@@ -247,88 +307,82 @@ export function createMcpServer() {
|
|
|
247
307
|
return { success: true };
|
|
248
308
|
});
|
|
249
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
|
+
|
|
250
319
|
return server;
|
|
251
320
|
}
|
|
252
321
|
|
|
253
|
-
async function
|
|
322
|
+
async function dispatchMcpMethod(responder, method, params, id) {
|
|
254
323
|
try {
|
|
255
324
|
if (method === 'initialize') {
|
|
256
|
-
|
|
257
|
-
protocolVersion: PROTOCOL_VERSION,
|
|
258
|
-
capabilities: { tools: {}, resources: { subscribe: true, listChanged: true } },
|
|
259
|
-
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
|
|
260
|
-
} });
|
|
325
|
+
responder({ jsonrpc: '2.0', id, result: SERVER_INFO });
|
|
261
326
|
return;
|
|
262
327
|
}
|
|
263
328
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const resources = await listResources(subMgr.list());
|
|
271
|
-
session.sendResponse({ jsonrpc: '2.0', id, result: { resources } });
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (method === 'resources/read') {
|
|
276
|
-
const uri = params?.uri;
|
|
277
|
-
if (!uri) {
|
|
278
|
-
session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } });
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
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 参数' });
|
|
282
335
|
const data = await readResource(uri);
|
|
283
|
-
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
subMgr.unsubscribe(uri);
|
|
308
|
-
session.sendResponse({ jsonrpc: '2.0', id, result: { success: true } });
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
if (method === 'tools/call') {
|
|
313
|
-
const toolName = params?.name;
|
|
314
|
-
if (!toolName) {
|
|
315
|
-
session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32602, message: 'Invalid params: missing tool name' } });
|
|
316
|
-
return;
|
|
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 || {});
|
|
317
360
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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' } });
|
|
321
369
|
}
|
|
322
|
-
|
|
323
|
-
session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } });
|
|
324
370
|
} catch (e) {
|
|
325
|
-
|
|
326
|
-
|
|
371
|
+
const mcpError = toMcpError(e);
|
|
372
|
+
log(`MCP error [${method}]: ${mcpError.message}`);
|
|
373
|
+
responder({ jsonrpc: '2.0', id, error: { code: -32603, message: mcpError.message } });
|
|
327
374
|
}
|
|
328
375
|
}
|
|
329
376
|
|
|
377
|
+
async function handleMcpRequest(session, method, params, id) {
|
|
378
|
+
await dispatchMcpMethod(
|
|
379
|
+
(response) => session.sendResponse(response),
|
|
380
|
+
method, params, id
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
330
384
|
export async function startServer() {
|
|
331
|
-
if (
|
|
385
|
+
if (transport === 'stdio') {
|
|
332
386
|
await initCadConnection();
|
|
333
387
|
const mcpServer = createMcpServer();
|
|
334
388
|
const transport = new StdioServerTransport();
|
|
@@ -361,11 +415,11 @@ export async function startServer() {
|
|
|
361
415
|
});
|
|
362
416
|
|
|
363
417
|
const PUBLIC_PATHS = ['/health'];
|
|
364
|
-
if (
|
|
418
|
+
if (apiKey) {
|
|
365
419
|
app.use((req, res, next) => {
|
|
366
420
|
if (PUBLIC_PATHS.includes(req.path)) return next();
|
|
367
421
|
const auth = req.get('Authorization');
|
|
368
|
-
if (auth === `Bearer ${
|
|
422
|
+
if (auth === `Bearer ${apiKey}`) return next();
|
|
369
423
|
res.status(401).json({ error: 'Unauthorized' });
|
|
370
424
|
});
|
|
371
425
|
}
|
|
@@ -374,11 +428,33 @@ export async function startServer() {
|
|
|
374
428
|
res.json({ status: 'ok', cad: cad.connected ? 'connected' : 'disconnected' });
|
|
375
429
|
});
|
|
376
430
|
|
|
377
|
-
|
|
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) {
|
|
378
451
|
app.use((req, res, next) => {
|
|
379
|
-
res.setHeader('Access-Control-Allow-Origin',
|
|
380
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
381
|
-
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');
|
|
382
458
|
if (req.method === 'OPTIONS') return res.status(204).end();
|
|
383
459
|
next();
|
|
384
460
|
});
|
|
@@ -386,153 +462,172 @@ export async function startServer() {
|
|
|
386
462
|
|
|
387
463
|
createMcpServer();
|
|
388
464
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
session.sendEndpoint('/mcp');
|
|
395
|
-
session.sendCapabilities({
|
|
396
|
-
protocolVersion: PROTOCOL_VERSION,
|
|
397
|
-
capabilities: { tools: {}, resources: { subscribe: true, listChanged: true } },
|
|
398
|
-
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
sseSessionManager.add(clientId, session);
|
|
402
|
-
|
|
403
|
-
res.on('close', () => {
|
|
404
|
-
sseSessionManager.remove(clientId);
|
|
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();
|
|
407
470
|
|
|
408
|
-
|
|
409
|
-
const clientId = req.get('mcp-session-id') || req.get('Last-Event-ID') || crypto.randomUUID();
|
|
410
|
-
|
|
411
|
-
const session = new SseSession(res, { clientId });
|
|
412
|
-
|
|
413
|
-
session.sendEndpoint('/mcp');
|
|
414
|
-
session.sendCapabilities({
|
|
415
|
-
protocolVersion: PROTOCOL_VERSION,
|
|
416
|
-
capabilities: { tools: {}, resources: { subscribe: true, listChanged: true } },
|
|
417
|
-
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
sseSessionManager.add(clientId, session);
|
|
421
|
-
|
|
422
|
-
res.on('close', () => {
|
|
423
|
-
sseSessionManager.remove(clientId);
|
|
424
|
-
});
|
|
425
|
-
}));
|
|
471
|
+
res.setHeader('mcp-session-id', clientId);
|
|
426
472
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
473
|
+
let session;
|
|
474
|
+
if (sessionId && sseSessionManager.has(sessionId)) {
|
|
475
|
+
session = sseSessionManager.get(sessionId);
|
|
476
|
+
} else {
|
|
477
|
+
session = new SseSession(res, { clientId, resumeFromId: lastEventId });
|
|
478
|
+
sseSessionManager.add(clientId, session);
|
|
479
|
+
}
|
|
434
480
|
|
|
435
|
-
if (
|
|
436
|
-
|
|
437
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
438
|
-
res.setHeader('Connection', 'keep-alive');
|
|
439
|
-
|
|
440
|
-
if (sessionId && sseSessionManager.has(sessionId)) {
|
|
441
|
-
const session = sseSessionManager.get(sessionId);
|
|
442
|
-
if (!id) {
|
|
443
|
-
res.status(204).end();
|
|
444
|
-
return;
|
|
445
|
-
}
|
|
446
|
-
await handleMcpRequest(session, method, params, id);
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
481
|
+
if (lastEventId && lastEventId > 0) {
|
|
482
|
+
session.sendEvent(SSE_EVENTS.MESSAGE, { type: 'resume', lastEventId }, lastEventId + 1);
|
|
449
483
|
}
|
|
450
484
|
|
|
451
|
-
|
|
452
|
-
const { send } = createSseResponseHandler(res, wantsSse);
|
|
485
|
+
session.sendPing();
|
|
453
486
|
|
|
454
|
-
|
|
487
|
+
res.on('close', () => {
|
|
488
|
+
log(`SSE close: session ${clientId}`);
|
|
489
|
+
sseSessionManager.remove(clientId);
|
|
490
|
+
});
|
|
491
|
+
}
|
|
455
492
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
protocolVersion: PROTOCOL_VERSION,
|
|
460
|
-
capabilities: { tools: {}, resources: { subscribe: true, listChanged: true } },
|
|
461
|
-
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
|
|
462
|
-
} }, true);
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
493
|
+
if (enableSse) {
|
|
494
|
+
app.get('/sse', (req, res) => setupSseConnection(req, res));
|
|
495
|
+
app.get('/mcp/sse', (req, res) => setupSseConnection(req, res));
|
|
465
496
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
+
});
|
|
469
516
|
}
|
|
470
517
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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();
|
|
474
524
|
return;
|
|
475
525
|
}
|
|
476
526
|
|
|
477
|
-
if (
|
|
478
|
-
const uri = params?.uri;
|
|
479
|
-
if (!uri) {
|
|
480
|
-
send({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } }, true);
|
|
481
|
-
return;
|
|
482
|
-
}
|
|
527
|
+
if (session) {
|
|
483
528
|
try {
|
|
484
|
-
|
|
485
|
-
send({ jsonrpc: '2.0', id, result: { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(data) }] } }, true);
|
|
529
|
+
await handleMcpRequest(session, method, params, id);
|
|
486
530
|
} catch (e) {
|
|
487
|
-
|
|
531
|
+
log('SSE message error: ' + e.message);
|
|
532
|
+
session.sendError(-32603, e.message, id);
|
|
488
533
|
}
|
|
489
|
-
|
|
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
|
+
);
|
|
490
543
|
}
|
|
544
|
+
});
|
|
545
|
+
}
|
|
491
546
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
}
|
|
498
|
-
subMgr.subscribe(uri);
|
|
499
|
-
send({ jsonrpc: '2.0', id, result: { success: true } }, true);
|
|
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' });
|
|
500
552
|
return;
|
|
501
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
|
+
});
|
|
502
565
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
566
|
+
app.post('/mcp', mcpLimiter, async (req, res) => {
|
|
567
|
+
const body = req.body || {};
|
|
568
|
+
const { method, params, id } = body;
|
|
569
|
+
const accept = req.get('Accept') || '';
|
|
570
|
+
const wantsSse = accept.includes('text/event-stream');
|
|
571
|
+
const sessionId = req.get('mcp-session-id') || '';
|
|
572
|
+
log(`${req.method} ${req.path} - session: ${sessionId} - method: ${method} - wantsSSE: ${wantsSse}`);
|
|
573
|
+
|
|
574
|
+
if (wantsSse) {
|
|
575
|
+
if (!enableSse) {
|
|
576
|
+
res.status(400).json({ error: 'SSE mode is disabled' });
|
|
511
577
|
return;
|
|
512
578
|
}
|
|
513
579
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
580
|
+
const clientId = sessionId || crypto.randomUUID();
|
|
581
|
+
res.setHeader('mcp-session-id', clientId);
|
|
582
|
+
|
|
583
|
+
let session;
|
|
584
|
+
if (sessionId && sseSessionManager.has(sessionId)) {
|
|
585
|
+
session = sseSessionManager.get(sessionId);
|
|
586
|
+
if (!id) {
|
|
587
|
+
res.status(204).end();
|
|
518
588
|
return;
|
|
519
589
|
}
|
|
520
|
-
|
|
521
|
-
|
|
590
|
+
await handleMcpRequest(session, method, params, id);
|
|
591
|
+
res.status(204).end();
|
|
522
592
|
return;
|
|
523
593
|
}
|
|
524
594
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
595
|
+
session = new SseSession(res, { clientId });
|
|
596
|
+
sseSessionManager.add(clientId, session);
|
|
597
|
+
|
|
598
|
+
if (!id) {
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
await handleMcpRequest(session, method, params, id);
|
|
602
|
+
return;
|
|
529
603
|
}
|
|
604
|
+
|
|
605
|
+
res.setHeader('mcp-session-id', sessionId || crypto.randomUUID());
|
|
606
|
+
const { send } = createSseResponseHandler(res, wantsSse);
|
|
607
|
+
|
|
608
|
+
if (!id) return res.status(204).end();
|
|
609
|
+
|
|
610
|
+
await dispatchMcpMethod(
|
|
611
|
+
(response) => send(response, true),
|
|
612
|
+
method, params, id
|
|
613
|
+
);
|
|
530
614
|
});
|
|
531
615
|
|
|
532
|
-
app.listen(
|
|
616
|
+
const httpServer = app.listen(config.port, config.host, async () => {
|
|
533
617
|
await initCadConnection();
|
|
534
|
-
console.error(`@lisp MCP Server 启动 - http://${
|
|
618
|
+
console.error(`@lisp MCP Server 启动 - http://${config.host}:${config.port} - Streamable HTTP`);
|
|
535
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'));
|
|
536
631
|
}
|
|
537
632
|
}
|
|
538
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);
|