honeybook-mcp 0.4.4 → 0.4.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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -2
- package/dist/auth.js +98 -81
- package/dist/bundle.js +63 -55
- package/dist/client.js +24 -6
- package/dist/index.js +1 -1
- package/dist/tools/sessions.js +6 -6
- package/package.json +1 -1
- package/server.json +2 -2
- package/skills/honeybook-fpx/SKILL.md +30 -28
- package/skills/honeybook-fpx/references/requests.md +7 -6
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "HoneyBook client-portal tools for Claude Code",
|
|
10
|
-
"version": "0.4.
|
|
10
|
+
"version": "0.4.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.4.
|
|
18
|
+
"version": "0.4.5",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Chall"
|
|
21
21
|
},
|
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["
|
|
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,8 @@ 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
|
-
- **"
|
|
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.
|
|
115
116
|
|
|
116
117
|
## Security
|
|
117
118
|
|
package/dist/auth.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ────────────────────────────────────────────────────────────────────────────
|
|
2
|
-
// Session capture — Pattern A template (read_local_storage
|
|
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
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
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
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
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
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
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.
|
|
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
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
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
|
-
{
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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
|
-
//
|
|
143
|
-
//
|
|
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
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
// the
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
|
|
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
|
|
170
|
-
'
|
|
171
|
-
'
|
|
172
|
-
'
|
|
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`.
|
|
181
|
-
//
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
//
|
|
203
|
-
// back to the portal subdomain
|
|
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.
|
|
39204
|
+
version: "0.4.5",
|
|
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
|
-
//
|
|
39276
|
-
//
|
|
39277
|
-
//
|
|
39278
|
-
//
|
|
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.
|
|
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
|
-
//
|
|
39292
|
-
//
|
|
39293
|
-
//
|
|
39294
|
-
//
|
|
39295
|
-
//
|
|
39296
|
-
//
|
|
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
|
-
{
|
|
39300
|
-
|
|
39301
|
-
|
|
39302
|
-
|
|
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
|
-
|
|
39306
|
-
|
|
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
|
-
//
|
|
39323
|
-
//
|
|
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
|
|
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:
|
|
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:
|
|
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["
|
|
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.
|
|
39844
|
+
version: "0.4.5",
|
|
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
|
|
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.
|
|
14
|
+
version: '0.4.5', // x-release-please-version
|
|
15
15
|
tools: [
|
|
16
16
|
registerSessionTools,
|
|
17
17
|
registerWorkspaceFileTools,
|
package/dist/tools/sessions.js
CHANGED
|
@@ -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
|
|
14
|
-
* `
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
|
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[\"
|
|
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
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.
|
|
9
|
+
"version": "0.4.5",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "honeybook-mcp",
|
|
14
|
-
"version": "0.4.
|
|
14
|
+
"version": "0.4.5",
|
|
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
|
|
18
|
-
|
|
19
|
-
`
|
|
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
|
|
34
|
-
- `honeybook.com` —
|
|
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
|
|
44
|
-
--
|
|
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
|
-
|
|
64
|
-
|
|
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 (
|
|
70
|
-
|
|
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.
|
|
74
|
-
USER_ID=$(jq -r '.localStorage.
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
81
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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`/`$
|
|
8
|
-
|
|
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
|
-
|
|
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
|