@atlisp/mcp 1.8.11 → 1.8.13

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.
@@ -0,0 +1,347 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>@lisp MCP Tool Playground</title>
5
+ <style>
6
+ * { box-sizing: border-box; margin: 0; padding: 0; }
7
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1a1a2e; color: #eee; padding: 20px; }
8
+ h1 { color: #00d4ff; margin-bottom: 20px; font-size: 24px; }
9
+ .header { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; margin-bottom: 20px; }
10
+ .search-box { flex: 1; min-width: 200px; background: #16213e; border: 1px solid #333; color: #eee; padding: 10px 14px; border-radius: 6px; font-size: 14px; outline: none; }
11
+ .search-box:focus { border-color: #00d4ff; }
12
+ .category-filter { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 20px; }
13
+ .cat-btn { background: #16213e; border: 1px solid #333; color: #888; padding: 6px 14px; border-radius: 16px; cursor: pointer; font-size: 13px; transition: all .15s; }
14
+ .cat-btn:hover { border-color: #00d4ff; color: #eee; }
15
+ .cat-btn.active { background: #00d4ff; color: #1a1a2e; border-color: #00d4ff; }
16
+ .tool-count { color: #888; font-size: 13px; margin-bottom: 12px; }
17
+ .category-group { margin-bottom: 24px; }
18
+ .category-title { color: #00d4ff; font-size: 16px; font-weight: 600; padding: 10px 0 8px; border-bottom: 1px solid #333; margin-bottom: 8px; cursor: pointer; display: flex; align-items: center; gap: 8px; }
19
+ .category-title .arrow { color: #555; transition: transform .15s; font-size: 12px; }
20
+ .category-title.collapsed .arrow { transform: rotate(-90deg); }
21
+ .category-title .count { color: #888; font-size: 12px; font-weight: 400; }
22
+ .tool-card { background: #16213e; border: 1px solid #333; border-radius: 6px; padding: 12px 16px; margin-bottom: 6px; cursor: pointer; transition: border-color .15s; }
23
+ .tool-card:hover { border-color: #00d4ff44; }
24
+ .tool-card.expanded { border-color: #00d4ff; }
25
+ .tool-name { color: #49cc90; font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; font-size: 14px; }
26
+ .tool-desc { color: #999; font-size: 13px; margin-top: 4px; line-height: 1.4; }
27
+ .tool-meta { display: flex; gap: 12px; margin-top: 6px; font-size: 12px; color: #666; }
28
+ .tool-meta span { background: #0f0f23; padding: 2px 8px; border-radius: 3px; }
29
+ .tool-details { display: none; margin-top: 12px; padding-top: 12px; border-top: 1px solid #333; }
30
+ .tool-card.expanded .tool-details { display: block; }
31
+ .params-table { width: 100%; border-collapse: collapse; font-size: 13px; margin-bottom: 12px; }
32
+ .params-table th { color: #888; text-align: left; padding: 6px 8px; border-bottom: 1px solid #333; font-weight: 500; }
33
+ .params-table td { padding: 6px 8px; border-bottom: 1px solid #222; color: #ccc; }
34
+ .params-table .required { color: #f93e3e; margin-left: 4px; }
35
+ .param-input { width: 100%; background: #0f0f23; border: 1px solid #333; color: #eee; padding: 6px 10px; border-radius: 4px; font-size: 13px; font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; }
36
+ .param-input:focus { border-color: #00d4ff; outline: none; }
37
+ .param-input.array-input { min-height: 60px; resize: vertical; }
38
+ .btn { background: #00d4ff; color: #1a1a2e; border: none; padding: 8px 18px; border-radius: 5px; cursor: pointer; font-size: 13px; font-weight: 600; transition: opacity .15s; }
39
+ .btn:hover { opacity: .85; }
40
+ .btn:disabled { opacity: .4; cursor: not-allowed; }
41
+ .btn.execute { background: #49cc90; }
42
+ .btn.execute.running { background: #888; }
43
+ .response-area { display: none; margin-top: 12px; }
44
+ .response-area.visible { display: block; }
45
+ .response-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
46
+ .response-header label { color: #888; font-size: 12px; font-weight: 500; }
47
+ .response-status { font-size: 12px; padding: 2px 8px; border-radius: 3px; }
48
+ .response-status.ok { background: #49cc9033; color: #49cc90; }
49
+ .response-status.err { background: #f93e3e33; color: #f93e3e; }
50
+ .response-json { background: #0f0f23; border: 1px solid #333; border-radius: 4px; padding: 12px; font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; font-size: 12px; color: #00d4ff; white-space: pre-wrap; word-break: break-all; max-height: 400px; overflow: auto; }
51
+ .no-results { color: #666; text-align: center; padding: 40px; font-size: 14px; }
52
+ .toolbar { display: flex; gap: 8px; align-items: center; }
53
+ .expand-all-btn { background: none; border: 1px solid #333; color: #888; padding: 6px 14px; border-radius: 5px; cursor: pointer; font-size: 12px; }
54
+ .expand-all-btn:hover { border-color: #00d4ff44; color: #eee; }
55
+ .scroll-top { position: fixed; bottom: 20px; right: 20px; width: 40px; height: 40px; background: #16213e; border: 1px solid #333; border-radius: 50%; color: #00d4ff; font-size: 18px; cursor: pointer; display: none; align-items: center; justify-content: center; }
56
+ .scroll-top.visible { display: flex; }
57
+ .scroll-top:hover { border-color: #00d4ff; }
58
+ ::-webkit-scrollbar { width: 6px; }
59
+ ::-webkit-scrollbar-track { background: #0f0f23; }
60
+ ::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
61
+ ::-webkit-scrollbar-thumb:hover { background: #555; }
62
+ @media (max-width: 600px) {
63
+ body { padding: 10px; }
64
+ .header { flex-direction: column; }
65
+ .search-box { width: 100%; }
66
+ }
67
+ </style>
68
+ </head>
69
+ <body>
70
+ <h1>🧰 @lisp MCP Tool Playground</h1>
71
+ <div class="header">
72
+ <input class="search-box" id="search" placeholder="Search tools by name or description..." oninput="filterTools()">
73
+ <div class="toolbar">
74
+ <button class="expand-all-btn" onclick="expandAll()">Expand All</button>
75
+ <button class="expand-all-btn" onclick="collapseAll()">Collapse All</button>
76
+ </div>
77
+ </div>
78
+ <div class="category-filter" id="categoryFilter"></div>
79
+ <div class="tool-count" id="toolCount"></div>
80
+ <div id="toolList"></div>
81
+ <button class="scroll-top" id="scrollTop" onclick="window.scrollTo({top:0,behavior:'smooth'})">↑</button>
82
+
83
+ <script>
84
+ var TOOLS = __TOOLS_JSON__;
85
+ var activeCategory = null;
86
+ var searchQuery = '';
87
+
88
+ function categorize(name) {
89
+ if (/^batch_/.test(name)) return 'Batch';
90
+ if (/^boolean_/.test(name)) return 'Boolean';
91
+ if (/^(create_3d_|create_box|create_cone|create_cylinder|create_sphere|create_torus|create_wedge|extrude_|revolve_|slice_|thicken_|offset_surface|create_loft_|get_3d_)/.test(name)) return '3D';
92
+ if (/xdata/i.test(name)) return 'Xdata';
93
+ if (/sheet/i.test(name)) return 'Sheet Set';
94
+ if (/pdf/i.test(name)) return 'PDF';
95
+ if (/^(xref|dgn|dwf)/i.test(name)) return 'Xref';
96
+ if (/constraint/i.test(name)) return 'Constraint';
97
+ if (/parametric/i.test(name)) return 'Parametric';
98
+ if (/pipeline/i.test(name)) return 'Pipeline';
99
+ if (/trigger/i.test(name)) return 'Trigger';
100
+ if (/repl/i.test(name)) return 'REPL';
101
+ if (/^(plot_|publish_|save_pdf_layout)/.test(name)) return 'Plot';
102
+ if (/^(connect_cad|eval_lisp|install_|init_|get_cad_info|get_platform_info|at_command|new_document|bring_to_front|get_system_status)/.test(name)) return 'System';
103
+ if (/^(list_|search_|measure_|import_funlib)/.test(name)) return 'Query';
104
+ if (/^(import_|export_)/.test(name)) return 'Import/Export';
105
+ if (/^(create_layer|delete_layer|set_layer|set_current_layer)/.test(name)) return 'Layer';
106
+ if (/^(create_block|insert_block|explode_block|get_block|set_block)/.test(name)) return 'Block';
107
+ if (/^(create_dim|list_dim|set_dim|create_hatch|set_hatch)/.test(name)) return 'Dimension & Hatch';
108
+ if (/^(create_ucs|set_ucs|delete_ucs|list_ucs|get_current_ucs|create_layout|delete_layout|rename_layout|set_layout|list_layouts|get_current_layout|create_named_view|list_named_views|restore_named_view)/.test(name)) return 'UCS & Layout';
109
+ if (/^(open_dwg|save_dwg|close_dwg)/.test(name)) return 'Drawing';
110
+ if (/^(create_text|list_text|create_mleader|list_mleader|set_mleader|create_linetype|list_linetypes|load_linetype)/.test(name)) return 'Style';
111
+ if (/^(attach_|detach_)/.test(name)) return 'Underlay';
112
+ if (/^(create_group|delete_group|list_groups|get_group)/.test(name)) return 'Group';
113
+ if (/^(create_entity|delete_entity|get_entity|set_entity|select_entities)/.test(name)) return 'Entity';
114
+ return 'Other';
115
+ }
116
+
117
+ function getCategories() {
118
+ var cats = {};
119
+ TOOLS.forEach(function(t) {
120
+ var c = categorize(t.name);
121
+ if (!cats[c]) cats[c] = [];
122
+ cats[c].push(t);
123
+ });
124
+ var order = ['System','Query','Entity','Layer','Block','Drawing','Dimension & Hatch','UCS & Layout','3D','Batch','Boolean','Group','Style','Xdata','Sheet Set','PDF','Xref','Underlay','Import/Export','Constraint','Parametric','Plot','Pipeline','Trigger','REPL','Other'];
125
+ var sorted = {};
126
+ order.forEach(function(c) { if (cats[c]) sorted[c] = cats[c]; });
127
+ Object.keys(cats).filter(function(c) { return !order.includes(c); }).forEach(function(c) { sorted[c] = cats[c]; });
128
+ return sorted;
129
+ }
130
+
131
+ function escapeHtml(s) {
132
+ var d = document.createElement('div');
133
+ d.textContent = s;
134
+ return d.innerHTML;
135
+ }
136
+
137
+ function buildForm(inputSchema) {
138
+ var props = inputSchema.properties || {};
139
+ var required = inputSchema.required || [];
140
+ var keys = Object.keys(props);
141
+ if (keys.length === 0) return '<p style="color:#666;font-size:13px;padding:6px 0">No parameters required.</p>';
142
+ var html = '<table class="params-table"><tr><th>Parameter</th><th>Type</th><th>Description</th><th>Value</th></tr>';
143
+ keys.forEach(function(k) {
144
+ var p = props[k];
145
+ var isReq = required.includes(k);
146
+ var typeLabel = p.type || 'string';
147
+ var inputHtml = '';
148
+ if (p.enum) {
149
+ inputHtml = '<select class="param-input" data-param="' + k + '" data-type="string"><option value="">-- select --</option>';
150
+ p.enum.forEach(function(v) {
151
+ inputHtml += '<option value="' + escapeHtml(v) + '">' + escapeHtml(v) + '</option>';
152
+ });
153
+ inputHtml += '</select>';
154
+ } else if (p.type === 'boolean') {
155
+ inputHtml = '<select class="param-input" data-param="' + k + '" data-type="boolean"><option value="">-- select --</option><option value="true">true</option><option value="false">false</option></select>';
156
+ } else if (p.type === 'integer' || p.type === 'number') {
157
+ inputHtml = '<input class="param-input" data-param="' + k + '" data-type="' + p.type + '" placeholder="' + typeLabel + '" step="any">';
158
+ } else if (p.type === 'array' || (p.type === 'object' && !p.properties)) {
159
+ inputHtml = '<textarea class="param-input array-input" data-param="' + k + '" data-type="json" placeholder=\'JSON array/object, e.g. ["a","b"] or {"key":"val"}\' rows="2"></textarea>';
160
+ } else if (p.type === 'object' && p.properties) {
161
+ inputHtml = '<textarea class="param-input array-input" data-param="' + k + '" data-type="json" placeholder=\'JSON object\' rows="2"></textarea>';
162
+ } else {
163
+ inputHtml = '<input class="param-input" data-param="' + k + '" data-type="string" placeholder="' + escapeHtml(p.description || '') + '">';
164
+ }
165
+ html += '<tr><td><code>' + escapeHtml(k) + '</code>' + (isReq ? '<span class="required">*</span>' : '') + '</td><td style="color:#888">' + typeLabel + '</td><td>' + escapeHtml(p.description || '-') + '</td><td>' + inputHtml + '</td></tr>';
166
+ });
167
+ html += '</table>';
168
+ return html;
169
+ }
170
+
171
+ function collectParams(card) {
172
+ var params = {};
173
+ card.querySelectorAll('.param-input').forEach(function(el) {
174
+ var key = el.dataset.param;
175
+ var val = el.value.trim();
176
+ if (val === '') return;
177
+ var type = el.dataset.type;
178
+ if (type === 'number' || type === 'integer') val = Number(val);
179
+ else if (type === 'boolean') val = val === 'true';
180
+ else if (type === 'json') { try { val = JSON.parse(val); } catch(e) { return; } }
181
+ params[key] = val;
182
+ });
183
+ return params;
184
+ }
185
+
186
+ function renderTools() {
187
+ var categories = getCategories();
188
+ var container = document.getElementById('toolList');
189
+ var filterEl = document.getElementById('categoryFilter');
190
+ container.innerHTML = '';
191
+ filterEl.innerHTML = '<button class="cat-btn active" data-cat="" onclick="setCategory(this,\'\')">All</button>';
192
+
193
+ var totalCount = 0;
194
+ var catKeys = Object.keys(categories);
195
+ catKeys.forEach(function(c) {
196
+ var count = categories[c].length;
197
+ if (activeCategory && activeCategory !== c) count = 0;
198
+ else if (searchQuery) {
199
+ var q = searchQuery.toLowerCase();
200
+ count = categories[c].filter(function(t) { return t.name.toLowerCase().includes(q) || (t.description && t.description.toLowerCase().includes(q)); }).length;
201
+ }
202
+ totalCount += count;
203
+ filterEl.innerHTML += '<button class="cat-btn' + (activeCategory === c ? ' active' : '') + '" data-cat="' + c + '" onclick="setCategory(this,\'' + c + '\')">' + c + '</button>';
204
+ });
205
+
206
+ var countText = totalCount + ' tool' + (totalCount !== 1 ? 's' : '');
207
+ if (searchQuery) countText += ' matching "' + searchQuery + '"';
208
+ document.getElementById('toolCount').textContent = countText;
209
+
210
+ var hasResults = false;
211
+ catKeys.forEach(function(c) {
212
+ var tools = categories[c];
213
+ if (searchQuery) {
214
+ var q = searchQuery.toLowerCase();
215
+ tools = tools.filter(function(t) { return t.name.toLowerCase().includes(q) || (t.description && t.description.toLowerCase().includes(q)); });
216
+ }
217
+ if (activeCategory && activeCategory !== c) tools = [];
218
+ if (tools.length === 0) return;
219
+ hasResults = true;
220
+
221
+ var groupHtml = '<div class="category-group"><div class="category-title collapsed" onclick="toggleCategory(this)"><span class="arrow">▼</span> ' + c + ' <span class="count">(' + tools.length + ')</span></div>';
222
+ tools.forEach(function(t) {
223
+ var hasParams = t.inputSchema && t.inputSchema.properties && Object.keys(t.inputSchema.properties).length > 0;
224
+ var paramsCount = hasParams ? Object.keys(t.inputSchema.properties).length + ' params' : 'no params';
225
+ var reqCount = (t.inputSchema.required && t.inputSchema.required.length) ? '<span>' + t.inputSchema.required.length + ' required</span>' : '';
226
+ groupHtml += '<div class="tool-card" id="tool-' + t.name + '" onclick="toggleCard(this)">';
227
+ groupHtml += '<div class="tool-name">' + escapeHtml(t.name) + '</div>';
228
+ groupHtml += '<div class="tool-desc">' + escapeHtml(t.description || '') + '</div>';
229
+ groupHtml += '<div class="tool-meta"><span>' + paramsCount + '</span>' + reqCount + '</div>';
230
+ groupHtml += '<div class="tool-details">';
231
+ if (hasParams) {
232
+ groupHtml += buildForm(t.inputSchema);
233
+ } else {
234
+ groupHtml += '<p style="color:#666;font-size:13px;padding:6px 0">No parameters required.</p>';
235
+ }
236
+ groupHtml += '<button class="btn execute" onclick="event.stopPropagation();executeTool(this,\'' + t.name + '\')">▶ Execute</button>';
237
+ groupHtml += '<div class="response-area" id="response-' + t.name + '">';
238
+ groupHtml += '<div class="response-header"><label>Response</label><span class="response-status" id="status-' + t.name + '"></span></div>';
239
+ groupHtml += '<div class="response-json" id="result-' + t.name + '"></div>';
240
+ groupHtml += '</div></div></div>';
241
+ });
242
+ groupHtml += '</div>';
243
+ container.innerHTML += groupHtml;
244
+ });
245
+
246
+ if (!hasResults) {
247
+ container.innerHTML = '<div class="no-results">No tools found matching your search.</div>';
248
+ }
249
+
250
+ if (searchQuery) {
251
+ container.querySelectorAll('.category-group').forEach(function(g) {
252
+ var visibleCards = g.querySelectorAll('.tool-card');
253
+ if (visibleCards.length <= 3) {
254
+ var title = g.querySelector('.category-title');
255
+ if (title) title.classList.remove('collapsed');
256
+ }
257
+ });
258
+ }
259
+ }
260
+
261
+ function toggleCategory(el) {
262
+ el.classList.toggle('collapsed');
263
+ var group = el.parentElement;
264
+ var cards = group.querySelectorAll('.tool-card');
265
+ cards.forEach(function(c) { c.style.display = el.classList.contains('collapsed') ? 'none' : ''; });
266
+ }
267
+
268
+ function toggleCard(el) {
269
+ if (event && (event.target.closest('.param-input') || event.target.closest('.btn') || event.target.closest('.response-area'))) return;
270
+ el.classList.toggle('expanded');
271
+ }
272
+
273
+ async function executeTool(btn, toolName) {
274
+ if (btn.disabled) return;
275
+ var card = btn.closest('.tool-card');
276
+ var responseArea = document.getElementById('response-' + toolName);
277
+ var statusEl = document.getElementById('status-' + toolName);
278
+ var resultEl = document.getElementById('result-' + toolName);
279
+
280
+ btn.disabled = true;
281
+ btn.textContent = '⏳ Running...';
282
+ btn.classList.add('running');
283
+ responseArea.classList.remove('visible');
284
+ statusEl.textContent = '';
285
+ statusEl.className = 'response-status';
286
+ resultEl.textContent = '';
287
+
288
+ var arguments = collectParams(card);
289
+ var body = { jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: toolName, arguments: arguments } };
290
+
291
+ try {
292
+ var res = await fetch('/mcp', {
293
+ method: 'POST',
294
+ headers: { 'Content-Type': 'application/json' },
295
+ body: JSON.stringify(body)
296
+ });
297
+ var data = await res.json();
298
+ responseArea.classList.add('visible');
299
+ if (res.ok) {
300
+ statusEl.textContent = res.status + ' OK';
301
+ statusEl.className = 'response-status ok';
302
+ } else {
303
+ statusEl.textContent = res.status + ' Error';
304
+ statusEl.className = 'response-status err';
305
+ }
306
+ resultEl.textContent = JSON.stringify(data, null, 2);
307
+ } catch (err) {
308
+ responseArea.classList.add('visible');
309
+ statusEl.textContent = 'Network Error';
310
+ statusEl.className = 'response-status err';
311
+ resultEl.textContent = err.message;
312
+ } finally {
313
+ btn.disabled = false;
314
+ btn.textContent = '▶ Execute';
315
+ btn.classList.remove('running');
316
+ }
317
+ }
318
+
319
+ function setCategory(el, cat) {
320
+ document.querySelectorAll('.cat-btn').forEach(function(b) { b.classList.remove('active'); });
321
+ el.classList.add('active');
322
+ activeCategory = cat || null;
323
+ renderTools();
324
+ }
325
+
326
+ function filterTools() {
327
+ searchQuery = document.getElementById('search').value;
328
+ renderTools();
329
+ }
330
+
331
+ function expandAll() {
332
+ document.querySelectorAll('.category-title').forEach(function(t) { t.classList.remove('collapsed'); });
333
+ document.querySelectorAll('.tool-card').forEach(function(c) { c.style.display = ''; });
334
+ }
335
+
336
+ function collapseAll() {
337
+ document.querySelectorAll('.category-title').forEach(function(t) { t.classList.add('collapsed'); });
338
+ }
339
+
340
+ window.addEventListener('scroll', function() {
341
+ document.getElementById('scrollTop').classList.toggle('visible', window.scrollY > 400);
342
+ });
343
+
344
+ renderTools();
345
+ </script>
346
+ </body>
347
+ </html>
@@ -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.11",
3
+ "version": "1.8.13",
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').copyFileSync('src/cad-worker.js','dist/cad-worker.js')\"",
21
+ "build:worker": "node -e \"const fs=require('fs');const path=require('path');for(const f of ['cad-worker.js','lisp-security.js','logger.js','config.js'])fs.copyFileSync('src/'+f,'dist/'+f);if(!fs.existsSync('dist/html'))fs.mkdirSync('dist/html');for(const f of fs.readdirSync('src/html'))fs.copyFileSync('src/html/'+f,'dist/html/'+f)\"",
22
22
  "prepublishOnly": "npm run build",
23
23
  "test": "vitest run",
24
24
  "test:watch": "vitest"