honeybook-mcp 0.1.12 → 0.2.4
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 +22 -0
- package/dist/auth.js +14 -0
- package/dist/bundle.js +403 -86
- package/dist/index.js +1 -1
- package/package.json +9 -9
- package/server.json +2 -2
|
@@ -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.2.4"
|
|
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.2.4",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Chall"
|
|
21
21
|
},
|
package/README.md
CHANGED
|
@@ -22,6 +22,28 @@ Ask Claude things like:
|
|
|
22
22
|
- Magic-link emails from the wedding vendors that use HoneyBook
|
|
23
23
|
- [fetchproxy 0.3.0 browser extension](https://github.com/chrischall/fetchproxy) — installed in Chrome (Web Store) or Safari (.dmg). Used by `use_magic_link` to snapshot the session out of your signed-in vendor portal tab.
|
|
24
24
|
|
|
25
|
+
## Acknowledgement of Terms
|
|
26
|
+
|
|
27
|
+
By using this MCP server, you acknowledge and agree to the following:
|
|
28
|
+
|
|
29
|
+
**1. This server accesses your own HoneyBook workspace.** Every request is dispatched through your own signed-in browser session via the fetchproxy extension. It does not — and cannot — access anyone else's workspace.
|
|
30
|
+
|
|
31
|
+
**2. [HoneyBook's Terms of Service](https://www.honeybook.com/legal/terms-of-service) govern your use of this server**, just as they govern your direct use of honeybook.com. The clauses most relevant here:
|
|
32
|
+
|
|
33
|
+
> In connection with your use of the Service you will not engage in or use any data mining, robots, scraping or similar data gathering or extraction methods.
|
|
34
|
+
|
|
35
|
+
And: users may not "modify, copy, frame, scrape, rent, lease, loan, sell, distribute or create derivative works based on the Service or the Service Content."
|
|
36
|
+
|
|
37
|
+
You are agreeing to those terms — read by the maintainer 2026-05-23 — every time you invoke a tool in this server. HoneyBook's ToS broadly prohibits data mining and scraping; this MCP is an unofficial automation tool and HoneyBook has not granted it an exception.
|
|
38
|
+
|
|
39
|
+
**3. Personal, single-business use only.** This project is not affiliated with, endorsed by, sponsored by, or in partnership with HoneyBook, Inc. It is a personal automation tool that an individual HoneyBook account holder can use to drive their own workspace. Do not use it on behalf of another business, do not bulk-extract HoneyBook's directory or template content, and do not create a derivative SaaS product on top of it.
|
|
40
|
+
|
|
41
|
+
**4. Stability is not guaranteed.** This server calls internal HoneyBook endpoints that HoneyBook may change without notice. It may break.
|
|
42
|
+
|
|
43
|
+
**5. You accept full responsibility** for any consequences of using this server in connection with your HoneyBook account — rate limiting, account warnings, suspension, or any enforcement action HoneyBook takes. HoneyBook can detect automated traffic and may block your IP or workspace. If HoneyBook objects to your use, stop using this server.
|
|
44
|
+
|
|
45
|
+
This section is the maintainer's good-faith summary of the terms — it is not legal advice and does not modify or supersede HoneyBook's actual ToS.
|
|
46
|
+
|
|
25
47
|
## Installation
|
|
26
48
|
|
|
27
49
|
### From source
|
package/dist/auth.js
CHANGED
|
@@ -111,7 +111,21 @@ export async function captureSessionViaFetchproxy(opts) {
|
|
|
111
111
|
// HoneyBook serves both the main app (honeybook.com) and per-vendor
|
|
112
112
|
// portal subdomains (*.hbportal.co). The extension matches on suffix,
|
|
113
113
|
// so listing both apexes covers any vendor.
|
|
114
|
+
//
|
|
115
|
+
// Both are needed:
|
|
116
|
+
// - `hbportal.co` is where the vendor magic-link tab lives, so
|
|
117
|
+
// `jStorage` reads target it (via `storageDomain` below).
|
|
118
|
+
// - `honeybook.com` is required for the `hb-api-fingerprint`
|
|
119
|
+
// capture, because that header rides outgoing requests to
|
|
120
|
+
// `https://api.honeybook.com/api/v2/*` and the extension gates
|
|
121
|
+
// every captureHeader urlPattern host against declared domains.
|
|
122
|
+
//
|
|
123
|
+
// 0.4.1+ requires multi-domain MCPs to pick a `storageDomain` so
|
|
124
|
+
// the cookie / localStorage / sessionStorage / indexedDb reads
|
|
125
|
+
// know which tab to target. captureRequestHeader is independently
|
|
126
|
+
// routed by urlPattern.
|
|
114
127
|
domains: ['honeybook.com', 'hbportal.co'],
|
|
128
|
+
storageDomain: 'hbportal.co',
|
|
115
129
|
declare: {
|
|
116
130
|
cookies: [],
|
|
117
131
|
// We don't need the full `jStorage` blob (it's ~8KB of mostly
|
package/dist/bundle.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createRequire as __createRequire } from "module"; const require = __createRequire(import.meta.url);
|
|
1
2
|
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -7570,6 +7571,10 @@ var require_receiver = __commonJS({
|
|
|
7570
7571
|
* extensions
|
|
7571
7572
|
* @param {Boolean} [options.isServer=false] Specifies whether to operate in
|
|
7572
7573
|
* client or server mode
|
|
7574
|
+
* @param {Number} [options.maxBufferedChunks=0] The maximum number of
|
|
7575
|
+
* buffered data chunks
|
|
7576
|
+
* @param {Number} [options.maxFragments=0] The maximum number of message
|
|
7577
|
+
* fragments
|
|
7573
7578
|
* @param {Number} [options.maxPayload=0] The maximum allowed message length
|
|
7574
7579
|
* @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
|
|
7575
7580
|
* not to skip UTF-8 validation for text and close messages
|
|
@@ -7580,6 +7585,8 @@ var require_receiver = __commonJS({
|
|
|
7580
7585
|
this._binaryType = options.binaryType || BINARY_TYPES[0];
|
|
7581
7586
|
this._extensions = options.extensions || {};
|
|
7582
7587
|
this._isServer = !!options.isServer;
|
|
7588
|
+
this._maxBufferedChunks = options.maxBufferedChunks | 0;
|
|
7589
|
+
this._maxFragments = options.maxFragments | 0;
|
|
7583
7590
|
this._maxPayload = options.maxPayload | 0;
|
|
7584
7591
|
this._skipUTF8Validation = !!options.skipUTF8Validation;
|
|
7585
7592
|
this[kWebSocket] = void 0;
|
|
@@ -7609,6 +7616,18 @@ var require_receiver = __commonJS({
|
|
|
7609
7616
|
*/
|
|
7610
7617
|
_write(chunk, encoding, cb) {
|
|
7611
7618
|
if (this._opcode === 8 && this._state == GET_INFO) return cb();
|
|
7619
|
+
if (this._maxBufferedChunks > 0 && this._buffers.length >= this._maxBufferedChunks) {
|
|
7620
|
+
cb(
|
|
7621
|
+
this.createError(
|
|
7622
|
+
RangeError,
|
|
7623
|
+
"Too many buffered chunks",
|
|
7624
|
+
false,
|
|
7625
|
+
1008,
|
|
7626
|
+
"WS_ERR_TOO_MANY_BUFFERED_PARTS"
|
|
7627
|
+
)
|
|
7628
|
+
);
|
|
7629
|
+
return;
|
|
7630
|
+
}
|
|
7612
7631
|
this._bufferedBytes += chunk.length;
|
|
7613
7632
|
this._buffers.push(chunk);
|
|
7614
7633
|
this.startLoop(cb);
|
|
@@ -7938,6 +7957,17 @@ var require_receiver = __commonJS({
|
|
|
7938
7957
|
return;
|
|
7939
7958
|
}
|
|
7940
7959
|
if (data.length) {
|
|
7960
|
+
if (this._maxFragments > 0 && this._fragments.length >= this._maxFragments) {
|
|
7961
|
+
const error51 = this.createError(
|
|
7962
|
+
RangeError,
|
|
7963
|
+
"Too many message fragments",
|
|
7964
|
+
false,
|
|
7965
|
+
1008,
|
|
7966
|
+
"WS_ERR_TOO_MANY_BUFFERED_PARTS"
|
|
7967
|
+
);
|
|
7968
|
+
cb(error51);
|
|
7969
|
+
return;
|
|
7970
|
+
}
|
|
7941
7971
|
this._messageLength = this._totalPayloadLength;
|
|
7942
7972
|
this._fragments.push(data);
|
|
7943
7973
|
}
|
|
@@ -7967,6 +7997,17 @@ var require_receiver = __commonJS({
|
|
|
7967
7997
|
cb(error51);
|
|
7968
7998
|
return;
|
|
7969
7999
|
}
|
|
8000
|
+
if (this._maxFragments > 0 && this._fragments.length >= this._maxFragments) {
|
|
8001
|
+
const error51 = this.createError(
|
|
8002
|
+
RangeError,
|
|
8003
|
+
"Too many message fragments",
|
|
8004
|
+
false,
|
|
8005
|
+
1008,
|
|
8006
|
+
"WS_ERR_TOO_MANY_BUFFERED_PARTS"
|
|
8007
|
+
);
|
|
8008
|
+
cb(error51);
|
|
8009
|
+
return;
|
|
8010
|
+
}
|
|
7970
8011
|
this._fragments.push(buf);
|
|
7971
8012
|
}
|
|
7972
8013
|
this.dataMessage(cb);
|
|
@@ -9173,6 +9214,10 @@ var require_websocket = __commonJS({
|
|
|
9173
9214
|
* multiple times in the same tick
|
|
9174
9215
|
* @param {Function} [options.generateMask] The function used to generate the
|
|
9175
9216
|
* masking key
|
|
9217
|
+
* @param {Number} [options.maxBufferedChunks=0] The maximum number of
|
|
9218
|
+
* buffered data chunks
|
|
9219
|
+
* @param {Number} [options.maxFragments=0] The maximum number of message
|
|
9220
|
+
* fragments
|
|
9176
9221
|
* @param {Number} [options.maxPayload=0] The maximum allowed message size
|
|
9177
9222
|
* @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
|
|
9178
9223
|
* not to skip UTF-8 validation for text and close messages
|
|
@@ -9184,6 +9229,8 @@ var require_websocket = __commonJS({
|
|
|
9184
9229
|
binaryType: this.binaryType,
|
|
9185
9230
|
extensions: this._extensions,
|
|
9186
9231
|
isServer: this._isServer,
|
|
9232
|
+
maxBufferedChunks: options.maxBufferedChunks,
|
|
9233
|
+
maxFragments: options.maxFragments,
|
|
9187
9234
|
maxPayload: options.maxPayload,
|
|
9188
9235
|
skipUTF8Validation: options.skipUTF8Validation
|
|
9189
9236
|
});
|
|
@@ -9483,6 +9530,8 @@ var require_websocket = __commonJS({
|
|
|
9483
9530
|
autoPong: true,
|
|
9484
9531
|
closeTimeout: CLOSE_TIMEOUT,
|
|
9485
9532
|
protocolVersion: protocolVersions[1],
|
|
9533
|
+
maxBufferedChunks: 1024 * 1024,
|
|
9534
|
+
maxFragments: 128 * 1024,
|
|
9486
9535
|
maxPayload: 100 * 1024 * 1024,
|
|
9487
9536
|
skipUTF8Validation: false,
|
|
9488
9537
|
perMessageDeflate: true,
|
|
@@ -9725,6 +9774,8 @@ var require_websocket = __commonJS({
|
|
|
9725
9774
|
websocket.setSocket(socket, head, {
|
|
9726
9775
|
allowSynchronousEvents: opts.allowSynchronousEvents,
|
|
9727
9776
|
generateMask: opts.generateMask,
|
|
9777
|
+
maxBufferedChunks: opts.maxBufferedChunks,
|
|
9778
|
+
maxFragments: opts.maxFragments,
|
|
9728
9779
|
maxPayload: opts.maxPayload,
|
|
9729
9780
|
skipUTF8Validation: opts.skipUTF8Validation
|
|
9730
9781
|
});
|
|
@@ -10067,6 +10118,10 @@ var require_websocket_server = __commonJS({
|
|
|
10067
10118
|
* called
|
|
10068
10119
|
* @param {Function} [options.handleProtocols] A hook to handle protocols
|
|
10069
10120
|
* @param {String} [options.host] The hostname where to bind the server
|
|
10121
|
+
* @param {Number} [options.maxBufferedChunks=1048576] The maximum number of
|
|
10122
|
+
* buffered data chunks
|
|
10123
|
+
* @param {Number} [options.maxFragments=131072] The maximum number of message
|
|
10124
|
+
* fragments
|
|
10070
10125
|
* @param {Number} [options.maxPayload=104857600] The maximum allowed message
|
|
10071
10126
|
* size
|
|
10072
10127
|
* @param {Boolean} [options.noServer=false] Enable no server mode
|
|
@@ -10088,6 +10143,8 @@ var require_websocket_server = __commonJS({
|
|
|
10088
10143
|
options = {
|
|
10089
10144
|
allowSynchronousEvents: true,
|
|
10090
10145
|
autoPong: true,
|
|
10146
|
+
maxBufferedChunks: 1024 * 1024,
|
|
10147
|
+
maxFragments: 128 * 1024,
|
|
10091
10148
|
maxPayload: 100 * 1024 * 1024,
|
|
10092
10149
|
skipUTF8Validation: false,
|
|
10093
10150
|
perMessageDeflate: false,
|
|
@@ -10367,6 +10424,8 @@ var require_websocket_server = __commonJS({
|
|
|
10367
10424
|
socket.removeListener("error", socketOnError);
|
|
10368
10425
|
ws.setSocket(socket, head, {
|
|
10369
10426
|
allowSynchronousEvents: this.options.allowSynchronousEvents,
|
|
10427
|
+
maxBufferedChunks: this.options.maxBufferedChunks,
|
|
10428
|
+
maxFragments: this.options.maxFragments,
|
|
10370
10429
|
maxPayload: this.options.maxPayload,
|
|
10371
10430
|
skipUTF8Validation: this.options.skipUTF8Validation
|
|
10372
10431
|
});
|
|
@@ -34700,6 +34759,7 @@ async function getActiveClient(origin) {
|
|
|
34700
34759
|
|
|
34701
34760
|
// node_modules/@fetchproxy/protocol/dist/frames.js
|
|
34702
34761
|
var PROTOCOL_VERSION = 2;
|
|
34762
|
+
var HKDF_SESSION_INFO = "fetchproxy/1.0.0/session";
|
|
34703
34763
|
var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
34704
34764
|
"fetch",
|
|
34705
34765
|
"read_cookies",
|
|
@@ -34774,7 +34834,7 @@ var ProtocolError = class extends Error {
|
|
|
34774
34834
|
var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
34775
34835
|
var BASE64_RE = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
34776
34836
|
var SCOPE_KEY_RE = /^[A-Za-z0-9_.\-]{1,256}$/;
|
|
34777
|
-
var SCOPE_KEY_GLOB_RE = /^[A-Za-z0-9_.\-]{1,
|
|
34837
|
+
var SCOPE_KEY_GLOB_RE = /^[A-Za-z0-9_.\-]{1,256}\*?$/;
|
|
34778
34838
|
var HEADER_NAME_RE = /^[A-Za-z0-9_\-]{1,128}$/;
|
|
34779
34839
|
var HOSTNAME_RE = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$/i;
|
|
34780
34840
|
function assertObject(x, label) {
|
|
@@ -34903,7 +34963,7 @@ function assertStoragePointersArray(value, label, declaredKeys) {
|
|
|
34903
34963
|
if (entry.jsonPointer === void 0) {
|
|
34904
34964
|
throw new ProtocolError(`${label}[${i}].jsonPointer: missing`);
|
|
34905
34965
|
}
|
|
34906
|
-
if (typeof entry.key !== "string" || !
|
|
34966
|
+
if (typeof entry.key !== "string" || !SCOPE_KEY_RE.test(entry.key)) {
|
|
34907
34967
|
throw new ProtocolError(`${label}[${i}].key: invalid key ${JSON.stringify(entry.key)}`);
|
|
34908
34968
|
}
|
|
34909
34969
|
if (typeof entry.jsonPointer !== "string" || !isValidJsonPointer(entry.jsonPointer)) {
|
|
@@ -35002,6 +35062,8 @@ function validateFrame(raw) {
|
|
|
35002
35062
|
return validateReady(raw);
|
|
35003
35063
|
if (t === "frame")
|
|
35004
35064
|
return validateEncrypted(raw);
|
|
35065
|
+
if (t === "pair-pending")
|
|
35066
|
+
return validatePairPending(raw);
|
|
35005
35067
|
throw new ProtocolError(`unknown frame type: ${String(t)}`);
|
|
35006
35068
|
}
|
|
35007
35069
|
function validateHello(raw) {
|
|
@@ -35103,6 +35165,17 @@ function validateEncrypted(raw) {
|
|
|
35103
35165
|
assertBase64(raw.ciphertext, "frame.ciphertext");
|
|
35104
35166
|
return raw;
|
|
35105
35167
|
}
|
|
35168
|
+
var PAIR_CODE_RE = /^\d{3}-\d{3}$/;
|
|
35169
|
+
function validatePairPending(raw) {
|
|
35170
|
+
assertString(raw.mcpId, "pair-pending.mcpId");
|
|
35171
|
+
if (!isValidMcpId(raw.mcpId))
|
|
35172
|
+
throw new ProtocolError("pair-pending.mcpId: invalid format");
|
|
35173
|
+
assertString(raw.pairCode, "pair-pending.pairCode");
|
|
35174
|
+
if (!PAIR_CODE_RE.test(raw.pairCode)) {
|
|
35175
|
+
throw new ProtocolError(`pair-pending.pairCode: must match XXX-XXX, got ${String(raw.pairCode)}`);
|
|
35176
|
+
}
|
|
35177
|
+
return { type: "pair-pending", mcpId: raw.mcpId, pairCode: raw.pairCode };
|
|
35178
|
+
}
|
|
35106
35179
|
function validateInnerFrame(raw) {
|
|
35107
35180
|
assertObject(raw, "inner");
|
|
35108
35181
|
const t = raw.type;
|
|
@@ -35685,15 +35758,22 @@ async function startHost(opts) {
|
|
|
35685
35758
|
let extensionWs = null;
|
|
35686
35759
|
const peers = /* @__PURE__ */ new Map();
|
|
35687
35760
|
const ownInnerListeners = [];
|
|
35761
|
+
const disconnectListeners = [];
|
|
35762
|
+
const pendingPairListeners = [];
|
|
35688
35763
|
let ownSession = null;
|
|
35764
|
+
let ownPendingPairCode = null;
|
|
35689
35765
|
let resolveOwnSession;
|
|
35690
35766
|
let rejectOwnSession;
|
|
35691
|
-
|
|
35692
|
-
|
|
35693
|
-
|
|
35694
|
-
|
|
35695
|
-
|
|
35696
|
-
|
|
35767
|
+
let ownSessionReady;
|
|
35768
|
+
function resetSessionPromise() {
|
|
35769
|
+
ownSessionReady = new Promise((resolve, reject) => {
|
|
35770
|
+
resolveOwnSession = resolve;
|
|
35771
|
+
rejectOwnSession = reject;
|
|
35772
|
+
});
|
|
35773
|
+
ownSessionReady.catch(() => {
|
|
35774
|
+
});
|
|
35775
|
+
}
|
|
35776
|
+
resetSessionPromise();
|
|
35697
35777
|
let extensionHello = null;
|
|
35698
35778
|
wss.on("connection", (ws) => {
|
|
35699
35779
|
let identified = null;
|
|
@@ -35762,11 +35842,12 @@ async function startHost(opts) {
|
|
|
35762
35842
|
}
|
|
35763
35843
|
const extPub = fromB64(frame.extensionSessionPub);
|
|
35764
35844
|
const shared = await ecdhX25519(opts.ownIdentity.x25519Priv, extPub);
|
|
35765
|
-
const key = await hkdfSha256(shared, ownSessionNonce, enc2.encode(
|
|
35766
|
-
if (
|
|
35767
|
-
|
|
35768
|
-
|
|
35769
|
-
|
|
35845
|
+
const key = await hkdfSha256(shared, ownSessionNonce, enc2.encode(HKDF_SESSION_INFO), 32);
|
|
35846
|
+
if (extensionWs !== ws)
|
|
35847
|
+
return;
|
|
35848
|
+
ownSession = new SessionState(key);
|
|
35849
|
+
ownPendingPairCode = null;
|
|
35850
|
+
resolveOwnSession(ownSession);
|
|
35770
35851
|
} else {
|
|
35771
35852
|
const slot = peers.get(frame.mcpId);
|
|
35772
35853
|
if (slot)
|
|
@@ -35793,6 +35874,16 @@ async function startHost(opts) {
|
|
|
35793
35874
|
extensionWs.send(JSON.stringify(frame));
|
|
35794
35875
|
}
|
|
35795
35876
|
}
|
|
35877
|
+
if (frame.type === "pair-pending" && identified === "extension") {
|
|
35878
|
+
if (frame.mcpId === opts.ownMcpId) {
|
|
35879
|
+
ownPendingPairCode = frame.pairCode;
|
|
35880
|
+
pendingPairListeners.forEach((cb) => cb(frame.pairCode));
|
|
35881
|
+
} else {
|
|
35882
|
+
const slot = peers.get(frame.mcpId);
|
|
35883
|
+
if (slot)
|
|
35884
|
+
slot.ws.send(JSON.stringify(frame));
|
|
35885
|
+
}
|
|
35886
|
+
}
|
|
35796
35887
|
} catch (e) {
|
|
35797
35888
|
console.error("[fetchproxy] host: message handler error:", e);
|
|
35798
35889
|
try {
|
|
@@ -35808,6 +35899,9 @@ async function startHost(opts) {
|
|
|
35808
35899
|
if (!ownSession) {
|
|
35809
35900
|
rejectOwnSession(new Error("extension disconnected before ready"));
|
|
35810
35901
|
}
|
|
35902
|
+
ownSession = null;
|
|
35903
|
+
resetSessionPromise();
|
|
35904
|
+
disconnectListeners.forEach((cb) => cb());
|
|
35811
35905
|
}
|
|
35812
35906
|
if (identified === "peer" && peerMcpId)
|
|
35813
35907
|
peers.delete(peerMcpId);
|
|
@@ -35832,7 +35926,14 @@ async function startHost(opts) {
|
|
|
35832
35926
|
},
|
|
35833
35927
|
onOwnInner: (cb) => {
|
|
35834
35928
|
ownInnerListeners.push(cb);
|
|
35835
|
-
}
|
|
35929
|
+
},
|
|
35930
|
+
onExtensionDisconnect: (cb) => {
|
|
35931
|
+
disconnectListeners.push(cb);
|
|
35932
|
+
},
|
|
35933
|
+
onPendingPair: (cb) => {
|
|
35934
|
+
pendingPairListeners.push(cb);
|
|
35935
|
+
},
|
|
35936
|
+
pendingPairCode: () => ownPendingPairCode
|
|
35836
35937
|
};
|
|
35837
35938
|
}
|
|
35838
35939
|
|
|
@@ -35862,36 +35963,57 @@ async function startPeer(opts) {
|
|
|
35862
35963
|
const sessionNonce = fromB64(hello.sessionNonce);
|
|
35863
35964
|
ws.send(JSON.stringify(hello));
|
|
35864
35965
|
const innerListeners = [];
|
|
35966
|
+
const renegotiateListeners = [];
|
|
35967
|
+
const pendingPairListeners = [];
|
|
35865
35968
|
let session = null;
|
|
35969
|
+
let pendingPairCode = null;
|
|
35970
|
+
let resolveFirstReady;
|
|
35971
|
+
let rejectFirstReady;
|
|
35866
35972
|
const sessionPromise = new Promise((resolve, reject) => {
|
|
35867
|
-
|
|
35868
|
-
|
|
35869
|
-
|
|
35870
|
-
|
|
35871
|
-
|
|
35872
|
-
|
|
35873
|
-
|
|
35874
|
-
|
|
35875
|
-
|
|
35876
|
-
|
|
35877
|
-
|
|
35973
|
+
resolveFirstReady = resolve;
|
|
35974
|
+
rejectFirstReady = reject;
|
|
35975
|
+
});
|
|
35976
|
+
const onMessage = async (data) => {
|
|
35977
|
+
try {
|
|
35978
|
+
const raw = JSON.parse(data.toString());
|
|
35979
|
+
const frame = validateFrame(raw);
|
|
35980
|
+
if (frame.type === "ready" && frame.mcpId === opts.mcpId) {
|
|
35981
|
+
const extPub = fromB64(frame.extensionSessionPub);
|
|
35982
|
+
const shared = await ecdhX25519(opts.identity.x25519Priv, extPub);
|
|
35983
|
+
const sessionKey = await hkdfSha256(shared, sessionNonce, enc3.encode(HKDF_SESSION_INFO), 32);
|
|
35984
|
+
const isRenegotiation = session !== null;
|
|
35985
|
+
session = new SessionState(sessionKey);
|
|
35986
|
+
pendingPairCode = null;
|
|
35987
|
+
if (isRenegotiation) {
|
|
35988
|
+
renegotiateListeners.forEach((cb) => cb());
|
|
35989
|
+
} else {
|
|
35990
|
+
resolveFirstReady(session);
|
|
35878
35991
|
}
|
|
35879
|
-
|
|
35880
|
-
|
|
35881
|
-
|
|
35882
|
-
|
|
35883
|
-
|
|
35992
|
+
return;
|
|
35993
|
+
}
|
|
35994
|
+
if (frame.type === "pair-pending" && frame.mcpId === opts.mcpId) {
|
|
35995
|
+
pendingPairCode = frame.pairCode;
|
|
35996
|
+
pendingPairListeners.forEach((cb) => cb(frame.pairCode));
|
|
35997
|
+
return;
|
|
35998
|
+
}
|
|
35999
|
+
if (frame.type === "frame" && frame.mcpId === opts.mcpId) {
|
|
36000
|
+
if (!session)
|
|
36001
|
+
return;
|
|
36002
|
+
if (!session.acceptInboundSeq(frame.seq))
|
|
36003
|
+
return;
|
|
36004
|
+
try {
|
|
35884
36005
|
const inner = await openEncryptedFrame(session.sessionKey, frame);
|
|
35885
36006
|
innerListeners.forEach((cb) => cb(inner));
|
|
36007
|
+
} catch {
|
|
35886
36008
|
}
|
|
35887
|
-
} catch (e) {
|
|
35888
|
-
reject(e instanceof Error ? e : new Error(String(e)));
|
|
35889
36009
|
}
|
|
35890
|
-
}
|
|
35891
|
-
|
|
35892
|
-
|
|
35893
|
-
|
|
35894
|
-
|
|
36010
|
+
} catch (e) {
|
|
36011
|
+
rejectFirstReady(e instanceof Error ? e : new Error(String(e)));
|
|
36012
|
+
}
|
|
36013
|
+
};
|
|
36014
|
+
ws.on("message", onMessage);
|
|
36015
|
+
ws.once("close", () => {
|
|
36016
|
+
rejectFirstReady(new Error("peer WS closed before ready"));
|
|
35895
36017
|
});
|
|
35896
36018
|
sessionPromise.catch(() => {
|
|
35897
36019
|
});
|
|
@@ -35899,13 +36021,21 @@ async function startPeer(opts) {
|
|
|
35899
36021
|
ws,
|
|
35900
36022
|
session: sessionPromise,
|
|
35901
36023
|
sendInner: async (inner) => {
|
|
35902
|
-
|
|
36024
|
+
await sessionPromise;
|
|
36025
|
+
const s = session;
|
|
35903
36026
|
const sealed = await sealInnerFrame(s.sessionKey, opts.mcpId, s.nextOutboundSeq(), inner);
|
|
35904
36027
|
ws.send(JSON.stringify(sealed));
|
|
35905
36028
|
},
|
|
35906
36029
|
onInner: (cb) => {
|
|
35907
36030
|
innerListeners.push(cb);
|
|
35908
36031
|
},
|
|
36032
|
+
onRenegotiate: (cb) => {
|
|
36033
|
+
renegotiateListeners.push(cb);
|
|
36034
|
+
},
|
|
36035
|
+
onPendingPair: (cb) => {
|
|
36036
|
+
pendingPairListeners.push(cb);
|
|
36037
|
+
},
|
|
36038
|
+
pendingPairCode: () => pendingPairCode,
|
|
35909
36039
|
close: () => ws.close()
|
|
35910
36040
|
};
|
|
35911
36041
|
return handle;
|
|
@@ -35962,6 +36092,32 @@ async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
|
|
|
35962
36092
|
return id;
|
|
35963
36093
|
}
|
|
35964
36094
|
|
|
36095
|
+
// node_modules/@fetchproxy/server/dist/error-kind.js
|
|
36096
|
+
function classifyFetchError(error51) {
|
|
36097
|
+
if (/Could not establish connection/i.test(error51) || /Receiving end does not exist/i.test(error51)) {
|
|
36098
|
+
return "content_script_unreachable";
|
|
36099
|
+
}
|
|
36100
|
+
if (/^tab fetch failed:/.test(error51)) {
|
|
36101
|
+
return "tab_fetch_failed";
|
|
36102
|
+
}
|
|
36103
|
+
if (/^fetch threw:/.test(error51)) {
|
|
36104
|
+
return "tab_fetch_failed";
|
|
36105
|
+
}
|
|
36106
|
+
if (/^no tab matching /.test(error51)) {
|
|
36107
|
+
return "no_tab";
|
|
36108
|
+
}
|
|
36109
|
+
if (/not in domains \[/.test(error51)) {
|
|
36110
|
+
return "domain_denied";
|
|
36111
|
+
}
|
|
36112
|
+
if (/^capability .+ not granted/.test(error51)) {
|
|
36113
|
+
return "capability_denied";
|
|
36114
|
+
}
|
|
36115
|
+
if (/^(request|response) body too large:/.test(error51)) {
|
|
36116
|
+
return "body_too_large";
|
|
36117
|
+
}
|
|
36118
|
+
return "other";
|
|
36119
|
+
}
|
|
36120
|
+
|
|
35965
36121
|
// node_modules/@fetchproxy/server/dist/ws-server.js
|
|
35966
36122
|
var FetchproxyProtocolError = class extends Error {
|
|
35967
36123
|
constructor(message) {
|
|
@@ -36004,7 +36160,11 @@ function assertUrlInDomains(field, url2, domains) {
|
|
|
36004
36160
|
}
|
|
36005
36161
|
var DEFAULT_JSON_OK_STATUSES = [200, 201, 202, 204];
|
|
36006
36162
|
var FetchproxyServer = class {
|
|
36007
|
-
/**
|
|
36163
|
+
/**
|
|
36164
|
+
* Bridge role. `null` until the first verb call (or an explicit
|
|
36165
|
+
* `connect()`) — `listen()` no longer triggers the role election
|
|
36166
|
+
* as of 0.5.3+. Reset to `null` on `close()`.
|
|
36167
|
+
*/
|
|
36008
36168
|
role = null;
|
|
36009
36169
|
opts;
|
|
36010
36170
|
hostHandle = null;
|
|
@@ -36026,6 +36186,12 @@ var FetchproxyServer = class {
|
|
|
36026
36186
|
pendingIdb = /* @__PURE__ */ new Map();
|
|
36027
36187
|
mcpId = null;
|
|
36028
36188
|
identity = null;
|
|
36189
|
+
// 0.5.3+: in-flight role-election / handle-start promise. Set the
|
|
36190
|
+
// first time a verb call runs `ensureConnected`, awaited by concurrent
|
|
36191
|
+
// callers, cleared once the connection is up. Single source of truth
|
|
36192
|
+
// for "we're connecting right now" so two parallel first-calls don't
|
|
36193
|
+
// race the port bind.
|
|
36194
|
+
connectingPromise = null;
|
|
36029
36195
|
constructor(opts) {
|
|
36030
36196
|
if (!Array.isArray(opts.domains) || opts.domains.length === 0) {
|
|
36031
36197
|
throw new Error("FetchproxyServer: opts.domains must be a non-empty array of hostnames");
|
|
@@ -36077,23 +36243,87 @@ var FetchproxyServer = class {
|
|
|
36077
36243
|
};
|
|
36078
36244
|
}
|
|
36079
36245
|
/**
|
|
36080
|
-
*
|
|
36081
|
-
* from disk (creating it on first call)
|
|
36082
|
-
*
|
|
36083
|
-
*
|
|
36084
|
-
* `
|
|
36085
|
-
*
|
|
36246
|
+
* Prepare the bridge for use. Loads the long-term identity keypair
|
|
36247
|
+
* from disk (creating it on first call) and computes this instance's
|
|
36248
|
+
* `mcpId`. Does NOT bind the bridge port or dial any WebSocket — the
|
|
36249
|
+
* connection is established lazily on the first verb call (see
|
|
36250
|
+
* `ensureConnected` / `getOrConnect`).
|
|
36251
|
+
*
|
|
36252
|
+
* Pre-0.5.3 behavior: `listen()` also did role election and started
|
|
36253
|
+
* the host/peer immediately, which meant every configured-but-unused
|
|
36254
|
+
* MCP claimed bridge resources at MCP-client boot. Several MCPs
|
|
36255
|
+
* starting in parallel under Claude Desktop also produced noisy
|
|
36256
|
+
* `ERR_CONNECTION_REFUSED` errors in the extension if it raced ahead
|
|
36257
|
+
* of the first MCP's port bind. Deferring keeps boot quiet and
|
|
36258
|
+
* leaves the port unowned until something actually needs it.
|
|
36259
|
+
*
|
|
36260
|
+
* Calling `listen()` twice without an intervening `close()` is a
|
|
36261
|
+
* no-op (the second call's identity load is idempotent).
|
|
36086
36262
|
*/
|
|
36087
36263
|
async listen() {
|
|
36088
|
-
|
|
36089
|
-
|
|
36264
|
+
if (!this.identity) {
|
|
36265
|
+
this.identity = await loadOrCreateIdentity(this.opts.serverName, this.opts.identityDir);
|
|
36266
|
+
}
|
|
36267
|
+
if (!this.mcpId) {
|
|
36268
|
+
this.mcpId = generateMcpId(this.opts.serverName, this.opts.version);
|
|
36269
|
+
}
|
|
36270
|
+
}
|
|
36271
|
+
/**
|
|
36272
|
+
* Force an eager bridge connection (role-election + host/peer handle
|
|
36273
|
+
* start + listener wiring) without waiting for the first verb call.
|
|
36274
|
+
* Useful for callers that want to surface the role / connection
|
|
36275
|
+
* outcome at boot, or for tests whose harness dials a mock extension
|
|
36276
|
+
* immediately after server construction. Production MCPs that just
|
|
36277
|
+
* answer tool calls should NOT call this — the lazy connect via
|
|
36278
|
+
* `ensureConnected` will do the right thing on first use, keeping
|
|
36279
|
+
* boot cheap and avoiding port-bind contention for MCPs that never
|
|
36280
|
+
* actually get invoked.
|
|
36281
|
+
*
|
|
36282
|
+
* Idempotent: a second call after the first has resolved is a no-op
|
|
36283
|
+
* (the existing handle is reused). Throws if `listen()` was never
|
|
36284
|
+
* called.
|
|
36285
|
+
*/
|
|
36286
|
+
async connect() {
|
|
36287
|
+
await this.ensureConnected();
|
|
36288
|
+
}
|
|
36289
|
+
/**
|
|
36290
|
+
* Establish the bridge connection (role-election + host/peer handle
|
|
36291
|
+
* start + listener wiring) the first time a verb is invoked.
|
|
36292
|
+
* Idempotent after the connection is up; concurrent first-callers
|
|
36293
|
+
* share the same in-flight promise so only one election happens.
|
|
36294
|
+
*
|
|
36295
|
+
* Throws if `listen()` was never called — the contract is that the
|
|
36296
|
+
* MCP author still must wire `transport.start()` at boot to load
|
|
36297
|
+
* identity / set mcpId, even though the WS doesn't open until a
|
|
36298
|
+
* verb runs.
|
|
36299
|
+
*/
|
|
36300
|
+
async ensureConnected() {
|
|
36301
|
+
if (this.hostHandle || this.peerHandle)
|
|
36302
|
+
return;
|
|
36303
|
+
if (this.connectingPromise) {
|
|
36304
|
+
await this.connectingPromise;
|
|
36305
|
+
return;
|
|
36306
|
+
}
|
|
36307
|
+
if (!this.identity || !this.mcpId) {
|
|
36308
|
+
throw new Error("FetchproxyServer: ensureConnected called before listen() \u2014 call listen() at MCP boot to load identity");
|
|
36309
|
+
}
|
|
36310
|
+
this.connectingPromise = this.doConnect();
|
|
36311
|
+
try {
|
|
36312
|
+
await this.connectingPromise;
|
|
36313
|
+
} finally {
|
|
36314
|
+
this.connectingPromise = null;
|
|
36315
|
+
}
|
|
36316
|
+
}
|
|
36317
|
+
async doConnect() {
|
|
36318
|
+
const identity = this.identity;
|
|
36319
|
+
const mcpId = this.mcpId;
|
|
36090
36320
|
const el = await electRole({ host: this.opts.host, port: this.opts.port });
|
|
36091
36321
|
if (el.role === "host") {
|
|
36092
36322
|
this.role = "host";
|
|
36093
36323
|
this.hostHandle = await startHost({
|
|
36094
36324
|
httpServer: el.server,
|
|
36095
|
-
ownIdentity:
|
|
36096
|
-
ownMcpId:
|
|
36325
|
+
ownIdentity: identity,
|
|
36326
|
+
ownMcpId: mcpId,
|
|
36097
36327
|
ownServerName: this.opts.serverName,
|
|
36098
36328
|
ownVersion: this.opts.version,
|
|
36099
36329
|
ownDomains: this.opts.domains,
|
|
@@ -36108,13 +36338,17 @@ var FetchproxyServer = class {
|
|
|
36108
36338
|
onPairCode: this.opts.onPairCode
|
|
36109
36339
|
});
|
|
36110
36340
|
this.hostHandle.onOwnInner((inner) => this.onInner(inner));
|
|
36341
|
+
this.hostHandle.onExtensionDisconnect(() => this.rejectAllPending());
|
|
36342
|
+
this.hostHandle.onPendingPair((code) => {
|
|
36343
|
+
this.rejectAllPending(this.pairingErrorMessage(code));
|
|
36344
|
+
});
|
|
36111
36345
|
} else {
|
|
36112
36346
|
this.role = "peer";
|
|
36113
36347
|
this.peerHandle = await startPeer({
|
|
36114
36348
|
host: this.opts.host,
|
|
36115
36349
|
port: this.opts.port,
|
|
36116
|
-
identity
|
|
36117
|
-
mcpId
|
|
36350
|
+
identity,
|
|
36351
|
+
mcpId,
|
|
36118
36352
|
serverName: this.opts.serverName,
|
|
36119
36353
|
version: this.opts.version,
|
|
36120
36354
|
domains: this.opts.domains,
|
|
@@ -36128,8 +36362,19 @@ var FetchproxyServer = class {
|
|
|
36128
36362
|
sessionStoragePointers: this.opts.sessionStoragePointers
|
|
36129
36363
|
});
|
|
36130
36364
|
this.peerHandle.onInner((inner) => this.onInner(inner));
|
|
36365
|
+
this.peerHandle.onRenegotiate(() => this.rejectAllPending());
|
|
36366
|
+
this.peerHandle.onPendingPair((code) => {
|
|
36367
|
+
this.rejectAllPending(this.pairingErrorMessage(code));
|
|
36368
|
+
});
|
|
36369
|
+
if (this.opts.onPairCode) {
|
|
36370
|
+
const cb = this.opts.onPairCode;
|
|
36371
|
+
this.peerHandle.onPendingPair((code) => cb(code));
|
|
36372
|
+
}
|
|
36131
36373
|
}
|
|
36132
36374
|
}
|
|
36375
|
+
pairingErrorMessage(code) {
|
|
36376
|
+
return `fetchproxy: pairing required for ${this.opts.serverName} \u2014 open the fetchproxy extension popup in Chrome and approve the pair request. Verify the pair code matches: ${code}`;
|
|
36377
|
+
}
|
|
36133
36378
|
/**
|
|
36134
36379
|
* Raw single-shot fetch through the bridge. Most callers should prefer
|
|
36135
36380
|
* the verb shortcuts (`get` / `post` / `getJson` / `postJson` / `getHtml`)
|
|
@@ -36145,8 +36390,11 @@ var FetchproxyServer = class {
|
|
|
36145
36390
|
* offline, etc.).
|
|
36146
36391
|
*/
|
|
36147
36392
|
async fetch(init) {
|
|
36148
|
-
|
|
36149
|
-
|
|
36393
|
+
await this.ensureConnected();
|
|
36394
|
+
const pendingCode = this.currentPendingPairCode();
|
|
36395
|
+
if (pendingCode !== null) {
|
|
36396
|
+
const error51 = this.pairingErrorMessage(pendingCode);
|
|
36397
|
+
return { ok: false, error: error51, kind: classifyFetchError(error51) };
|
|
36150
36398
|
}
|
|
36151
36399
|
const id = this.nextRequestId++;
|
|
36152
36400
|
const inner = { type: "request", id, op: "fetch", init };
|
|
@@ -36287,9 +36535,8 @@ var FetchproxyServer = class {
|
|
|
36287
36535
|
if (!this.opts.capabilities.includes("read_cookies")) {
|
|
36288
36536
|
throw new Error('FetchproxyServer.readCookies(): MCP did not declare "read_cookies" in capabilities \u2014 add it to FetchproxyServerOpts.capabilities to enable this verb');
|
|
36289
36537
|
}
|
|
36290
|
-
|
|
36291
|
-
|
|
36292
|
-
}
|
|
36538
|
+
await this.ensureConnected();
|
|
36539
|
+
this.throwIfPendingPair();
|
|
36293
36540
|
if (opts.subdomain !== void 0)
|
|
36294
36541
|
assertSubdomainLabel(opts.subdomain);
|
|
36295
36542
|
const baseDomain = this.resolveBaseDomain(opts.domain);
|
|
@@ -36346,9 +36593,8 @@ var FetchproxyServer = class {
|
|
|
36346
36593
|
if (!this.opts.capabilities.includes(op)) {
|
|
36347
36594
|
throw new Error(`FetchproxyServer.${op === "read_local_storage" ? "readLocalStorage" : "readSessionStorage"}(): MCP did not declare ${JSON.stringify(op)} in capabilities`);
|
|
36348
36595
|
}
|
|
36349
|
-
|
|
36350
|
-
|
|
36351
|
-
}
|
|
36596
|
+
await this.ensureConnected();
|
|
36597
|
+
this.throwIfPendingPair();
|
|
36352
36598
|
if (!Array.isArray(opts.keys) || opts.keys.length === 0) {
|
|
36353
36599
|
throw new Error(`FetchproxyServer.${op}: opts.keys must be a non-empty array`);
|
|
36354
36600
|
}
|
|
@@ -36403,9 +36649,8 @@ var FetchproxyServer = class {
|
|
|
36403
36649
|
if (!this.opts.capabilities.includes("capture_request_header")) {
|
|
36404
36650
|
throw new Error('FetchproxyServer.captureRequestHeader(): MCP did not declare "capture_request_header" in capabilities');
|
|
36405
36651
|
}
|
|
36406
|
-
|
|
36407
|
-
|
|
36408
|
-
}
|
|
36652
|
+
await this.ensureConnected();
|
|
36653
|
+
this.throwIfPendingPair();
|
|
36409
36654
|
const declared = this.opts.captureHeaders.find((d) => d.urlPattern === opts.urlPattern && d.headerName === opts.headerName);
|
|
36410
36655
|
if (!declared) {
|
|
36411
36656
|
throw new Error(`FetchproxyServer.captureRequestHeader: (urlPattern=${JSON.stringify(opts.urlPattern)}, headerName=${JSON.stringify(opts.headerName)}) not declared in captureHeaders`);
|
|
@@ -36446,9 +36691,8 @@ var FetchproxyServer = class {
|
|
|
36446
36691
|
if (!this.opts.capabilities.includes("read_indexed_db")) {
|
|
36447
36692
|
throw new Error('FetchproxyServer.readIndexedDb(): MCP did not declare "read_indexed_db" in capabilities');
|
|
36448
36693
|
}
|
|
36449
|
-
|
|
36450
|
-
|
|
36451
|
-
}
|
|
36694
|
+
await this.ensureConnected();
|
|
36695
|
+
this.throwIfPendingPair();
|
|
36452
36696
|
if (!Array.isArray(opts.keys) || opts.keys.length === 0) {
|
|
36453
36697
|
throw new Error("FetchproxyServer.readIndexedDb: opts.keys must be a non-empty array");
|
|
36454
36698
|
}
|
|
@@ -36526,10 +36770,11 @@ var FetchproxyServer = class {
|
|
|
36526
36770
|
if (inner.op === void 0 || inner.op === "fetch") {
|
|
36527
36771
|
fetchCb({ ok: true, status: inner.status, url: inner.url, body: inner.body });
|
|
36528
36772
|
} else {
|
|
36529
|
-
|
|
36773
|
+
const error51 = `unexpected ${inner.op} response on fetch awaiter`;
|
|
36774
|
+
fetchCb({ ok: false, error: error51, kind: classifyFetchError(error51) });
|
|
36530
36775
|
}
|
|
36531
36776
|
} else {
|
|
36532
|
-
fetchCb({ ok: false, error: inner.error });
|
|
36777
|
+
fetchCb({ ok: false, error: inner.error, kind: classifyFetchError(inner.error) });
|
|
36533
36778
|
}
|
|
36534
36779
|
return;
|
|
36535
36780
|
}
|
|
@@ -36596,6 +36841,52 @@ var FetchproxyServer = class {
|
|
|
36596
36841
|
}
|
|
36597
36842
|
}
|
|
36598
36843
|
}
|
|
36844
|
+
rejectAllPending(reason = "extension disconnected") {
|
|
36845
|
+
const err = new FetchproxyProtocolError(reason);
|
|
36846
|
+
for (const cb of this.pending.values()) {
|
|
36847
|
+
cb({ ok: false, error: err.message, kind: classifyFetchError(err.message) });
|
|
36848
|
+
}
|
|
36849
|
+
this.pending.clear();
|
|
36850
|
+
for (const cb of this.pendingReadCookies.values()) {
|
|
36851
|
+
cb({ ok: false, error: err.message });
|
|
36852
|
+
}
|
|
36853
|
+
this.pendingReadCookies.clear();
|
|
36854
|
+
for (const { reject } of this.pendingStorage.values())
|
|
36855
|
+
reject(err);
|
|
36856
|
+
this.pendingStorage.clear();
|
|
36857
|
+
for (const { reject } of this.pendingCapture.values())
|
|
36858
|
+
reject(err);
|
|
36859
|
+
this.pendingCapture.clear();
|
|
36860
|
+
for (const { reject } of this.pendingIdb.values())
|
|
36861
|
+
reject(err);
|
|
36862
|
+
this.pendingIdb.clear();
|
|
36863
|
+
}
|
|
36864
|
+
/**
|
|
36865
|
+
* 0.5.2+: read the current pair-pending pair code from whichever handle
|
|
36866
|
+
* is active, returning null when none is pending. Public verbs call this
|
|
36867
|
+
* at the top so that a tool invoked while the bridge is waiting on user
|
|
36868
|
+
* approval fails fast with the actionable error rather than hanging on a
|
|
36869
|
+
* sealed frame the extension will never process.
|
|
36870
|
+
*/
|
|
36871
|
+
currentPendingPairCode() {
|
|
36872
|
+
if (this.hostHandle)
|
|
36873
|
+
return this.hostHandle.pendingPairCode();
|
|
36874
|
+
if (this.peerHandle)
|
|
36875
|
+
return this.peerHandle.pendingPairCode();
|
|
36876
|
+
return null;
|
|
36877
|
+
}
|
|
36878
|
+
/**
|
|
36879
|
+
* 0.5.2+: throw `FetchproxyProtocolError` with the actionable pair-code
|
|
36880
|
+
* message if the bridge is waiting on user approval. Used by the verb
|
|
36881
|
+
* methods (readCookies, readLocalStorage, etc.) that surface errors via
|
|
36882
|
+
* thrown exceptions rather than `ok:false` discriminated unions.
|
|
36883
|
+
*/
|
|
36884
|
+
throwIfPendingPair() {
|
|
36885
|
+
const code = this.currentPendingPairCode();
|
|
36886
|
+
if (code !== null) {
|
|
36887
|
+
throw new FetchproxyProtocolError(this.pairingErrorMessage(code));
|
|
36888
|
+
}
|
|
36889
|
+
}
|
|
36599
36890
|
/**
|
|
36600
36891
|
* Shut down the bridge. Host: terminates the WebSocket server and any
|
|
36601
36892
|
* still-attached extension/peer clients. Peer: closes the upstream
|
|
@@ -36603,6 +36894,10 @@ var FetchproxyServer = class {
|
|
|
36603
36894
|
* twice in a row.
|
|
36604
36895
|
*/
|
|
36605
36896
|
async close() {
|
|
36897
|
+
this.rejectAllPending();
|
|
36898
|
+
if (this.connectingPromise) {
|
|
36899
|
+
await this.connectingPromise.catch(() => void 0);
|
|
36900
|
+
}
|
|
36606
36901
|
if (this.hostHandle)
|
|
36607
36902
|
await this.hostHandle.close();
|
|
36608
36903
|
if (this.peerHandle)
|
|
@@ -36610,6 +36905,7 @@ var FetchproxyServer = class {
|
|
|
36610
36905
|
this.hostHandle = null;
|
|
36611
36906
|
this.peerHandle = null;
|
|
36612
36907
|
this.role = null;
|
|
36908
|
+
this.connectingPromise = null;
|
|
36613
36909
|
}
|
|
36614
36910
|
};
|
|
36615
36911
|
|
|
@@ -36669,11 +36965,19 @@ async function bootstrap(opts) {
|
|
|
36669
36965
|
})),
|
|
36670
36966
|
onPairCode: opts.onPairCode
|
|
36671
36967
|
});
|
|
36968
|
+
const storageDomainOpts = {};
|
|
36969
|
+
if (opts.storageDomain !== void 0)
|
|
36970
|
+
storageDomainOpts.domain = opts.storageDomain;
|
|
36971
|
+
if (opts.storageSubdomain !== void 0)
|
|
36972
|
+
storageDomainOpts.subdomain = opts.storageSubdomain;
|
|
36672
36973
|
try {
|
|
36673
36974
|
await server2.listen();
|
|
36674
36975
|
const cookies = {};
|
|
36675
36976
|
if (opts.declare.cookies.length > 0) {
|
|
36676
|
-
const joined = await server2.readCookies({
|
|
36977
|
+
const joined = await server2.readCookies({
|
|
36978
|
+
keys: opts.declare.cookies,
|
|
36979
|
+
...storageDomainOpts
|
|
36980
|
+
});
|
|
36677
36981
|
for (const piece of joined.split("; ")) {
|
|
36678
36982
|
if (!piece)
|
|
36679
36983
|
continue;
|
|
@@ -36690,9 +36994,9 @@ async function bootstrap(opts) {
|
|
|
36690
36994
|
for (const p of localStoragePointers) {
|
|
36691
36995
|
pointers[p.outputKey] = { storageKey: p.storageKey, jsonPointer: p.jsonPointer };
|
|
36692
36996
|
}
|
|
36693
|
-
|
|
36694
|
-
localStorage = await stub.readLocalStorage({
|
|
36997
|
+
localStorage = await server2.readLocalStorage({
|
|
36695
36998
|
keys: allKeys,
|
|
36999
|
+
...storageDomainOpts,
|
|
36696
37000
|
...localStoragePointers.length > 0 ? { pointers } : {}
|
|
36697
37001
|
});
|
|
36698
37002
|
}
|
|
@@ -36703,9 +37007,9 @@ async function bootstrap(opts) {
|
|
|
36703
37007
|
for (const p of sessionStoragePointers) {
|
|
36704
37008
|
pointers[p.outputKey] = { storageKey: p.storageKey, jsonPointer: p.jsonPointer };
|
|
36705
37009
|
}
|
|
36706
|
-
|
|
36707
|
-
sessionStorage = await stub.readSessionStorage({
|
|
37010
|
+
sessionStorage = await server2.readSessionStorage({
|
|
36708
37011
|
keys: allKeys,
|
|
37012
|
+
...storageDomainOpts,
|
|
36709
37013
|
...sessionStoragePointers.length > 0 ? { pointers } : {}
|
|
36710
37014
|
});
|
|
36711
37015
|
}
|
|
@@ -36726,13 +37030,11 @@ async function bootstrap(opts) {
|
|
|
36726
37030
|
}
|
|
36727
37031
|
const indexedDbBucket = {};
|
|
36728
37032
|
for (const d of indexedDb) {
|
|
36729
|
-
if (!server2.readIndexedDb) {
|
|
36730
|
-
throw new Error("bootstrap: server factory does not implement readIndexedDb (declared indexedDb but server stub omits it)");
|
|
36731
|
-
}
|
|
36732
37033
|
const values = await server2.readIndexedDb({
|
|
36733
37034
|
database: d.database,
|
|
36734
37035
|
store: d.store,
|
|
36735
|
-
keys: [...d.keys]
|
|
37036
|
+
keys: [...d.keys],
|
|
37037
|
+
...storageDomainOpts
|
|
36736
37038
|
});
|
|
36737
37039
|
indexedDbBucket[`${d.database}/${d.store}`] = values;
|
|
36738
37040
|
}
|
|
@@ -36764,7 +37066,7 @@ var BootstrapDisabledError = class extends Error {
|
|
|
36764
37066
|
// package.json
|
|
36765
37067
|
var package_default = {
|
|
36766
37068
|
name: "honeybook-mcp",
|
|
36767
|
-
version: "0.
|
|
37069
|
+
version: "0.2.4",
|
|
36768
37070
|
mcpName: "io.github.chrischall/honeybook-mcp",
|
|
36769
37071
|
description: "HoneyBook client-portal MCP server for Claude",
|
|
36770
37072
|
author: "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -36798,23 +37100,23 @@ var package_default = {
|
|
|
36798
37100
|
},
|
|
36799
37101
|
scripts: {
|
|
36800
37102
|
build: "tsc && npm run bundle",
|
|
36801
|
-
bundle:
|
|
37103
|
+
bundle: `esbuild src/index.ts --bundle --platform=node --format=esm --external:dotenv --banner:js='import { createRequire as __createRequire } from "module"; const require = __createRequire(import.meta.url);' --outfile=dist/bundle.js`,
|
|
36802
37104
|
dev: "node --env-file=.env dist/index.js",
|
|
36803
37105
|
test: "vitest run",
|
|
36804
37106
|
"test:watch": "vitest"
|
|
36805
37107
|
},
|
|
36806
37108
|
dependencies: {
|
|
36807
|
-
"@fetchproxy/bootstrap": "^0.
|
|
37109
|
+
"@fetchproxy/bootstrap": "^0.6.0",
|
|
36808
37110
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
36809
|
-
dotenv: "^17.4.
|
|
36810
|
-
zod: "^4.4.
|
|
37111
|
+
dotenv: "^17.4.2",
|
|
37112
|
+
zod: "^4.4.3"
|
|
36811
37113
|
},
|
|
36812
37114
|
devDependencies: {
|
|
36813
|
-
"@types/node": "^25.
|
|
36814
|
-
"@vitest/coverage-v8": "^4.1.
|
|
37115
|
+
"@types/node": "^25.9.1",
|
|
37116
|
+
"@vitest/coverage-v8": "^4.1.7",
|
|
36815
37117
|
esbuild: "^0.28.0",
|
|
36816
|
-
typescript: "^6.0.
|
|
36817
|
-
vitest: "^4.1.
|
|
37118
|
+
typescript: "^6.0.3",
|
|
37119
|
+
vitest: "^4.1.7"
|
|
36818
37120
|
}
|
|
36819
37121
|
};
|
|
36820
37122
|
|
|
@@ -36848,7 +37150,21 @@ async function captureSessionViaFetchproxy(opts) {
|
|
|
36848
37150
|
// HoneyBook serves both the main app (honeybook.com) and per-vendor
|
|
36849
37151
|
// portal subdomains (*.hbportal.co). The extension matches on suffix,
|
|
36850
37152
|
// so listing both apexes covers any vendor.
|
|
37153
|
+
//
|
|
37154
|
+
// Both are needed:
|
|
37155
|
+
// - `hbportal.co` is where the vendor magic-link tab lives, so
|
|
37156
|
+
// `jStorage` reads target it (via `storageDomain` below).
|
|
37157
|
+
// - `honeybook.com` is required for the `hb-api-fingerprint`
|
|
37158
|
+
// capture, because that header rides outgoing requests to
|
|
37159
|
+
// `https://api.honeybook.com/api/v2/*` and the extension gates
|
|
37160
|
+
// every captureHeader urlPattern host against declared domains.
|
|
37161
|
+
//
|
|
37162
|
+
// 0.4.1+ requires multi-domain MCPs to pick a `storageDomain` so
|
|
37163
|
+
// the cookie / localStorage / sessionStorage / indexedDb reads
|
|
37164
|
+
// know which tab to target. captureRequestHeader is independently
|
|
37165
|
+
// routed by urlPattern.
|
|
36851
37166
|
domains: ["honeybook.com", "hbportal.co"],
|
|
37167
|
+
storageDomain: "hbportal.co",
|
|
36852
37168
|
declare: {
|
|
36853
37169
|
cookies: [],
|
|
36854
37170
|
// We don't need the full `jStorage` blob (it's ~8KB of mostly
|
|
@@ -37416,7 +37732,8 @@ try {
|
|
|
37416
37732
|
}
|
|
37417
37733
|
var server = new McpServer({
|
|
37418
37734
|
name: "honeybook-mcp",
|
|
37419
|
-
version: "0.
|
|
37735
|
+
version: "0.2.4"
|
|
37736
|
+
// x-release-please-version
|
|
37420
37737
|
});
|
|
37421
37738
|
registerSessionTools(server);
|
|
37422
37739
|
registerWorkspaceFileTools(server);
|
package/dist/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import { registerContractTools } from './tools/contracts.js';
|
|
|
18
18
|
import { registerInvoiceTools } from './tools/invoices.js';
|
|
19
19
|
const server = new McpServer({
|
|
20
20
|
name: 'honeybook-mcp',
|
|
21
|
-
version: '0.
|
|
21
|
+
version: '0.2.4', // x-release-please-version
|
|
22
22
|
});
|
|
23
23
|
registerSessionTools(server);
|
|
24
24
|
registerWorkspaceFileTools(server);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "honeybook-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.4",
|
|
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>",
|
|
@@ -34,22 +34,22 @@
|
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
|
36
36
|
"build": "tsc && npm run bundle",
|
|
37
|
-
"bundle": "esbuild src/index.ts --bundle --platform=node --format=esm --external:dotenv --outfile=dist/bundle.js",
|
|
37
|
+
"bundle": "esbuild src/index.ts --bundle --platform=node --format=esm --external:dotenv --banner:js='import { createRequire as __createRequire } from \"module\"; const require = __createRequire(import.meta.url);' --outfile=dist/bundle.js",
|
|
38
38
|
"dev": "node --env-file=.env dist/index.js",
|
|
39
39
|
"test": "vitest run",
|
|
40
40
|
"test:watch": "vitest"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@fetchproxy/bootstrap": "^0.
|
|
43
|
+
"@fetchproxy/bootstrap": "^0.6.0",
|
|
44
44
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
45
|
-
"dotenv": "^17.4.
|
|
46
|
-
"zod": "^4.4.
|
|
45
|
+
"dotenv": "^17.4.2",
|
|
46
|
+
"zod": "^4.4.3"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@types/node": "^25.
|
|
50
|
-
"@vitest/coverage-v8": "^4.1.
|
|
49
|
+
"@types/node": "^25.9.1",
|
|
50
|
+
"@vitest/coverage-v8": "^4.1.7",
|
|
51
51
|
"esbuild": "^0.28.0",
|
|
52
|
-
"typescript": "^6.0.
|
|
53
|
-
"vitest": "^4.1.
|
|
52
|
+
"typescript": "^6.0.3",
|
|
53
|
+
"vitest": "^4.1.7"
|
|
54
54
|
}
|
|
55
55
|
}
|
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.2.4",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "honeybook-mcp",
|
|
14
|
-
"version": "0.
|
|
14
|
+
"version": "0.2.4",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|