@atlisp/mcp 1.8.21 → 1.8.25
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 +52 -4
- package/dist/atlisp-mcp.js +1978 -958
- package/dist/cad-rothelper.dll +0 -0
- package/dist/cad-worker.js +236 -174
- package/dist/config.js +59 -56
- package/dist/handlers/debug-handlers.js +126 -0
- package/dist/handlers/meta-handlers.js +70 -0
- package/dist/handlers/resource-cache.js +48 -16
- package/dist/handlers/resource-defs.js +3 -0
- package/dist/handlers/resource-handlers.js +123 -0
- package/dist/handlers/skill-handlers.js +152 -0
- package/dist/lisp-security.js +65 -0
- package/dist/prompts/definitions/workflow-batch-doc.json +27 -0
- package/package.json +3 -2
- package/dist/pipelines/analyze-and-report.json +0 -16
- package/dist/pipelines/batch-export-pdfs.json +0 -11
- package/dist/pipelines/batch-layer-move.json +0 -19
- package/dist/pipelines/export-current-drawing.json +0 -18
- package/dist/pipelines/purge-and-save.json +0 -16
|
@@ -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
|
+
}
|
package/dist/lisp-security.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "1.8.25",
|
|
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",
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "analyze-and-report",
|
|
3
|
-
"description": "分析当前图纸实体并生成统计报告。先获取所有实体,再逐个获取详细信息",
|
|
4
|
-
"steps": [
|
|
5
|
-
{
|
|
6
|
-
"name": "entities",
|
|
7
|
-
"tool": "stream_entities",
|
|
8
|
-
"args": { "chunk": 500, "offset": 0 }
|
|
9
|
-
},
|
|
10
|
-
{
|
|
11
|
-
"name": "info",
|
|
12
|
-
"tool": "get_cad_info",
|
|
13
|
-
"args": {}
|
|
14
|
-
}
|
|
15
|
-
]
|
|
16
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "batch-layer-move",
|
|
3
|
-
"description": "按条件选择实体并移动到目标图层。先选择指定类型/图层的实体,再移动到新图层",
|
|
4
|
-
"steps": [
|
|
5
|
-
{
|
|
6
|
-
"name": "selection",
|
|
7
|
-
"tool": "select_entities",
|
|
8
|
-
"args": { "filter": { "type": "LINE", "layer": "0" } }
|
|
9
|
-
},
|
|
10
|
-
{
|
|
11
|
-
"name": "move",
|
|
12
|
-
"tool": "batch_set_layer",
|
|
13
|
-
"args": {
|
|
14
|
-
"handles": "$ref:selection.result",
|
|
15
|
-
"layerName": "TargetLayer"
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
]
|
|
19
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "export-current-drawing",
|
|
3
|
-
"description": "导出当前图纸为 PDF。先获取图纸信息,再执行导出",
|
|
4
|
-
"steps": [
|
|
5
|
-
{
|
|
6
|
-
"name": "info",
|
|
7
|
-
"tool": "get_cad_info",
|
|
8
|
-
"args": {}
|
|
9
|
-
},
|
|
10
|
-
{
|
|
11
|
-
"name": "export",
|
|
12
|
-
"tool": "export_pdf",
|
|
13
|
-
"args": {
|
|
14
|
-
"outputPath": "$ref:info.docName.pdf"
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
]
|
|
18
|
-
}
|