@atlisp/mcp 1.1.2 → 1.1.5
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/package.json +1 -1
- package/src/atlisp-mcp.js +157 -171
- package/src/cad-worker.js +3 -2
- package/src/cad.js +73 -42
- package/src/config.js +25 -3
- package/src/session-manager.js +89 -0
- package/src/session-transport.js +89 -0
- package/src/subscription-manager.js +27 -12
package/package.json
CHANGED
package/src/atlisp-mcp.js
CHANGED
|
@@ -7,7 +7,8 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
7
7
|
const require = createRequire(import.meta.url);
|
|
8
8
|
const { version } = require(path.join(__dirname, '..', 'package.json'));
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
const isMcpScript = process.argv[1]?.includes('atlisp-mcp');
|
|
11
|
+
if (isMcpScript) {
|
|
11
12
|
const args = process.argv.slice(2);
|
|
12
13
|
for (let i = 0; i < args.length; i++) {
|
|
13
14
|
if (args[i] === '--version' || args[i] === '-v') {
|
|
@@ -34,6 +35,17 @@ Examples:
|
|
|
34
35
|
atlisp-mcp --transport stdio # Same as --stdio
|
|
35
36
|
`);
|
|
36
37
|
process.exit(0);
|
|
38
|
+
} else if (args[i] === '--transport' && args[i + 1]) {
|
|
39
|
+
process.env.TRANSPORT = args[i + 1];
|
|
40
|
+
i++;
|
|
41
|
+
} else if (args[i] === '--stdio') {
|
|
42
|
+
process.env.TRANSPORT = 'stdio';
|
|
43
|
+
} else if (args[i] === '--port' && args[i + 1]) {
|
|
44
|
+
process.env.PORT = args[i + 1];
|
|
45
|
+
i++;
|
|
46
|
+
} else if (args[i] === '--host' && args[i + 1]) {
|
|
47
|
+
process.env.HOST = args[i + 1];
|
|
48
|
+
i++;
|
|
37
49
|
}
|
|
38
50
|
}
|
|
39
51
|
}
|
|
@@ -43,11 +55,11 @@ import rawBody from 'raw-body';
|
|
|
43
55
|
import iconv from 'iconv-lite';
|
|
44
56
|
import rateLimit from 'express-rate-limit';
|
|
45
57
|
import { randomUUID } from 'crypto';
|
|
58
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
46
59
|
const { decode: charsetDecode, encodingExists } = iconv;
|
|
47
60
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
48
61
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
49
|
-
import {
|
|
50
|
-
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
62
|
+
import { SessionTransport } from './session-transport.js';
|
|
51
63
|
import {
|
|
52
64
|
ListToolsRequestSchema,
|
|
53
65
|
CallToolRequestSchema,
|
|
@@ -67,9 +79,14 @@ import * as functionHandlers from './handlers/function-handlers.js';
|
|
|
67
79
|
import { listResources, readResource } from './handlers/resource-handlers.js';
|
|
68
80
|
import { listPrompts, getPrompt } from './handlers/prompt-handlers.js';
|
|
69
81
|
import * as subMgr from './subscription-manager.js';
|
|
70
|
-
import {
|
|
82
|
+
import { sessionManager } from './subscription-manager.js';
|
|
83
|
+
import { SseSessionManager } from './sse-session-manager.js';
|
|
84
|
+
import { SseSession } from './sse-session.js';
|
|
71
85
|
import config from './config.js';
|
|
72
86
|
|
|
87
|
+
const mcpContext = new AsyncLocalStorage();
|
|
88
|
+
const sseSessionManager = new SseSessionManager();
|
|
89
|
+
|
|
73
90
|
const SERVER_CAPABILITIES = {
|
|
74
91
|
tools: {},
|
|
75
92
|
resources: { subscribe: true, listChanged: true },
|
|
@@ -125,6 +142,37 @@ const TOOL_HANDLERS = {
|
|
|
125
142
|
}),
|
|
126
143
|
};
|
|
127
144
|
|
|
145
|
+
function getOrCreateSessionServer(sessionId) {
|
|
146
|
+
let entry = sessionServers.get(sessionId);
|
|
147
|
+
if (!entry) {
|
|
148
|
+
const server = createMcpServer();
|
|
149
|
+
const transport = new SessionTransport(sessionId);
|
|
150
|
+
server.connect(transport);
|
|
151
|
+
entry = { server, transport };
|
|
152
|
+
sessionServers.set(sessionId, entry);
|
|
153
|
+
sessionTranports.set(sessionId, transport);
|
|
154
|
+
}
|
|
155
|
+
return entry;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function removeSessionServer(sessionId) {
|
|
159
|
+
const entry = sessionServers.get(sessionId);
|
|
160
|
+
if (entry) {
|
|
161
|
+
entry.transport.close().catch(() => {});
|
|
162
|
+
entry.server.close().catch(() => {});
|
|
163
|
+
sessionServers.delete(sessionId);
|
|
164
|
+
sessionTranports.delete(sessionId);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function broadcastToAllSessions(uri) {
|
|
169
|
+
for (const [sid, entry] of sessionServers) {
|
|
170
|
+
entry.server.sendResourceUpdated({ uri }).catch(err => {
|
|
171
|
+
log(`Failed to send resource update to session ${sid}: ${err.message}`);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
128
176
|
export const tools = [
|
|
129
177
|
{
|
|
130
178
|
name: 'connect_cad',
|
|
@@ -299,10 +347,11 @@ async function initCadConnection() {
|
|
|
299
347
|
}
|
|
300
348
|
}
|
|
301
349
|
|
|
302
|
-
|
|
350
|
+
const sessionServers = new Map();
|
|
351
|
+
const sessionTranports = new Map();
|
|
303
352
|
|
|
304
353
|
export function createMcpServer() {
|
|
305
|
-
server = new Server(
|
|
354
|
+
const server = new Server(
|
|
306
355
|
{ name: SERVER_NAME, version: SERVER_VERSION },
|
|
307
356
|
{ capabilities: SERVER_CAPABILITIES }
|
|
308
357
|
);
|
|
@@ -314,9 +363,14 @@ export function createMcpServer() {
|
|
|
314
363
|
return await handleToolCall(name, args || {});
|
|
315
364
|
});
|
|
316
365
|
|
|
317
|
-
server.setRequestHandler(ListResourcesRequestSchema, async () =>
|
|
318
|
-
|
|
319
|
-
|
|
366
|
+
server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
|
|
367
|
+
const ctx = mcpContext.getStore();
|
|
368
|
+
const sessionId = ctx?.sessionId;
|
|
369
|
+
const subscribedUris = sessionId ? subMgr.list(sessionId) : subMgr.list();
|
|
370
|
+
return {
|
|
371
|
+
resources: await listResources(subscribedUris)
|
|
372
|
+
};
|
|
373
|
+
});
|
|
320
374
|
|
|
321
375
|
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
322
376
|
const { uri } = request.params;
|
|
@@ -340,13 +394,15 @@ export function createMcpServer() {
|
|
|
340
394
|
|
|
341
395
|
server.setRequestHandler(SubscribeRequestSchema, async (request) => {
|
|
342
396
|
const { uri } = request.params;
|
|
343
|
-
|
|
397
|
+
const ctx = mcpContext.getStore();
|
|
398
|
+
subMgr.subscribe(uri, ctx?.sessionId);
|
|
344
399
|
return { success: true };
|
|
345
400
|
});
|
|
346
401
|
|
|
347
402
|
server.setRequestHandler(UnsubscribeRequestSchema, async (request) => {
|
|
348
403
|
const { uri } = request.params;
|
|
349
|
-
|
|
404
|
+
const ctx = mcpContext.getStore();
|
|
405
|
+
subMgr.unsubscribe(uri, ctx?.sessionId);
|
|
350
406
|
return { success: true };
|
|
351
407
|
});
|
|
352
408
|
|
|
@@ -362,74 +418,18 @@ export function createMcpServer() {
|
|
|
362
418
|
return server;
|
|
363
419
|
}
|
|
364
420
|
|
|
365
|
-
async function dispatchMcpMethod(responder, method, params, id) {
|
|
366
|
-
try {
|
|
367
|
-
if (method === 'initialize') {
|
|
368
|
-
responder({ jsonrpc: '2.0', id, result: SERVER_INFO });
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
const methodHandlers = {
|
|
373
|
-
'tools/list': async () => ({ tools }),
|
|
374
|
-
'resources/list': async () => ({ resources: await listResources(subMgr.list()) }),
|
|
375
|
-
'resources/read': async () => {
|
|
376
|
-
const uri = params?.uri;
|
|
377
|
-
if (!uri) throw createError('INVALID_PARAMS', { detail: '缺少 uri 参数' });
|
|
378
|
-
const data = await readResource(uri);
|
|
379
|
-
return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(data) }] };
|
|
380
|
-
},
|
|
381
|
-
'resources/subscribe': async () => {
|
|
382
|
-
const uri = params?.uri;
|
|
383
|
-
if (!uri) throw createError('INVALID_PARAMS', { detail: '缺少 uri 参数' });
|
|
384
|
-
subMgr.subscribe(uri);
|
|
385
|
-
return { success: true };
|
|
386
|
-
},
|
|
387
|
-
'resources/unsubscribe': async () => {
|
|
388
|
-
const uri = params?.uri;
|
|
389
|
-
if (!uri) throw createError('INVALID_PARAMS', { detail: '缺少 uri 参数' });
|
|
390
|
-
subMgr.unsubscribe(uri);
|
|
391
|
-
return { success: true };
|
|
392
|
-
},
|
|
393
|
-
'tools/call': async () => {
|
|
394
|
-
const toolName = params?.name;
|
|
395
|
-
if (!toolName) throw createError('INVALID_PARAMS', { detail: 'missing tool name' });
|
|
396
|
-
return await handleToolCall(toolName, params?.arguments || {});
|
|
397
|
-
},
|
|
398
|
-
'prompts/list': async () => ({ prompts: await listPrompts() }),
|
|
399
|
-
'prompts/get': async () => {
|
|
400
|
-
const promptName = params?.name;
|
|
401
|
-
if (!promptName) throw createError('INVALID_PARAMS', { detail: '缺少 prompt name 参数' });
|
|
402
|
-
return await getPrompt(promptName, params?.arguments || {});
|
|
403
|
-
}
|
|
404
|
-
};
|
|
405
|
-
|
|
406
|
-
const handler = methodHandlers[method];
|
|
407
|
-
if (handler) {
|
|
408
|
-
const result = await handler();
|
|
409
|
-
responder({ jsonrpc: '2.0', id, result });
|
|
410
|
-
} else {
|
|
411
|
-
responder({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } });
|
|
412
|
-
}
|
|
413
|
-
} catch (e) {
|
|
414
|
-
const mcpError = toMcpError(e);
|
|
415
|
-
log(`MCP error [${method}]: ${mcpError.message}`);
|
|
416
|
-
responder({ jsonrpc: '2.0', id, error: { code: -32603, message: mcpError.message } });
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
421
|
|
|
420
|
-
async function handleMcpRequest(session, method, params, id) {
|
|
421
|
-
await dispatchMcpMethod(
|
|
422
|
-
(response) => session.sendResponse(response),
|
|
423
|
-
method, params, id
|
|
424
|
-
);
|
|
425
|
-
}
|
|
426
422
|
|
|
427
423
|
export async function startServer() {
|
|
428
424
|
if (config.transport === 'stdio') {
|
|
429
425
|
await initCadConnection();
|
|
430
426
|
const mcpServer = createMcpServer();
|
|
431
427
|
const transport = new StdioServerTransport();
|
|
432
|
-
|
|
428
|
+
const sessionId = 'stdio-session';
|
|
429
|
+
sessionManager.createSession(sessionId);
|
|
430
|
+
await mcpContext.run({ sessionId }, async () => {
|
|
431
|
+
await mcpServer.connect(transport);
|
|
432
|
+
});
|
|
433
433
|
console.error(`${SERVER_NAME} 运行在 stdio 模式`);
|
|
434
434
|
|
|
435
435
|
subMgr.setNotify((uri, data) => {
|
|
@@ -490,129 +490,115 @@ export async function startServer() {
|
|
|
490
490
|
});
|
|
491
491
|
}
|
|
492
492
|
|
|
493
|
-
const mcpServer = createMcpServer();
|
|
494
|
-
|
|
495
|
-
const transport = new StreamableHTTPServerTransport({
|
|
496
|
-
sessionIdGenerator: () => randomUUID(),
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
await mcpServer.connect(transport);
|
|
500
|
-
log('MCP Server connected to StreamableHTTPServerTransport');
|
|
501
|
-
|
|
502
493
|
subMgr.setNotify((uri, data) => {
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
494
|
+
broadcastToAllSessions(uri);
|
|
495
|
+
for (const session of sseSessionManager.listActive()) {
|
|
496
|
+
sseSessionManager.sendToClient(session.clientId, 'resource_updated', { uri });
|
|
497
|
+
}
|
|
506
498
|
});
|
|
507
499
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
res.status(500).json({ error: 'Internal server error' });
|
|
515
|
-
}
|
|
500
|
+
async function handleMcpPost(req, res) {
|
|
501
|
+
const sessionId = req.headers['mcp-session-id'] || req.query.sessionId || randomUUID();
|
|
502
|
+
sessionManager.createSession(sessionId);
|
|
503
|
+
const body = req.body;
|
|
504
|
+
if (!body || !body.method) {
|
|
505
|
+
return res.status(400).json({ error: 'Invalid request' });
|
|
516
506
|
}
|
|
517
|
-
});
|
|
518
507
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
508
|
+
const { server, transport } = getOrCreateSessionServer(sessionId);
|
|
509
|
+
transport.resetCleanupTimer();
|
|
510
|
+
|
|
511
|
+
await mcpContext.run({ sessionId }, async () => {
|
|
512
|
+
try {
|
|
513
|
+
const response = await transport.handleRequest(body);
|
|
514
|
+
if (response) {
|
|
515
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
516
|
+
if (transport.sessionId) {
|
|
517
|
+
headers['mcp-session-id'] = transport.sessionId;
|
|
518
|
+
}
|
|
519
|
+
res.writeHead(200, headers);
|
|
520
|
+
res.end(JSON.stringify(response));
|
|
521
|
+
} else {
|
|
522
|
+
if (!res.headersSent) {
|
|
523
|
+
res.status(202).end();
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
} catch (error) {
|
|
527
|
+
log(`Transport error [${sessionId}]: ${error.message}`);
|
|
528
|
+
if (!res.headersSent) {
|
|
529
|
+
res.status(500).json({ error: 'Internal server error', message: error.message });
|
|
530
|
+
}
|
|
526
531
|
}
|
|
527
|
-
}
|
|
528
|
-
}
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
app.all('/mcp', mcpLimiter, handleMcpPost);
|
|
536
|
+
app.all('/mcp/:path', mcpLimiter, handleMcpPost);
|
|
537
|
+
app.get('/mcp/stream', mcpLimiter, handleMcpPost);
|
|
529
538
|
|
|
530
539
|
const sseEndpoint = '/sse';
|
|
531
540
|
app.get(sseEndpoint, async (req, res) => {
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
await sseTransport.handleRequest(req, res);
|
|
536
|
-
} catch (error) {
|
|
537
|
-
log(`SSE transport error: ${error.message}`);
|
|
538
|
-
if (!res.headersSent) {
|
|
539
|
-
res.status(500).json({ error: 'Internal server error' });
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
});
|
|
543
|
-
app.post(sseEndpoint, mcpLimiter, async (req, res) => {
|
|
544
|
-
const sessionId = req.headers['mcp-session-id'];
|
|
545
|
-
const sseTransport = sessionId ? sseEndpoint + '?sessionId=' + sessionId : sseEndpoint;
|
|
546
|
-
try {
|
|
547
|
-
const transport = new SSEServerTransport(sseEndpoint, res);
|
|
548
|
-
await mcpServer.connect(transport);
|
|
549
|
-
await transport.handleRequest(req, res);
|
|
550
|
-
} catch (error) {
|
|
551
|
-
log(`SSE POST error: ${error.message}`);
|
|
552
|
-
if (!res.headersSent) {
|
|
553
|
-
res.status(500).json({ error: 'Internal server error' });
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
});
|
|
541
|
+
const sessionId = randomUUID();
|
|
542
|
+
sessionManager.createSession(sessionId);
|
|
543
|
+
getOrCreateSessionServer(sessionId);
|
|
557
544
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
try {
|
|
561
|
-
await mcpServer.connect(sseTransport);
|
|
562
|
-
await sseTransport.handleRequest(req, res);
|
|
563
|
-
} catch (error) {
|
|
564
|
-
log(`/mcp/sse transport error: ${error.message}`);
|
|
565
|
-
if (!res.headersSent) {
|
|
566
|
-
res.status(500).json({ error: 'Internal server error' });
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
});
|
|
570
|
-
app.post('/mcp/sse', mcpLimiter, async (req, res) => {
|
|
571
|
-
const sseTransport = new SSEServerTransport('/mcp/sse', res);
|
|
572
|
-
try {
|
|
573
|
-
await mcpServer.connect(sseTransport);
|
|
574
|
-
await sseTransport.handleRequest(req, res);
|
|
575
|
-
} catch (error) {
|
|
576
|
-
log(`/mcp/sse POST error: ${error.message}`);
|
|
577
|
-
if (!res.headersSent) {
|
|
578
|
-
res.status(500).json({ error: 'Internal server error' });
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
});
|
|
545
|
+
const sseSession = new SseSession(res, { clientId: sessionId });
|
|
546
|
+
sseSessionManager.add(sessionId, sseSession);
|
|
582
547
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
res.status(500).json({ error: 'Internal server error' });
|
|
590
|
-
}
|
|
591
|
-
}
|
|
548
|
+
sseSession.sendEndpoint(`/message?sessionId=${sessionId}`);
|
|
549
|
+
|
|
550
|
+
req.on('close', () => {
|
|
551
|
+
sseSessionManager.remove(sessionId);
|
|
552
|
+
removeSessionServer(sessionId);
|
|
553
|
+
});
|
|
592
554
|
});
|
|
593
555
|
|
|
594
556
|
app.post('/message', mcpLimiter, async (req, res) => {
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
557
|
+
const sessionId = req.query.sessionId;
|
|
558
|
+
if (!sessionId) {
|
|
559
|
+
return res.status(400).json({ error: 'Missing sessionId' });
|
|
560
|
+
}
|
|
561
|
+
const sseSession = sseSessionManager.get(sessionId);
|
|
562
|
+
if (!sseSession) {
|
|
563
|
+
return res.status(404).json({ error: 'Session not found' });
|
|
564
|
+
}
|
|
565
|
+
sessionManager.createSession(sessionId);
|
|
566
|
+
const { server, transport } = getOrCreateSessionServer(sessionId);
|
|
567
|
+
transport.resetCleanupTimer();
|
|
568
|
+
|
|
569
|
+
const body = req.body;
|
|
570
|
+
if (body && body.method) {
|
|
571
|
+
try {
|
|
572
|
+
const response = await transport.handleRequest(body);
|
|
573
|
+
if (response) {
|
|
574
|
+
sseSession.sendResponse(response);
|
|
575
|
+
}
|
|
576
|
+
if (!res.headersSent) {
|
|
577
|
+
res.status(202).end();
|
|
578
|
+
}
|
|
579
|
+
} catch (error) {
|
|
580
|
+
log(`SSE message error [${sessionId}]: ${error.message}`);
|
|
581
|
+
if (!res.headersSent) {
|
|
582
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
583
|
+
}
|
|
604
584
|
}
|
|
585
|
+
} else {
|
|
586
|
+
res.status(400).json({ error: 'Invalid request' });
|
|
605
587
|
}
|
|
606
588
|
});
|
|
607
589
|
|
|
608
590
|
const httpServer = app.listen(config.port, config.host, async () => {
|
|
609
591
|
await initCadConnection();
|
|
610
|
-
console.error(`@lisp MCP Server 启动 - http://${config.host}:${config.port}
|
|
592
|
+
console.error(`@lisp MCP Server 启动 - http://${config.host}:${config.port} (多会话模式)`);
|
|
611
593
|
});
|
|
612
594
|
|
|
613
595
|
async function shutdown(signal) {
|
|
614
596
|
console.error(`\n收到 ${signal},正在优雅关闭...`);
|
|
615
|
-
|
|
597
|
+
sseSessionManager.closeAll();
|
|
598
|
+
sessionManager.clear();
|
|
599
|
+
for (const [sid] of sessionServers) {
|
|
600
|
+
removeSessionServer(sid);
|
|
601
|
+
}
|
|
616
602
|
await cad.disconnect();
|
|
617
603
|
closeLog();
|
|
618
604
|
httpServer.close(() => process.exit(0));
|
package/src/cad-worker.js
CHANGED
|
@@ -244,10 +244,11 @@ process.stdin.on('data', (data) => {
|
|
|
244
244
|
if (!line.trim()) continue;
|
|
245
245
|
try {
|
|
246
246
|
const msg = JSON.parse(line);
|
|
247
|
+
const requestId = msg.requestId;
|
|
247
248
|
handleMessage(msg).then(result => {
|
|
248
|
-
process.stdout.write(JSON.stringify(result) + '\n');
|
|
249
|
+
process.stdout.write(JSON.stringify({ ...result, requestId }) + '\n');
|
|
249
250
|
}).catch(err => {
|
|
250
|
-
process.stdout.write(JSON.stringify({ error: err.message }) + '\n');
|
|
251
|
+
process.stdout.write(JSON.stringify({ error: err.message, requestId }) + '\n');
|
|
251
252
|
});
|
|
252
253
|
} catch (e) {
|
|
253
254
|
process.stdout.write(JSON.stringify({ error: 'invalid message' }) + '\n');
|
package/src/cad.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
|
+
import { randomUUID } from 'crypto';
|
|
4
5
|
import { CAD_PLATFORMS, FILE_EXTENSIONS } from './constants.js';
|
|
5
6
|
import config from './config.js';
|
|
6
7
|
|
|
@@ -15,8 +16,19 @@ let _platform = null;
|
|
|
15
16
|
let _version = null;
|
|
16
17
|
|
|
17
18
|
let heartbeatTimer = null;
|
|
19
|
+
const pendingRequests = new Map();
|
|
20
|
+
let stdoutHandlerSetup = false;
|
|
21
|
+
|
|
22
|
+
const MAX_BUFFER_SIZE = 1024 * 1024;
|
|
18
23
|
|
|
19
24
|
export function resetWorker() {
|
|
25
|
+
for (const [id, { reject, timeout }] of pendingRequests) {
|
|
26
|
+
clearTimeout(timeout);
|
|
27
|
+
reject(new Error('Worker reset'));
|
|
28
|
+
}
|
|
29
|
+
pendingRequests.clear();
|
|
30
|
+
stdoutHandlerSetup = false;
|
|
31
|
+
|
|
20
32
|
if (worker) {
|
|
21
33
|
worker.kill();
|
|
22
34
|
worker = null;
|
|
@@ -27,6 +39,43 @@ export function resetWorker() {
|
|
|
27
39
|
}
|
|
28
40
|
}
|
|
29
41
|
|
|
42
|
+
function setupStdoutHandler(w) {
|
|
43
|
+
let buffer = '';
|
|
44
|
+
stdoutHandlerSetup = true;
|
|
45
|
+
|
|
46
|
+
w.stdout.on('data', (data) => {
|
|
47
|
+
try {
|
|
48
|
+
buffer += data.toString();
|
|
49
|
+
if (buffer.length > MAX_BUFFER_SIZE) {
|
|
50
|
+
buffer = buffer.slice(-MAX_BUFFER_SIZE);
|
|
51
|
+
}
|
|
52
|
+
const lines = buffer.split('\n');
|
|
53
|
+
buffer = lines.pop() || '';
|
|
54
|
+
for (const line of lines) {
|
|
55
|
+
if (!line.trim()) continue;
|
|
56
|
+
try {
|
|
57
|
+
const result = JSON.parse(line);
|
|
58
|
+
const rid = result.requestId;
|
|
59
|
+
if (rid && pendingRequests.has(rid)) {
|
|
60
|
+
const { resolve, reject, timeout } = pendingRequests.get(rid);
|
|
61
|
+
clearTimeout(timeout);
|
|
62
|
+
pendingRequests.delete(rid);
|
|
63
|
+
if (result.error) {
|
|
64
|
+
reject(new Error(result.error));
|
|
65
|
+
} else {
|
|
66
|
+
resolve(result);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} catch (parseErr) {
|
|
70
|
+
console.error('JSON parse error in worker output:', parseErr.message);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} catch (e) {
|
|
74
|
+
console.error('Unexpected error in worker stdout handler:', e.message);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
30
79
|
export function getWorker() {
|
|
31
80
|
if (!worker || !worker.connected) {
|
|
32
81
|
if (worker) worker.kill();
|
|
@@ -34,18 +83,27 @@ export function getWorker() {
|
|
|
34
83
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
35
84
|
});
|
|
36
85
|
worker.connected = true;
|
|
37
|
-
|
|
86
|
+
|
|
38
87
|
worker.stderr.on('data', (d) => console.error('worker stderr:', d.toString()));
|
|
39
88
|
const w = worker;
|
|
40
89
|
w.on('exit', (code) => {
|
|
41
90
|
console.error('worker exited:', code);
|
|
42
91
|
if (w !== worker) return;
|
|
43
92
|
w.connected = false;
|
|
93
|
+
|
|
94
|
+
for (const [id, { reject, timeout }] of pendingRequests) {
|
|
95
|
+
clearTimeout(timeout);
|
|
96
|
+
reject(new Error(`Worker exited with code ${code}`));
|
|
97
|
+
}
|
|
98
|
+
pendingRequests.clear();
|
|
99
|
+
stdoutHandlerSetup = false;
|
|
100
|
+
|
|
44
101
|
if (heartbeatTimer) {
|
|
45
102
|
clearInterval(heartbeatTimer);
|
|
46
103
|
heartbeatTimer = null;
|
|
47
104
|
}
|
|
48
105
|
});
|
|
106
|
+
setupStdoutHandler(w);
|
|
49
107
|
startHeartbeat();
|
|
50
108
|
}
|
|
51
109
|
return worker;
|
|
@@ -68,53 +126,26 @@ function startHeartbeat() {
|
|
|
68
126
|
}
|
|
69
127
|
|
|
70
128
|
export function sendMessage(msg) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
let responded = false;
|
|
74
|
-
let buffer = '';
|
|
129
|
+
const w = getWorker();
|
|
130
|
+
const requestId = randomUUID();
|
|
75
131
|
|
|
132
|
+
return new Promise((resolve, reject) => {
|
|
76
133
|
const timeout = setTimeout(() => {
|
|
77
|
-
|
|
78
|
-
|
|
134
|
+
const pending = pendingRequests.get(requestId);
|
|
135
|
+
if (pending) {
|
|
136
|
+
pendingRequests.delete(requestId);
|
|
79
137
|
reject(new Error('Timeout waiting for worker'));
|
|
80
138
|
}
|
|
81
139
|
}, MESSAGE_TIMEOUT);
|
|
82
140
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const result = JSON.parse(line);
|
|
92
|
-
if (result.error) {
|
|
93
|
-
if (!responded) {
|
|
94
|
-
responded = true;
|
|
95
|
-
clearTimeout(timeout);
|
|
96
|
-
w.stdout.off('data', handler);
|
|
97
|
-
reject(new Error(result.error));
|
|
98
|
-
}
|
|
99
|
-
} else {
|
|
100
|
-
if (!responded) {
|
|
101
|
-
responded = true;
|
|
102
|
-
clearTimeout(timeout);
|
|
103
|
-
w.stdout.off('data', handler);
|
|
104
|
-
resolve(result);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
} catch (parseErr) {
|
|
108
|
-
console.error('JSON parse error in worker output:', parseErr.message, 'line:', line.slice(0, 200));
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
} catch (e) {
|
|
112
|
-
console.error('Unexpected error in worker stdout handler:', e.message);
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
w.stdout.on('data', handler);
|
|
117
|
-
w.stdin.write(JSON.stringify(msg) + '\n');
|
|
141
|
+
pendingRequests.set(requestId, { resolve, reject, timeout });
|
|
142
|
+
try {
|
|
143
|
+
w.stdin.write(JSON.stringify({ ...msg, requestId }) + '\n');
|
|
144
|
+
} catch (e) {
|
|
145
|
+
clearTimeout(timeout);
|
|
146
|
+
pendingRequests.delete(requestId);
|
|
147
|
+
reject(new Error(`Failed to write to worker: ${e.message}`));
|
|
148
|
+
}
|
|
118
149
|
});
|
|
119
150
|
}
|
|
120
151
|
|
package/src/config.js
CHANGED
|
@@ -1,7 +1,29 @@
|
|
|
1
|
+
function parseArgs() {
|
|
2
|
+
const args = process.argv.slice(2);
|
|
3
|
+
const result = { transport: 'http', port: '8110', host: '0.0.0.0' };
|
|
4
|
+
for (let i = 0; i < args.length; i++) {
|
|
5
|
+
if (args[i] === '--transport' && args[i + 1]) {
|
|
6
|
+
result.transport = args[i + 1];
|
|
7
|
+
i++;
|
|
8
|
+
} else if (args[i] === '--stdio') {
|
|
9
|
+
result.transport = 'stdio';
|
|
10
|
+
} else if (args[i] === '--port' && args[i + 1]) {
|
|
11
|
+
result.port = args[i + 1];
|
|
12
|
+
i++;
|
|
13
|
+
} else if (args[i] === '--host' && args[i + 1]) {
|
|
14
|
+
result.host = args[i + 1];
|
|
15
|
+
i++;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const cliArgs = parseArgs();
|
|
22
|
+
|
|
1
23
|
const config = {
|
|
2
|
-
port: parseInt(process.env.PORT ||
|
|
3
|
-
host: process.env.HOST ||
|
|
4
|
-
transport: process.env.TRANSPORT ||
|
|
24
|
+
port: parseInt(process.env.PORT || cliArgs.port, 10),
|
|
25
|
+
host: process.env.HOST || cliArgs.host,
|
|
26
|
+
transport: process.env.TRANSPORT || cliArgs.transport,
|
|
5
27
|
apiKey: process.env.MCP_API_KEY || '',
|
|
6
28
|
enableSse: process.env.ENABLE_SSE !== 'false',
|
|
7
29
|
messageTimeout: parseInt(process.env.MESSAGE_TIMEOUT || '60000', 10),
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export class Session {
|
|
2
|
+
constructor(clientId) {
|
|
3
|
+
this.clientId = clientId;
|
|
4
|
+
this.subscriptions = new Set();
|
|
5
|
+
this.createdAt = Date.now();
|
|
6
|
+
this.connected = false;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class SessionManager {
|
|
11
|
+
#sessions = new Map();
|
|
12
|
+
|
|
13
|
+
createSession(clientId) {
|
|
14
|
+
let session = this.#sessions.get(clientId);
|
|
15
|
+
if (!session) {
|
|
16
|
+
session = new Session(clientId);
|
|
17
|
+
this.#sessions.set(clientId, session);
|
|
18
|
+
}
|
|
19
|
+
return session;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
getSession(clientId) {
|
|
23
|
+
return this.#sessions.get(clientId) || null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
hasSession(clientId) {
|
|
27
|
+
return this.#sessions.has(clientId);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
removeSession(clientId) {
|
|
31
|
+
return this.#sessions.delete(clientId);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
subscribe(clientId, uri) {
|
|
35
|
+
const session = this.#sessions.get(clientId);
|
|
36
|
+
if (session) {
|
|
37
|
+
session.subscriptions.add(uri);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
unsubscribe(clientId, uri) {
|
|
44
|
+
const session = this.#sessions.get(clientId);
|
|
45
|
+
if (session) {
|
|
46
|
+
return session.subscriptions.delete(uri);
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
getSubscriptions(clientId) {
|
|
52
|
+
const session = this.#sessions.get(clientId);
|
|
53
|
+
return session ? Array.from(session.subscriptions) : [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
isSubscribed(clientId, uri) {
|
|
57
|
+
const session = this.#sessions.get(clientId);
|
|
58
|
+
return session ? session.subscriptions.has(uri) : false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getAllSubscribedUris() {
|
|
62
|
+
const all = new Set();
|
|
63
|
+
for (const session of this.#sessions.values()) {
|
|
64
|
+
for (const uri of session.subscriptions) {
|
|
65
|
+
all.add(uri);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return all;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
list() {
|
|
72
|
+
return Array.from(this.#sessions.entries()).map(([id, s]) => ({
|
|
73
|
+
clientId: id,
|
|
74
|
+
subscriptions: Array.from(s.subscriptions),
|
|
75
|
+
createdAt: s.createdAt,
|
|
76
|
+
connected: s.connected
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
count() {
|
|
81
|
+
return this.#sessions.size;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
clear() {
|
|
85
|
+
this.#sessions.clear();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export default SessionManager;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
|
|
3
|
+
const SESSION_TIMEOUT = 30 * 60 * 1000;
|
|
4
|
+
|
|
5
|
+
export class SessionTransport {
|
|
6
|
+
constructor(sessionId) {
|
|
7
|
+
this.sessionId = sessionId || randomUUID();
|
|
8
|
+
this.onclose = null;
|
|
9
|
+
this.onerror = null;
|
|
10
|
+
this.onmessage = null;
|
|
11
|
+
this._pendingRequest = null;
|
|
12
|
+
this._started = false;
|
|
13
|
+
this._closed = false;
|
|
14
|
+
this._cleanupTimer = null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async start() {
|
|
18
|
+
if (this._started) {
|
|
19
|
+
throw new Error('Transport already started');
|
|
20
|
+
}
|
|
21
|
+
this._started = true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async send(message) {
|
|
25
|
+
if (this._closed) return;
|
|
26
|
+
if (this._pendingRequest) {
|
|
27
|
+
const { resolve, reject, timeout } = this._pendingRequest;
|
|
28
|
+
clearTimeout(timeout);
|
|
29
|
+
this._pendingRequest = null;
|
|
30
|
+
resolve(message);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async close() {
|
|
35
|
+
if (this._closed) return;
|
|
36
|
+
this._closed = true;
|
|
37
|
+
if (this._pendingRequest) {
|
|
38
|
+
const { reject, timeout } = this._pendingRequest;
|
|
39
|
+
clearTimeout(timeout);
|
|
40
|
+
this._pendingRequest = null;
|
|
41
|
+
reject(new Error('Transport closed'));
|
|
42
|
+
}
|
|
43
|
+
this._clearCleanupTimer();
|
|
44
|
+
this.onclose?.();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async handleRequest(message, timeoutMs = 60000) {
|
|
48
|
+
if (this._closed) {
|
|
49
|
+
throw new Error('Transport closed');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!message || !message.method) {
|
|
53
|
+
throw new Error('Invalid message');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!message.id) {
|
|
57
|
+
this.onmessage?.(message);
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const timeout = setTimeout(() => {
|
|
63
|
+
if (this._pendingRequest?.resolve === resolve) {
|
|
64
|
+
this._pendingRequest = null;
|
|
65
|
+
reject(new Error('Request timeout'));
|
|
66
|
+
}
|
|
67
|
+
}, timeoutMs);
|
|
68
|
+
|
|
69
|
+
this._pendingRequest = { resolve, reject, timeout };
|
|
70
|
+
this.onmessage?.(message);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
resetCleanupTimer() {
|
|
75
|
+
this._clearCleanupTimer();
|
|
76
|
+
this._cleanupTimer = setTimeout(() => {
|
|
77
|
+
this.close();
|
|
78
|
+
}, SESSION_TIMEOUT);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
_clearCleanupTimer() {
|
|
82
|
+
if (this._cleanupTimer) {
|
|
83
|
+
clearTimeout(this._cleanupTimer);
|
|
84
|
+
this._cleanupTimer = null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export default SessionTransport;
|
|
@@ -1,25 +1,38 @@
|
|
|
1
|
-
|
|
1
|
+
import { SessionManager } from './session-manager.js';
|
|
2
|
+
|
|
3
|
+
const sessionManager = new SessionManager();
|
|
2
4
|
let _notifyFn = null;
|
|
3
5
|
|
|
4
6
|
export function setNotify(fn) {
|
|
5
7
|
_notifyFn = fn;
|
|
6
8
|
}
|
|
7
9
|
|
|
8
|
-
export function subscribe(uri) {
|
|
9
|
-
|
|
10
|
+
export function subscribe(uri, sessionId) {
|
|
11
|
+
if (sessionId) {
|
|
12
|
+
return sessionManager.subscribe(sessionId, uri);
|
|
13
|
+
}
|
|
10
14
|
return true;
|
|
11
15
|
}
|
|
12
16
|
|
|
13
|
-
export function unsubscribe(uri) {
|
|
14
|
-
|
|
17
|
+
export function unsubscribe(uri, sessionId) {
|
|
18
|
+
if (sessionId) {
|
|
19
|
+
return sessionManager.unsubscribe(sessionId, uri);
|
|
20
|
+
}
|
|
21
|
+
return true;
|
|
15
22
|
}
|
|
16
23
|
|
|
17
|
-
export function list() {
|
|
18
|
-
|
|
24
|
+
export function list(sessionId) {
|
|
25
|
+
if (sessionId) {
|
|
26
|
+
return sessionManager.getSubscriptions(sessionId);
|
|
27
|
+
}
|
|
28
|
+
return Array.from(sessionManager.getAllSubscribedUris());
|
|
19
29
|
}
|
|
20
30
|
|
|
21
|
-
export function isSubscribed(uri) {
|
|
22
|
-
|
|
31
|
+
export function isSubscribed(uri, sessionId) {
|
|
32
|
+
if (sessionId) {
|
|
33
|
+
return sessionManager.isSubscribed(sessionId, uri);
|
|
34
|
+
}
|
|
35
|
+
return sessionManager.getAllSubscribedUris().has(uri);
|
|
23
36
|
}
|
|
24
37
|
|
|
25
38
|
export function notify(uri, data) {
|
|
@@ -29,9 +42,11 @@ export function notify(uri, data) {
|
|
|
29
42
|
}
|
|
30
43
|
|
|
31
44
|
export function clear() {
|
|
32
|
-
|
|
45
|
+
sessionManager.clear();
|
|
33
46
|
}
|
|
34
47
|
|
|
35
48
|
export function size() {
|
|
36
|
-
return
|
|
37
|
-
}
|
|
49
|
+
return sessionManager.count();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export { sessionManager };
|