honeybook-mcp 0.4.3 → 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 +643 -151
- package/dist/client.js +24 -6
- package/dist/index.js +1 -1
- package/dist/tools/sessions.js +6 -6
- package/package.json +3 -3
- 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);
|