@atlisp/mcp 1.8.10 → 1.8.12
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 +636 -300
- package/dist/atlisp-mcp.js +14350 -11639
- package/dist/cad-worker.js +6 -66
- package/dist/config.js +241 -0
- package/dist/lisp-security.js +76 -0
- package/dist/logger.js +163 -0
- package/package.json +2 -2
package/dist/cad-worker.js
CHANGED
|
@@ -4,6 +4,7 @@ import process from 'process';
|
|
|
4
4
|
import fs from 'fs';
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import crypto from 'crypto';
|
|
7
|
+
import { validateLispCodeWorker, checkParens } from './lisp-security.js';
|
|
7
8
|
|
|
8
9
|
const BUSY_RETRIES = parseInt(process.env.BUSY_RETRIES || '10', 10);
|
|
9
10
|
const BUSY_DELAY = parseInt(process.env.BUSY_DELAY || '500', 10);
|
|
@@ -110,8 +111,8 @@ const cadLaunch = edge.func(function() {/*
|
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
} catch (Exception ex) {
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
System.Diagnostics.Debug.WriteLine("cadLaunch create " + progIds[i] + " error: " + ex.Message);
|
|
115
|
+
}
|
|
115
116
|
}
|
|
116
117
|
|
|
117
118
|
return new { success = false, error = "No CAD found" };
|
|
@@ -470,68 +471,7 @@ async function waitForIdle(platform) {
|
|
|
470
471
|
return false;
|
|
471
472
|
}
|
|
472
473
|
|
|
473
|
-
|
|
474
|
-
{ re: /\beval\s*[(\s]/i, msg: '禁止使用 eval 函数' },
|
|
475
|
-
{ re: /\bload\s*[(\s"]/i, msg: '禁止使用 load 函数' },
|
|
476
|
-
{ re: /\bstartapp\s*[(\s]/i, msg: '禁止使用 startapp 函数' },
|
|
477
|
-
{ re: /\bvl-bt\b/i, msg: '禁止使用 vl-bt 函数' },
|
|
478
|
-
{ re: /\bvl-exit-with-value\b/i, msg: '禁止使用 vl-exit-with-value' },
|
|
479
|
-
{ re: /\bvl-exit-with-error\b/i, msg: '禁止使用 vl-exit-with-error' },
|
|
480
|
-
{ re: /\bvlax-import-type-library\b/i, msg: '禁止导入类型库' },
|
|
481
|
-
{ re: /\bvlax-add-cmd\b/i, msg: '禁止注册新命令' },
|
|
482
|
-
{ re: /\bvla-Delete\b/i, msg: '禁止直接删除 vla 对象' },
|
|
483
|
-
{ re: /\bvla-Save\b/i, msg: '禁止直接保存文档' },
|
|
484
|
-
{ re: /\bcommand\s*["\s]*[_(]?\s*["']?\.?\s*_?(?:quit|exit|close|open|new|recover|audit|purge|wblock|export)/i, msg: '禁止使用危险 CAD 命令' },
|
|
485
|
-
{ re: /\bdirectory\s*[(\s]/i, msg: '禁止使用 directory 函数' },
|
|
486
|
-
{ re: /\b(?:read-line|read-char|write-line|write-char)\s*[(\s]/i, msg: '禁止直接文件 I/O' },
|
|
487
|
-
{ re: /\bvla-deletefile\b/i, msg: '禁止删除文件' },
|
|
488
|
-
{ re: /\bvla-copyfile\b/i, msg: '禁止复制文件' },
|
|
489
|
-
{ re: /\bvla-movefile\b/i, msg: '禁止移动文件' },
|
|
490
|
-
{ re: /\bdos_command_string\s*[(\s]/i, msg: '禁止使用 dos_command_string' },
|
|
491
|
-
];
|
|
492
|
-
|
|
493
|
-
function validateLispCode(code) {
|
|
494
|
-
const withoutStrings = code.replace(/"([^"\\]*(?:\\.[^"\\]*)*)"/g, '');
|
|
495
|
-
const withoutComments = withoutStrings.replace(/;.*$/gm, '');
|
|
496
|
-
for (const { re, msg } of DANGEROUS_PATTERNS) {
|
|
497
|
-
if (re.test(withoutComments)) {
|
|
498
|
-
return { valid: false, error: msg };
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
return { valid: true };
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
export function checkParens(code) {
|
|
505
|
-
let depth = 0;
|
|
506
|
-
let inString = false;
|
|
507
|
-
for (let i = 0; i < code.length; i++) {
|
|
508
|
-
const ch = code[i];
|
|
509
|
-
if (inString) {
|
|
510
|
-
if (ch === '\\' && i + 1 < code.length) {
|
|
511
|
-
i++;
|
|
512
|
-
continue;
|
|
513
|
-
}
|
|
514
|
-
if (ch === '"') {
|
|
515
|
-
inString = false;
|
|
516
|
-
}
|
|
517
|
-
continue;
|
|
518
|
-
}
|
|
519
|
-
if (ch === '"') {
|
|
520
|
-
inString = true;
|
|
521
|
-
continue;
|
|
522
|
-
}
|
|
523
|
-
if (ch === ';') {
|
|
524
|
-
while (i < code.length && code[i] !== '\n') i++;
|
|
525
|
-
continue;
|
|
526
|
-
}
|
|
527
|
-
if (ch === '(') depth++;
|
|
528
|
-
if (ch === ')') depth--;
|
|
529
|
-
if (depth < 0) return { valid: false, error: '多余右括号 )', pos: i };
|
|
530
|
-
}
|
|
531
|
-
if (depth > 0) return { valid: false, error: `缺少 ${depth} 个右括号 )`, pos: code.length - 1 };
|
|
532
|
-
if (depth < 0) return { valid: false, error: `多余 ${-depth} 个左括号 (` };
|
|
533
|
-
return { valid: true };
|
|
534
|
-
}
|
|
474
|
+
export { checkParens } from './lisp-security.js';
|
|
535
475
|
|
|
536
476
|
async function connectToPlatform(targetPlatform) {
|
|
537
477
|
return new Promise((resolve, reject) => {
|
|
@@ -609,7 +549,7 @@ async function handleMessage(msg) {
|
|
|
609
549
|
if (!parenCheck.valid) {
|
|
610
550
|
return { success: false, error: `括号错误: ${parenCheck.error} (位置 ${parenCheck.pos})` };
|
|
611
551
|
}
|
|
612
|
-
const injectionCheck =
|
|
552
|
+
const injectionCheck = validateLispCodeWorker(msg.code);
|
|
613
553
|
if (!injectionCheck.valid) {
|
|
614
554
|
return { success: false, error: `安全限制: ${injectionCheck.error}` };
|
|
615
555
|
}
|
|
@@ -662,7 +602,7 @@ async function handleMessage(msg) {
|
|
|
662
602
|
if (!parenCheck.valid) {
|
|
663
603
|
return { success: false, error: `括号错误: ${parenCheck.error} (位置 ${parenCheck.pos})` };
|
|
664
604
|
}
|
|
665
|
-
const injectionCheck =
|
|
605
|
+
const injectionCheck = validateLispCodeWorker(msg.code);
|
|
666
606
|
if (!injectionCheck.valid) {
|
|
667
607
|
return { success: false, error: `安全限制: ${injectionCheck.error}` };
|
|
668
608
|
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
|
|
6
|
+
const envSchema = z.object({
|
|
7
|
+
PORT: z.string().optional().default('8110'),
|
|
8
|
+
HOST: z.string().optional().default('0.0.0.0'),
|
|
9
|
+
TRANSPORT: z.enum(['http', 'stdio', 'ws']).optional().default('http'),
|
|
10
|
+
MCP_API_KEY: z.string().optional().default(''),
|
|
11
|
+
ENABLE_SSE: z.string().optional().default('true'),
|
|
12
|
+
MESSAGE_TIMEOUT: z.string().optional().default('60000'),
|
|
13
|
+
HEARTBEAT_INTERVAL: z.string().optional().default('30000'),
|
|
14
|
+
BUSY_RETRIES: z.string().optional().default('10'),
|
|
15
|
+
BUSY_DELAY: z.string().optional().default('500'),
|
|
16
|
+
ENABLE_CORS: z.string().optional().default('true'),
|
|
17
|
+
CORS_ORIGIN: z.string().optional().default('*'),
|
|
18
|
+
RATE_LIMIT_WINDOW: z.string().optional().default('60000'),
|
|
19
|
+
RATE_LIMIT_MAX: z.string().optional().default('100'),
|
|
20
|
+
RESOURCE_CACHE_TTL: z.string().optional().default('5000'),
|
|
21
|
+
FUNCTION_LIB_CACHE_TTL: z.string().optional().default('300000'),
|
|
22
|
+
DEBUG: z.string().optional().default('false'),
|
|
23
|
+
DEBUG_FILE: z.string().optional().default(''),
|
|
24
|
+
FUNCTION_LIB_URL: z.string().optional().default('http://s3.atlisp.cn/json/functions.json'),
|
|
25
|
+
CONFIG_FILE: z.string().optional().default(''),
|
|
26
|
+
API_KEY_RATE_LIMIT: z.string().optional().default('1000'),
|
|
27
|
+
METRICS_ENABLED: z.string().optional().default('true'),
|
|
28
|
+
REQUEST_LOG_ENABLED: z.string().optional().default('true'),
|
|
29
|
+
SSL_KEY_PATH: z.string().optional().default(''),
|
|
30
|
+
SSL_CERT_PATH: z.string().optional().default(''),
|
|
31
|
+
LLM_API_KEY: z.string().optional().default(''),
|
|
32
|
+
LLM_API_URL: z.string().optional().default('https://api.openai.com/v1/chat/completions'),
|
|
33
|
+
LLM_MODEL: z.string().optional().default('gpt-4o-mini'),
|
|
34
|
+
LLM_MAX_TOKENS: z.string().optional().default('4096'),
|
|
35
|
+
LLM_TEMPERATURE: z.string().optional().default('0.7'),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const configSchema = z.object({
|
|
39
|
+
port: z.number().optional(),
|
|
40
|
+
host: z.string().optional(),
|
|
41
|
+
transport: z.enum(['http', 'stdio', 'ws']).optional(),
|
|
42
|
+
apiKey: z.string().optional(),
|
|
43
|
+
enableSse: z.boolean().optional(),
|
|
44
|
+
messageTimeout: z.number().optional(),
|
|
45
|
+
heartbeatInterval: z.number().optional(),
|
|
46
|
+
busyRetries: z.number().optional(),
|
|
47
|
+
busyDelay: z.number().optional(),
|
|
48
|
+
enableCors: z.boolean().optional(),
|
|
49
|
+
corsOrigin: z.string().optional(),
|
|
50
|
+
rateLimitWindow: z.number().optional(),
|
|
51
|
+
rateLimitMax: z.number().optional(),
|
|
52
|
+
resourceCacheTtl: z.number().optional(),
|
|
53
|
+
functionLibCacheTtl: z.number().optional(),
|
|
54
|
+
debug: z.boolean().optional(),
|
|
55
|
+
debugFile: z.string().optional(),
|
|
56
|
+
functionLibUrl: z.string().optional(),
|
|
57
|
+
configFile: z.string().optional(),
|
|
58
|
+
apiKeyRateLimit: z.number().optional(),
|
|
59
|
+
metricsEnabled: z.boolean().optional(),
|
|
60
|
+
apiKeys: z.array(z.object({
|
|
61
|
+
key: z.string(),
|
|
62
|
+
limit: z.number(),
|
|
63
|
+
name: z.string().optional(),
|
|
64
|
+
role: z.string().optional()
|
|
65
|
+
})).optional(),
|
|
66
|
+
sslKey: z.string().optional(),
|
|
67
|
+
sslCert: z.string().optional(),
|
|
68
|
+
llmApiKey: z.string().optional(),
|
|
69
|
+
llmApiUrl: z.string().optional(),
|
|
70
|
+
llmModel: z.string().optional(),
|
|
71
|
+
llmMaxTokens: z.number().optional(),
|
|
72
|
+
llmTemperature: z.number().optional(),
|
|
73
|
+
maxPoolSize: z.number().optional(),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
function parseArgs() {
|
|
77
|
+
const args = process.argv.slice(2);
|
|
78
|
+
const result = {};
|
|
79
|
+
for (let i = 0; i < args.length; i++) {
|
|
80
|
+
if (args[i] === '--transport' && args[i + 1]) {
|
|
81
|
+
result.transport = args[i + 1];
|
|
82
|
+
i++;
|
|
83
|
+
} else if (args[i] === '--stdio') {
|
|
84
|
+
result.transport = 'stdio';
|
|
85
|
+
} else if (args[i] === '--ws') {
|
|
86
|
+
result.transport = 'ws';
|
|
87
|
+
} else if (args[i] === '--port' && args[i + 1]) {
|
|
88
|
+
result.port = args[i + 1];
|
|
89
|
+
i++;
|
|
90
|
+
} else if (args[i] === '--host' && args[i + 1]) {
|
|
91
|
+
result.host = args[i + 1];
|
|
92
|
+
i++;
|
|
93
|
+
} else if (args[i] === '--config' && args[i + 1]) {
|
|
94
|
+
result.configFile = args[i + 1];
|
|
95
|
+
i++;
|
|
96
|
+
} else if (args[i] === '--ssl-key' && args[i + 1]) {
|
|
97
|
+
result.sslKey = args[i + 1];
|
|
98
|
+
i++;
|
|
99
|
+
} else if (args[i] === '--ssl-cert' && args[i + 1]) {
|
|
100
|
+
result.sslCert = args[i + 1];
|
|
101
|
+
i++;
|
|
102
|
+
} else if (args[i] === '--debug') {
|
|
103
|
+
result.debug = true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export { parseArgs };
|
|
110
|
+
|
|
111
|
+
function loadConfigFile(filePath) {
|
|
112
|
+
if (!filePath || !fs.existsSync(filePath)) return null;
|
|
113
|
+
try {
|
|
114
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
115
|
+
const parsed = JSON.parse(raw);
|
|
116
|
+
const result = configSchema.safeParse(parsed);
|
|
117
|
+
if (result.success) {
|
|
118
|
+
return result.data;
|
|
119
|
+
}
|
|
120
|
+
console.error('配置文件格式错误:', result.error);
|
|
121
|
+
} catch (e) {
|
|
122
|
+
console.error('加载配置文件失败:', e.message);
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const cliArgs = parseArgs();
|
|
128
|
+
const envResult = envSchema.safeParse(process.env);
|
|
129
|
+
|
|
130
|
+
if (!envResult.success) {
|
|
131
|
+
console.error('环境变量配置错误:', envResult.error.flatten().fieldErrors);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const env = envResult.data;
|
|
136
|
+
|
|
137
|
+
const configFilePath = env.CONFIG_FILE || cliArgs.configFile ||
|
|
138
|
+
path.join(os.homedir(), '.atlisp', 'atlisp.config.json');
|
|
139
|
+
|
|
140
|
+
const fileConfig = loadConfigFile(configFilePath);
|
|
141
|
+
|
|
142
|
+
const config = {
|
|
143
|
+
port: fileConfig?.port || parseInt(cliArgs.port || env.PORT, 10),
|
|
144
|
+
host: fileConfig?.host || cliArgs.host || env.HOST,
|
|
145
|
+
transport: fileConfig?.transport ?? cliArgs.transport ?? env.TRANSPORT,
|
|
146
|
+
apiKey: fileConfig?.apiKey || env.MCP_API_KEY,
|
|
147
|
+
enableSse: fileConfig?.enableSse ?? (env.ENABLE_SSE === 'true'),
|
|
148
|
+
messageTimeout: fileConfig?.messageTimeout || parseInt(env.MESSAGE_TIMEOUT, 10),
|
|
149
|
+
heartbeatInterval: fileConfig?.heartbeatInterval || parseInt(env.HEARTBEAT_INTERVAL, 10),
|
|
150
|
+
busyRetries: fileConfig?.busyRetries || parseInt(env.BUSY_RETRIES, 10),
|
|
151
|
+
busyDelay: fileConfig?.busyDelay || parseInt(env.BUSY_DELAY, 10),
|
|
152
|
+
enableCors: fileConfig?.enableCors ?? (env.ENABLE_CORS === 'true'),
|
|
153
|
+
corsOrigin: fileConfig?.corsOrigin || env.CORS_ORIGIN,
|
|
154
|
+
rateLimitWindow: fileConfig?.rateLimitWindow || parseInt(env.RATE_LIMIT_WINDOW, 10),
|
|
155
|
+
rateLimitMax: fileConfig?.rateLimitMax || parseInt(env.RATE_LIMIT_MAX, 10),
|
|
156
|
+
resourceCacheTtl: fileConfig?.resourceCacheTtl || parseInt(env.RESOURCE_CACHE_TTL, 10),
|
|
157
|
+
functionLibCacheTtl: fileConfig?.functionLibCacheTtl || parseInt(env.FUNCTION_LIB_CACHE_TTL, 10),
|
|
158
|
+
debug: fileConfig?.debug ?? (env.DEBUG === 'true'),
|
|
159
|
+
debugFile: fileConfig?.debugFile || env.DEBUG_FILE,
|
|
160
|
+
functionLibUrl: fileConfig?.functionLibUrl || env.FUNCTION_LIB_URL,
|
|
161
|
+
configFile: configFilePath,
|
|
162
|
+
apiKeyRateLimit: fileConfig?.apiKeyRateLimit || parseInt(env.API_KEY_RATE_LIMIT, 10),
|
|
163
|
+
apiKeys: fileConfig?.apiKeys || [],
|
|
164
|
+
metricsEnabled: fileConfig?.metricsEnabled ?? (env.METRICS_ENABLED === 'true'),
|
|
165
|
+
requestLogEnabled: fileConfig?.requestLogEnabled ?? (env.REQUEST_LOG_ENABLED !== 'false'),
|
|
166
|
+
sslKey: fileConfig?.sslKey || env.SSL_KEY_PATH || cliArgs.sslKey,
|
|
167
|
+
sslCert: fileConfig?.sslCert || env.SSL_CERT_PATH || cliArgs.sslCert,
|
|
168
|
+
llmApiKey: fileConfig?.llmApiKey || env.LLM_API_KEY,
|
|
169
|
+
llmApiUrl: fileConfig?.llmApiUrl || env.LLM_API_URL,
|
|
170
|
+
llmModel: fileConfig?.llmModel || env.LLM_MODEL,
|
|
171
|
+
llmMaxTokens: fileConfig?.llmMaxTokens || parseInt(env.LLM_MAX_TOKENS, 10),
|
|
172
|
+
llmTemperature: fileConfig?.llmTemperature || parseFloat(env.LLM_TEMPERATURE),
|
|
173
|
+
maxPoolSize: fileConfig?.maxPoolSize || 3,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
export function validateConfig() {
|
|
177
|
+
const errors = [];
|
|
178
|
+
if (config.transport === 'http' || config.transport === 'ws') {
|
|
179
|
+
if (config.port < 1 || config.port > 65535) {
|
|
180
|
+
errors.push(`端口号无效: ${config.port}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (config.sslKey && !fs.existsSync(config.sslKey)) {
|
|
184
|
+
errors.push(`SSL 密钥文件不存在: ${config.sslKey}`);
|
|
185
|
+
}
|
|
186
|
+
if (config.sslCert && !fs.existsSync(config.sslCert)) {
|
|
187
|
+
errors.push(`SSL 证书文件不存在: ${config.sslCert}`);
|
|
188
|
+
}
|
|
189
|
+
if (config.messageTimeout < 1000) {
|
|
190
|
+
errors.push(`消息超时过短: ${config.messageTimeout}ms (最小 1000ms)`);
|
|
191
|
+
}
|
|
192
|
+
if (config.rateLimitMax < 1) {
|
|
193
|
+
errors.push(`速率限制无效: ${config.rateLimitMax}`);
|
|
194
|
+
}
|
|
195
|
+
if (errors.length > 0) {
|
|
196
|
+
console.error('配置验证错误:');
|
|
197
|
+
errors.forEach(e => console.error(` - ${e}`));
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let configWatchers = [];
|
|
204
|
+
|
|
205
|
+
if (configFilePath && fs.existsSync(configFilePath)) {
|
|
206
|
+
try {
|
|
207
|
+
fs.watch(configFilePath, (eventType) => {
|
|
208
|
+
if (eventType === 'change') {
|
|
209
|
+
const newFileConfig = loadConfigFile(configFilePath);
|
|
210
|
+
if (newFileConfig) {
|
|
211
|
+
Object.assign(config, newFileConfig);
|
|
212
|
+
configWatchers.forEach(fn => fn(config));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
} catch (e) { console.error(`Config file watch error: ${e.message}`); }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function getConfig(key) {
|
|
220
|
+
return config[key];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function getAllConfig() {
|
|
224
|
+
return { ...config };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function reloadConfig() {
|
|
228
|
+
const newFileConfig = loadConfigFile(config.configFile);
|
|
229
|
+
if (newFileConfig) {
|
|
230
|
+
Object.assign(config, newFileConfig);
|
|
231
|
+
configWatchers.forEach(fn => fn(config));
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function onConfigChange(callback) {
|
|
238
|
+
configWatchers.push(callback);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export default config;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { log, error as logError } from './logger.js';
|
|
2
|
+
|
|
3
|
+
export const THREAT_BLOCK = 'block';
|
|
4
|
+
export const THREAT_WARN = 'warn';
|
|
5
|
+
export const THREAT_ALLOW = 'allow';
|
|
6
|
+
|
|
7
|
+
export const SECURITY_RULES = [
|
|
8
|
+
{ re: /\beval\s*[(\s]/i, severity: THREAT_BLOCK, msg: '禁止使用 eval 函数' },
|
|
9
|
+
{ re: /\bstartapp\s*[(\s]/i, severity: THREAT_BLOCK, msg: '禁止使用 startapp 函数' },
|
|
10
|
+
{ re: /\bvl-bt\b/i, severity: THREAT_BLOCK, msg: '禁止使用 vl-bt 函数' },
|
|
11
|
+
{ re: /\bvl-exit-with-value\b/i, severity: THREAT_BLOCK, msg: '禁止使用 vl-exit-with-value' },
|
|
12
|
+
{ re: /\bvl-exit-with-error\b/i, severity: THREAT_BLOCK, msg: '禁止使用 vl-exit-with-error' },
|
|
13
|
+
{ re: /\bvlax-import-type-library\b/i, severity: THREAT_BLOCK, msg: '禁止导入类型库' },
|
|
14
|
+
{ re: /\bvlax-add-cmd\b/i, severity: THREAT_BLOCK, msg: '禁止注册新命令' },
|
|
15
|
+
{ re: /\bvla-Delete\b/i, severity: THREAT_BLOCK, msg: '禁止直接删除 vla 对象' },
|
|
16
|
+
{ re: /\bvla-Save\b/i, severity: THREAT_BLOCK, msg: '禁止直接保存文档' },
|
|
17
|
+
{ re: /\bdirectory\s*[(\s]/i, severity: THREAT_BLOCK, msg: '禁止使用 directory 函数' },
|
|
18
|
+
{ re: /\bvla-deletefile\b/i, severity: THREAT_BLOCK, msg: '禁止删除文件' },
|
|
19
|
+
{ re: /\bvla-copyfile\b/i, severity: THREAT_BLOCK, msg: '禁止复制文件' },
|
|
20
|
+
{ re: /\bvla-movefile\b/i, severity: THREAT_BLOCK, msg: '禁止移动文件' },
|
|
21
|
+
{ re: /\bvl-file-copy\s*[(\s]/i, severity: THREAT_BLOCK, msg: '禁止文件复制' },
|
|
22
|
+
{ re: /\bvl-file-delete\s*[(\s]/i, severity: THREAT_BLOCK, msg: '禁止文件删除' },
|
|
23
|
+
{ re: /\bvl-file-rename\s*[(\s]/i, severity: THREAT_BLOCK, msg: '禁止文件重命名' },
|
|
24
|
+
{ re: /\bdos_command_string\s*[(\s]/i, severity: THREAT_BLOCK, msg: '禁止使用 dos_command_string' },
|
|
25
|
+
{ re: /\bload\s*[(\s"]/i, severity: THREAT_WARN, msg: 'load 函数可能加载外部代码' },
|
|
26
|
+
{ re: /\bcommand\s*["\s]*[_(]?\s*["']?\.?\s*_?(?:quit|exit|close|open|new|recover|audit|purge|wblock|export)/i, severity: THREAT_BLOCK, msg: '禁止使用危险 CAD 命令' },
|
|
27
|
+
{ re: /\b(?:read-line|read-char|write-line|write-char)\s*[(\s]/i, severity: THREAT_WARN, msg: '文件 I/O 操作可能影响系统' },
|
|
28
|
+
{ re: /\bvla-add\b/i, severity: THREAT_ALLOW, msg: 'vla-add 创建新对象' },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
export function stripStringsAndComments(code) {
|
|
32
|
+
const withoutStrings = code.replace(/"([^"\\]*(?:\\.[^"\\]*)*)"/g, '');
|
|
33
|
+
return withoutStrings.replace(/;.*$/gm, '');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function validateLispCodeWorker(code) {
|
|
37
|
+
const clean = stripStringsAndComments(code);
|
|
38
|
+
for (const { re, msg } of SECURITY_RULES) {
|
|
39
|
+
if (re.test(clean)) {
|
|
40
|
+
return { valid: false, error: msg };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { valid: true };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function checkParens(code) {
|
|
47
|
+
let depth = 0;
|
|
48
|
+
let inString = false;
|
|
49
|
+
for (let i = 0; i < code.length; i++) {
|
|
50
|
+
const ch = code[i];
|
|
51
|
+
if (inString) {
|
|
52
|
+
if (ch === '\\' && i + 1 < code.length) {
|
|
53
|
+
i++;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (ch === '"') {
|
|
57
|
+
inString = false;
|
|
58
|
+
}
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (ch === '"') {
|
|
62
|
+
inString = true;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (ch === ';') {
|
|
66
|
+
while (i < code.length && code[i] !== '\n') i++;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (ch === '(') depth++;
|
|
70
|
+
if (ch === ')') depth--;
|
|
71
|
+
if (depth < 0) return { valid: false, error: '多余右括号 )', pos: i };
|
|
72
|
+
}
|
|
73
|
+
if (depth > 0) return { valid: false, error: `缺少 ${depth} 个右括号 )`, pos: code.length - 1 };
|
|
74
|
+
if (depth < 0) return { valid: false, error: `多余 ${-depth} 个左括号 (` };
|
|
75
|
+
return { valid: true };
|
|
76
|
+
}
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import config from './config.js';
|
|
5
|
+
|
|
6
|
+
const DEBUG_FILE = config.debugFile || path.join(os.tmpdir(), 'mcp-server-debug.log');
|
|
7
|
+
const MAX_LOG_SIZE = 10 * 1024 * 1024;
|
|
8
|
+
const MAX_LOG_FILES = 5;
|
|
9
|
+
|
|
10
|
+
let stream = null;
|
|
11
|
+
|
|
12
|
+
function rotateLog() {
|
|
13
|
+
try {
|
|
14
|
+
if (!fs.existsSync(DEBUG_FILE)) return;
|
|
15
|
+
const stat = fs.statSync(DEBUG_FILE);
|
|
16
|
+
if (stat.size < MAX_LOG_SIZE) return;
|
|
17
|
+
if (stream) { stream.end(); stream = null; }
|
|
18
|
+
for (let i = MAX_LOG_FILES - 1; i > 0; i--) {
|
|
19
|
+
const oldPath = `${DEBUG_FILE}.${i}`;
|
|
20
|
+
if (fs.existsSync(oldPath)) {
|
|
21
|
+
try { fs.renameSync(oldPath, `${DEBUG_FILE}.${i + 1}`); } catch (e) { console.error('log rotate rename error:', e.message); }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
try { fs.renameSync(DEBUG_FILE, `${DEBUG_FILE}.1`); } catch (e) { console.error('log rotate rename error:', e.message); }
|
|
25
|
+
} catch (e) { console.error('log rotate error:', e.message); }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getStream() {
|
|
29
|
+
if (!stream) {
|
|
30
|
+
rotateLog();
|
|
31
|
+
stream = fs.createWriteStream(DEBUG_FILE, { flags: 'a' });
|
|
32
|
+
}
|
|
33
|
+
return stream;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function formatMessage(level, message, meta = {}) {
|
|
37
|
+
const entry = {
|
|
38
|
+
timestamp: new Date().toISOString(),
|
|
39
|
+
level,
|
|
40
|
+
message,
|
|
41
|
+
...meta,
|
|
42
|
+
pid: process.pid,
|
|
43
|
+
hostname: os.hostname(),
|
|
44
|
+
};
|
|
45
|
+
return JSON.stringify(entry);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function log(message, meta = {}) {
|
|
49
|
+
getStream().write(formatMessage('INFO', message, meta) + '\n');
|
|
50
|
+
notify('info', message);
|
|
51
|
+
if (config.debug) {
|
|
52
|
+
console.log(`[INFO] ${message}`, meta);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function error(message, meta = {}) {
|
|
57
|
+
getStream().write(formatMessage('ERROR', message, meta) + '\n');
|
|
58
|
+
notify('error', message);
|
|
59
|
+
if (config.debug) {
|
|
60
|
+
console.error(`[ERROR] ${message}`, meta);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function warn(message, meta = {}) {
|
|
65
|
+
getStream().write(formatMessage('WARN', message, meta) + '\n');
|
|
66
|
+
notify('warning', message);
|
|
67
|
+
if (config.debug) {
|
|
68
|
+
console.warn(`[WARN] ${message}`, meta);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function debug(message, meta = {}) {
|
|
73
|
+
if (config.debug) {
|
|
74
|
+
getStream().write(formatMessage('DEBUG', message, meta) + '\n');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let _logNotify = null;
|
|
79
|
+
|
|
80
|
+
export function setLoggingNotify(fn) {
|
|
81
|
+
_logNotify = fn;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function getLoggingNotify() {
|
|
85
|
+
return _logNotify;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function notify(level, message) {
|
|
89
|
+
if (_logNotify) {
|
|
90
|
+
try { _logNotify(level, message); } catch {}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function closeLog() {
|
|
95
|
+
if (stream) {
|
|
96
|
+
stream.end();
|
|
97
|
+
stream = null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let requestStream = null;
|
|
102
|
+
const REQUEST_LOG_FILE = path.join(os.homedir(), '@lisp', 'logs', 'requests.log');
|
|
103
|
+
|
|
104
|
+
function ensureRequestLogDir() {
|
|
105
|
+
const dir = path.dirname(REQUEST_LOG_FILE);
|
|
106
|
+
if (!fs.existsSync(dir)) {
|
|
107
|
+
try { fs.mkdirSync(dir, { recursive: true }); } catch (e) { console.error('request log mkdir error:', e.message); }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function rotateRequestLog() {
|
|
112
|
+
try {
|
|
113
|
+
if (!fs.existsSync(REQUEST_LOG_FILE)) return;
|
|
114
|
+
const stat = fs.statSync(REQUEST_LOG_FILE);
|
|
115
|
+
if (stat.size < 10 * 1024 * 1024) return;
|
|
116
|
+
if (requestStream) { requestStream.end(); requestStream = null; }
|
|
117
|
+
for (let i = 5; i > 0; i--) {
|
|
118
|
+
const oldPath = `${REQUEST_LOG_FILE}.${i}`;
|
|
119
|
+
if (fs.existsSync(oldPath)) {
|
|
120
|
+
try { fs.renameSync(oldPath, `${REQUEST_LOG_FILE}.${i + 1}`); } catch (e) { console.error('request log rotate error:', e.message); }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
try { fs.renameSync(REQUEST_LOG_FILE, `${REQUEST_LOG_FILE}.1`); } catch (e) { console.error('request log rotate error:', e.message); }
|
|
124
|
+
} catch (e) { console.error('request log rotate error:', e.message); }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function logRequest(req) {
|
|
128
|
+
if (!config.requestLogEnabled) return;
|
|
129
|
+
try {
|
|
130
|
+
ensureRequestLogDir();
|
|
131
|
+
rotateRequestLog();
|
|
132
|
+
if (!requestStream) {
|
|
133
|
+
requestStream = fs.createWriteStream(REQUEST_LOG_FILE, { flags: 'a' });
|
|
134
|
+
}
|
|
135
|
+
const entry = {
|
|
136
|
+
timestamp: new Date().toISOString(),
|
|
137
|
+
method: req.method,
|
|
138
|
+
path: req.path,
|
|
139
|
+
query: req.query,
|
|
140
|
+
ip: req.ip || req.connection?.remoteAddress,
|
|
141
|
+
userAgent: req.get('user-agent'),
|
|
142
|
+
};
|
|
143
|
+
requestStream.write(JSON.stringify(entry) + '\n');
|
|
144
|
+
} catch (e) { console.error('request log error:', e.message); }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function logResponse(req, res, duration) {
|
|
148
|
+
if (!config.requestLogEnabled) return;
|
|
149
|
+
try {
|
|
150
|
+
ensureRequestLogDir();
|
|
151
|
+
if (!requestStream) {
|
|
152
|
+
requestStream = fs.createWriteStream(REQUEST_LOG_FILE, { flags: 'a' });
|
|
153
|
+
}
|
|
154
|
+
const entry = {
|
|
155
|
+
timestamp: new Date().toISOString(),
|
|
156
|
+
method: req.method,
|
|
157
|
+
path: req.path,
|
|
158
|
+
statusCode: res.statusCode,
|
|
159
|
+
duration: `${duration}ms`,
|
|
160
|
+
};
|
|
161
|
+
requestStream.write(JSON.stringify(entry) + '\n');
|
|
162
|
+
} catch (e) { console.error('request log error:', e.message); }
|
|
163
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atlisp/mcp",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.12",
|
|
4
4
|
"description": "MCP Server for @lisp on CAD,support AutoCAD/GstarCAD/ZWCAD/BricsCAD or CAD platform compatible with AutoLISP",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"start": "node src/atlisp-mcp.js",
|
|
19
19
|
"build": "npm run build:main && npm run build:worker",
|
|
20
20
|
"build:main": "esbuild src/atlisp-mcp.js --bundle --platform=node --target=node18 --format=esm --outdir=dist --external:edge-js --packages=external --banner:js=\"#!/usr/bin/env node\"",
|
|
21
|
-
"build:worker": "node -e \"require('fs')
|
|
21
|
+
"build:worker": "node -e \"const fs=require('fs');for(const f of ['cad-worker.js','lisp-security.js','logger.js','config.js'])fs.copyFileSync('src/'+f,'dist/'+f)\"",
|
|
22
22
|
"prepublishOnly": "npm run build",
|
|
23
23
|
"test": "vitest run",
|
|
24
24
|
"test:watch": "vitest"
|