@atlisp/mcp 1.8.23 → 1.8.26

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.
@@ -6,6 +6,7 @@ import { log } from '../logger.js';
6
6
  import { parseLispList, parseLispRecursive, lispPairsToObject, escapeLispString } from '../handler-utils.js';
7
7
  import { createError, ERROR_CODES } from '../errors.js';
8
8
  import config from '../config.js';
9
+ import { tools } from '../tools/index.js';
9
10
  import fs from 'fs';
10
11
  import path from 'path';
11
12
 
@@ -1090,6 +1091,122 @@ async function getDwgContext() {
1090
1091
  }
1091
1092
  }
1092
1093
 
1094
+ async function getDrawingAnalysis(params = {}) {
1095
+ const snapshot = await getCadSnapshot();
1096
+ if (!snapshot || !snapshot.cadConnected) {
1097
+ return { connected: false, error: 'CAD 未连接' };
1098
+ }
1099
+
1100
+ try {
1101
+ const detailedCode = `(progn
1102
+ (vl-load-com)
1103
+ (setq r nil)
1104
+ ;; entity counts by type
1105
+ (if (setq ss (ssget "_X"))
1106
+ (progn
1107
+ (setq total (sslength ss) i 0 cnt nil)
1108
+ (while (< i total)
1109
+ (setq typ (cdr (assoc 0 (entget (ssname ss i)))))
1110
+ (setq cnt (if (assoc typ cnt) (subst (cons typ (1+ (cdr (assoc typ cnt)))) (assoc typ cnt) cnt) (cons (cons typ 1) cnt)))
1111
+ (setq i (1+ i)))
1112
+ (setq r (cons (cons "entityCount" total) r))
1113
+ (setq r (cons (cons "entityByType" cnt) r)))
1114
+ (setq r (cons (cons "entityCount" 0) r)))
1115
+ ;; block usage
1116
+ (setq blocks nil)
1117
+ (vlax-for b (vla-get-blocks (vla-get-activedocument (vlax-get-acad-object)))
1118
+ (if (and (= (vla-get-IsXref b) :vlax-false) (/= (vla-get-Name b) "*Model_Space") (/= (vla-get-Name b) "*Paper_Space"))
1119
+ (setq blocks (cons (list "name" (vla-get-Name b) "count" (vla-get-Count b) "isDynamic" (vla-get-IsDynamicBlock b)) blocks))))
1120
+ (setq r (cons (cons "blockUsage" blocks) r))
1121
+ ;; layer summary
1122
+ (setq layers nil le (tblnext "layer" t))
1123
+ (while le
1124
+ (setq ln (cdr (assoc 2 le)) lc (cdr (assoc 62 le)) ll (cdr (assoc 6 le)))
1125
+ (setq layers (cons (list "name" ln "color" (if (< lc 0) (- lc) lc) "off" (< lc 0) "linetype" ll) layers))
1126
+ (setq le (tblnext "layer" nil)))
1127
+ (setq r (cons (cons "layers" layers) r))
1128
+ ;; text stats
1129
+ (if (setq ts (ssget "_X" '((0 . "TEXT,MTEXT"))))
1130
+ (progn
1131
+ (setq tc (sslength ts) th nil ti 0)
1132
+ (while (< ti tc)
1133
+ (setq h (cdr (assoc 40 (entget (ssname ts ti)))))
1134
+ (if h (setq th (cons h th)))
1135
+ (setq ti (1+ ti)))
1136
+ (setq th (vl-sort th '<))
1137
+ (setq r (cons (cons "textCount" tc) r))
1138
+ (setq r (cons (cons "textHeights" (list "min" (car th) "max" (last th) "median" (nth (fix (/ (length th) 2)) th))) r)))
1139
+ (setq r (cons (cons "textCount" 0) r)))
1140
+ ;; system variables
1141
+ (setq r (cons (cons "sysvars" (list
1142
+ (cons "ltscale" (getvar "ltscale"))
1143
+ (cons "dimscale" (getvar "dimscale"))
1144
+ (cons "insunits" (getvar "insunits"))
1145
+ (cons "measurement" (getvar "measurement"))
1146
+ (cons "celtype" (getvar "celtype"))
1147
+ (cons "clayer" (getvar "clayer"))
1148
+ (cons "cecolor" (getvar "cecolor"))
1149
+ (cons "osmode" (getvar "osmode"))
1150
+ (cons "snapmode" (getvar "snapmode"))
1151
+ (cons "gridmode" (getvar "gridmode"))
1152
+ (cons "orthomode" (getvar "orthomode"))
1153
+ (cons "plinetype" (getvar "plinetype"))
1154
+ (cons "fillmode" (getvar "fillmode"))
1155
+ (cons "pickstyle" (getvar "pickstyle"))
1156
+ )) r))
1157
+ (vl-prin1-to-string (reverse r)))`;
1158
+
1159
+ const resultStr = await cad.sendCommandWithResult(detailedCode);
1160
+ const parsed = parseLispRecursive(resultStr);
1161
+ if (!parsed || !Array.isArray(parsed)) {
1162
+ return { connected: true, error: '分析结果解析失败' };
1163
+ }
1164
+ const analysis = {};
1165
+ for (const pair of parsed) {
1166
+ if (Array.isArray(pair) && pair.length >= 2) {
1167
+ analysis[String(pair[0])] = pair[1];
1168
+ }
1169
+ }
1170
+ return { connected: true, ...analysis };
1171
+ } catch (e) {
1172
+ log(`getDrawingAnalysis error: ${e.message}`);
1173
+ return { connected: true, error: e.message };
1174
+ }
1175
+ }
1176
+
1177
+ function getToolCategories() {
1178
+ const cats = {};
1179
+ for (const t of tools) {
1180
+ const cat = t.annotations?.category || '其他';
1181
+ if (!cats[cat]) cats[cat] = { count: 0, tools: [] };
1182
+ cats[cat].count++;
1183
+ cats[cat].tools.push(t.name);
1184
+ }
1185
+ return Object.entries(cats).map(([category, info]) => ({
1186
+ category, count: info.count, tools: info.tools,
1187
+ })).sort((a, b) => b.count - a.count);
1188
+ }
1189
+
1190
+ async function checkDrawingStandards() {
1191
+ const standards = getDraftingStandards();
1192
+ if (!standards || standards.error) {
1193
+ return { ok: false, error: standards?.error || '制图规范未加载' };
1194
+ }
1195
+ try {
1196
+ const issues = [];
1197
+ for (const rule of standards.layers || []) {
1198
+ const code = `(tblsearch "LAYER" "${escapeLispString(rule.name || rule.pattern)}")`;
1199
+ const exists = await cad.sendCommandWithResult(code);
1200
+ if (rule.required && (!exists || exists === 'nil' || exists === '')) {
1201
+ issues.push({ type: 'missing_layer', message: `缺少必需图层: ${rule.name}` });
1202
+ }
1203
+ }
1204
+ return { ok: issues.length === 0, issues };
1205
+ } catch (e) {
1206
+ return { ok: false, error: e.message };
1207
+ }
1208
+ }
1209
+
1093
1210
  function getToolRelationships() {
1094
1211
  return {
1095
1212
  selection: {
@@ -1385,6 +1502,12 @@ export async function readResource(uri) {
1385
1502
  return await getDwgContext();
1386
1503
  case 'atlisp://tools/relationships':
1387
1504
  return getToolRelationships();
1505
+ case 'atlisp://dwg/analysis':
1506
+ return await getDrawingAnalysis(params);
1507
+ case 'atlisp://tools/categories':
1508
+ return getToolCategories();
1509
+ case 'atlisp://dwg/standards/check':
1510
+ return await checkDrawingStandards();
1388
1511
  default:
1389
1512
  throw new Error(`资源不存在: ${uri}`);
1390
1513
  }
@@ -0,0 +1,152 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ const SKILLS_DIR = path.join(os.homedir(), '.atlisp', 'skills');
6
+ const REGISTRY_URL = process.env.SKILL_REGISTRY_URL || 'https://raw.githubusercontent.com/atlisp/skill-registry/main/index.json';
7
+ const CACHE_PATH = path.join(os.homedir(), '.atlisp', 'skill-registry-cache.json');
8
+
9
+ function readCache() {
10
+ try { if (fs.existsSync(CACHE_PATH)) return JSON.parse(fs.readFileSync(CACHE_PATH, 'utf-8')); } catch {}
11
+ return null;
12
+ }
13
+
14
+ function writeCache(data) {
15
+ try { fs.writeFileSync(CACHE_PATH, JSON.stringify(data, null, 2), 'utf-8'); } catch {}
16
+ }
17
+
18
+ async function fetchIndex() {
19
+ let index = readCache();
20
+ if (index && (Date.now() - index.fetchedAt < 3600000)) return index;
21
+ try {
22
+ const resp = await fetch(REGISTRY_URL, { headers: { 'User-Agent': 'atlisp-mcp' } });
23
+ if (resp.ok) {
24
+ index = await resp.json();
25
+ index.fetchedAt = Date.now();
26
+ writeCache(index);
27
+ }
28
+ } catch {}
29
+ return index;
30
+ }
31
+
32
+ export async function skill_search(a) {
33
+ const index = await fetchIndex();
34
+ if (!index?.skills) return { content: [{ type: 'text', text: '无法获取 skill 注册表索引' }], isError: true };
35
+ const q = (a.query || '').toLowerCase();
36
+ const results = index.skills.filter(s =>
37
+ s.name.toLowerCase().includes(q) ||
38
+ (s.description || '').toLowerCase().includes(q) ||
39
+ (s.tags || []).some(t => t.toLowerCase().includes(q))
40
+ );
41
+ if (!results.length) return { content: [{ type: 'text', text: `未找到匹配 "${a.query}" 的 skill` }] };
42
+ const lines = [`找到 ${results.length} 个 skill:\n`];
43
+ for (const s of results) {
44
+ lines.push(`**${s.name}**`);
45
+ if (s.description) lines.push(` 描述: ${s.description}`);
46
+ if (s.tags?.length) lines.push(` 标签: ${s.tags.join(', ')}`);
47
+ if (s.version) lines.push(` 版本: ${s.version}`);
48
+ lines.push('');
49
+ }
50
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
51
+ }
52
+
53
+ export async function skill_install(a) {
54
+ if (!a.name) return { content: [{ type: 'text', text: '需要 skill 名称' }], isError: true };
55
+ const index = await fetchIndex();
56
+ if (!index?.skills) return { content: [{ type: 'text', text: '无法获取 skill 注册表索引' }], isError: true };
57
+ const skill = index.skills.find(s => s.name === a.name);
58
+ if (!skill) return { content: [{ type: 'text', text: `未找到 skill: ${a.name}` }], isError: true };
59
+ const downloadUrl = skill.download_url || skill.url;
60
+ if (!downloadUrl) return { content: [{ type: 'text', text: `Skill "${a.name}" 没有下载地址` }], isError: true };
61
+ let content;
62
+ try {
63
+ const resp = await fetch(downloadUrl, { headers: { 'User-Agent': 'atlisp-mcp' } });
64
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
65
+ content = await resp.text();
66
+ } catch (e) {
67
+ return { content: [{ type: 'text', text: `下载失败: ${e.message}` }], isError: true };
68
+ }
69
+ try { fs.mkdirSync(SKILLS_DIR, { recursive: true }); } catch {}
70
+ const safeName = a.name.replace(/[^a-zA-Z0-9\u4e00-\u9fff_-]/g, '-').substring(0, 40);
71
+ const skillFile = path.join(SKILLS_DIR, `skill-${safeName}.md`);
72
+ if (fs.existsSync(skillFile)) {
73
+ return { content: [{ type: 'text', text: `Skill "${a.name}" 已安装 (${skillFile})` }], isError: true };
74
+ }
75
+ let finalContent = content;
76
+ if (!content.match(/^---\n/)) {
77
+ const tags = (skill.tags || []).join(', ');
78
+ finalContent = `---\nname: ${a.name}\ndescription: ${skill.description || ''}\ntags: ${tags}\n---\n\n${content}`;
79
+ }
80
+ fs.writeFileSync(skillFile, finalContent, 'utf-8');
81
+ return { content: [{ type: 'text', text: `${skill.name} 安装成功\n文件: ${skillFile}${skill.version ? `\n版本: ${skill.version}` : ''}` }] };
82
+ }
83
+
84
+ export async function skill_list_installed() {
85
+ if (!fs.existsSync(SKILLS_DIR)) return { content: [{ type: 'text', text: '暂无已安装的 skill' }] };
86
+ const results = [];
87
+ try {
88
+ const entries = fs.readdirSync(SKILLS_DIR, { withFileTypes: true });
89
+ for (const entry of entries) {
90
+ if (entry.isFile() && entry.name.endsWith('.md')) {
91
+ const fullPath = path.join(SKILLS_DIR, entry.name);
92
+ try {
93
+ const raw = fs.readFileSync(fullPath, 'utf-8');
94
+ const match = raw.match(/^---\n([\s\S]*?)\n---/);
95
+ if (match) {
96
+ const fm = {};
97
+ for (const line of match[1].split('\n')) {
98
+ const sep = line.indexOf(':');
99
+ if (sep > 0) fm[line.slice(0, sep).trim()] = line.slice(sep + 1).trim();
100
+ }
101
+ results.push({ name: fm.name || entry.name.replace('.md', ''), description: fm.description || '', tags: fm.tags || '' });
102
+ } else {
103
+ results.push({ name: entry.name.replace('.md', ''), description: '', tags: '' });
104
+ }
105
+ } catch {}
106
+ }
107
+ }
108
+ } catch {}
109
+ if (!results.length) return { content: [{ type: 'text', text: '暂无已安装的 skill' }] };
110
+ const lines = [`已安装 ${results.length} 个 skill:\n`];
111
+ for (const s of results) {
112
+ lines.push(`**${s.name}**${s.description ? ` — ${s.description}` : ''}`);
113
+ if (s.tags) lines.push(` 标签: ${s.tags}`);
114
+ }
115
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
116
+ }
117
+
118
+ export async function skill_uninstall(a) {
119
+ if (!a.name) return { content: [{ type: 'text', text: '需要 skill 名称' }], isError: true };
120
+ if (!fs.existsSync(SKILLS_DIR)) return { content: [{ type: 'text', text: `未安装 skill: ${a.name}` }], isError: true };
121
+ let found = null;
122
+ try {
123
+ const entries = fs.readdirSync(SKILLS_DIR, { withFileTypes: true });
124
+ for (const entry of entries) {
125
+ if (entry.isFile() && entry.name.endsWith('.md')) {
126
+ const fullPath = path.join(SKILLS_DIR, entry.name);
127
+ try {
128
+ const raw = fs.readFileSync(fullPath, 'utf-8');
129
+ const match = raw.match(/^---\n([\s\S]*?)\n---/);
130
+ if (match) {
131
+ for (const line of match[1].split('\n')) {
132
+ if (line.startsWith('name:') && line.slice(5).trim() === a.name) { found = fullPath; break; }
133
+ }
134
+ } else if (entry.name.replace('.md', '') === a.name) { found = fullPath; }
135
+ } catch {}
136
+ if (found) break;
137
+ }
138
+ }
139
+ } catch {}
140
+ if (!found) return { content: [{ type: 'text', text: `未安装 skill: ${a.name}` }], isError: true };
141
+ fs.unlinkSync(found);
142
+ return { content: [{ type: 'text', text: `${a.name} 已卸载` }] };
143
+ }
144
+
145
+ export async function skill_list_categories() {
146
+ const index = await fetchIndex();
147
+ if (!index?.skills) return { content: [{ type: 'text', text: '无法获取 skill 注册表索引' }], isError: true };
148
+ const cats = new Set();
149
+ for (const s of index.skills) for (const t of (s.tags || [])) cats.add(t);
150
+ const sorted = Array.from(cats).sort();
151
+ return { content: [{ type: 'text', text: sorted.length ? `注册表分类 (${sorted.length}):\n${sorted.join('\n')}` : '暂无分类' }] };
152
+ }
@@ -116,3 +116,68 @@ export function checkParens(code) {
116
116
  if (depth < 0) return { valid: false, error: `多余 ${-depth} 个左括号 (` };
117
117
  return { valid: true };
118
118
  }
119
+
120
+ // === Session Sandbox (merged from lisp-sandbox.js) ===
121
+
122
+ export const RULES = SECURITY_RULES;
123
+
124
+ const sessionAllowlist = new Map();
125
+
126
+ export function getSessionAllowlist(sessionId) {
127
+ if (!sessionAllowlist.has(sessionId)) {
128
+ sessionAllowlist.set(sessionId, new Set());
129
+ }
130
+ return sessionAllowlist.get(sessionId);
131
+ }
132
+
133
+ export function addToAllowlist(sessionId, ruleIndex) {
134
+ const list = getSessionAllowlist(sessionId);
135
+ list.add(ruleIndex);
136
+ }
137
+
138
+ export function removeFromAllowlist(sessionId, ruleIndex) {
139
+ const list = getSessionAllowlist(sessionId);
140
+ list.delete(ruleIndex);
141
+ }
142
+
143
+ export function clearSessionAllowlist(sessionId) {
144
+ sessionAllowlist.delete(sessionId);
145
+ }
146
+
147
+ export function validateLispCode(code, sessionId = null) {
148
+ const withoutComments = stripStringsAndComments(code);
149
+ const effectiveSeverity = getEffectiveSeverity();
150
+
151
+ const results = [];
152
+ let blocked = false;
153
+
154
+ for (let i = 0; i < RULES.length; i++) {
155
+ const rule = RULES[i];
156
+ if (rule.re.test(withoutComments)) {
157
+ const isAllowed = sessionId && getSessionAllowlist(sessionId).has(i);
158
+
159
+ const severity = effectiveSeverity === THREAT_ALLOW ? THREAT_ALLOW
160
+ : isAllowed ? THREAT_ALLOW
161
+ : effectiveSeverity;
162
+
163
+ results.push({
164
+ ruleIndex: i,
165
+ severity,
166
+ message: rule.msg,
167
+ blocked: severity === THREAT_BLOCK,
168
+ warned: severity === THREAT_WARN,
169
+ allowed: severity === THREAT_ALLOW,
170
+ });
171
+ if (severity === THREAT_BLOCK) {
172
+ blocked = true;
173
+ }
174
+ }
175
+ }
176
+
177
+ return {
178
+ valid: !blocked,
179
+ blocked,
180
+ results,
181
+ summary: blocked ? `安全限制: ${results.filter(r => r.blocked).map(r => r.message).join('; ')}` : '通过',
182
+ };
183
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "workflow-batch-doc",
3
+ "description": "批量文档处理工作流:打开多个 DWG → 执行操作 → 保存/导出 → 关闭。适用于批量清理、替换文字、统一修改",
4
+ "arguments": [
5
+ { "name": "action", "description": "步骤: open/process/save/report", "required": true, "default": "process" },
6
+ { "name": "operation", "description": "操作类型: cleanup/find-replace/check-standards/export-pdf", "required": false, "default": "cleanup" },
7
+ { "name": "files", "description": "DWG 文件路径列表,用逗号分隔", "required": false }
8
+ ],
9
+ "templates": {
10
+ "open": {
11
+ "code": "",
12
+ "description": "1. 确定目标文件列表\n2. 对每个文件,循环调用 open_dwg({ filePath }) → 执行操作 → save_dwg/export_pdf → close_dwg\n\n示例流程:\n```\n文件列表 = [\"project/a.dwg\", \"project/b.dwg\"]\nfor each file in 文件列表:\n open_dwg({ filePath })\n ... 执行操作 ...\n save_dwg()\n close_dwg()\n```"
13
+ },
14
+ "process": {
15
+ "code": "",
16
+ "description": "选择 operation:\n- cleanup: 打开文件 → 执行 eval_lisp({ code: '(command \"_.PURGE\" \"_B\" \"*\" \"_N\")(command \"_.AUDIT\" \"_Y\")' }) → save_dwg → close_dwg\n- find-replace: 打开文件 → replace_text({ pattern, replacement }) → save_dwg → close_dwg\n- check-standards: 打开文件 → eval_lisp_with_result 检查图层/样式 → 记录结果 → close_dwg (不保存)\n- export-pdf: 打开文件 → export_pdf({ outputPath, layout: \"Model\" }) → close_dwg (不保存)\n\n对每个文件重复此流程,收集结果。"
17
+ },
18
+ "save": {
19
+ "code": "",
20
+ "description": "使用 save_dwg() 保存当前文件。如需另存为不同路径,传入 filePath 参数。\n完成后用 close_dwg({ save: false }) 关闭。"
21
+ },
22
+ "report": {
23
+ "code": "",
24
+ "description": "汇总所有文件的操作结果:\n- 已处理文件数\n- 每个文件的执行结果(成功/失败)\n- 失败原因\n\n使用 get_system_status 检查 CAD 最终状态。"
25
+ }
26
+ }
27
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlisp/mcp",
3
- "version": "1.8.23",
3
+ "version": "1.8.26",
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": {
@@ -16,9 +16,10 @@
16
16
  ],
17
17
  "scripts": {
18
18
  "start": "node src/atlisp-mcp.js",
19
- "build": "npm run build:main && npm run build:worker",
19
+ "build": "npm run build:main && npm run build:worker && npm run build:rot",
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
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);if(!fs.existsSync('dist/handlers'))fs.mkdirSync('dist/handlers');for(const f of fs.readdirSync('src/handlers'))fs.copyFileSync('src/handlers/'+f,'dist/handlers/'+f);if(!fs.existsSync('dist/prompts/definitions'))fs.mkdirSync('dist/prompts/definitions',{recursive:true});for(const f of fs.readdirSync('src/prompts/definitions'))fs.copyFileSync('src/prompts/definitions/'+f,'dist/prompts/definitions/'+f)\"",
22
+ "build:rot": "powershell -Command \"& 'C:\\Windows\\Microsoft.NET\\Framework\\v4.0.30319\\csc.exe' /target:library /out:dist\\cad-rothelper.dll src\\cad-rothelper.cs\"",
22
23
  "prepublishOnly": "npm run build",
23
24
  "test": "vitest run",
24
25
  "test:watch": "vitest",