@classcad/mcp 0.2.1 → 0.3.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/README.md +128 -87
- package/dist/app/SOURCE.txt +4 -0
- package/dist/app/assets/index-54a14bc0.css +8 -0
- package/dist/app/assets/index-c650c783.js +11257 -0
- package/dist/app/assets/troika-three-text.esm-188d86b5.js +220 -0
- package/dist/app/favicon.svg +98 -0
- package/dist/app/index.html +19 -0
- package/dist/app/manifest.json +14 -0
- package/dist/auth.d.ts +8 -0
- package/dist/auth.js +49 -10
- package/dist/auth.js.map +1 -1
- package/dist/backend.d.ts +8 -0
- package/dist/backend.js +39 -0
- package/dist/backend.js.map +1 -0
- package/dist/client.d.ts +48 -11
- package/dist/client.js +152 -167
- package/dist/client.js.map +1 -1
- package/dist/daemon.d.ts +0 -2
- package/dist/daemon.js +77 -62
- package/dist/daemon.js.map +1 -1
- package/dist/engine/key.d.ts +29 -0
- package/dist/engine/key.js +145 -0
- package/dist/engine/key.js.map +1 -0
- package/dist/engine/wasm-worker.js +7 -7
- package/dist/engine/wasm-worker.js.map +1 -1
- package/dist/engine/wasm.d.ts +16 -14
- package/dist/engine/wasm.js +22 -30
- package/dist/engine/wasm.js.map +1 -1
- package/dist/mcp-server.d.ts +4 -4
- package/dist/mcp-server.js +72 -28
- package/dist/mcp-server.js.map +1 -1
- package/dist/queue.js +1 -1
- package/dist/queue.js.map +1 -1
- package/dist/server.js +69 -36
- package/dist/server.js.map +1 -1
- package/dist/share/hub.d.ts +70 -0
- package/dist/share/hub.js +494 -0
- package/dist/share/hub.js.map +1 -0
- package/dist/share/protocol.d.ts +87 -0
- package/dist/share/protocol.js +54 -0
- package/dist/share/protocol.js.map +1 -0
- package/dist/share/relay.d.ts +45 -0
- package/dist/share/relay.js +230 -0
- package/dist/share/relay.js.map +1 -0
- package/dist/share/server.d.ts +27 -0
- package/dist/share/server.js +399 -0
- package/dist/share/server.js.map +1 -0
- package/dist/share/session.d.ts +120 -0
- package/dist/share/session.js +336 -0
- package/dist/share/session.js.map +1 -0
- package/dist/tools/auth.js +1 -1
- package/dist/tools/auth.js.map +1 -1
- package/dist/tools/docs.js +3 -2
- package/dist/tools/docs.js.map +1 -1
- package/dist/tools/lifecycle.js +36 -9
- package/dist/tools/lifecycle.js.map +1 -1
- package/dist/tools/share.d.ts +18 -0
- package/dist/tools/share.js +277 -0
- package/dist/tools/share.js.map +1 -0
- package/dist/tools/viewer.d.ts +1 -1
- package/dist/tools/viewer.js +11 -6
- package/dist/tools/viewer.js.map +1 -1
- package/dist/viewer-page/viewer.js +19 -2
- package/package.json +4 -3
- package/dist/bridge/protocol.d.ts +0 -87
- package/dist/bridge/protocol.js +0 -14
- package/dist/bridge/protocol.js.map +0 -1
- package/dist/bridge/server.d.ts +0 -35
- package/dist/bridge/server.js +0 -236
- package/dist/bridge/server.js.map +0 -1
- package/dist/tools/bridge.d.ts +0 -4
- package/dist/tools/bridge.js +0 -115
- package/dist/tools/bridge.js.map +0 -1
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
// share/server.ts — where sessions are joined on this machine: the local listener.
|
|
2
|
+
//
|
|
3
|
+
// http://127.0.0.1:<port>/?invite=<token> the app (Buerligons), docked into the session of that invite
|
|
4
|
+
// ws://127.0.0.1:<port>/session/?invite=… a session, joined as a guest: what a ClassCAD server's WebSocket is
|
|
5
|
+
// ws://127.0.0.1:<port>/session/?host=… a page that hosts a session of its own and offers an invite here
|
|
6
|
+
// …/assets/… the app's own files
|
|
7
|
+
//
|
|
8
|
+
// Two kinds of session are joined here, by the same ?invite=:
|
|
9
|
+
// • the MCP's own (share/hub.ts): the engine is the MCP's, apps are guests.
|
|
10
|
+
// • a page's: an app that runs the engine in its own page (buerli's
|
|
11
|
+
// WASMClient) hosts its session itself, but a page cannot listen. So it
|
|
12
|
+
// connects out, with ?host=<invite>, and waits. When a guest joins with
|
|
13
|
+
// that invite the page is told and opens one more connection
|
|
14
|
+
// (?host=<invite>&guest=<id>), which is joined to the guest's: from then on
|
|
15
|
+
// the two talk to each other and this listener only passes the bytes. That
|
|
16
|
+
// is how an MCP session joins the engine of a foreign app: as its guest.
|
|
17
|
+
//
|
|
18
|
+
// One listener per MCP process (the daemon, or a shim serving in-process),
|
|
19
|
+
// shared by all of its sessions. Every session has invites of its own: a page
|
|
20
|
+
// is in exactly one session, and two sessions never see each other. The
|
|
21
|
+
// listener binds 127.0.0.1 and answers only requests that name it as their
|
|
22
|
+
// host. An invite is the key to its session — the link is a secret — and the
|
|
23
|
+
// only one: apps of any origin may join with it, as they may join a server.
|
|
24
|
+
import { createServer } from 'node:http';
|
|
25
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
26
|
+
import { dirname, extname, join, relative, sep } from 'node:path';
|
|
27
|
+
import { fileURLToPath } from 'node:url';
|
|
28
|
+
import { gzipSync } from 'node:zlib';
|
|
29
|
+
import { WebSocketServer } from 'ws';
|
|
30
|
+
import { randomUUID } from 'node:crypto';
|
|
31
|
+
import { authStatus } from '../auth.js';
|
|
32
|
+
/** The app's build, copied into the package (scripts/copy-build-assets.mjs). */
|
|
33
|
+
const APP_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'app');
|
|
34
|
+
/** The port apps look for the MCP on (DEFAULT_SESSION_URL in @buerli.io/classcad); CLASSCAD_VIEWER_PORT names another. */
|
|
35
|
+
const DEFAULT_PORT = 9098;
|
|
36
|
+
/** The WebSocket path the app's build connects to (its WSCLIENT_URL). */
|
|
37
|
+
export const SESSION_PATH = '/session';
|
|
38
|
+
/** Content types of what an app build is made of. */
|
|
39
|
+
const MIME = {
|
|
40
|
+
'.html': 'text/html; charset=utf-8',
|
|
41
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
42
|
+
'.css': 'text/css; charset=utf-8',
|
|
43
|
+
'.json': 'application/json; charset=utf-8',
|
|
44
|
+
'.svg': 'image/svg+xml',
|
|
45
|
+
'.png': 'image/png',
|
|
46
|
+
'.ico': 'image/x-icon',
|
|
47
|
+
'.woff': 'font/woff',
|
|
48
|
+
'.woff2': 'font/woff2',
|
|
49
|
+
'.ttf': 'font/ttf',
|
|
50
|
+
'.wasm': 'application/wasm',
|
|
51
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
52
|
+
'.map': 'application/json; charset=utf-8',
|
|
53
|
+
};
|
|
54
|
+
/** Text formats: served gzipped to whoever takes it. */
|
|
55
|
+
const COMPRESSIBLE = new Set(['.html', '.js', '.css', '.json', '.svg', '.txt']);
|
|
56
|
+
/** The app's files, read once: path → bytes. Empty when this build has no app. */
|
|
57
|
+
let assets = null;
|
|
58
|
+
function loadAssets() {
|
|
59
|
+
if (assets)
|
|
60
|
+
return assets;
|
|
61
|
+
assets = new Map();
|
|
62
|
+
const walk = (dir) => {
|
|
63
|
+
for (const name of readdirSync(dir)) {
|
|
64
|
+
const file = join(dir, name);
|
|
65
|
+
if (statSync(file).isDirectory())
|
|
66
|
+
walk(file);
|
|
67
|
+
else {
|
|
68
|
+
const body = readFileSync(file);
|
|
69
|
+
const ext = extname(name);
|
|
70
|
+
assets.set(relative(APP_DIR, file).split(sep).join('/'), { body, gzip: COMPRESSIBLE.has(ext) && body.length > 1024 ? gzipSync(body) : null, type: MIME[ext] ?? 'application/octet-stream' });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
if (existsSync(APP_DIR))
|
|
75
|
+
walk(APP_DIR);
|
|
76
|
+
return assets;
|
|
77
|
+
}
|
|
78
|
+
/** True when this build carries the app (dist/app/index.html). */
|
|
79
|
+
export function appAvailable() {
|
|
80
|
+
return existsSync(join(APP_DIR, 'index.html'));
|
|
81
|
+
}
|
|
82
|
+
/** Invite → the hub of its session. */
|
|
83
|
+
const hubs = new Map();
|
|
84
|
+
/** Makes an invite joinable on the listener. Returns the function that takes it off again. */
|
|
85
|
+
export function offerInvite(token, hub) {
|
|
86
|
+
hubs.set(token, hub);
|
|
87
|
+
return () => void hubs.delete(token);
|
|
88
|
+
}
|
|
89
|
+
/** Invite → the page that offers it right now. */
|
|
90
|
+
const pages = new Map();
|
|
91
|
+
/** Invite → who waits for a page to offer it (waitForPage). */
|
|
92
|
+
const pageWaiters = new Map();
|
|
93
|
+
/** How long a page has to meet a guest that joined. */
|
|
94
|
+
const MEET_TIMEOUT_MS = 10_000;
|
|
95
|
+
/** How often a page's standing connection is pinged; one that misses a beat is closed. */
|
|
96
|
+
const HEARTBEAT_MS = 30_000;
|
|
97
|
+
/** True when the invite is joined on this listener: a page offers it here right now, or it is of an MCP session of this process. */
|
|
98
|
+
export function joinedHere(token) {
|
|
99
|
+
return pages.has(token) || hubs.has(token);
|
|
100
|
+
}
|
|
101
|
+
/** Waits until a page offers the invite (a page finds the MCP again within seconds of its start). False after `timeoutMs`. */
|
|
102
|
+
export function waitForPage(token, timeoutMs) {
|
|
103
|
+
if (pages.has(token))
|
|
104
|
+
return Promise.resolve(true);
|
|
105
|
+
return new Promise(resolve => {
|
|
106
|
+
let set = pageWaiters.get(token);
|
|
107
|
+
if (!set)
|
|
108
|
+
pageWaiters.set(token, (set = new Set()));
|
|
109
|
+
const waiters = set;
|
|
110
|
+
const done = () => {
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
resolve(true);
|
|
113
|
+
};
|
|
114
|
+
const timer = setTimeout(() => {
|
|
115
|
+
waiters.delete(done);
|
|
116
|
+
if (!waiters.size)
|
|
117
|
+
pageWaiters.delete(token);
|
|
118
|
+
resolve(false);
|
|
119
|
+
}, timeoutMs);
|
|
120
|
+
waiters.add(done);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/** Closes a WebSocket that may be closing already. */
|
|
124
|
+
const shut = (ws, code, reason) => {
|
|
125
|
+
try {
|
|
126
|
+
ws.close(code, reason);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
/* already closing */
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
/** A page offers an invite: it stays joinable for as long as this connection lasts. */
|
|
133
|
+
function offer(token, control) {
|
|
134
|
+
const page = { token, control, waiting: new Map(), joined: new Set() };
|
|
135
|
+
pages.set(token, page);
|
|
136
|
+
const waiters = pageWaiters.get(token);
|
|
137
|
+
if (waiters) {
|
|
138
|
+
pageWaiters.delete(token);
|
|
139
|
+
for (const done of waiters)
|
|
140
|
+
done();
|
|
141
|
+
}
|
|
142
|
+
// A page that went away without a word (a closed laptop) must not stay offered.
|
|
143
|
+
let alive = true;
|
|
144
|
+
control.on('pong', () => (alive = true));
|
|
145
|
+
const beat = setInterval(() => {
|
|
146
|
+
if (!alive)
|
|
147
|
+
return control.terminate();
|
|
148
|
+
alive = false;
|
|
149
|
+
control.ping();
|
|
150
|
+
}, HEARTBEAT_MS);
|
|
151
|
+
beat.unref();
|
|
152
|
+
const end = () => {
|
|
153
|
+
clearInterval(beat);
|
|
154
|
+
if (pages.get(token) === page)
|
|
155
|
+
pages.delete(token);
|
|
156
|
+
// The host is gone: so is its session, for everyone in it.
|
|
157
|
+
for (const { guest, timer } of page.waiting.values()) {
|
|
158
|
+
clearTimeout(timer);
|
|
159
|
+
shut(guest, 1001, 'the session is over');
|
|
160
|
+
}
|
|
161
|
+
page.waiting.clear();
|
|
162
|
+
for (const [guest, link] of page.joined) {
|
|
163
|
+
shut(guest, 1001, 'the session is over');
|
|
164
|
+
shut(link, 1001, 'the session is over');
|
|
165
|
+
}
|
|
166
|
+
page.joined.clear();
|
|
167
|
+
};
|
|
168
|
+
control.on('close', end);
|
|
169
|
+
control.on('error', end);
|
|
170
|
+
}
|
|
171
|
+
/** A guest joined a page's session: the page is told, and has a moment to meet it. */
|
|
172
|
+
function knock(page, guest) {
|
|
173
|
+
const id = randomUUID();
|
|
174
|
+
const early = [];
|
|
175
|
+
const buffer = (data, isBinary) => void early.push({ data, isBinary });
|
|
176
|
+
guest.on('message', buffer);
|
|
177
|
+
const timer = setTimeout(() => {
|
|
178
|
+
page.waiting.delete(id);
|
|
179
|
+
shut(guest, 1011, 'the host did not answer');
|
|
180
|
+
}, MEET_TIMEOUT_MS);
|
|
181
|
+
page.waiting.set(id, { guest, early, buffer, timer });
|
|
182
|
+
guest.on('close', () => {
|
|
183
|
+
if (page.waiting.delete(id))
|
|
184
|
+
clearTimeout(timer);
|
|
185
|
+
});
|
|
186
|
+
page.control.send(JSON.stringify({ relay: 'guest', guest: id }));
|
|
187
|
+
}
|
|
188
|
+
/** The page's connection for one guest: from here on the two talk to each other. */
|
|
189
|
+
function meet(page, id, link) {
|
|
190
|
+
const waiting = page.waiting.get(id);
|
|
191
|
+
if (!waiting)
|
|
192
|
+
return shut(link, 1008, 'nobody is waiting');
|
|
193
|
+
page.waiting.delete(id);
|
|
194
|
+
clearTimeout(waiting.timer);
|
|
195
|
+
const { guest, early, buffer } = waiting;
|
|
196
|
+
const pair = [guest, link];
|
|
197
|
+
page.joined.add(pair);
|
|
198
|
+
guest.off('message', buffer);
|
|
199
|
+
for (const m of early)
|
|
200
|
+
link.send(m.data, { binary: m.isBinary });
|
|
201
|
+
guest.on('message', (data, isBinary) => {
|
|
202
|
+
if (link.readyState === link.OPEN)
|
|
203
|
+
link.send(data, { binary: isBinary });
|
|
204
|
+
});
|
|
205
|
+
link.on('message', (data, isBinary) => {
|
|
206
|
+
if (guest.readyState === guest.OPEN)
|
|
207
|
+
guest.send(data, { binary: isBinary });
|
|
208
|
+
});
|
|
209
|
+
const end = () => {
|
|
210
|
+
if (!page.joined.delete(pair))
|
|
211
|
+
return;
|
|
212
|
+
shut(guest, 1000, '');
|
|
213
|
+
shut(link, 1000, '');
|
|
214
|
+
};
|
|
215
|
+
for (const ws of pair) {
|
|
216
|
+
ws.on('close', end);
|
|
217
|
+
ws.on('error', end);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/** How many apps are in a session here: docked into the MCP's own, or hosting one of theirs. */
|
|
221
|
+
export function dockedApps() {
|
|
222
|
+
let n = pages.size;
|
|
223
|
+
for (const hub of new Set(hubs.values()))
|
|
224
|
+
n += hub.guests;
|
|
225
|
+
return n;
|
|
226
|
+
}
|
|
227
|
+
/** How many pages offer a session of their own here. While one does, somebody may be about to join it. */
|
|
228
|
+
export function offeringPages() {
|
|
229
|
+
return pages.size;
|
|
230
|
+
}
|
|
231
|
+
/** The one listener of this process, once somebody asked for it. */
|
|
232
|
+
let listener = null;
|
|
233
|
+
/** Every WebSocket that came in: guests, pages, and the connections pages meet their guests on. */
|
|
234
|
+
let sockets = null;
|
|
235
|
+
/**
|
|
236
|
+
* Stops the listener (the process is shutting down). Every WebSocket is ended
|
|
237
|
+
* with it: a page that offers a session keeps its connection open for as long
|
|
238
|
+
* as it can, and a server waits for its connections before it is closed.
|
|
239
|
+
*/
|
|
240
|
+
export async function closeListener() {
|
|
241
|
+
const running = listener;
|
|
242
|
+
listener = null;
|
|
243
|
+
if (!running)
|
|
244
|
+
return;
|
|
245
|
+
try {
|
|
246
|
+
const { server } = await running;
|
|
247
|
+
for (const ws of sockets?.clients ?? [])
|
|
248
|
+
ws.terminate();
|
|
249
|
+
sockets = null;
|
|
250
|
+
pages.clear();
|
|
251
|
+
server.closeAllConnections?.();
|
|
252
|
+
await new Promise(resolve => server.close(() => resolve()));
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
/* never started */
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/** Starts the listener (once per process) and returns its port. */
|
|
259
|
+
export function listen() {
|
|
260
|
+
listener ??= new Promise((resolve, reject) => {
|
|
261
|
+
const server = createServer((req, res) => {
|
|
262
|
+
try {
|
|
263
|
+
handle(req, res);
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
if (!res.headersSent)
|
|
267
|
+
res.writeHead(500, { 'content-type': 'text/plain; charset=utf-8' });
|
|
268
|
+
res.end(String(err?.message ?? err));
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
const wss = new WebSocketServer({ noServer: true, maxPayload: 256 * 1024 * 1024 });
|
|
272
|
+
sockets = wss;
|
|
273
|
+
server.on('upgrade', (req, socket, head) => {
|
|
274
|
+
upgrade(req, socket).then(to => {
|
|
275
|
+
if (!to)
|
|
276
|
+
return;
|
|
277
|
+
wss.handleUpgrade(req, socket, head, ws => {
|
|
278
|
+
if (to.as === 'guest')
|
|
279
|
+
to.hub.join(ws, to.token);
|
|
280
|
+
else if (to.as === 'visitor')
|
|
281
|
+
knock(to.page, ws);
|
|
282
|
+
else if (to.as === 'page')
|
|
283
|
+
offer(to.token, ws);
|
|
284
|
+
else
|
|
285
|
+
meet(to.page, to.guest, ws);
|
|
286
|
+
});
|
|
287
|
+
}, () => refuse(socket, 500, 'error'));
|
|
288
|
+
});
|
|
289
|
+
const wanted = Number(process.env.CLASSCAD_VIEWER_PORT ?? DEFAULT_PORT);
|
|
290
|
+
const bound = () => {
|
|
291
|
+
server.unref();
|
|
292
|
+
resolve({ server, port: server.address().port });
|
|
293
|
+
};
|
|
294
|
+
server.once('error', (err) => {
|
|
295
|
+
// Another MCP process of this machine has the port: any free one will do.
|
|
296
|
+
if (err.code === 'EADDRINUSE' && wanted !== 0) {
|
|
297
|
+
server.once('error', reject);
|
|
298
|
+
server.listen(0, '127.0.0.1', bound);
|
|
299
|
+
}
|
|
300
|
+
else
|
|
301
|
+
reject(err);
|
|
302
|
+
});
|
|
303
|
+
server.listen(wanted, '127.0.0.1', bound);
|
|
304
|
+
});
|
|
305
|
+
return listener;
|
|
306
|
+
}
|
|
307
|
+
/** True when the request names this listener as its host: not a page of another site reaching in by a name of its own (DNS rebinding). */
|
|
308
|
+
const ownHost = (req) => {
|
|
309
|
+
const port = req.socket.localPort ?? 0;
|
|
310
|
+
return req.headers.host === `127.0.0.1:${port}` || req.headers.host === `localhost:${port}`;
|
|
311
|
+
};
|
|
312
|
+
/** Ends a WebSocket handshake with an HTTP status: the client sees a failed connection. */
|
|
313
|
+
function refuse(socket, status, reason) {
|
|
314
|
+
socket.end(`HTTP/1.1 ${status} ${reason}\r\nConnection: close\r\nContent-Length: 0\r\n\r\n`);
|
|
315
|
+
}
|
|
316
|
+
/** Decides whether a WebSocket may come in, and as what. */
|
|
317
|
+
async function upgrade(req, socket) {
|
|
318
|
+
const port = req.socket.localPort ?? 0;
|
|
319
|
+
if (!ownHost(req))
|
|
320
|
+
return refuse(socket, 403, 'Forbidden'), null;
|
|
321
|
+
const url = new URL(req.url ?? '/', `http://127.0.0.1:${port}`);
|
|
322
|
+
if (url.pathname.replace(/\/+$/, '') !== SESSION_PATH)
|
|
323
|
+
return refuse(socket, 404, 'Not Found'), null;
|
|
324
|
+
// A page that hosts its own session: offering an invite, or meeting a guest who joined with it.
|
|
325
|
+
const hosted = url.searchParams.get('host');
|
|
326
|
+
if (hosted) {
|
|
327
|
+
const guest = url.searchParams.get('guest');
|
|
328
|
+
const page = pages.get(hosted);
|
|
329
|
+
if (guest)
|
|
330
|
+
return page?.waiting.has(guest) ? { as: 'meeting', page, guest } : (refuse(socket, 404, 'Not Found'), null);
|
|
331
|
+
// An invite is offered once; the MCP's own invites are not a page's to offer.
|
|
332
|
+
if (page || hubs.has(hosted) || !/^[A-Za-z0-9_-]{16,128}$/.test(hosted))
|
|
333
|
+
return refuse(socket, 409, 'Conflict'), null;
|
|
334
|
+
return { as: 'page', token: hosted };
|
|
335
|
+
}
|
|
336
|
+
// A guest. A server takes the invite as a query parameter (browsers) or as a header (programs).
|
|
337
|
+
const header = req.headers['classcad-invite'];
|
|
338
|
+
const token = url.searchParams.get('invite') ?? (Array.isArray(header) ? header[0] : header) ?? '';
|
|
339
|
+
const hub = hubs.get(token);
|
|
340
|
+
if (hub) {
|
|
341
|
+
// The engine behind the session is the signed-in machine's.
|
|
342
|
+
if (!(await authStatus()).signedIn)
|
|
343
|
+
return refuse(socket, 401, 'Unauthorized'), null;
|
|
344
|
+
return { as: 'guest', hub, token };
|
|
345
|
+
}
|
|
346
|
+
const page = pages.get(token);
|
|
347
|
+
if (page)
|
|
348
|
+
return { as: 'visitor', page };
|
|
349
|
+
// Like a server: no invite, no session — the handshake just fails.
|
|
350
|
+
return refuse(socket, 403, 'Forbidden'), null;
|
|
351
|
+
}
|
|
352
|
+
/** On every answer: no type sniffing, and the link (it carries the invite) is never sent on as a referrer. */
|
|
353
|
+
const SAFE_HEADERS = { 'x-content-type-options': 'nosniff', 'referrer-policy': 'no-referrer' };
|
|
354
|
+
/** HTTP: the app's page (only with an invite) and its files. */
|
|
355
|
+
function handle(req, res) {
|
|
356
|
+
const port = req.socket.localPort ?? 0;
|
|
357
|
+
// Only this listener's own address: no DNS rebinding, no other sites' pages.
|
|
358
|
+
if (!ownHost(req))
|
|
359
|
+
return text(res, 403, 'forbidden');
|
|
360
|
+
if (req.method !== 'GET' && req.method !== 'HEAD')
|
|
361
|
+
return text(res, 405, 'method not allowed');
|
|
362
|
+
const url = new URL(req.url ?? '/', `http://127.0.0.1:${port}`);
|
|
363
|
+
const path = decodeURIComponent(url.pathname);
|
|
364
|
+
const files = loadAssets();
|
|
365
|
+
if (path === '/' || path === '/index.html') {
|
|
366
|
+
const page = files.get('index.html');
|
|
367
|
+
if (!page)
|
|
368
|
+
return text(res, 404, 'This build of the ClassCAD MCP carries no app.');
|
|
369
|
+
// The app without an invite would host a session of its own, and there is none to host here.
|
|
370
|
+
if (!url.searchParams.get('invite'))
|
|
371
|
+
return text(res, 404, 'Open the link your agent gave you: it carries the invite to its session.');
|
|
372
|
+
res.writeHead(200, {
|
|
373
|
+
...SAFE_HEADERS,
|
|
374
|
+
'content-type': page.type,
|
|
375
|
+
'cache-control': 'no-store',
|
|
376
|
+
// The app's own files, and WebSockets to this listener only. Styles are set from scripts (styled-components, antd).
|
|
377
|
+
'content-security-policy': `default-src 'self'; script-src 'self' 'wasm-unsafe-eval' blob:; worker-src 'self' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' ws://127.0.0.1:${port} ws://localhost:${port} data: blob:; frame-ancestors 'none'; base-uri 'none'; form-action 'none'`,
|
|
378
|
+
});
|
|
379
|
+
return void res.end(req.method === 'HEAD' ? undefined : page.body);
|
|
380
|
+
}
|
|
381
|
+
const asset = files.get(path.replace(/^\/+/, ''));
|
|
382
|
+
if (!asset || path.endsWith('index.html'))
|
|
383
|
+
return text(res, 404, 'not found');
|
|
384
|
+
const zipped = asset.gzip && /\bgzip\b/.test(String(req.headers['accept-encoding'] ?? ''));
|
|
385
|
+
res.writeHead(200, {
|
|
386
|
+
...SAFE_HEADERS,
|
|
387
|
+
'content-type': asset.type,
|
|
388
|
+
// Built files carry a hash in their name: they never change.
|
|
389
|
+
'cache-control': path.startsWith('/assets/') ? 'public, max-age=31536000, immutable' : 'no-cache',
|
|
390
|
+
...(zipped ? { 'content-encoding': 'gzip', vary: 'accept-encoding' } : {}),
|
|
391
|
+
});
|
|
392
|
+
res.end(req.method === 'HEAD' ? undefined : zipped ? asset.gzip : asset.body);
|
|
393
|
+
}
|
|
394
|
+
/** A plain-text answer. */
|
|
395
|
+
function text(res, status, body) {
|
|
396
|
+
res.writeHead(status, { ...SAFE_HEADERS, 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' });
|
|
397
|
+
res.end(body);
|
|
398
|
+
}
|
|
399
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/share/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { Client } from '../client.js';
|
|
2
|
+
import { type ClientIdentity, type Peer, type SelectionItem, type SelectTarget } from './protocol.js';
|
|
3
|
+
/**
|
|
4
|
+
* How a session's app reaches the user:
|
|
5
|
+
* browser the MCP opens it in the user's default browser, with the first model
|
|
6
|
+
* host the host has a browser pane of its own, next to the conversation (the Claude desktop
|
|
7
|
+
* app): the agent is told to open the link there, and nothing is opened from here
|
|
8
|
+
* off nothing is opened; the agent hands over the link
|
|
9
|
+
*/
|
|
10
|
+
export type Show = 'browser' | 'host' | 'off';
|
|
11
|
+
/** What the environment says about it: CLASSCAD_VIEWER_OPEN (browser | host | 0), else by the host that started the MCP. */
|
|
12
|
+
export declare function showFromEnv(env?: NodeJS.ProcessEnv): Show;
|
|
13
|
+
/**
|
|
14
|
+
* When a session gets a link that works beyond its machine (CLASSCAD_SHARE), where a relay is there to share it on:
|
|
15
|
+
* always from its first model on (the default): the app's link IS that link, wherever the relay holds the
|
|
16
|
+
* session — one link, which the user opens and may send on. A relay that cannot be reached, or that
|
|
17
|
+
* goes away, leaves the link of this machine: the app is offered under that one instead.
|
|
18
|
+
* ask when the user asks for one (the `share` tool): the app's link is this machine's, the other a second one
|
|
19
|
+
* off never
|
|
20
|
+
* Whichever it is, the session does not depend on the relay: the listener on this machine is always there.
|
|
21
|
+
*/
|
|
22
|
+
export type Sharing = 'ask' | 'always' | 'off';
|
|
23
|
+
/** What the environment says about it. Without a relay there is nothing to share on. */
|
|
24
|
+
export declare function sharingFromEnv(env?: NodeJS.ProcessEnv): Sharing;
|
|
25
|
+
/** A session's link for everybody else: whether the relay holds it right now, and why not. */
|
|
26
|
+
export type Shared = {
|
|
27
|
+
url: string;
|
|
28
|
+
live: boolean;
|
|
29
|
+
problem: string | null;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* The app, offered to the user — once per session: its link, and how it got to them or should.
|
|
33
|
+
* `shared`: the link is the session's share link (it works from anywhere); else it is this machine's.
|
|
34
|
+
* `fallback`: offered again, on this machine, because the share link it was offered under stopped working.
|
|
35
|
+
*/
|
|
36
|
+
export type Offer = {
|
|
37
|
+
url: string;
|
|
38
|
+
how: 'opened' | 'host' | 'link';
|
|
39
|
+
shared?: true;
|
|
40
|
+
fallback?: true;
|
|
41
|
+
};
|
|
42
|
+
/** What a session's sharing is opened with. */
|
|
43
|
+
export type ShareOptions = {
|
|
44
|
+
/** The session's engine client. */
|
|
45
|
+
client: Client;
|
|
46
|
+
/** Runs work in the session's tool queue. */
|
|
47
|
+
queue: <T>(work: () => Promise<T>) => Promise<T>;
|
|
48
|
+
/** Offer the app's link (this build must carry the app). */
|
|
49
|
+
app: boolean;
|
|
50
|
+
/** How the app reaches the user. */
|
|
51
|
+
show: Show;
|
|
52
|
+
/** When the session gets a link for everybody else. Default: what the environment says. */
|
|
53
|
+
sharing?: Sharing;
|
|
54
|
+
/** Who this session is, for the others: asked whenever it is published (the host's name is known late). */
|
|
55
|
+
identity: () => ClientIdentity;
|
|
56
|
+
log?: (msg: string) => void;
|
|
57
|
+
};
|
|
58
|
+
/** What one participant has selected. `total` counts all of it; `items` may be cut to what fits a presence frame. */
|
|
59
|
+
export type PeerSelection = {
|
|
60
|
+
peer: Peer;
|
|
61
|
+
items: SelectionItem[];
|
|
62
|
+
total: number;
|
|
63
|
+
};
|
|
64
|
+
/** A session's company, as its tools use it. */
|
|
65
|
+
export type Share = {
|
|
66
|
+
/** The link that docks the app into this session on this machine; null when this build carries no app or the listener did not start. */
|
|
67
|
+
readonly url: string | null;
|
|
68
|
+
/** The link the app is offered under right now: the share link where every session is shared and the relay holds this one, else `url`. */
|
|
69
|
+
readonly appUrl: string | null;
|
|
70
|
+
/** False while this session is a guest in somebody else's (use_session with an invite): theirs is the app the user has open. */
|
|
71
|
+
readonly hosting: boolean;
|
|
72
|
+
/** How the app reaches the user. */
|
|
73
|
+
readonly show: Show;
|
|
74
|
+
/** When the session gets a link for everybody else. */
|
|
75
|
+
readonly sharing: Sharing;
|
|
76
|
+
/** The link that docks the app into this session from anywhere, while the relay holds it; null otherwise. */
|
|
77
|
+
readonly sharedUrl: string | null;
|
|
78
|
+
/**
|
|
79
|
+
* Shares the session beyond this machine: an invite of its own, offered on
|
|
80
|
+
* the relay. Whoever opens the link is in the session like the user at the
|
|
81
|
+
* app. Shared already: the same link. Waits `patience` ms at most for the
|
|
82
|
+
* relay; one that does not take the offer by then (`live: false`, with the
|
|
83
|
+
* reason) is asked again in the background until it does — nothing fails,
|
|
84
|
+
* the session just is not shared yet. Rejects only where a session cannot be
|
|
85
|
+
* shared at all (no relay, somebody else's session).
|
|
86
|
+
*/
|
|
87
|
+
publish: (patience?: number) => Promise<Shared>;
|
|
88
|
+
/** Where every session is shared: starts offering this one, without waiting for it. For the moment before the first model. */
|
|
89
|
+
warm: () => void;
|
|
90
|
+
/** Takes the shared link back: it stops working, and whoever joined with it is disconnected (`kicked`). Null when the session was not shared. */
|
|
91
|
+
unpublish: () => {
|
|
92
|
+
kicked: number;
|
|
93
|
+
} | null;
|
|
94
|
+
/** Opens the app in the user's default browser now (`link`: under that link; default `appUrl`). False where there is no browser to open. */
|
|
95
|
+
open: (link?: string) => boolean;
|
|
96
|
+
/**
|
|
97
|
+
* A tool ran that may have changed the model or the engine. With the first
|
|
98
|
+
* model of a session the app is offered, once: the answer says how (and in
|
|
99
|
+
* `browser` mode it has been opened by then). Null every other time.
|
|
100
|
+
*/
|
|
101
|
+
touch: () => Promise<Offer | null>;
|
|
102
|
+
/** The agent was handed `link` some other way (`view`): the app is not offered again. */
|
|
103
|
+
offered: (link?: string) => void;
|
|
104
|
+
/** How many apps are in the session (agents not counted): somebody has it open. */
|
|
105
|
+
docked: () => number;
|
|
106
|
+
/** Everyone else in the session, as far as this session knows. */
|
|
107
|
+
peers: () => Peer[];
|
|
108
|
+
/** What the others have selected: one entry per participant that shares its selection. */
|
|
109
|
+
selections: () => PeerSelection[];
|
|
110
|
+
/** Asks the others to select `items`, and returns what they have selected a moment later. */
|
|
111
|
+
select: (items: SelectTarget[], replace?: boolean) => Promise<PeerSelection[]>;
|
|
112
|
+
/** The session is over: the link stops working, docked apps are told. */
|
|
113
|
+
close: () => void;
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* Opens a session's sharing: its hub, the app's invite on the listener, and an
|
|
117
|
+
* ear for whoever is in the session. Nothing is started: the engine comes up
|
|
118
|
+
* with the first tool call, or with the first guest.
|
|
119
|
+
*/
|
|
120
|
+
export declare function openShare(opts: ShareOptions): Promise<Share>;
|