honeybook-mcp 0.1.13 → 0.2.4

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.
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "metadata": {
9
9
  "description": "HoneyBook client-portal tools for Claude Code",
10
- "version": "0.1.13"
10
+ "version": "0.2.4"
11
11
  },
12
12
  "plugins": [
13
13
  {
@@ -15,7 +15,7 @@
15
15
  "displayName": "HoneyBook",
16
16
  "source": "./",
17
17
  "description": "HoneyBook client-portal MCP for Claude — view wedding-vendor contracts and invoices via MCP",
18
- "version": "0.1.13",
18
+ "version": "0.2.4",
19
19
  "author": {
20
20
  "name": "Chris Chall"
21
21
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "honeybook",
3
3
  "displayName": "HoneyBook",
4
- "version": "0.1.13",
4
+ "version": "0.2.4",
5
5
  "description": "HoneyBook client-portal MCP for Claude — view wedding-vendor contracts and invoices via MCP",
6
6
  "author": {
7
7
  "name": "Chris Chall",
package/README.md CHANGED
@@ -22,6 +22,28 @@ Ask Claude things like:
22
22
  - Magic-link emails from the wedding vendors that use HoneyBook
23
23
  - [fetchproxy 0.3.0 browser extension](https://github.com/chrischall/fetchproxy) — installed in Chrome (Web Store) or Safari (.dmg). Used by `use_magic_link` to snapshot the session out of your signed-in vendor portal tab.
24
24
 
25
+ ## Acknowledgement of Terms
26
+
27
+ By using this MCP server, you acknowledge and agree to the following:
28
+
29
+ **1. This server accesses your own HoneyBook workspace.** Every request is dispatched through your own signed-in browser session via the fetchproxy extension. It does not — and cannot — access anyone else's workspace.
30
+
31
+ **2. [HoneyBook's Terms of Service](https://www.honeybook.com/legal/terms-of-service) govern your use of this server**, just as they govern your direct use of honeybook.com. The clauses most relevant here:
32
+
33
+ > In connection with your use of the Service you will not engage in or use any data mining, robots, scraping or similar data gathering or extraction methods.
34
+
35
+ And: users may not "modify, copy, frame, scrape, rent, lease, loan, sell, distribute or create derivative works based on the Service or the Service Content."
36
+
37
+ You are agreeing to those terms — read by the maintainer 2026-05-23 — every time you invoke a tool in this server. HoneyBook's ToS broadly prohibits data mining and scraping; this MCP is an unofficial automation tool and HoneyBook has not granted it an exception.
38
+
39
+ **3. Personal, single-business use only.** This project is not affiliated with, endorsed by, sponsored by, or in partnership with HoneyBook, Inc. It is a personal automation tool that an individual HoneyBook account holder can use to drive their own workspace. Do not use it on behalf of another business, do not bulk-extract HoneyBook's directory or template content, and do not create a derivative SaaS product on top of it.
40
+
41
+ **4. Stability is not guaranteed.** This server calls internal HoneyBook endpoints that HoneyBook may change without notice. It may break.
42
+
43
+ **5. You accept full responsibility** for any consequences of using this server in connection with your HoneyBook account — rate limiting, account warnings, suspension, or any enforcement action HoneyBook takes. HoneyBook can detect automated traffic and may block your IP or workspace. If HoneyBook objects to your use, stop using this server.
44
+
45
+ This section is the maintainer's good-faith summary of the terms — it is not legal advice and does not modify or supersede HoneyBook's actual ToS.
46
+
25
47
  ## Installation
26
48
 
27
49
  ### From source
package/dist/bundle.js CHANGED
@@ -7571,6 +7571,10 @@ var require_receiver = __commonJS({
7571
7571
  * extensions
7572
7572
  * @param {Boolean} [options.isServer=false] Specifies whether to operate in
7573
7573
  * client or server mode
7574
+ * @param {Number} [options.maxBufferedChunks=0] The maximum number of
7575
+ * buffered data chunks
7576
+ * @param {Number} [options.maxFragments=0] The maximum number of message
7577
+ * fragments
7574
7578
  * @param {Number} [options.maxPayload=0] The maximum allowed message length
7575
7579
  * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
7576
7580
  * not to skip UTF-8 validation for text and close messages
@@ -7581,6 +7585,8 @@ var require_receiver = __commonJS({
7581
7585
  this._binaryType = options.binaryType || BINARY_TYPES[0];
7582
7586
  this._extensions = options.extensions || {};
7583
7587
  this._isServer = !!options.isServer;
7588
+ this._maxBufferedChunks = options.maxBufferedChunks | 0;
7589
+ this._maxFragments = options.maxFragments | 0;
7584
7590
  this._maxPayload = options.maxPayload | 0;
7585
7591
  this._skipUTF8Validation = !!options.skipUTF8Validation;
7586
7592
  this[kWebSocket] = void 0;
@@ -7610,6 +7616,18 @@ var require_receiver = __commonJS({
7610
7616
  */
7611
7617
  _write(chunk, encoding, cb) {
7612
7618
  if (this._opcode === 8 && this._state == GET_INFO) return cb();
7619
+ if (this._maxBufferedChunks > 0 && this._buffers.length >= this._maxBufferedChunks) {
7620
+ cb(
7621
+ this.createError(
7622
+ RangeError,
7623
+ "Too many buffered chunks",
7624
+ false,
7625
+ 1008,
7626
+ "WS_ERR_TOO_MANY_BUFFERED_PARTS"
7627
+ )
7628
+ );
7629
+ return;
7630
+ }
7613
7631
  this._bufferedBytes += chunk.length;
7614
7632
  this._buffers.push(chunk);
7615
7633
  this.startLoop(cb);
@@ -7939,6 +7957,17 @@ var require_receiver = __commonJS({
7939
7957
  return;
7940
7958
  }
7941
7959
  if (data.length) {
7960
+ if (this._maxFragments > 0 && this._fragments.length >= this._maxFragments) {
7961
+ const error51 = this.createError(
7962
+ RangeError,
7963
+ "Too many message fragments",
7964
+ false,
7965
+ 1008,
7966
+ "WS_ERR_TOO_MANY_BUFFERED_PARTS"
7967
+ );
7968
+ cb(error51);
7969
+ return;
7970
+ }
7942
7971
  this._messageLength = this._totalPayloadLength;
7943
7972
  this._fragments.push(data);
7944
7973
  }
@@ -7968,6 +7997,17 @@ var require_receiver = __commonJS({
7968
7997
  cb(error51);
7969
7998
  return;
7970
7999
  }
8000
+ if (this._maxFragments > 0 && this._fragments.length >= this._maxFragments) {
8001
+ const error51 = this.createError(
8002
+ RangeError,
8003
+ "Too many message fragments",
8004
+ false,
8005
+ 1008,
8006
+ "WS_ERR_TOO_MANY_BUFFERED_PARTS"
8007
+ );
8008
+ cb(error51);
8009
+ return;
8010
+ }
7971
8011
  this._fragments.push(buf);
7972
8012
  }
7973
8013
  this.dataMessage(cb);
@@ -9174,6 +9214,10 @@ var require_websocket = __commonJS({
9174
9214
  * multiple times in the same tick
9175
9215
  * @param {Function} [options.generateMask] The function used to generate the
9176
9216
  * masking key
9217
+ * @param {Number} [options.maxBufferedChunks=0] The maximum number of
9218
+ * buffered data chunks
9219
+ * @param {Number} [options.maxFragments=0] The maximum number of message
9220
+ * fragments
9177
9221
  * @param {Number} [options.maxPayload=0] The maximum allowed message size
9178
9222
  * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
9179
9223
  * not to skip UTF-8 validation for text and close messages
@@ -9185,6 +9229,8 @@ var require_websocket = __commonJS({
9185
9229
  binaryType: this.binaryType,
9186
9230
  extensions: this._extensions,
9187
9231
  isServer: this._isServer,
9232
+ maxBufferedChunks: options.maxBufferedChunks,
9233
+ maxFragments: options.maxFragments,
9188
9234
  maxPayload: options.maxPayload,
9189
9235
  skipUTF8Validation: options.skipUTF8Validation
9190
9236
  });
@@ -9484,6 +9530,8 @@ var require_websocket = __commonJS({
9484
9530
  autoPong: true,
9485
9531
  closeTimeout: CLOSE_TIMEOUT,
9486
9532
  protocolVersion: protocolVersions[1],
9533
+ maxBufferedChunks: 1024 * 1024,
9534
+ maxFragments: 128 * 1024,
9487
9535
  maxPayload: 100 * 1024 * 1024,
9488
9536
  skipUTF8Validation: false,
9489
9537
  perMessageDeflate: true,
@@ -9726,6 +9774,8 @@ var require_websocket = __commonJS({
9726
9774
  websocket.setSocket(socket, head, {
9727
9775
  allowSynchronousEvents: opts.allowSynchronousEvents,
9728
9776
  generateMask: opts.generateMask,
9777
+ maxBufferedChunks: opts.maxBufferedChunks,
9778
+ maxFragments: opts.maxFragments,
9729
9779
  maxPayload: opts.maxPayload,
9730
9780
  skipUTF8Validation: opts.skipUTF8Validation
9731
9781
  });
@@ -10068,6 +10118,10 @@ var require_websocket_server = __commonJS({
10068
10118
  * called
10069
10119
  * @param {Function} [options.handleProtocols] A hook to handle protocols
10070
10120
  * @param {String} [options.host] The hostname where to bind the server
10121
+ * @param {Number} [options.maxBufferedChunks=1048576] The maximum number of
10122
+ * buffered data chunks
10123
+ * @param {Number} [options.maxFragments=131072] The maximum number of message
10124
+ * fragments
10071
10125
  * @param {Number} [options.maxPayload=104857600] The maximum allowed message
10072
10126
  * size
10073
10127
  * @param {Boolean} [options.noServer=false] Enable no server mode
@@ -10089,6 +10143,8 @@ var require_websocket_server = __commonJS({
10089
10143
  options = {
10090
10144
  allowSynchronousEvents: true,
10091
10145
  autoPong: true,
10146
+ maxBufferedChunks: 1024 * 1024,
10147
+ maxFragments: 128 * 1024,
10092
10148
  maxPayload: 100 * 1024 * 1024,
10093
10149
  skipUTF8Validation: false,
10094
10150
  perMessageDeflate: false,
@@ -10368,6 +10424,8 @@ var require_websocket_server = __commonJS({
10368
10424
  socket.removeListener("error", socketOnError);
10369
10425
  ws.setSocket(socket, head, {
10370
10426
  allowSynchronousEvents: this.options.allowSynchronousEvents,
10427
+ maxBufferedChunks: this.options.maxBufferedChunks,
10428
+ maxFragments: this.options.maxFragments,
10371
10429
  maxPayload: this.options.maxPayload,
10372
10430
  skipUTF8Validation: this.options.skipUTF8Validation
10373
10431
  });
@@ -34701,6 +34759,7 @@ async function getActiveClient(origin) {
34701
34759
 
34702
34760
  // node_modules/@fetchproxy/protocol/dist/frames.js
34703
34761
  var PROTOCOL_VERSION = 2;
34762
+ var HKDF_SESSION_INFO = "fetchproxy/1.0.0/session";
34704
34763
  var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
34705
34764
  "fetch",
34706
34765
  "read_cookies",
@@ -34775,7 +34834,7 @@ var ProtocolError = class extends Error {
34775
34834
  var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
34776
34835
  var BASE64_RE = /^[A-Za-z0-9+/]*={0,2}$/;
34777
34836
  var SCOPE_KEY_RE = /^[A-Za-z0-9_.\-]{1,256}$/;
34778
- var SCOPE_KEY_GLOB_RE = /^[A-Za-z0-9_.\-]{1,255}\*?$/;
34837
+ var SCOPE_KEY_GLOB_RE = /^[A-Za-z0-9_.\-]{1,256}\*?$/;
34779
34838
  var HEADER_NAME_RE = /^[A-Za-z0-9_\-]{1,128}$/;
34780
34839
  var HOSTNAME_RE = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$/i;
34781
34840
  function assertObject(x, label) {
@@ -34904,7 +34963,7 @@ function assertStoragePointersArray(value, label, declaredKeys) {
34904
34963
  if (entry.jsonPointer === void 0) {
34905
34964
  throw new ProtocolError(`${label}[${i}].jsonPointer: missing`);
34906
34965
  }
34907
- if (typeof entry.key !== "string" || !SCOPE_KEY_GLOB_RE.test(entry.key)) {
34966
+ if (typeof entry.key !== "string" || !SCOPE_KEY_RE.test(entry.key)) {
34908
34967
  throw new ProtocolError(`${label}[${i}].key: invalid key ${JSON.stringify(entry.key)}`);
34909
34968
  }
34910
34969
  if (typeof entry.jsonPointer !== "string" || !isValidJsonPointer(entry.jsonPointer)) {
@@ -35003,6 +35062,8 @@ function validateFrame(raw) {
35003
35062
  return validateReady(raw);
35004
35063
  if (t === "frame")
35005
35064
  return validateEncrypted(raw);
35065
+ if (t === "pair-pending")
35066
+ return validatePairPending(raw);
35006
35067
  throw new ProtocolError(`unknown frame type: ${String(t)}`);
35007
35068
  }
35008
35069
  function validateHello(raw) {
@@ -35104,6 +35165,17 @@ function validateEncrypted(raw) {
35104
35165
  assertBase64(raw.ciphertext, "frame.ciphertext");
35105
35166
  return raw;
35106
35167
  }
35168
+ var PAIR_CODE_RE = /^\d{3}-\d{3}$/;
35169
+ function validatePairPending(raw) {
35170
+ assertString(raw.mcpId, "pair-pending.mcpId");
35171
+ if (!isValidMcpId(raw.mcpId))
35172
+ throw new ProtocolError("pair-pending.mcpId: invalid format");
35173
+ assertString(raw.pairCode, "pair-pending.pairCode");
35174
+ if (!PAIR_CODE_RE.test(raw.pairCode)) {
35175
+ throw new ProtocolError(`pair-pending.pairCode: must match XXX-XXX, got ${String(raw.pairCode)}`);
35176
+ }
35177
+ return { type: "pair-pending", mcpId: raw.mcpId, pairCode: raw.pairCode };
35178
+ }
35107
35179
  function validateInnerFrame(raw) {
35108
35180
  assertObject(raw, "inner");
35109
35181
  const t = raw.type;
@@ -35686,15 +35758,22 @@ async function startHost(opts) {
35686
35758
  let extensionWs = null;
35687
35759
  const peers = /* @__PURE__ */ new Map();
35688
35760
  const ownInnerListeners = [];
35761
+ const disconnectListeners = [];
35762
+ const pendingPairListeners = [];
35689
35763
  let ownSession = null;
35764
+ let ownPendingPairCode = null;
35690
35765
  let resolveOwnSession;
35691
35766
  let rejectOwnSession;
35692
- const ownSessionReady = new Promise((resolve, reject) => {
35693
- resolveOwnSession = resolve;
35694
- rejectOwnSession = reject;
35695
- });
35696
- ownSessionReady.catch(() => {
35697
- });
35767
+ let ownSessionReady;
35768
+ function resetSessionPromise() {
35769
+ ownSessionReady = new Promise((resolve, reject) => {
35770
+ resolveOwnSession = resolve;
35771
+ rejectOwnSession = reject;
35772
+ });
35773
+ ownSessionReady.catch(() => {
35774
+ });
35775
+ }
35776
+ resetSessionPromise();
35698
35777
  let extensionHello = null;
35699
35778
  wss.on("connection", (ws) => {
35700
35779
  let identified = null;
@@ -35763,11 +35842,12 @@ async function startHost(opts) {
35763
35842
  }
35764
35843
  const extPub = fromB64(frame.extensionSessionPub);
35765
35844
  const shared = await ecdhX25519(opts.ownIdentity.x25519Priv, extPub);
35766
- const key = await hkdfSha256(shared, ownSessionNonce, enc2.encode("fetchproxy/0.1.0/session"), 32);
35767
- if (!ownSession) {
35768
- ownSession = new SessionState(key);
35769
- resolveOwnSession(ownSession);
35770
- }
35845
+ const key = await hkdfSha256(shared, ownSessionNonce, enc2.encode(HKDF_SESSION_INFO), 32);
35846
+ if (extensionWs !== ws)
35847
+ return;
35848
+ ownSession = new SessionState(key);
35849
+ ownPendingPairCode = null;
35850
+ resolveOwnSession(ownSession);
35771
35851
  } else {
35772
35852
  const slot = peers.get(frame.mcpId);
35773
35853
  if (slot)
@@ -35794,6 +35874,16 @@ async function startHost(opts) {
35794
35874
  extensionWs.send(JSON.stringify(frame));
35795
35875
  }
35796
35876
  }
35877
+ if (frame.type === "pair-pending" && identified === "extension") {
35878
+ if (frame.mcpId === opts.ownMcpId) {
35879
+ ownPendingPairCode = frame.pairCode;
35880
+ pendingPairListeners.forEach((cb) => cb(frame.pairCode));
35881
+ } else {
35882
+ const slot = peers.get(frame.mcpId);
35883
+ if (slot)
35884
+ slot.ws.send(JSON.stringify(frame));
35885
+ }
35886
+ }
35797
35887
  } catch (e) {
35798
35888
  console.error("[fetchproxy] host: message handler error:", e);
35799
35889
  try {
@@ -35809,6 +35899,9 @@ async function startHost(opts) {
35809
35899
  if (!ownSession) {
35810
35900
  rejectOwnSession(new Error("extension disconnected before ready"));
35811
35901
  }
35902
+ ownSession = null;
35903
+ resetSessionPromise();
35904
+ disconnectListeners.forEach((cb) => cb());
35812
35905
  }
35813
35906
  if (identified === "peer" && peerMcpId)
35814
35907
  peers.delete(peerMcpId);
@@ -35833,7 +35926,14 @@ async function startHost(opts) {
35833
35926
  },
35834
35927
  onOwnInner: (cb) => {
35835
35928
  ownInnerListeners.push(cb);
35836
- }
35929
+ },
35930
+ onExtensionDisconnect: (cb) => {
35931
+ disconnectListeners.push(cb);
35932
+ },
35933
+ onPendingPair: (cb) => {
35934
+ pendingPairListeners.push(cb);
35935
+ },
35936
+ pendingPairCode: () => ownPendingPairCode
35837
35937
  };
35838
35938
  }
35839
35939
 
@@ -35863,36 +35963,57 @@ async function startPeer(opts) {
35863
35963
  const sessionNonce = fromB64(hello.sessionNonce);
35864
35964
  ws.send(JSON.stringify(hello));
35865
35965
  const innerListeners = [];
35966
+ const renegotiateListeners = [];
35967
+ const pendingPairListeners = [];
35866
35968
  let session = null;
35969
+ let pendingPairCode = null;
35970
+ let resolveFirstReady;
35971
+ let rejectFirstReady;
35867
35972
  const sessionPromise = new Promise((resolve, reject) => {
35868
- const onMessage = async (data) => {
35869
- try {
35870
- const raw = JSON.parse(data.toString());
35871
- const frame = validateFrame(raw);
35872
- if (frame.type === "ready" && frame.mcpId === opts.mcpId) {
35873
- const extPub = fromB64(frame.extensionSessionPub);
35874
- const shared = await ecdhX25519(opts.identity.x25519Priv, extPub);
35875
- const sessionKey = await hkdfSha256(shared, sessionNonce, enc3.encode("fetchproxy/0.1.0/session"), 32);
35876
- session = new SessionState(sessionKey);
35877
- resolve(session);
35878
- return;
35973
+ resolveFirstReady = resolve;
35974
+ rejectFirstReady = reject;
35975
+ });
35976
+ const onMessage = async (data) => {
35977
+ try {
35978
+ const raw = JSON.parse(data.toString());
35979
+ const frame = validateFrame(raw);
35980
+ if (frame.type === "ready" && frame.mcpId === opts.mcpId) {
35981
+ const extPub = fromB64(frame.extensionSessionPub);
35982
+ const shared = await ecdhX25519(opts.identity.x25519Priv, extPub);
35983
+ const sessionKey = await hkdfSha256(shared, sessionNonce, enc3.encode(HKDF_SESSION_INFO), 32);
35984
+ const isRenegotiation = session !== null;
35985
+ session = new SessionState(sessionKey);
35986
+ pendingPairCode = null;
35987
+ if (isRenegotiation) {
35988
+ renegotiateListeners.forEach((cb) => cb());
35989
+ } else {
35990
+ resolveFirstReady(session);
35879
35991
  }
35880
- if (frame.type === "frame" && frame.mcpId === opts.mcpId) {
35881
- if (!session)
35882
- return;
35883
- if (!session.acceptInboundSeq(frame.seq))
35884
- return;
35992
+ return;
35993
+ }
35994
+ if (frame.type === "pair-pending" && frame.mcpId === opts.mcpId) {
35995
+ pendingPairCode = frame.pairCode;
35996
+ pendingPairListeners.forEach((cb) => cb(frame.pairCode));
35997
+ return;
35998
+ }
35999
+ if (frame.type === "frame" && frame.mcpId === opts.mcpId) {
36000
+ if (!session)
36001
+ return;
36002
+ if (!session.acceptInboundSeq(frame.seq))
36003
+ return;
36004
+ try {
35885
36005
  const inner = await openEncryptedFrame(session.sessionKey, frame);
35886
36006
  innerListeners.forEach((cb) => cb(inner));
36007
+ } catch {
35887
36008
  }
35888
- } catch (e) {
35889
- reject(e instanceof Error ? e : new Error(String(e)));
35890
36009
  }
35891
- };
35892
- ws.on("message", onMessage);
35893
- ws.once("close", () => {
35894
- reject(new Error("peer WS closed before ready"));
35895
- });
36010
+ } catch (e) {
36011
+ rejectFirstReady(e instanceof Error ? e : new Error(String(e)));
36012
+ }
36013
+ };
36014
+ ws.on("message", onMessage);
36015
+ ws.once("close", () => {
36016
+ rejectFirstReady(new Error("peer WS closed before ready"));
35896
36017
  });
35897
36018
  sessionPromise.catch(() => {
35898
36019
  });
@@ -35900,13 +36021,21 @@ async function startPeer(opts) {
35900
36021
  ws,
35901
36022
  session: sessionPromise,
35902
36023
  sendInner: async (inner) => {
35903
- const s = await sessionPromise;
36024
+ await sessionPromise;
36025
+ const s = session;
35904
36026
  const sealed = await sealInnerFrame(s.sessionKey, opts.mcpId, s.nextOutboundSeq(), inner);
35905
36027
  ws.send(JSON.stringify(sealed));
35906
36028
  },
35907
36029
  onInner: (cb) => {
35908
36030
  innerListeners.push(cb);
35909
36031
  },
36032
+ onRenegotiate: (cb) => {
36033
+ renegotiateListeners.push(cb);
36034
+ },
36035
+ onPendingPair: (cb) => {
36036
+ pendingPairListeners.push(cb);
36037
+ },
36038
+ pendingPairCode: () => pendingPairCode,
35910
36039
  close: () => ws.close()
35911
36040
  };
35912
36041
  return handle;
@@ -35963,6 +36092,32 @@ async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
35963
36092
  return id;
35964
36093
  }
35965
36094
 
36095
+ // node_modules/@fetchproxy/server/dist/error-kind.js
36096
+ function classifyFetchError(error51) {
36097
+ if (/Could not establish connection/i.test(error51) || /Receiving end does not exist/i.test(error51)) {
36098
+ return "content_script_unreachable";
36099
+ }
36100
+ if (/^tab fetch failed:/.test(error51)) {
36101
+ return "tab_fetch_failed";
36102
+ }
36103
+ if (/^fetch threw:/.test(error51)) {
36104
+ return "tab_fetch_failed";
36105
+ }
36106
+ if (/^no tab matching /.test(error51)) {
36107
+ return "no_tab";
36108
+ }
36109
+ if (/not in domains \[/.test(error51)) {
36110
+ return "domain_denied";
36111
+ }
36112
+ if (/^capability .+ not granted/.test(error51)) {
36113
+ return "capability_denied";
36114
+ }
36115
+ if (/^(request|response) body too large:/.test(error51)) {
36116
+ return "body_too_large";
36117
+ }
36118
+ return "other";
36119
+ }
36120
+
35966
36121
  // node_modules/@fetchproxy/server/dist/ws-server.js
35967
36122
  var FetchproxyProtocolError = class extends Error {
35968
36123
  constructor(message) {
@@ -36005,7 +36160,11 @@ function assertUrlInDomains(field, url2, domains) {
36005
36160
  }
36006
36161
  var DEFAULT_JSON_OK_STATUSES = [200, 201, 202, 204];
36007
36162
  var FetchproxyServer = class {
36008
- /** Set after `listen()` succeeds. Null while not listening. */
36163
+ /**
36164
+ * Bridge role. `null` until the first verb call (or an explicit
36165
+ * `connect()`) — `listen()` no longer triggers the role election
36166
+ * as of 0.5.3+. Reset to `null` on `close()`.
36167
+ */
36009
36168
  role = null;
36010
36169
  opts;
36011
36170
  hostHandle = null;
@@ -36027,6 +36186,12 @@ var FetchproxyServer = class {
36027
36186
  pendingIdb = /* @__PURE__ */ new Map();
36028
36187
  mcpId = null;
36029
36188
  identity = null;
36189
+ // 0.5.3+: in-flight role-election / handle-start promise. Set the
36190
+ // first time a verb call runs `ensureConnected`, awaited by concurrent
36191
+ // callers, cleared once the connection is up. Single source of truth
36192
+ // for "we're connecting right now" so two parallel first-calls don't
36193
+ // race the port bind.
36194
+ connectingPromise = null;
36030
36195
  constructor(opts) {
36031
36196
  if (!Array.isArray(opts.domains) || opts.domains.length === 0) {
36032
36197
  throw new Error("FetchproxyServer: opts.domains must be a non-empty array of hostnames");
@@ -36078,23 +36243,87 @@ var FetchproxyServer = class {
36078
36243
  };
36079
36244
  }
36080
36245
  /**
36081
- * Start the WebSocket bridge. Loads the long-term identity keypair
36082
- * from disk (creating it on first call), elects the host-vs-peer
36083
- * role by attempting to bind the configured port, and stands up the
36084
- * matching handshake machinery. Idempotent only insofar as it leaves
36085
- * `role` non-null on success; calling `listen()` twice without an
36086
- * intervening `close()` is a programming error.
36246
+ * Prepare the bridge for use. Loads the long-term identity keypair
36247
+ * from disk (creating it on first call) and computes this instance's
36248
+ * `mcpId`. Does NOT bind the bridge port or dial any WebSocket — the
36249
+ * connection is established lazily on the first verb call (see
36250
+ * `ensureConnected` / `getOrConnect`).
36251
+ *
36252
+ * Pre-0.5.3 behavior: `listen()` also did role election and started
36253
+ * the host/peer immediately, which meant every configured-but-unused
36254
+ * MCP claimed bridge resources at MCP-client boot. Several MCPs
36255
+ * starting in parallel under Claude Desktop also produced noisy
36256
+ * `ERR_CONNECTION_REFUSED` errors in the extension if it raced ahead
36257
+ * of the first MCP's port bind. Deferring keeps boot quiet and
36258
+ * leaves the port unowned until something actually needs it.
36259
+ *
36260
+ * Calling `listen()` twice without an intervening `close()` is a
36261
+ * no-op (the second call's identity load is idempotent).
36087
36262
  */
36088
36263
  async listen() {
36089
- this.identity = await loadOrCreateIdentity(this.opts.serverName, this.opts.identityDir);
36090
- this.mcpId = generateMcpId(this.opts.serverName, this.opts.version);
36264
+ if (!this.identity) {
36265
+ this.identity = await loadOrCreateIdentity(this.opts.serverName, this.opts.identityDir);
36266
+ }
36267
+ if (!this.mcpId) {
36268
+ this.mcpId = generateMcpId(this.opts.serverName, this.opts.version);
36269
+ }
36270
+ }
36271
+ /**
36272
+ * Force an eager bridge connection (role-election + host/peer handle
36273
+ * start + listener wiring) without waiting for the first verb call.
36274
+ * Useful for callers that want to surface the role / connection
36275
+ * outcome at boot, or for tests whose harness dials a mock extension
36276
+ * immediately after server construction. Production MCPs that just
36277
+ * answer tool calls should NOT call this — the lazy connect via
36278
+ * `ensureConnected` will do the right thing on first use, keeping
36279
+ * boot cheap and avoiding port-bind contention for MCPs that never
36280
+ * actually get invoked.
36281
+ *
36282
+ * Idempotent: a second call after the first has resolved is a no-op
36283
+ * (the existing handle is reused). Throws if `listen()` was never
36284
+ * called.
36285
+ */
36286
+ async connect() {
36287
+ await this.ensureConnected();
36288
+ }
36289
+ /**
36290
+ * Establish the bridge connection (role-election + host/peer handle
36291
+ * start + listener wiring) the first time a verb is invoked.
36292
+ * Idempotent after the connection is up; concurrent first-callers
36293
+ * share the same in-flight promise so only one election happens.
36294
+ *
36295
+ * Throws if `listen()` was never called — the contract is that the
36296
+ * MCP author still must wire `transport.start()` at boot to load
36297
+ * identity / set mcpId, even though the WS doesn't open until a
36298
+ * verb runs.
36299
+ */
36300
+ async ensureConnected() {
36301
+ if (this.hostHandle || this.peerHandle)
36302
+ return;
36303
+ if (this.connectingPromise) {
36304
+ await this.connectingPromise;
36305
+ return;
36306
+ }
36307
+ if (!this.identity || !this.mcpId) {
36308
+ throw new Error("FetchproxyServer: ensureConnected called before listen() \u2014 call listen() at MCP boot to load identity");
36309
+ }
36310
+ this.connectingPromise = this.doConnect();
36311
+ try {
36312
+ await this.connectingPromise;
36313
+ } finally {
36314
+ this.connectingPromise = null;
36315
+ }
36316
+ }
36317
+ async doConnect() {
36318
+ const identity = this.identity;
36319
+ const mcpId = this.mcpId;
36091
36320
  const el = await electRole({ host: this.opts.host, port: this.opts.port });
36092
36321
  if (el.role === "host") {
36093
36322
  this.role = "host";
36094
36323
  this.hostHandle = await startHost({
36095
36324
  httpServer: el.server,
36096
- ownIdentity: this.identity,
36097
- ownMcpId: this.mcpId,
36325
+ ownIdentity: identity,
36326
+ ownMcpId: mcpId,
36098
36327
  ownServerName: this.opts.serverName,
36099
36328
  ownVersion: this.opts.version,
36100
36329
  ownDomains: this.opts.domains,
@@ -36109,13 +36338,17 @@ var FetchproxyServer = class {
36109
36338
  onPairCode: this.opts.onPairCode
36110
36339
  });
36111
36340
  this.hostHandle.onOwnInner((inner) => this.onInner(inner));
36341
+ this.hostHandle.onExtensionDisconnect(() => this.rejectAllPending());
36342
+ this.hostHandle.onPendingPair((code) => {
36343
+ this.rejectAllPending(this.pairingErrorMessage(code));
36344
+ });
36112
36345
  } else {
36113
36346
  this.role = "peer";
36114
36347
  this.peerHandle = await startPeer({
36115
36348
  host: this.opts.host,
36116
36349
  port: this.opts.port,
36117
- identity: this.identity,
36118
- mcpId: this.mcpId,
36350
+ identity,
36351
+ mcpId,
36119
36352
  serverName: this.opts.serverName,
36120
36353
  version: this.opts.version,
36121
36354
  domains: this.opts.domains,
@@ -36129,8 +36362,19 @@ var FetchproxyServer = class {
36129
36362
  sessionStoragePointers: this.opts.sessionStoragePointers
36130
36363
  });
36131
36364
  this.peerHandle.onInner((inner) => this.onInner(inner));
36365
+ this.peerHandle.onRenegotiate(() => this.rejectAllPending());
36366
+ this.peerHandle.onPendingPair((code) => {
36367
+ this.rejectAllPending(this.pairingErrorMessage(code));
36368
+ });
36369
+ if (this.opts.onPairCode) {
36370
+ const cb = this.opts.onPairCode;
36371
+ this.peerHandle.onPendingPair((code) => cb(code));
36372
+ }
36132
36373
  }
36133
36374
  }
36375
+ pairingErrorMessage(code) {
36376
+ return `fetchproxy: pairing required for ${this.opts.serverName} \u2014 open the fetchproxy extension popup in Chrome and approve the pair request. Verify the pair code matches: ${code}`;
36377
+ }
36134
36378
  /**
36135
36379
  * Raw single-shot fetch through the bridge. Most callers should prefer
36136
36380
  * the verb shortcuts (`get` / `post` / `getJson` / `postJson` / `getHtml`)
@@ -36146,8 +36390,11 @@ var FetchproxyServer = class {
36146
36390
  * offline, etc.).
36147
36391
  */
36148
36392
  async fetch(init) {
36149
- if (!this.hostHandle && !this.peerHandle) {
36150
- throw new Error("FetchproxyServer.fetch called before listen() \u2014 not listening");
36393
+ await this.ensureConnected();
36394
+ const pendingCode = this.currentPendingPairCode();
36395
+ if (pendingCode !== null) {
36396
+ const error51 = this.pairingErrorMessage(pendingCode);
36397
+ return { ok: false, error: error51, kind: classifyFetchError(error51) };
36151
36398
  }
36152
36399
  const id = this.nextRequestId++;
36153
36400
  const inner = { type: "request", id, op: "fetch", init };
@@ -36288,9 +36535,8 @@ var FetchproxyServer = class {
36288
36535
  if (!this.opts.capabilities.includes("read_cookies")) {
36289
36536
  throw new Error('FetchproxyServer.readCookies(): MCP did not declare "read_cookies" in capabilities \u2014 add it to FetchproxyServerOpts.capabilities to enable this verb');
36290
36537
  }
36291
- if (!this.hostHandle && !this.peerHandle) {
36292
- throw new Error("FetchproxyServer.readCookies called before listen() \u2014 not listening");
36293
- }
36538
+ await this.ensureConnected();
36539
+ this.throwIfPendingPair();
36294
36540
  if (opts.subdomain !== void 0)
36295
36541
  assertSubdomainLabel(opts.subdomain);
36296
36542
  const baseDomain = this.resolveBaseDomain(opts.domain);
@@ -36347,9 +36593,8 @@ var FetchproxyServer = class {
36347
36593
  if (!this.opts.capabilities.includes(op)) {
36348
36594
  throw new Error(`FetchproxyServer.${op === "read_local_storage" ? "readLocalStorage" : "readSessionStorage"}(): MCP did not declare ${JSON.stringify(op)} in capabilities`);
36349
36595
  }
36350
- if (!this.hostHandle && !this.peerHandle) {
36351
- throw new Error(`FetchproxyServer.${op} called before listen() \u2014 not listening`);
36352
- }
36596
+ await this.ensureConnected();
36597
+ this.throwIfPendingPair();
36353
36598
  if (!Array.isArray(opts.keys) || opts.keys.length === 0) {
36354
36599
  throw new Error(`FetchproxyServer.${op}: opts.keys must be a non-empty array`);
36355
36600
  }
@@ -36404,9 +36649,8 @@ var FetchproxyServer = class {
36404
36649
  if (!this.opts.capabilities.includes("capture_request_header")) {
36405
36650
  throw new Error('FetchproxyServer.captureRequestHeader(): MCP did not declare "capture_request_header" in capabilities');
36406
36651
  }
36407
- if (!this.hostHandle && !this.peerHandle) {
36408
- throw new Error("FetchproxyServer.captureRequestHeader called before listen() \u2014 not listening");
36409
- }
36652
+ await this.ensureConnected();
36653
+ this.throwIfPendingPair();
36410
36654
  const declared = this.opts.captureHeaders.find((d) => d.urlPattern === opts.urlPattern && d.headerName === opts.headerName);
36411
36655
  if (!declared) {
36412
36656
  throw new Error(`FetchproxyServer.captureRequestHeader: (urlPattern=${JSON.stringify(opts.urlPattern)}, headerName=${JSON.stringify(opts.headerName)}) not declared in captureHeaders`);
@@ -36447,9 +36691,8 @@ var FetchproxyServer = class {
36447
36691
  if (!this.opts.capabilities.includes("read_indexed_db")) {
36448
36692
  throw new Error('FetchproxyServer.readIndexedDb(): MCP did not declare "read_indexed_db" in capabilities');
36449
36693
  }
36450
- if (!this.hostHandle && !this.peerHandle) {
36451
- throw new Error("FetchproxyServer.readIndexedDb called before listen() \u2014 not listening");
36452
- }
36694
+ await this.ensureConnected();
36695
+ this.throwIfPendingPair();
36453
36696
  if (!Array.isArray(opts.keys) || opts.keys.length === 0) {
36454
36697
  throw new Error("FetchproxyServer.readIndexedDb: opts.keys must be a non-empty array");
36455
36698
  }
@@ -36527,10 +36770,11 @@ var FetchproxyServer = class {
36527
36770
  if (inner.op === void 0 || inner.op === "fetch") {
36528
36771
  fetchCb({ ok: true, status: inner.status, url: inner.url, body: inner.body });
36529
36772
  } else {
36530
- fetchCb({ ok: false, error: `unexpected ${inner.op} response on fetch awaiter` });
36773
+ const error51 = `unexpected ${inner.op} response on fetch awaiter`;
36774
+ fetchCb({ ok: false, error: error51, kind: classifyFetchError(error51) });
36531
36775
  }
36532
36776
  } else {
36533
- fetchCb({ ok: false, error: inner.error });
36777
+ fetchCb({ ok: false, error: inner.error, kind: classifyFetchError(inner.error) });
36534
36778
  }
36535
36779
  return;
36536
36780
  }
@@ -36597,6 +36841,52 @@ var FetchproxyServer = class {
36597
36841
  }
36598
36842
  }
36599
36843
  }
36844
+ rejectAllPending(reason = "extension disconnected") {
36845
+ const err = new FetchproxyProtocolError(reason);
36846
+ for (const cb of this.pending.values()) {
36847
+ cb({ ok: false, error: err.message, kind: classifyFetchError(err.message) });
36848
+ }
36849
+ this.pending.clear();
36850
+ for (const cb of this.pendingReadCookies.values()) {
36851
+ cb({ ok: false, error: err.message });
36852
+ }
36853
+ this.pendingReadCookies.clear();
36854
+ for (const { reject } of this.pendingStorage.values())
36855
+ reject(err);
36856
+ this.pendingStorage.clear();
36857
+ for (const { reject } of this.pendingCapture.values())
36858
+ reject(err);
36859
+ this.pendingCapture.clear();
36860
+ for (const { reject } of this.pendingIdb.values())
36861
+ reject(err);
36862
+ this.pendingIdb.clear();
36863
+ }
36864
+ /**
36865
+ * 0.5.2+: read the current pair-pending pair code from whichever handle
36866
+ * is active, returning null when none is pending. Public verbs call this
36867
+ * at the top so that a tool invoked while the bridge is waiting on user
36868
+ * approval fails fast with the actionable error rather than hanging on a
36869
+ * sealed frame the extension will never process.
36870
+ */
36871
+ currentPendingPairCode() {
36872
+ if (this.hostHandle)
36873
+ return this.hostHandle.pendingPairCode();
36874
+ if (this.peerHandle)
36875
+ return this.peerHandle.pendingPairCode();
36876
+ return null;
36877
+ }
36878
+ /**
36879
+ * 0.5.2+: throw `FetchproxyProtocolError` with the actionable pair-code
36880
+ * message if the bridge is waiting on user approval. Used by the verb
36881
+ * methods (readCookies, readLocalStorage, etc.) that surface errors via
36882
+ * thrown exceptions rather than `ok:false` discriminated unions.
36883
+ */
36884
+ throwIfPendingPair() {
36885
+ const code = this.currentPendingPairCode();
36886
+ if (code !== null) {
36887
+ throw new FetchproxyProtocolError(this.pairingErrorMessage(code));
36888
+ }
36889
+ }
36600
36890
  /**
36601
36891
  * Shut down the bridge. Host: terminates the WebSocket server and any
36602
36892
  * still-attached extension/peer clients. Peer: closes the upstream
@@ -36604,6 +36894,10 @@ var FetchproxyServer = class {
36604
36894
  * twice in a row.
36605
36895
  */
36606
36896
  async close() {
36897
+ this.rejectAllPending();
36898
+ if (this.connectingPromise) {
36899
+ await this.connectingPromise.catch(() => void 0);
36900
+ }
36607
36901
  if (this.hostHandle)
36608
36902
  await this.hostHandle.close();
36609
36903
  if (this.peerHandle)
@@ -36611,6 +36905,7 @@ var FetchproxyServer = class {
36611
36905
  this.hostHandle = null;
36612
36906
  this.peerHandle = null;
36613
36907
  this.role = null;
36908
+ this.connectingPromise = null;
36614
36909
  }
36615
36910
  };
36616
36911
 
@@ -36699,8 +36994,7 @@ async function bootstrap(opts) {
36699
36994
  for (const p of localStoragePointers) {
36700
36995
  pointers[p.outputKey] = { storageKey: p.storageKey, jsonPointer: p.jsonPointer };
36701
36996
  }
36702
- const stub = server2;
36703
- localStorage = await stub.readLocalStorage({
36997
+ localStorage = await server2.readLocalStorage({
36704
36998
  keys: allKeys,
36705
36999
  ...storageDomainOpts,
36706
37000
  ...localStoragePointers.length > 0 ? { pointers } : {}
@@ -36713,8 +37007,7 @@ async function bootstrap(opts) {
36713
37007
  for (const p of sessionStoragePointers) {
36714
37008
  pointers[p.outputKey] = { storageKey: p.storageKey, jsonPointer: p.jsonPointer };
36715
37009
  }
36716
- const stub = server2;
36717
- sessionStorage = await stub.readSessionStorage({
37010
+ sessionStorage = await server2.readSessionStorage({
36718
37011
  keys: allKeys,
36719
37012
  ...storageDomainOpts,
36720
37013
  ...sessionStoragePointers.length > 0 ? { pointers } : {}
@@ -36737,9 +37030,6 @@ async function bootstrap(opts) {
36737
37030
  }
36738
37031
  const indexedDbBucket = {};
36739
37032
  for (const d of indexedDb) {
36740
- if (!server2.readIndexedDb) {
36741
- throw new Error("bootstrap: server factory does not implement readIndexedDb (declared indexedDb but server stub omits it)");
36742
- }
36743
37033
  const values = await server2.readIndexedDb({
36744
37034
  database: d.database,
36745
37035
  store: d.store,
@@ -36776,7 +37066,7 @@ var BootstrapDisabledError = class extends Error {
36776
37066
  // package.json
36777
37067
  var package_default = {
36778
37068
  name: "honeybook-mcp",
36779
- version: "0.1.13",
37069
+ version: "0.2.4",
36780
37070
  mcpName: "io.github.chrischall/honeybook-mcp",
36781
37071
  description: "HoneyBook client-portal MCP server for Claude",
36782
37072
  author: "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -36816,17 +37106,17 @@ var package_default = {
36816
37106
  "test:watch": "vitest"
36817
37107
  },
36818
37108
  dependencies: {
36819
- "@fetchproxy/bootstrap": "^0.4.2",
37109
+ "@fetchproxy/bootstrap": "^0.6.0",
36820
37110
  "@modelcontextprotocol/sdk": "^1.29.0",
36821
- dotenv: "^17.4.0",
36822
- zod: "^4.4.2"
37111
+ dotenv: "^17.4.2",
37112
+ zod: "^4.4.3"
36823
37113
  },
36824
37114
  devDependencies: {
36825
- "@types/node": "^25.5.0",
36826
- "@vitest/coverage-v8": "^4.1.2",
37115
+ "@types/node": "^25.9.1",
37116
+ "@vitest/coverage-v8": "^4.1.7",
36827
37117
  esbuild: "^0.28.0",
36828
- typescript: "^6.0.2",
36829
- vitest: "^4.1.2"
37118
+ typescript: "^6.0.3",
37119
+ vitest: "^4.1.7"
36830
37120
  }
36831
37121
  };
36832
37122
 
@@ -37442,7 +37732,8 @@ try {
37442
37732
  }
37443
37733
  var server = new McpServer({
37444
37734
  name: "honeybook-mcp",
37445
- version: "0.1.13"
37735
+ version: "0.2.4"
37736
+ // x-release-please-version
37446
37737
  });
37447
37738
  registerSessionTools(server);
37448
37739
  registerWorkspaceFileTools(server);
package/dist/index.js CHANGED
@@ -18,7 +18,7 @@ import { registerContractTools } from './tools/contracts.js';
18
18
  import { registerInvoiceTools } from './tools/invoices.js';
19
19
  const server = new McpServer({
20
20
  name: 'honeybook-mcp',
21
- version: '0.1.13',
21
+ version: '0.2.4', // x-release-please-version
22
22
  });
23
23
  registerSessionTools(server);
24
24
  registerWorkspaceFileTools(server);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "honeybook-mcp",
3
- "version": "0.1.13",
3
+ "version": "0.2.4",
4
4
  "mcpName": "io.github.chrischall/honeybook-mcp",
5
5
  "description": "HoneyBook client-portal MCP server for Claude",
6
6
  "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -40,16 +40,16 @@
40
40
  "test:watch": "vitest"
41
41
  },
42
42
  "dependencies": {
43
- "@fetchproxy/bootstrap": "^0.4.2",
43
+ "@fetchproxy/bootstrap": "^0.6.0",
44
44
  "@modelcontextprotocol/sdk": "^1.29.0",
45
- "dotenv": "^17.4.0",
46
- "zod": "^4.4.2"
45
+ "dotenv": "^17.4.2",
46
+ "zod": "^4.4.3"
47
47
  },
48
48
  "devDependencies": {
49
- "@types/node": "^25.5.0",
50
- "@vitest/coverage-v8": "^4.1.2",
49
+ "@types/node": "^25.9.1",
50
+ "@vitest/coverage-v8": "^4.1.7",
51
51
  "esbuild": "^0.28.0",
52
- "typescript": "^6.0.2",
53
- "vitest": "^4.1.2"
52
+ "typescript": "^6.0.3",
53
+ "vitest": "^4.1.7"
54
54
  }
55
55
  }
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/chrischall/honeybook-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "0.1.13",
9
+ "version": "0.2.4",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "honeybook-mcp",
14
- "version": "0.1.13",
14
+ "version": "0.2.4",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },