@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
package/dist/auth.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// auth.ts — the MCP works for signed-in classcad.ch accounts.
|
|
2
|
+
//
|
|
3
|
+
// Sign-in is the loopback flow command-line tools use (gh, gcloud, firebase):
|
|
4
|
+
//
|
|
5
|
+
// 1. beginLogin() opens a one-shot listener on 127.0.0.1:<random port> and
|
|
6
|
+
// returns https://classcad.ch/connect?port=<port>&state=<random>.
|
|
7
|
+
// 2. The user opens that link. classcad.ch signs them in (or up) with the
|
|
8
|
+
// existing Firebase accounts: Google, GitHub, email. Already signed in,
|
|
9
|
+
// it is one "Continue" click.
|
|
10
|
+
// 3. The site sends the browser to http://127.0.0.1:<port>/callback#state=…&token=…
|
|
11
|
+
// The Firebase refresh token rides in the FRAGMENT, which never reaches
|
|
12
|
+
// a server log; the callback page POSTs it to this listener (same
|
|
13
|
+
// origin) and wipes it from the address bar.
|
|
14
|
+
// 4. The MCP exchanges the refresh token at Firebase (proof that the account
|
|
15
|
+
// exists and is enabled), stores it in ~/.classcad-mcp/auth.json and
|
|
16
|
+
// resolves everyone waiting in waitForLogin() — that is how the agent
|
|
17
|
+
// learns "done": the `login` tool it is waiting on returns.
|
|
18
|
+
//
|
|
19
|
+
// Every later use refreshes the token at most once an hour (Firebase records
|
|
20
|
+
// that as the account's last refresh). A rejected token (account disabled or
|
|
21
|
+
// deleted, token revoked) signs the machine out; an unreachable Firebase is
|
|
22
|
+
// tolerated for OFFLINE_GRACE_MS after the last successful check.
|
|
23
|
+
import { createServer } from 'node:http';
|
|
24
|
+
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
25
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
26
|
+
import { homedir } from 'node:os';
|
|
27
|
+
import { dirname, join } from 'node:path';
|
|
28
|
+
/** Firebase project whose accounts may use the MCP (classcad.ch / buerli.io accounts). */
|
|
29
|
+
const PROJECT = process.env.CLASSCAD_AUTH_PROJECT || 'buerli';
|
|
30
|
+
/** The project's public web API key (the same one classcad.ch ships). */
|
|
31
|
+
const API_KEY = process.env.CLASSCAD_AUTH_API_KEY || 'AIzaSyCzKhVOXJOwLtmK9SjCoEEIkZ6N7ilSp9E';
|
|
32
|
+
const TOKEN_URL = process.env.CLASSCAD_AUTH_TOKEN_URL || 'https://securetoken.googleapis.com/v1/token';
|
|
33
|
+
/** The page that signs the user in and hands the token back. */
|
|
34
|
+
const LOGIN_URL = process.env.CLASSCAD_AUTH_URL || 'https://classcad.ch/connect';
|
|
35
|
+
const AUTH_FILE = process.env.CLASSCAD_AUTH_FILE || join(homedir(), '.classcad-mcp', 'auth.json');
|
|
36
|
+
const OFFLINE_GRACE_MS = 14 * 24 * 3600 * 1000;
|
|
37
|
+
const LOGIN_TTL_MS = 15 * 60 * 1000;
|
|
38
|
+
/** The token was refused by Firebase: the sign-in is gone for good. */
|
|
39
|
+
class Rejected extends Error {
|
|
40
|
+
}
|
|
41
|
+
// ─── storage ───────────────────────────────────────────────────────────────
|
|
42
|
+
function readStored() {
|
|
43
|
+
try {
|
|
44
|
+
const s = JSON.parse(readFileSync(AUTH_FILE, 'utf8'));
|
|
45
|
+
return s.refreshToken && s.project === PROJECT ? s : null;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function writeStored(s) {
|
|
52
|
+
mkdirSync(dirname(AUTH_FILE), { recursive: true });
|
|
53
|
+
writeFileSync(AUTH_FILE, JSON.stringify(s, null, 2), { mode: 0o600 });
|
|
54
|
+
}
|
|
55
|
+
// ─── verification ──────────────────────────────────────────────────────────
|
|
56
|
+
/** Firebase's answers for a refresh token that will never work again. */
|
|
57
|
+
const DEAD_TOKEN = /^(TOKEN_EXPIRED|USER_DISABLED|USER_NOT_FOUND|INVALID_REFRESH_TOKEN|MISSING_REFRESH_TOKEN|PROJECT_NUMBER_MISMATCH)\b/;
|
|
58
|
+
/** Exchanges a refresh token at Firebase. Throws Rejected for a dead token, anything else for "unreachable/misconfigured". */
|
|
59
|
+
async function exchange(refreshToken) {
|
|
60
|
+
const res = await fetch(`${TOKEN_URL}?key=${encodeURIComponent(API_KEY)}`, {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
// The web API key only serves the sites it is restricted to; the MCP
|
|
63
|
+
// speaks for the sign-in page's site.
|
|
64
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded', referer: `${new URL(LOGIN_URL).origin}/` },
|
|
65
|
+
body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken }),
|
|
66
|
+
signal: AbortSignal.timeout(15000),
|
|
67
|
+
});
|
|
68
|
+
const body = (await res.json().catch(() => ({})));
|
|
69
|
+
const message = String(body?.error?.message ?? `HTTP ${res.status}`);
|
|
70
|
+
if (DEAD_TOKEN.test(message))
|
|
71
|
+
throw new Rejected(message);
|
|
72
|
+
if (!res.ok)
|
|
73
|
+
throw new Error(`Firebase answered ${res.status}: ${message}`);
|
|
74
|
+
const claims = JSON.parse(Buffer.from(String(body.id_token).split('.')[1] ?? '', 'base64url').toString('utf8') || '{}');
|
|
75
|
+
if (claims.aud !== PROJECT)
|
|
76
|
+
throw new Rejected(`token belongs to project "${claims.aud}", not "${PROJECT}"`);
|
|
77
|
+
return {
|
|
78
|
+
account: { uid: claims.user_id ?? claims.sub ?? body.user_id, email: claims.email ?? null, name: claims.name ?? null },
|
|
79
|
+
refreshToken: body.refresh_token ?? refreshToken,
|
|
80
|
+
expiresAt: Date.now() + Number(body.expires_in ?? 3600) * 1000,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
let verifiedUntil = 0;
|
|
84
|
+
let checking = null;
|
|
85
|
+
/** The current sign-in, checked against Firebase at most once per token lifetime. */
|
|
86
|
+
export function authStatus() {
|
|
87
|
+
const stored = readStored();
|
|
88
|
+
if (!stored) {
|
|
89
|
+
verifiedUntil = 0;
|
|
90
|
+
return Promise.resolve({ signedIn: false, reason: 'not signed in' });
|
|
91
|
+
}
|
|
92
|
+
const account = { uid: stored.uid, email: stored.email, name: stored.name };
|
|
93
|
+
if (Date.now() < verifiedUntil)
|
|
94
|
+
return Promise.resolve({ signedIn: true, account });
|
|
95
|
+
checking ??= (async () => {
|
|
96
|
+
try {
|
|
97
|
+
const r = await exchange(stored.refreshToken);
|
|
98
|
+
writeStored({ ...stored, ...r.account, refreshToken: r.refreshToken, verifiedAt: Date.now() });
|
|
99
|
+
verifiedUntil = r.expiresAt - 60_000;
|
|
100
|
+
return { signedIn: true, account: r.account };
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
if (err instanceof Rejected) {
|
|
104
|
+
rmSync(AUTH_FILE, { force: true });
|
|
105
|
+
return { signedIn: false, reason: `the sign-in is no longer valid (${err.message}); sign in again` };
|
|
106
|
+
}
|
|
107
|
+
if (Date.now() - stored.verifiedAt < OFFLINE_GRACE_MS)
|
|
108
|
+
return { signedIn: true, account, offline: true };
|
|
109
|
+
return { signedIn: false, reason: `could not reach Firebase to confirm the sign-in (${err.message})` };
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
checking = null;
|
|
113
|
+
}
|
|
114
|
+
})();
|
|
115
|
+
return checking;
|
|
116
|
+
}
|
|
117
|
+
/** Forgets the sign-in on this machine. */
|
|
118
|
+
export function logout() {
|
|
119
|
+
verifiedUntil = 0;
|
|
120
|
+
const had = existsSync(AUTH_FILE);
|
|
121
|
+
rmSync(AUTH_FILE, { force: true });
|
|
122
|
+
return had;
|
|
123
|
+
}
|
|
124
|
+
let pending = null;
|
|
125
|
+
/**
|
|
126
|
+
* Starts a sign-in (or returns the one already waiting): the link the user
|
|
127
|
+
* has to open. `client` names the host on the page ("Claude Code").
|
|
128
|
+
*/
|
|
129
|
+
export async function beginLogin(client) {
|
|
130
|
+
if (pending && Date.now() < pending.expiresAt)
|
|
131
|
+
return { url: pending.url, expiresAt: pending.expiresAt };
|
|
132
|
+
const state = randomBytes(24).toString('base64url');
|
|
133
|
+
let resolve;
|
|
134
|
+
let reject;
|
|
135
|
+
const done = new Promise((res, rej) => ((resolve = res), (reject = rej)));
|
|
136
|
+
done.catch(() => { });
|
|
137
|
+
const server = createServer((req, res) => handle(req, res).catch(err => send(res, 500, { ok: false, error: String(err?.message ?? err) })));
|
|
138
|
+
await new Promise((res, rej) => server.once('error', rej).listen(0, '127.0.0.1', () => res()));
|
|
139
|
+
const port = server.address().port;
|
|
140
|
+
const q = new URLSearchParams({ port: String(port), state });
|
|
141
|
+
if (client)
|
|
142
|
+
q.set('client', client);
|
|
143
|
+
const p = { url: `${LOGIN_URL}?${q}`, state, port, server, expiresAt: Date.now() + LOGIN_TTL_MS, done, resolve, reject };
|
|
144
|
+
pending = p;
|
|
145
|
+
const timer = setTimeout(() => finish(p, new Error('the sign-in link expired')), LOGIN_TTL_MS);
|
|
146
|
+
timer.unref();
|
|
147
|
+
server.unref();
|
|
148
|
+
return { url: p.url, expiresAt: p.expiresAt };
|
|
149
|
+
async function handle(req, res) {
|
|
150
|
+
// Only this listener's own address: no DNS rebinding, no other sites.
|
|
151
|
+
if (req.headers.host !== `127.0.0.1:${port}`)
|
|
152
|
+
return send(res, 403, { ok: false, error: 'forbidden' });
|
|
153
|
+
const path = new URL(req.url ?? '/', `http://127.0.0.1:${port}`).pathname;
|
|
154
|
+
if (req.method === 'GET' && path === '/callback') {
|
|
155
|
+
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store', 'referrer-policy': 'no-referrer' });
|
|
156
|
+
return res.end(callbackPage(client ?? 'your agent'));
|
|
157
|
+
}
|
|
158
|
+
if (req.method === 'POST' && path === '/complete') {
|
|
159
|
+
if (req.headers.origin !== `http://127.0.0.1:${port}`)
|
|
160
|
+
return send(res, 403, { ok: false, error: 'forbidden' });
|
|
161
|
+
const body = JSON.parse((await readBody(req)) || '{}');
|
|
162
|
+
if (!sameSecret(String(body.state ?? ''), state))
|
|
163
|
+
return send(res, 400, { ok: false, error: 'This link does not belong to the waiting sign-in. Ask your agent for a new one.' });
|
|
164
|
+
if (pending !== p)
|
|
165
|
+
return send(res, 410, { ok: false, error: 'This sign-in is already finished.' });
|
|
166
|
+
try {
|
|
167
|
+
const r = await exchange(String(body.token ?? ''));
|
|
168
|
+
writeStored({ ...r.account, refreshToken: r.refreshToken, project: PROJECT, verifiedAt: Date.now() });
|
|
169
|
+
verifiedUntil = r.expiresAt - 60_000;
|
|
170
|
+
send(res, 200, { ok: true, email: r.account.email, name: r.account.name });
|
|
171
|
+
finish(p, r.account);
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
send(res, 400, { ok: false, error: `Sign-in was refused: ${err.message}` });
|
|
175
|
+
}
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
send(res, 404, { ok: false, error: 'not found' });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function finish(p, result) {
|
|
182
|
+
if (pending !== p)
|
|
183
|
+
return;
|
|
184
|
+
pending = null;
|
|
185
|
+
if (result instanceof Error)
|
|
186
|
+
p.reject(result);
|
|
187
|
+
else
|
|
188
|
+
p.resolve(result);
|
|
189
|
+
// Let the callback page's response flush, then stop listening.
|
|
190
|
+
setTimeout(() => p.server.close(), 1000).unref();
|
|
191
|
+
}
|
|
192
|
+
/** The link of a sign-in that is still waiting, if any. */
|
|
193
|
+
export function pendingLogin() {
|
|
194
|
+
return pending && Date.now() < pending.expiresAt ? { url: pending.url, expiresAt: pending.expiresAt } : null;
|
|
195
|
+
}
|
|
196
|
+
/** Waits up to `ms` for the waiting sign-in. null = still waiting (or none started). */
|
|
197
|
+
export async function waitForLogin(ms) {
|
|
198
|
+
const p = pending;
|
|
199
|
+
if (!p)
|
|
200
|
+
return null;
|
|
201
|
+
let t;
|
|
202
|
+
const timeout = new Promise(res => (t = setTimeout(() => res(null), ms)));
|
|
203
|
+
try {
|
|
204
|
+
return await Promise.race([p.done, timeout]);
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
clearTimeout(t);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// ─── helpers ───────────────────────────────────────────────────────────────
|
|
211
|
+
function sameSecret(a, b) {
|
|
212
|
+
const x = Buffer.from(a);
|
|
213
|
+
const y = Buffer.from(b);
|
|
214
|
+
return x.length === y.length && timingSafeEqual(x, y);
|
|
215
|
+
}
|
|
216
|
+
function readBody(req) {
|
|
217
|
+
return new Promise((resolve, reject) => {
|
|
218
|
+
let data = '';
|
|
219
|
+
req.setEncoding('utf8');
|
|
220
|
+
req.on('data', c => {
|
|
221
|
+
data += c;
|
|
222
|
+
if (data.length > 64 * 1024)
|
|
223
|
+
req.destroy(new Error('body too large'));
|
|
224
|
+
});
|
|
225
|
+
req.on('end', () => resolve(data));
|
|
226
|
+
req.on('error', reject);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
function send(res, status, body) {
|
|
230
|
+
if (res.headersSent)
|
|
231
|
+
return;
|
|
232
|
+
res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' });
|
|
233
|
+
res.end(JSON.stringify(body));
|
|
234
|
+
}
|
|
235
|
+
const escapeHtml = (s) => s.replace(/[&<>"']/g, c => `&#${c.charCodeAt(0)};`);
|
|
236
|
+
/** The page classcad.ch hands the browser to: posts the fragment to this listener, then says what happened. */
|
|
237
|
+
function callbackPage(client) {
|
|
238
|
+
const who = escapeHtml(client);
|
|
239
|
+
return `<!doctype html>
|
|
240
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
241
|
+
<title>ClassCAD · connecting</title>
|
|
242
|
+
<style>
|
|
243
|
+
:root { color-scheme: light dark; --bg: #f6f5f2; --fg: #16161a; --muted: #6b6b73; --ok: #127a3a; --bad: #b3261e; }
|
|
244
|
+
@media (prefers-color-scheme: dark) { :root { --bg: #111114; --fg: #ececf0; --muted: #9a9aa3; --ok: #5bd48a; --bad: #ff8a80; } }
|
|
245
|
+
body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: var(--bg); color: var(--fg);
|
|
246
|
+
font: 16px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; padding: 0 16px; }
|
|
247
|
+
main { max-width: 28rem; text-align: center; }
|
|
248
|
+
h1 { font-size: 1.5rem; margin: 0 0 .5rem; }
|
|
249
|
+
p { margin: .25rem 0; color: var(--muted); }
|
|
250
|
+
.ok { color: var(--ok); } .bad { color: var(--bad); }
|
|
251
|
+
</style></head>
|
|
252
|
+
<body><main><h1 id="t">Connecting ClassCAD …</h1><p id="m">One moment.</p></main>
|
|
253
|
+
<script>
|
|
254
|
+
(async () => {
|
|
255
|
+
const t = document.getElementById('t'), m = document.getElementById('m')
|
|
256
|
+
const q = new URLSearchParams(location.hash.slice(1))
|
|
257
|
+
history.replaceState(null, '', location.pathname)
|
|
258
|
+
try {
|
|
259
|
+
const r = await fetch('/complete', { method: 'POST', headers: { 'content-type': 'application/json' },
|
|
260
|
+
body: JSON.stringify({ state: q.get('state'), token: q.get('token') }) })
|
|
261
|
+
const b = await r.json()
|
|
262
|
+
if (!b.ok) throw new Error(b.error)
|
|
263
|
+
t.textContent = 'You are signed in.'; t.className = 'ok'
|
|
264
|
+
m.textContent = 'Signed in as ' + (b.email || b.name || 'your account') + '. You can close this tab and go back to ${who}.'
|
|
265
|
+
} catch (e) {
|
|
266
|
+
t.textContent = 'That did not work.'; t.className = 'bad'
|
|
267
|
+
m.textContent = (e && e.message) || 'Ask ${who} for a new sign-in link.'
|
|
268
|
+
}
|
|
269
|
+
})()
|
|
270
|
+
</script></body></html>`;
|
|
271
|
+
}
|
|
272
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/auth.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export declare const PROTOCOL_VERSION = 1;
|
|
2
|
+
export type Capability = 'engine.execute' | 'selection.read' | 'selection.write';
|
|
3
|
+
export type RawSelection = {
|
|
4
|
+
containerId: number;
|
|
5
|
+
graphicId: number;
|
|
6
|
+
prodRefId: number;
|
|
7
|
+
};
|
|
8
|
+
export type SelectionEntity = {
|
|
9
|
+
kind: 'face' | 'edge' | 'vertex' | 'curve' | 'unknown';
|
|
10
|
+
classcadId?: number;
|
|
11
|
+
position?: {
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
z: number;
|
|
15
|
+
};
|
|
16
|
+
normal?: {
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
z: number;
|
|
20
|
+
};
|
|
21
|
+
graphicType?: string;
|
|
22
|
+
raw: RawSelection;
|
|
23
|
+
};
|
|
24
|
+
export type ShareKind = 'invite' | 'bridge';
|
|
25
|
+
export type AnnounceMessage = {
|
|
26
|
+
type: 'announce';
|
|
27
|
+
protocolVersion: number;
|
|
28
|
+
/** The share token this connection serves (one bridge connection per token). */
|
|
29
|
+
token: string;
|
|
30
|
+
kind: ShareKind;
|
|
31
|
+
drawingId: string;
|
|
32
|
+
app: string;
|
|
33
|
+
appVersion?: string;
|
|
34
|
+
capabilities: Capability[];
|
|
35
|
+
/** Stable per-connection id so the MCP can tell apart several apps for one token. */
|
|
36
|
+
clientId: string;
|
|
37
|
+
/** Legacy (pre-token) announce field; treated as the token when `token` is absent. */
|
|
38
|
+
sessionId?: string;
|
|
39
|
+
/** Legacy modeler announce field; treated as an 'invite' token when `token` is absent. */
|
|
40
|
+
invite?: string;
|
|
41
|
+
};
|
|
42
|
+
export type RequestMessage = {
|
|
43
|
+
type: 'request';
|
|
44
|
+
id: number;
|
|
45
|
+
method: BridgeMethod;
|
|
46
|
+
params?: unknown;
|
|
47
|
+
};
|
|
48
|
+
export type ResponseMessage = {
|
|
49
|
+
type: 'response';
|
|
50
|
+
id: number;
|
|
51
|
+
result?: unknown;
|
|
52
|
+
error?: {
|
|
53
|
+
code: string;
|
|
54
|
+
message: string;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
export type EventMessage = {
|
|
58
|
+
type: 'event';
|
|
59
|
+
channel: BridgeEventChannel;
|
|
60
|
+
payload: unknown;
|
|
61
|
+
};
|
|
62
|
+
export type BridgeEnvelope = AnnounceMessage | RequestMessage | ResponseMessage | EventMessage;
|
|
63
|
+
/** Methods the MCP calls on the app. */
|
|
64
|
+
export type BridgeMethod = 'engine.execute' | 'session.attached' | 'session.detached' | 'selection.get' | 'selection.set';
|
|
65
|
+
/** engine.execute reply: the engine's text messages and inflated binary graphic packages, in emission order. */
|
|
66
|
+
export type EngineExecuteResult = {
|
|
67
|
+
messages: Record<string, any>[];
|
|
68
|
+
binaryMessages: Record<string, any>[];
|
|
69
|
+
/** Local WASM engine only: emitted messages that could not be decoded (dropped, never thrown into the engine). */
|
|
70
|
+
decodeErrors?: string[];
|
|
71
|
+
};
|
|
72
|
+
export type SessionAttachedParams = {
|
|
73
|
+
peerId: string;
|
|
74
|
+
role?: 'edit' | 'view';
|
|
75
|
+
client?: string;
|
|
76
|
+
};
|
|
77
|
+
export type SelectionGetResult = SelectionEntity[];
|
|
78
|
+
export type SelectionSetParams = {
|
|
79
|
+
items: Array<RawSelection | {
|
|
80
|
+
classcadId: number;
|
|
81
|
+
}>;
|
|
82
|
+
replace?: boolean;
|
|
83
|
+
};
|
|
84
|
+
export type BridgeEventChannel = 'selection.changed';
|
|
85
|
+
export type SelectionChangedPayload = {
|
|
86
|
+
items: SelectionEntity[];
|
|
87
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// bridge/protocol.ts — wire protocol between a buerli app and this MCP.
|
|
2
|
+
//
|
|
3
|
+
// Mirror of @buerli.io/classcad's src/bridge/protocol.ts (source of truth
|
|
4
|
+
// there). Apps open an OUTBOUND WebSocket to the MCP's bridge listener and
|
|
5
|
+
// announce a share token. Two kinds:
|
|
6
|
+
// • 'invite' — the app runs against a ClassCAD server; the MCP joins the
|
|
7
|
+
// server session with the token (use_session) and uses the bridge for
|
|
8
|
+
// app state (selection) only.
|
|
9
|
+
// • 'bridge' — the engine runs inside the app (WASM); the MCP relays its
|
|
10
|
+
// engine commands over this link (engine.execute) — the bridge IS the
|
|
11
|
+
// engine transport.
|
|
12
|
+
// Plain TS, importable from Node and the browser.
|
|
13
|
+
export const PROTOCOL_VERSION = 1;
|
|
14
|
+
//# sourceMappingURL=protocol.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../src/bridge/protocol.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,EAAE;AACF,0EAA0E;AAC1E,2EAA2E;AAC3E,qCAAqC;AACrC,2EAA2E;AAC3E,0EAA0E;AAC1E,kCAAkC;AAClC,2EAA2E;AAC3E,0EAA0E;AAC1E,wBAAwB;AACxB,kDAAkD;AAElD,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAA"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { WebSocket } from 'ws';
|
|
2
|
+
import type { BridgeMethod, SelectionEntity, ShareKind } from './protocol.js';
|
|
3
|
+
import { PROTOCOL_VERSION } from './protocol.js';
|
|
4
|
+
export type AppConnection = {
|
|
5
|
+
clientId: string;
|
|
6
|
+
/** The share token this connection serves. */
|
|
7
|
+
token: string;
|
|
8
|
+
kind: ShareKind;
|
|
9
|
+
/** @deprecated alias of `token` (pre-token announces). */
|
|
10
|
+
sessionId: string;
|
|
11
|
+
drawingId: string;
|
|
12
|
+
app: string;
|
|
13
|
+
appVersion?: string;
|
|
14
|
+
capabilities: string[];
|
|
15
|
+
protocolVersion: number;
|
|
16
|
+
announcedAt: number;
|
|
17
|
+
request: <T = unknown>(method: BridgeMethod, params?: unknown) => Promise<T>;
|
|
18
|
+
onClose: (cb: () => void) => () => void;
|
|
19
|
+
cachedSelection: SelectionEntity[] | null;
|
|
20
|
+
socket: WebSocket;
|
|
21
|
+
};
|
|
22
|
+
export type BridgeRegistry = {
|
|
23
|
+
list: (token?: string) => AppConnection[];
|
|
24
|
+
pick: (token: string, clientId?: string) => AppConnection | null;
|
|
25
|
+
waitFor: (token: string, timeoutMs?: number) => Promise<AppConnection | null>;
|
|
26
|
+
onSelectionChanged: (sessionId: string, cb: (items: SelectionEntity[]) => void) => () => void;
|
|
27
|
+
close: () => Promise<void>;
|
|
28
|
+
readonly url: string;
|
|
29
|
+
};
|
|
30
|
+
export type StartBridgeOptions = {
|
|
31
|
+
listen?: string;
|
|
32
|
+
log?: (msg: string) => void;
|
|
33
|
+
};
|
|
34
|
+
export declare function startBridgeServer(opts?: StartBridgeOptions): Promise<BridgeRegistry>;
|
|
35
|
+
export { PROTOCOL_VERSION };
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// bridge/server.ts — WebSocket listener for CC apps to opt into.
|
|
2
|
+
//
|
|
3
|
+
// Apps connect outbound to ws://127.0.0.1:9096/bridge (default) and send an
|
|
4
|
+
// `announce` with a share token. The MCP keeps a registry of these
|
|
5
|
+
// connections keyed by clientId and indexed by token. Two uses:
|
|
6
|
+
// • kind 'invite' — app-state bridge (selection) next to a server session
|
|
7
|
+
// the MCP joined with the same token.
|
|
8
|
+
// • kind 'bridge' — the engine transport itself for in-app (WASM) engines:
|
|
9
|
+
// client.ts relays every engine command through `engine.execute`.
|
|
10
|
+
import { WebSocketServer } from 'ws';
|
|
11
|
+
import { PROTOCOL_VERSION } from './protocol.js';
|
|
12
|
+
const REQUEST_TIMEOUT = 60_000; // app-side picks can be long
|
|
13
|
+
export async function startBridgeServer(opts = {}) {
|
|
14
|
+
const listen = opts.listen ?? 'ws://127.0.0.1:9096/bridge';
|
|
15
|
+
const url = new URL(listen);
|
|
16
|
+
// Bind ONE address family explicitly. 'localhost' resolves to ::1 or
|
|
17
|
+
// 127.0.0.1 depending on the platform, so two processes could both hold
|
|
18
|
+
// "localhost:9096" and apps would land on either — the reason the MCP
|
|
19
|
+
// runs as a single daemon in the first place.
|
|
20
|
+
const host = (url.hostname || '127.0.0.1') === 'localhost' ? '127.0.0.1' : url.hostname;
|
|
21
|
+
const port = Number(url.port || '9096');
|
|
22
|
+
const path = url.pathname || '/bridge';
|
|
23
|
+
const wss = new WebSocketServer({ host, port, path });
|
|
24
|
+
const connections = new Map(); // clientId → conn
|
|
25
|
+
const selectionListeners = new Map();
|
|
26
|
+
// Resolvers waiting for a token to be announced (waitFor).
|
|
27
|
+
const waiters = new Map();
|
|
28
|
+
wss.on('connection', (socket) => {
|
|
29
|
+
let conn = null;
|
|
30
|
+
const pending = new Map();
|
|
31
|
+
const closeListeners = new Set();
|
|
32
|
+
const sendRaw = (env) => {
|
|
33
|
+
try {
|
|
34
|
+
socket.send(JSON.stringify(env));
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
// socket may already be closing; safe to ignore
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
let nextId = 1;
|
|
41
|
+
const request = (method, params) => {
|
|
42
|
+
const id = nextId++;
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const timer = setTimeout(() => {
|
|
45
|
+
if (pending.has(id)) {
|
|
46
|
+
pending.delete(id);
|
|
47
|
+
reject(new Error(`bridge request timeout (${REQUEST_TIMEOUT}ms): ${method}`));
|
|
48
|
+
}
|
|
49
|
+
}, REQUEST_TIMEOUT);
|
|
50
|
+
pending.set(id, { resolve: resolve, reject, timer });
|
|
51
|
+
sendRaw({ type: 'request', id, method, params });
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
socket.on('message', (data) => {
|
|
55
|
+
let env;
|
|
56
|
+
try {
|
|
57
|
+
env = JSON.parse(data.toString());
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (env.type === 'announce') {
|
|
63
|
+
const a = env;
|
|
64
|
+
if (conn)
|
|
65
|
+
return; // ignore re-announce on the same socket
|
|
66
|
+
// Token: current apps send `token` + `kind`; the modeler used to send
|
|
67
|
+
// its Drogon invite as `invite`, older builds a `sessionId`.
|
|
68
|
+
const token = a.token ?? a.invite ?? a.sessionId;
|
|
69
|
+
if (!token)
|
|
70
|
+
return;
|
|
71
|
+
const kind = a.kind ?? 'invite';
|
|
72
|
+
conn = {
|
|
73
|
+
clientId: a.clientId,
|
|
74
|
+
token,
|
|
75
|
+
kind,
|
|
76
|
+
sessionId: token,
|
|
77
|
+
drawingId: a.drawingId,
|
|
78
|
+
app: a.app,
|
|
79
|
+
appVersion: a.appVersion,
|
|
80
|
+
capabilities: a.capabilities,
|
|
81
|
+
protocolVersion: a.protocolVersion,
|
|
82
|
+
announcedAt: Date.now(),
|
|
83
|
+
request,
|
|
84
|
+
onClose: (cb) => {
|
|
85
|
+
closeListeners.add(cb);
|
|
86
|
+
return () => {
|
|
87
|
+
closeListeners.delete(cb);
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
cachedSelection: null,
|
|
91
|
+
socket,
|
|
92
|
+
};
|
|
93
|
+
connections.set(a.clientId, conn);
|
|
94
|
+
opts.log?.(`app ${a.clientId} (${a.app}) announced token ${token} [${a.kind ?? 'bridge'}]; ${connections.size} app(s)`);
|
|
95
|
+
const waiting = waiters.get(token);
|
|
96
|
+
if (waiting) {
|
|
97
|
+
waiters.delete(token);
|
|
98
|
+
for (const resolve of waiting)
|
|
99
|
+
resolve(conn);
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (env.type === 'response') {
|
|
104
|
+
const r = env;
|
|
105
|
+
const p = pending.get(r.id);
|
|
106
|
+
if (!p)
|
|
107
|
+
return;
|
|
108
|
+
pending.delete(r.id);
|
|
109
|
+
if (p.timer)
|
|
110
|
+
clearTimeout(p.timer);
|
|
111
|
+
if (r.error) {
|
|
112
|
+
p.reject(new Error(`${r.error.code}: ${r.error.message}`));
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
p.resolve(r.result);
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (env.type === 'event' && conn) {
|
|
120
|
+
const e = env;
|
|
121
|
+
if (e.channel === 'selection.changed') {
|
|
122
|
+
const payload = (e.payload ?? {});
|
|
123
|
+
const items = payload.items ?? [];
|
|
124
|
+
conn.cachedSelection = items;
|
|
125
|
+
const subs = selectionListeners.get(conn.sessionId);
|
|
126
|
+
if (subs)
|
|
127
|
+
for (const cb of subs)
|
|
128
|
+
try {
|
|
129
|
+
cb(items);
|
|
130
|
+
}
|
|
131
|
+
catch { }
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
socket.on('close', () => {
|
|
137
|
+
// reject any in-flight requests
|
|
138
|
+
for (const [, p] of pending) {
|
|
139
|
+
try {
|
|
140
|
+
p.reject(new Error('bridge socket closed'));
|
|
141
|
+
}
|
|
142
|
+
catch { }
|
|
143
|
+
if (p.timer)
|
|
144
|
+
clearTimeout(p.timer);
|
|
145
|
+
}
|
|
146
|
+
pending.clear();
|
|
147
|
+
if (conn) {
|
|
148
|
+
connections.delete(conn.clientId);
|
|
149
|
+
opts.log?.(`app ${conn.clientId} (${conn.app}) gone (token ${conn.token}); ${connections.size} app(s)`);
|
|
150
|
+
}
|
|
151
|
+
for (const cb of closeListeners)
|
|
152
|
+
try {
|
|
153
|
+
cb();
|
|
154
|
+
}
|
|
155
|
+
catch { }
|
|
156
|
+
closeListeners.clear();
|
|
157
|
+
});
|
|
158
|
+
socket.on('error', () => { });
|
|
159
|
+
});
|
|
160
|
+
// wait for listening, but don't block other startup if the port is busy —
|
|
161
|
+
// surface the error to the caller.
|
|
162
|
+
await new Promise((resolve, reject) => {
|
|
163
|
+
wss.once('listening', () => resolve());
|
|
164
|
+
wss.once('error', (err) => reject(err));
|
|
165
|
+
});
|
|
166
|
+
return {
|
|
167
|
+
url: `ws://${host}:${port}${path}`,
|
|
168
|
+
list: (token) => {
|
|
169
|
+
const all = [...connections.values()];
|
|
170
|
+
return token ? all.filter(c => c.token === token) : all;
|
|
171
|
+
},
|
|
172
|
+
pick: (token, clientId) => {
|
|
173
|
+
if (clientId)
|
|
174
|
+
return connections.get(clientId) ?? null;
|
|
175
|
+
const matches = [...connections.values()].filter(c => c.token === token);
|
|
176
|
+
if (matches.length === 0)
|
|
177
|
+
return null;
|
|
178
|
+
// most recently announced wins
|
|
179
|
+
matches.sort((a, b) => b.announcedAt - a.announcedAt);
|
|
180
|
+
return matches[0];
|
|
181
|
+
},
|
|
182
|
+
waitFor: (token, timeoutMs = 5_000) => {
|
|
183
|
+
const now = [...connections.values()].filter(c => c.token === token).sort((a, b) => b.announcedAt - a.announcedAt)[0];
|
|
184
|
+
if (now)
|
|
185
|
+
return Promise.resolve(now);
|
|
186
|
+
return new Promise(resolve => {
|
|
187
|
+
let set = waiters.get(token);
|
|
188
|
+
if (!set) {
|
|
189
|
+
set = new Set();
|
|
190
|
+
waiters.set(token, set);
|
|
191
|
+
}
|
|
192
|
+
const target = set;
|
|
193
|
+
const cb = (c) => {
|
|
194
|
+
clearTimeout(timer);
|
|
195
|
+
resolve(c);
|
|
196
|
+
};
|
|
197
|
+
const timer = setTimeout(() => {
|
|
198
|
+
target.delete(cb);
|
|
199
|
+
if (target.size === 0)
|
|
200
|
+
waiters.delete(token);
|
|
201
|
+
resolve(null);
|
|
202
|
+
}, timeoutMs);
|
|
203
|
+
target.add(cb);
|
|
204
|
+
});
|
|
205
|
+
},
|
|
206
|
+
onSelectionChanged: (sessionId, cb) => {
|
|
207
|
+
let set = selectionListeners.get(sessionId);
|
|
208
|
+
if (!set) {
|
|
209
|
+
set = new Set();
|
|
210
|
+
selectionListeners.set(sessionId, set);
|
|
211
|
+
}
|
|
212
|
+
set.add(cb);
|
|
213
|
+
return () => {
|
|
214
|
+
const s = selectionListeners.get(sessionId);
|
|
215
|
+
if (!s)
|
|
216
|
+
return;
|
|
217
|
+
s.delete(cb);
|
|
218
|
+
if (s.size === 0)
|
|
219
|
+
selectionListeners.delete(sessionId);
|
|
220
|
+
};
|
|
221
|
+
},
|
|
222
|
+
close: async () => {
|
|
223
|
+
for (const [, conn] of connections) {
|
|
224
|
+
try {
|
|
225
|
+
conn.socket.close();
|
|
226
|
+
}
|
|
227
|
+
catch { }
|
|
228
|
+
}
|
|
229
|
+
connections.clear();
|
|
230
|
+
await new Promise((resolve) => wss.close(() => resolve()));
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
// re-export PROTOCOL_VERSION for callers that want to log compat info
|
|
235
|
+
export { PROTOCOL_VERSION };
|
|
236
|
+
//# sourceMappingURL=server.js.map
|