honeybook-mcp 0.3.5 → 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 +134 -10
- package/dist/index.js +1 -1
- package/package.json +4 -4
- 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
|
@@ -34710,8 +34710,12 @@ var API_KEY_RE = new RegExp([
|
|
|
34710
34710
|
// webhook signing secret (Stripe-style)
|
|
34711
34711
|
].map((p) => `\\b${p}`).join("|"), "g");
|
|
34712
34712
|
var QUERY_SECRET_RE = /([?&](?:access_token|refresh_token|client_secret|api_?key|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
|
|
34713
|
+
var AWS_SIGV4_RE = /([?&]X-Amz-(?:Signature|Security-Token|Credential)=)[^&#\s"'<>`]+/gi;
|
|
34714
|
+
var JSON_SECRET_KEYS = "access_token|refresh_token|client_secret|api_?key|password|passwd|secret|token";
|
|
34715
|
+
var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")[^"]*(")`, "gi");
|
|
34716
|
+
var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')[^']*(')`, "gi");
|
|
34713
34717
|
function redactSecrets(text) {
|
|
34714
|
-
return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(JWT_RE, "[REDACTED]");
|
|
34718
|
+
return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JWT_RE, "[REDACTED]");
|
|
34715
34719
|
}
|
|
34716
34720
|
function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX) {
|
|
34717
34721
|
const str = text === null || text === void 0 ? "" : String(text);
|
|
@@ -35067,6 +35071,7 @@ var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
|
35067
35071
|
"capture_request_header",
|
|
35068
35072
|
"capture_redirect",
|
|
35069
35073
|
"read_indexed_db",
|
|
35074
|
+
"read_dom",
|
|
35070
35075
|
"download"
|
|
35071
35076
|
]);
|
|
35072
35077
|
|
|
@@ -35368,6 +35373,44 @@ function assertIndexedDbScopesArray(value, label) {
|
|
|
35368
35373
|
}
|
|
35369
35374
|
}
|
|
35370
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
|
+
}
|
|
35371
35414
|
function validateFrame(raw) {
|
|
35372
35415
|
assertObject(raw, "frame");
|
|
35373
35416
|
const t = raw.type;
|
|
@@ -35443,6 +35486,9 @@ function validateHello(raw) {
|
|
|
35443
35486
|
if (raw.sessionStoragePointers !== void 0) {
|
|
35444
35487
|
assertStoragePointersArray(raw.sessionStoragePointers, "hello.sessionStoragePointers", raw.sessionStorageKeys);
|
|
35445
35488
|
}
|
|
35489
|
+
if (raw.domSelectors !== void 0) {
|
|
35490
|
+
assertDomSelectorsArray(raw.domSelectors, "hello.domSelectors");
|
|
35491
|
+
}
|
|
35446
35492
|
assertBase64(raw.identityX25519Pub, "hello.identityX25519Pub");
|
|
35447
35493
|
assertBase64(raw.identityEd25519Pub, "hello.identityEd25519Pub");
|
|
35448
35494
|
assertBase64(raw.sessionNonce, "hello.sessionNonce");
|
|
@@ -35677,6 +35723,21 @@ function validateInnerRequest(raw) {
|
|
|
35677
35723
|
}
|
|
35678
35724
|
return raw;
|
|
35679
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
|
+
}
|
|
35680
35741
|
if (raw.op === "download") {
|
|
35681
35742
|
assertObject(raw.init, "inner.init");
|
|
35682
35743
|
if (raw.init.url === void 0) {
|
|
@@ -35702,7 +35763,7 @@ function validateInnerRequest(raw) {
|
|
|
35702
35763
|
}
|
|
35703
35764
|
return raw;
|
|
35704
35765
|
}
|
|
35705
|
-
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)}`);
|
|
35706
35767
|
}
|
|
35707
35768
|
function assertNonEmptyKeyArray(value, label) {
|
|
35708
35769
|
if (!Array.isArray(value)) {
|
|
@@ -35787,6 +35848,13 @@ function validateInnerResponse(raw) {
|
|
|
35787
35848
|
assertObject(raw.values, "inner.values");
|
|
35788
35849
|
return raw;
|
|
35789
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
|
+
}
|
|
35790
35858
|
if (op === "download") {
|
|
35791
35859
|
assertObject(raw.value, "inner.value");
|
|
35792
35860
|
assertString(raw.value.path, "inner.value.path");
|
|
@@ -36126,6 +36194,13 @@ async function buildServerHello(opts) {
|
|
|
36126
36194
|
jsonPointer: d.jsonPointer
|
|
36127
36195
|
}));
|
|
36128
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
|
+
}
|
|
36129
36204
|
return hello;
|
|
36130
36205
|
}
|
|
36131
36206
|
|
|
@@ -36215,7 +36290,8 @@ async function startHost(opts) {
|
|
|
36215
36290
|
captureHeaders: opts.ownCaptureHeaders,
|
|
36216
36291
|
indexedDbScopes: opts.ownIndexedDbScopes,
|
|
36217
36292
|
localStoragePointers: opts.ownLocalStoragePointers,
|
|
36218
|
-
sessionStoragePointers: opts.ownSessionStoragePointers
|
|
36293
|
+
sessionStoragePointers: opts.ownSessionStoragePointers,
|
|
36294
|
+
domSelectors: opts.ownDomSelectors
|
|
36219
36295
|
});
|
|
36220
36296
|
const ownSessionNonce = fromB64(ownHello.sessionNonce);
|
|
36221
36297
|
let extensionWs = null;
|
|
@@ -36450,6 +36526,7 @@ async function startPeer(opts) {
|
|
|
36450
36526
|
sessionStorageKeys: opts.sessionStorageKeys,
|
|
36451
36527
|
captureHeaders: opts.captureHeaders,
|
|
36452
36528
|
indexedDbScopes: opts.indexedDbScopes,
|
|
36529
|
+
domSelectors: opts.domSelectors,
|
|
36453
36530
|
localStoragePointers: opts.localStoragePointers,
|
|
36454
36531
|
sessionStoragePointers: opts.sessionStoragePointers
|
|
36455
36532
|
});
|
|
@@ -36857,6 +36934,11 @@ var FetchproxyServer = class {
|
|
|
36857
36934
|
key: d.key,
|
|
36858
36935
|
jsonPointer: d.jsonPointer
|
|
36859
36936
|
})),
|
|
36937
|
+
domSelectors: (opts.domSelectors ?? []).map((d) => ({
|
|
36938
|
+
name: d.name,
|
|
36939
|
+
selector: d.selector,
|
|
36940
|
+
...d.attribute !== void 0 ? { attribute: d.attribute } : {}
|
|
36941
|
+
})),
|
|
36860
36942
|
// 0.8.0+: timer + lazy-revive default to ON. Every realty MCP
|
|
36861
36943
|
// adapter was about to set these to the same numbers anyway; the
|
|
36862
36944
|
// back-door is `0` (explicit opt-out) if a caller genuinely wants
|
|
@@ -36977,6 +37059,7 @@ var FetchproxyServer = class {
|
|
|
36977
37059
|
ownIndexedDbScopes: this.opts.indexedDbScopes,
|
|
36978
37060
|
ownLocalStoragePointers: this.opts.localStoragePointers,
|
|
36979
37061
|
ownSessionStoragePointers: this.opts.sessionStoragePointers,
|
|
37062
|
+
ownDomSelectors: this.opts.domSelectors,
|
|
36980
37063
|
onPairCode: this.opts.onPairCode
|
|
36981
37064
|
});
|
|
36982
37065
|
this.hostHandle.onOwnInner((inner) => this.onInner(inner));
|
|
@@ -37004,7 +37087,8 @@ var FetchproxyServer = class {
|
|
|
37004
37087
|
captureHeaders: this.opts.captureHeaders,
|
|
37005
37088
|
indexedDbScopes: this.opts.indexedDbScopes,
|
|
37006
37089
|
localStoragePointers: this.opts.localStoragePointers,
|
|
37007
|
-
sessionStoragePointers: this.opts.sessionStoragePointers
|
|
37090
|
+
sessionStoragePointers: this.opts.sessionStoragePointers,
|
|
37091
|
+
domSelectors: this.opts.domSelectors
|
|
37008
37092
|
});
|
|
37009
37093
|
this.peerHandle.onInner((inner) => this.onInner(inner));
|
|
37010
37094
|
this.peerHandle.onRenegotiate(() => {
|
|
@@ -37979,6 +38063,46 @@ var FetchproxyServer = class {
|
|
|
37979
38063
|
await this.sendInnerFrame(inner);
|
|
37980
38064
|
return this._withVerbTimeout(pending, this.pendingIdb, id, origin);
|
|
37981
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
|
+
}
|
|
37982
38106
|
assertScopeSubset(requested, declared, label) {
|
|
37983
38107
|
const undeclared = undeclaredKeys(requested, declared);
|
|
37984
38108
|
if (undeclared.length > 0) {
|
|
@@ -38050,7 +38174,7 @@ var FetchproxyServer = class {
|
|
|
38050
38174
|
if (storageCb) {
|
|
38051
38175
|
this.pendingStorage.delete(inner.id);
|
|
38052
38176
|
if (inner.ok) {
|
|
38053
|
-
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) {
|
|
38054
38178
|
storageCb.resolve({ ...inner.values });
|
|
38055
38179
|
} else {
|
|
38056
38180
|
storageCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on storage awaiter`));
|
|
@@ -38408,7 +38532,7 @@ function bridgeErrorInfo(err) {
|
|
|
38408
38532
|
// package.json
|
|
38409
38533
|
var package_default = {
|
|
38410
38534
|
name: "honeybook-mcp",
|
|
38411
|
-
version: "0.
|
|
38535
|
+
version: "0.4.0",
|
|
38412
38536
|
mcpName: "io.github.chrischall/honeybook-mcp",
|
|
38413
38537
|
description: "HoneyBook client-portal MCP server for Claude",
|
|
38414
38538
|
author: "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -38448,7 +38572,7 @@ var package_default = {
|
|
|
38448
38572
|
"test:watch": "vitest"
|
|
38449
38573
|
},
|
|
38450
38574
|
dependencies: {
|
|
38451
|
-
"@chrischall/mcp-utils": "^0.
|
|
38575
|
+
"@chrischall/mcp-utils": "^0.13.0",
|
|
38452
38576
|
"@fetchproxy/bootstrap": "^1.0.0",
|
|
38453
38577
|
"@fetchproxy/server": "^1.0.0",
|
|
38454
38578
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
@@ -38456,10 +38580,10 @@ var package_default = {
|
|
|
38456
38580
|
zod: "^4.4.3"
|
|
38457
38581
|
},
|
|
38458
38582
|
devDependencies: {
|
|
38459
|
-
"@types/node": "^
|
|
38583
|
+
"@types/node": "^26.0.0",
|
|
38460
38584
|
"@vitest/coverage-v8": "^4.1.7",
|
|
38461
38585
|
esbuild: "^0.28.0",
|
|
38462
|
-
typescript: "^
|
|
38586
|
+
typescript: "^7.0.2",
|
|
38463
38587
|
vitest: "^4.1.7"
|
|
38464
38588
|
}
|
|
38465
38589
|
};
|
|
@@ -39045,7 +39169,7 @@ var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
|
39045
39169
|
await loadDotenvSafely({ path: join3(__dirname, "..", ".env"), override: false });
|
|
39046
39170
|
await runMcp({
|
|
39047
39171
|
name: "honeybook-mcp",
|
|
39048
|
-
version: "0.
|
|
39172
|
+
version: "0.4.0",
|
|
39049
39173
|
// x-release-please-version
|
|
39050
39174
|
tools: [
|
|
39051
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",
|
|
@@ -48,10 +48,10 @@
|
|
|
48
48
|
"zod": "^4.4.3"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@types/node": "^
|
|
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`).
|