@atlisp/mcp 1.0.9 → 1.0.11

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/src/cad.js CHANGED
@@ -3,13 +3,7 @@ import path from 'path';
3
3
  import os from 'os';
4
4
  import fs from 'fs';
5
5
  import { fileURLToPath } from 'url';
6
-
7
- const CAD_PLATFORMS = ['AutoCAD', 'ZWCAD', 'GStarCAD', 'BricsCAD'];
8
- const FILE_EXTENSIONS = {
9
- 'AutoCAD': ['.fas', '.vlx'],
10
- 'ZWCAD': ['.zelx', '.vls'],
11
- 'BricsCAD': ['.des']
12
- };
6
+ import { CAD_PLATFORMS, FILE_EXTENSIONS } from './constants.js';
13
7
 
14
8
  const MESSAGE_TIMEOUT = parseInt(process.env.MESSAGE_TIMEOUT || '60000', 10);
15
9
 
@@ -20,8 +14,23 @@ let worker = null;
20
14
  let _platform = null;
21
15
  let _version = null;
22
16
 
23
- function getWorker() {
17
+ const HEARTBEAT_INTERVAL = parseInt(process.env.HEARTBEAT_INTERVAL || '30000', 10);
18
+ let heartbeatTimer = null;
19
+
20
+ export function resetWorker() {
21
+ if (worker) {
22
+ worker.kill();
23
+ worker = null;
24
+ }
25
+ if (heartbeatTimer) {
26
+ clearInterval(heartbeatTimer);
27
+ heartbeatTimer = null;
28
+ }
29
+ }
30
+
31
+ export function getWorker() {
24
32
  if (!worker || !worker.connected) {
33
+ if (worker) worker.kill();
25
34
  worker = spawn('node', [workerPath], {
26
35
  stdio: ['pipe', 'pipe', 'pipe']
27
36
  });
@@ -31,12 +40,33 @@ function getWorker() {
31
40
  worker.on('exit', (code) => {
32
41
  console.error('worker exited:', code);
33
42
  worker.connected = false;
43
+ if (heartbeatTimer) {
44
+ clearInterval(heartbeatTimer);
45
+ heartbeatTimer = null;
46
+ }
34
47
  });
48
+ startHeartbeat();
35
49
  }
36
50
  return worker;
37
51
  }
38
52
 
39
- function sendMessage(msg) {
53
+ function startHeartbeat() {
54
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
55
+ heartbeatTimer = setInterval(async () => {
56
+ try {
57
+ const result = await sendMessage({ type: 'ping' });
58
+ if (!result?.pong) {
59
+ console.error('heartbeat failed, worker may be dead');
60
+ resetWorker();
61
+ }
62
+ } catch (e) {
63
+ console.error('heartbeat error:', e.message);
64
+ resetWorker();
65
+ }
66
+ }, HEARTBEAT_INTERVAL);
67
+ }
68
+
69
+ export function sendMessage(msg) {
40
70
  return new Promise((resolve, reject) => {
41
71
  const w = getWorker();
42
72
  let responded = false;
@@ -110,9 +140,10 @@ class CadConnection {
110
140
  if (!this.connected) throw new Error('未连接 CAD');
111
141
  try {
112
142
  const result = await sendMessage({ type: 'send', code: code, platform: _platform });
113
- return result && result.success;
143
+ if (result.success) return true;
144
+ throw new Error(result.error || '发送失败');
114
145
  } catch (e) {
115
- return false;
146
+ throw e;
116
147
  }
117
148
  }
118
149
 
@@ -120,12 +151,12 @@ class CadConnection {
120
151
  if (!this.connected) throw new Error('未连接 CAD');
121
152
  try {
122
153
  const result = await sendMessage({ type: 'sendResult', code: code, platform: _platform });
123
- if (result && result.success) {
154
+ if (result.success) {
124
155
  return result.result || '';
125
156
  }
126
- return null;
157
+ throw new Error(result.error || '发送失败');
127
158
  } catch (e) {
128
- return null;
159
+ throw e;
129
160
  }
130
161
  }
131
162
 
@@ -172,11 +203,25 @@ class CadConnection {
172
203
  this.version = null;
173
204
  this.product = null;
174
205
  _platform = null;
175
- if (worker) {
176
- worker.kill();
177
- worker = null;
206
+ resetWorker();
207
+ }
208
+
209
+ async ping() {
210
+ try {
211
+ const result = await sendMessage({ type: 'ping' });
212
+ return result && result.pong;
213
+ } catch (e) {
214
+ return false;
178
215
  }
179
216
  }
217
+
218
+ _reset() {
219
+ this.connected = false;
220
+ this.version = null;
221
+ this.product = null;
222
+ _platform = null;
223
+ resetWorker();
224
+ }
180
225
  }
181
226
 
182
227
  export const cad = new CadConnection();
package/src/cli.js CHANGED
@@ -4,7 +4,7 @@ import path from 'path';
4
4
  import { fileURLToPath } from 'url';
5
5
 
6
6
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
- const serverPath = path.join(__dirname, 'index.js');
7
+ const serverPath = path.join(__dirname, 'atlisp-mcp.js');
8
8
 
9
9
  const args = process.argv.slice(2);
10
10
  const env = { ...process.env };
@@ -0,0 +1,22 @@
1
+ export const CAD_PLATFORMS = ['AutoCAD', 'ZWCAD', 'GStarCAD', 'BricsCAD'];
2
+ export const FILE_EXTENSIONS = {
3
+ 'AutoCAD': ['.fas', '.vlx'],
4
+ 'ZWCAD': ['.zelx', '.vls'],
5
+ 'BricsCAD': ['.des']
6
+ };
7
+
8
+ export const PROTOCOL_VERSION = '2024-11-05';
9
+ export const SERVER_NAME = 'atlisp-mcp-server';
10
+ export const SERVER_VERSION = '1.0.11';
11
+
12
+ export const MOCK_PACKAGES = [
13
+ { name: 'base', description: '基础函数库', version: '1.0.0' },
14
+ { name: 'at-pm', description: '工程项目管理', version: '2.1.0' },
15
+ { name: 'network', description: '网络功能模块', version: '1.5.0' },
16
+ { name: 'userman', description: '用户管理', version: '1.2.0' },
17
+ { name: 'pkgman', description: '包管理器', version: '2.0.0' },
18
+ { name: 'sidebar', description: '侧边栏工具', version: '1.0.0' },
19
+ { name: 'aibot', description: 'AI 助手', version: '0.9.0' },
20
+ { name: 'tips', description: '每日提示', version: '1.0.0' },
21
+ { name: 'function', description: '函数库', version: '3.0.0' }
22
+ ];
@@ -0,0 +1,69 @@
1
+ import { cad } from '../cad.js';
2
+ import { log } from '../logger.js';
3
+
4
+ export async function connectCad() {
5
+ log('connect_cad called, cad.connected before: ' + cad.connected);
6
+ try {
7
+ log('calling cad.connect()...');
8
+ const connected = await cad.connect();
9
+ log('cad.connect() returned: ' + connected + ', cad.connected: ' + cad.connected);
10
+ if (connected) {
11
+ return { content: [{ type: 'text', text: `已连接到 ${cad.getPlatform()} ${cad.getVersion()}` }] };
12
+ }
13
+ } catch (e) {
14
+ log('connect_cad error: ' + e.message);
15
+ }
16
+ return { content: [{ type: 'text', text: '未找到运行中的 CAD,请确保 CAD 已启动并启用 COM 接口' }], isError: true };
17
+ }
18
+
19
+ export async function evalLisp(code, withResult = false) {
20
+ if (!cad.connected) { await cad.connect(); }
21
+ if (!cad.connected) {
22
+ return { content: [{ type: 'text', text: '未连接 CAD' }], isError: true };
23
+ }
24
+ try {
25
+ if (withResult) {
26
+ const result = await cad.sendCommandWithResult(code);
27
+ if (result !== null) {
28
+ return { content: [{ type: 'text', text: result }] };
29
+ }
30
+ return { content: [{ type: 'text', text: '执行失败或无返回值' }], isError: true };
31
+ } else {
32
+ await cad.sendCommand(code + '\n');
33
+ return { content: [{ type: 'text', text: `已发送命令: ${code}` }] };
34
+ }
35
+ } catch (e) {
36
+ return { content: [{ type: 'text', text: `错误: ${e.message}` }], isError: true };
37
+ }
38
+ }
39
+
40
+ export async function getCadInfo() {
41
+ if (!cad.connected) { await cad.connect(); }
42
+ if (!cad.connected) return { content: [{ type: 'text', text: 'CAD 未连接' }] };
43
+ const info = await cad.getInfo();
44
+ return { content: [{ type: 'text', text: info }] };
45
+ }
46
+
47
+ export async function atCommand(command) {
48
+ if (!cad.connected) return { content: [{ type: 'text', text: '请先连接 CAD' }], isError: true };
49
+ await cad.sendCommand(`${command}\n`);
50
+ return { content: [{ type: 'text', text: `已发送命令: ${command}` }] };
51
+ }
52
+
53
+ export async function newDocument() {
54
+ if (!cad.connected) { await cad.connect(); }
55
+ if (!cad.connected) return { content: [{ type: 'text', text: '未连接 CAD' }], isError: true };
56
+ const result = await cad.newDoc();
57
+ if (result) {
58
+ return { content: [{ type: 'text', text: '已在 CAD 中新建空白文档' }] };
59
+ }
60
+ return { content: [{ type: 'text', text: '新建文档失败' }], isError: true };
61
+ }
62
+
63
+ export async function installAtlisp() {
64
+ if (!cad.connected) { await cad.connect(); }
65
+ if (!cad.connected) return { content: [{ type: 'text', text: '未连接 CAD' }], isError: true };
66
+ const installCode = '(progn(vl-load-com)(setq s strcat h "http" o(vlax-create-object (s"win"h".win"h"request.5.1"))v vlax-invoke e eval r read)(v o\'open "get" (s h"://atlisp.""cn/@"):vlax-true)(v o\'send)(v o\'WaitforResponse 1000)(e(r(vlax-get-property o\'ResponseText))))';
67
+ await cad.sendCommand(installCode + '\n');
68
+ return { content: [{ type: 'text', text: '已发送 @lisp 安装代码到 CAD' }] };
69
+ }
@@ -0,0 +1,109 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ const FUNCTIONS_DIR = process.env.FUNCTIONS_DIR || 'E:/Users/vitalgg/atlisp/lib/json';
5
+ const FUNCTIONS_URL = process.env.FUNCTIONS_URL || 'http://s3.atlisp.cn/json/';
6
+
7
+ export async function getFunctionUsage(funcName, packageName) {
8
+ try {
9
+ let results = [];
10
+
11
+ const url = packageName
12
+ ? `${FUNCTIONS_URL}${packageName}.json`
13
+ : `${FUNCTIONS_URL}all.json`;
14
+
15
+ try {
16
+ const response = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0' } });
17
+ if (response.ok) {
18
+ const data = await response.json();
19
+ if (data.functions) {
20
+ const func = data.functions.find(f => f.name === funcName);
21
+ if (func) results.push(func);
22
+ }
23
+ }
24
+ } catch (e) {}
25
+
26
+ if (results.length === 0 && fs.existsSync(FUNCTIONS_DIR)) {
27
+ if (packageName) {
28
+ const filePath = path.join(FUNCTIONS_DIR, `${packageName}.json`);
29
+ if (fs.existsSync(filePath)) {
30
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
31
+ const func = data.functions?.find(f => f.name === funcName);
32
+ if (func) results.push(func);
33
+ }
34
+ } else {
35
+ const files = fs.readdirSync(FUNCTIONS_DIR).filter(f => f.endsWith('.json'));
36
+ for (const file of files) {
37
+ const data = JSON.parse(fs.readFileSync(path.join(FUNCTIONS_DIR, file), 'utf-8'));
38
+ const func = data.functions?.find(f => f.name === funcName);
39
+ if (func) results.push(func);
40
+ }
41
+ }
42
+ }
43
+
44
+ if (results.length === 0) {
45
+ return { content: [{ type: 'text', text: `未找到函数: ${funcName}` }], isError: true };
46
+ }
47
+
48
+ const f = results[0];
49
+ const text = `函数: ${f.name}
50
+ 描述: ${f.description || '-'}
51
+ 参数: ${f.params?.join(', ') || '-'}
52
+ 返回值: ${f.return || '-'}
53
+ 示例: ${f.usage || '-'}`;
54
+
55
+ return { content: [{ type: 'text', text }] };
56
+ } catch (e) {
57
+ return { content: [{ type: 'text', text: `错误: ${e.message}` }], isError: true };
58
+ }
59
+ }
60
+
61
+ export async function listFunctions(packageName) {
62
+ try {
63
+ let allFuncs = [];
64
+
65
+ const url = packageName
66
+ ? `${FUNCTIONS_URL}${packageName}.json`
67
+ : `${FUNCTIONS_URL}all.json`;
68
+
69
+ try {
70
+ const response = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0' } });
71
+ if (response.ok) {
72
+ const data = await response.json();
73
+ if (data.functions) {
74
+ allFuncs = packageName
75
+ ? data.functions
76
+ : data.functions.map(f => `${f.name}: ${f.description || '-'}`);
77
+ }
78
+ }
79
+ } catch (e) {}
80
+
81
+ if (allFuncs.length === 0 && fs.existsSync(FUNCTIONS_DIR)) {
82
+ const files = fs.readdirSync(FUNCTIONS_DIR).filter(f => f.endsWith('.json'));
83
+ for (const file of files) {
84
+ const data = JSON.parse(fs.readFileSync(path.join(FUNCTIONS_DIR, file), 'utf-8'));
85
+ if (packageName) {
86
+ if (data.package === packageName) {
87
+ allFuncs = data.functions || [];
88
+ }
89
+ } else {
90
+ for (const f of (data.functions || [])) {
91
+ allFuncs.push(`${f.name}: ${f.description || '-'}`);
92
+ }
93
+ }
94
+ }
95
+ }
96
+
97
+ if (allFuncs.length === 0) {
98
+ return { content: [{ type: 'text', text: '函数库为空' }] };
99
+ }
100
+
101
+ if (packageName) {
102
+ return { content: [{ type: 'text', text: `${packageName} 包函数 (${allFuncs.length}):\n${allFuncs.map(f => f.name).join('\n')}` }] };
103
+ }
104
+
105
+ return { content: [{ type: 'text', text: `函数库共 ${allFuncs.length} 个函数` }] };
106
+ } catch (e) {
107
+ return { content: [{ type: 'text', text: `错误: ${e.message}` }], isError: true };
108
+ }
109
+ }
@@ -0,0 +1,26 @@
1
+ import { cad } from '../cad.js';
2
+ import { MOCK_PACKAGES } from '../constants.js';
3
+
4
+ export async function listPackages() {
5
+ const packages = MOCK_PACKAGES.map(p => `${p.name} v${p.version} - ${p.description}`);
6
+ return { content: [{ type: 'text', text: `已安装的 @lisp 包:\n${packages.join('\n')}` }] };
7
+ }
8
+
9
+ export async function searchPackages(query) {
10
+ const results = MOCK_PACKAGES.filter(p => p.name.includes(query) || p.description.includes(query));
11
+ if (results.length === 0) {
12
+ return { content: [{ type: 'text', text: `未找到包含 "${query}" 的包` }] };
13
+ }
14
+ return { content: [{ type: 'text', text: `搜索结果:\n${results.map(p => `${p.name} v${p.version} - ${p.description}`).join('\n')}` }] };
15
+ }
16
+
17
+ export async function installPackage(packageName) {
18
+ const pkg = MOCK_PACKAGES.find(p => p.name === packageName);
19
+ if (!pkg) {
20
+ return { content: [{ type: 'text', text: `错误: 包 "${packageName}" 不存在` }], isError: true };
21
+ }
22
+ if (cad.connected) {
23
+ await cad.sendCommand(`(@::load-module '${packageName})\n`);
24
+ }
25
+ return { content: [{ type: 'text', text: `包 "${packageName}" 安装命令已发送!` }] };
26
+ }
package/src/logger.js ADDED
@@ -0,0 +1,30 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ const DEBUG_FILE = process.env.DEBUG_FILE || path.join(os.tmpdir(), 'mcp-server-debug.log');
6
+
7
+ let stream = null;
8
+
9
+ function getStream() {
10
+ if (!stream) {
11
+ stream = fs.createWriteStream(DEBUG_FILE, { flags: 'a' });
12
+ }
13
+ return stream;
14
+ }
15
+
16
+ export function log(msg) {
17
+ const entry = `${new Date().toISOString()} ${msg}\n`;
18
+ getStream().write(entry);
19
+ }
20
+
21
+ export function logError(msg) {
22
+ log(`ERROR: ${msg}`);
23
+ }
24
+
25
+ export function closeLog() {
26
+ if (stream) {
27
+ stream.end();
28
+ stream = null;
29
+ }
30
+ }
@@ -0,0 +1,32 @@
1
+ export function setupSseHeaders(res) {
2
+ res.setHeader('Content-Type', 'text/event-stream');
3
+ res.setHeader('Cache-Control', 'no-cache');
4
+ res.setHeader('Connection', 'keep-alive');
5
+ res.setHeader('Transfer-Encoding', 'chunked');
6
+ }
7
+
8
+ export function sendSSE(res, data, event = 'message') {
9
+ const json = JSON.stringify(data);
10
+ res.write(`event: ${event}\n`);
11
+ res.write(`data: ${json}\n\n`);
12
+ if (typeof res.flush === 'function') res.flush();
13
+ }
14
+
15
+ export function createSseResponseHandler(res, acceptSse = false) {
16
+ if (acceptSse) {
17
+ setupSseHeaders(res);
18
+ }
19
+
20
+ return {
21
+ send: (data, done = false) => {
22
+ const json = JSON.stringify(data);
23
+ if (acceptSse) {
24
+ sendSSE(res, data);
25
+ if (done) res.end();
26
+ } else {
27
+ res.setHeader('Content-Type', 'application/json');
28
+ res.end(json);
29
+ }
30
+ }
31
+ };
32
+ }