@atlisp/mcp 1.3.0 → 1.3.1
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/dist/atlisp-mcp.js +2314 -0
- package/dist/cad-worker.js +221 -0
- package/package.json +11 -6
- package/src/atlisp-mcp.js +0 -630
- package/src/cad-worker.js +0 -498
- package/src/cad.js +0 -271
- package/src/config.js +0 -50
- package/src/constants.js +0 -23
- package/src/errors.js +0 -55
- package/src/handlers/cad-handlers.js +0 -110
- package/src/handlers/function-handlers.js +0 -107
- package/src/handlers/package-handlers.js +0 -66
- package/src/handlers/prompt-handlers.js +0 -268
- package/src/handlers/resource-handlers.js +0 -364
- package/src/logger.js +0 -30
- package/src/session-manager.js +0 -89
- package/src/session-transport.js +0 -89
- package/src/sse-session-manager.js +0 -133
- package/src/sse-session.js +0 -314
- package/src/subscription-manager.js +0 -52
package/src/atlisp-mcp.js
DELETED
|
@@ -1,630 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
import { createRequire } from 'module';
|
|
5
|
-
|
|
6
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
const require = createRequire(import.meta.url);
|
|
8
|
-
const { version } = require(path.join(__dirname, '..', 'package.json'));
|
|
9
|
-
|
|
10
|
-
const isMcpScript = process.argv[1]?.includes('atlisp-mcp');
|
|
11
|
-
if (isMcpScript) {
|
|
12
|
-
const args = process.argv.slice(2);
|
|
13
|
-
for (let i = 0; i < args.length; i++) {
|
|
14
|
-
if (args[i] === '--version' || args[i] === '-v') {
|
|
15
|
-
console.log(`atlisp-mcp v${version}`);
|
|
16
|
-
process.exit(0);
|
|
17
|
-
} else if (args[i] === '--help' || args[i] === '-h') {
|
|
18
|
-
console.log(`
|
|
19
|
-
@lisp MCP Server v${version}
|
|
20
|
-
|
|
21
|
-
Usage: atlisp-mcp [options]
|
|
22
|
-
|
|
23
|
-
Options:
|
|
24
|
-
-v, --version Show version number
|
|
25
|
-
--transport <type> Transport type: http or stdio (default: http)
|
|
26
|
-
--port <port> HTTP port (default: 8110)
|
|
27
|
-
--host <host> HTTP host (default: 0.0.0.0)
|
|
28
|
-
--stdio Shorthand for --transport stdio
|
|
29
|
-
-h, --help Show this help message
|
|
30
|
-
|
|
31
|
-
Examples:
|
|
32
|
-
atlisp-mcp # Start HTTP server on port 8110
|
|
33
|
-
atlisp-mcp --port 3000 # Start HTTP server on port 3000
|
|
34
|
-
atlisp-mcp --stdio # Start in stdio mode for MCP clients
|
|
35
|
-
atlisp-mcp --transport stdio # Same as --stdio
|
|
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++;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
import express from 'express';
|
|
54
|
-
import rawBody from 'raw-body';
|
|
55
|
-
import iconv from 'iconv-lite';
|
|
56
|
-
import rateLimit from 'express-rate-limit';
|
|
57
|
-
import { randomUUID } from 'crypto';
|
|
58
|
-
import { AsyncLocalStorage } from 'async_hooks';
|
|
59
|
-
const { decode: charsetDecode, encodingExists } = iconv;
|
|
60
|
-
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
61
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
62
|
-
import { SessionTransport } from './session-transport.js';
|
|
63
|
-
import {
|
|
64
|
-
ListToolsRequestSchema,
|
|
65
|
-
CallToolRequestSchema,
|
|
66
|
-
ListResourcesRequestSchema,
|
|
67
|
-
ReadResourceRequestSchema,
|
|
68
|
-
SubscribeRequestSchema,
|
|
69
|
-
UnsubscribeRequestSchema,
|
|
70
|
-
ListPromptsRequestSchema,
|
|
71
|
-
GetPromptRequestSchema,
|
|
72
|
-
} from '@modelcontextprotocol/sdk/types.js';
|
|
73
|
-
import { cad } from './cad.js';
|
|
74
|
-
import { CAD_PLATFORMS, FILE_EXTENSIONS, PROTOCOL_VERSION, SERVER_NAME, SERVER_VERSION } from './constants.js';
|
|
75
|
-
import { log, closeLog } from './logger.js';
|
|
76
|
-
import * as cadHandlers from './handlers/cad-handlers.js';
|
|
77
|
-
import * as packageHandlers from './handlers/package-handlers.js';
|
|
78
|
-
import * as functionHandlers from './handlers/function-handlers.js';
|
|
79
|
-
import { listResources, readResource } from './handlers/resource-handlers.js';
|
|
80
|
-
import { listPrompts, getPrompt } from './handlers/prompt-handlers.js';
|
|
81
|
-
import * as subMgr from './subscription-manager.js';
|
|
82
|
-
import { sessionManager } from './subscription-manager.js';
|
|
83
|
-
import { SseSessionManager } from './sse-session-manager.js';
|
|
84
|
-
import { SseSession } from './sse-session.js';
|
|
85
|
-
import config from './config.js';
|
|
86
|
-
|
|
87
|
-
const mcpContext = new AsyncLocalStorage();
|
|
88
|
-
const sseSessionManager = new SseSessionManager();
|
|
89
|
-
|
|
90
|
-
const SERVER_CAPABILITIES = {
|
|
91
|
-
tools: {},
|
|
92
|
-
resources: { subscribe: true, listChanged: true },
|
|
93
|
-
prompts: {}
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
const SERVER_INFO = {
|
|
97
|
-
protocolVersion: PROTOCOL_VERSION,
|
|
98
|
-
capabilities: SERVER_CAPABILITIES,
|
|
99
|
-
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
const { port, host, transport, apiKey, rateLimitWindow, rateLimitMax } = config;
|
|
103
|
-
|
|
104
|
-
if (apiKey) log('API Key authentication enabled');
|
|
105
|
-
|
|
106
|
-
const mcpLimiter = rateLimit({
|
|
107
|
-
windowMs: rateLimitWindow,
|
|
108
|
-
max: rateLimitMax,
|
|
109
|
-
standardHeaders: true,
|
|
110
|
-
legacyHeaders: false,
|
|
111
|
-
message: { error: '请求过于频繁,请稍后再试' }
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
export { CAD_PLATFORMS, FILE_EXTENSIONS, MOCK_PACKAGES } from './constants.js';
|
|
115
|
-
|
|
116
|
-
const FUNCTION_LIB_URL = process.env.FUNCTION_LIB_URL || 'http://s3.atlisp.cn/json/functions.json';
|
|
117
|
-
let cachedFunctionLib = null;
|
|
118
|
-
let cachedAt = null;
|
|
119
|
-
const CACHE_TTL = 5 * 60 * 1000;
|
|
120
|
-
|
|
121
|
-
export async function loadAtlibFunctionLib() {
|
|
122
|
-
const now = Date.now();
|
|
123
|
-
if (cachedFunctionLib && cachedAt && (now - cachedAt) < CACHE_TTL) {
|
|
124
|
-
return cachedFunctionLib;
|
|
125
|
-
}
|
|
126
|
-
try {
|
|
127
|
-
const response = await fetch(FUNCTION_LIB_URL, { headers: { 'User-Agent': 'atlisp-mcp' } });
|
|
128
|
-
if (response.ok) {
|
|
129
|
-
const data = await response.json();
|
|
130
|
-
cachedFunctionLib = data;
|
|
131
|
-
cachedAt = now;
|
|
132
|
-
return data;
|
|
133
|
-
}
|
|
134
|
-
} catch (e) {
|
|
135
|
-
log(`loadAtlibFunctionLib error: ${e.message}`);
|
|
136
|
-
}
|
|
137
|
-
return null;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const TOOL_HANDLERS = {
|
|
141
|
-
connect_cad: () => cadHandlers.connectCad(),
|
|
142
|
-
eval_lisp: (a) => cadHandlers.evalLisp(a.code),
|
|
143
|
-
eval_lisp_with_result: (a) => cadHandlers.evalLisp(a.code, true, a.encoding),
|
|
144
|
-
get_cad_info: () => cadHandlers.getCadInfo(),
|
|
145
|
-
list_packages: () => packageHandlers.listPackages(),
|
|
146
|
-
search_packages: (a) => packageHandlers.searchPackages(a.query),
|
|
147
|
-
install_package: (a) => packageHandlers.installPackage(a.packageName),
|
|
148
|
-
get_platform_info: () => ({ content: [{ type: 'text', text: `支持的 CAD 平台:\n${CAD_PLATFORMS.join('\n')}\n当前连接: ${cad.connected ? '已连接' : '未连接'}` }] }),
|
|
149
|
-
at_command: (a) => cadHandlers.atCommand(a.command),
|
|
150
|
-
new_document: () => cadHandlers.newDocument(),
|
|
151
|
-
bring_to_front: () => cadHandlers.bringToFront(),
|
|
152
|
-
install_atlisp: () => cadHandlers.installAtlisp(),
|
|
153
|
-
init_atlisp: () => cadHandlers.initAtlisp(),
|
|
154
|
-
get_function_usage: (a) => functionHandlers.getFunctionUsage(a.name, a.package),
|
|
155
|
-
list_symbols: (a) => functionHandlers.listFunctions(a.package),
|
|
156
|
-
import_funlib: async (a) => {
|
|
157
|
-
const data = await loadAtlibFunctionLib();
|
|
158
|
-
if (!data) return { content: [{ type: 'text', text: '加载函数库失败' }], isError: true };
|
|
159
|
-
if (a.format === 'list') {
|
|
160
|
-
const items = data.functions?.map(f => `${f.name}: ${f.description || ''}`) || [];
|
|
161
|
-
return { content: [{ type: 'text', text: items.join('\n') }] };
|
|
162
|
-
}
|
|
163
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
164
|
-
},
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
function getOrCreateSessionServer(sessionId) {
|
|
168
|
-
let entry = sessionServers.get(sessionId);
|
|
169
|
-
if (!entry) {
|
|
170
|
-
const server = createMcpServer();
|
|
171
|
-
const transport = new SessionTransport(sessionId);
|
|
172
|
-
server.connect(transport);
|
|
173
|
-
entry = { server, transport };
|
|
174
|
-
sessionServers.set(sessionId, entry);
|
|
175
|
-
sessionTranports.set(sessionId, transport);
|
|
176
|
-
}
|
|
177
|
-
return entry;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function removeSessionServer(sessionId) {
|
|
181
|
-
const entry = sessionServers.get(sessionId);
|
|
182
|
-
if (entry) {
|
|
183
|
-
entry.transport.close().catch(() => {});
|
|
184
|
-
entry.server.close().catch(() => {});
|
|
185
|
-
sessionServers.delete(sessionId);
|
|
186
|
-
sessionTranports.delete(sessionId);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function broadcastToAllSessions(uri) {
|
|
191
|
-
for (const [sid, entry] of sessionServers) {
|
|
192
|
-
entry.server.sendResourceUpdated({ uri }).catch(err => {
|
|
193
|
-
log(`Failed to send resource update to session ${sid}: ${err.message}`);
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export const tools = [
|
|
199
|
-
{
|
|
200
|
-
name: 'connect_cad',
|
|
201
|
-
description: '连接到 CAD (AutoCAD/ZWCAD/GStarCAD/BricsCAD)',
|
|
202
|
-
inputSchema: { type: 'object', properties: {} }
|
|
203
|
-
},
|
|
204
|
-
{
|
|
205
|
-
name: 'eval_lisp',
|
|
206
|
-
description: '在 CAD 中执行 AutoLISP 代码(不返回结果)',
|
|
207
|
-
inputSchema: {
|
|
208
|
-
type: 'object',
|
|
209
|
-
properties: { code: { type: 'string', description: '要执行的 LISP 代码' } },
|
|
210
|
-
required: ['code']
|
|
211
|
-
}
|
|
212
|
-
},
|
|
213
|
-
{
|
|
214
|
-
name: 'eval_lisp_with_result',
|
|
215
|
-
description: '在 CAD 中执行 AutoLISP 代码并返回结果',
|
|
216
|
-
inputSchema: {
|
|
217
|
-
type: 'object',
|
|
218
|
-
properties: {
|
|
219
|
-
code: { type: 'string', description: '要执行的 LISP 代码' },
|
|
220
|
-
encoding: { type: 'string', description: '结果编码,如 utf-8, gbk, gb2312, gb18030(可选,默认自动检测)' }
|
|
221
|
-
},
|
|
222
|
-
required: ['code']
|
|
223
|
-
}
|
|
224
|
-
},
|
|
225
|
-
{
|
|
226
|
-
name: 'get_cad_info',
|
|
227
|
-
description: '获取当前 CAD 信息',
|
|
228
|
-
inputSchema: { type: 'object', properties: {} }
|
|
229
|
-
},
|
|
230
|
-
{
|
|
231
|
-
name: 'list_packages',
|
|
232
|
-
description: '列出已安装的 @lisp 包',
|
|
233
|
-
inputSchema: { type: 'object', properties: {} }
|
|
234
|
-
},
|
|
235
|
-
{
|
|
236
|
-
name: 'search_packages',
|
|
237
|
-
description: '搜索 @lisp 包',
|
|
238
|
-
inputSchema: {
|
|
239
|
-
type: 'object',
|
|
240
|
-
properties: { query: { type: 'string', description: '搜索关键词' } },
|
|
241
|
-
required: ['query']
|
|
242
|
-
}
|
|
243
|
-
},
|
|
244
|
-
{
|
|
245
|
-
name: 'install_package',
|
|
246
|
-
description: '安装 @lisp 包到 CAD',
|
|
247
|
-
inputSchema: {
|
|
248
|
-
type: 'object',
|
|
249
|
-
properties: { packageName: { type: 'string', description: '包名称' } },
|
|
250
|
-
required: ['packageName']
|
|
251
|
-
}
|
|
252
|
-
},
|
|
253
|
-
{
|
|
254
|
-
name: 'get_platform_info',
|
|
255
|
-
description: '获取 CAD 平台信息',
|
|
256
|
-
inputSchema: { type: 'object', properties: {} }
|
|
257
|
-
},
|
|
258
|
-
{
|
|
259
|
-
name: 'at_command',
|
|
260
|
-
description: '执行 @lisp 命令',
|
|
261
|
-
inputSchema: {
|
|
262
|
-
type: 'object',
|
|
263
|
-
properties: { command: { type: 'string', description: '@lisp 命令' } },
|
|
264
|
-
required: ['command']
|
|
265
|
-
}
|
|
266
|
-
},
|
|
267
|
-
{
|
|
268
|
-
name: 'new_document',
|
|
269
|
-
description: '在 CAD 中新建空白文档',
|
|
270
|
-
inputSchema: { type: 'object', properties: {} }
|
|
271
|
-
},
|
|
272
|
-
{
|
|
273
|
-
name: 'bring_to_front',
|
|
274
|
-
description: '将 CAD 窗口切换到前台(从后台运行状态唤醒)',
|
|
275
|
-
inputSchema: { type: 'object', properties: {} }
|
|
276
|
-
},
|
|
277
|
-
{
|
|
278
|
-
name: 'install_atlisp',
|
|
279
|
-
description: '在 CAD 中安装 @lisp',
|
|
280
|
-
inputSchema: { type: 'object', properties: {} }
|
|
281
|
-
},
|
|
282
|
-
{
|
|
283
|
-
name: 'init_atlisp',
|
|
284
|
-
description: '初始化 CAD 中的 @lisp 环境,加载函数库和初始变量',
|
|
285
|
-
inputSchema: { type: 'object', properties: {} }
|
|
286
|
-
},
|
|
287
|
-
{
|
|
288
|
-
name: 'get_function_usage',
|
|
289
|
-
description: '获取 @lisp 函数用法',
|
|
290
|
-
inputSchema: {
|
|
291
|
-
type: 'object',
|
|
292
|
-
properties: {
|
|
293
|
-
name: { type: 'string', description: '函数名,如 string:length' },
|
|
294
|
-
package: { type: 'string', description: '包名,如 string(可选,不传则自动搜索)' }
|
|
295
|
-
},
|
|
296
|
-
required: ['name']
|
|
297
|
-
}
|
|
298
|
-
},
|
|
299
|
-
{
|
|
300
|
-
name: 'list_symbols',
|
|
301
|
-
description: '列出 @lisp 所有符号,用于检查函数和变量',
|
|
302
|
-
inputSchema: {
|
|
303
|
-
type: 'object',
|
|
304
|
-
properties: {
|
|
305
|
-
package: { type: 'string', description: '包名,如 string(可选)' }
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
},
|
|
309
|
-
{
|
|
310
|
-
name: 'import_funlib',
|
|
311
|
-
description: '从网络导入 @lisp 函数库数据到 AI Agent',
|
|
312
|
-
inputSchema: {
|
|
313
|
-
type: 'object',
|
|
314
|
-
properties: {
|
|
315
|
-
format: { type: 'string', enum: ['json', 'list'], description: '返回格式:json(完整JSON)或 list(函数名:描述 列表),默认 json' }
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
];
|
|
320
|
-
|
|
321
|
-
export async function handleToolCall(name, args) {
|
|
322
|
-
const handler = TOOL_HANDLERS[name];
|
|
323
|
-
if (!handler) return { content: [{ type: 'text', text: `未知工具: ${name}` }], isError: true };
|
|
324
|
-
try {
|
|
325
|
-
const result = await handler(args || {});
|
|
326
|
-
notifyResourceChanges(name);
|
|
327
|
-
return result;
|
|
328
|
-
} catch (e) {
|
|
329
|
-
log(`Tool call error [${name}]: ${e.message}`);
|
|
330
|
-
return { content: [{ type: 'text', text: `工具执行错误: ${e.message}` }], isError: true };
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function notifyResourceChanges(toolName) {
|
|
335
|
-
const resourceMap = {
|
|
336
|
-
connect_cad: ['atlisp://cad/info'],
|
|
337
|
-
new_document: ['atlisp://dwg/name', 'atlisp://dwg/path', 'atlisp://cad/layers', 'atlisp://cad/entities'],
|
|
338
|
-
install_package: ['atlisp://packages'],
|
|
339
|
-
install_atlisp: ['atlisp://packages'],
|
|
340
|
-
};
|
|
341
|
-
const uris = resourceMap[toolName];
|
|
342
|
-
if (uris) {
|
|
343
|
-
for (const uri of uris) {
|
|
344
|
-
if (subMgr.isSubscribed(uri)) {
|
|
345
|
-
subMgr.notify(uri, null);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
async function initCadConnection() {
|
|
352
|
-
log('Attempting to connect to CAD on startup...');
|
|
353
|
-
try {
|
|
354
|
-
const connected = await cad.connect();
|
|
355
|
-
if (connected) {
|
|
356
|
-
log('Auto-connected to CAD: ' + cad.getPlatform() + ' ' + cad.getVersion());
|
|
357
|
-
console.error('已自动连接到 CAD: ' + cad.getPlatform() + ' ' + cad.getVersion());
|
|
358
|
-
} else {
|
|
359
|
-
log('No CAD found on startup');
|
|
360
|
-
}
|
|
361
|
-
} catch (e) {
|
|
362
|
-
log('Auto-connect failed: ' + e.message);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
const sessionServers = new Map();
|
|
367
|
-
const sessionTranports = new Map();
|
|
368
|
-
|
|
369
|
-
export function createMcpServer() {
|
|
370
|
-
const server = new Server(
|
|
371
|
-
{ name: SERVER_NAME, version: SERVER_VERSION },
|
|
372
|
-
{ capabilities: SERVER_CAPABILITIES }
|
|
373
|
-
);
|
|
374
|
-
|
|
375
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
|
|
376
|
-
|
|
377
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
378
|
-
const { name, arguments: args } = request.params;
|
|
379
|
-
return await handleToolCall(name, args || {});
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
|
|
383
|
-
const ctx = mcpContext.getStore();
|
|
384
|
-
const sessionId = ctx?.sessionId;
|
|
385
|
-
const subscribedUris = sessionId ? subMgr.list(sessionId) : subMgr.list();
|
|
386
|
-
return {
|
|
387
|
-
resources: await listResources(subscribedUris)
|
|
388
|
-
};
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
392
|
-
const { uri } = request.params;
|
|
393
|
-
try {
|
|
394
|
-
const data = await readResource(uri);
|
|
395
|
-
return {
|
|
396
|
-
contents: [{
|
|
397
|
-
uri,
|
|
398
|
-
mimeType: 'application/json',
|
|
399
|
-
text: JSON.stringify(data)
|
|
400
|
-
}]
|
|
401
|
-
};
|
|
402
|
-
} catch (e) {
|
|
403
|
-
return {
|
|
404
|
-
contents: [],
|
|
405
|
-
isError: true,
|
|
406
|
-
error: { code: -32603, message: e.message }
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
server.setRequestHandler(SubscribeRequestSchema, async (request) => {
|
|
412
|
-
const { uri } = request.params;
|
|
413
|
-
const ctx = mcpContext.getStore();
|
|
414
|
-
subMgr.subscribe(uri, ctx?.sessionId);
|
|
415
|
-
return { success: true };
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
server.setRequestHandler(UnsubscribeRequestSchema, async (request) => {
|
|
419
|
-
const { uri } = request.params;
|
|
420
|
-
const ctx = mcpContext.getStore();
|
|
421
|
-
subMgr.unsubscribe(uri, ctx?.sessionId);
|
|
422
|
-
return { success: true };
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
426
|
-
prompts: await listPrompts()
|
|
427
|
-
}));
|
|
428
|
-
|
|
429
|
-
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
430
|
-
const { name, arguments: args } = request.params;
|
|
431
|
-
return await getPrompt(name, args || {});
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
return server;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
export async function startServer() {
|
|
440
|
-
if (config.transport === 'stdio') {
|
|
441
|
-
await initCadConnection();
|
|
442
|
-
const mcpServer = createMcpServer();
|
|
443
|
-
const transport = new StdioServerTransport();
|
|
444
|
-
const sessionId = 'stdio-session';
|
|
445
|
-
sessionManager.createSession(sessionId);
|
|
446
|
-
await mcpContext.run({ sessionId }, async () => {
|
|
447
|
-
await mcpServer.connect(transport);
|
|
448
|
-
});
|
|
449
|
-
console.error(`${SERVER_NAME} 运行在 stdio 模式`);
|
|
450
|
-
|
|
451
|
-
subMgr.setNotify((uri, data) => {
|
|
452
|
-
mcpServer.sendResourceUpdated({ uri }).catch(err => {
|
|
453
|
-
log(`Failed to send resource update: ${err.message}`);
|
|
454
|
-
});
|
|
455
|
-
});
|
|
456
|
-
} else {
|
|
457
|
-
const app = express();
|
|
458
|
-
|
|
459
|
-
const JSON_CONTENT_TYPES = ['application/json', 'application/json-rpc', 'application/vnd.api+json'];
|
|
460
|
-
app.use(async (req, res, next) => {
|
|
461
|
-
const ct = (req.headers['content-type'] || '').toLowerCase();
|
|
462
|
-
if (!JSON_CONTENT_TYPES.some(t => ct.startsWith(t))) return next();
|
|
463
|
-
try {
|
|
464
|
-
const buf = await rawBody(req, { limit: '10mb' });
|
|
465
|
-
const m = ct.match(/charset\s*=\s*([^\s;]+)/i);
|
|
466
|
-
let charset = m ? m[1].toLowerCase() : 'utf-8';
|
|
467
|
-
const charsetMap = { 'gb2312': 'gbk', 'gb18030': 'gbk', 'gbk': 'gbk', '936': 'gbk' };
|
|
468
|
-
charset = charsetMap[charset] || charset;
|
|
469
|
-
const str = encodingExists(charset) ? charsetDecode(buf, charset) : buf.toString('utf-8');
|
|
470
|
-
req.body = JSON.parse(str);
|
|
471
|
-
next();
|
|
472
|
-
} catch (e) {
|
|
473
|
-
res.status(400).json({ error: 'Invalid JSON body', message: e.message });
|
|
474
|
-
}
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
app.use((req, res, next) => {
|
|
478
|
-
log(`${req.method} ${req.path}`);
|
|
479
|
-
next();
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
const PUBLIC_PATHS = ['/health'];
|
|
483
|
-
if (apiKey) {
|
|
484
|
-
app.use((req, res, next) => {
|
|
485
|
-
if (PUBLIC_PATHS.includes(req.path)) return next();
|
|
486
|
-
const auth = req.get('Authorization');
|
|
487
|
-
if (auth === `Bearer ${apiKey}`) return next();
|
|
488
|
-
res.status(401).json({ error: 'Unauthorized' });
|
|
489
|
-
});
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
app.get('/health', (req, res) => {
|
|
493
|
-
res.json({ status: 'ok', cad: cad.connected ? 'connected' : 'disconnected' });
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
if (config.enableCors) {
|
|
497
|
-
app.use((req, res, next) => {
|
|
498
|
-
res.setHeader('Access-Control-Allow-Origin', config.corsOrigin);
|
|
499
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, CONNECT');
|
|
500
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Accept, mcp-session-id, last-event-id');
|
|
501
|
-
res.setHeader('Access-Control-Expose-Headers', 'mcp-session-id, content-type, x-accel-buffering');
|
|
502
|
-
res.setHeader('Access-Control-Max-Age', '86400');
|
|
503
|
-
res.setHeader('Vary', 'Accept');
|
|
504
|
-
if (req.method === 'OPTIONS') return res.status(204).end();
|
|
505
|
-
next();
|
|
506
|
-
});
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
subMgr.setNotify((uri, data) => {
|
|
510
|
-
broadcastToAllSessions(uri);
|
|
511
|
-
for (const session of sseSessionManager.listActive()) {
|
|
512
|
-
sseSessionManager.sendToClient(session.clientId, 'resource_updated', { uri });
|
|
513
|
-
}
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
async function handleMcpPost(req, res) {
|
|
517
|
-
const sessionId = req.headers['mcp-session-id'] || req.query.sessionId || randomUUID();
|
|
518
|
-
sessionManager.createSession(sessionId);
|
|
519
|
-
const body = req.body;
|
|
520
|
-
if (!body || !body.method) {
|
|
521
|
-
return res.status(400).json({ error: 'Invalid request' });
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
const { server, transport } = getOrCreateSessionServer(sessionId);
|
|
525
|
-
transport.resetCleanupTimer();
|
|
526
|
-
|
|
527
|
-
await mcpContext.run({ sessionId }, async () => {
|
|
528
|
-
try {
|
|
529
|
-
const response = await transport.handleRequest(body);
|
|
530
|
-
if (response) {
|
|
531
|
-
const headers = { 'Content-Type': 'application/json' };
|
|
532
|
-
if (transport.sessionId) {
|
|
533
|
-
headers['mcp-session-id'] = transport.sessionId;
|
|
534
|
-
}
|
|
535
|
-
res.writeHead(200, headers);
|
|
536
|
-
res.end(JSON.stringify(response));
|
|
537
|
-
} else {
|
|
538
|
-
if (!res.headersSent) {
|
|
539
|
-
res.status(202).end();
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
} catch (error) {
|
|
543
|
-
log(`Transport error [${sessionId}]: ${error.message}`);
|
|
544
|
-
if (!res.headersSent) {
|
|
545
|
-
res.status(500).json({ error: 'Internal server error', message: error.message });
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
});
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
app.all('/mcp', mcpLimiter, handleMcpPost);
|
|
552
|
-
app.all('/mcp/:path', mcpLimiter, handleMcpPost);
|
|
553
|
-
app.get('/mcp/stream', mcpLimiter, handleMcpPost);
|
|
554
|
-
|
|
555
|
-
const sseEndpoint = '/sse';
|
|
556
|
-
app.get(sseEndpoint, async (req, res) => {
|
|
557
|
-
const sessionId = randomUUID();
|
|
558
|
-
sessionManager.createSession(sessionId);
|
|
559
|
-
getOrCreateSessionServer(sessionId);
|
|
560
|
-
|
|
561
|
-
const sseSession = new SseSession(res, { clientId: sessionId });
|
|
562
|
-
sseSessionManager.add(sessionId, sseSession);
|
|
563
|
-
|
|
564
|
-
sseSession.sendEndpoint(`/message?sessionId=${sessionId}`);
|
|
565
|
-
|
|
566
|
-
req.on('close', () => {
|
|
567
|
-
sseSessionManager.remove(sessionId);
|
|
568
|
-
removeSessionServer(sessionId);
|
|
569
|
-
});
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
app.post('/message', mcpLimiter, async (req, res) => {
|
|
573
|
-
const sessionId = req.query.sessionId;
|
|
574
|
-
if (!sessionId) {
|
|
575
|
-
return res.status(400).json({ error: 'Missing sessionId' });
|
|
576
|
-
}
|
|
577
|
-
const sseSession = sseSessionManager.get(sessionId);
|
|
578
|
-
if (!sseSession) {
|
|
579
|
-
return res.status(404).json({ error: 'Session not found' });
|
|
580
|
-
}
|
|
581
|
-
sessionManager.createSession(sessionId);
|
|
582
|
-
const { server, transport } = getOrCreateSessionServer(sessionId);
|
|
583
|
-
transport.resetCleanupTimer();
|
|
584
|
-
|
|
585
|
-
const body = req.body;
|
|
586
|
-
if (body && body.method) {
|
|
587
|
-
try {
|
|
588
|
-
const response = await transport.handleRequest(body);
|
|
589
|
-
if (response) {
|
|
590
|
-
sseSession.sendResponse(response);
|
|
591
|
-
}
|
|
592
|
-
if (!res.headersSent) {
|
|
593
|
-
res.status(202).end();
|
|
594
|
-
}
|
|
595
|
-
} catch (error) {
|
|
596
|
-
log(`SSE message error [${sessionId}]: ${error.message}`);
|
|
597
|
-
if (!res.headersSent) {
|
|
598
|
-
res.status(500).json({ error: 'Internal server error' });
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
} else {
|
|
602
|
-
res.status(400).json({ error: 'Invalid request' });
|
|
603
|
-
}
|
|
604
|
-
});
|
|
605
|
-
|
|
606
|
-
const httpServer = app.listen(config.port, config.host, async () => {
|
|
607
|
-
await initCadConnection();
|
|
608
|
-
console.error(`@lisp MCP Server 启动 - http://${config.host}:${config.port} (多会话模式)`);
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
async function shutdown(signal) {
|
|
612
|
-
console.error(`\n收到 ${signal},正在优雅关闭...`);
|
|
613
|
-
sseSessionManager.closeAll();
|
|
614
|
-
sessionManager.clear();
|
|
615
|
-
for (const [sid] of sessionServers) {
|
|
616
|
-
removeSessionServer(sid);
|
|
617
|
-
}
|
|
618
|
-
await cad.disconnect();
|
|
619
|
-
closeLog();
|
|
620
|
-
httpServer.close(() => process.exit(0));
|
|
621
|
-
setTimeout(() => process.exit(1), 5000);
|
|
622
|
-
}
|
|
623
|
-
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
624
|
-
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
if (!process.env.VITEST) {
|
|
629
|
-
await startServer();
|
|
630
|
-
}
|