@atlisp/mcp 1.0.19 → 1.0.20
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 +116 -1
- package/package.json +2 -1
- package/src/atlisp-mcp.js +284 -200
- package/src/cad-worker.js +11 -4
- package/src/cad.js +31 -24
- package/src/config.js +28 -0
- package/src/constants.js +2 -1
- package/src/errors.js +55 -0
- package/src/handlers/function-handlers.js +25 -13
- package/src/handlers/prompt-handlers.js +268 -0
- package/src/handlers/resource-handlers.js +82 -35
- package/src/sse-session-manager.js +1 -3
- package/src/sse-session.js +125 -15
- package/src/subscription-manager.js +11 -0
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import { cad } from '../cad.js';
|
|
2
2
|
import { CAD_PLATFORMS, FILE_EXTENSIONS, MOCK_PACKAGES } from '../constants.js';
|
|
3
|
+
import { log } from '../logger.js';
|
|
4
|
+
import config from '../config.js';
|
|
5
|
+
|
|
6
|
+
const CACHE_TTL = config.resourceCacheTtl;
|
|
7
|
+
const resourceCache = new Map();
|
|
8
|
+
|
|
9
|
+
function getCached(key) {
|
|
10
|
+
const entry = resourceCache.get(key);
|
|
11
|
+
if (entry && Date.now() - entry.timestamp < CACHE_TTL) {
|
|
12
|
+
return entry.data;
|
|
13
|
+
}
|
|
14
|
+
resourceCache.delete(key);
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function setCache(key, data) {
|
|
19
|
+
resourceCache.set(key, { data, timestamp: Date.now() });
|
|
20
|
+
}
|
|
3
21
|
|
|
4
22
|
function parseLispList(str) {
|
|
5
23
|
if (!str || typeof str !== 'string') return null;
|
|
@@ -122,33 +140,41 @@ export async function readResource(uri) {
|
|
|
122
140
|
}
|
|
123
141
|
|
|
124
142
|
async function getCadInfo() {
|
|
143
|
+
const cached = getCached('cad:info');
|
|
144
|
+
if (cached) return cached;
|
|
145
|
+
|
|
125
146
|
if (!cad.connected) {
|
|
126
147
|
await cad.connect();
|
|
127
148
|
}
|
|
128
|
-
|
|
149
|
+
|
|
129
150
|
if (!cad.connected) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
version: null,
|
|
134
|
-
busy: false
|
|
135
|
-
};
|
|
151
|
+
const result = { connected: false, platform: null, version: null, busy: false };
|
|
152
|
+
setCache('cad:info', result);
|
|
153
|
+
return result;
|
|
136
154
|
}
|
|
137
|
-
|
|
155
|
+
|
|
138
156
|
let busy = false;
|
|
139
157
|
try {
|
|
140
158
|
busy = await cad.isBusy();
|
|
141
|
-
} catch (e) {
|
|
142
|
-
|
|
143
|
-
|
|
159
|
+
} catch (e) {
|
|
160
|
+
log(`Error checking CAD busy status: ${e.message}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const result = {
|
|
144
164
|
connected: true,
|
|
145
165
|
platform: cad.getPlatform(),
|
|
146
166
|
version: cad.getVersion(),
|
|
147
167
|
busy
|
|
148
168
|
};
|
|
169
|
+
setCache('cad:info', result);
|
|
170
|
+
return result;
|
|
149
171
|
}
|
|
150
172
|
|
|
151
173
|
async function getLayers(filterName = null) {
|
|
174
|
+
const cacheKey = filterName ? `layers:${filterName}` : 'layers:all';
|
|
175
|
+
const cached = getCached(cacheKey);
|
|
176
|
+
if (cached) return cached;
|
|
177
|
+
|
|
152
178
|
if (!cad.connected) await cad.connect();
|
|
153
179
|
if (!cad.connected) return [];
|
|
154
180
|
|
|
@@ -159,8 +185,11 @@ async function getLayers(filterName = null) {
|
|
|
159
185
|
|
|
160
186
|
const result = await cad.sendCommandWithResult(code);
|
|
161
187
|
|
|
162
|
-
if (!result || result === "" || result === "nil")
|
|
163
|
-
|
|
188
|
+
if (!result || result === "" || result === "nil") {
|
|
189
|
+
setCache(cacheKey, []);
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
|
|
164
193
|
let layers = [];
|
|
165
194
|
try {
|
|
166
195
|
layers = JSON.parse(result);
|
|
@@ -172,7 +201,7 @@ async function getLayers(filterName = null) {
|
|
|
172
201
|
layers = [];
|
|
173
202
|
}
|
|
174
203
|
}
|
|
175
|
-
|
|
204
|
+
|
|
176
205
|
if (!Array.isArray(layers)) {
|
|
177
206
|
if (typeof layers === 'number') {
|
|
178
207
|
layers = [[result, layers, 0]];
|
|
@@ -180,8 +209,8 @@ async function getLayers(filterName = null) {
|
|
|
180
209
|
layers = [[layers, 7, 0]];
|
|
181
210
|
}
|
|
182
211
|
}
|
|
183
|
-
|
|
184
|
-
|
|
212
|
+
|
|
213
|
+
const mapped = layers.map(l => {
|
|
185
214
|
const name = Array.isArray(l) ? l[0] : l;
|
|
186
215
|
const color = Array.isArray(l) && l[1] !== undefined ? l[1] : 7;
|
|
187
216
|
const flags = Array.isArray(l) && l[2] !== undefined ? l[2] : 0;
|
|
@@ -192,58 +221,76 @@ async function getLayers(filterName = null) {
|
|
|
192
221
|
frozen: (flags & 1) !== 0
|
|
193
222
|
};
|
|
194
223
|
});
|
|
224
|
+
setCache(cacheKey, mapped);
|
|
225
|
+
return mapped;
|
|
195
226
|
} catch (e) {
|
|
227
|
+
setCache(cacheKey, []);
|
|
196
228
|
return [];
|
|
197
229
|
}
|
|
198
230
|
}
|
|
199
231
|
|
|
200
232
|
async function getEntities(filterType = null) {
|
|
233
|
+
const cacheKey = filterType ? `entities:${filterType}` : 'entities:all';
|
|
234
|
+
const cached = getCached(cacheKey);
|
|
235
|
+
if (cached) return cached;
|
|
236
|
+
|
|
201
237
|
if (!cad.connected) await cad.connect();
|
|
202
|
-
if (!cad.connected)
|
|
203
|
-
|
|
238
|
+
if (!cad.connected) {
|
|
239
|
+
const result = { total: 0, byType: {} };
|
|
240
|
+
setCache(cacheKey, result);
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
|
|
204
244
|
try {
|
|
205
245
|
let code;
|
|
206
246
|
if (filterType) {
|
|
207
247
|
code = `(sslength (ssget "_X" (list (cons 0 "${filterType}"))))`;
|
|
208
248
|
const count = await cad.sendCommandWithResult(code);
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
};
|
|
249
|
+
const result = { type: filterType, count: parseInt(count) || 0 };
|
|
250
|
+
setCache(cacheKey, result);
|
|
251
|
+
return result;
|
|
213
252
|
} else {
|
|
214
253
|
code = `(progn(setq ss(ssget "_X"))(if ss(progn(setq elst(mapcar (quote(lambda(x)(cdr(assoc 0 (entget x))))) (pickset:to-list ss)))(stat:stat elst))))`;
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
if (!
|
|
218
|
-
|
|
254
|
+
const resultStr = await cad.sendCommandWithResult(code);
|
|
255
|
+
|
|
256
|
+
if (!resultStr || resultStr === "" || resultStr === "nil") {
|
|
257
|
+
const result = { total: 0, byType: {} };
|
|
258
|
+
setCache(cacheKey, result);
|
|
259
|
+
return result;
|
|
219
260
|
}
|
|
220
|
-
|
|
261
|
+
|
|
221
262
|
let statResult = [];
|
|
222
263
|
try {
|
|
223
|
-
statResult = JSON.parse(
|
|
264
|
+
statResult = JSON.parse(resultStr);
|
|
224
265
|
} catch (e) {
|
|
225
|
-
const parsed = parseLispList(
|
|
266
|
+
const parsed = parseLispList(resultStr);
|
|
226
267
|
if (parsed) {
|
|
227
268
|
statResult = parsed;
|
|
228
269
|
}
|
|
229
270
|
}
|
|
230
|
-
|
|
271
|
+
|
|
231
272
|
if (!Array.isArray(statResult)) {
|
|
232
|
-
|
|
273
|
+
const result = { total: 0, byType: {} };
|
|
274
|
+
setCache(cacheKey, result);
|
|
275
|
+
return result;
|
|
233
276
|
}
|
|
234
|
-
|
|
277
|
+
|
|
235
278
|
const byType = {};
|
|
236
279
|
statResult.forEach(item => {
|
|
237
280
|
if (Array.isArray(item) && item.length >= 2) {
|
|
238
281
|
byType[item[0]] = item[1];
|
|
239
282
|
}
|
|
240
283
|
});
|
|
241
|
-
|
|
284
|
+
|
|
242
285
|
const total = Object.values(byType).reduce((sum, n) => sum + n, 0);
|
|
243
|
-
|
|
286
|
+
const result = { total, byType };
|
|
287
|
+
setCache(cacheKey, result);
|
|
288
|
+
return result;
|
|
244
289
|
}
|
|
245
290
|
} catch (e) {
|
|
246
|
-
|
|
291
|
+
const result = { total: 0, byType: {} };
|
|
292
|
+
setCache(cacheKey, result);
|
|
293
|
+
return result;
|
|
247
294
|
}
|
|
248
295
|
}
|
|
249
296
|
|
package/src/sse-session.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from 'crypto';
|
|
2
2
|
|
|
3
3
|
const DEFAULT_HEARTBEAT_INTERVAL = 30000;
|
|
4
|
-
const
|
|
4
|
+
const DEFAULT_RETRY_INTERVAL = 30000;
|
|
5
|
+
const MAX_SENT_MESSAGES = 1000;
|
|
5
6
|
|
|
6
7
|
export const SSE_EVENTS = {
|
|
7
8
|
MESSAGE: 'message',
|
|
@@ -19,15 +20,26 @@ export class SseSession {
|
|
|
19
20
|
#heartbeatTimer = null;
|
|
20
21
|
#lastEventId = 0;
|
|
21
22
|
#heartbeatInterval;
|
|
23
|
+
#retryInterval;
|
|
22
24
|
#sessionData = {};
|
|
25
|
+
#sentMessages = [];
|
|
26
|
+
#backpressureBuffer = [];
|
|
27
|
+
#drainHandler = null;
|
|
23
28
|
|
|
24
29
|
constructor(res, options = {}) {
|
|
25
30
|
this.#res = res;
|
|
26
31
|
this.#clientId = options.clientId || randomUUID();
|
|
27
32
|
this.#heartbeatInterval = options.heartbeatInterval || DEFAULT_HEARTBEAT_INTERVAL;
|
|
33
|
+
this.#retryInterval = options.retryInterval || DEFAULT_RETRY_INTERVAL;
|
|
28
34
|
this.#sessionData = options.sessionData || {};
|
|
29
35
|
|
|
36
|
+
if (options.resumeFromId && options.resumeFromId > 0) {
|
|
37
|
+
this.#lastEventId = options.resumeFromId;
|
|
38
|
+
}
|
|
39
|
+
|
|
30
40
|
this._setupSseHeaders();
|
|
41
|
+
this._sendRetry();
|
|
42
|
+
this._setupDrain();
|
|
31
43
|
}
|
|
32
44
|
|
|
33
45
|
get clientId() {
|
|
@@ -38,10 +50,22 @@ export class SseSession {
|
|
|
38
50
|
return this.#active;
|
|
39
51
|
}
|
|
40
52
|
|
|
53
|
+
get lastEventId() {
|
|
54
|
+
return this.#lastEventId;
|
|
55
|
+
}
|
|
56
|
+
|
|
41
57
|
get sessionData() {
|
|
42
58
|
return this.#sessionData;
|
|
43
59
|
}
|
|
44
60
|
|
|
61
|
+
get sentMessages() {
|
|
62
|
+
return [...this.#sentMessages];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
getMessagesSince(eventId) {
|
|
66
|
+
return this.#sentMessages.filter(m => m.id > eventId);
|
|
67
|
+
}
|
|
68
|
+
|
|
45
69
|
setSessionData(key, value) {
|
|
46
70
|
this.#sessionData[key] = value;
|
|
47
71
|
}
|
|
@@ -50,49 +74,122 @@ export class SseSession {
|
|
|
50
74
|
return this.#sessionData[key];
|
|
51
75
|
}
|
|
52
76
|
|
|
77
|
+
get retryInterval() {
|
|
78
|
+
return this.#retryInterval;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
setRetryInterval(ms) {
|
|
82
|
+
this.#retryInterval = ms;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
_sendRetry() {
|
|
86
|
+
this._write(`retry: ${this.#retryInterval}\n\n`);
|
|
87
|
+
}
|
|
88
|
+
|
|
53
89
|
_setupSseHeaders() {
|
|
54
90
|
this.#res.setHeader('Content-Type', 'text/event-stream');
|
|
55
91
|
this.#res.setHeader('Cache-Control', 'no-cache');
|
|
56
92
|
this.#res.setHeader('Connection', 'keep-alive');
|
|
57
93
|
this.#res.setHeader('Transfer-Encoding', 'chunked');
|
|
58
94
|
this.#res.setHeader('X-Accel-Buffering', 'no');
|
|
95
|
+
if (typeof this.#res.flush !== 'function') {
|
|
96
|
+
this.#res.flush = () => {};
|
|
97
|
+
}
|
|
59
98
|
}
|
|
60
99
|
|
|
61
100
|
_formatEvent(event, data, id = null) {
|
|
62
|
-
|
|
101
|
+
const lines = [];
|
|
63
102
|
if (id !== null) {
|
|
64
103
|
lines.push(`id: ${id}`);
|
|
65
104
|
}
|
|
66
105
|
lines.push(`event: ${event}`);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
106
|
+
|
|
107
|
+
if (data !== null && data !== undefined) {
|
|
108
|
+
const jsonStr = JSON.stringify(data);
|
|
109
|
+
lines.push(`data: ${jsonStr}`);
|
|
110
|
+
} else {
|
|
111
|
+
lines.push('data:');
|
|
112
|
+
}
|
|
113
|
+
|
|
71
114
|
return lines.join('\n') + '\n\n';
|
|
72
115
|
}
|
|
73
116
|
|
|
117
|
+
_setupDrain() {
|
|
118
|
+
if (typeof this.#res.on !== 'function') return;
|
|
119
|
+
this.#drainHandler = () => {
|
|
120
|
+
if (!this.#active) return;
|
|
121
|
+
if (this.#backpressureBuffer.length > 0) {
|
|
122
|
+
this._drainBuffer();
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
this.#res.on('drain', this.#drainHandler);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
_drainBuffer() {
|
|
129
|
+
const batch = this.#backpressureBuffer.splice(0, 50);
|
|
130
|
+
for (const item of batch) {
|
|
131
|
+
const ok = this.#res.write(item);
|
|
132
|
+
if (!ok) {
|
|
133
|
+
this.#backpressureBuffer.unshift(...batch.slice(batch.indexOf(item)));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
this._flush();
|
|
138
|
+
if (this.#backpressureBuffer.length > 0) {
|
|
139
|
+
this._drainBuffer();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
74
143
|
_write(content) {
|
|
75
144
|
if (!this.#active) return false;
|
|
76
145
|
try {
|
|
146
|
+
if (this.#backpressureBuffer.length > 0) {
|
|
147
|
+
this.#backpressureBuffer.push(content);
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
77
150
|
const result = this.#res.write(content);
|
|
78
151
|
if (typeof this.#res.flush === 'function') {
|
|
79
152
|
this.#res.flush();
|
|
80
153
|
}
|
|
81
154
|
if (result === false) {
|
|
82
|
-
this.#
|
|
83
|
-
return
|
|
155
|
+
this.#backpressureBuffer.push(content);
|
|
156
|
+
return true;
|
|
84
157
|
}
|
|
85
158
|
return true;
|
|
86
159
|
} catch (e) {
|
|
87
160
|
this.#active = false;
|
|
161
|
+
this.close();
|
|
88
162
|
return false;
|
|
89
163
|
}
|
|
90
164
|
}
|
|
91
165
|
|
|
166
|
+
_flush() {
|
|
167
|
+
try {
|
|
168
|
+
if (typeof this.#res.flush === 'function') {
|
|
169
|
+
this.#res.flush();
|
|
170
|
+
}
|
|
171
|
+
} catch (e) {
|
|
172
|
+
// Ignore flush errors
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
flushBackpressure() {
|
|
177
|
+
if (this.#backpressureBuffer.length > 0) {
|
|
178
|
+
this._drainBuffer();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
92
182
|
sendEvent(event, data, id = null) {
|
|
93
183
|
this.#lastEventId++;
|
|
94
184
|
const eventId = id !== null ? id : this.#lastEventId;
|
|
95
|
-
|
|
185
|
+
const formatted = this._formatEvent(event, data, eventId);
|
|
186
|
+
this.#sentMessages.push({ event, data, id: eventId, timestamp: Date.now() });
|
|
187
|
+
if (this.#sentMessages.length > MAX_SENT_MESSAGES) {
|
|
188
|
+
this.#sentMessages.splice(0, this.#sentMessages.length - MAX_SENT_MESSAGES);
|
|
189
|
+
}
|
|
190
|
+
this._write(formatted);
|
|
191
|
+
this._flush();
|
|
192
|
+
return true;
|
|
96
193
|
}
|
|
97
194
|
|
|
98
195
|
sendMessage(data) {
|
|
@@ -100,7 +197,7 @@ export class SseSession {
|
|
|
100
197
|
}
|
|
101
198
|
|
|
102
199
|
sendEndpoint(path) {
|
|
103
|
-
return this.
|
|
200
|
+
return this._write(`: endpoint ${path}\n\n`);
|
|
104
201
|
}
|
|
105
202
|
|
|
106
203
|
sendError(code, message, id = null) {
|
|
@@ -108,7 +205,7 @@ export class SseSession {
|
|
|
108
205
|
}
|
|
109
206
|
|
|
110
207
|
sendPing() {
|
|
111
|
-
return this.
|
|
208
|
+
return this._write(': ping ' + Date.now() + '\n\n');
|
|
112
209
|
}
|
|
113
210
|
|
|
114
211
|
sendProgress(current, total, message = '') {
|
|
@@ -116,7 +213,7 @@ export class SseSession {
|
|
|
116
213
|
}
|
|
117
214
|
|
|
118
215
|
sendCapabilities(capabilities) {
|
|
119
|
-
return this.
|
|
216
|
+
return this._write(`: capabilities ${JSON.stringify(capabilities)}\n\n`);
|
|
120
217
|
}
|
|
121
218
|
|
|
122
219
|
sendResponse(jsonrpcResponse, id = null) {
|
|
@@ -150,6 +247,11 @@ export class SseSession {
|
|
|
150
247
|
close() {
|
|
151
248
|
this.#active = false;
|
|
152
249
|
this.stopHeartbeat();
|
|
250
|
+
if (this.#drainHandler) {
|
|
251
|
+
this.#res.removeListener('drain', this.#drainHandler);
|
|
252
|
+
this.#drainHandler = null;
|
|
253
|
+
}
|
|
254
|
+
this.#backpressureBuffer = [];
|
|
153
255
|
try {
|
|
154
256
|
this.#res.end();
|
|
155
257
|
} catch (e) {
|
|
@@ -163,10 +265,19 @@ export function setupSseHeaders(res) {
|
|
|
163
265
|
res.setHeader('Cache-Control', 'no-cache');
|
|
164
266
|
res.setHeader('Connection', 'keep-alive');
|
|
165
267
|
res.setHeader('Transfer-Encoding', 'chunked');
|
|
268
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
269
|
+
res.setHeader('Vary', 'Accept');
|
|
270
|
+
if (typeof res.flush !== 'function') {
|
|
271
|
+
res.flush = () => {};
|
|
272
|
+
}
|
|
166
273
|
}
|
|
167
274
|
|
|
168
|
-
|
|
275
|
+
let sseGlobalId = 0;
|
|
276
|
+
|
|
277
|
+
export function sendSSE(res, data, event = SSE_EVENTS.MESSAGE, id = null) {
|
|
278
|
+
const eventId = id !== null ? id : ++sseGlobalId;
|
|
169
279
|
const lines = [
|
|
280
|
+
`id: ${eventId}`,
|
|
170
281
|
`event: ${event}`,
|
|
171
282
|
`data: ${JSON.stringify(data)}`,
|
|
172
283
|
''
|
|
@@ -182,11 +293,10 @@ export function createSseResponseHandler(res, acceptSse = false) {
|
|
|
182
293
|
|
|
183
294
|
return {
|
|
184
295
|
send: (data, done = false) => {
|
|
185
|
-
const json = JSON.stringify(data);
|
|
186
296
|
if (acceptSse) {
|
|
187
297
|
sendSSE(res, data);
|
|
188
|
-
if (done) res.end();
|
|
189
298
|
} else {
|
|
299
|
+
const json = JSON.stringify(data);
|
|
190
300
|
res.setHeader('Content-Type', 'application/json');
|
|
191
301
|
res.end(json);
|
|
192
302
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
const subscriptions = new Map();
|
|
2
|
+
let _notifyFn = null;
|
|
3
|
+
|
|
4
|
+
export function setNotify(fn) {
|
|
5
|
+
_notifyFn = fn;
|
|
6
|
+
}
|
|
2
7
|
|
|
3
8
|
export function subscribe(uri) {
|
|
4
9
|
subscriptions.set(uri, Date.now());
|
|
@@ -17,6 +22,12 @@ export function isSubscribed(uri) {
|
|
|
17
22
|
return subscriptions.has(uri);
|
|
18
23
|
}
|
|
19
24
|
|
|
25
|
+
export function notify(uri, data) {
|
|
26
|
+
if (_notifyFn) {
|
|
27
|
+
_notifyFn(uri, data);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
20
31
|
export function clear() {
|
|
21
32
|
subscriptions.clear();
|
|
22
33
|
}
|