@classcad/mcp 0.1.0
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/LICENSE +21 -0
- package/README.md +361 -0
- package/dist/auth.d.ts +32 -0
- package/dist/auth.js +272 -0
- package/dist/auth.js.map +1 -0
- package/dist/bridge/protocol.d.ts +87 -0
- package/dist/bridge/protocol.js +14 -0
- package/dist/bridge/protocol.js.map +1 -0
- package/dist/bridge/server.d.ts +35 -0
- package/dist/bridge/server.js +236 -0
- package/dist/bridge/server.js.map +1 -0
- package/dist/client.d.ts +98 -0
- package/dist/client.js +879 -0
- package/dist/client.js.map +1 -0
- package/dist/daemon.d.ts +38 -0
- package/dist/daemon.js +390 -0
- package/dist/daemon.js.map +1 -0
- package/dist/engine/wasm-worker.d.ts +1 -0
- package/dist/engine/wasm-worker.js +176 -0
- package/dist/engine/wasm-worker.js.map +1 -0
- package/dist/engine/wasm.d.ts +60 -0
- package/dist/engine/wasm.js +212 -0
- package/dist/engine/wasm.js.map +1 -0
- package/dist/mcp-server.d.ts +23 -0
- package/dist/mcp-server.js +141 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/ports.d.ts +2 -0
- package/dist/ports.js +5 -0
- package/dist/ports.js.map +1 -0
- package/dist/queue.d.ts +9 -0
- package/dist/queue.js +50 -0
- package/dist/queue.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +423 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/auth.d.ts +7 -0
- package/dist/tools/auth.js +63 -0
- package/dist/tools/auth.js.map +1 -0
- package/dist/tools/bridge.d.ts +4 -0
- package/dist/tools/bridge.js +115 -0
- package/dist/tools/bridge.js.map +1 -0
- package/dist/tools/docs.d.ts +13 -0
- package/dist/tools/docs.js +180 -0
- package/dist/tools/docs.js.map +1 -0
- package/dist/tools/lifecycle.d.ts +3 -0
- package/dist/tools/lifecycle.js +156 -0
- package/dist/tools/lifecycle.js.map +1 -0
- package/dist/tools/script.d.ts +3 -0
- package/dist/tools/script.js +102 -0
- package/dist/tools/script.js.map +1 -0
- package/dist/tools/snapshot.d.ts +3 -0
- package/dist/tools/snapshot.js +174 -0
- package/dist/tools/snapshot.js.map +1 -0
- package/dist/tools/state.d.ts +3 -0
- package/dist/tools/state.js +95 -0
- package/dist/tools/state.js.map +1 -0
- package/dist/types.d.ts +46 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +44 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// wasm-worker.ts — the published ClassCAD WASM engine inside a Node
|
|
2
|
+
// worker_thread. This is what ClassCADWasmWorker.js does in the browser,
|
|
3
|
+
// minus the browser: the engine is synchronous and blocks its thread for the
|
|
4
|
+
// duration of a command, so it lives in its own thread and the daemon's
|
|
5
|
+
// event loop (other sessions, the bridge, /health) stays responsive.
|
|
6
|
+
//
|
|
7
|
+
// Two shims make the web-built engine run in Node:
|
|
8
|
+
// • location: the engine's license key is bound to "allowed origins"
|
|
9
|
+
// (configured on classcad.ch/user). The engine compares
|
|
10
|
+
// globalThis.location.origin with the siteLocation handed to init() and
|
|
11
|
+
// with the key's origins. The MCP presents itself as one of those
|
|
12
|
+
// origins (CLASSCAD_WASM_ORIGIN, default http://localhost:3000 - the
|
|
13
|
+
// origin most dev keys already allow).
|
|
14
|
+
// • XMLHttpRequest: the glue was built with -sENVIRONMENT=web and loads its
|
|
15
|
+
// side modules (lgs2d, lgs3d, ExpWasm) through XHR. We answer those from
|
|
16
|
+
// the local asset directory. A real XHR completes on a later task; the
|
|
17
|
+
// glue relies on that ordering while linking, hence the deferred onload.
|
|
18
|
+
//
|
|
19
|
+
// Protocol with the parent (see wasm.ts):
|
|
20
|
+
// → { type:'init', dir, key, origin } ← { type:'ready', ms, memoryMB } | { type:'error', message }
|
|
21
|
+
// → { type:'execute', id, command } ← { type:'result', id, messages, binaryMessages, decodeErrors }
|
|
22
|
+
// | { type:'error', id, message, fatal? } (fatal: the engine trapped — retire it)
|
|
23
|
+
import { parentPort } from 'node:worker_threads';
|
|
24
|
+
import { readFileSync } from 'node:fs';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
import { pathToFileURL } from 'node:url';
|
|
27
|
+
import { inflateRawSync } from 'node:zlib';
|
|
28
|
+
const port = parentPort;
|
|
29
|
+
let engine = null;
|
|
30
|
+
function installShims(dir, origin) {
|
|
31
|
+
const u = new URL(origin);
|
|
32
|
+
const loc = {
|
|
33
|
+
href: u.href,
|
|
34
|
+
origin: u.origin,
|
|
35
|
+
protocol: u.protocol,
|
|
36
|
+
host: u.host,
|
|
37
|
+
hostname: u.hostname,
|
|
38
|
+
port: u.port,
|
|
39
|
+
pathname: '/',
|
|
40
|
+
search: '',
|
|
41
|
+
hash: '',
|
|
42
|
+
};
|
|
43
|
+
globalThis.location = loc;
|
|
44
|
+
globalThis.XMLHttpRequest = class {
|
|
45
|
+
url = '';
|
|
46
|
+
async = true;
|
|
47
|
+
status = 0;
|
|
48
|
+
responseType = '';
|
|
49
|
+
response = null;
|
|
50
|
+
responseText = '';
|
|
51
|
+
onload = null;
|
|
52
|
+
onerror = null;
|
|
53
|
+
open(_method, url, async = true) {
|
|
54
|
+
this.url = String(url);
|
|
55
|
+
this.async = async;
|
|
56
|
+
}
|
|
57
|
+
setRequestHeader() { }
|
|
58
|
+
send() {
|
|
59
|
+
const name = this.url.replace(/^.*\//, '');
|
|
60
|
+
let buf;
|
|
61
|
+
try {
|
|
62
|
+
buf = readFileSync(join(dir, name));
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
this.status = 404;
|
|
66
|
+
if (this.onerror)
|
|
67
|
+
this.onerror(err);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
this.status = 200;
|
|
71
|
+
this.response = this.responseType === 'arraybuffer' ? buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) : buf.toString();
|
|
72
|
+
this.responseText = this.responseType === 'arraybuffer' ? '' : buf.toString();
|
|
73
|
+
if (this.async && this.onload)
|
|
74
|
+
setImmediate(() => this.onload());
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
return loc;
|
|
78
|
+
}
|
|
79
|
+
/** What the engine printed while starting: the only reason it gives when it refuses to run (a bad key exits silently). */
|
|
80
|
+
const initLogs = [];
|
|
81
|
+
async function init(msg) {
|
|
82
|
+
const t0 = Date.now();
|
|
83
|
+
const loc = installShims(msg.dir, msg.origin);
|
|
84
|
+
const factory = (await import(pathToFileURL(join(msg.dir, 'ClassCADWasm.js')).href)).default;
|
|
85
|
+
const logs = initLogs;
|
|
86
|
+
engine = await factory({
|
|
87
|
+
wasmBinary: readFileSync(join(msg.dir, 'ClassCADWasm.wasm')),
|
|
88
|
+
locateFile: (f) => f,
|
|
89
|
+
print: (s) => logs.push(s),
|
|
90
|
+
printErr: (s) => logs.push(s),
|
|
91
|
+
});
|
|
92
|
+
// Same file layout ClassCADWasmWorker.js prepares (classcadkey mode).
|
|
93
|
+
const { FS } = engine;
|
|
94
|
+
FS.mkdir('/home/web_user/ClassCAD/');
|
|
95
|
+
FS.mkdir('/home/web_user/data');
|
|
96
|
+
FS.writeFile('.classcad.appkey', msg.key);
|
|
97
|
+
FS.writeFile('classcad.cfe', new Int8Array(readFileSync(join(msg.dir, 'classcad.cfe'))));
|
|
98
|
+
FS.writeFile('/home/web_user/ClassCAD/classcad.ini', '[common]\n"datapath"="/home/web_user/data"\n"verbosefilename"="/home/web_user/data/classcad.log"\n"verboselevel"="16583"\n"loglevel"="info"\n"tessellatecurves"="0"\n"cc_productrefcreation"="3"\n\n[classes]\nsystem = "classcad.cfe"\n');
|
|
99
|
+
FS.writeFile('filterconfig.json', readFileSync(join(msg.dir, 'filterconfig.json'), 'utf8'));
|
|
100
|
+
engine.init(JSON.stringify({
|
|
101
|
+
configurationType: 'file',
|
|
102
|
+
configurationData: '/home/web_user/ClassCAD/classcad.ini',
|
|
103
|
+
sendTreePatches: true,
|
|
104
|
+
compression: false,
|
|
105
|
+
siteLocation: JSON.stringify(loc),
|
|
106
|
+
}), false);
|
|
107
|
+
port.postMessage({ type: 'ready', ms: Date.now() - t0, memoryMB: Math.round(engine.HEAPU8.length / 1048576), logs: logs.slice(0, 10) });
|
|
108
|
+
}
|
|
109
|
+
// The engine throws plain objects too (e.g. a refused license key): show their fields, not "[object Object]".
|
|
110
|
+
const errorText = (err) => {
|
|
111
|
+
if (err instanceof Error)
|
|
112
|
+
return err.message;
|
|
113
|
+
if (err && typeof err === 'object') {
|
|
114
|
+
try {
|
|
115
|
+
return JSON.stringify(err);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
/* circular: fall through */
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return String(err);
|
|
122
|
+
};
|
|
123
|
+
function execute(msg) {
|
|
124
|
+
const messages = [];
|
|
125
|
+
const binaryMessages = [];
|
|
126
|
+
const decodeErrors = [];
|
|
127
|
+
const command = JSON.stringify(msg.command);
|
|
128
|
+
// The callbacks run INSIDE engine.execute, between WASM frames. An exception
|
|
129
|
+
// thrown here would unwind through the engine and leave it in an undefined
|
|
130
|
+
// state — so they never throw; what cannot be decoded is reported instead.
|
|
131
|
+
try {
|
|
132
|
+
engine.execute(command, engine, (_m, text) => {
|
|
133
|
+
try {
|
|
134
|
+
messages.push(JSON.parse(text));
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
decodeErrors.push(`message: ${errorText(err)}`);
|
|
138
|
+
}
|
|
139
|
+
}, (mod, ptr, len) => {
|
|
140
|
+
try {
|
|
141
|
+
const bytes = mod.HEAPU8.slice(ptr, ptr + len);
|
|
142
|
+
binaryMessages.push(JSON.parse(inflateRawSync(Buffer.from(bytes)).toString()));
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
decodeErrors.push(`binary package: ${errorText(err)}`);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
// A throw out of engine.execute is a trap or an abort (the engine handles
|
|
151
|
+
// its own errors and reports them in the Result). Its state is undefined
|
|
152
|
+
// from here on: the parent retires this engine.
|
|
153
|
+
port.postMessage({ type: 'error', id: msg.id, fatal: true, message: `${String(msg.command.command)}: ${errorText(err)}` });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
port.postMessage({ type: 'result', id: msg.id, messages, binaryMessages, decodeErrors });
|
|
157
|
+
}
|
|
158
|
+
port.on('message', (msg) => {
|
|
159
|
+
try {
|
|
160
|
+
if (msg.type === 'init') {
|
|
161
|
+
init(msg).catch(err => {
|
|
162
|
+
const said = initLogs.filter(l => l.trim()).slice(-8).join(' | ');
|
|
163
|
+
port.postMessage({ type: 'error', message: errorText(err) + (said ? ` — engine output: ${said}` : '') });
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
else if (msg.type === 'execute') {
|
|
167
|
+
if (!engine)
|
|
168
|
+
throw new Error('engine not initialized');
|
|
169
|
+
execute(msg);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
port.postMessage({ type: 'error', id: msg.id, message: errorText(err) });
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
//# sourceMappingURL=wasm-worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wasm-worker.js","sourceRoot":"","sources":["../../src/engine/wasm-worker.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { EngineExecuteResult } from '../bridge/protocol.js';
|
|
2
|
+
/** The ClassCAD release the MCP hosts (the same the buerli apps load). */
|
|
3
|
+
export declare const DEFAULT_WASM_VERSION = "21.2.0";
|
|
4
|
+
/**
|
|
5
|
+
* The MCP's built-in key: wasm, enterprise, allowed origin http://localhost:3000,
|
|
6
|
+
* valid for six months (issued 2026-09-30). Built in so the local engine needs
|
|
7
|
+
* no configuration; CLASSCAD_WASM_KEY overrides it. Renew it before
|
|
8
|
+
* DEV_WASM_KEY_EXPIRES with buerli-backend/functions/scripts/appkey.mjs and
|
|
9
|
+
* ship a release: the engine refuses an expired key.
|
|
10
|
+
*/
|
|
11
|
+
export declare const DEV_WASM_KEY = "MS4xLlZZUG51VkNpOGdjQm50RXB0VkE1RnQ1ekVVazNOR1dYMk9weHlxRGJjazRSdGYwTFRPTFl5NVdjYmY4VnFOOXlWTDQ0OUNzSExTbmhQRHZpNEVidXRubFJhZUpQVVA3aW1xZFBlWXlIcFRocFVlamVSZmJUaVM3aWcwWnlpdXNjSm5OaHFnTDhmN2EvSVRsZWxFQy9BRXJSQzZ5VUNLUC9UbFBiTTFINk10UlhNbkYwZm5QN2J0bE5DSWdWWW5RTldFSjFNcGYzaEJ4VjBjTHZlcW9tUXY0bklzVFFWSVl5T0hZVHM4ZU8vbENlaTZhdGxFbERycy9WVldoZHIwNm5DdWRvVEhTNGlmSGRWQ2FSak5QSmVNc1BmK1ZMYm5WVFI4aXl2MENadW1MbEsra2N3L0tqc0hLanZTUXJBTG5EQjcvOWpjcFhwbHlHNkFYRVppeVVESFZ0Ly9EL1VKdndMZ2E3S1ZFcFNRTStmS0xSZDR0RFYrVU41VHBrZ0wvYk5uK2huR3FHU2FtUExybXVGdXQ0T0EycHhiSlNZakZvYjFqbUZEbG5hVk1BTW11c1RqblVyQklGWGFxOUJDZnZRck13eHlGeE1RdEtCdGpnQUd5b1VSNmZmaFRXWVhpOGVtL2ZkU3hMNjFRamF4ejdLVExPS3pFOXJVMVgzcXBPQVo1NEFVenFVVzg0Zk9BeUZ0KzRMT2M0dzJMMWJ1U0YxUkZFby9CZXZ4UGRzQ3A4ZWUxMUxiTDRCQ0pFUEZJMGQyYU9JU0grN0NqOFZhVEpHb2JKVys3cktPemUyTTE4V3hOZnJMWVlhZXJzdDEwdDl1YTFja3lPdUZzRjMwdWFiejUvdGRVeENRRjhhcysxNFR2Slo4THowd1lxRkx2RTgrcG12N2VpT1ZUTzRtVSsvWXpLYkljVXFoNjNRdllkOU5jeWx3c0ZqRkZ4NGJlbGxrcHFuQjFNNHcvZVVmQ2VpNzRXT256ZGtNQzJiYWx2US9VWWJvS3ltbjZHVWtzbjZJczd0ZU5RMGs2TGovTm5mcFllcldNUXQxeEhjWnVySlZxd2hDRjBTTVRhSmhHWFNQODkwd3RFdkNRSS8zSHNYdUpseWFkWkpmZDZHRmJhNTRldEdoNUIveE80aWdNMEVuekVUMWVKa0tWMWhtbTduZjlZZzRwZmw4QnI5Z0ZZdFlPV080S2hWTjczYTB5Y0g5SlpMUUJydmlBVFowQm1uL3RNYnlKWitadmZjYzN1S2lvdXl4VC9ONlJwejlycFdrUmJvSHZSaHF4VkdjY3B4bG1nbnZudkNVaHZSZktFNTYvaDh0eEZXcXFncEo4VFR4cndFQVo3R0liRlVYVWN6TUhoQW9FV2lSWnVMTWxQck0zTVJWQkJ4YXA5a1BUeWVGaUFXU0tzZ2krUHRQbVp4ZTJldlRkekYzTEpvSkc4R25mQjZ5MVE4VWE5Q09wU3pMN05aamFpak9NRmU0dXBFaVI0andrOTUxMDVkcW9TTFExY0JoU2J0MytiNjkxcE1YYmNFZGNjU1ZYSnE5ejdubUJCeGs1THdUaURRdjdDTkQyZGNmejdKa0hXSENFcklvN2ZyMVFKU1V2dG5RRk5YUGxPNWhsTnZGVU9qWngwQkNDMGc2UDFrQUxVUEoweWtaZ3pQSnRYckdHL09XQjFUaERtTVNvMVpmdE1vbTlCdENjVGhMeGJuWDJmODlEcTNHZFE4RUd2UVV0cFRCNkVkeTdBOEE1aVNMemR1OEJhek5nYUwrclFlNk9CTko4VVEweEMzdmUrTFFoTmtVMTVzQWlycmtVdDJmL3JkMDM2bHd5d3ZnZTg2Umlvc1pnOVQ2RHVhNWRoQ1N3L0ZTQmpCOUo0QmI2Kys2TmNEOS9OTGFXNUIwUXJzdDRXYjR0cDEzL3Q3RXlxWEZiV3JLMVdiQlZtMnVhd3pzLzZ4RklR";
|
|
12
|
+
export declare const DEV_WASM_KEY_EXPIRES = "2027-03-30T09:25:07Z";
|
|
13
|
+
export declare const DEFAULT_WASM_ORIGIN = "http://localhost:3000";
|
|
14
|
+
/** Files of one release under https://awvstatic.com/classcad/download/release/<version>/wasm/. */
|
|
15
|
+
export declare const WASM_ASSETS: string[];
|
|
16
|
+
export type LocalWasmOptions = {
|
|
17
|
+
/** ClassCAD key (classcad.ch/user). Required — without it there is no local engine. */
|
|
18
|
+
key: string;
|
|
19
|
+
/** Origin the engine believes it runs on; must be in the key's allowed origins. */
|
|
20
|
+
origin?: string;
|
|
21
|
+
/** Release to host. */
|
|
22
|
+
version?: string;
|
|
23
|
+
/** Asset directory override. */
|
|
24
|
+
dir?: string;
|
|
25
|
+
/** Download base override (default awvstatic release/<version>/wasm). */
|
|
26
|
+
url?: string;
|
|
27
|
+
log?: (msg: string) => void;
|
|
28
|
+
};
|
|
29
|
+
export type LocalEngine = {
|
|
30
|
+
execute: (command: Record<string, unknown>) => Promise<EngineExecuteResult>;
|
|
31
|
+
/**
|
|
32
|
+
* Health check: one GetTree must come back as a Result with a tree within
|
|
33
|
+
* `timeoutMs`. False = the engine is gone or wedged — retire it (close) and
|
|
34
|
+
* start a new one.
|
|
35
|
+
*/
|
|
36
|
+
ping: (timeoutMs?: number) => Promise<boolean>;
|
|
37
|
+
/** `reason` is kept as deathReason. */
|
|
38
|
+
close: (reason?: string) => void;
|
|
39
|
+
/** Closed, crashed, trapped or retired: execute() only rejects from here on. */
|
|
40
|
+
readonly closed: boolean;
|
|
41
|
+
/** Why the engine is closed (null while it runs). */
|
|
42
|
+
readonly deathReason: string | null;
|
|
43
|
+
readonly version: string;
|
|
44
|
+
readonly origin: string;
|
|
45
|
+
readonly dir: string;
|
|
46
|
+
readonly memoryMB: number;
|
|
47
|
+
};
|
|
48
|
+
/** Resolves the local engine settings from the environment (the built-in key unless CLASSCAD_WASM_KEY is set). */
|
|
49
|
+
export declare function wasmOptionsFromEnv(env?: NodeJS.ProcessEnv): LocalWasmOptions | null;
|
|
50
|
+
export declare function defaultWasmDir(version?: string): string;
|
|
51
|
+
export declare function assetBaseUrl(version?: string): string;
|
|
52
|
+
/** Downloads the release assets that are missing from `dir` (atomic per file). */
|
|
53
|
+
export declare function ensureWasmAssets(opts?: {
|
|
54
|
+
version?: string;
|
|
55
|
+
dir?: string;
|
|
56
|
+
url?: string;
|
|
57
|
+
log?: (m: string) => void;
|
|
58
|
+
}): Promise<string>;
|
|
59
|
+
/** Downloads (if needed) and boots one engine in its own worker thread. */
|
|
60
|
+
export declare function startLocalEngine(opts: LocalWasmOptions): Promise<LocalEngine>;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// wasm.ts — the MCP's own ClassCAD engine: the published WASM build, hosted
|
|
2
|
+
// in a worker thread (wasm-worker.ts). No server, no browser tab: the MCP
|
|
3
|
+
// can model on its own wherever Node runs.
|
|
4
|
+
//
|
|
5
|
+
// What it needs:
|
|
6
|
+
// • the assets of one ClassCAD release (~60 MB: glue, main module, three
|
|
7
|
+
// side modules, class file, filter config), downloaded once from
|
|
8
|
+
// awvstatic.com into CLASSCAD_WASM_DIR (default ~/.classcad-mcp/wasm/<version>),
|
|
9
|
+
// • a ClassCAD key whose allowed origins include the origin the engine is
|
|
10
|
+
// told it runs on (CLASSCAD_WASM_ORIGIN, default http://localhost:3000).
|
|
11
|
+
// A six-month key is built in (DEV_WASM_KEY); CLASSCAD_WASM_KEY
|
|
12
|
+
// overrides it. Keys and origins are managed on classcad.ch/user.
|
|
13
|
+
//
|
|
14
|
+
// One engine = one drawing = one MCP session. execute() has the same shape
|
|
15
|
+
// as the app bridge's engine.execute (messages + inflated binary packages),
|
|
16
|
+
// so the client treats both the same way.
|
|
17
|
+
import { Worker } from 'node:worker_threads';
|
|
18
|
+
import { createWriteStream, existsSync, mkdirSync, renameSync, statSync, unlinkSync } from 'node:fs';
|
|
19
|
+
import { pipeline } from 'node:stream/promises';
|
|
20
|
+
import { Readable } from 'node:stream';
|
|
21
|
+
import { homedir } from 'node:os';
|
|
22
|
+
import { join, dirname } from 'node:path';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
/** The ClassCAD release the MCP hosts (the same the buerli apps load). */
|
|
25
|
+
export const DEFAULT_WASM_VERSION = '21.2.0';
|
|
26
|
+
/**
|
|
27
|
+
* The MCP's built-in key: wasm, enterprise, allowed origin http://localhost:3000,
|
|
28
|
+
* valid for six months (issued 2026-09-30). Built in so the local engine needs
|
|
29
|
+
* no configuration; CLASSCAD_WASM_KEY overrides it. Renew it before
|
|
30
|
+
* DEV_WASM_KEY_EXPIRES with buerli-backend/functions/scripts/appkey.mjs and
|
|
31
|
+
* ship a release: the engine refuses an expired key.
|
|
32
|
+
*/
|
|
33
|
+
export const DEV_WASM_KEY = 'MS4xLlZZUG51VkNpOGdjQm50RXB0VkE1RnQ1ekVVazNOR1dYMk9weHlxRGJjazRSdGYwTFRPTFl5NVdjYmY4VnFOOXlWTDQ0OUNzSExTbmhQRHZpNEVidXRubFJhZUpQVVA3aW1xZFBlWXlIcFRocFVlamVSZmJUaVM3aWcwWnlpdXNjSm5OaHFnTDhmN2EvSVRsZWxFQy9BRXJSQzZ5VUNLUC9UbFBiTTFINk10UlhNbkYwZm5QN2J0bE5DSWdWWW5RTldFSjFNcGYzaEJ4VjBjTHZlcW9tUXY0bklzVFFWSVl5T0hZVHM4ZU8vbENlaTZhdGxFbERycy9WVldoZHIwNm5DdWRvVEhTNGlmSGRWQ2FSak5QSmVNc1BmK1ZMYm5WVFI4aXl2MENadW1MbEsra2N3L0tqc0hLanZTUXJBTG5EQjcvOWpjcFhwbHlHNkFYRVppeVVESFZ0Ly9EL1VKdndMZ2E3S1ZFcFNRTStmS0xSZDR0RFYrVU41VHBrZ0wvYk5uK2huR3FHU2FtUExybXVGdXQ0T0EycHhiSlNZakZvYjFqbUZEbG5hVk1BTW11c1RqblVyQklGWGFxOUJDZnZRck13eHlGeE1RdEtCdGpnQUd5b1VSNmZmaFRXWVhpOGVtL2ZkU3hMNjFRamF4ejdLVExPS3pFOXJVMVgzcXBPQVo1NEFVenFVVzg0Zk9BeUZ0KzRMT2M0dzJMMWJ1U0YxUkZFby9CZXZ4UGRzQ3A4ZWUxMUxiTDRCQ0pFUEZJMGQyYU9JU0grN0NqOFZhVEpHb2JKVys3cktPemUyTTE4V3hOZnJMWVlhZXJzdDEwdDl1YTFja3lPdUZzRjMwdWFiejUvdGRVeENRRjhhcysxNFR2Slo4THowd1lxRkx2RTgrcG12N2VpT1ZUTzRtVSsvWXpLYkljVXFoNjNRdllkOU5jeWx3c0ZqRkZ4NGJlbGxrcHFuQjFNNHcvZVVmQ2VpNzRXT256ZGtNQzJiYWx2US9VWWJvS3ltbjZHVWtzbjZJczd0ZU5RMGs2TGovTm5mcFllcldNUXQxeEhjWnVySlZxd2hDRjBTTVRhSmhHWFNQODkwd3RFdkNRSS8zSHNYdUpseWFkWkpmZDZHRmJhNTRldEdoNUIveE80aWdNMEVuekVUMWVKa0tWMWhtbTduZjlZZzRwZmw4QnI5Z0ZZdFlPV080S2hWTjczYTB5Y0g5SlpMUUJydmlBVFowQm1uL3RNYnlKWitadmZjYzN1S2lvdXl4VC9ONlJwejlycFdrUmJvSHZSaHF4VkdjY3B4bG1nbnZudkNVaHZSZktFNTYvaDh0eEZXcXFncEo4VFR4cndFQVo3R0liRlVYVWN6TUhoQW9FV2lSWnVMTWxQck0zTVJWQkJ4YXA5a1BUeWVGaUFXU0tzZ2krUHRQbVp4ZTJldlRkekYzTEpvSkc4R25mQjZ5MVE4VWE5Q09wU3pMN05aamFpak9NRmU0dXBFaVI0andrOTUxMDVkcW9TTFExY0JoU2J0MytiNjkxcE1YYmNFZGNjU1ZYSnE5ejdubUJCeGs1THdUaURRdjdDTkQyZGNmejdKa0hXSENFcklvN2ZyMVFKU1V2dG5RRk5YUGxPNWhsTnZGVU9qWngwQkNDMGc2UDFrQUxVUEoweWtaZ3pQSnRYckdHL09XQjFUaERtTVNvMVpmdE1vbTlCdENjVGhMeGJuWDJmODlEcTNHZFE4RUd2UVV0cFRCNkVkeTdBOEE1aVNMemR1OEJhek5nYUwrclFlNk9CTko4VVEweEMzdmUrTFFoTmtVMTVzQWlycmtVdDJmL3JkMDM2bHd5d3ZnZTg2Umlvc1pnOVQ2RHVhNWRoQ1N3L0ZTQmpCOUo0QmI2Kys2TmNEOS9OTGFXNUIwUXJzdDRXYjR0cDEzL3Q3RXlxWEZiV3JLMVdiQlZtMnVhd3pzLzZ4RklR';
|
|
34
|
+
export const DEV_WASM_KEY_EXPIRES = '2027-03-30T09:25:07Z';
|
|
35
|
+
export const DEFAULT_WASM_ORIGIN = 'http://localhost:3000';
|
|
36
|
+
/** Files of one release under https://awvstatic.com/classcad/download/release/<version>/wasm/. */
|
|
37
|
+
export const WASM_ASSETS = ['ClassCADWasm.js', 'ClassCADWasm.wasm', 'lgs2d.wasm', 'lgs3d.wasm', 'ExpWasm.wasm', 'classcad.cfe', 'filterconfig.json'];
|
|
38
|
+
/** Resolves the local engine settings from the environment (the built-in key unless CLASSCAD_WASM_KEY is set). */
|
|
39
|
+
export function wasmOptionsFromEnv(env = process.env) {
|
|
40
|
+
const key = env.CLASSCAD_WASM_KEY || DEV_WASM_KEY;
|
|
41
|
+
if (!key)
|
|
42
|
+
return null;
|
|
43
|
+
return {
|
|
44
|
+
key,
|
|
45
|
+
origin: env.CLASSCAD_WASM_ORIGIN,
|
|
46
|
+
version: env.CLASSCAD_WASM_VERSION,
|
|
47
|
+
dir: env.CLASSCAD_WASM_DIR,
|
|
48
|
+
url: env.CLASSCAD_WASM_URL,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function defaultWasmDir(version = DEFAULT_WASM_VERSION) {
|
|
52
|
+
return join(homedir(), '.classcad-mcp', 'wasm', version);
|
|
53
|
+
}
|
|
54
|
+
export function assetBaseUrl(version = DEFAULT_WASM_VERSION) {
|
|
55
|
+
return `https://awvstatic.com/classcad/download/release/${version}/wasm`;
|
|
56
|
+
}
|
|
57
|
+
/** Downloads the release assets that are missing from `dir` (atomic per file). */
|
|
58
|
+
export async function ensureWasmAssets(opts = {}) {
|
|
59
|
+
const version = opts.version ?? DEFAULT_WASM_VERSION;
|
|
60
|
+
const dir = opts.dir ?? defaultWasmDir(version);
|
|
61
|
+
const base = (opts.url ?? assetBaseUrl(version)).replace(/\/+$/, '');
|
|
62
|
+
const log = opts.log ?? (() => { });
|
|
63
|
+
mkdirSync(dir, { recursive: true });
|
|
64
|
+
const missing = WASM_ASSETS.filter(f => !existsSync(join(dir, f)) || statSync(join(dir, f)).size === 0);
|
|
65
|
+
if (missing.length === 0)
|
|
66
|
+
return dir;
|
|
67
|
+
log(`downloading ClassCAD ${version} WASM assets (${missing.length} file(s)) from ${base} to ${dir}`);
|
|
68
|
+
for (const name of missing) {
|
|
69
|
+
const t0 = Date.now();
|
|
70
|
+
const res = await fetch(`${base}/${name}`);
|
|
71
|
+
if (!res.ok || !res.body)
|
|
72
|
+
throw new Error(`download failed: ${base}/${name} → HTTP ${res.status}`);
|
|
73
|
+
const part = join(dir, `${name}.part`);
|
|
74
|
+
try {
|
|
75
|
+
await pipeline(Readable.fromWeb(res.body), createWriteStream(part));
|
|
76
|
+
renameSync(part, join(dir, name));
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
try {
|
|
80
|
+
unlinkSync(part);
|
|
81
|
+
}
|
|
82
|
+
catch { }
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
log(` ${name}: ${Math.round(statSync(join(dir, name)).size / 1048576)} MB in ${Date.now() - t0} ms`);
|
|
86
|
+
}
|
|
87
|
+
return dir;
|
|
88
|
+
}
|
|
89
|
+
/** Downloads (if needed) and boots one engine in its own worker thread. */
|
|
90
|
+
export async function startLocalEngine(opts) {
|
|
91
|
+
if (!opts.key)
|
|
92
|
+
throw new Error('no ClassCAD key for the local WASM engine (CLASSCAD_WASM_KEY)');
|
|
93
|
+
const version = opts.version ?? DEFAULT_WASM_VERSION;
|
|
94
|
+
const origin = opts.origin ?? DEFAULT_WASM_ORIGIN;
|
|
95
|
+
const log = opts.log ?? (() => { });
|
|
96
|
+
const dir = await ensureWasmAssets({ version, dir: opts.dir, url: opts.url, log });
|
|
97
|
+
const workerPath = join(dirname(fileURLToPath(import.meta.url)), 'wasm-worker.js');
|
|
98
|
+
const worker = new Worker(workerPath);
|
|
99
|
+
let memoryMB = 0;
|
|
100
|
+
let closed = false;
|
|
101
|
+
let deathReason = null;
|
|
102
|
+
let nextId = 1;
|
|
103
|
+
const pending = new Map();
|
|
104
|
+
const failAll = (why) => {
|
|
105
|
+
for (const [, p] of pending)
|
|
106
|
+
p.reject(new Error(why));
|
|
107
|
+
pending.clear();
|
|
108
|
+
};
|
|
109
|
+
const retire = (why) => {
|
|
110
|
+
if (!closed)
|
|
111
|
+
deathReason = why;
|
|
112
|
+
closed = true;
|
|
113
|
+
failAll(why);
|
|
114
|
+
worker.terminate().catch(() => { });
|
|
115
|
+
};
|
|
116
|
+
worker.on('error', err => retire(`WASM engine crashed: ${err.message}`));
|
|
117
|
+
worker.on('exit', code => retire(`WASM engine exited (code ${code})`));
|
|
118
|
+
const t0 = Date.now();
|
|
119
|
+
await new Promise((resolve, reject) => {
|
|
120
|
+
const onMessage = (m) => {
|
|
121
|
+
if (m?.type === 'ready') {
|
|
122
|
+
memoryMB = m.memoryMB;
|
|
123
|
+
worker.off('message', onMessage);
|
|
124
|
+
log(`WASM engine ${version} ready in ${m.ms} ms (origin ${origin}, ${m.memoryMB} MB heap)`);
|
|
125
|
+
resolve();
|
|
126
|
+
}
|
|
127
|
+
else if (m?.type === 'error') {
|
|
128
|
+
worker.off('message', onMessage);
|
|
129
|
+
// The engine refuses a bad key by exiting without a word: name the likely cause.
|
|
130
|
+
const expired = opts.key === DEV_WASM_KEY && Date.now() > Date.parse(DEV_WASM_KEY_EXPIRES);
|
|
131
|
+
const hint = expired
|
|
132
|
+
? ` The MCP's built-in engine key expired on ${DEV_WASM_KEY_EXPIRES.slice(0, 10)}: update the MCP (or set CLASSCAD_WASM_KEY).`
|
|
133
|
+
: opts.key !== DEV_WASM_KEY
|
|
134
|
+
? ' Check CLASSCAD_WASM_KEY: the engine refuses an expired key or one whose allowed origins do not include ' + origin + '.'
|
|
135
|
+
: '';
|
|
136
|
+
reject(new Error(`WASM engine failed to start: ${m.message}.${hint}`));
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
worker.on('message', onMessage);
|
|
140
|
+
worker.once('error', err => reject(new Error(`WASM engine failed to start: ${err.message}`)));
|
|
141
|
+
worker.postMessage({ type: 'init', dir, key: opts.key, origin });
|
|
142
|
+
}).catch(err => {
|
|
143
|
+
worker.terminate().catch(() => { });
|
|
144
|
+
throw err;
|
|
145
|
+
});
|
|
146
|
+
void t0;
|
|
147
|
+
worker.on('message', (m) => {
|
|
148
|
+
if (m?.type === 'result' || (m?.type === 'error' && typeof m.id === 'number')) {
|
|
149
|
+
const p = pending.get(m.id);
|
|
150
|
+
if (!p)
|
|
151
|
+
return;
|
|
152
|
+
pending.delete(m.id);
|
|
153
|
+
if (m.type === 'result') {
|
|
154
|
+
p.resolve({ messages: m.messages ?? [], binaryMessages: m.binaryMessages ?? [], decodeErrors: m.decodeErrors ?? [] });
|
|
155
|
+
}
|
|
156
|
+
else if (m.fatal) {
|
|
157
|
+
// The engine trapped mid-command: nothing it answers from now on can be trusted.
|
|
158
|
+
const why = `WASM engine aborted (${m.message})`;
|
|
159
|
+
log(`${why} — retired; the next command starts a new engine with an empty drawing`);
|
|
160
|
+
p.reject(new Error(`${why}. The local engine was retired and the drawing is lost; the next command starts a new engine with an empty drawing.`));
|
|
161
|
+
retire(why);
|
|
162
|
+
}
|
|
163
|
+
else
|
|
164
|
+
p.reject(new Error(m.message));
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
const execute = (command) => new Promise((resolve, reject) => {
|
|
168
|
+
if (closed)
|
|
169
|
+
return reject(new Error(`WASM engine is closed${deathReason ? ` (${deathReason})` : ''}`));
|
|
170
|
+
const id = nextId++;
|
|
171
|
+
pending.set(id, { resolve, reject });
|
|
172
|
+
worker.postMessage({ type: 'execute', id, command });
|
|
173
|
+
});
|
|
174
|
+
return {
|
|
175
|
+
execute,
|
|
176
|
+
ping: async (timeoutMs = 10_000) => {
|
|
177
|
+
if (closed)
|
|
178
|
+
return false;
|
|
179
|
+
let timer;
|
|
180
|
+
const timeout = new Promise(r => (timer = setTimeout(() => r(null), timeoutMs)));
|
|
181
|
+
try {
|
|
182
|
+
const res = await Promise.race([execute({ command: 'GetTree', commandVersion: 'v1', transactionID: `ping-${nextId}` }), timeout]);
|
|
183
|
+
const tree = res?.messages.find(m => m?.command === 'Result');
|
|
184
|
+
return !!tree && ((tree.result && typeof tree.result === 'object' && 'tree' in tree.result) || !!tree.structure);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
clearTimeout(timer);
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
close: (reason = 'WASM engine closed') => {
|
|
194
|
+
if (closed)
|
|
195
|
+
return;
|
|
196
|
+
retire(reason);
|
|
197
|
+
},
|
|
198
|
+
get closed() {
|
|
199
|
+
return closed;
|
|
200
|
+
},
|
|
201
|
+
get deathReason() {
|
|
202
|
+
return deathReason;
|
|
203
|
+
},
|
|
204
|
+
version,
|
|
205
|
+
origin,
|
|
206
|
+
dir,
|
|
207
|
+
get memoryMB() {
|
|
208
|
+
return memoryMB;
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=wasm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wasm.js","sourceRoot":"","sources":["../../src/engine/wasm.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { type Client, type EnginePolicy } from './client.js';
|
|
3
|
+
import { type LocalWasmOptions } from './engine/wasm.js';
|
|
4
|
+
import type { BridgeRegistry } from './bridge/server.js';
|
|
5
|
+
export declare const VERSION = "0.1.0";
|
|
6
|
+
export declare const DEFAULT_WS_URL = "ws://0.0.0.0:9094/";
|
|
7
|
+
export type McpServerOptions = {
|
|
8
|
+
/** ClassCAD worker this instance connects to (lazily, on the first tool call). */
|
|
9
|
+
wsUrl: string;
|
|
10
|
+
/** The shared bridge registry (apps announcing share tokens); null when no listener runs. */
|
|
11
|
+
bridge: () => BridgeRegistry | null;
|
|
12
|
+
/** Engine policy when nothing else decides: 'auto' (worker, else local WASM), 'drogon', 'wasm'. Default 'auto'. */
|
|
13
|
+
engine?: EnginePolicy;
|
|
14
|
+
/** Local WASM engine settings (key, origin, …); null = not available. */
|
|
15
|
+
wasm?: LocalWasmOptions | null;
|
|
16
|
+
/** Where the instance reports engine decisions. */
|
|
17
|
+
log?: (msg: string) => void;
|
|
18
|
+
};
|
|
19
|
+
/** Builds a connected-ready MCP server: the engine client plus all tools. Nothing is opened yet. */
|
|
20
|
+
export declare function createMcpServer(opts: McpServerOptions): Promise<{
|
|
21
|
+
server: McpServer;
|
|
22
|
+
client: Client;
|
|
23
|
+
}>;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// mcp-server.ts — one ClassCAD MCP server instance: an engine client plus
|
|
2
|
+
// every tool, wired the same way whether the instance serves a stdio host
|
|
3
|
+
// directly or one HTTP session of the daemon. The daemon creates one of these
|
|
4
|
+
// per MCP session, so every Claude tab has its own engine connection,
|
|
5
|
+
// emission config and tool queue.
|
|
6
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { connect } from './client.js';
|
|
9
|
+
import { DEFAULT_WASM_ORIGIN, DEFAULT_WASM_VERSION, defaultWasmDir } from './engine/wasm.js';
|
|
10
|
+
import { registerLifecycleTools } from './tools/lifecycle.js';
|
|
11
|
+
import { registerStateTools } from './tools/state.js';
|
|
12
|
+
import { DAEMON_NOTE, registerDocsTools, serverInstructions } from './tools/docs.js';
|
|
13
|
+
import { registerSnapshotTool } from './tools/snapshot.js';
|
|
14
|
+
import { registerScriptTool } from './tools/script.js';
|
|
15
|
+
import { registerBridgeTools } from './tools/bridge.js';
|
|
16
|
+
import { serializeTools } from './queue.js';
|
|
17
|
+
import { registerAuthTool, requireSignIn, SIGN_IN_NOTE } from './tools/auth.js';
|
|
18
|
+
import { authStatus } from './auth.js';
|
|
19
|
+
export const VERSION = '0.1.0';
|
|
20
|
+
export const DEFAULT_WS_URL = 'ws://0.0.0.0:9094/';
|
|
21
|
+
/** Builds a connected-ready MCP server: the engine client plus all tools. Nothing is opened yet. */
|
|
22
|
+
export async function createMcpServer(opts) {
|
|
23
|
+
const bridgeRegistry = opts.bridge();
|
|
24
|
+
// Build the client without opening the WS. The first tool call that needs
|
|
25
|
+
// the worker will open it — either use_session (with a named session) or
|
|
26
|
+
// any geometry tool (with no session header). This keeps the MCP passive
|
|
27
|
+
// at startup so it never creates a stray ephemeral session.
|
|
28
|
+
const client = await connect(opts.wsUrl, { graphics: true, bridge: opts.bridge, engine: opts.engine, wasm: opts.wasm, log: opts.log });
|
|
29
|
+
const server = new McpServer({
|
|
30
|
+
name: 'classcad',
|
|
31
|
+
version: VERSION,
|
|
32
|
+
},
|
|
33
|
+
// The initialize-handshake instructions carry the full v1 method index —
|
|
34
|
+
// hosts surface them to the agent, so it knows every method from turn one.
|
|
35
|
+
{ instructions: `${SIGN_IN_NOTE}\n\n${serverInstructions()}` });
|
|
36
|
+
// One tool call at a time: every tool below shares this client's single
|
|
37
|
+
// engine connection and drawing (see queue.ts for the reasoning, incl.
|
|
38
|
+
// subagents). Must run before the first registerTool.
|
|
39
|
+
serializeTools(server);
|
|
40
|
+
// Every tool but the sign-in, status and docs needs a signed-in machine.
|
|
41
|
+
// Registered after the queue patch, so the check runs before queueing.
|
|
42
|
+
requireSignIn(server);
|
|
43
|
+
registerAuthTool(server);
|
|
44
|
+
server.registerTool('session_info', {
|
|
45
|
+
title: 'Session info',
|
|
46
|
+
description: 'Return ClassCAD MCP session status: sign-in (auth), transport (ws = ClassCAD worker, bridge = an app\'s in-page engine, wasm = the MCP\'s own local engine), engine policy, whether a local WASM engine is available, WS URL, current session id, share token, connection state, package version. ' + DAEMON_NOTE,
|
|
47
|
+
inputSchema: {},
|
|
48
|
+
}, async () => {
|
|
49
|
+
const local = client.localEngine;
|
|
50
|
+
const auth = await authStatus();
|
|
51
|
+
const info = {
|
|
52
|
+
auth: auth.signedIn ? { signedIn: true, email: auth.account.email, ...(auth.offline ? { offline: true } : {}) } : { signedIn: false, reason: auth.reason },
|
|
53
|
+
transport: client.transport,
|
|
54
|
+
enginePolicy: client.engine,
|
|
55
|
+
wsUrl: client.transport === 'ws' ? client.url : null,
|
|
56
|
+
sessionId: client.sessionId,
|
|
57
|
+
shareToken: client.shareToken,
|
|
58
|
+
connected: client.connected,
|
|
59
|
+
wasm: {
|
|
60
|
+
available: client.wasmAvailable,
|
|
61
|
+
running: local !== null,
|
|
62
|
+
version: local?.version ?? opts.wasm?.version ?? DEFAULT_WASM_VERSION,
|
|
63
|
+
origin: local?.origin ?? opts.wasm?.origin ?? DEFAULT_WASM_ORIGIN,
|
|
64
|
+
dir: local?.dir ?? opts.wasm?.dir ?? defaultWasmDir(opts.wasm?.version ?? DEFAULT_WASM_VERSION),
|
|
65
|
+
memoryMB: local?.memoryMB ?? null,
|
|
66
|
+
},
|
|
67
|
+
version: VERSION,
|
|
68
|
+
};
|
|
69
|
+
return { content: [{ type: 'text', text: JSON.stringify(info) }] };
|
|
70
|
+
});
|
|
71
|
+
server.registerTool('use_session', {
|
|
72
|
+
title: 'Switch session',
|
|
73
|
+
description: 'Call this BEFORE any other classcad tool when the user names a session or hands you a session/token URL — the MCP opens its engine link lazily, so the first tool call decides which session is used. Modes: (1) sessionId — attaches to a named ClassCAD session (ClassCAD-Session-Id header) on the configured worker; empty/omitted = fresh worker-assigned session. (2) url — a ws(s):// URL from a multi-client server, typically carrying an invite token (wss://host/?invite=…); connected VERBATIM. (3) an http(s):// APP SHARE LINK from a buerli app\'s Session Management: with ?invite=… the token is applied to the configured worker URL (the app runs against a ClassCAD server); with ?bridge=… the app runs the engine IN-PAGE (WASM) and the MCP attaches to it through the bridge listener — every command then runs inside that app, and the app must stay open. Without either, the MCP runs its own session. (4) engine — WHICH ENGINE runs that own session: "drogon" = the configured ClassCAD worker (server), "wasm" = the MCP\'s OWN local engine (the published WASM build hosted in this process, no server and no browser needed; requires a configured key), "auto" (default) = the worker if reachable, otherwise the local engine. Use "drogon" when the user says to connect to their (Drogon/ClassCAD) server, "wasm" when they say WASM/local/offline; a token or URL always decides by itself. Reconnecting clears cached structure/graphic state — the next tool call repopulates it.',
|
|
74
|
+
inputSchema: {
|
|
75
|
+
sessionId: z.string().optional()
|
|
76
|
+
.describe('Target session id (named-session model, worker only). Empty string or omitted = no header (worker-assigned session).'),
|
|
77
|
+
url: z.string().optional()
|
|
78
|
+
.describe('ws(s):// URL to connect to VERBATIM — e.g. a multi-client token/invite URL (wss://host/?invite=…). An http(s):// app share link with ?invite= is accepted too: the invite token is extracted and applied to the configured worker URL. Takes precedence over sessionId.'),
|
|
79
|
+
engine: z.enum(['auto', 'drogon', 'wasm']).optional()
|
|
80
|
+
.describe('Engine for the MCP\'s own session when no url is given: auto (worker, else local WASM), drogon (the ClassCAD worker only), wasm (the local engine only). Sticks for the session until changed.'),
|
|
81
|
+
},
|
|
82
|
+
}, async ({ sessionId, url, engine }) => {
|
|
83
|
+
const target = sessionId && sessionId.length > 0 ? sessionId : null;
|
|
84
|
+
try {
|
|
85
|
+
let wsUrl = url && url.length > 0 ? url : null;
|
|
86
|
+
// App share links (http/https) point at the WEB APP, not the CAD
|
|
87
|
+
// server — carry over only the invite token onto the worker base URL.
|
|
88
|
+
if (wsUrl && /^https?:\/\//i.test(wsUrl)) {
|
|
89
|
+
const params = new URL(wsUrl).searchParams;
|
|
90
|
+
const bridge = params.get('bridge');
|
|
91
|
+
if (bridge) {
|
|
92
|
+
// In-app engine: attach through the bridge, no worker involved.
|
|
93
|
+
await client.reconnectBridge(bridge);
|
|
94
|
+
return {
|
|
95
|
+
content: [{ type: 'text', text: JSON.stringify({ ok: true, transport: 'bridge', shareToken: bridge }) }],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const invite = params.get('invite');
|
|
99
|
+
if (!invite) {
|
|
100
|
+
throw new Error(`"${wsUrl}" is an http(s) app link without an ?invite= or ?bridge= token. ` +
|
|
101
|
+
'Pass the ws(s):// URL of the ClassCAD server (optionally with ?invite=…), or an app share link that carries ?invite= / ?bridge=.');
|
|
102
|
+
}
|
|
103
|
+
const base = client.baseUrl.replace(/\/+$/, '');
|
|
104
|
+
wsUrl = `${base}/?invite=${encodeURIComponent(invite)}`;
|
|
105
|
+
}
|
|
106
|
+
if (wsUrl)
|
|
107
|
+
await client.reconnectUrl(wsUrl);
|
|
108
|
+
else
|
|
109
|
+
await client.reconnect(target, engine);
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
113
|
+
return {
|
|
114
|
+
isError: true,
|
|
115
|
+
content: [{ type: 'text', text: JSON.stringify({ ok: false, error: msg }) }],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
content: [{
|
|
120
|
+
type: 'text',
|
|
121
|
+
text: JSON.stringify({
|
|
122
|
+
ok: true,
|
|
123
|
+
transport: client.transport,
|
|
124
|
+
engine: client.transport === 'wasm' ? 'wasm (local, in this process)' : 'drogon (ClassCAD worker)',
|
|
125
|
+
sessionId: client.sessionId,
|
|
126
|
+
wsUrl: client.transport === 'ws' ? client.url : null,
|
|
127
|
+
}),
|
|
128
|
+
}],
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
registerLifecycleTools(server, client);
|
|
132
|
+
registerStateTools(server, client);
|
|
133
|
+
registerDocsTools(server);
|
|
134
|
+
registerSnapshotTool(server, client);
|
|
135
|
+
registerScriptTool(server, client);
|
|
136
|
+
// Bridge tools (app-state read/write) — only when the listener is up.
|
|
137
|
+
if (bridgeRegistry)
|
|
138
|
+
registerBridgeTools(server, client, bridgeRegistry);
|
|
139
|
+
return { server, client };
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=mcp-server.js.map
|