node-lab-mcp 0.1.2 → 0.1.3
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/dist/index.js +82 -23
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -25315,7 +25315,7 @@ function getPartyInfo(partySocketOptions, defaultProtocol, defaultParams = {}) {
|
|
|
25315
25315
|
host: rawHost,
|
|
25316
25316
|
path: rawPath,
|
|
25317
25317
|
protocol: rawProtocol,
|
|
25318
|
-
room
|
|
25318
|
+
room,
|
|
25319
25319
|
party,
|
|
25320
25320
|
basePath,
|
|
25321
25321
|
prefix,
|
|
@@ -25328,13 +25328,13 @@ function getPartyInfo(partySocketOptions, defaultProtocol, defaultParams = {}) {
|
|
|
25328
25328
|
const name = party ?? "main";
|
|
25329
25329
|
const path = rawPath ? `/${rawPath}` : "";
|
|
25330
25330
|
const protocol = rawProtocol || (host.startsWith("localhost:") || host.startsWith("127.0.0.1:") || host.startsWith("192.168.") || host.startsWith("10.") || host.startsWith("172.") && host.split(".")[1] >= "16" && host.split(".")[1] <= "31" || host.startsWith("[::ffff:7f00:1]:") ? defaultProtocol : `${defaultProtocol}s`);
|
|
25331
|
-
const baseUrl = `${protocol}://${host}/${basePath || `${prefix || "parties"}/${name}/${
|
|
25331
|
+
const baseUrl = `${protocol}://${host}/${basePath || `${prefix || "parties"}/${name}/${room}`}${path}`;
|
|
25332
25332
|
const makeUrl = (query2 = {}) => `${baseUrl}?${new URLSearchParams([...Object.entries(defaultParams), ...Object.entries(query2).filter(valueIsNotNil)])}`;
|
|
25333
25333
|
const urlProvider = typeof query === "function" ? async () => makeUrl(await query()) : makeUrl(query);
|
|
25334
25334
|
return {
|
|
25335
25335
|
host,
|
|
25336
25336
|
path,
|
|
25337
|
-
room
|
|
25337
|
+
room,
|
|
25338
25338
|
name,
|
|
25339
25339
|
protocol,
|
|
25340
25340
|
partyUrl: baseUrl,
|
|
@@ -25386,11 +25386,11 @@ var PartySocket = class extends ReconnectingWebSocket {
|
|
|
25386
25386
|
this.setWSProperties(wsOptions);
|
|
25387
25387
|
}
|
|
25388
25388
|
setWSProperties(wsOptions) {
|
|
25389
|
-
const { _pk, _pkurl, name, room
|
|
25389
|
+
const { _pk, _pkurl, name, room, host, path, basePath } = wsOptions;
|
|
25390
25390
|
this._pk = _pk;
|
|
25391
25391
|
this._pkurl = _pkurl;
|
|
25392
25392
|
this.name = name;
|
|
25393
|
-
this.room =
|
|
25393
|
+
this.room = room;
|
|
25394
25394
|
this.host = host;
|
|
25395
25395
|
this.path = path;
|
|
25396
25396
|
this.basePath = basePath;
|
|
@@ -25453,14 +25453,34 @@ function getWSOptions(partySocketOptions) {
|
|
|
25453
25453
|
// party-bridge.ts
|
|
25454
25454
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
25455
25455
|
var PartyBridge = class {
|
|
25456
|
-
constructor(host,
|
|
25456
|
+
constructor(host, room) {
|
|
25457
25457
|
this.host = host;
|
|
25458
|
-
this.room
|
|
25459
|
-
|
|
25460
|
-
|
|
25461
|
-
|
|
25458
|
+
if (room) this.openRoom(room);
|
|
25459
|
+
}
|
|
25460
|
+
ps = null;
|
|
25461
|
+
pending = /* @__PURE__ */ new Map();
|
|
25462
|
+
peers = 0;
|
|
25463
|
+
room;
|
|
25464
|
+
openRoom(room) {
|
|
25465
|
+
if (this.ps) {
|
|
25466
|
+
try {
|
|
25467
|
+
this.ps.close();
|
|
25468
|
+
} catch {
|
|
25469
|
+
}
|
|
25470
|
+
}
|
|
25471
|
+
for (const [id, p] of this.pending) {
|
|
25472
|
+
clearTimeout(p.timer);
|
|
25473
|
+
p.reject(new Error("relay room switched mid-command"));
|
|
25474
|
+
this.pending.delete(id);
|
|
25475
|
+
}
|
|
25476
|
+
this.peers = 0;
|
|
25477
|
+
this.room = room;
|
|
25478
|
+
const ps = new PartySocket({ host: this.host, room });
|
|
25479
|
+
this.ps = ps;
|
|
25480
|
+
ps.addEventListener("message", (e) => this.onMessage(typeof e.data === "string" ? e.data : ""));
|
|
25481
|
+
ps.addEventListener("open", () => process.stderr.write(`[party] joined room "${room}" on ${this.host}
|
|
25462
25482
|
`));
|
|
25463
|
-
|
|
25483
|
+
ps.addEventListener("close", () => {
|
|
25464
25484
|
this.peers = 0;
|
|
25465
25485
|
for (const [id, p] of this.pending) {
|
|
25466
25486
|
clearTimeout(p.timer);
|
|
@@ -25469,18 +25489,27 @@ var PartyBridge = class {
|
|
|
25469
25489
|
}
|
|
25470
25490
|
});
|
|
25471
25491
|
}
|
|
25472
|
-
|
|
25473
|
-
|
|
25474
|
-
|
|
25492
|
+
// GraphBridge: runtime room switching. The promise resolves immediately after the new socket is
|
|
25493
|
+
// requested — the `open` event follows async; check status()/connected() to know when the editor pairs.
|
|
25494
|
+
async setRoom(room) {
|
|
25495
|
+
this.openRoom(room);
|
|
25496
|
+
}
|
|
25497
|
+
currentRoom() {
|
|
25498
|
+
return this.room;
|
|
25499
|
+
}
|
|
25475
25500
|
// peers counts everyone in the room including us, so >1 means an editor has joined.
|
|
25476
25501
|
connected() {
|
|
25477
|
-
return this.ps
|
|
25502
|
+
return this.ps?.readyState === PartySocket.OPEN && this.peers > 1;
|
|
25478
25503
|
}
|
|
25479
25504
|
status() {
|
|
25505
|
+
if (!this.ps || !this.room) return `no relay room set \u2014 call set_room with a pairing code from the lab's MCP modal`;
|
|
25480
25506
|
if (this.ps.readyState !== PartySocket.OPEN) return `connecting to relay ${this.host}\u2026`;
|
|
25481
25507
|
return this.peers > 1 ? `editor connected via relay (room "${this.room}")` : `in room "${this.room}", waiting for the editor to pair`;
|
|
25482
25508
|
}
|
|
25483
25509
|
send(cmd, timeoutMs = 8e3) {
|
|
25510
|
+
if (!this.ps || !this.room) {
|
|
25511
|
+
return Promise.reject(new Error(`No relay room set. Call set_room with the pairing code from the lab's MCP modal.`));
|
|
25512
|
+
}
|
|
25484
25513
|
if (this.ps.readyState !== PartySocket.OPEN) {
|
|
25485
25514
|
return Promise.reject(new Error(`Not connected to the relay (${this.host}) yet \u2014 retry in a moment.`));
|
|
25486
25515
|
}
|
|
@@ -27184,17 +27213,23 @@ function gradeHeadless(spec, inputs, expected, ticks = 16) {
|
|
|
27184
27213
|
}
|
|
27185
27214
|
|
|
27186
27215
|
// server.ts
|
|
27187
|
-
var
|
|
27216
|
+
var initialRoom = process.env.NODE_LAB_ROOM;
|
|
27217
|
+
var useLocal = process.env.NODE_LAB_LOCAL === "1";
|
|
27188
27218
|
var bridge;
|
|
27189
|
-
if (
|
|
27190
|
-
const host = process.env.NODE_LAB_PARTY_HOST ?? DEFAULT_PARTY_HOST;
|
|
27191
|
-
bridge = new PartyBridge(host, room);
|
|
27192
|
-
process.stderr.write(`[node-lab-mcp] relay mode \u2014 room "${room}" @ ${host}
|
|
27193
|
-
`);
|
|
27194
|
-
} else {
|
|
27219
|
+
if (useLocal) {
|
|
27195
27220
|
const local = new Bridge();
|
|
27196
27221
|
local.start(BRIDGE_PORT);
|
|
27197
27222
|
bridge = local;
|
|
27223
|
+
process.stderr.write(`[node-lab-mcp] local-WS mode \u2014 ws://localhost:${BRIDGE_PORT}
|
|
27224
|
+
`);
|
|
27225
|
+
} else {
|
|
27226
|
+
const host = process.env.NODE_LAB_PARTY_HOST ?? DEFAULT_PARTY_HOST;
|
|
27227
|
+
bridge = new PartyBridge(host, initialRoom);
|
|
27228
|
+
process.stderr.write(
|
|
27229
|
+
initialRoom ? `[node-lab-mcp] relay mode \u2014 room "${initialRoom}" @ ${host}
|
|
27230
|
+
` : `[node-lab-mcp] relay mode (no room yet) \u2014 agent must call set_room with the pairing code @ ${host}
|
|
27231
|
+
`
|
|
27232
|
+
);
|
|
27198
27233
|
}
|
|
27199
27234
|
var server = new McpServer({ name: "node-lab", version: "0.1.0" });
|
|
27200
27235
|
var json = (data) => ({ content: [{ type: "text", text: JSON.stringify(data, null, 2) }] });
|
|
@@ -27202,6 +27237,30 @@ var fail = (e) => ({
|
|
|
27202
27237
|
content: [{ type: "text", text: `ERROR: ${e instanceof Error ? e.message : String(e)}` }],
|
|
27203
27238
|
isError: true
|
|
27204
27239
|
});
|
|
27240
|
+
server.registerTool(
|
|
27241
|
+
"set_room",
|
|
27242
|
+
{
|
|
27243
|
+
description: `Switch the PartyKit relay room this MCP connects to. Pair with any browser tab on the fly without restarting the MCP server. Pass the 4-4 pairing code from the lab's MCP modal (e.g. "138n-f2le"). Returns the new status \u2014 call bridge_status again after a short delay to confirm the editor joined.`,
|
|
27244
|
+
inputSchema: { room: external_exports.string() }
|
|
27245
|
+
},
|
|
27246
|
+
async ({ room }) => {
|
|
27247
|
+
try {
|
|
27248
|
+
if (!bridge.setRoom) throw new Error("set_room is only available in relay mode. Start the MCP without NODE_LAB_LOCAL=1.");
|
|
27249
|
+
await bridge.setRoom(room);
|
|
27250
|
+
return json({ room, status: bridge.status() });
|
|
27251
|
+
} catch (e) {
|
|
27252
|
+
return fail(e);
|
|
27253
|
+
}
|
|
27254
|
+
}
|
|
27255
|
+
);
|
|
27256
|
+
server.registerTool(
|
|
27257
|
+
"get_room",
|
|
27258
|
+
{
|
|
27259
|
+
description: "Read the current PartyKit relay room id (the 4-4 pairing code), plus the bridge status.",
|
|
27260
|
+
inputSchema: {}
|
|
27261
|
+
},
|
|
27262
|
+
async () => json({ room: bridge.currentRoom?.() ?? null, status: bridge.status() })
|
|
27263
|
+
);
|
|
27205
27264
|
server.registerTool(
|
|
27206
27265
|
"bridge_status",
|
|
27207
27266
|
{
|
|
@@ -27321,5 +27380,5 @@ live(
|
|
|
27321
27380
|
);
|
|
27322
27381
|
var transport = new StdioServerTransport();
|
|
27323
27382
|
await server.connect(transport);
|
|
27324
|
-
process.stderr.write(`[node-lab-mcp] ready \u2014 ${
|
|
27383
|
+
process.stderr.write(`[node-lab-mcp] ready \u2014 ${bridge.status()}
|
|
27325
27384
|
`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-lab-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "MCP server to drive the IFR Node Lab graph from any MCP client (Claude / Codex / Gemini …) — locally or, via a PartyKit relay, against the deployed app.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|