@polypad/mcp 0.1.0 → 0.1.1
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 +10 -0
- package/dist/index.js +454 -75
- 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 =
|
|
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) {
|
|
@@ -34802,7 +34840,7 @@ function commandSchemas() {
|
|
|
34802
34840
|
}
|
|
34803
34841
|
|
|
34804
34842
|
// src/bridge.ts
|
|
34805
|
-
import { randomInt, randomUUID
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
34870
|
-
|
|
34871
|
-
|
|
34872
|
-
|
|
34873
|
-
|
|
34874
|
-
|
|
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(
|
|
34912
|
-
const id =
|
|
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
|
-
|
|
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,
|
|
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
|
-
//
|
|
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
|
|
35294
|
+
let kind = "unknown";
|
|
34959
35295
|
const helloTimer = setTimeout(() => {
|
|
34960
|
-
if (
|
|
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 =
|
|
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 (
|
|
34977
|
-
|
|
34978
|
-
if (
|
|
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
|
+
}
|
|
34979
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
|
+
}
|
|
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
|
-
|
|
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 || !
|
|
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,
|
|
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
|
|
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");
|
|
@@ -35104,7 +35485,7 @@ function createPolypadMcp(options = {}) {
|
|
|
35104
35485
|
}
|
|
35105
35486
|
|
|
35106
35487
|
// src/index.ts
|
|
35107
|
-
var VERSION = true ? "0.1.
|
|
35488
|
+
var VERSION = true ? "0.1.1" : "0.0.0-dev";
|
|
35108
35489
|
var err = (line2) => process.stderr.write(`${line2}
|
|
35109
35490
|
`);
|
|
35110
35491
|
async function main() {
|
|
@@ -35129,12 +35510,9 @@ ${USAGE}`);
|
|
|
35129
35510
|
const mcp = createPolypadMcp({ port: cli.port, origins: cli.origins, version: VERSION, log: err });
|
|
35130
35511
|
try {
|
|
35131
35512
|
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
35513
|
} catch (error62) {
|
|
35135
|
-
|
|
35136
|
-
|
|
35137
|
-
err(`Could not open the bridge: ${reason}`);
|
|
35514
|
+
mcp.bridge.unavailable = `Polypad's local bridge could not start: ${error62.message} Tell the user; Polypad cannot be reached until it runs.`;
|
|
35515
|
+
err(`Could not open the bridge: ${error62.message}`);
|
|
35138
35516
|
}
|
|
35139
35517
|
const shutdown = () => {
|
|
35140
35518
|
void mcp.close().finally(() => process.exit(0));
|
|
@@ -35143,6 +35521,7 @@ ${USAGE}`);
|
|
|
35143
35521
|
process.on("SIGTERM", shutdown);
|
|
35144
35522
|
process.stdin.on("end", shutdown);
|
|
35145
35523
|
process.stdin.on("close", shutdown);
|
|
35524
|
+
process.on("exit", () => mcp.bridge.releaseSync());
|
|
35146
35525
|
await mcp.server.connect(new StdioServerTransport());
|
|
35147
35526
|
}
|
|
35148
35527
|
main().catch((error62) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polypad/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
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",
|