honeybook-mcp 0.3.4 → 0.3.6

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.3.4"
10
+ "version": "0.3.6"
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.3.4",
18
+ "version": "0.3.6",
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.3.4",
4
+ "version": "0.3.6",
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/dist/auth.js CHANGED
@@ -154,6 +154,24 @@ export async function captureSessionViaFetchproxy(opts) {
154
154
  throw new Error(`HoneyBook auth: fetchproxy bridge is down (extension service worker unreachable after retry). ${bridgeError.hint ?? ''}`.trimEnd());
155
155
  }
156
156
  const msg = e instanceof Error ? e.message : String(e);
157
+ // The hb-api-fingerprint capture rejects with a bare `timeout` when no
158
+ // api.honeybook.com/api/v2/* request fires before its one-shot listener
159
+ // window elapses. This is the common "tab already loaded and idle" case:
160
+ // a HoneyBook portal fires its API calls on load, then goes quiet, so by
161
+ // the time bootstrap has paired, read jStorage, and armed the capture
162
+ // listener there's no fresh request left to sniff the header off. The fix
163
+ // is to make the portal issue a *new* request (refresh the tab, open a
164
+ // workspace) WHILE the capture is waiting — NOT to (re)open the magic
165
+ // link, which is already open. classifyBridgeError keys off the error
166
+ // class, so the capture timeout surfaces as a FetchproxyProtocolError
167
+ // ('protocol'), not 'timeout' — match on the message too.
168
+ if (bridgeError.type === 'timeout' || /timeout/i.test(msg)) {
169
+ throw new Error('HoneyBook auth: fetchproxy capture timed out waiting for the hb-api-fingerprint header. ' +
170
+ 'That header can only be read off a live api.honeybook.com request, and a portal tab that has ' +
171
+ 'already finished loading sits idle. Keep the vendor magic-link URL open in Chrome (with the ' +
172
+ 'fetchproxy extension installed), then re-run use_magic_link and — within ~30s, while it waits — ' +
173
+ 'refresh the portal tab (or open a workspace/file) so the page issues a fresh API request.');
174
+ }
157
175
  throw new Error(`HoneyBook auth: fetchproxy capture failed: ${msg} — ` +
158
176
  'open the vendor magic-link URL in Chrome (with the fetchproxy 0.3.0 extension installed), ' +
159
177
  'then retry.');
package/dist/bundle.js CHANGED
@@ -12,7 +12,11 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
12
12
  throw Error('Dynamic require of "' + x + '" is not supported');
13
13
  });
14
14
  var __commonJS = (cb, mod) => function __require2() {
15
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
15
+ try {
16
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
17
+ } catch (e) {
18
+ throw mod = 0, e;
19
+ }
16
20
  };
17
21
  var __export = (target, all) => {
18
22
  for (var name in all)
@@ -34706,8 +34710,12 @@ var API_KEY_RE = new RegExp([
34706
34710
  // webhook signing secret (Stripe-style)
34707
34711
  ].map((p) => `\\b${p}`).join("|"), "g");
34708
34712
  var QUERY_SECRET_RE = /([?&](?:access_token|refresh_token|client_secret|api_?key|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
34713
+ var AWS_SIGV4_RE = /([?&]X-Amz-(?:Signature|Security-Token|Credential)=)[^&#\s"'<>`]+/gi;
34714
+ var JSON_SECRET_KEYS = "access_token|refresh_token|client_secret|api_?key|password|passwd|secret|token";
34715
+ var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")[^"]*(")`, "gi");
34716
+ var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')[^']*(')`, "gi");
34709
34717
  function redactSecrets(text) {
34710
- return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(JWT_RE, "[REDACTED]");
34718
+ return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JWT_RE, "[REDACTED]");
34711
34719
  }
34712
34720
  function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX) {
34713
34721
  const str = text === null || text === void 0 ? "" : String(text);
@@ -34790,6 +34798,8 @@ var NonNegInt = external_exports.number().int().nonnegative();
34790
34798
  var NonEmptyString = external_exports.string().min(1);
34791
34799
  var IsoDate = external_exports.iso.date();
34792
34800
  var IsoTime = external_exports.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, "must be HH:MM (24h), e.g. 19:30");
34801
+ var NumericIdString = external_exports.string().regex(/^\d+$/, "must be a numeric id (digits only)").describe("A numeric id string (digits only), safe to interpolate into a URL path.");
34802
+ var SafePathSegment = external_exports.string().min(1).regex(/^[^/?#\s]+$/, 'must not contain "/", "?", "#", or whitespace').refine((v) => !v.includes(".."), 'must not contain ".."').describe('A single URL path segment, safe to interpolate: no "/", "..", "?", "#", or whitespace.');
34793
34803
  var schemaOrigin = external_exports.string().optional().describe("Portal origin (e.g. https://<vendor>.example.co) selecting which active session to use. Optional when only one session is active.");
34794
34804
  var schemaConfirm = external_exports.boolean().optional().describe("Must be true to proceed. Without this, the tool returns a preview.");
34795
34805
  var paginationSchema = {
@@ -36006,10 +36016,20 @@ async function openEncryptedFrame(sessionKey, frame) {
36006
36016
 
36007
36017
  // node_modules/@fetchproxy/server/dist/election.js
36008
36018
  import { createServer, Server as HttpServer } from "node:http";
36019
+ var DEFAULT_BIND_TIMEOUT_MS = 5e3;
36009
36020
  async function electRole(opts) {
36010
36021
  const server = createServer();
36022
+ const bindTimeoutMs = opts.bindTimeoutMs ?? DEFAULT_BIND_TIMEOUT_MS;
36011
36023
  return new Promise((resolve, reject) => {
36024
+ let timer;
36025
+ const clearBindTimer = () => {
36026
+ if (timer) {
36027
+ clearTimeout(timer);
36028
+ timer = void 0;
36029
+ }
36030
+ };
36012
36031
  const onError = (e) => {
36032
+ clearBindTimer();
36013
36033
  server.removeListener("listening", onListening);
36014
36034
  if (e.code === "EADDRINUSE") {
36015
36035
  try {
@@ -36022,11 +36042,25 @@ async function electRole(opts) {
36022
36042
  }
36023
36043
  };
36024
36044
  const onListening = () => {
36045
+ clearBindTimer();
36025
36046
  server.removeListener("error", onError);
36026
36047
  resolve({ role: "host", server });
36027
36048
  };
36028
36049
  server.once("error", onError);
36029
36050
  server.once("listening", onListening);
36051
+ if (bindTimeoutMs > 0) {
36052
+ timer = setTimeout(() => {
36053
+ server.removeListener("error", onError);
36054
+ server.removeListener("listening", onListening);
36055
+ try {
36056
+ server.close();
36057
+ } catch {
36058
+ }
36059
+ reject(new Error(`fetchproxy: bind to ${opts.host}:${opts.port} timed out after ${bindTimeoutMs}ms (port may be in a bad state)`));
36060
+ }, bindTimeoutMs);
36061
+ if (typeof timer.unref === "function")
36062
+ timer.unref();
36063
+ }
36030
36064
  server.listen(opts.port, opts.host);
36031
36065
  });
36032
36066
  }
@@ -36119,6 +36153,44 @@ var SessionState = class {
36119
36153
  }
36120
36154
  };
36121
36155
 
36156
+ // node_modules/@fetchproxy/server/dist/session-ready.js
36157
+ var SESSION_READY_TIMEOUT_MS = 3e4;
36158
+ var FetchproxySessionNotReadyError = class extends Error {
36159
+ reason;
36160
+ pairCode;
36161
+ mcpId;
36162
+ hint;
36163
+ constructor(info) {
36164
+ const pairing = info.pairCode !== null && info.pairCode !== "";
36165
+ const hint = pairing ? `Open the Transporter extension popup and approve pair code ${info.pairCode} for "${info.mcpId}", then retry.` : `The extension is connected but hasn't confirmed a session for "${info.mcpId}" \u2014 sign in to the target site in that browser (and approve the requested scope if it changed), then retry.`;
36166
+ super(`fetchproxy: ${pairing ? "pairing not yet approved" : "no confirmed browser session"} for "${info.mcpId}". ${hint}`);
36167
+ this.name = "FetchproxySessionNotReadyError";
36168
+ this.reason = pairing ? "pair-required" : "not-ready";
36169
+ this.pairCode = pairing ? info.pairCode : null;
36170
+ this.mcpId = info.mcpId;
36171
+ this.hint = hint;
36172
+ Object.setPrototypeOf(this, new.target.prototype);
36173
+ }
36174
+ };
36175
+ async function awaitSessionReady(ready, opts) {
36176
+ const ms = opts.timeoutMs ?? SESSION_READY_TIMEOUT_MS;
36177
+ if (ms <= 0)
36178
+ return ready;
36179
+ let timer;
36180
+ const timeout = new Promise((_, reject) => {
36181
+ timer = setTimeout(() => {
36182
+ reject(new FetchproxySessionNotReadyError({ mcpId: opts.mcpId, pairCode: opts.pendingPairCode() }));
36183
+ }, ms);
36184
+ timer.unref?.();
36185
+ });
36186
+ try {
36187
+ return await Promise.race([ready, timeout]);
36188
+ } finally {
36189
+ if (timer)
36190
+ clearTimeout(timer);
36191
+ }
36192
+ }
36193
+
36122
36194
  // node_modules/@fetchproxy/server/dist/host.js
36123
36195
  var PUBLIC_ORIGIN_RE = /^https?:\/\/(?!(127\.0\.0\.1|localhost)(:|$))/i;
36124
36196
  var enc2 = new TextEncoder();
@@ -36206,6 +36278,29 @@ async function startHost(opts) {
36206
36278
  return;
36207
36279
  }
36208
36280
  if (frame.type === "hello" && frame.role === "server") {
36281
+ const peerEdPub = fromB64(frame.identityEd25519Pub);
36282
+ const peerSigMsg = concatBytes(enc2.encode(frame.mcpId), fromB64(frame.sessionNonce));
36283
+ const peerSig = fromB64(frame.sessionSig);
36284
+ let peerSigOk = false;
36285
+ try {
36286
+ peerSigOk = await ed25519Verify(peerEdPub, peerSigMsg, peerSig);
36287
+ } catch {
36288
+ peerSigOk = false;
36289
+ }
36290
+ if (!peerSigOk) {
36291
+ console.warn("[fetchproxy] peer hello signature invalid \u2014 refusing registration (possible squatter)");
36292
+ ws.close(1008, "peer hello signature invalid");
36293
+ return;
36294
+ }
36295
+ const existing = peers.get(frame.mcpId);
36296
+ if (existing && existing.ws !== ws) {
36297
+ const existingEdPub = existing.helloFrame.identityEd25519Pub;
36298
+ if (existingEdPub !== frame.identityEd25519Pub) {
36299
+ console.warn("[fetchproxy] peer mcpId already mapped to a different identity \u2014 refusing (mcpId squatting)");
36300
+ ws.close(1008, "mcpId already registered to another identity");
36301
+ return;
36302
+ }
36303
+ }
36209
36304
  identified = "peer";
36210
36305
  peerMcpId = frame.mcpId;
36211
36306
  peers.set(frame.mcpId, { ws, helloFrame: frame });
@@ -36298,8 +36393,10 @@ async function startHost(opts) {
36298
36393
  resetSessionPromise();
36299
36394
  disconnectListeners.forEach((cb) => cb());
36300
36395
  }
36301
- if (identified === "peer" && peerMcpId)
36302
- peers.delete(peerMcpId);
36396
+ if (identified === "peer" && peerMcpId) {
36397
+ if (peers.get(peerMcpId)?.ws === ws)
36398
+ peers.delete(peerMcpId);
36399
+ }
36303
36400
  });
36304
36401
  });
36305
36402
  return {
@@ -36315,7 +36412,10 @@ async function startHost(opts) {
36315
36412
  });
36316
36413
  }),
36317
36414
  sendOwnInner: async (inner) => {
36318
- const session = await ownSessionReady;
36415
+ const session = await awaitSessionReady(ownSessionReady, {
36416
+ mcpId: opts.ownMcpId,
36417
+ pendingPairCode: () => ownPendingPairCode
36418
+ });
36319
36419
  if (!extensionWs)
36320
36420
  throw new Error("host: no extension connected");
36321
36421
  const sealed = await sealInnerFrame(session.sessionKey, opts.ownMcpId, session.nextOutboundSeq(), inner);
@@ -36420,7 +36520,10 @@ async function startPeer(opts) {
36420
36520
  ws,
36421
36521
  session: sessionPromise,
36422
36522
  sendInner: async (inner) => {
36423
- await sessionPromise;
36523
+ await awaitSessionReady(sessionPromise, {
36524
+ mcpId: opts.mcpId,
36525
+ pendingPairCode: () => pendingPairCode
36526
+ });
36424
36527
  const s = session;
36425
36528
  const sealed = await sealInnerFrame(s.sessionKey, opts.mcpId, s.nextOutboundSeq(), inner);
36426
36529
  ws.send(JSON.stringify(sealed));
@@ -37086,6 +37189,35 @@ var FetchproxyServer = class {
37086
37189
  this.keepAliveTimer = null;
37087
37190
  }
37088
37191
  }
37192
+ /**
37193
+ * Send an inner request frame via whichever bridge handle is active. If the
37194
+ * send throws (e.g. `FetchproxySessionNotReadyError` — the session never
37195
+ * confirmed), the frame never reached the bridge, so no reply will arrive:
37196
+ * drop the just-registered pending resolver for this id (it lives in exactly
37197
+ * one of the op maps — request ids are unique) so it doesn't leak until the
37198
+ * server closes, then rethrow.
37199
+ */
37200
+ async sendInnerFrame(inner) {
37201
+ try {
37202
+ if (this.hostHandle) {
37203
+ await this.hostHandle.sendOwnInner(inner);
37204
+ } else if (this.peerHandle) {
37205
+ await this.peerHandle.sendInner(inner);
37206
+ }
37207
+ } catch (err) {
37208
+ if ("id" in inner && typeof inner.id === "number") {
37209
+ const { id } = inner;
37210
+ this.pending.delete(id);
37211
+ this.pendingReadCookies.delete(id);
37212
+ this.pendingStorage.delete(id);
37213
+ this.pendingCapture.delete(id);
37214
+ this.pendingRedirect.delete(id);
37215
+ this.pendingDownload.delete(id);
37216
+ this.pendingIdb.delete(id);
37217
+ }
37218
+ throw err;
37219
+ }
37220
+ }
37089
37221
  /**
37090
37222
  * Single bridge round-trip, wrapped by `fetchTimeoutMs` when set.
37091
37223
  * On timeout returns the `{ok:false, kind:'timeout'}` envelope —
@@ -37097,11 +37229,7 @@ var FetchproxyServer = class {
37097
37229
  const pending = new Promise((resolve) => {
37098
37230
  this.pending.set(id, resolve);
37099
37231
  });
37100
- if (this.hostHandle) {
37101
- await this.hostHandle.sendOwnInner(inner);
37102
- } else if (this.peerHandle) {
37103
- await this.peerHandle.sendInner(inner);
37104
- }
37232
+ await this.sendInnerFrame(inner);
37105
37233
  const timeoutMs = this.opts.fetchTimeoutMs;
37106
37234
  if (timeoutMs === void 0 || timeoutMs <= 0)
37107
37235
  return pending;
@@ -37130,6 +37258,51 @@ var FetchproxyServer = class {
37130
37258
  clearTimeout(timer);
37131
37259
  }
37132
37260
  }
37261
+ /**
37262
+ * FP-B2: bound a non-`fetch` verb's reply wait by `fetchTimeoutMs`.
37263
+ *
37264
+ * Before this, only `fetch()` raced its `pending` reply against a timer
37265
+ * (`_fetchOnceWithTimeout`); `readCookies`, the storage reads,
37266
+ * `capture_request_header`, `capture_redirect`, `download`, and
37267
+ * `read_indexed_db` awaited their `pending` promise with no race, so a
37268
+ * wedged extension hung the tool call indefinitely.
37269
+ *
37270
+ * `pending` is the already-registered reply promise. `pendingMap`/`id`
37271
+ * point at the op-specific map entry so we can drop it on expiry exactly
37272
+ * as `_fetchOnceWithTimeout` does — otherwise a late bridge reply would
37273
+ * resolve into a stale resolver / leak. On timeout we reject with a
37274
+ * `FetchproxyTimeoutError` (the same throwable the convenience methods
37275
+ * already surface for fetch timeouts). `0`/unset opts out (unbounded),
37276
+ * matching the fetch path.
37277
+ */
37278
+ async _withVerbTimeout(pending, pendingMap, id, url2) {
37279
+ const timeoutMs = this.opts.fetchTimeoutMs;
37280
+ if (timeoutMs === void 0 || timeoutMs <= 0)
37281
+ return pending;
37282
+ let timer;
37283
+ const start = Date.now();
37284
+ try {
37285
+ return await Promise.race([
37286
+ pending,
37287
+ new Promise((_resolve, reject) => {
37288
+ timer = setTimeout(() => {
37289
+ pendingMap.delete(id);
37290
+ reject(new FetchproxyTimeoutError({
37291
+ url: url2,
37292
+ timeoutMs,
37293
+ role: this.role,
37294
+ port: this.opts.port,
37295
+ elapsedMs: Date.now() - start,
37296
+ retryAttempted: false
37297
+ }));
37298
+ }, timeoutMs);
37299
+ })
37300
+ ]);
37301
+ } finally {
37302
+ if (timer)
37303
+ clearTimeout(timer);
37304
+ }
37305
+ }
37133
37306
  /**
37134
37307
  * Map an `ok:false` fetch result to its typed throwable. Centralizes
37135
37308
  * the kind-to-error-class switch so `request()` and (via the same
@@ -37420,12 +37593,8 @@ var FetchproxyServer = class {
37420
37593
  const pending = new Promise((resolve) => {
37421
37594
  this.pendingReadCookies.set(id, resolve);
37422
37595
  });
37423
- if (this.hostHandle) {
37424
- await this.hostHandle.sendOwnInner(inner);
37425
- } else if (this.peerHandle) {
37426
- await this.peerHandle.sendInner(inner);
37427
- }
37428
- const result = await pending;
37596
+ await this.sendInnerFrame(inner);
37597
+ const result = await this._withVerbTimeout(pending, this.pendingReadCookies, id, `https://${host}`);
37429
37598
  if (!result.ok) {
37430
37599
  throw new FetchproxyProtocolError(result.error);
37431
37600
  }
@@ -37491,12 +37660,8 @@ var FetchproxyServer = class {
37491
37660
  const pending = new Promise((resolve, reject) => {
37492
37661
  this.pendingStorage.set(id, { resolve, reject });
37493
37662
  });
37494
- if (this.hostHandle) {
37495
- await this.hostHandle.sendOwnInner(inner);
37496
- } else if (this.peerHandle) {
37497
- await this.peerHandle.sendInner(inner);
37498
- }
37499
- return pending;
37663
+ await this.sendInnerFrame(inner);
37664
+ return this._withVerbTimeout(pending, this.pendingStorage, id, `https://${host}`);
37500
37665
  }
37501
37666
  /**
37502
37667
  * 0.3.0+: snapshot the next outgoing request's named header. Single-
@@ -37600,12 +37765,8 @@ var FetchproxyServer = class {
37600
37765
  const pending = new Promise((resolve, reject) => {
37601
37766
  this.pendingCapture.set(id, { resolve, reject });
37602
37767
  });
37603
- if (this.hostHandle) {
37604
- await this.hostHandle.sendOwnInner(inner);
37605
- } else if (this.peerHandle) {
37606
- await this.peerHandle.sendInner(inner);
37607
- }
37608
- return pending;
37768
+ await this.sendInnerFrame(inner);
37769
+ return this._withVerbTimeout(pending, this.pendingCapture, id, `https://${opts.host}${opts.path ?? "/*"}`);
37609
37770
  }
37610
37771
  /**
37611
37772
  * Snapshot the redirect target URL of the next request the browser
@@ -37689,12 +37850,8 @@ var FetchproxyServer = class {
37689
37850
  const pending = new Promise((resolve, reject) => {
37690
37851
  this.pendingRedirect.set(id, { resolve, reject });
37691
37852
  });
37692
- if (this.hostHandle) {
37693
- await this.hostHandle.sendOwnInner(inner);
37694
- } else if (this.peerHandle) {
37695
- await this.peerHandle.sendInner(inner);
37696
- }
37697
- return pending;
37853
+ await this.sendInnerFrame(inner);
37854
+ return this._withVerbTimeout(pending, this.pendingRedirect, id, `https://${opts.host}${opts.path ?? "/*"}`);
37698
37855
  }
37699
37856
  /**
37700
37857
  * Download `url` through the BROWSER's own network stack via
@@ -37775,12 +37932,8 @@ var FetchproxyServer = class {
37775
37932
  const pending = new Promise((resolve, reject) => {
37776
37933
  this.pendingDownload.set(id, { resolve, reject });
37777
37934
  });
37778
- if (this.hostHandle) {
37779
- await this.hostHandle.sendOwnInner(inner);
37780
- } else if (this.peerHandle) {
37781
- await this.peerHandle.sendInner(inner);
37782
- }
37783
- return pending;
37935
+ await this.sendInnerFrame(inner);
37936
+ return this._withVerbTimeout(pending, this.pendingDownload, id, opts.url);
37784
37937
  }
37785
37938
  /**
37786
37939
  * 0.4.0+: read declared IndexedDB keys from the user's signed-in
@@ -37827,12 +37980,8 @@ var FetchproxyServer = class {
37827
37980
  const pending = new Promise((resolve, reject) => {
37828
37981
  this.pendingIdb.set(id, { resolve, reject });
37829
37982
  });
37830
- if (this.hostHandle) {
37831
- await this.hostHandle.sendOwnInner(inner);
37832
- } else if (this.peerHandle) {
37833
- await this.peerHandle.sendInner(inner);
37834
- }
37835
- return pending;
37983
+ await this.sendInnerFrame(inner);
37984
+ return this._withVerbTimeout(pending, this.pendingIdb, id, origin);
37836
37985
  }
37837
37986
  assertScopeSubset(requested, declared, label) {
37838
37987
  const undeclared = undeclaredKeys(requested, declared);
@@ -38263,7 +38412,7 @@ function bridgeErrorInfo(err) {
38263
38412
  // package.json
38264
38413
  var package_default = {
38265
38414
  name: "honeybook-mcp",
38266
- version: "0.3.4",
38415
+ version: "0.3.6",
38267
38416
  mcpName: "io.github.chrischall/honeybook-mcp",
38268
38417
  description: "HoneyBook client-portal MCP server for Claude",
38269
38418
  author: "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -38303,7 +38452,7 @@ var package_default = {
38303
38452
  "test:watch": "vitest"
38304
38453
  },
38305
38454
  dependencies: {
38306
- "@chrischall/mcp-utils": "^0.7.0",
38455
+ "@chrischall/mcp-utils": "^0.12.0",
38307
38456
  "@fetchproxy/bootstrap": "^1.0.0",
38308
38457
  "@fetchproxy/server": "^1.0.0",
38309
38458
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -38311,7 +38460,7 @@ var package_default = {
38311
38460
  zod: "^4.4.3"
38312
38461
  },
38313
38462
  devDependencies: {
38314
- "@types/node": "^25.9.1",
38463
+ "@types/node": "^26.0.0",
38315
38464
  "@vitest/coverage-v8": "^4.1.7",
38316
38465
  esbuild: "^0.28.0",
38317
38466
  typescript: "^6.0.3",
@@ -38402,6 +38551,11 @@ async function captureSessionViaFetchproxy(opts) {
38402
38551
  );
38403
38552
  }
38404
38553
  const msg = e instanceof Error ? e.message : String(e);
38554
+ if (bridgeError.type === "timeout" || /timeout/i.test(msg)) {
38555
+ throw new Error(
38556
+ "HoneyBook auth: fetchproxy capture timed out waiting for the hb-api-fingerprint header. That header can only be read off a live api.honeybook.com request, and a portal tab that has already finished loading sits idle. Keep the vendor magic-link URL open in Chrome (with the fetchproxy extension installed), then re-run use_magic_link and \u2014 within ~30s, while it waits \u2014 refresh the portal tab (or open a workspace/file) so the page issues a fresh API request."
38557
+ );
38558
+ }
38405
38559
  throw new Error(
38406
38560
  `HoneyBook auth: fetchproxy capture failed: ${msg} \u2014 open the vendor magic-link URL in Chrome (with the fetchproxy 0.3.0 extension installed), then retry.`
38407
38561
  );
@@ -38472,7 +38626,7 @@ function registerSessionTools(server) {
38472
38626
  server.registerTool(
38473
38627
  "use_magic_link",
38474
38628
  {
38475
- description: `Capture a HoneyBook client-portal session via the fetchproxy 0.3.0 browser extension. Prerequisites: install the fetchproxy extension in Chrome/Safari, then open the vendor's magic-link URL in that browser so you're signed into their portal. This tool then snapshots the page's localStorage["jStorage"] and the hb-api-fingerprint header into ~/.honeybook-mcp/sessions.json. All other tools use the most-recently-activated session by default. The magic_link_url arg is used only to derive the portalOrigin (cache key) \u2014 the tool does NOT open or navigate to it.`,
38629
+ description: `Capture a HoneyBook client-portal session via the fetchproxy 0.3.0 browser extension. Prerequisites: install the fetchproxy extension in Chrome/Safari, then open the vendor's magic-link URL in that browser so you're signed into their portal. This tool then snapshots the page's localStorage["jStorage"] and the hb-api-fingerprint header into ~/.honeybook-mcp/sessions.json. IMPORTANT: the hb-api-fingerprint header is sniffed off the next live api.honeybook.com request, so a portal tab that has already finished loading (and gone idle) yields nothing and the capture times out. While this tool is running, refresh the portal tab (or open a workspace/file) so the page issues a fresh API request. All other tools use the most-recently-activated session by default. The magic_link_url arg is used only to derive the portalOrigin (cache key) \u2014 the tool does NOT open or navigate to it.`,
38476
38630
  inputSchema: {
38477
38631
  magic_link_url: external_exports.string().url().describe(
38478
38632
  "Full magic-link URL from the vendor's HoneyBook email, e.g. https://<vendor>.hbportal.co/app/workspace_file/<id>/... Used only to derive the portal origin; you must already have this URL open in a Chrome tab with the fetchproxy extension installed."
@@ -38895,7 +39049,7 @@ var __dirname = dirname2(fileURLToPath(import.meta.url));
38895
39049
  await loadDotenvSafely({ path: join3(__dirname, "..", ".env"), override: false });
38896
39050
  await runMcp({
38897
39051
  name: "honeybook-mcp",
38898
- version: "0.3.4",
39052
+ version: "0.3.6",
38899
39053
  // x-release-please-version
38900
39054
  tools: [
38901
39055
  registerSessionTools,
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import { registerContractTools } from './tools/contracts.js';
11
11
  import { registerInvoiceTools } from './tools/invoices.js';
12
12
  await runMcp({
13
13
  name: 'honeybook-mcp',
14
- version: '0.3.4', // x-release-please-version
14
+ version: '0.3.6', // x-release-please-version
15
15
  tools: [
16
16
  registerSessionTools,
17
17
  registerWorkspaceFileTools,
@@ -42,7 +42,7 @@ export async function listActiveSessions() {
42
42
  }
43
43
  export function registerSessionTools(server) {
44
44
  server.registerTool('use_magic_link', {
45
- description: "Capture a HoneyBook client-portal session via the fetchproxy 0.3.0 browser extension. Prerequisites: install the fetchproxy extension in Chrome/Safari, then open the vendor's magic-link URL in that browser so you're signed into their portal. This tool then snapshots the page's localStorage[\"jStorage\"] and the hb-api-fingerprint header into ~/.honeybook-mcp/sessions.json. All other tools use the most-recently-activated session by default. The magic_link_url arg is used only to derive the portalOrigin (cache key) — the tool does NOT open or navigate to it.",
45
+ description: "Capture a HoneyBook client-portal session via the fetchproxy 0.3.0 browser extension. Prerequisites: install the fetchproxy extension in Chrome/Safari, then open the vendor's magic-link URL in that browser so you're signed into their portal. This tool then snapshots the page's localStorage[\"jStorage\"] and the hb-api-fingerprint header into ~/.honeybook-mcp/sessions.json. IMPORTANT: the hb-api-fingerprint header is sniffed off the next live api.honeybook.com request, so a portal tab that has already finished loading (and gone idle) yields nothing and the capture times out. While this tool is running, refresh the portal tab (or open a workspace/file) so the page issues a fresh API request. All other tools use the most-recently-activated session by default. The magic_link_url arg is used only to derive the portalOrigin (cache key) — the tool does NOT open or navigate to it.",
46
46
  inputSchema: {
47
47
  magic_link_url: z
48
48
  .string()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "honeybook-mcp",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
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,7 +40,7 @@
40
40
  "test:watch": "vitest"
41
41
  },
42
42
  "dependencies": {
43
- "@chrischall/mcp-utils": "^0.7.0",
43
+ "@chrischall/mcp-utils": "^0.12.0",
44
44
  "@fetchproxy/bootstrap": "^1.0.0",
45
45
  "@fetchproxy/server": "^1.0.0",
46
46
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -48,7 +48,7 @@
48
48
  "zod": "^4.4.3"
49
49
  },
50
50
  "devDependencies": {
51
- "@types/node": "^25.9.1",
51
+ "@types/node": "^26.0.0",
52
52
  "@vitest/coverage-v8": "^4.1.7",
53
53
  "esbuild": "^0.28.0",
54
54
  "typescript": "^6.0.3",
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.3.4",
9
+ "version": "0.3.6",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "honeybook-mcp",
14
- "version": "0.3.4",
14
+ "version": "0.3.6",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },