honeybook-mcp 0.4.4 → 0.4.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.4.4"
10
+ "version": "0.4.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.4.4",
18
+ "version": "0.4.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.4.4",
4
+ "version": "0.4.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/README.md CHANGED
@@ -86,7 +86,7 @@ HoneyBook has no public client-portal API. This MCP reuses the same auth state y
86
86
  **Per-vendor activation:**
87
87
 
88
88
  1. Make sure the vendor's portal tab is open (the magic link from their email).
89
- 2. In Claude, call `use_magic_link` with the magic-link URL — the tool asks the fetchproxy extension to snapshot the page's `localStorage["jStorage"]` and the `hb-api-fingerprint` request header, then closes the bridge. No headless browser is spawned.
89
+ 2. In Claude, call `use_magic_link` with the magic-link URL — the tool asks the fetchproxy extension to snapshot the auth fields out of the page's `localStorage["HONEYBOOK_REACT_CURR_USER"]`, then closes the bridge. The tab only has to be open and signed in; nothing is read off a live request. No headless browser is spawned.
90
90
  3. All other tools use the most-recently-activated session by default. Pass `origin` explicitly when multiple vendors are active.
91
91
 
92
92
  Sessions are stored in memory and persisted to `~/.honeybook-mcp/sessions.json` (mode 0600) so they survive MCP restarts. Re-run `use_magic_link` when a session expires.
@@ -111,7 +111,26 @@ Tools that touch a vendor accept an optional `origin` argument (e.g. `https://ac
111
111
  - **"HoneyBook auth expired"** — re-open the vendor's magic link in Chrome and re-run `use_magic_link`.
112
112
  - **"No active HoneyBook session"** — call `use_magic_link` first.
113
113
  - **"fetchproxy capture failed"** — install the [fetchproxy 0.3.0 extension](https://github.com/chrischall/fetchproxy), then open the vendor's magic link in that browser.
114
- - **"hb-api-fingerprint header not captured"** — refresh the portal tab so the page makes an `api.honeybook.com/api/v2/*` request, then retry.
114
+ - **"fetchproxy capture timed out"** — the extension found no signed-in portal tab to read. Open the vendor's magic link, confirm the portal page has loaded, then retry.
115
+ - **"no confirmed browser session"** — the extension is connected but has not approved this MCP. Open the Transporter popup and approve the pair code it shows, then retry.
116
+ - **"localStorage keys not in declared set"** — the MCP now reads a storage key your existing pairing doesn't cover. See [Upgrading from 0.4.4 or earlier](#upgrading-from-044-or-earlier). Retrying will not help, and this is not a version problem.
117
+
118
+ ## Upgrading from 0.4.4 or earlier
119
+
120
+ **0.4.5 requires a one-time re-approval in the browser extension.** HoneyBook moved the client-portal session out of `localStorage["jStorage"]` into `localStorage["HONEYBOOK_REACT_CURR_USER"]`, so 0.4.5 reads a different storage key. The extension approves a MCP's declared scope *at pair time*, so an existing pairing does not cover the new key and every capture is refused with:
121
+
122
+ ```
123
+ localStorage keys not in declared set: HONEYBOOK_REACT_CURR_USER
124
+ ```
125
+
126
+ To fix it, once:
127
+
128
+ 1. Open the **Transporter** extension popup.
129
+ 2. **Revoke** `honeybook-mcp`.
130
+ 3. Re-run `use_magic_link` — a fresh pair code appears.
131
+ 4. Approve it in the popup. You are approving the new scope.
132
+
133
+ Nothing else changes: existing sessions in `~/.honeybook-mcp/sessions.json` keep working, and a re-capture is only needed if the session itself has expired.
115
134
 
116
135
  ## Security
117
136
 
package/dist/auth.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // ────────────────────────────────────────────────────────────────────────────
2
- // Session capture — Pattern A template (read_local_storage + capture_header)
2
+ // Session capture — Pattern A template (read_local_storage)
3
3
  // ────────────────────────────────────────────────────────────────────────────
4
4
  //
5
5
  // Mirrors the canonical "browser-bootstrap + Node-direct" shape from
@@ -20,15 +20,23 @@
20
20
  // 2. fetchproxy bootstrap (this module).
21
21
  // The user has signed into their vendor portal via a magic link in
22
22
  // their real Chrome (the fetchproxy 0.3.0 extension is installed).
23
- // We call `@fetchproxy/bootstrap` once to:
24
- // • snapshot localStorage["jStorage"] — the HoneyBook web app
25
- // stores the bearer token + user id + trusted device id + the
26
- // HB_CURR_USER blob here, all under one key
27
- // • capture the `hb-api-fingerprint` request header on the first
28
- // api.honeybook.com/api/v2/* call the page makes — this is a
29
- // per-device FingerprintJS signal that the API requires on
30
- // every request (it's NOT in jStorage)
31
- // Then we close the bridge and operate from Node thereafter.
23
+ // We call `@fetchproxy/bootstrap` once to read the auth fields out
24
+ // of localStorage["HONEYBOOK_REACT_CURR_USER"], then close the
25
+ // bridge and operate from Node thereafter.
26
+ //
27
+ // HoneyBook used to keep this state in the AngularJS `jStorage`
28
+ // blob (HB_AUTH_TOKEN / HB_AUTH_USER_ID / HB_TRUSTED_DEVICE /
29
+ // HB_CURR_USER). It has since moved to the React-era
30
+ // `HONEYBOOK_REACT_CURR_USER` key; on a live signed-in portal
31
+ // jStorage is down to HB_TRUSTED_DEVICE, SESSION_COMPANY_ID,
32
+ // REDIRECT_URL and __jstorage_meta. We read the React key and keep
33
+ // the surviving jStorage trusted-device as a fallback.
34
+ //
35
+ // We no longer capture the `hb-api-fingerprint` request header.
36
+ // The API accepts requests without it (verified against
37
+ // /api/v2/users/{uid}/workspace_files), and capturing it was the
38
+ // only part of bootstrap that had to block on a live outgoing
39
+ // request — the "portal tab already went idle" timeout.
32
40
  //
33
41
  // 3. Error
34
42
  // Nothing to authenticate with. We throw a message that names the
@@ -36,14 +44,14 @@
36
44
  // extension installed) and retry the tool call.
37
45
  //
38
46
  // Why fetchproxy is only a one-shot read:
39
- // The bootstrap call snapshots both buckets (localStorage +
40
- // captured header) and returns. The MCP then operates from Node with
41
- // direct fetch — latency and reliability are not coupled to the
42
- // browser bridge for normal tool calls. When the bearer token
43
- // eventually expires, the existing 401 handler in `HoneyBookClient`
44
- // surfaces a helpful "re-bootstrap" error and the user re-runs the
45
- // capture (typically via the `use_magic_link` tool, which now wraps
46
- // this function instead of running Puppeteer).
47
+ // The bootstrap call snapshots the declared storage fields and
48
+ // returns. The MCP then operates from Node with direct fetch — latency
49
+ // and reliability are not coupled to the browser bridge for normal
50
+ // tool calls. When the bearer token eventually expires, the
51
+ // expired-session handler in `HoneyBookClient` surfaces a helpful
52
+ // "re-bootstrap" error and the user re-runs the capture (typically via
53
+ // the `use_magic_link` tool, which now wraps this function instead of
54
+ // running Puppeteer).
47
55
  //
48
56
  // Multi-domain scope:
49
57
  // HoneyBook serves both the main app (honeybook.com) and per-vendor
@@ -93,44 +101,54 @@ export async function captureSessionViaFetchproxy(opts) {
93
101
  // portal subdomains (*.hbportal.co). The extension matches on suffix,
94
102
  // so listing both apexes covers any vendor.
95
103
  //
96
- // Both are needed:
97
- // - `hbportal.co` is where the vendor magic-link tab lives, so
98
- // `jStorage` reads target it (via `storageDomain` below).
99
- // - `honeybook.com` is required for the `hb-api-fingerprint`
100
- // capture, because that header rides outgoing requests to
101
- // `api.honeybook.com/api/v2/*` and the extension gates
102
- // every captureHeader `host` against declared domains.
104
+ // `hbportal.co` is where the vendor magic-link tab lives, so the
105
+ // storage reads target it (via `storageDomain` below).
106
+ // `honeybook.com` stays declared because the same session is valid
107
+ // on the main app and callers may hold either origin.
103
108
  //
104
109
  // 0.4.1+ requires multi-domain MCPs to pick a `storageDomain` so
105
110
  // the cookie / localStorage / sessionStorage / indexedDb reads
106
- // know which tab to target. captureRequestHeader is independently
107
- // routed by its declared { host, path? }.
111
+ // know which tab to target.
108
112
  domains: ['honeybook.com', 'hbportal.co'],
109
113
  storageDomain: 'hbportal.co',
110
114
  declare: {
111
115
  cookies: [],
112
- // We don't need the full `jStorage` blob (it's ~8KB of mostly
113
- // unused state). 0.4.0's JSON-pointer extraction lets us
114
- // declare exactly the four fields the MCP uses; bootstrap
115
- // reads `jStorage` once and applies all pointers in one call.
116
- // The extension popup also shows the pointer paths verbatim
117
- // so the user can see precisely what's being read.
116
+ // `HONEYBOOK_REACT_CURR_USER` is ~6.5KB of mostly unused user
117
+ // state. 0.4.0's JSON-pointer extraction lets us declare exactly
118
+ // the fields the MCP uses; bootstrap reads each storage key once
119
+ // and applies all pointers in one call. The extension popup shows
120
+ // the pointer paths verbatim so the user can see precisely what's
121
+ // being read.
118
122
  localStorage: [],
119
123
  localStoragePointers: [
120
- { outputKey: 'HB_AUTH_TOKEN', storageKey: 'jStorage', jsonPointer: '/HB_AUTH_TOKEN' },
121
- { outputKey: 'HB_AUTH_USER_ID', storageKey: 'jStorage', jsonPointer: '/HB_AUTH_USER_ID' },
122
- { outputKey: 'HB_TRUSTED_DEVICE', storageKey: 'jStorage', jsonPointer: '/HB_TRUSTED_DEVICE' },
123
- { outputKey: 'HB_COMPANY_NAME', storageKey: 'jStorage', jsonPointer: '/HB_CURR_USER/company/company_name' },
124
+ {
125
+ outputKey: 'HB_AUTH_TOKEN',
126
+ storageKey: 'HONEYBOOK_REACT_CURR_USER',
127
+ jsonPointer: '/authentication_token',
128
+ },
129
+ { outputKey: 'HB_AUTH_USER_ID', storageKey: 'HONEYBOOK_REACT_CURR_USER', jsonPointer: '/_id' },
130
+ {
131
+ outputKey: 'HB_TRUSTED_DEVICE',
132
+ storageKey: 'HONEYBOOK_REACT_CURR_USER',
133
+ jsonPointer: '/trusted_device',
134
+ },
135
+ {
136
+ outputKey: 'HB_COMPANY_NAME',
137
+ storageKey: 'HONEYBOOK_REACT_CURR_USER',
138
+ jsonPointer: '/company/company_name',
139
+ },
140
+ // The one field of the old jStorage blob that survived the
141
+ // migration. Kept as a fallback so a further reshuffle of the
142
+ // React key doesn't cost us the trusted-device value too.
143
+ {
144
+ outputKey: 'HB_TRUSTED_DEVICE_LEGACY',
145
+ storageKey: 'jStorage',
146
+ jsonPointer: '/HB_TRUSTED_DEVICE',
147
+ },
124
148
  ],
125
149
  sessionStorage: [],
126
- captureHeaders: [
127
- // The page's first call to api.honeybook.com/api/v2/* carries the
128
- // `hb-api-fingerprint` header — a per-device FingerprintJS
129
- // signal that the API requires on every subsequent request.
130
- // It's NOT stored in jStorage; the only place to get it is off
131
- // a real outgoing request.
132
- { host: 'api.honeybook.com', path: '/api/v2/*', headerName: 'hb-api-fingerprint' },
133
- ],
150
+ // Nothing to sniff off a live request any more — see the header note above.
151
+ captureHeaders: [],
134
152
  },
135
153
  // 0.4.0: surface the pair code (six digits) on stderr so the user
136
154
  // can verify it against the extension popup. fetchproxy 0.4.0
@@ -139,9 +157,8 @@ export async function captureSessionViaFetchproxy(opts) {
139
157
  onPairCode: (code) => {
140
158
  process.stderr.write(`[honeybook-mcp] fetchproxy pair code: ${code}\n`);
141
159
  },
142
- // 0.4.0: capture_request_header blocks on a real outgoing request
143
- // to api.honeybook.com/api/v2/*. Surface a hint so the user knows
144
- // to interact with the portal if the bootstrap appears to stall.
160
+ // Surface the library's own progress hint so a stalled bootstrap
161
+ // tells the user what it is waiting on.
145
162
  onWaiting: (hint) => {
146
163
  process.stderr.write(`[honeybook-mcp] ${hint}\n`);
147
164
  },
@@ -154,53 +171,54 @@ export async function captureSessionViaFetchproxy(opts) {
154
171
  throw new Error(`HoneyBook auth: fetchproxy bridge is down (extension service worker unreachable after retry). ${bridgeError.hint ?? ''}`.trimEnd());
155
172
  }
156
173
  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
174
+ // The extension gates on the scope approved at pair time, so widening the
175
+ // declaration — as the HONEYBOOK_REACT_CURR_USER migration did — is refused
176
+ // until the user re-approves. Every user paired before that upgrade hits
177
+ // this exactly once. The library types the error with a `.hint` carrying
178
+ // the one remedy (revoke in the popup, re-run, approve the new scope), and
179
+ // that remedy is already inlined in `.message`; the generic suffix below is
180
+ // actively wrong here, because the magic link is already open. Match on the
181
+ // class name rather than `instanceof` so a duplicated @fetchproxy/server
182
+ // copy in the dependency tree can't silently defeat the check.
183
+ const errName = e?.name;
184
+ if (errName === 'FetchproxyScopeError' || /not in declared set/i.test(msg)) {
185
+ throw new Error(`HoneyBook auth: ${msg}`);
186
+ }
187
+ // Now that nothing blocks on a live outgoing request, a timeout means the
188
+ // extension never produced the declared storage reads — i.e. there is no
189
+ // signed-in portal tab for it to read from. classifyBridgeError keys off
190
+ // the error class, so this surfaces as a FetchproxyProtocolError
167
191
  // ('protocol'), not 'timeout' — match on the message too.
168
192
  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.');
193
+ throw new Error('HoneyBook auth: fetchproxy capture timed out. The extension never returned the vendor ' +
194
+ "portal's stored session, which usually means no tab is open and signed in on that portal. " +
195
+ 'Open the vendor magic-link URL in Chrome (with the fetchproxy extension installed), confirm ' +
196
+ 'the portal page has loaded, then re-run use_magic_link.');
174
197
  }
175
198
  throw new Error(`HoneyBook auth: fetchproxy capture failed: ${msg} — ` +
176
199
  'open the vendor magic-link URL in Chrome (with the fetchproxy 0.3.0 extension installed), ' +
177
200
  'then retry.');
178
201
  }
179
202
  // 0.4.0: pointer extractions land in `session.localStorage` keyed by
180
- // their declared `outputKey`. The raw `jStorage` JSON is never copied
181
- // into this process — only the four specific fields we actually need.
203
+ // their declared `outputKey`. No raw blob is ever copied into this
204
+ // process — only the specific fields we actually need.
182
205
  const authToken = session.localStorage['HB_AUTH_TOKEN'];
183
206
  const userId = session.localStorage['HB_AUTH_USER_ID'];
184
- const trustedDevice = session.localStorage['HB_TRUSTED_DEVICE'];
207
+ // Only these two are load-bearing. `hb-trusted-device` is sent when we have
208
+ // it but the API returns 200 without it, so a missing value must not fail
209
+ // the capture — requiring fields the API doesn't is what turned an upstream
210
+ // storage rename into a total outage.
211
+ const trustedDevice = session.localStorage['HB_TRUSTED_DEVICE'] || session.localStorage['HB_TRUSTED_DEVICE_LEGACY'];
185
212
  const companyNameFromHb = session.localStorage['HB_COMPANY_NAME'];
186
213
  if (!authToken) {
187
- throw new Error('HoneyBook auth: HB_AUTH_TOKEN not found at jStorage./HB_AUTH_TOKEN. ' +
214
+ throw new Error('HoneyBook auth: no auth token at HONEYBOOK_REACT_CURR_USER./authentication_token. ' +
188
215
  'Sign into the vendor portal via the magic-link URL and retry.');
189
216
  }
190
217
  if (!userId) {
191
- throw new Error('HoneyBook auth: HB_AUTH_USER_ID not found at jStorage./HB_AUTH_USER_ID.');
192
- }
193
- if (!trustedDevice) {
194
- throw new Error('HoneyBook auth: HB_TRUSTED_DEVICE not found at jStorage./HB_TRUSTED_DEVICE.');
195
- }
196
- const fingerprint = session.capturedHeaders['hb-api-fingerprint'];
197
- if (!fingerprint) {
198
- throw new Error('HoneyBook auth: hb-api-fingerprint header not captured. ' +
199
- 'Interact with the vendor portal (refresh the page, open a workspace) so the browser makes an ' +
200
- 'api.honeybook.com/api/v2/* request, then retry.');
218
+ throw new Error('HoneyBook auth: no user id at HONEYBOOK_REACT_CURR_USER./_id.');
201
219
  }
202
- // HB_CURR_USER may not be populated immediately on a fresh tab; fall
203
- // back to the portal subdomain so the user has a readable label.
220
+ // `company` is null on a client-side portal user, so this pointer usually
221
+ // resolves to nothing; fall back to the portal subdomain for a readable label.
204
222
  let companyName = companyNameFromHb || '';
205
223
  if (!companyName) {
206
224
  try {
@@ -215,8 +233,7 @@ export async function captureSessionViaFetchproxy(opts) {
215
233
  companyName,
216
234
  authToken,
217
235
  userId,
218
- trustedDevice,
219
- fingerprint,
236
+ ...(trustedDevice ? { trustedDevice } : {}),
220
237
  capturedAt: Date.now(),
221
238
  };
222
239
  sessionStore.add(captured);
package/dist/bundle.js CHANGED
@@ -35026,23 +35026,19 @@ var HoneyBookClient = class {
35026
35026
  accept: "application/json, text/plain, */*",
35027
35027
  "hb-api-auth-token": this.scope.authToken,
35028
35028
  "hb-api-user-id": this.scope.userId,
35029
- "hb-trusted-device": this.scope.trustedDevice,
35030
35029
  "hb-api-client-version": String(this.apiVersion),
35031
- "hb-api-fingerprint": this.scope.fingerprint,
35032
35030
  "hb-api-duplicate-calls-prevention-uuid": crypto.randomUUID(),
35033
35031
  "hb-admin-login": "false"
35034
35032
  };
35033
+ if (this.scope.trustedDevice) headers["hb-trusted-device"] = this.scope.trustedDevice;
35034
+ if (this.scope.fingerprint) headers["hb-api-fingerprint"] = this.scope.fingerprint;
35035
35035
  if (body !== void 0) headers["content-type"] = "application/json";
35036
35036
  const response = await fetch(`${API_BASE}${path}`, {
35037
35037
  method,
35038
35038
  headers,
35039
35039
  ...body !== void 0 ? { body: JSON.stringify(body) } : {}
35040
35040
  });
35041
- if (response.status === 401) {
35042
- throw new Error(
35043
- `HoneyBook auth expired for portal "${this.scope.companyName}" (${this.scope.portalOrigin}). Use the \`use_magic_link\` tool to capture a fresh session.`
35044
- );
35045
- }
35041
+ if (response.status === 401) throw this.authExpiredError();
35046
35042
  if (response.status === 429) {
35047
35043
  if (!isRateRetry) {
35048
35044
  await new Promise((r) => setTimeout(r, 2e3));
@@ -35052,6 +35048,9 @@ var HoneyBookClient = class {
35052
35048
  }
35053
35049
  if (!response.ok) {
35054
35050
  const text2 = await response.text();
35051
+ if (response.status === 404 && text2.includes("HBUnauthorizedError")) {
35052
+ throw this.authExpiredError();
35053
+ }
35055
35054
  if (text2.includes("HBWrongAPIVersionError") && !isVersionRetry) {
35056
35055
  try {
35057
35056
  const parsed = JSON.parse(text2);
@@ -35068,6 +35067,12 @@ var HoneyBookClient = class {
35068
35067
  const text = await response.text();
35069
35068
  return text ? JSON.parse(text) : null;
35070
35069
  }
35070
+ /** The one "your session died, re-capture it" error, raised from both auth paths. */
35071
+ authExpiredError() {
35072
+ return new Error(
35073
+ `HoneyBook auth expired for portal "${this.scope.companyName}" (${this.scope.portalOrigin}). Use the \`use_magic_link\` tool to capture a fresh session.`
35074
+ );
35075
+ }
35071
35076
  };
35072
35077
  function clearClientCache() {
35073
35078
  clientCache.clear();
@@ -39196,7 +39201,7 @@ function bridgeErrorInfo(err) {
39196
39201
  // package.json
39197
39202
  var package_default = {
39198
39203
  name: "honeybook-mcp",
39199
- version: "0.4.4",
39204
+ version: "0.4.6",
39200
39205
  mcpName: "io.github.chrischall/honeybook-mcp",
39201
39206
  description: "HoneyBook client-portal MCP server for Claude",
39202
39207
  author: "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -39272,44 +39277,54 @@ async function captureSessionViaFetchproxy(opts) {
39272
39277
  // portal subdomains (*.hbportal.co). The extension matches on suffix,
39273
39278
  // so listing both apexes covers any vendor.
39274
39279
  //
39275
- // Both are needed:
39276
- // - `hbportal.co` is where the vendor magic-link tab lives, so
39277
- // `jStorage` reads target it (via `storageDomain` below).
39278
- // - `honeybook.com` is required for the `hb-api-fingerprint`
39279
- // capture, because that header rides outgoing requests to
39280
- // `api.honeybook.com/api/v2/*` and the extension gates
39281
- // every captureHeader `host` against declared domains.
39280
+ // `hbportal.co` is where the vendor magic-link tab lives, so the
39281
+ // storage reads target it (via `storageDomain` below).
39282
+ // `honeybook.com` stays declared because the same session is valid
39283
+ // on the main app and callers may hold either origin.
39282
39284
  //
39283
39285
  // 0.4.1+ requires multi-domain MCPs to pick a `storageDomain` so
39284
39286
  // the cookie / localStorage / sessionStorage / indexedDb reads
39285
- // know which tab to target. captureRequestHeader is independently
39286
- // routed by its declared { host, path? }.
39287
+ // know which tab to target.
39287
39288
  domains: ["honeybook.com", "hbportal.co"],
39288
39289
  storageDomain: "hbportal.co",
39289
39290
  declare: {
39290
39291
  cookies: [],
39291
- // We don't need the full `jStorage` blob (it's ~8KB of mostly
39292
- // unused state). 0.4.0's JSON-pointer extraction lets us
39293
- // declare exactly the four fields the MCP uses; bootstrap
39294
- // reads `jStorage` once and applies all pointers in one call.
39295
- // The extension popup also shows the pointer paths verbatim
39296
- // so the user can see precisely what's being read.
39292
+ // `HONEYBOOK_REACT_CURR_USER` is ~6.5KB of mostly unused user
39293
+ // state. 0.4.0's JSON-pointer extraction lets us declare exactly
39294
+ // the fields the MCP uses; bootstrap reads each storage key once
39295
+ // and applies all pointers in one call. The extension popup shows
39296
+ // the pointer paths verbatim so the user can see precisely what's
39297
+ // being read.
39297
39298
  localStorage: [],
39298
39299
  localStoragePointers: [
39299
- { outputKey: "HB_AUTH_TOKEN", storageKey: "jStorage", jsonPointer: "/HB_AUTH_TOKEN" },
39300
- { outputKey: "HB_AUTH_USER_ID", storageKey: "jStorage", jsonPointer: "/HB_AUTH_USER_ID" },
39301
- { outputKey: "HB_TRUSTED_DEVICE", storageKey: "jStorage", jsonPointer: "/HB_TRUSTED_DEVICE" },
39302
- { outputKey: "HB_COMPANY_NAME", storageKey: "jStorage", jsonPointer: "/HB_CURR_USER/company/company_name" }
39300
+ {
39301
+ outputKey: "HB_AUTH_TOKEN",
39302
+ storageKey: "HONEYBOOK_REACT_CURR_USER",
39303
+ jsonPointer: "/authentication_token"
39304
+ },
39305
+ { outputKey: "HB_AUTH_USER_ID", storageKey: "HONEYBOOK_REACT_CURR_USER", jsonPointer: "/_id" },
39306
+ {
39307
+ outputKey: "HB_TRUSTED_DEVICE",
39308
+ storageKey: "HONEYBOOK_REACT_CURR_USER",
39309
+ jsonPointer: "/trusted_device"
39310
+ },
39311
+ {
39312
+ outputKey: "HB_COMPANY_NAME",
39313
+ storageKey: "HONEYBOOK_REACT_CURR_USER",
39314
+ jsonPointer: "/company/company_name"
39315
+ },
39316
+ // The one field of the old jStorage blob that survived the
39317
+ // migration. Kept as a fallback so a further reshuffle of the
39318
+ // React key doesn't cost us the trusted-device value too.
39319
+ {
39320
+ outputKey: "HB_TRUSTED_DEVICE_LEGACY",
39321
+ storageKey: "jStorage",
39322
+ jsonPointer: "/HB_TRUSTED_DEVICE"
39323
+ }
39303
39324
  ],
39304
39325
  sessionStorage: [],
39305
- captureHeaders: [
39306
- // The page's first call to api.honeybook.com/api/v2/* carries the
39307
- // `hb-api-fingerprint` header — a per-device FingerprintJS
39308
- // signal that the API requires on every subsequent request.
39309
- // It's NOT stored in jStorage; the only place to get it is off
39310
- // a real outgoing request.
39311
- { host: "api.honeybook.com", path: "/api/v2/*", headerName: "hb-api-fingerprint" }
39312
- ]
39326
+ // Nothing to sniff off a live request any more — see the header note above.
39327
+ captureHeaders: []
39313
39328
  },
39314
39329
  // 0.4.0: surface the pair code (six digits) on stderr so the user
39315
39330
  // can verify it against the extension popup. fetchproxy 0.4.0
@@ -39319,9 +39334,8 @@ async function captureSessionViaFetchproxy(opts) {
39319
39334
  process.stderr.write(`[honeybook-mcp] fetchproxy pair code: ${code}
39320
39335
  `);
39321
39336
  },
39322
- // 0.4.0: capture_request_header blocks on a real outgoing request
39323
- // to api.honeybook.com/api/v2/*. Surface a hint so the user knows
39324
- // to interact with the portal if the bootstrap appears to stall.
39337
+ // Surface the library's own progress hint so a stalled bootstrap
39338
+ // tells the user what it is waiting on.
39325
39339
  onWaiting: (hint) => {
39326
39340
  process.stderr.write(`[honeybook-mcp] ${hint}
39327
39341
  `);
@@ -39335,9 +39349,13 @@ async function captureSessionViaFetchproxy(opts) {
39335
39349
  );
39336
39350
  }
39337
39351
  const msg = e instanceof Error ? e.message : String(e);
39352
+ const errName = e?.name;
39353
+ if (errName === "FetchproxyScopeError" || /not in declared set/i.test(msg)) {
39354
+ throw new Error(`HoneyBook auth: ${msg}`);
39355
+ }
39338
39356
  if (bridgeError.type === "timeout" || /timeout/i.test(msg)) {
39339
39357
  throw new Error(
39340
- "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."
39358
+ "HoneyBook auth: fetchproxy capture timed out. The extension never returned the vendor portal's stored session, which usually means no tab is open and signed in on that portal. Open the vendor magic-link URL in Chrome (with the fetchproxy extension installed), confirm the portal page has loaded, then re-run use_magic_link."
39341
39359
  );
39342
39360
  }
39343
39361
  throw new Error(
@@ -39346,24 +39364,15 @@ async function captureSessionViaFetchproxy(opts) {
39346
39364
  }
39347
39365
  const authToken = session.localStorage["HB_AUTH_TOKEN"];
39348
39366
  const userId = session.localStorage["HB_AUTH_USER_ID"];
39349
- const trustedDevice = session.localStorage["HB_TRUSTED_DEVICE"];
39367
+ const trustedDevice = session.localStorage["HB_TRUSTED_DEVICE"] || session.localStorage["HB_TRUSTED_DEVICE_LEGACY"];
39350
39368
  const companyNameFromHb = session.localStorage["HB_COMPANY_NAME"];
39351
39369
  if (!authToken) {
39352
39370
  throw new Error(
39353
- "HoneyBook auth: HB_AUTH_TOKEN not found at jStorage./HB_AUTH_TOKEN. Sign into the vendor portal via the magic-link URL and retry."
39371
+ "HoneyBook auth: no auth token at HONEYBOOK_REACT_CURR_USER./authentication_token. Sign into the vendor portal via the magic-link URL and retry."
39354
39372
  );
39355
39373
  }
39356
39374
  if (!userId) {
39357
- throw new Error("HoneyBook auth: HB_AUTH_USER_ID not found at jStorage./HB_AUTH_USER_ID.");
39358
- }
39359
- if (!trustedDevice) {
39360
- throw new Error("HoneyBook auth: HB_TRUSTED_DEVICE not found at jStorage./HB_TRUSTED_DEVICE.");
39361
- }
39362
- const fingerprint = session.capturedHeaders["hb-api-fingerprint"];
39363
- if (!fingerprint) {
39364
- throw new Error(
39365
- "HoneyBook auth: hb-api-fingerprint header not captured. Interact with the vendor portal (refresh the page, open a workspace) so the browser makes an api.honeybook.com/api/v2/* request, then retry."
39366
- );
39375
+ throw new Error("HoneyBook auth: no user id at HONEYBOOK_REACT_CURR_USER./_id.");
39367
39376
  }
39368
39377
  let companyName = companyNameFromHb || "";
39369
39378
  if (!companyName) {
@@ -39378,8 +39387,7 @@ async function captureSessionViaFetchproxy(opts) {
39378
39387
  companyName,
39379
39388
  authToken,
39380
39389
  userId,
39381
- trustedDevice,
39382
- fingerprint,
39390
+ ...trustedDevice ? { trustedDevice } : {},
39383
39391
  capturedAt: Date.now()
39384
39392
  };
39385
39393
  sessionStore.add(captured);
@@ -39410,7 +39418,7 @@ function registerSessionTools(server) {
39410
39418
  server.registerTool(
39411
39419
  "use_magic_link",
39412
39420
  {
39413
- 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.`,
39421
+ 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 and leave that tab open. This tool then snapshots the auth fields out of the page's localStorage["HONEYBOOK_REACT_CURR_USER"] into ~/.honeybook-mcp/sessions.json. The tab only needs to be open and signed in \u2014 nothing is sniffed off a live request, so it does not matter whether the page is idle. 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.`,
39414
39422
  inputSchema: {
39415
39423
  magic_link_url: external_exports.string().url().describe(
39416
39424
  "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."
@@ -39833,7 +39841,7 @@ var __dirname = dirname2(fileURLToPath(import.meta.url));
39833
39841
  await loadDotenvSafely({ path: join4(__dirname, "..", ".env"), override: false });
39834
39842
  await runMcp({
39835
39843
  name: "honeybook-mcp",
39836
- version: "0.4.4",
39844
+ version: "0.4.6",
39837
39845
  // x-release-please-version
39838
39846
  tools: [
39839
39847
  registerSessionTools,
package/dist/client.js CHANGED
@@ -23,16 +23,23 @@ export class HoneyBookClient {
23
23
  this.apiVersion = apiVersion;
24
24
  }
25
25
  async request(method, path, body, isVersionRetry = false, isRateRetry = false) {
26
+ // Only the auth token, user id and a current client version are
27
+ // load-bearing. `hb-trusted-device` and `hb-api-fingerprint` are sent
28
+ // when the session has them — sessions captured by <=0.4.4 carry a
29
+ // fingerprint — but the API answers 200 without either, so they are
30
+ // omitted rather than serialized as the string "undefined".
26
31
  const headers = {
27
32
  accept: 'application/json, text/plain, */*',
28
33
  'hb-api-auth-token': this.scope.authToken,
29
34
  'hb-api-user-id': this.scope.userId,
30
- 'hb-trusted-device': this.scope.trustedDevice,
31
35
  'hb-api-client-version': String(this.apiVersion),
32
- 'hb-api-fingerprint': this.scope.fingerprint,
33
36
  'hb-api-duplicate-calls-prevention-uuid': crypto.randomUUID(),
34
37
  'hb-admin-login': 'false',
35
38
  };
39
+ if (this.scope.trustedDevice)
40
+ headers['hb-trusted-device'] = this.scope.trustedDevice;
41
+ if (this.scope.fingerprint)
42
+ headers['hb-api-fingerprint'] = this.scope.fingerprint;
36
43
  if (body !== undefined)
37
44
  headers['content-type'] = 'application/json';
38
45
  const response = await fetch(`${API_BASE}${path}`, {
@@ -40,10 +47,8 @@ export class HoneyBookClient {
40
47
  headers,
41
48
  ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
42
49
  });
43
- if (response.status === 401) {
44
- throw new Error(`HoneyBook auth expired for portal "${this.scope.companyName}" (${this.scope.portalOrigin}). ` +
45
- `Use the \`use_magic_link\` tool to capture a fresh session.`);
46
- }
50
+ if (response.status === 401)
51
+ throw this.authExpiredError();
47
52
  if (response.status === 429) {
48
53
  if (!isRateRetry) {
49
54
  await new Promise((r) => setTimeout(r, 2000));
@@ -53,6 +58,14 @@ export class HoneyBookClient {
53
58
  }
54
59
  if (!response.ok) {
55
60
  const text = await response.text();
61
+ // A revoked or stale token does NOT come back as 401 — the API answers
62
+ // 404 with an HBUnauthorizedError body. Without this, an expired session
63
+ // surfaced as an opaque "HoneyBook error 404" instead of the re-capture
64
+ // instruction. Gate on the body so a genuinely missing resource stays a
65
+ // plain 404.
66
+ if (response.status === 404 && text.includes('HBUnauthorizedError')) {
67
+ throw this.authExpiredError();
68
+ }
56
69
  if (text.includes('HBWrongAPIVersionError') && !isVersionRetry) {
57
70
  try {
58
71
  const parsed = JSON.parse(text);
@@ -70,6 +83,11 @@ export class HoneyBookClient {
70
83
  const text = await response.text();
71
84
  return (text ? JSON.parse(text) : null);
72
85
  }
86
+ /** The one "your session died, re-capture it" error, raised from both auth paths. */
87
+ authExpiredError() {
88
+ return new Error(`HoneyBook auth expired for portal "${this.scope.companyName}" (${this.scope.portalOrigin}). ` +
89
+ `Use the \`use_magic_link\` tool to capture a fresh session.`);
90
+ }
73
91
  }
74
92
  export function clearClientCache() {
75
93
  clientCache.clear();
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.4.4', // x-release-please-version
14
+ version: '0.4.6', // x-release-please-version
15
15
  tools: [
16
16
  registerSessionTools,
17
17
  registerWorkspaceFileTools,
@@ -10,10 +10,10 @@ import { captureSessionViaFetchproxy } from '../auth.js';
10
10
  * v3 behavior: the tool no longer drives Puppeteer. Instead, the user is
11
11
  * expected to have already opened the magic link in their real Chrome
12
12
  * (with the fetchproxy 0.3.0 extension installed). This tool then asks
13
- * the extension to snapshot the page's `localStorage["jStorage"]` and the
14
- * `hb-api-fingerprint` header off the first api.honeybook.com request,
15
- * via `@fetchproxy/bootstrap`. The result is persisted to
16
- * `~/.honeybook-mcp/sessions.json` and used by every other tool.
13
+ * the extension to snapshot the auth fields out of the page's
14
+ * `localStorage["HONEYBOOK_REACT_CURR_USER"]` via `@fetchproxy/bootstrap`.
15
+ * The result is persisted to `~/.honeybook-mcp/sessions.json` and used by
16
+ * every other tool.
17
17
  *
18
18
  * The `magic_link_url` arg is used only to derive the portalOrigin (the
19
19
  * cache key); we do NOT navigate to it. If the user hasn't actually
@@ -23,7 +23,7 @@ import { captureSessionViaFetchproxy } from '../auth.js';
23
23
  export async function useMagicLink(args) {
24
24
  const portalOrigin = normalizeOrigin(args.magic_link_url);
25
25
  const session = await captureSessionViaFetchproxy({ portalOrigin });
26
- // Clear client cache so getActiveClient picks up the fresh fingerprint.
26
+ // Clear client cache so getActiveClient rebuilds against the fresh token.
27
27
  clearClientCache();
28
28
  return textResult({
29
29
  ok: true,
@@ -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. 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.",
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 and leave that tab open. This tool then snapshots the auth fields out of the page's localStorage[\"HONEYBOOK_REACT_CURR_USER\"] into ~/.honeybook-mcp/sessions.json. The tab only needs to be open and signed in — nothing is sniffed off a live request, so it does not matter whether the page is idle. 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.4.4",
3
+ "version": "0.4.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>",
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.4.4",
9
+ "version": "0.4.6",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "honeybook-mcp",
14
- "version": "0.4.4",
14
+ "version": "0.4.6",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
@@ -14,9 +14,10 @@ description: >-
14
14
  HoneyBook has **no server-side login** a script can drive — a client never
15
15
  gets a password, only a magic-link email per vendor. The credential is
16
16
  whatever the signed-in `*.hbportal.co` portal tab already holds: a bearer
17
- token + user id + device id in `localStorage["jStorage"]`, plus a per-device
18
- `hb-api-fingerprint` request header that only appears on a live outgoing
19
- `api.honeybook.com/api/v2/*` call (it's FingerprintJS, not stored anywhere).
17
+ token + user id in `localStorage["HONEYBOOK_REACT_CURR_USER"]`. (HoneyBook
18
+ used to keep these in the AngularJS `localStorage["jStorage"]` blob as
19
+ `HB_AUTH_TOKEN`/`HB_AUTH_USER_ID`; that blob is now down to
20
+ `HB_TRUSTED_DEVICE`, `SESSION_COMPANY_ID` and routing state.)
20
21
  There's no bot wall on the API itself once you have those — `honeybook-mcp`'s
21
22
  own `client.ts` proves plain Node `fetch` works fine against
22
23
  `api.honeybook.com`. So this skill is **hybrid**: `fpx` captures the session
@@ -30,9 +31,8 @@ same retry rules.
30
31
 
31
32
  Two apexes are declared on one profile:
32
33
  - `hbportal.co` — the vendor's branded portal (e.g. `acme.hbportal.co`),
33
- where `jStorage` lives.
34
- - `honeybook.com` — required so the extension is allowed to capture the
35
- `hb-api-fingerprint` header off requests to `api.honeybook.com`.
34
+ where the stored session lives.
35
+ - `honeybook.com` — the main app, where the same session is also valid.
36
36
 
37
37
  ## One-time setup
38
38
 
@@ -40,8 +40,8 @@ Two apexes are declared on one profile:
40
40
  npm install -g @fetchproxy/cli # provides `fpx`
41
41
  fpx profile add honeybook --domain honeybook.com --domain hbportal.co
42
42
  fpx profile declare honeybook \
43
- --local-storage jStorage \
44
- --capture-header 'hb-api-fingerprint@api.honeybook.com/api/v2/*'
43
+ --local-storage HONEYBOOK_REACT_CURR_USER \
44
+ --local-storage jStorage
45
45
  fpx pair -p honeybook # prints a pair code → approve in Transporter
46
46
  ```
47
47
 
@@ -60,26 +60,24 @@ after the first approval every later `fpx` call reuses it.
60
60
  fpx session -p honeybook --storage-domain hbportal.co > /tmp/hb-session.json
61
61
  ```
62
62
 
63
- **The fingerprint capture needs a FRESH request, not a loaded page.** A
64
- portal tab that finished loading and went idle has no outgoing
65
- `api.honeybook.com/api/v2/*` call left to sniff, so the capture times out.
66
- While `fpx session` is waiting (~30s window), refresh the portal tab or open
67
- a workspace/file so the page issues a new API call.
63
+ The tab only has to be **open and signed in**. Nothing is sniffed off a live
64
+ request, so it does not matter whether the page has gone idle.
68
65
 
69
- 3. Extract the fields `client.ts` needs (`jStorage` is one raw localStorage
70
- value packing several keys as JSON — parse it with `fromjson`):
66
+ 3. Extract the fields `client.ts` needs (each localStorage value is one raw
67
+ JSON string — parse it with `fromjson`):
71
68
 
72
69
  ```sh
73
- AUTH_TOKEN=$(jq -r '.localStorage.jStorage | fromjson | .HB_AUTH_TOKEN' /tmp/hb-session.json)
74
- USER_ID=$(jq -r '.localStorage.jStorage | fromjson | .HB_AUTH_USER_ID' /tmp/hb-session.json)
75
- TRUSTED_DEVICE=$(jq -r '.localStorage.jStorage | fromjson | .HB_TRUSTED_DEVICE' /tmp/hb-session.json)
76
- FINGERPRINT=$(jq -r '.capturedHeaders["hb-api-fingerprint"]' /tmp/hb-session.json)
70
+ AUTH_TOKEN=$(jq -r '.localStorage.HONEYBOOK_REACT_CURR_USER | fromjson | .authentication_token' /tmp/hb-session.json)
71
+ USER_ID=$(jq -r '.localStorage.HONEYBOOK_REACT_CURR_USER | fromjson | ._id' /tmp/hb-session.json)
72
+ # Optional — the API returns 200 without it. The React blob and jStorage hold
73
+ # DIFFERENT values; either is accepted. `// empty` keeps an absent field from
74
+ # becoming the literal string "null".
75
+ TRUSTED_DEVICE=$(jq -r '.localStorage.jStorage | fromjson | .HB_TRUSTED_DEVICE // empty' /tmp/hb-session.json)
77
76
  PORTAL_ORIGIN='https://<vendor>.hbportal.co' # the magic-link URL's origin
78
77
  ```
79
78
 
80
- If any of `AUTH_TOKEN`/`USER_ID`/`TRUSTED_DEVICE`/`FINGERPRINT` come back
81
- empty/`null`, the capture didn't see what it needed — re-open the magic link
82
- (or interact with the tab) and re-run step 2.
79
+ If `AUTH_TOKEN` or `USER_ID` comes back empty/`null`, the capture didn't see
80
+ what it needed — re-open the magic link and re-run step 2.
83
81
 
84
82
  With more than one vendor tab open at once, disambiguate with
85
83
  `--storage-subdomain <vendor>` (e.g. `--storage-subdomain acme`).
@@ -94,17 +92,19 @@ API_VERSION=$(curl -s 'https://api.honeybook.com/api/gon?callback=parseGon' \
94
92
 
95
93
  ## Core call pattern
96
94
 
97
- Every real request carries the same eight headers
98
- (`client.ts`'s `HoneyBookClient.request`):
95
+ Every real request carries the same headers
96
+ (`client.ts`'s `HoneyBookClient.request`). Only `hb-api-auth-token`,
97
+ `hb-api-user-id` and a current `hb-api-client-version` are load-bearing —
98
+ `hb-trusted-device` is optional and `hb-api-fingerprint` is no longer
99
+ required at all:
99
100
 
100
101
  ```sh
101
102
  curl -s "https://api.honeybook.com/api/v2/users/$USER_ID/workspace_files" \
102
103
  -H 'accept: application/json, text/plain, */*' \
103
104
  -H "hb-api-auth-token: $AUTH_TOKEN" \
104
105
  -H "hb-api-user-id: $USER_ID" \
105
- -H "hb-trusted-device: $TRUSTED_DEVICE" \
106
+ ${TRUSTED_DEVICE:+-H "hb-trusted-device: $TRUSTED_DEVICE"} \
106
107
  -H "hb-api-client-version: $API_VERSION" \
107
- -H "hb-api-fingerprint: $FINGERPRINT" \
108
108
  -H "hb-api-duplicate-calls-prevention-uuid: $(uuidgen)" \
109
109
  -H 'hb-admin-login: false' \
110
110
  | jq '.data'
@@ -120,7 +120,9 @@ Ready-to-run commands for all four read endpoints are in
120
120
 
121
121
  ## The rules that matter
122
122
 
123
- - **401 → session expired.** Re-run the capture (magic link tab must still
123
+ - **401, or 404 with an `HBUnauthorizedError` body → session expired.** A
124
+ revoked token does not reliably come back as 401, so check the body type
125
+ before concluding a resource is missing. Re-run the capture (magic link tab must still
124
126
  be open and signed in).
125
127
  - **429 → rate limited.** `client.ts` waits 2s and retries once; do the same
126
128
  before giving up.
@@ -146,7 +148,7 @@ Ready-to-run commands for all four read endpoints are in
146
148
 
147
149
  ## Notes
148
150
 
149
- - Session data (`AUTH_TOKEN`, `TRUSTED_DEVICE`, `FINGERPRINT`) is opaque and
151
+ - Session data (`AUTH_TOKEN`, `TRUSTED_DEVICE`) is opaque and
150
152
  long-lived server-side (no client-visible JWT expiry) — keep it in shell
151
153
  variables, not a world-readable file, if you must persist it at all.
152
154
  - This project is developed and maintained by AI (Claude).
@@ -2,10 +2,10 @@
2
2
 
3
3
  Ready-to-run commands for the four live read endpoints `honeybook-mcp`
4
4
  actually calls (from `src/tools/*.ts` + `src/client.ts`). All are
5
- `GET api.honeybook.com/api/v2/*`, carrying the eight headers built in
5
+ `GET api.honeybook.com/api/v2/*`, carrying the same headers built in
6
6
  `HoneyBookClient.request` — see `../SKILL.md` for how to capture
7
- `$AUTH_TOKEN`/`$USER_ID`/`$TRUSTED_DEVICE`/`$FINGERPRINT`/`$API_VERSION`
8
- first.
7
+ `$AUTH_TOKEN`/`$USER_ID`/`$API_VERSION` first. `$TRUSTED_DEVICE` is
8
+ optional and `hb-api-fingerprint` is not required at all.
9
9
 
10
10
  ```sh
11
11
  hb_get() { # $1 = path (e.g. /api/v2/users/$USER_ID/workspace_files)
@@ -13,9 +13,8 @@ hb_get() { # $1 = path (e.g. /api/v2/users/$USER_ID/workspace_files)
13
13
  -H 'accept: application/json, text/plain, */*' \
14
14
  -H "hb-api-auth-token: $AUTH_TOKEN" \
15
15
  -H "hb-api-user-id: $USER_ID" \
16
- -H "hb-trusted-device: $TRUSTED_DEVICE" \
16
+ ${TRUSTED_DEVICE:+-H "hb-trusted-device: $TRUSTED_DEVICE"} \
17
17
  -H "hb-api-client-version: $API_VERSION" \
18
- -H "hb-api-fingerprint: $FINGERPRINT" \
19
18
  -H "hb-api-duplicate-calls-prevention-uuid: $(uuidgen)" \
20
19
  -H 'hb-admin-login: false'
21
20
  }
@@ -113,7 +112,9 @@ vendor — not an error.
113
112
  # Wrong API version — re-derive it from the error body itself (no /api/gon round-trip needed)
114
113
  jq -r 'select(type=="object") | .error_data.server_api_version // empty' /tmp/hb-resp.json
115
114
 
116
- # HTTP status: 401 = session expired (re-capture), 429 = rate limited (wait 2s, retry once)
115
+ # HTTP status: 401 = session expired (re-capture), 429 = rate limited (wait 2s, retry once).
116
+ # A 404 whose body names HBUnauthorizedError is ALSO an expired session, not a
117
+ # missing resource — a revoked token does not reliably come back as 401.
117
118
  ```
118
119
 
119
120
  A non-2xx HTTP status with a body matching `HBWrongAPIVersionError` means