honeybook-mcp 0.3.6 → 0.4.0
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/dist/bundle.js +128 -8
- package/dist/index.js +1 -1
- package/package.json +3 -3
- package/server.json +2 -2
- package/skills/honeybook-fpx/SKILL.md +152 -0
- package/skills/honeybook-fpx/references/requests.md +122 -0
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "HoneyBook client-portal tools for Claude Code",
|
|
10
|
-
"version": "0.
|
|
10
|
+
"version": "0.4.0"
|
|
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.
|
|
18
|
+
"version": "0.4.0",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Chall"
|
|
21
21
|
},
|
package/dist/bundle.js
CHANGED
|
@@ -35071,6 +35071,7 @@ var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
|
35071
35071
|
"capture_request_header",
|
|
35072
35072
|
"capture_redirect",
|
|
35073
35073
|
"read_indexed_db",
|
|
35074
|
+
"read_dom",
|
|
35074
35075
|
"download"
|
|
35075
35076
|
]);
|
|
35076
35077
|
|
|
@@ -35372,6 +35373,44 @@ function assertIndexedDbScopesArray(value, label) {
|
|
|
35372
35373
|
}
|
|
35373
35374
|
}
|
|
35374
35375
|
}
|
|
35376
|
+
var DOM_SELECTOR_RE = /^[^-]{1,512}$/;
|
|
35377
|
+
var DOM_ATTRIBUTE_RE = /^[A-Za-z_:][A-Za-z0-9_:.\-]{0,127}$/;
|
|
35378
|
+
function assertDomSelectorsArray(value, label) {
|
|
35379
|
+
if (!Array.isArray(value)) {
|
|
35380
|
+
throw new ProtocolError(`${label}: expected array, got ${typeof value}`);
|
|
35381
|
+
}
|
|
35382
|
+
const seen = /* @__PURE__ */ new Set();
|
|
35383
|
+
for (let i = 0; i < value.length; i++) {
|
|
35384
|
+
const entry = value[i];
|
|
35385
|
+
assertObject(entry, `${label}[${i}]`);
|
|
35386
|
+
if (entry.name === void 0) {
|
|
35387
|
+
throw new ProtocolError(`${label}[${i}].name: missing`);
|
|
35388
|
+
}
|
|
35389
|
+
if (entry.selector === void 0) {
|
|
35390
|
+
throw new ProtocolError(`${label}[${i}].selector: missing`);
|
|
35391
|
+
}
|
|
35392
|
+
if (typeof entry.name !== "string" || !SCOPE_KEY_RE.test(entry.name)) {
|
|
35393
|
+
throw new ProtocolError(`${label}[${i}].name: invalid ${JSON.stringify(entry.name)}`);
|
|
35394
|
+
}
|
|
35395
|
+
if (typeof entry.selector !== "string" || !DOM_SELECTOR_RE.test(entry.selector)) {
|
|
35396
|
+
throw new ProtocolError(`${label}[${i}].selector: invalid ${JSON.stringify(entry.selector)}`);
|
|
35397
|
+
}
|
|
35398
|
+
if (entry.attribute !== void 0) {
|
|
35399
|
+
if (typeof entry.attribute !== "string" || !DOM_ATTRIBUTE_RE.test(entry.attribute)) {
|
|
35400
|
+
throw new ProtocolError(`${label}[${i}].attribute: invalid ${JSON.stringify(entry.attribute)}`);
|
|
35401
|
+
}
|
|
35402
|
+
}
|
|
35403
|
+
if (seen.has(entry.name)) {
|
|
35404
|
+
throw new ProtocolError(`${label}: duplicate name ${JSON.stringify(entry.name)}`);
|
|
35405
|
+
}
|
|
35406
|
+
seen.add(entry.name);
|
|
35407
|
+
for (const k of Object.keys(entry)) {
|
|
35408
|
+
if (k !== "name" && k !== "selector" && k !== "attribute") {
|
|
35409
|
+
throw new ProtocolError(`${label}[${i}]: unexpected field ${JSON.stringify(k)}`);
|
|
35410
|
+
}
|
|
35411
|
+
}
|
|
35412
|
+
}
|
|
35413
|
+
}
|
|
35375
35414
|
function validateFrame(raw) {
|
|
35376
35415
|
assertObject(raw, "frame");
|
|
35377
35416
|
const t = raw.type;
|
|
@@ -35447,6 +35486,9 @@ function validateHello(raw) {
|
|
|
35447
35486
|
if (raw.sessionStoragePointers !== void 0) {
|
|
35448
35487
|
assertStoragePointersArray(raw.sessionStoragePointers, "hello.sessionStoragePointers", raw.sessionStorageKeys);
|
|
35449
35488
|
}
|
|
35489
|
+
if (raw.domSelectors !== void 0) {
|
|
35490
|
+
assertDomSelectorsArray(raw.domSelectors, "hello.domSelectors");
|
|
35491
|
+
}
|
|
35450
35492
|
assertBase64(raw.identityX25519Pub, "hello.identityX25519Pub");
|
|
35451
35493
|
assertBase64(raw.identityEd25519Pub, "hello.identityEd25519Pub");
|
|
35452
35494
|
assertBase64(raw.sessionNonce, "hello.sessionNonce");
|
|
@@ -35681,6 +35723,21 @@ function validateInnerRequest(raw) {
|
|
|
35681
35723
|
}
|
|
35682
35724
|
return raw;
|
|
35683
35725
|
}
|
|
35726
|
+
if (raw.op === "read_dom") {
|
|
35727
|
+
assertObject(raw.init, "inner.init");
|
|
35728
|
+
if (raw.init.origin === void 0)
|
|
35729
|
+
throw new ProtocolError("inner.init.origin: missing");
|
|
35730
|
+
if (raw.init.names === void 0)
|
|
35731
|
+
throw new ProtocolError("inner.init.names: missing");
|
|
35732
|
+
assertHttpsOriginOnly(raw.init.origin, "inner.init.origin");
|
|
35733
|
+
assertNonEmptyKeyArray(raw.init.names, "inner.init.names");
|
|
35734
|
+
for (const k of Object.keys(raw.init)) {
|
|
35735
|
+
if (k !== "origin" && k !== "names") {
|
|
35736
|
+
throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on read_dom`);
|
|
35737
|
+
}
|
|
35738
|
+
}
|
|
35739
|
+
return raw;
|
|
35740
|
+
}
|
|
35684
35741
|
if (raw.op === "download") {
|
|
35685
35742
|
assertObject(raw.init, "inner.init");
|
|
35686
35743
|
if (raw.init.url === void 0) {
|
|
@@ -35706,7 +35763,7 @@ function validateInnerRequest(raw) {
|
|
|
35706
35763
|
}
|
|
35707
35764
|
return raw;
|
|
35708
35765
|
}
|
|
35709
|
-
throw new ProtocolError(`inner.op: must be one of "fetch", "read_cookies", "read_local_storage", "read_session_storage", "capture_request_header", "capture_redirect", "read_indexed_db", "download"; got ${JSON.stringify(raw.op)}`);
|
|
35766
|
+
throw new ProtocolError(`inner.op: must be one of "fetch", "read_cookies", "read_local_storage", "read_session_storage", "capture_request_header", "capture_redirect", "read_indexed_db", "read_dom", "download"; got ${JSON.stringify(raw.op)}`);
|
|
35710
35767
|
}
|
|
35711
35768
|
function assertNonEmptyKeyArray(value, label) {
|
|
35712
35769
|
if (!Array.isArray(value)) {
|
|
@@ -35791,6 +35848,13 @@ function validateInnerResponse(raw) {
|
|
|
35791
35848
|
assertObject(raw.values, "inner.values");
|
|
35792
35849
|
return raw;
|
|
35793
35850
|
}
|
|
35851
|
+
if (op === "read_dom") {
|
|
35852
|
+
if (raw.values === void 0) {
|
|
35853
|
+
throw new ProtocolError("inner.values: missing on read_dom response");
|
|
35854
|
+
}
|
|
35855
|
+
assertStringMap(raw.values, "inner.values");
|
|
35856
|
+
return raw;
|
|
35857
|
+
}
|
|
35794
35858
|
if (op === "download") {
|
|
35795
35859
|
assertObject(raw.value, "inner.value");
|
|
35796
35860
|
assertString(raw.value.path, "inner.value.path");
|
|
@@ -36130,6 +36194,13 @@ async function buildServerHello(opts) {
|
|
|
36130
36194
|
jsonPointer: d.jsonPointer
|
|
36131
36195
|
}));
|
|
36132
36196
|
}
|
|
36197
|
+
if (opts.domSelectors && opts.domSelectors.length > 0) {
|
|
36198
|
+
hello.domSelectors = opts.domSelectors.map((d) => ({
|
|
36199
|
+
name: d.name,
|
|
36200
|
+
selector: d.selector,
|
|
36201
|
+
...d.attribute !== void 0 ? { attribute: d.attribute } : {}
|
|
36202
|
+
}));
|
|
36203
|
+
}
|
|
36133
36204
|
return hello;
|
|
36134
36205
|
}
|
|
36135
36206
|
|
|
@@ -36219,7 +36290,8 @@ async function startHost(opts) {
|
|
|
36219
36290
|
captureHeaders: opts.ownCaptureHeaders,
|
|
36220
36291
|
indexedDbScopes: opts.ownIndexedDbScopes,
|
|
36221
36292
|
localStoragePointers: opts.ownLocalStoragePointers,
|
|
36222
|
-
sessionStoragePointers: opts.ownSessionStoragePointers
|
|
36293
|
+
sessionStoragePointers: opts.ownSessionStoragePointers,
|
|
36294
|
+
domSelectors: opts.ownDomSelectors
|
|
36223
36295
|
});
|
|
36224
36296
|
const ownSessionNonce = fromB64(ownHello.sessionNonce);
|
|
36225
36297
|
let extensionWs = null;
|
|
@@ -36454,6 +36526,7 @@ async function startPeer(opts) {
|
|
|
36454
36526
|
sessionStorageKeys: opts.sessionStorageKeys,
|
|
36455
36527
|
captureHeaders: opts.captureHeaders,
|
|
36456
36528
|
indexedDbScopes: opts.indexedDbScopes,
|
|
36529
|
+
domSelectors: opts.domSelectors,
|
|
36457
36530
|
localStoragePointers: opts.localStoragePointers,
|
|
36458
36531
|
sessionStoragePointers: opts.sessionStoragePointers
|
|
36459
36532
|
});
|
|
@@ -36861,6 +36934,11 @@ var FetchproxyServer = class {
|
|
|
36861
36934
|
key: d.key,
|
|
36862
36935
|
jsonPointer: d.jsonPointer
|
|
36863
36936
|
})),
|
|
36937
|
+
domSelectors: (opts.domSelectors ?? []).map((d) => ({
|
|
36938
|
+
name: d.name,
|
|
36939
|
+
selector: d.selector,
|
|
36940
|
+
...d.attribute !== void 0 ? { attribute: d.attribute } : {}
|
|
36941
|
+
})),
|
|
36864
36942
|
// 0.8.0+: timer + lazy-revive default to ON. Every realty MCP
|
|
36865
36943
|
// adapter was about to set these to the same numbers anyway; the
|
|
36866
36944
|
// back-door is `0` (explicit opt-out) if a caller genuinely wants
|
|
@@ -36981,6 +37059,7 @@ var FetchproxyServer = class {
|
|
|
36981
37059
|
ownIndexedDbScopes: this.opts.indexedDbScopes,
|
|
36982
37060
|
ownLocalStoragePointers: this.opts.localStoragePointers,
|
|
36983
37061
|
ownSessionStoragePointers: this.opts.sessionStoragePointers,
|
|
37062
|
+
ownDomSelectors: this.opts.domSelectors,
|
|
36984
37063
|
onPairCode: this.opts.onPairCode
|
|
36985
37064
|
});
|
|
36986
37065
|
this.hostHandle.onOwnInner((inner) => this.onInner(inner));
|
|
@@ -37008,7 +37087,8 @@ var FetchproxyServer = class {
|
|
|
37008
37087
|
captureHeaders: this.opts.captureHeaders,
|
|
37009
37088
|
indexedDbScopes: this.opts.indexedDbScopes,
|
|
37010
37089
|
localStoragePointers: this.opts.localStoragePointers,
|
|
37011
|
-
sessionStoragePointers: this.opts.sessionStoragePointers
|
|
37090
|
+
sessionStoragePointers: this.opts.sessionStoragePointers,
|
|
37091
|
+
domSelectors: this.opts.domSelectors
|
|
37012
37092
|
});
|
|
37013
37093
|
this.peerHandle.onInner((inner) => this.onInner(inner));
|
|
37014
37094
|
this.peerHandle.onRenegotiate(() => {
|
|
@@ -37983,6 +38063,46 @@ var FetchproxyServer = class {
|
|
|
37983
38063
|
await this.sendInnerFrame(inner);
|
|
37984
38064
|
return this._withVerbTimeout(pending, this.pendingIdb, id, origin);
|
|
37985
38065
|
}
|
|
38066
|
+
/**
|
|
38067
|
+
* 1.4.0+: read declared DOM values from the user's signed-in tab.
|
|
38068
|
+
* Requires `'read_dom'` in capabilities AND every requested `name` to
|
|
38069
|
+
* match a declared `domSelectors` entry. The extension reads each
|
|
38070
|
+
* declared selector from the matched tab's DOM (isolated-world
|
|
38071
|
+
* `querySelector`, value or attribute) — no page-JS execution.
|
|
38072
|
+
*
|
|
38073
|
+
* Returns a `Record<string, string>` of `name → value`, with names
|
|
38074
|
+
* whose element (or attribute) was absent omitted. Throws
|
|
38075
|
+
* `FetchproxyProtocolError` on bridge failures and a plain `Error` on
|
|
38076
|
+
* developer mistakes (undeclared capability, undeclared name).
|
|
38077
|
+
*/
|
|
38078
|
+
async readDom(opts) {
|
|
38079
|
+
if (!this.opts.capabilities.includes("read_dom")) {
|
|
38080
|
+
throw new Error('FetchproxyServer.readDom(): MCP did not declare "read_dom" in capabilities');
|
|
38081
|
+
}
|
|
38082
|
+
await this.ensureConnected();
|
|
38083
|
+
this.throwIfPendingPair();
|
|
38084
|
+
if (!Array.isArray(opts.names) || opts.names.length === 0) {
|
|
38085
|
+
throw new Error("FetchproxyServer.readDom: opts.names must be a non-empty array");
|
|
38086
|
+
}
|
|
38087
|
+
this.assertScopeSubset(opts.names, this.opts.domSelectors.map((d) => d.name), "domSelectors");
|
|
38088
|
+
if (opts.subdomain !== void 0)
|
|
38089
|
+
assertSubdomainLabel(opts.subdomain);
|
|
38090
|
+
const baseDomain = this.resolveBaseDomain(opts.domain);
|
|
38091
|
+
const host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
|
|
38092
|
+
const origin = `https://${host}`;
|
|
38093
|
+
const id = this.nextRequestId++;
|
|
38094
|
+
const inner = {
|
|
38095
|
+
type: "request",
|
|
38096
|
+
id,
|
|
38097
|
+
op: "read_dom",
|
|
38098
|
+
init: { origin, names: [...opts.names] }
|
|
38099
|
+
};
|
|
38100
|
+
const pending = new Promise((resolve, reject) => {
|
|
38101
|
+
this.pendingStorage.set(id, { resolve, reject });
|
|
38102
|
+
});
|
|
38103
|
+
await this.sendInnerFrame(inner);
|
|
38104
|
+
return this._withVerbTimeout(pending, this.pendingStorage, id, origin);
|
|
38105
|
+
}
|
|
37986
38106
|
assertScopeSubset(requested, declared, label) {
|
|
37987
38107
|
const undeclared = undeclaredKeys(requested, declared);
|
|
37988
38108
|
if (undeclared.length > 0) {
|
|
@@ -38054,7 +38174,7 @@ var FetchproxyServer = class {
|
|
|
38054
38174
|
if (storageCb) {
|
|
38055
38175
|
this.pendingStorage.delete(inner.id);
|
|
38056
38176
|
if (inner.ok) {
|
|
38057
|
-
if ((inner.op === "read_local_storage" || inner.op === "read_session_storage") && inner.values) {
|
|
38177
|
+
if ((inner.op === "read_local_storage" || inner.op === "read_session_storage" || inner.op === "read_dom") && inner.values) {
|
|
38058
38178
|
storageCb.resolve({ ...inner.values });
|
|
38059
38179
|
} else {
|
|
38060
38180
|
storageCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on storage awaiter`));
|
|
@@ -38412,7 +38532,7 @@ function bridgeErrorInfo(err) {
|
|
|
38412
38532
|
// package.json
|
|
38413
38533
|
var package_default = {
|
|
38414
38534
|
name: "honeybook-mcp",
|
|
38415
|
-
version: "0.
|
|
38535
|
+
version: "0.4.0",
|
|
38416
38536
|
mcpName: "io.github.chrischall/honeybook-mcp",
|
|
38417
38537
|
description: "HoneyBook client-portal MCP server for Claude",
|
|
38418
38538
|
author: "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -38452,7 +38572,7 @@ var package_default = {
|
|
|
38452
38572
|
"test:watch": "vitest"
|
|
38453
38573
|
},
|
|
38454
38574
|
dependencies: {
|
|
38455
|
-
"@chrischall/mcp-utils": "^0.
|
|
38575
|
+
"@chrischall/mcp-utils": "^0.13.0",
|
|
38456
38576
|
"@fetchproxy/bootstrap": "^1.0.0",
|
|
38457
38577
|
"@fetchproxy/server": "^1.0.0",
|
|
38458
38578
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
@@ -38463,7 +38583,7 @@ var package_default = {
|
|
|
38463
38583
|
"@types/node": "^26.0.0",
|
|
38464
38584
|
"@vitest/coverage-v8": "^4.1.7",
|
|
38465
38585
|
esbuild: "^0.28.0",
|
|
38466
|
-
typescript: "^
|
|
38586
|
+
typescript: "^7.0.2",
|
|
38467
38587
|
vitest: "^4.1.7"
|
|
38468
38588
|
}
|
|
38469
38589
|
};
|
|
@@ -39049,7 +39169,7 @@ var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
|
39049
39169
|
await loadDotenvSafely({ path: join3(__dirname, "..", ".env"), override: false });
|
|
39050
39170
|
await runMcp({
|
|
39051
39171
|
name: "honeybook-mcp",
|
|
39052
|
-
version: "0.
|
|
39172
|
+
version: "0.4.0",
|
|
39053
39173
|
// x-release-please-version
|
|
39054
39174
|
tools: [
|
|
39055
39175
|
registerSessionTools,
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { registerContractTools } from './tools/contracts.js';
|
|
|
11
11
|
import { registerInvoiceTools } from './tools/invoices.js';
|
|
12
12
|
await runMcp({
|
|
13
13
|
name: 'honeybook-mcp',
|
|
14
|
-
version: '0.
|
|
14
|
+
version: '0.4.0', // x-release-please-version
|
|
15
15
|
tools: [
|
|
16
16
|
registerSessionTools,
|
|
17
17
|
registerWorkspaceFileTools,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "honeybook-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"mcpName": "io.github.chrischall/honeybook-mcp",
|
|
5
5
|
"description": "HoneyBook client-portal MCP server for Claude",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"test:watch": "vitest"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@chrischall/mcp-utils": "^0.
|
|
43
|
+
"@chrischall/mcp-utils": "^0.13.0",
|
|
44
44
|
"@fetchproxy/bootstrap": "^1.0.0",
|
|
45
45
|
"@fetchproxy/server": "^1.0.0",
|
|
46
46
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"@types/node": "^26.0.0",
|
|
52
52
|
"@vitest/coverage-v8": "^4.1.7",
|
|
53
53
|
"esbuild": "^0.28.0",
|
|
54
|
-
"typescript": "^
|
|
54
|
+
"typescript": "^7.0.2",
|
|
55
55
|
"vitest": "^4.1.7"
|
|
56
56
|
}
|
|
57
57
|
}
|
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.
|
|
9
|
+
"version": "0.4.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "honeybook-mcp",
|
|
14
|
-
"version": "0.
|
|
14
|
+
"version": "0.4.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: honeybook-fpx
|
|
3
|
+
description: >-
|
|
4
|
+
Read HoneyBook client-portal data (contracts, invoices, proposals, payment
|
|
5
|
+
methods, workspace status) from a shell with the fpx CLI (@fetchproxy/cli)
|
|
6
|
+
instead of running the honeybook-mcp server — capture a vendor session once
|
|
7
|
+
via the signed-in browser tab, then curl api.honeybook.com directly. Use
|
|
8
|
+
when you want HoneyBook data without the MCP, in a script, or on a machine
|
|
9
|
+
where the MCP isn't installed.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# HoneyBook via fpx + curl (no MCP)
|
|
13
|
+
|
|
14
|
+
HoneyBook has **no server-side login** a script can drive — a client never
|
|
15
|
+
gets a password, only a magic-link email per vendor. The credential is
|
|
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).
|
|
20
|
+
There's no bot wall on the API itself once you have those — `honeybook-mcp`'s
|
|
21
|
+
own `client.ts` proves plain Node `fetch` works fine against
|
|
22
|
+
`api.honeybook.com`. So this skill is **hybrid**: `fpx` captures the session
|
|
23
|
+
**once** (per vendor), then plain `curl` does every read from then on.
|
|
24
|
+
|
|
25
|
+
This mirrors `src/auth.ts` (`captureSessionViaFetchproxy`) and `src/client.ts`
|
|
26
|
+
(`HoneyBookClient.request`) in `honeybook-mcp` — same headers, same base URL,
|
|
27
|
+
same retry rules.
|
|
28
|
+
|
|
29
|
+
## Multi-domain scope
|
|
30
|
+
|
|
31
|
+
Two apexes are declared on one profile:
|
|
32
|
+
- `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`.
|
|
36
|
+
|
|
37
|
+
## One-time setup
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
npm install -g @fetchproxy/cli # provides `fpx`
|
|
41
|
+
fpx profile add honeybook --domain honeybook.com --domain hbportal.co
|
|
42
|
+
fpx profile declare honeybook \
|
|
43
|
+
--local-storage jStorage \
|
|
44
|
+
--capture-header 'hb-api-fingerprint@api.honeybook.com/api/v2/*'
|
|
45
|
+
fpx pair -p honeybook # prints a pair code → approve in Transporter
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Requirements: the **Transporter** browser extension installed, its Chrome
|
|
49
|
+
**Site access** allowing both `honeybook.com` and `hbportal.co`, and a vendor
|
|
50
|
+
magic-link URL already open (signed in) in that browser. Pairing persists —
|
|
51
|
+
after the first approval every later `fpx` call reuses it.
|
|
52
|
+
|
|
53
|
+
## Capture a vendor session (once per vendor, and again when it expires)
|
|
54
|
+
|
|
55
|
+
1. Click the vendor's HoneyBook magic-link email in the browser with
|
|
56
|
+
Transporter installed. This signs you into `<vendor>.hbportal.co`.
|
|
57
|
+
2. **While that tab is open**, run:
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
fpx session -p honeybook --storage-domain hbportal.co > /tmp/hb-session.json
|
|
61
|
+
```
|
|
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.
|
|
68
|
+
|
|
69
|
+
3. Extract the fields `client.ts` needs (`jStorage` is one raw localStorage
|
|
70
|
+
value packing several keys as JSON — parse it with `fromjson`):
|
|
71
|
+
|
|
72
|
+
```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)
|
|
77
|
+
PORTAL_ORIGIN='https://<vendor>.hbportal.co' # the magic-link URL's origin
|
|
78
|
+
```
|
|
79
|
+
|
|
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.
|
|
83
|
+
|
|
84
|
+
With more than one vendor tab open at once, disambiguate with
|
|
85
|
+
`--storage-subdomain <vendor>` (e.g. `--storage-subdomain acme`).
|
|
86
|
+
|
|
87
|
+
4. Get the current API version (`client.ts`'s `fetchApiVersion` parses this
|
|
88
|
+
same endpoint):
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
API_VERSION=$(curl -s 'https://api.honeybook.com/api/gon?callback=parseGon' \
|
|
92
|
+
| grep -oE '"api_version":[[:space:]]*[0-9]+' | grep -oE '[0-9]+$')
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Core call pattern
|
|
96
|
+
|
|
97
|
+
Every real request carries the same eight headers
|
|
98
|
+
(`client.ts`'s `HoneyBookClient.request`):
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
curl -s "https://api.honeybook.com/api/v2/users/$USER_ID/workspace_files" \
|
|
102
|
+
-H 'accept: application/json, text/plain, */*' \
|
|
103
|
+
-H "hb-api-auth-token: $AUTH_TOKEN" \
|
|
104
|
+
-H "hb-api-user-id: $USER_ID" \
|
|
105
|
+
-H "hb-trusted-device: $TRUSTED_DEVICE" \
|
|
106
|
+
-H "hb-api-client-version: $API_VERSION" \
|
|
107
|
+
-H "hb-api-fingerprint: $FINGERPRINT" \
|
|
108
|
+
-H "hb-api-duplicate-calls-prevention-uuid: $(uuidgen)" \
|
|
109
|
+
-H 'hb-admin-login: false' \
|
|
110
|
+
| jq '.data'
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`hb-api-duplicate-calls-prevention-uuid` must be a **fresh random UUID on
|
|
114
|
+
every request** — the MCP mints one with `crypto.randomUUID()` per call, not
|
|
115
|
+
once per session. Reusing a value risks HoneyBook treating a legitimate
|
|
116
|
+
repeat as a duplicate.
|
|
117
|
+
|
|
118
|
+
Ready-to-run commands for all four read endpoints are in
|
|
119
|
+
`references/requests.md`.
|
|
120
|
+
|
|
121
|
+
## The rules that matter
|
|
122
|
+
|
|
123
|
+
- **401 → session expired.** Re-run the capture (magic link tab must still
|
|
124
|
+
be open and signed in).
|
|
125
|
+
- **429 → rate limited.** `client.ts` waits 2s and retries once; do the same
|
|
126
|
+
before giving up.
|
|
127
|
+
- **Body contains `"HBWrongAPIVersionError"` → stale `hb-api-client-version`.**
|
|
128
|
+
The error body itself carries the correct value at
|
|
129
|
+
`.error_data.server_api_version` — read that (or re-run the `/api/gon`
|
|
130
|
+
fetch above) and retry the SAME request with the fresh version.
|
|
131
|
+
- **`sign_contract` / `pay_invoice` are not real API calls.** `honeybook-mcp`
|
|
132
|
+
can't replay HoneyBook's browser-side signing/SCA flow, so those tools just
|
|
133
|
+
return a deep link — `$PORTAL_ORIGIN/app/workspace_file/<file_id>/agreement`
|
|
134
|
+
(sign) or `/invoice` (pay) — for the user to open themselves. There's no
|
|
135
|
+
POST body to transcribe for either; don't invent one.
|
|
136
|
+
|
|
137
|
+
## Output / exit-code contract
|
|
138
|
+
|
|
139
|
+
- `fpx session`/`fpx pair`/`fpx health` are bridge round-trips: exit `0` on a
|
|
140
|
+
successful bridge read regardless of upstream status, `1` on a usage error
|
|
141
|
+
(bad flag, undeclared scope), `2` if the bridge/extension is unreachable or
|
|
142
|
+
pairing is still pending. There's no bot-wall (`3`)/upstream-HTTP (`4`)
|
|
143
|
+
exit code on these — HoneyBook's own API isn't bridge-walled.
|
|
144
|
+
- The actual reads go through plain `curl` afterward — check the HTTP status
|
|
145
|
+
and the `HBWrongAPIVersionError` body text yourself, as above.
|
|
146
|
+
|
|
147
|
+
## Notes
|
|
148
|
+
|
|
149
|
+
- Session data (`AUTH_TOKEN`, `TRUSTED_DEVICE`, `FINGERPRINT`) is opaque and
|
|
150
|
+
long-lived server-side (no client-visible JWT expiry) — keep it in shell
|
|
151
|
+
variables, not a world-readable file, if you must persist it at all.
|
|
152
|
+
- This project is developed and maintained by AI (Claude).
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# HoneyBook requests for fpx + curl
|
|
2
|
+
|
|
3
|
+
Ready-to-run commands for the four live read endpoints `honeybook-mcp`
|
|
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
|
|
6
|
+
`HoneyBookClient.request` — see `../SKILL.md` for how to capture
|
|
7
|
+
`$AUTH_TOKEN`/`$USER_ID`/`$TRUSTED_DEVICE`/`$FINGERPRINT`/`$API_VERSION`
|
|
8
|
+
first.
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
hb_get() { # $1 = path (e.g. /api/v2/users/$USER_ID/workspace_files)
|
|
12
|
+
curl -s "https://api.honeybook.com$1" \
|
|
13
|
+
-H 'accept: application/json, text/plain, */*' \
|
|
14
|
+
-H "hb-api-auth-token: $AUTH_TOKEN" \
|
|
15
|
+
-H "hb-api-user-id: $USER_ID" \
|
|
16
|
+
-H "hb-trusted-device: $TRUSTED_DEVICE" \
|
|
17
|
+
-H "hb-api-client-version: $API_VERSION" \
|
|
18
|
+
-H "hb-api-fingerprint: $FINGERPRINT" \
|
|
19
|
+
-H "hb-api-duplicate-calls-prevention-uuid: $(uuidgen)" \
|
|
20
|
+
-H 'hb-admin-login: false'
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`sign_contract`/`pay_invoice` are **not** included below — the MCP itself
|
|
25
|
+
doesn't call a signing/payment API; it returns a deep link
|
|
26
|
+
(`$PORTAL_ORIGIN/app/workspace_file/<file_id>/agreement` or `/invoice`) for
|
|
27
|
+
the user to open in their browser. There's no request shape to transcribe.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 1. List a vendor's shared files
|
|
32
|
+
|
|
33
|
+
`list_workspace_files` (`src/tools/workspace_files.ts`):
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
hb_get "/api/v2/users/$USER_ID/workspace_files" > /tmp/hb-files.json
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Response envelope (`HBListEnvelope<T>`, `src/types.ts`):
|
|
40
|
+
`{ data: [...], cur_page, last_page, last_id?, total_count? }`.
|
|
41
|
+
**Pagination is not wired up** in the MCP either — if `last_page` is
|
|
42
|
+
`false`, more results exist on later pages that neither the MCP nor this
|
|
43
|
+
skill fetches.
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
# Filter to a file_type client-side (agreement | invoice | brochure | proposal)
|
|
47
|
+
jq '[.data[] | select(.file_type == "agreement")]' /tmp/hb-files.json
|
|
48
|
+
|
|
49
|
+
# Compact listing: id, type, title, accepted/paid flags
|
|
50
|
+
jq -r '.data[] | [.["_id"], .file_type, .file_title, (.is_file_accepted|tostring), (.has_pending_payment|tostring)] | @tsv' /tmp/hb-files.json
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 2. Get one file's detail
|
|
54
|
+
|
|
55
|
+
`get_workspace_file` (`src/tools/workspace_files.ts`):
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
hb_get "/api/v2/workspace_files/$FILE_ID" > /tmp/hb-file.json
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The raw response is large on proposal-class files (a real one hit ~1.3 MB,
|
|
62
|
+
mostly vendor-internal fields the MCP prunes off `company`:
|
|
63
|
+
`vendor_emails`, `workflow_automation_infos`, `brochure_templates`,
|
|
64
|
+
`questionnaires`, `lead_sources`, `proposals`, `agreements`, `invoices`,
|
|
65
|
+
`vendor_packages`, `contact_forms`, `stripe_persons`,
|
|
66
|
+
`user_pipeline_stages`, `project_types`, `company_assets`). Project what you
|
|
67
|
+
need instead of dumping the whole body:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
# Summary-equivalent: identity, status, vendor, event, pricing totals, payments
|
|
71
|
+
jq '{
|
|
72
|
+
id: .["_id"], title: .file_title, type: .file_type,
|
|
73
|
+
status: .status_name, accepted: .is_file_accepted,
|
|
74
|
+
vendor: .company.company_name,
|
|
75
|
+
event: (.event | {date: .event_date, type, couple_names}),
|
|
76
|
+
total_price: .vendor_proposal.total_price,
|
|
77
|
+
payments: (.payments_container.payments // [] | map({due_date, amount, is_paid}))
|
|
78
|
+
}' /tmp/hb-file.json
|
|
79
|
+
|
|
80
|
+
# Full contract text + signatures (the "agreement" section)
|
|
81
|
+
jq '.agreement' /tmp/hb-file.json
|
|
82
|
+
|
|
83
|
+
# Full line items (the "pricing" section)
|
|
84
|
+
jq '.vendor_proposal' /tmp/hb-file.json
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## 3. Get a workspace (vendor project)
|
|
88
|
+
|
|
89
|
+
`get_workspace` (`src/tools/workspaces.ts`) — `workspace_id` is
|
|
90
|
+
`.workspace._id` on any workspace_file from endpoint 1 or 2:
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
hb_get "/api/v2/workspaces/$WORKSPACE_ID" \
|
|
94
|
+
| jq '{id: .["_id"], has_sent_files, has_signed_files, has_paid_payments}'
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## 4. List saved payment methods
|
|
98
|
+
|
|
99
|
+
`list_payment_methods` (`src/tools/payment_methods.ts`):
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
hb_get "/api/v2/users/$USER_ID/payment_methods" | jq '.'
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Empty array (`[]`) if the client has no payment method saved with this
|
|
106
|
+
vendor — not an error.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Error shapes to check on every response
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
# Wrong API version — re-derive it from the error body itself (no /api/gon round-trip needed)
|
|
114
|
+
jq -r 'select(type=="object") | .error_data.server_api_version // empty' /tmp/hb-resp.json
|
|
115
|
+
|
|
116
|
+
# HTTP status: 401 = session expired (re-capture), 429 = rate limited (wait 2s, retry once)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
A non-2xx HTTP status with a body matching `HBWrongAPIVersionError` means
|
|
120
|
+
retry the *same* request with `hb-api-client-version` set to
|
|
121
|
+
`.error_data.server_api_version` from the body (or a fresh `/api/gon` fetch,
|
|
122
|
+
per `../SKILL.md`).
|