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.
Files changed (2) hide show
  1. package/dist/index.js +82 -23
  2. 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: room2,
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}/${room2}`}${path}`;
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: room2,
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: room2, host, path, basePath } = wsOptions;
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 = room2;
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, room2) {
25456
+ constructor(host, room) {
25457
25457
  this.host = host;
25458
- this.room = room2;
25459
- this.ps = new PartySocket({ host, room: room2 });
25460
- this.ps.addEventListener("message", (e) => this.onMessage(typeof e.data === "string" ? e.data : ""));
25461
- this.ps.addEventListener("open", () => process.stderr.write(`[party] joined room "${room2}" on ${host}
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
- this.ps.addEventListener("close", () => {
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
- ps;
25473
- pending = /* @__PURE__ */ new Map();
25474
- peers = 0;
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.readyState === PartySocket.OPEN && this.peers > 1;
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 room = process.env.NODE_LAB_ROOM;
27216
+ var initialRoom = process.env.NODE_LAB_ROOM;
27217
+ var useLocal = process.env.NODE_LAB_LOCAL === "1";
27188
27218
  var bridge;
27189
- if (room) {
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 ${room ? `relay room "${room}"` : `local bridge ws://localhost:${BRIDGE_PORT}`}
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.2",
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": {