@polypad/mcp 0.1.0 → 0.1.2

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 (3) hide show
  1. package/README.md +10 -0
  2. package/dist/index.js +478 -88
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -38,6 +38,16 @@ claude mcp add polypad -- npx -y @polypad/mcp
38
38
 
39
39
  Safari cannot connect to a local server, so use Chrome, Edge or Firefox for the Polypad tab. The claude.ai website cannot be used either: it needs a hosted server.
40
40
 
41
+ ## Several AI clients at once
42
+
43
+ You can add Polypad to Claude Code and Claude Desktop (or Cursor) and use them at the same time. The first server to start owns the connection and the others share it, so every client drives the same Polypad tab and there is one pairing code. If the first one is closed, another takes over with a new code: enter it in the Connect AI dialog. Use version 0.1.1 or newer everywhere.
44
+
45
+ ## Troubleshooting
46
+
47
+ - **"Server not running"** in the dialog: your AI client has not started the server yet. Ask it something about Polypad, and check `npx -y @polypad/mcp --help` works (Node 22 or newer).
48
+ - **"Wrong code"**: enter the code from the latest start. It changes whenever the connection's owner restarts.
49
+ - **"Port 41720 is already in use"** (the tools say so): a program that is not a Polypad MCP server (or an old 0.1.0 one) holds the port. Close it, or use `--port 41721` here and in the dialog's Advanced section. The server checks again every 2 seconds.
50
+
41
51
  ## Options
42
52
 
43
53
  ```
package/dist/index.js CHANGED
@@ -9446,7 +9446,7 @@ var require_websocket = __commonJS({
9446
9446
  var http = __require("http");
9447
9447
  var net = __require("net");
9448
9448
  var tls = __require("tls");
9449
- var { randomBytes, createHash } = __require("crypto");
9449
+ var { randomBytes: randomBytes2, createHash } = __require("crypto");
9450
9450
  var { Duplex, Readable } = __require("stream");
9451
9451
  var { URL: URL2 } = __require("url");
9452
9452
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -9997,7 +9997,7 @@ var require_websocket = __commonJS({
9997
9997
  }
9998
9998
  }
9999
9999
  const defaultPort = isSecure ? 443 : 80;
10000
- const key = randomBytes(16).toString("base64");
10000
+ const key = randomBytes2(16).toString("base64");
10001
10001
  const request = isSecure ? https.request : http.request;
10002
10002
  const protocolSet = /* @__PURE__ */ new Set();
10003
10003
  let perMessageDeflate;
@@ -32205,6 +32205,8 @@ var CLOSE_BAD_MESSAGE = 4004;
32205
32205
  var CLOSE_BAD_VERSION = 4005;
32206
32206
  var CLOSE_HELLO_TIMEOUT = 4006;
32207
32207
  var CLOSE_SHUTDOWN = 4007;
32208
+ var CLOSE_BAD_TOKEN = 4008;
32209
+ var BRIDGE_RETRY_MS = 2e3;
32208
32210
  var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
32209
32211
  function parse3(text) {
32210
32212
  try {
@@ -32234,6 +32236,37 @@ function parseTabMessage(text) {
32234
32236
  return null;
32235
32237
  }
32236
32238
  }
32239
+ function parseClientMessage(text) {
32240
+ const m = parse3(text);
32241
+ if (!m) return null;
32242
+ switch (m.type) {
32243
+ case "relay-hello":
32244
+ if (typeof m.token !== "string" || typeof m.v !== "number") return null;
32245
+ return { type: "relay-hello", v: m.v, token: m.token };
32246
+ case "relay-call":
32247
+ if (typeof m.id !== "string" || typeof m.name !== "string") return null;
32248
+ return { type: "relay-call", id: m.id, name: m.name, input: m.input };
32249
+ default:
32250
+ return parseTabMessage(text);
32251
+ }
32252
+ }
32253
+ function parsePrimaryMessage(text) {
32254
+ const m = parse3(text);
32255
+ if (!m) return null;
32256
+ switch (m.type) {
32257
+ case "relay-welcome":
32258
+ if (typeof m.v !== "number" || typeof m.code !== "string") return null;
32259
+ return { type: "relay-welcome", v: m.v, code: m.code, server: typeof m.server === "string" ? m.server : void 0 };
32260
+ case "relay-result":
32261
+ if (typeof m.id !== "string" || !isResult(m.result)) return null;
32262
+ return { type: "relay-result", id: m.id, result: m.result };
32263
+ case "ping":
32264
+ case "pong":
32265
+ return { type: m.type };
32266
+ default:
32267
+ return null;
32268
+ }
32269
+ }
32237
32270
  var encode3 = (message) => JSON.stringify(message);
32238
32271
  var PAIRING_ALPHABET = "ABCDEFGHJKMNPQRTUVWXYZ346789";
32239
32272
  var CODE_SHAPE = /^[A-Z0-9]{3}-?[A-Z0-9]{3}$/;
@@ -32243,6 +32276,7 @@ function normalizePairingCode(input2) {
32243
32276
  const code = `${flat.slice(0, 3)}-${flat.slice(3)}`;
32244
32277
  return CODE_SHAPE.test(code) ? code : null;
32245
32278
  }
32279
+ var bridgeUrl = (port = BRIDGE_DEFAULT_PORT) => `ws://127.0.0.1:${port}`;
32246
32280
 
32247
32281
  // src/cli.ts
32248
32282
  var USAGE = `polypad-mcp: MCP server that lets an AI assistant build in your open Polypad tab.
@@ -32259,6 +32293,10 @@ Options:
32259
32293
  The MCP client starts this program and talks to it over stdio. It prints a pairing code to stderr
32260
32294
  (new on every start). In Polypad, click Connect AI and enter the code.
32261
32295
 
32296
+ Several clients (Claude Code and Claude Desktop, say) can each start one: the first owns the port and
32297
+ the others share its Polypad tab and its code. Environment: POLYPAD_MCP_HOME moves the folder that
32298
+ holds .polypad/mcp-<port>.json (default: your home folder).
32299
+
32262
32300
  Add it to Claude Code: claude mcp add polypad -- npx -y @polypad/mcp
32263
32301
  `;
32264
32302
  function parseCli(argv) {
@@ -34771,25 +34809,25 @@ var inputs = {
34771
34809
  };
34772
34810
  var descriptions = {
34773
34811
  new_project: "Start a new, empty Polypad project (optionally from a template), replacing the open one. All lengths in Polypad are millimetres and angles are degrees; Z is up.",
34774
- set_parameter: "Create or change a named parameter (a number or an expression). Any length field of any feature can use the name, so one change resizes everything bound to it. Reports features that no longer build.",
34812
+ set_parameter: "Create or change a named parameter (a number or an expression). Any length field of any feature can use the name, so one change resizes everything bound to it. Reports features that no longer build. Set the sizes up front, then use the names in fields.",
34775
34813
  add_primitive: "Add a solid box, cylinder, sphere or cone as a new body. Returns the new feature's id, which is also the body id for later commands. Sizes in mm.",
34776
34814
  add_sketch: "Add a 2D sketch on a base plane or on a flat face and draw shapes in it (rectangle, circle, line, polyline, arc, slot, polygon). The sketch is solved and its remaining degrees of freedom reported. Use extrude or revolve to turn closed shapes into a solid.",
34777
34815
  extrude: "Extrude the closed shapes of a sketch into a solid: as a new body, or cut from / joined to an existing body. Returns the feature id (the new body's id when mode is new).",
34778
- revolve: "Revolve the closed shapes of a sketch about an axis in the sketch plane to make a solid of revolution: new body, or cut / join with an existing body.",
34779
- combine: "Boolean-combine bodies: union, subtract (remove the tools from the target) or intersect. The target keeps its id; the tools are consumed.",
34780
- hole: "Drill a screw hole (M2 to M10, with clearance, tap or heat-set insert fit and optional counterbore / countersink) into a flat face. Call list_faces first and pass the face index and its rebuildId.",
34781
- fillet: "Round the given edges of a body with a radius. Call list_edges first and pass the edge indices and their rebuildId.",
34782
- chamfer: "Bevel the given edges of a body. Call list_edges first and pass the edge indices and their rebuildId.",
34783
- shell: "Hollow out a body, leaving the given faces open, with walls of a given thickness. Call list_faces first and pass the face indices and their rebuildId.",
34784
- pattern: "Repeat a body in a row or grid, or around an axis, either fusing the copies into the body or making each copy a body of its own.",
34785
- mirror: "Mirror a whole body across a plane (yz, xz or xy, optionally moved along its normal), joining the copy into the body or making it a separate body.",
34816
+ revolve: "Revolve the closed shapes of a sketch about an axis in the sketch plane to make a solid of revolution: new body, or cut / join with an existing body. Use it for round parts: draw half the outline beside the axis.",
34817
+ combine: "Boolean-combine bodies: union, subtract (remove the tools from the target) or intersect. The target keeps its id; the tools are consumed. To hollow a body use shell, not a subtracted box; to check a collision, intersect two bodies, measure the result, then undo.",
34818
+ hole: "Drill a screw hole (M2 to M10, with clearance, tap or heat-set insert fit and optional counterbore / countersink) into a flat face. Call list_faces first and pass the face index and its rebuildId. Use this for every screw, standoff or insert hole instead of cutting circles.",
34819
+ fillet: "Round the given edges of a body with a radius. Call list_edges first (narrow it with its filter) and pass the edge indices and their rebuildId. If it fails, try a smaller radius or fewer edges.",
34820
+ chamfer: "Bevel the given edges of a body. Call list_edges first (narrow it with its filter) and pass the edge indices and their rebuildId. If it fails, try a smaller distance or fewer edges.",
34821
+ shell: "Hollow out a body, leaving the given faces open, with walls of a given thickness. Call list_faces first and pass the face indices and their rebuildId. Use this for enclosures and cups instead of subtracting a smaller box; list_faces with filter normal finds the face to open.",
34822
+ pattern: "Repeat a body in a row or grid, or around an axis, either fusing the copies into the body or making each copy a body of its own. Model one copy (or one cutter) and pattern it instead of adding copies by hand.",
34823
+ mirror: "Mirror a whole body across a plane (yz, xz or xy, optionally moved along its normal), joining the copy into the body or making it a separate body. Model one side of a symmetric part, then mirror it.",
34786
34824
  move: "Move, rotate and/or scale a whole body (translate in mm, rotate in degrees about X, Y, Z around a pivot).",
34787
- update_feature: "Change fields of an existing feature (for example an extrude's distance or a box's width); everything after it rebuilds. The change is refused, and the project left as it was, if the result is not a valid model.",
34825
+ update_feature: "Change fields of an existing feature (for example an extrude's distance or a box's width); everything after it rebuilds. The change is refused, and the project left as it was, if the result is not a valid model. Prefer this over deleting and rebuilding a feature.",
34788
34826
  delete_feature: "Delete a feature. Refused while other features still use it; delete those first.",
34789
34827
  undo: "Undo the last change made to the project. Can be repeated.",
34790
34828
  get_project: "Describe the whole project: parameters, every feature in order (id, name, type, main values, whether it built or the error), and every body (volume, area, bounding box). Start here, and call it again after changes to check the result.",
34791
- list_faces: "List the faces of a body: index, flat or curved, normal, centre, area, and for flat faces the sketch frame (origin, x and y axes) with the face's bounds in it. The indices go into hole, shell and add_sketch (face). Returns a rebuildId those commands require.",
34792
- list_edges: "List the edges of a body: index, convex / concave / smooth, length, straight or curved, direction, midpoint, and the faces they border. The indices go into fillet and chamfer. Returns a rebuildId those commands require.",
34829
+ list_faces: "List the faces of a body: index, flat or curved, normal, centre, area, and for flat faces the sketch frame (origin, x and y axes) with the face's bounds in it. The indices go into hole, shell and add_sketch (face). Returns a rebuildId those commands require. A body can have hundreds of faces: use the filter (normal, kind).",
34830
+ list_edges: "List the edges of a body: index, convex / concave / smooth, length, straight or curved, direction, midpoint, and the faces they border. The indices go into fillet and chamfer. Returns a rebuildId those commands require. A body can have hundreds of edges: use the filter (face, direction, kind).",
34793
34831
  measure: "Measure bodies: volume (mm3), surface area (mm2), bounding box (mm) and its centre.",
34794
34832
  render_view: "Render the model to a PNG image from a standard view, to look at the result. Needs the Polypad browser tab."
34795
34833
  };
@@ -34802,7 +34840,7 @@ function commandSchemas() {
34802
34840
  }
34803
34841
 
34804
34842
  // src/bridge.ts
34805
- import { randomInt, randomUUID, timingSafeEqual } from "crypto";
34843
+ import { randomBytes, randomInt, randomUUID as randomUUID2 } from "crypto";
34806
34844
  import { createServer } from "http";
34807
34845
 
34808
34846
  // ../../node_modules/.pnpm/ws@8.22.0/node_modules/ws/wrapper.mjs
@@ -34814,6 +34852,176 @@ var import_sender = __toESM(require_sender(), 1);
34814
34852
  var import_subprotocol = __toESM(require_subprotocol(), 1);
34815
34853
  var import_websocket = __toESM(require_websocket(), 1);
34816
34854
  var import_websocket_server = __toESM(require_websocket_server(), 1);
34855
+ var wrapper_default = import_websocket.default;
34856
+
34857
+ // src/primary-file.ts
34858
+ import { timingSafeEqual } from "crypto";
34859
+ import { chmodSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
34860
+ import { homedir } from "os";
34861
+ import { join } from "path";
34862
+ function resolveHome(override) {
34863
+ return override ?? (process.env.POLYPAD_MCP_HOME || homedir());
34864
+ }
34865
+ var primaryFilePath = (home, port) => join(home, ".polypad", `mcp-${port}.json`);
34866
+ function writePrimaryFile(home, port, info) {
34867
+ const file2 = primaryFilePath(home, port);
34868
+ mkdirSync(join(home, ".polypad"), { recursive: true, mode: 448 });
34869
+ writeFileSync(file2, JSON.stringify(info), { mode: 384 });
34870
+ try {
34871
+ chmodSync(file2, 384);
34872
+ } catch {
34873
+ }
34874
+ }
34875
+ function readPrimaryFile(home, port) {
34876
+ try {
34877
+ const value = JSON.parse(readFileSync(primaryFilePath(home, port), "utf8"));
34878
+ if (typeof value !== "object" || value === null) return null;
34879
+ const { pid, token, code } = value;
34880
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || typeof token !== "string" || token.length < 16 || typeof code !== "string") return null;
34881
+ return { pid, token, code };
34882
+ } catch {
34883
+ return null;
34884
+ }
34885
+ }
34886
+ function removePrimaryFile(home, port, token) {
34887
+ try {
34888
+ const current = readPrimaryFile(home, port);
34889
+ if (current && !tokensMatch(current.token, token)) return;
34890
+ rmSync(primaryFilePath(home, port), { force: true });
34891
+ } catch {
34892
+ }
34893
+ }
34894
+ function pidAlive(pid) {
34895
+ try {
34896
+ process.kill(pid, 0);
34897
+ return true;
34898
+ } catch (error62) {
34899
+ return error62.code === "EPERM";
34900
+ }
34901
+ }
34902
+ function tokensMatch(a, b) {
34903
+ const x = Buffer.from(a);
34904
+ const y = Buffer.from(b);
34905
+ return x.length === y.length && timingSafeEqual(x, y);
34906
+ }
34907
+
34908
+ // src/relay-link.ts
34909
+ import { randomUUID } from "crypto";
34910
+ var fail = (text) => ({ ok: false, text });
34911
+ var RelayLink = class {
34912
+ constructor(options) {
34913
+ this.options = options;
34914
+ }
34915
+ options;
34916
+ // The primary's pairing code, from relay-welcome.
34917
+ code = "";
34918
+ ws = null;
34919
+ open = false;
34920
+ pending = /* @__PURE__ */ new Map();
34921
+ closeHandler = null;
34922
+ get connected() {
34923
+ return this.open;
34924
+ }
34925
+ // Resolves once the primary has welcomed this relay. Rejects (and leaves nothing open) for anything else.
34926
+ connect() {
34927
+ return new Promise((resolve, reject) => {
34928
+ const ws = new wrapper_default(bridgeUrl(this.options.port), { maxPayload: BRIDGE_MAX_MESSAGE_BYTES, perMessageDeflate: false, handshakeTimeout: this.options.handshakeTimeoutMs ?? 3e3 });
34929
+ this.ws = ws;
34930
+ let settled = false;
34931
+ const giveUp = (reason) => {
34932
+ if (settled) return;
34933
+ settled = true;
34934
+ clearTimeout(helloTimer);
34935
+ ws.terminate();
34936
+ this.ws = null;
34937
+ reject(new Error(reason));
34938
+ };
34939
+ const helloTimer = setTimeout(() => giveUp("No answer to relay-hello"), this.options.handshakeTimeoutMs ?? 3e3);
34940
+ ws.on("error", (error62) => giveUp(error62.message));
34941
+ ws.on("open", () => ws.send(encode3({ type: "relay-hello", v: BRIDGE_PROTOCOL_VERSION, token: this.options.token })));
34942
+ ws.on("message", (data, isBinary) => {
34943
+ if (isBinary) return void ws.close(1003, "Text frames only");
34944
+ const message = parsePrimaryMessage(rawToString(data));
34945
+ if (!message) return void (settled ? ws.close(1002, "Not a Polypad bridge message") : giveUp("Not a Polypad MCP server"));
34946
+ switch (message.type) {
34947
+ case "relay-welcome":
34948
+ if (settled) return;
34949
+ settled = true;
34950
+ clearTimeout(helloTimer);
34951
+ this.code = message.code;
34952
+ this.open = true;
34953
+ resolve();
34954
+ return;
34955
+ case "relay-result": {
34956
+ const entry = this.pending.get(message.id);
34957
+ if (!entry) return;
34958
+ clearTimeout(entry.timer);
34959
+ this.pending.delete(message.id);
34960
+ entry.resolve(message.result);
34961
+ return;
34962
+ }
34963
+ case "ping":
34964
+ if (ws.readyState === ws.OPEN) ws.send(encode3({ type: "pong" }));
34965
+ return;
34966
+ case "pong":
34967
+ return;
34968
+ }
34969
+ });
34970
+ ws.on("close", (_code, reason) => {
34971
+ if (!settled) return giveUp(reason.toString() || "Closed before relay-welcome");
34972
+ const wasOpen = this.open;
34973
+ this.open = false;
34974
+ this.ws = null;
34975
+ this.failAll("The Polypad MCP server that this one shares stopped before Polypad answered. The change may or may not have been applied: call get_project to check (this server reconnects by itself).");
34976
+ if (wasOpen) this.closeHandler?.();
34977
+ });
34978
+ });
34979
+ }
34980
+ // Called once when an established link ends (the primary exited), not when close() is used.
34981
+ onClose(handler) {
34982
+ this.closeHandler = handler;
34983
+ }
34984
+ // Never rejects: every failure is an ok:false result with text for the model.
34985
+ call(name, input2) {
34986
+ const ws = this.ws;
34987
+ if (!ws || !this.open) return Promise.resolve(fail("The link to the shared Polypad MCP server is down."));
34988
+ const id = randomUUID();
34989
+ return new Promise((resolve) => {
34990
+ const timer = setTimeout(() => {
34991
+ this.pending.delete(id);
34992
+ resolve(fail(`Polypad did not answer ${name} within ${Math.round(this.options.callTimeoutMs / 1e3)} s. The change may still have been applied: call get_project to check before retrying.`));
34993
+ }, this.options.callTimeoutMs);
34994
+ this.pending.set(id, { resolve, timer, name });
34995
+ try {
34996
+ ws.send(encode3({ type: "relay-call", id, name, input: input2 }));
34997
+ } catch (error62) {
34998
+ clearTimeout(timer);
34999
+ this.pending.delete(id);
35000
+ resolve(fail(`Could not send ${name} to the shared Polypad MCP server: ${error62 instanceof Error ? error62.message : String(error62)}`));
35001
+ }
35002
+ });
35003
+ }
35004
+ close() {
35005
+ this.closeHandler = null;
35006
+ this.open = false;
35007
+ this.failAll("The Polypad MCP server stopped before Polypad answered.");
35008
+ this.ws?.terminate();
35009
+ this.ws = null;
35010
+ }
35011
+ failAll(text) {
35012
+ for (const [id, entry] of [...this.pending]) {
35013
+ clearTimeout(entry.timer);
35014
+ this.pending.delete(id);
35015
+ entry.resolve(fail(text));
35016
+ }
35017
+ }
35018
+ };
35019
+ function rawToString(data) {
35020
+ if (typeof data === "string") return data;
35021
+ if (Array.isArray(data)) return Buffer.concat(data).toString("utf8");
35022
+ if (data instanceof ArrayBuffer) return Buffer.from(data).toString("utf8");
35023
+ return data.toString("utf8");
35024
+ }
34817
35025
 
34818
35026
  // src/bridge.ts
34819
35027
  function newPairingCode() {
@@ -34824,30 +35032,39 @@ function newPairingCode() {
34824
35032
  function normalizeOrigin(origin) {
34825
35033
  return origin.trim().replace(/\/+$/, "").toLowerCase();
34826
35034
  }
34827
- function codesMatch(a, b) {
34828
- const x = Buffer.from(a);
34829
- const y = Buffer.from(b);
34830
- return x.length === y.length && timingSafeEqual(x, y);
34831
- }
34832
- var fail = (text) => ({ ok: false, text });
35035
+ var fail2 = (text) => ({ ok: false, text });
34833
35036
  var Bridge = class {
34834
- code;
34835
35037
  allowedOrigins;
34836
35038
  options;
35039
+ home;
34837
35040
  requestedPort;
35041
+ ownCode;
35042
+ firstAttach = true;
35043
+ closed = false;
35044
+ attaching = false;
35045
+ retryTimer = null;
34838
35046
  http = null;
34839
35047
  wss = null;
34840
35048
  tab = null;
34841
35049
  tabName;
34842
35050
  pending = /* @__PURE__ */ new Map();
34843
35051
  badCodeTimes = [];
35052
+ badTokenTimes = [];
34844
35053
  pingTimer = null;
34845
35054
  alive = /* @__PURE__ */ new WeakSet();
35055
+ // Primary: the secret the relays have to present, and the relays that did.
35056
+ relayToken = null;
35057
+ relays = /* @__PURE__ */ new Set();
35058
+ // Relay: the link to the primary and its pairing code.
35059
+ link = null;
35060
+ linkCode = "";
34846
35061
  // Set when the socket could not be opened (port in use): every call then answers with this.
34847
35062
  unavailable = null;
34848
35063
  port = 0;
35064
+ role = "starting";
34849
35065
  constructor(options = {}) {
34850
- this.code = options.code ?? newPairingCode();
35066
+ this.ownCode = options.code ?? newPairingCode();
35067
+ this.home = resolveHome(options.homeDir);
34851
35068
  this.requestedPort = options.port ?? BRIDGE_DEFAULT_PORT;
34852
35069
  this.allowedOrigins = new Set([...BRIDGE_DEFAULT_ORIGINS, ...options.origins ?? []].map(normalizeOrigin));
34853
35070
  this.options = {
@@ -34856,65 +35073,45 @@ var Bridge = class {
34856
35073
  badCodeWindowMs: options.badCodeWindowMs ?? 6e4,
34857
35074
  helloTimeoutMs: options.helloTimeoutMs ?? 1e4,
34858
35075
  pingIntervalMs: options.pingIntervalMs ?? 2e4,
35076
+ retryMs: options.retryMs ?? BRIDGE_RETRY_MS,
34859
35077
  log: options.log ?? (() => {
34860
35078
  })
34861
35079
  };
34862
35080
  }
35081
+ // The pairing code the user enters in the tab: this server's own as primary, the primary's as a relay.
35082
+ get code() {
35083
+ return this.role === "relay" ? this.linkCode : this.ownCode;
35084
+ }
35085
+ // A primary: whether the tab is paired. A relay: whether the link to the primary is up (the primary answers
35086
+ // "not connected" itself when it has no tab).
34863
35087
  get connected() {
34864
- return this.tab !== null;
35088
+ if (this.role === "relay") return this.link?.connected ?? false;
35089
+ return this.role === "primary" && this.tab !== null;
34865
35090
  }
34866
35091
  get clientName() {
34867
35092
  return this.tabName;
34868
35093
  }
34869
- // Starts listening on 127.0.0.1. Rejects (for example EADDRINUSE) without leaving anything open.
34870
- listen() {
34871
- return new Promise((resolve, reject) => {
34872
- const http = createServer((_req, res) => {
34873
- res.writeHead(426, { "content-type": "text/plain" });
34874
- res.end("Polypad MCP bridge: WebSocket only.\n");
34875
- });
34876
- const wss = new import_websocket_server.default({
34877
- noServer: true,
34878
- maxPayload: BRIDGE_MAX_MESSAGE_BYTES,
34879
- perMessageDeflate: false
34880
- });
34881
- http.on("upgrade", (req, socket, head) => {
34882
- const origin = req.headers.origin;
34883
- if (typeof origin !== "string" || !this.allowedOrigins.has(normalizeOrigin(origin))) {
34884
- this.options.log(`Rejected a connection from origin ${typeof origin === "string" ? origin : "(none)"}. Add it with --origin if it is yours.`);
34885
- socket.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
34886
- return;
34887
- }
34888
- if (wss.clients.size >= 16) {
34889
- socket.end("HTTP/1.1 503 Service Unavailable\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
34890
- return;
34891
- }
34892
- wss.handleUpgrade(req, socket, head, (ws) => this.accept(ws));
34893
- });
34894
- http.once("error", (error62) => {
34895
- wss.close();
34896
- reject(error62);
34897
- });
34898
- http.listen(this.requestedPort, "127.0.0.1", () => {
34899
- this.http = http;
34900
- this.wss = wss;
34901
- this.port = http.address().port;
34902
- http.on("error", (error62) => this.options.log(`Bridge error: ${error62.message}`));
34903
- this.startPings();
34904
- resolve(this.port);
34905
- });
34906
- });
35094
+ // Starts serving on 127.0.0.1 as the primary, or, when the port is taken by a Polypad MCP server, relays through it.
35095
+ // Resolves with the port. When the port holder is something else this still resolves (role "busy": the tools explain
35096
+ // it and the server keeps trying); other errors, for example EACCES, reject.
35097
+ async listen() {
35098
+ await this.attach(true);
35099
+ return this.port;
34907
35100
  }
34908
35101
  // Runs a command on the paired tab. Never rejects: every failure is an ok:false result with text for the model.
34909
35102
  call(name, input2) {
35103
+ if (this.role === "relay") {
35104
+ const link = this.link;
35105
+ return link ? link.call(name, input2) : Promise.resolve(fail2(this.notConnectedText()));
35106
+ }
34910
35107
  const tab = this.tab;
34911
- if (!tab) return Promise.resolve(fail(this.notConnectedText()));
34912
- const id = randomUUID();
35108
+ if (!tab) return Promise.resolve(fail2(this.notConnectedText()));
35109
+ const id = randomUUID2();
34913
35110
  return new Promise((resolve) => {
34914
35111
  const timer = setTimeout(() => {
34915
35112
  this.pending.delete(id);
34916
35113
  resolve(
34917
- fail(
35114
+ fail2(
34918
35115
  `Polypad did not answer ${name} within ${Math.round(this.options.callTimeoutMs / 1e3)} s. The change may still have been applied: call get_project to check before retrying.`
34919
35116
  )
34920
35117
  );
@@ -34923,7 +35120,7 @@ var Bridge = class {
34923
35120
  try {
34924
35121
  tab.send(encode3({ type: "call", id, name, input: input2 }));
34925
35122
  } catch (error62) {
34926
- this.settle(id, fail(`Could not send ${name} to Polypad: ${error62 instanceof Error ? error62.message : String(error62)}`));
35123
+ this.settle(id, fail2(`Could not send ${name} to Polypad: ${error62 instanceof Error ? error62.message : String(error62)}`));
34927
35124
  }
34928
35125
  });
34929
35126
  }
@@ -34933,11 +35130,18 @@ var Bridge = class {
34933
35130
  return `Polypad is not connected. Open Polypad in Chrome, Edge or Firefox, click Connect AI and enter code ${this.code}.${port}`;
34934
35131
  }
34935
35132
  async close() {
35133
+ this.closed = true;
35134
+ if (this.retryTimer) clearTimeout(this.retryTimer);
35135
+ this.retryTimer = null;
35136
+ this.releaseSync();
35137
+ this.link?.close();
35138
+ this.link = null;
34936
35139
  if (this.pingTimer) clearInterval(this.pingTimer);
34937
35140
  this.pingTimer = null;
34938
35141
  for (const client of this.wss?.clients ?? []) client.close(CLOSE_SHUTDOWN, "Server stopped");
34939
35142
  this.failPending(null, "The Polypad MCP server stopped before Polypad answered.");
34940
35143
  this.tab = null;
35144
+ this.relays.clear();
34941
35145
  const http = this.http;
34942
35146
  this.http = null;
34943
35147
  this.wss?.close();
@@ -34949,15 +35153,147 @@ var Bridge = class {
34949
35153
  });
34950
35154
  }
34951
35155
  }
34952
- // --- internals --------------------------------------------------------------------------------------------------
35156
+ // Deletes the file that tells other servers to relay through this one; safe to call from a process "exit" handler.
35157
+ releaseSync() {
35158
+ if (this.relayToken) removePrimaryFile(this.home, this.port, this.relayToken);
35159
+ this.relayToken = null;
35160
+ }
35161
+ // --- roles ------------------------------------------------------------------------------------------------------
35162
+ // Finds this server's role: primary if it can bind the port, else relay if a live Polypad MCP server holds it, else busy.
35163
+ async attach(initial) {
35164
+ if (this.closed || this.attaching) return;
35165
+ this.attaching = true;
35166
+ try {
35167
+ try {
35168
+ await this.bind();
35169
+ this.becomePrimary();
35170
+ return;
35171
+ } catch (error62) {
35172
+ if (error62.code !== "EADDRINUSE") {
35173
+ if (initial) throw error62;
35174
+ this.options.log(`Could not open the bridge: ${error62.message}`);
35175
+ this.becomeBusy(`Polypad's local bridge could not start: ${error62.message}`);
35176
+ return;
35177
+ }
35178
+ }
35179
+ if (this.closed) return;
35180
+ if (await this.tryRelay()) return;
35181
+ this.becomeBusy(
35182
+ `Polypad's local bridge could not start: port ${this.requestedPort} is already in use by a program that is not a Polypad MCP server this one can share. Tell the user: close that program, or start the Polypad server with --port <other> (and enter the same port in the Connect AI dialog). This server keeps checking and shares the connection by itself once the port is free.`
35183
+ );
35184
+ } finally {
35185
+ this.attaching = false;
35186
+ }
35187
+ }
35188
+ becomePrimary() {
35189
+ this.role = "primary";
35190
+ this.unavailable = null;
35191
+ if (!this.firstAttach) this.ownCode = newPairingCode();
35192
+ this.firstAttach = false;
35193
+ this.relayToken = randomBytes(32).toString("base64url");
35194
+ let shared = true;
35195
+ try {
35196
+ writePrimaryFile(this.home, this.port, { pid: process.pid, token: this.relayToken, code: this.ownCode });
35197
+ } catch (error62) {
35198
+ shared = false;
35199
+ this.relayToken = null;
35200
+ this.options.log(`Could not write the file other Polypad MCP servers use to share this one: ${error62.message}`);
35201
+ }
35202
+ this.options.log(`Polypad MCP server ready on ws://127.0.0.1:${this.port}${shared ? "" : " (not shareable with other MCP clients)"}`);
35203
+ this.options.log(`Pairing code: ${this.ownCode} (open Polypad, click Connect AI, enter this code)`);
35204
+ }
35205
+ async tryRelay() {
35206
+ const info = readPrimaryFile(this.home, this.requestedPort);
35207
+ if (!info || !pidAlive(info.pid)) return false;
35208
+ const link = new RelayLink({ port: this.requestedPort, token: info.token, callTimeoutMs: this.options.callTimeoutMs });
35209
+ try {
35210
+ await link.connect();
35211
+ } catch (error62) {
35212
+ this.options.log(`Port ${this.requestedPort} is taken and the Polypad MCP server there did not accept this one as a relay: ${error62.message}`);
35213
+ return false;
35214
+ }
35215
+ if (this.closed) {
35216
+ link.close();
35217
+ return true;
35218
+ }
35219
+ this.link = link;
35220
+ this.linkCode = link.code;
35221
+ this.port = this.requestedPort;
35222
+ this.role = "relay";
35223
+ this.firstAttach = false;
35224
+ this.unavailable = null;
35225
+ link.onClose(() => {
35226
+ if (this.link !== link) return;
35227
+ this.link = null;
35228
+ this.role = "busy";
35229
+ this.options.log("The Polypad MCP server this one was sharing stopped. Looking for a new one to share, or taking over.");
35230
+ void this.attach(false).catch((error62) => this.options.log(`Bridge error: ${error62.message}`));
35231
+ });
35232
+ this.options.log(`Port ${this.requestedPort} is used by another Polypad MCP server (pid ${info.pid}): relaying through it, same Polypad tab and same code.`);
35233
+ this.options.log(`Pairing code: ${this.linkCode} (open Polypad, click Connect AI, enter this code)`);
35234
+ return true;
35235
+ }
35236
+ becomeBusy(text) {
35237
+ const first = this.role !== "busy" || this.unavailable !== text;
35238
+ this.role = "busy";
35239
+ this.port = this.requestedPort;
35240
+ this.unavailable = text;
35241
+ if (first) this.options.log(`Could not open the bridge: port ${this.requestedPort} is already in use. Trying again every ${Math.round(this.options.retryMs / 1e3)} s. Start with --port <other> to use another port.`);
35242
+ if (this.closed) return;
35243
+ this.retryTimer = setTimeout(() => {
35244
+ this.retryTimer = null;
35245
+ void this.attach(false).catch((error62) => this.options.log(`Bridge error: ${error62.message}`));
35246
+ }, this.options.retryMs);
35247
+ this.retryTimer.unref();
35248
+ }
35249
+ // --- the primary's socket server --------------------------------------------------------------------------------
35250
+ // Starts listening on 127.0.0.1. Rejects (for example EADDRINUSE) without leaving anything open.
35251
+ bind() {
35252
+ return new Promise((resolve, reject) => {
35253
+ const http = createServer((_req, res) => {
35254
+ res.writeHead(426, { "content-type": "text/plain" });
35255
+ res.end("Polypad MCP bridge: WebSocket only.\n");
35256
+ });
35257
+ const wss = new import_websocket_server.default({
35258
+ noServer: true,
35259
+ maxPayload: BRIDGE_MAX_MESSAGE_BYTES,
35260
+ perMessageDeflate: false
35261
+ });
35262
+ http.on("upgrade", (req, socket, head) => {
35263
+ const origin = req.headers.origin;
35264
+ if (origin !== void 0 && (typeof origin !== "string" || !this.allowedOrigins.has(normalizeOrigin(origin)))) {
35265
+ this.options.log(`Rejected a connection from origin ${typeof origin === "string" ? origin : "(unreadable)"}. Add it with --origin if it is yours.`);
35266
+ socket.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
35267
+ return;
35268
+ }
35269
+ if (wss.clients.size >= 16) {
35270
+ socket.end("HTTP/1.1 503 Service Unavailable\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
35271
+ return;
35272
+ }
35273
+ wss.handleUpgrade(req, socket, head, (ws) => this.accept(ws, origin !== void 0));
35274
+ });
35275
+ http.once("error", (error62) => {
35276
+ wss.close();
35277
+ reject(error62);
35278
+ });
35279
+ http.listen(this.requestedPort, "127.0.0.1", () => {
35280
+ this.http = http;
35281
+ this.wss = wss;
35282
+ this.port = http.address().port;
35283
+ http.on("error", (error62) => this.options.log(`Bridge error: ${error62.message}`));
35284
+ this.startPings();
35285
+ resolve();
35286
+ });
35287
+ });
35288
+ }
34953
35289
  send(ws, message) {
34954
35290
  if (ws.readyState === ws.OPEN) ws.send(encode3(message));
34955
35291
  }
34956
- accept(ws) {
35292
+ accept(ws, hasOrigin) {
34957
35293
  this.alive.add(ws);
34958
- let paired = false;
35294
+ let kind = "unknown";
34959
35295
  const helloTimer = setTimeout(() => {
34960
- if (!paired) ws.close(CLOSE_HELLO_TIMEOUT, "No hello received");
35296
+ if (kind === "unknown") ws.close(CLOSE_HELLO_TIMEOUT, "No hello received");
34961
35297
  }, this.options.helloTimeoutMs);
34962
35298
  ws.on("pong", () => this.alive.add(ws));
34963
35299
  ws.on("error", () => {
@@ -34965,7 +35301,7 @@ var Bridge = class {
34965
35301
  ws.on("message", (data, isBinary) => {
34966
35302
  this.alive.add(ws);
34967
35303
  if (isBinary) return void ws.close(CLOSE_BAD_MESSAGE, "Text frames only");
34968
- const message = parseTabMessage(rawToString(data));
35304
+ const message = parseClientMessage(rawToString2(data));
34969
35305
  if (!message) return void ws.close(CLOSE_BAD_MESSAGE, "Not a Polypad bridge message");
34970
35306
  switch (message.type) {
34971
35307
  case "ping":
@@ -34973,10 +35309,26 @@ var Bridge = class {
34973
35309
  case "pong":
34974
35310
  return;
34975
35311
  case "hello":
34976
- if (paired) return;
34977
- paired = this.pair(ws, message.code, message.v, message.client);
34978
- if (paired) clearTimeout(helloTimer);
35312
+ if (kind !== "unknown") return;
35313
+ if (!hasOrigin) return void ws.close(CLOSE_BAD_MESSAGE, "A Polypad tab connects from a web page (no Origin header)");
35314
+ if (this.pair(ws, message.code, message.v, message.client)) {
35315
+ kind = "tab";
35316
+ clearTimeout(helloTimer);
35317
+ }
35318
+ return;
35319
+ case "relay-hello":
35320
+ if (kind !== "unknown") return;
35321
+ if (this.admitRelay(ws, message.token, message.v)) {
35322
+ kind = "relay";
35323
+ clearTimeout(helloTimer);
35324
+ }
34979
35325
  return;
35326
+ case "relay-call": {
35327
+ if (kind !== "relay") return void ws.close(CLOSE_BAD_MESSAGE, "Say relay-hello first");
35328
+ const { id } = message;
35329
+ void this.call(message.name, message.input).then((result) => this.send(ws, { type: "relay-result", id, result }));
35330
+ return;
35331
+ }
34980
35332
  case "result":
34981
35333
  if (this.tab !== ws) return void ws.close(CLOSE_BAD_MESSAGE, "Pair first");
34982
35334
  if (this.pending.get(message.id)?.socket === ws) this.settle(message.id, message.result);
@@ -34985,6 +35337,10 @@ var Bridge = class {
34985
35337
  });
34986
35338
  ws.on("close", () => {
34987
35339
  clearTimeout(helloTimer);
35340
+ if (kind === "relay") {
35341
+ this.relays.delete(ws);
35342
+ this.options.log("A Polypad MCP server that shared this one has left.");
35343
+ }
34988
35344
  if (this.tab === ws) {
34989
35345
  this.tab = null;
34990
35346
  this.tabName = void 0;
@@ -34993,17 +35349,21 @@ var Bridge = class {
34993
35349
  this.failPending(ws, "Polypad disconnected before it answered (the tab was closed, reloaded or disconnected). The change may or may not have been applied. Ask the user to reconnect with Connect AI, then call get_project.");
34994
35350
  });
34995
35351
  }
35352
+ // Wrong attempts within the window; true when no more are tolerated.
35353
+ tooMany(times) {
35354
+ const now = Date.now();
35355
+ while (times.length && now - times[0] > this.options.badCodeWindowMs) times.shift();
35356
+ return times.length >= this.options.maxBadCodes;
35357
+ }
34996
35358
  // Returns true when the tab is now the paired one; otherwise closes it with the reason.
34997
35359
  pair(ws, code, version2, client) {
34998
- const now = Date.now();
34999
- while (this.badCodeTimes.length && now - this.badCodeTimes[0] > this.options.badCodeWindowMs) this.badCodeTimes.shift();
35000
- if (this.badCodeTimes.length >= this.options.maxBadCodes) {
35360
+ if (this.tooMany(this.badCodeTimes)) {
35001
35361
  ws.close(CLOSE_RATE_LIMITED, "Too many wrong codes. Wait a minute or restart the MCP server.");
35002
35362
  return false;
35003
35363
  }
35004
35364
  const given = normalizePairingCode(code);
35005
- if (!given || !codesMatch(given, this.code)) {
35006
- this.badCodeTimes.push(now);
35365
+ if (!given || !tokensMatch(given, this.ownCode)) {
35366
+ this.badCodeTimes.push(Date.now());
35007
35367
  this.options.log("Rejected a pairing attempt with a wrong code.");
35008
35368
  ws.close(CLOSE_BAD_CODE, "Wrong pairing code");
35009
35369
  return false;
@@ -35024,6 +35384,27 @@ var Bridge = class {
35024
35384
  this.options.log(`Polypad connected${client ? ` (${client})` : ""}.`);
35025
35385
  return true;
35026
35386
  }
35387
+ // Returns true when the socket is now a relay; otherwise closes it with the reason.
35388
+ admitRelay(ws, token, version2) {
35389
+ if (this.tooMany(this.badTokenTimes)) {
35390
+ ws.close(CLOSE_RATE_LIMITED, "Too many wrong tokens");
35391
+ return false;
35392
+ }
35393
+ if (!this.relayToken || !tokensMatch(token, this.relayToken)) {
35394
+ this.badTokenTimes.push(Date.now());
35395
+ this.options.log("Rejected a relay with a wrong token.");
35396
+ ws.close(CLOSE_BAD_TOKEN, "Wrong relay token");
35397
+ return false;
35398
+ }
35399
+ if (version2 !== BRIDGE_PROTOCOL_VERSION) {
35400
+ ws.close(CLOSE_BAD_VERSION, `Protocol version ${version2} is not supported (server speaks ${BRIDGE_PROTOCOL_VERSION})`);
35401
+ return false;
35402
+ }
35403
+ this.relays.add(ws);
35404
+ this.send(ws, { type: "relay-welcome", v: BRIDGE_PROTOCOL_VERSION, code: this.ownCode, server: "polypad-mcp" });
35405
+ this.options.log("Another Polypad MCP server (Claude Code or Desktop, say) now shares this one.");
35406
+ return true;
35407
+ }
35027
35408
  settle(id, result) {
35028
35409
  const entry = this.pending.get(id);
35029
35410
  if (!entry) return;
@@ -35034,7 +35415,7 @@ var Bridge = class {
35034
35415
  // Fails the calls waiting on `socket`, or all of them when it is null.
35035
35416
  failPending(socket, text) {
35036
35417
  for (const [id, entry] of [...this.pending]) {
35037
- if (socket === null || entry.socket === socket) this.settle(id, fail(text));
35418
+ if (socket === null || entry.socket === socket) this.settle(id, fail2(text));
35038
35419
  }
35039
35420
  }
35040
35421
  startPings() {
@@ -35057,7 +35438,7 @@ var Bridge = class {
35057
35438
  this.pingTimer.unref();
35058
35439
  }
35059
35440
  };
35060
- function rawToString(data) {
35441
+ function rawToString2(data) {
35061
35442
  if (typeof data === "string") return data;
35062
35443
  if (Array.isArray(data)) return Buffer.concat(data).toString("utf8");
35063
35444
  if (data instanceof ArrayBuffer) return Buffer.from(data).toString("utf8");
@@ -35070,12 +35451,23 @@ var SERVER_INSTRUCTIONS = `Polypad is a parametric CAD editor. These tools build
35070
35451
  Units and axes: lengths are millimetres, angles degrees, Z is up. Any length field accepts a number or an expression over the project's parameters ("width / 2"); prefer set_parameter for sizes that belong together.
35071
35452
 
35072
35453
  Workflow:
35073
- 1. Start with get_project to see what is already there (the user may have work open). new_project replaces the open project, so use it only when asked or when the project is empty.
35454
+ 1. Start with get_project to see what is already there (the user may have work open). new_project replaces the open project: if it has features, ask the user first and never call it over their work; use it freely only when the project is empty.
35074
35455
  2. Build with add_primitive, or add_sketch then extrude / revolve. Ids returned by a command are the feature and body ids for later calls.
35075
35456
  3. For hole, shell, fillet, chamfer and for sketching on a face, call list_faces or list_edges first. Pass the indices they return together with the same rebuildId. The rebuildId changes whenever the model changes, so after any change list again; a stale one is refused ("Call list_edges again").
35076
35457
  4. A feature that does not build stays in the project with its error: fix it with update_feature, or remove it with delete_feature or undo.
35077
35458
  5. Check your work: measure gives volume and bounding box, and render_view returns a PNG (iso, top, front, right) so you can look at the result. Compare the numbers with what was asked before you finish.
35078
35459
 
35460
+ Pick the tool by task:
35461
+ - Hollow part (enclosure, box, cup): shell the solid with the open face(s) removed (list_faces with filter normal "+z" finds the top); don't subtract a smaller box.
35462
+ - Screw holes, standoffs, inserts: hole (size, fit close/normal/loose/tap/insert, counterbore or countersink, blind depth), not cut circles.
35463
+ - Repeated parts (rows, grids, around a circle, lattices): model one body, or one cutter body, then pattern it (then combine subtract for a cutter). Symmetric parts: model one side, then mirror.
35464
+ - Round parts (knobs, turrets, finials, domes): add_sketch on XZ (sketch x = world X, sketch y = world Z), draw half the outline at x >= 0, then revolve with axis "y" (the world Z axis).
35465
+ - Sizes that belong together or may change: set_parameter first, then use the names in fields.
35466
+ - Changing or fixing a feature: update_feature (or undo the last step); don't delete and rebuild.
35467
+ - Picking edges: use the list_edges filter (face, direction, kind) and the list_faces filter (normal, kind) instead of reading every edge. For the rim of the top: list_faces normal "+z", then list_edges face <index>.
35468
+ - Fillet fails: try a smaller radius or fewer edges at once, or use chamfer.
35469
+ - Clearances and collisions: measure gives bounding boxes; combine intersect on two bodies, then measure, shows an overlap; then undo.
35470
+
35079
35471
  Results: a result with isError set was refused or something in the model did not build; the text says what and how to fix it. Nothing is changed by a refused call.`;
35080
35472
  function toToolResult(result) {
35081
35473
  const content = [{ type: "text", text: result.text }];
@@ -35104,7 +35496,7 @@ function createPolypadMcp(options = {}) {
35104
35496
  }
35105
35497
 
35106
35498
  // src/index.ts
35107
- var VERSION = true ? "0.1.0" : "0.0.0-dev";
35499
+ var VERSION = true ? "0.1.2" : "0.0.0-dev";
35108
35500
  var err = (line2) => process.stderr.write(`${line2}
35109
35501
  `);
35110
35502
  async function main() {
@@ -35129,12 +35521,9 @@ ${USAGE}`);
35129
35521
  const mcp = createPolypadMcp({ port: cli.port, origins: cli.origins, version: VERSION, log: err });
35130
35522
  try {
35131
35523
  await mcp.bridge.listen();
35132
- err(`Polypad MCP server ready on ws://127.0.0.1:${mcp.bridge.port}`);
35133
- err(`Pairing code: ${mcp.bridge.code} (open Polypad, click Connect AI, enter this code)`);
35134
35524
  } catch (error62) {
35135
- const reason = error62.code === "EADDRINUSE" ? `port ${cli.port} is already in use (another polypad-mcp running?). Start with --port <other>.` : error62.message;
35136
- mcp.bridge.unavailable = `Polypad's local bridge could not start: ${reason} Tell the user; Polypad cannot be reached until it runs.`;
35137
- err(`Could not open the bridge: ${reason}`);
35525
+ mcp.bridge.unavailable = `Polypad's local bridge could not start: ${error62.message} Tell the user; Polypad cannot be reached until it runs.`;
35526
+ err(`Could not open the bridge: ${error62.message}`);
35138
35527
  }
35139
35528
  const shutdown = () => {
35140
35529
  void mcp.close().finally(() => process.exit(0));
@@ -35143,6 +35532,7 @@ ${USAGE}`);
35143
35532
  process.on("SIGTERM", shutdown);
35144
35533
  process.stdin.on("end", shutdown);
35145
35534
  process.stdin.on("close", shutdown);
35535
+ process.on("exit", () => mcp.bridge.releaseSync());
35146
35536
  await mcp.server.connect(new StdioServerTransport());
35147
35537
  }
35148
35538
  main().catch((error62) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polypad/mcp",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "description": "MCP server that lets Claude, Cursor or another AI assistant build and inspect 3D models in your open Polypad tab (polypad.ink). Runs locally, nothing is uploaded.",
6
6
  "license": "MIT",