honeybook-mcp 0.3.3 → 0.3.5

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.3"
10
+ "version": "0.3.5"
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.3",
18
+ "version": "0.3.5",
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.3",
4
+ "version": "0.3.5",
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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chris Hall
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # HoneyBook MCP
2
2
 
3
+ [![CI](https://github.com/chrischall/honeybook-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/chrischall/honeybook-mcp/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/honeybook-mcp)](https://www.npmjs.com/package/honeybook-mcp)
5
+ [![license](https://img.shields.io/npm/l/honeybook-mcp)](LICENSE)
6
+
3
7
  A [Model Context Protocol](https://modelcontextprotocol.io) server that connects Claude to the [HoneyBook](https://www.honeybook.com) client portal, giving you natural-language access to contracts and invoices sent by your wedding vendors.
4
8
 
5
9
  > [!WARNING]
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)
@@ -34790,6 +34794,8 @@ var NonNegInt = external_exports.number().int().nonnegative();
34790
34794
  var NonEmptyString = external_exports.string().min(1);
34791
34795
  var IsoDate = external_exports.iso.date();
34792
34796
  var IsoTime = external_exports.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, "must be HH:MM (24h), e.g. 19:30");
34797
+ 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.");
34798
+ 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
34799
  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
34800
  var schemaConfirm = external_exports.boolean().optional().describe("Must be true to proceed. Without this, the tool returns a preview.");
34795
34801
  var paginationSchema = {
@@ -36006,10 +36012,20 @@ async function openEncryptedFrame(sessionKey, frame) {
36006
36012
 
36007
36013
  // node_modules/@fetchproxy/server/dist/election.js
36008
36014
  import { createServer, Server as HttpServer } from "node:http";
36015
+ var DEFAULT_BIND_TIMEOUT_MS = 5e3;
36009
36016
  async function electRole(opts) {
36010
36017
  const server = createServer();
36018
+ const bindTimeoutMs = opts.bindTimeoutMs ?? DEFAULT_BIND_TIMEOUT_MS;
36011
36019
  return new Promise((resolve, reject) => {
36020
+ let timer;
36021
+ const clearBindTimer = () => {
36022
+ if (timer) {
36023
+ clearTimeout(timer);
36024
+ timer = void 0;
36025
+ }
36026
+ };
36012
36027
  const onError = (e) => {
36028
+ clearBindTimer();
36013
36029
  server.removeListener("listening", onListening);
36014
36030
  if (e.code === "EADDRINUSE") {
36015
36031
  try {
@@ -36022,11 +36038,25 @@ async function electRole(opts) {
36022
36038
  }
36023
36039
  };
36024
36040
  const onListening = () => {
36041
+ clearBindTimer();
36025
36042
  server.removeListener("error", onError);
36026
36043
  resolve({ role: "host", server });
36027
36044
  };
36028
36045
  server.once("error", onError);
36029
36046
  server.once("listening", onListening);
36047
+ if (bindTimeoutMs > 0) {
36048
+ timer = setTimeout(() => {
36049
+ server.removeListener("error", onError);
36050
+ server.removeListener("listening", onListening);
36051
+ try {
36052
+ server.close();
36053
+ } catch {
36054
+ }
36055
+ reject(new Error(`fetchproxy: bind to ${opts.host}:${opts.port} timed out after ${bindTimeoutMs}ms (port may be in a bad state)`));
36056
+ }, bindTimeoutMs);
36057
+ if (typeof timer.unref === "function")
36058
+ timer.unref();
36059
+ }
36030
36060
  server.listen(opts.port, opts.host);
36031
36061
  });
36032
36062
  }
@@ -36119,6 +36149,44 @@ var SessionState = class {
36119
36149
  }
36120
36150
  };
36121
36151
 
36152
+ // node_modules/@fetchproxy/server/dist/session-ready.js
36153
+ var SESSION_READY_TIMEOUT_MS = 3e4;
36154
+ var FetchproxySessionNotReadyError = class extends Error {
36155
+ reason;
36156
+ pairCode;
36157
+ mcpId;
36158
+ hint;
36159
+ constructor(info) {
36160
+ const pairing = info.pairCode !== null && info.pairCode !== "";
36161
+ 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.`;
36162
+ super(`fetchproxy: ${pairing ? "pairing not yet approved" : "no confirmed browser session"} for "${info.mcpId}". ${hint}`);
36163
+ this.name = "FetchproxySessionNotReadyError";
36164
+ this.reason = pairing ? "pair-required" : "not-ready";
36165
+ this.pairCode = pairing ? info.pairCode : null;
36166
+ this.mcpId = info.mcpId;
36167
+ this.hint = hint;
36168
+ Object.setPrototypeOf(this, new.target.prototype);
36169
+ }
36170
+ };
36171
+ async function awaitSessionReady(ready, opts) {
36172
+ const ms = opts.timeoutMs ?? SESSION_READY_TIMEOUT_MS;
36173
+ if (ms <= 0)
36174
+ return ready;
36175
+ let timer;
36176
+ const timeout = new Promise((_, reject) => {
36177
+ timer = setTimeout(() => {
36178
+ reject(new FetchproxySessionNotReadyError({ mcpId: opts.mcpId, pairCode: opts.pendingPairCode() }));
36179
+ }, ms);
36180
+ timer.unref?.();
36181
+ });
36182
+ try {
36183
+ return await Promise.race([ready, timeout]);
36184
+ } finally {
36185
+ if (timer)
36186
+ clearTimeout(timer);
36187
+ }
36188
+ }
36189
+
36122
36190
  // node_modules/@fetchproxy/server/dist/host.js
36123
36191
  var PUBLIC_ORIGIN_RE = /^https?:\/\/(?!(127\.0\.0\.1|localhost)(:|$))/i;
36124
36192
  var enc2 = new TextEncoder();
@@ -36206,6 +36274,29 @@ async function startHost(opts) {
36206
36274
  return;
36207
36275
  }
36208
36276
  if (frame.type === "hello" && frame.role === "server") {
36277
+ const peerEdPub = fromB64(frame.identityEd25519Pub);
36278
+ const peerSigMsg = concatBytes(enc2.encode(frame.mcpId), fromB64(frame.sessionNonce));
36279
+ const peerSig = fromB64(frame.sessionSig);
36280
+ let peerSigOk = false;
36281
+ try {
36282
+ peerSigOk = await ed25519Verify(peerEdPub, peerSigMsg, peerSig);
36283
+ } catch {
36284
+ peerSigOk = false;
36285
+ }
36286
+ if (!peerSigOk) {
36287
+ console.warn("[fetchproxy] peer hello signature invalid \u2014 refusing registration (possible squatter)");
36288
+ ws.close(1008, "peer hello signature invalid");
36289
+ return;
36290
+ }
36291
+ const existing = peers.get(frame.mcpId);
36292
+ if (existing && existing.ws !== ws) {
36293
+ const existingEdPub = existing.helloFrame.identityEd25519Pub;
36294
+ if (existingEdPub !== frame.identityEd25519Pub) {
36295
+ console.warn("[fetchproxy] peer mcpId already mapped to a different identity \u2014 refusing (mcpId squatting)");
36296
+ ws.close(1008, "mcpId already registered to another identity");
36297
+ return;
36298
+ }
36299
+ }
36209
36300
  identified = "peer";
36210
36301
  peerMcpId = frame.mcpId;
36211
36302
  peers.set(frame.mcpId, { ws, helloFrame: frame });
@@ -36298,8 +36389,10 @@ async function startHost(opts) {
36298
36389
  resetSessionPromise();
36299
36390
  disconnectListeners.forEach((cb) => cb());
36300
36391
  }
36301
- if (identified === "peer" && peerMcpId)
36302
- peers.delete(peerMcpId);
36392
+ if (identified === "peer" && peerMcpId) {
36393
+ if (peers.get(peerMcpId)?.ws === ws)
36394
+ peers.delete(peerMcpId);
36395
+ }
36303
36396
  });
36304
36397
  });
36305
36398
  return {
@@ -36315,7 +36408,10 @@ async function startHost(opts) {
36315
36408
  });
36316
36409
  }),
36317
36410
  sendOwnInner: async (inner) => {
36318
- const session = await ownSessionReady;
36411
+ const session = await awaitSessionReady(ownSessionReady, {
36412
+ mcpId: opts.ownMcpId,
36413
+ pendingPairCode: () => ownPendingPairCode
36414
+ });
36319
36415
  if (!extensionWs)
36320
36416
  throw new Error("host: no extension connected");
36321
36417
  const sealed = await sealInnerFrame(session.sessionKey, opts.ownMcpId, session.nextOutboundSeq(), inner);
@@ -36420,7 +36516,10 @@ async function startPeer(opts) {
36420
36516
  ws,
36421
36517
  session: sessionPromise,
36422
36518
  sendInner: async (inner) => {
36423
- await sessionPromise;
36519
+ await awaitSessionReady(sessionPromise, {
36520
+ mcpId: opts.mcpId,
36521
+ pendingPairCode: () => pendingPairCode
36522
+ });
36424
36523
  const s = session;
36425
36524
  const sealed = await sealInnerFrame(s.sessionKey, opts.mcpId, s.nextOutboundSeq(), inner);
36426
36525
  ws.send(JSON.stringify(sealed));
@@ -37086,6 +37185,35 @@ var FetchproxyServer = class {
37086
37185
  this.keepAliveTimer = null;
37087
37186
  }
37088
37187
  }
37188
+ /**
37189
+ * Send an inner request frame via whichever bridge handle is active. If the
37190
+ * send throws (e.g. `FetchproxySessionNotReadyError` — the session never
37191
+ * confirmed), the frame never reached the bridge, so no reply will arrive:
37192
+ * drop the just-registered pending resolver for this id (it lives in exactly
37193
+ * one of the op maps — request ids are unique) so it doesn't leak until the
37194
+ * server closes, then rethrow.
37195
+ */
37196
+ async sendInnerFrame(inner) {
37197
+ try {
37198
+ if (this.hostHandle) {
37199
+ await this.hostHandle.sendOwnInner(inner);
37200
+ } else if (this.peerHandle) {
37201
+ await this.peerHandle.sendInner(inner);
37202
+ }
37203
+ } catch (err) {
37204
+ if ("id" in inner && typeof inner.id === "number") {
37205
+ const { id } = inner;
37206
+ this.pending.delete(id);
37207
+ this.pendingReadCookies.delete(id);
37208
+ this.pendingStorage.delete(id);
37209
+ this.pendingCapture.delete(id);
37210
+ this.pendingRedirect.delete(id);
37211
+ this.pendingDownload.delete(id);
37212
+ this.pendingIdb.delete(id);
37213
+ }
37214
+ throw err;
37215
+ }
37216
+ }
37089
37217
  /**
37090
37218
  * Single bridge round-trip, wrapped by `fetchTimeoutMs` when set.
37091
37219
  * On timeout returns the `{ok:false, kind:'timeout'}` envelope —
@@ -37097,11 +37225,7 @@ var FetchproxyServer = class {
37097
37225
  const pending = new Promise((resolve) => {
37098
37226
  this.pending.set(id, resolve);
37099
37227
  });
37100
- if (this.hostHandle) {
37101
- await this.hostHandle.sendOwnInner(inner);
37102
- } else if (this.peerHandle) {
37103
- await this.peerHandle.sendInner(inner);
37104
- }
37228
+ await this.sendInnerFrame(inner);
37105
37229
  const timeoutMs = this.opts.fetchTimeoutMs;
37106
37230
  if (timeoutMs === void 0 || timeoutMs <= 0)
37107
37231
  return pending;
@@ -37130,6 +37254,51 @@ var FetchproxyServer = class {
37130
37254
  clearTimeout(timer);
37131
37255
  }
37132
37256
  }
37257
+ /**
37258
+ * FP-B2: bound a non-`fetch` verb's reply wait by `fetchTimeoutMs`.
37259
+ *
37260
+ * Before this, only `fetch()` raced its `pending` reply against a timer
37261
+ * (`_fetchOnceWithTimeout`); `readCookies`, the storage reads,
37262
+ * `capture_request_header`, `capture_redirect`, `download`, and
37263
+ * `read_indexed_db` awaited their `pending` promise with no race, so a
37264
+ * wedged extension hung the tool call indefinitely.
37265
+ *
37266
+ * `pending` is the already-registered reply promise. `pendingMap`/`id`
37267
+ * point at the op-specific map entry so we can drop it on expiry exactly
37268
+ * as `_fetchOnceWithTimeout` does — otherwise a late bridge reply would
37269
+ * resolve into a stale resolver / leak. On timeout we reject with a
37270
+ * `FetchproxyTimeoutError` (the same throwable the convenience methods
37271
+ * already surface for fetch timeouts). `0`/unset opts out (unbounded),
37272
+ * matching the fetch path.
37273
+ */
37274
+ async _withVerbTimeout(pending, pendingMap, id, url2) {
37275
+ const timeoutMs = this.opts.fetchTimeoutMs;
37276
+ if (timeoutMs === void 0 || timeoutMs <= 0)
37277
+ return pending;
37278
+ let timer;
37279
+ const start = Date.now();
37280
+ try {
37281
+ return await Promise.race([
37282
+ pending,
37283
+ new Promise((_resolve, reject) => {
37284
+ timer = setTimeout(() => {
37285
+ pendingMap.delete(id);
37286
+ reject(new FetchproxyTimeoutError({
37287
+ url: url2,
37288
+ timeoutMs,
37289
+ role: this.role,
37290
+ port: this.opts.port,
37291
+ elapsedMs: Date.now() - start,
37292
+ retryAttempted: false
37293
+ }));
37294
+ }, timeoutMs);
37295
+ })
37296
+ ]);
37297
+ } finally {
37298
+ if (timer)
37299
+ clearTimeout(timer);
37300
+ }
37301
+ }
37133
37302
  /**
37134
37303
  * Map an `ok:false` fetch result to its typed throwable. Centralizes
37135
37304
  * the kind-to-error-class switch so `request()` and (via the same
@@ -37420,12 +37589,8 @@ var FetchproxyServer = class {
37420
37589
  const pending = new Promise((resolve) => {
37421
37590
  this.pendingReadCookies.set(id, resolve);
37422
37591
  });
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;
37592
+ await this.sendInnerFrame(inner);
37593
+ const result = await this._withVerbTimeout(pending, this.pendingReadCookies, id, `https://${host}`);
37429
37594
  if (!result.ok) {
37430
37595
  throw new FetchproxyProtocolError(result.error);
37431
37596
  }
@@ -37491,12 +37656,8 @@ var FetchproxyServer = class {
37491
37656
  const pending = new Promise((resolve, reject) => {
37492
37657
  this.pendingStorage.set(id, { resolve, reject });
37493
37658
  });
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;
37659
+ await this.sendInnerFrame(inner);
37660
+ return this._withVerbTimeout(pending, this.pendingStorage, id, `https://${host}`);
37500
37661
  }
37501
37662
  /**
37502
37663
  * 0.3.0+: snapshot the next outgoing request's named header. Single-
@@ -37600,12 +37761,8 @@ var FetchproxyServer = class {
37600
37761
  const pending = new Promise((resolve, reject) => {
37601
37762
  this.pendingCapture.set(id, { resolve, reject });
37602
37763
  });
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;
37764
+ await this.sendInnerFrame(inner);
37765
+ return this._withVerbTimeout(pending, this.pendingCapture, id, `https://${opts.host}${opts.path ?? "/*"}`);
37609
37766
  }
37610
37767
  /**
37611
37768
  * Snapshot the redirect target URL of the next request the browser
@@ -37689,12 +37846,8 @@ var FetchproxyServer = class {
37689
37846
  const pending = new Promise((resolve, reject) => {
37690
37847
  this.pendingRedirect.set(id, { resolve, reject });
37691
37848
  });
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;
37849
+ await this.sendInnerFrame(inner);
37850
+ return this._withVerbTimeout(pending, this.pendingRedirect, id, `https://${opts.host}${opts.path ?? "/*"}`);
37698
37851
  }
37699
37852
  /**
37700
37853
  * Download `url` through the BROWSER's own network stack via
@@ -37775,12 +37928,8 @@ var FetchproxyServer = class {
37775
37928
  const pending = new Promise((resolve, reject) => {
37776
37929
  this.pendingDownload.set(id, { resolve, reject });
37777
37930
  });
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;
37931
+ await this.sendInnerFrame(inner);
37932
+ return this._withVerbTimeout(pending, this.pendingDownload, id, opts.url);
37784
37933
  }
37785
37934
  /**
37786
37935
  * 0.4.0+: read declared IndexedDB keys from the user's signed-in
@@ -37827,12 +37976,8 @@ var FetchproxyServer = class {
37827
37976
  const pending = new Promise((resolve, reject) => {
37828
37977
  this.pendingIdb.set(id, { resolve, reject });
37829
37978
  });
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;
37979
+ await this.sendInnerFrame(inner);
37980
+ return this._withVerbTimeout(pending, this.pendingIdb, id, origin);
37836
37981
  }
37837
37982
  assertScopeSubset(requested, declared, label) {
37838
37983
  const undeclared = undeclaredKeys(requested, declared);
@@ -38263,7 +38408,7 @@ function bridgeErrorInfo(err) {
38263
38408
  // package.json
38264
38409
  var package_default = {
38265
38410
  name: "honeybook-mcp",
38266
- version: "0.3.3",
38411
+ version: "0.3.5",
38267
38412
  mcpName: "io.github.chrischall/honeybook-mcp",
38268
38413
  description: "HoneyBook client-portal MCP server for Claude",
38269
38414
  author: "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -38303,7 +38448,7 @@ var package_default = {
38303
38448
  "test:watch": "vitest"
38304
38449
  },
38305
38450
  dependencies: {
38306
- "@chrischall/mcp-utils": "^0.7.0",
38451
+ "@chrischall/mcp-utils": "^0.10.3",
38307
38452
  "@fetchproxy/bootstrap": "^1.0.0",
38308
38453
  "@fetchproxy/server": "^1.0.0",
38309
38454
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -38402,6 +38547,11 @@ async function captureSessionViaFetchproxy(opts) {
38402
38547
  );
38403
38548
  }
38404
38549
  const msg = e instanceof Error ? e.message : String(e);
38550
+ if (bridgeError.type === "timeout" || /timeout/i.test(msg)) {
38551
+ throw new Error(
38552
+ "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."
38553
+ );
38554
+ }
38405
38555
  throw new Error(
38406
38556
  `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
38557
  );
@@ -38472,7 +38622,7 @@ function registerSessionTools(server) {
38472
38622
  server.registerTool(
38473
38623
  "use_magic_link",
38474
38624
  {
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.`,
38625
+ 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
38626
  inputSchema: {
38477
38627
  magic_link_url: external_exports.string().url().describe(
38478
38628
  "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 +39045,7 @@ var __dirname = dirname2(fileURLToPath(import.meta.url));
38895
39045
  await loadDotenvSafely({ path: join3(__dirname, "..", ".env"), override: false });
38896
39046
  await runMcp({
38897
39047
  name: "honeybook-mcp",
38898
- version: "0.3.3",
39048
+ version: "0.3.5",
38899
39049
  // x-release-please-version
38900
39050
  tools: [
38901
39051
  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.3', // x-release-please-version
14
+ version: '0.3.5', // 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.3",
3
+ "version": "0.3.5",
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.10.3",
44
44
  "@fetchproxy/bootstrap": "^1.0.0",
45
45
  "@fetchproxy/server": "^1.0.0",
46
46
  "@modelcontextprotocol/sdk": "^1.29.0",
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.3",
9
+ "version": "0.3.5",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "honeybook-mcp",
14
- "version": "0.3.3",
14
+ "version": "0.3.5",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },