zola-mcp 1.1.3 → 1.2.3

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.
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "metadata": {
9
9
  "description": "Zola wedding planning tools for Claude Code",
10
- "version": "1.1.3"
10
+ "version": "1.2.3"
11
11
  },
12
12
  "plugins": [
13
13
  {
@@ -15,7 +15,7 @@
15
15
  "displayName": "Zola",
16
16
  "source": "./",
17
17
  "description": "Zola wedding planning tools for Claude — vendors, budget, guests, seating, events, registry, inquiries, and more via MCP",
18
- "version": "1.1.3",
18
+ "version": "1.2.3",
19
19
  "author": {
20
20
  "name": "Chris Chall"
21
21
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "zola",
3
3
  "displayName": "Zola",
4
- "version": "1.1.3",
4
+ "version": "1.2.3",
5
5
  "description": "Zola wedding planning tools for Claude — vendors, budget, guests, seating, events, registry, inquiries, and more via MCP",
6
6
  "author": {
7
7
  "name": "Chris Chall",
package/README.md CHANGED
@@ -25,6 +25,28 @@ Ask Claude things like:
25
25
  - A [Zola](https://www.zola.com) account
26
26
  - For the no-env-var path: the [fetchproxy 0.3.0 Chrome / Safari extension](https://github.com/chrischall/fetchproxy)
27
27
 
28
+ ## Acknowledgement of Terms
29
+
30
+ By using this MCP server, you acknowledge and agree to the following:
31
+
32
+ **1. This server accesses your own Zola account.** Auth happens via your own credentials. It does not — and cannot — access anyone else's wedding website, registry, or guest list.
33
+
34
+ **2. [Zola's Terms of Use](https://www.zola.com/terms) govern your use of this server**, just as they govern your direct use of zola.com. The clauses most relevant here:
35
+
36
+ > [You may not use] any hardware or software intended to surreptitiously intercept or otherwise obtain any information… including but not limited to the use of any "scraping" or other data mining techniques, robots or similar data gathering and extraction tools.
37
+
38
+ And, critically, on agent-acting-as-you: *"You are responsible for maintaining the confidentiality of your account and password… You accept full responsibility for all activities that occur under your account and password, **even if such actions are undertaken by your Authorized Agent or other third party**."*
39
+
40
+ You are agreeing to those terms — read by the maintainer 2026-05-23 — every time you invoke a tool in this server. Zola's ToU is explicit: this MCP acting as your Authorized Agent counts as you.
41
+
42
+ **3. Personal, non-commercial use only.** This project is not affiliated with, endorsed by, sponsored by, or in partnership with Zola, Inc. It is a personal automation tool for one couple to manage their own wedding website, registry, vendor research, and guest list. Do not use it to bulk-extract Zola's vendor directory, scrape registries, or compete with Zola.
43
+
44
+ **4. Stability is not guaranteed.** This server may call internal Zola endpoints that change without notice. It may break.
45
+
46
+ **5. You accept full responsibility** for any consequences of using this server in connection with your Zola account — rate limiting, account warnings, suspension, or any enforcement action. Per Zola's ToU, anything this MCP does under your account is your action — review guest list edits, registry changes, and inquiries before confirming. If Zola objects to your use, stop using this server.
47
+
48
+ This section is the maintainer's good-faith summary of the terms — it is not legal advice and does not modify or supersede Zola's actual ToU.
49
+
28
50
  ## Installation
29
51
 
30
52
  ### Option A — MCPB (recommended)
package/dist/bundle.js CHANGED
@@ -7571,6 +7571,10 @@ var require_receiver = __commonJS({
7571
7571
  * extensions
7572
7572
  * @param {Boolean} [options.isServer=false] Specifies whether to operate in
7573
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
7574
7578
  * @param {Number} [options.maxPayload=0] The maximum allowed message length
7575
7579
  * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
7576
7580
  * not to skip UTF-8 validation for text and close messages
@@ -7581,6 +7585,8 @@ var require_receiver = __commonJS({
7581
7585
  this._binaryType = options.binaryType || BINARY_TYPES[0];
7582
7586
  this._extensions = options.extensions || {};
7583
7587
  this._isServer = !!options.isServer;
7588
+ this._maxBufferedChunks = options.maxBufferedChunks | 0;
7589
+ this._maxFragments = options.maxFragments | 0;
7584
7590
  this._maxPayload = options.maxPayload | 0;
7585
7591
  this._skipUTF8Validation = !!options.skipUTF8Validation;
7586
7592
  this[kWebSocket] = void 0;
@@ -7610,6 +7616,18 @@ var require_receiver = __commonJS({
7610
7616
  */
7611
7617
  _write(chunk, encoding, cb) {
7612
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
+ }
7613
7631
  this._bufferedBytes += chunk.length;
7614
7632
  this._buffers.push(chunk);
7615
7633
  this.startLoop(cb);
@@ -7939,6 +7957,17 @@ var require_receiver = __commonJS({
7939
7957
  return;
7940
7958
  }
7941
7959
  if (data.length) {
7960
+ if (this._maxFragments > 0 && this._fragments.length >= this._maxFragments) {
7961
+ const error48 = this.createError(
7962
+ RangeError,
7963
+ "Too many message fragments",
7964
+ false,
7965
+ 1008,
7966
+ "WS_ERR_TOO_MANY_BUFFERED_PARTS"
7967
+ );
7968
+ cb(error48);
7969
+ return;
7970
+ }
7942
7971
  this._messageLength = this._totalPayloadLength;
7943
7972
  this._fragments.push(data);
7944
7973
  }
@@ -7968,6 +7997,17 @@ var require_receiver = __commonJS({
7968
7997
  cb(error48);
7969
7998
  return;
7970
7999
  }
8000
+ if (this._maxFragments > 0 && this._fragments.length >= this._maxFragments) {
8001
+ const error48 = this.createError(
8002
+ RangeError,
8003
+ "Too many message fragments",
8004
+ false,
8005
+ 1008,
8006
+ "WS_ERR_TOO_MANY_BUFFERED_PARTS"
8007
+ );
8008
+ cb(error48);
8009
+ return;
8010
+ }
7971
8011
  this._fragments.push(buf);
7972
8012
  }
7973
8013
  this.dataMessage(cb);
@@ -9174,6 +9214,10 @@ var require_websocket = __commonJS({
9174
9214
  * multiple times in the same tick
9175
9215
  * @param {Function} [options.generateMask] The function used to generate the
9176
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
9177
9221
  * @param {Number} [options.maxPayload=0] The maximum allowed message size
9178
9222
  * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
9179
9223
  * not to skip UTF-8 validation for text and close messages
@@ -9185,6 +9229,8 @@ var require_websocket = __commonJS({
9185
9229
  binaryType: this.binaryType,
9186
9230
  extensions: this._extensions,
9187
9231
  isServer: this._isServer,
9232
+ maxBufferedChunks: options.maxBufferedChunks,
9233
+ maxFragments: options.maxFragments,
9188
9234
  maxPayload: options.maxPayload,
9189
9235
  skipUTF8Validation: options.skipUTF8Validation
9190
9236
  });
@@ -9484,6 +9530,8 @@ var require_websocket = __commonJS({
9484
9530
  autoPong: true,
9485
9531
  closeTimeout: CLOSE_TIMEOUT,
9486
9532
  protocolVersion: protocolVersions[1],
9533
+ maxBufferedChunks: 1024 * 1024,
9534
+ maxFragments: 128 * 1024,
9487
9535
  maxPayload: 100 * 1024 * 1024,
9488
9536
  skipUTF8Validation: false,
9489
9537
  perMessageDeflate: true,
@@ -9726,6 +9774,8 @@ var require_websocket = __commonJS({
9726
9774
  websocket.setSocket(socket, head, {
9727
9775
  allowSynchronousEvents: opts.allowSynchronousEvents,
9728
9776
  generateMask: opts.generateMask,
9777
+ maxBufferedChunks: opts.maxBufferedChunks,
9778
+ maxFragments: opts.maxFragments,
9729
9779
  maxPayload: opts.maxPayload,
9730
9780
  skipUTF8Validation: opts.skipUTF8Validation
9731
9781
  });
@@ -10068,6 +10118,10 @@ var require_websocket_server = __commonJS({
10068
10118
  * called
10069
10119
  * @param {Function} [options.handleProtocols] A hook to handle protocols
10070
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
10071
10125
  * @param {Number} [options.maxPayload=104857600] The maximum allowed message
10072
10126
  * size
10073
10127
  * @param {Boolean} [options.noServer=false] Enable no server mode
@@ -10089,6 +10143,8 @@ var require_websocket_server = __commonJS({
10089
10143
  options = {
10090
10144
  allowSynchronousEvents: true,
10091
10145
  autoPong: true,
10146
+ maxBufferedChunks: 1024 * 1024,
10147
+ maxFragments: 128 * 1024,
10092
10148
  maxPayload: 100 * 1024 * 1024,
10093
10149
  skipUTF8Validation: false,
10094
10150
  perMessageDeflate: false,
@@ -10368,6 +10424,8 @@ var require_websocket_server = __commonJS({
10368
10424
  socket.removeListener("error", socketOnError);
10369
10425
  ws.setSocket(socket, head, {
10370
10426
  allowSynchronousEvents: this.options.allowSynchronousEvents,
10427
+ maxBufferedChunks: this.options.maxBufferedChunks,
10428
+ maxFragments: this.options.maxFragments,
10371
10429
  maxPayload: this.options.maxPayload,
10372
10430
  skipUTF8Validation: this.options.skipUTF8Validation
10373
10431
  });
@@ -33746,6 +33804,7 @@ import { fileURLToPath } from "url";
33746
33804
 
33747
33805
  // node_modules/@fetchproxy/protocol/dist/frames.js
33748
33806
  var PROTOCOL_VERSION = 2;
33807
+ var HKDF_SESSION_INFO = "fetchproxy/1.0.0/session";
33749
33808
  var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
33750
33809
  "fetch",
33751
33810
  "read_cookies",
@@ -33820,7 +33879,7 @@ var ProtocolError = class extends Error {
33820
33879
  var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
33821
33880
  var BASE64_RE = /^[A-Za-z0-9+/]*={0,2}$/;
33822
33881
  var SCOPE_KEY_RE = /^[A-Za-z0-9_.\-]{1,256}$/;
33823
- var SCOPE_KEY_GLOB_RE = /^[A-Za-z0-9_.\-]{1,255}\*?$/;
33882
+ var SCOPE_KEY_GLOB_RE = /^[A-Za-z0-9_.\-]{1,256}\*?$/;
33824
33883
  var HEADER_NAME_RE = /^[A-Za-z0-9_\-]{1,128}$/;
33825
33884
  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;
33826
33885
  function assertObject(x, label) {
@@ -33949,7 +34008,7 @@ function assertStoragePointersArray(value, label, declaredKeys) {
33949
34008
  if (entry.jsonPointer === void 0) {
33950
34009
  throw new ProtocolError(`${label}[${i}].jsonPointer: missing`);
33951
34010
  }
33952
- if (typeof entry.key !== "string" || !SCOPE_KEY_GLOB_RE.test(entry.key)) {
34011
+ if (typeof entry.key !== "string" || !SCOPE_KEY_RE.test(entry.key)) {
33953
34012
  throw new ProtocolError(`${label}[${i}].key: invalid key ${JSON.stringify(entry.key)}`);
33954
34013
  }
33955
34014
  if (typeof entry.jsonPointer !== "string" || !isValidJsonPointer(entry.jsonPointer)) {
@@ -34048,6 +34107,8 @@ function validateFrame(raw) {
34048
34107
  return validateReady(raw);
34049
34108
  if (t === "frame")
34050
34109
  return validateEncrypted(raw);
34110
+ if (t === "pair-pending")
34111
+ return validatePairPending(raw);
34051
34112
  throw new ProtocolError(`unknown frame type: ${String(t)}`);
34052
34113
  }
34053
34114
  function validateHello(raw) {
@@ -34149,6 +34210,17 @@ function validateEncrypted(raw) {
34149
34210
  assertBase64(raw.ciphertext, "frame.ciphertext");
34150
34211
  return raw;
34151
34212
  }
34213
+ var PAIR_CODE_RE = /^\d{3}-\d{3}$/;
34214
+ function validatePairPending(raw) {
34215
+ assertString(raw.mcpId, "pair-pending.mcpId");
34216
+ if (!isValidMcpId(raw.mcpId))
34217
+ throw new ProtocolError("pair-pending.mcpId: invalid format");
34218
+ assertString(raw.pairCode, "pair-pending.pairCode");
34219
+ if (!PAIR_CODE_RE.test(raw.pairCode)) {
34220
+ throw new ProtocolError(`pair-pending.pairCode: must match XXX-XXX, got ${String(raw.pairCode)}`);
34221
+ }
34222
+ return { type: "pair-pending", mcpId: raw.mcpId, pairCode: raw.pairCode };
34223
+ }
34152
34224
  function validateInnerFrame(raw) {
34153
34225
  assertObject(raw, "inner");
34154
34226
  const t = raw.type;
@@ -34731,15 +34803,22 @@ async function startHost(opts) {
34731
34803
  let extensionWs = null;
34732
34804
  const peers = /* @__PURE__ */ new Map();
34733
34805
  const ownInnerListeners = [];
34806
+ const disconnectListeners = [];
34807
+ const pendingPairListeners = [];
34734
34808
  let ownSession = null;
34809
+ let ownPendingPairCode = null;
34735
34810
  let resolveOwnSession;
34736
34811
  let rejectOwnSession;
34737
- const ownSessionReady = new Promise((resolve, reject) => {
34738
- resolveOwnSession = resolve;
34739
- rejectOwnSession = reject;
34740
- });
34741
- ownSessionReady.catch(() => {
34742
- });
34812
+ let ownSessionReady;
34813
+ function resetSessionPromise() {
34814
+ ownSessionReady = new Promise((resolve, reject) => {
34815
+ resolveOwnSession = resolve;
34816
+ rejectOwnSession = reject;
34817
+ });
34818
+ ownSessionReady.catch(() => {
34819
+ });
34820
+ }
34821
+ resetSessionPromise();
34743
34822
  let extensionHello = null;
34744
34823
  wss.on("connection", (ws) => {
34745
34824
  let identified = null;
@@ -34808,11 +34887,12 @@ async function startHost(opts) {
34808
34887
  }
34809
34888
  const extPub = fromB64(frame.extensionSessionPub);
34810
34889
  const shared = await ecdhX25519(opts.ownIdentity.x25519Priv, extPub);
34811
- const key = await hkdfSha256(shared, ownSessionNonce, enc2.encode("fetchproxy/0.1.0/session"), 32);
34812
- if (!ownSession) {
34813
- ownSession = new SessionState(key);
34814
- resolveOwnSession(ownSession);
34815
- }
34890
+ const key = await hkdfSha256(shared, ownSessionNonce, enc2.encode(HKDF_SESSION_INFO), 32);
34891
+ if (extensionWs !== ws)
34892
+ return;
34893
+ ownSession = new SessionState(key);
34894
+ ownPendingPairCode = null;
34895
+ resolveOwnSession(ownSession);
34816
34896
  } else {
34817
34897
  const slot = peers.get(frame.mcpId);
34818
34898
  if (slot)
@@ -34839,6 +34919,16 @@ async function startHost(opts) {
34839
34919
  extensionWs.send(JSON.stringify(frame));
34840
34920
  }
34841
34921
  }
34922
+ if (frame.type === "pair-pending" && identified === "extension") {
34923
+ if (frame.mcpId === opts.ownMcpId) {
34924
+ ownPendingPairCode = frame.pairCode;
34925
+ pendingPairListeners.forEach((cb) => cb(frame.pairCode));
34926
+ } else {
34927
+ const slot = peers.get(frame.mcpId);
34928
+ if (slot)
34929
+ slot.ws.send(JSON.stringify(frame));
34930
+ }
34931
+ }
34842
34932
  } catch (e) {
34843
34933
  console.error("[fetchproxy] host: message handler error:", e);
34844
34934
  try {
@@ -34854,6 +34944,9 @@ async function startHost(opts) {
34854
34944
  if (!ownSession) {
34855
34945
  rejectOwnSession(new Error("extension disconnected before ready"));
34856
34946
  }
34947
+ ownSession = null;
34948
+ resetSessionPromise();
34949
+ disconnectListeners.forEach((cb) => cb());
34857
34950
  }
34858
34951
  if (identified === "peer" && peerMcpId)
34859
34952
  peers.delete(peerMcpId);
@@ -34878,7 +34971,14 @@ async function startHost(opts) {
34878
34971
  },
34879
34972
  onOwnInner: (cb) => {
34880
34973
  ownInnerListeners.push(cb);
34881
- }
34974
+ },
34975
+ onExtensionDisconnect: (cb) => {
34976
+ disconnectListeners.push(cb);
34977
+ },
34978
+ onPendingPair: (cb) => {
34979
+ pendingPairListeners.push(cb);
34980
+ },
34981
+ pendingPairCode: () => ownPendingPairCode
34882
34982
  };
34883
34983
  }
34884
34984
 
@@ -34908,36 +35008,57 @@ async function startPeer(opts) {
34908
35008
  const sessionNonce = fromB64(hello.sessionNonce);
34909
35009
  ws.send(JSON.stringify(hello));
34910
35010
  const innerListeners = [];
35011
+ const renegotiateListeners = [];
35012
+ const pendingPairListeners = [];
34911
35013
  let session = null;
35014
+ let pendingPairCode = null;
35015
+ let resolveFirstReady;
35016
+ let rejectFirstReady;
34912
35017
  const sessionPromise = new Promise((resolve, reject) => {
34913
- const onMessage = async (data) => {
34914
- try {
34915
- const raw = JSON.parse(data.toString());
34916
- const frame = validateFrame(raw);
34917
- if (frame.type === "ready" && frame.mcpId === opts.mcpId) {
34918
- const extPub = fromB64(frame.extensionSessionPub);
34919
- const shared = await ecdhX25519(opts.identity.x25519Priv, extPub);
34920
- const sessionKey = await hkdfSha256(shared, sessionNonce, enc3.encode("fetchproxy/0.1.0/session"), 32);
34921
- session = new SessionState(sessionKey);
34922
- resolve(session);
34923
- return;
35018
+ resolveFirstReady = resolve;
35019
+ rejectFirstReady = reject;
35020
+ });
35021
+ const onMessage = async (data) => {
35022
+ try {
35023
+ const raw = JSON.parse(data.toString());
35024
+ const frame = validateFrame(raw);
35025
+ if (frame.type === "ready" && frame.mcpId === opts.mcpId) {
35026
+ const extPub = fromB64(frame.extensionSessionPub);
35027
+ const shared = await ecdhX25519(opts.identity.x25519Priv, extPub);
35028
+ const sessionKey = await hkdfSha256(shared, sessionNonce, enc3.encode(HKDF_SESSION_INFO), 32);
35029
+ const isRenegotiation = session !== null;
35030
+ session = new SessionState(sessionKey);
35031
+ pendingPairCode = null;
35032
+ if (isRenegotiation) {
35033
+ renegotiateListeners.forEach((cb) => cb());
35034
+ } else {
35035
+ resolveFirstReady(session);
34924
35036
  }
34925
- if (frame.type === "frame" && frame.mcpId === opts.mcpId) {
34926
- if (!session)
34927
- return;
34928
- if (!session.acceptInboundSeq(frame.seq))
34929
- return;
35037
+ return;
35038
+ }
35039
+ if (frame.type === "pair-pending" && frame.mcpId === opts.mcpId) {
35040
+ pendingPairCode = frame.pairCode;
35041
+ pendingPairListeners.forEach((cb) => cb(frame.pairCode));
35042
+ return;
35043
+ }
35044
+ if (frame.type === "frame" && frame.mcpId === opts.mcpId) {
35045
+ if (!session)
35046
+ return;
35047
+ if (!session.acceptInboundSeq(frame.seq))
35048
+ return;
35049
+ try {
34930
35050
  const inner = await openEncryptedFrame(session.sessionKey, frame);
34931
35051
  innerListeners.forEach((cb) => cb(inner));
35052
+ } catch {
34932
35053
  }
34933
- } catch (e) {
34934
- reject(e instanceof Error ? e : new Error(String(e)));
34935
35054
  }
34936
- };
34937
- ws.on("message", onMessage);
34938
- ws.once("close", () => {
34939
- reject(new Error("peer WS closed before ready"));
34940
- });
35055
+ } catch (e) {
35056
+ rejectFirstReady(e instanceof Error ? e : new Error(String(e)));
35057
+ }
35058
+ };
35059
+ ws.on("message", onMessage);
35060
+ ws.once("close", () => {
35061
+ rejectFirstReady(new Error("peer WS closed before ready"));
34941
35062
  });
34942
35063
  sessionPromise.catch(() => {
34943
35064
  });
@@ -34945,13 +35066,21 @@ async function startPeer(opts) {
34945
35066
  ws,
34946
35067
  session: sessionPromise,
34947
35068
  sendInner: async (inner) => {
34948
- const s = await sessionPromise;
35069
+ await sessionPromise;
35070
+ const s = session;
34949
35071
  const sealed = await sealInnerFrame(s.sessionKey, opts.mcpId, s.nextOutboundSeq(), inner);
34950
35072
  ws.send(JSON.stringify(sealed));
34951
35073
  },
34952
35074
  onInner: (cb) => {
34953
35075
  innerListeners.push(cb);
34954
35076
  },
35077
+ onRenegotiate: (cb) => {
35078
+ renegotiateListeners.push(cb);
35079
+ },
35080
+ onPendingPair: (cb) => {
35081
+ pendingPairListeners.push(cb);
35082
+ },
35083
+ pendingPairCode: () => pendingPairCode,
34955
35084
  close: () => ws.close()
34956
35085
  };
34957
35086
  return handle;
@@ -35008,6 +35137,32 @@ async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
35008
35137
  return id;
35009
35138
  }
35010
35139
 
35140
+ // node_modules/@fetchproxy/server/dist/error-kind.js
35141
+ function classifyFetchError(error48) {
35142
+ if (/Could not establish connection/i.test(error48) || /Receiving end does not exist/i.test(error48)) {
35143
+ return "content_script_unreachable";
35144
+ }
35145
+ if (/^tab fetch failed:/.test(error48)) {
35146
+ return "tab_fetch_failed";
35147
+ }
35148
+ if (/^fetch threw:/.test(error48)) {
35149
+ return "tab_fetch_failed";
35150
+ }
35151
+ if (/^no tab matching /.test(error48)) {
35152
+ return "no_tab";
35153
+ }
35154
+ if (/not in domains \[/.test(error48)) {
35155
+ return "domain_denied";
35156
+ }
35157
+ if (/^capability .+ not granted/.test(error48)) {
35158
+ return "capability_denied";
35159
+ }
35160
+ if (/^(request|response) body too large:/.test(error48)) {
35161
+ return "body_too_large";
35162
+ }
35163
+ return "other";
35164
+ }
35165
+
35011
35166
  // node_modules/@fetchproxy/server/dist/ws-server.js
35012
35167
  var FetchproxyProtocolError = class extends Error {
35013
35168
  constructor(message) {
@@ -35050,7 +35205,11 @@ function assertUrlInDomains(field, url2, domains) {
35050
35205
  }
35051
35206
  var DEFAULT_JSON_OK_STATUSES = [200, 201, 202, 204];
35052
35207
  var FetchproxyServer = class {
35053
- /** Set after `listen()` succeeds. Null while not listening. */
35208
+ /**
35209
+ * Bridge role. `null` until the first verb call (or an explicit
35210
+ * `connect()`) — `listen()` no longer triggers the role election
35211
+ * as of 0.5.3+. Reset to `null` on `close()`.
35212
+ */
35054
35213
  role = null;
35055
35214
  opts;
35056
35215
  hostHandle = null;
@@ -35072,6 +35231,12 @@ var FetchproxyServer = class {
35072
35231
  pendingIdb = /* @__PURE__ */ new Map();
35073
35232
  mcpId = null;
35074
35233
  identity = null;
35234
+ // 0.5.3+: in-flight role-election / handle-start promise. Set the
35235
+ // first time a verb call runs `ensureConnected`, awaited by concurrent
35236
+ // callers, cleared once the connection is up. Single source of truth
35237
+ // for "we're connecting right now" so two parallel first-calls don't
35238
+ // race the port bind.
35239
+ connectingPromise = null;
35075
35240
  constructor(opts) {
35076
35241
  if (!Array.isArray(opts.domains) || opts.domains.length === 0) {
35077
35242
  throw new Error("FetchproxyServer: opts.domains must be a non-empty array of hostnames");
@@ -35123,23 +35288,87 @@ var FetchproxyServer = class {
35123
35288
  };
35124
35289
  }
35125
35290
  /**
35126
- * Start the WebSocket bridge. Loads the long-term identity keypair
35127
- * from disk (creating it on first call), elects the host-vs-peer
35128
- * role by attempting to bind the configured port, and stands up the
35129
- * matching handshake machinery. Idempotent only insofar as it leaves
35130
- * `role` non-null on success; calling `listen()` twice without an
35131
- * intervening `close()` is a programming error.
35291
+ * Prepare the bridge for use. Loads the long-term identity keypair
35292
+ * from disk (creating it on first call) and computes this instance's
35293
+ * `mcpId`. Does NOT bind the bridge port or dial any WebSocket — the
35294
+ * connection is established lazily on the first verb call (see
35295
+ * `ensureConnected` / `getOrConnect`).
35296
+ *
35297
+ * Pre-0.5.3 behavior: `listen()` also did role election and started
35298
+ * the host/peer immediately, which meant every configured-but-unused
35299
+ * MCP claimed bridge resources at MCP-client boot. Several MCPs
35300
+ * starting in parallel under Claude Desktop also produced noisy
35301
+ * `ERR_CONNECTION_REFUSED` errors in the extension if it raced ahead
35302
+ * of the first MCP's port bind. Deferring keeps boot quiet and
35303
+ * leaves the port unowned until something actually needs it.
35304
+ *
35305
+ * Calling `listen()` twice without an intervening `close()` is a
35306
+ * no-op (the second call's identity load is idempotent).
35132
35307
  */
35133
35308
  async listen() {
35134
- this.identity = await loadOrCreateIdentity(this.opts.serverName, this.opts.identityDir);
35135
- this.mcpId = generateMcpId(this.opts.serverName, this.opts.version);
35309
+ if (!this.identity) {
35310
+ this.identity = await loadOrCreateIdentity(this.opts.serverName, this.opts.identityDir);
35311
+ }
35312
+ if (!this.mcpId) {
35313
+ this.mcpId = generateMcpId(this.opts.serverName, this.opts.version);
35314
+ }
35315
+ }
35316
+ /**
35317
+ * Force an eager bridge connection (role-election + host/peer handle
35318
+ * start + listener wiring) without waiting for the first verb call.
35319
+ * Useful for callers that want to surface the role / connection
35320
+ * outcome at boot, or for tests whose harness dials a mock extension
35321
+ * immediately after server construction. Production MCPs that just
35322
+ * answer tool calls should NOT call this — the lazy connect via
35323
+ * `ensureConnected` will do the right thing on first use, keeping
35324
+ * boot cheap and avoiding port-bind contention for MCPs that never
35325
+ * actually get invoked.
35326
+ *
35327
+ * Idempotent: a second call after the first has resolved is a no-op
35328
+ * (the existing handle is reused). Throws if `listen()` was never
35329
+ * called.
35330
+ */
35331
+ async connect() {
35332
+ await this.ensureConnected();
35333
+ }
35334
+ /**
35335
+ * Establish the bridge connection (role-election + host/peer handle
35336
+ * start + listener wiring) the first time a verb is invoked.
35337
+ * Idempotent after the connection is up; concurrent first-callers
35338
+ * share the same in-flight promise so only one election happens.
35339
+ *
35340
+ * Throws if `listen()` was never called — the contract is that the
35341
+ * MCP author still must wire `transport.start()` at boot to load
35342
+ * identity / set mcpId, even though the WS doesn't open until a
35343
+ * verb runs.
35344
+ */
35345
+ async ensureConnected() {
35346
+ if (this.hostHandle || this.peerHandle)
35347
+ return;
35348
+ if (this.connectingPromise) {
35349
+ await this.connectingPromise;
35350
+ return;
35351
+ }
35352
+ if (!this.identity || !this.mcpId) {
35353
+ throw new Error("FetchproxyServer: ensureConnected called before listen() \u2014 call listen() at MCP boot to load identity");
35354
+ }
35355
+ this.connectingPromise = this.doConnect();
35356
+ try {
35357
+ await this.connectingPromise;
35358
+ } finally {
35359
+ this.connectingPromise = null;
35360
+ }
35361
+ }
35362
+ async doConnect() {
35363
+ const identity = this.identity;
35364
+ const mcpId = this.mcpId;
35136
35365
  const el = await electRole({ host: this.opts.host, port: this.opts.port });
35137
35366
  if (el.role === "host") {
35138
35367
  this.role = "host";
35139
35368
  this.hostHandle = await startHost({
35140
35369
  httpServer: el.server,
35141
- ownIdentity: this.identity,
35142
- ownMcpId: this.mcpId,
35370
+ ownIdentity: identity,
35371
+ ownMcpId: mcpId,
35143
35372
  ownServerName: this.opts.serverName,
35144
35373
  ownVersion: this.opts.version,
35145
35374
  ownDomains: this.opts.domains,
@@ -35154,13 +35383,17 @@ var FetchproxyServer = class {
35154
35383
  onPairCode: this.opts.onPairCode
35155
35384
  });
35156
35385
  this.hostHandle.onOwnInner((inner) => this.onInner(inner));
35386
+ this.hostHandle.onExtensionDisconnect(() => this.rejectAllPending());
35387
+ this.hostHandle.onPendingPair((code) => {
35388
+ this.rejectAllPending(this.pairingErrorMessage(code));
35389
+ });
35157
35390
  } else {
35158
35391
  this.role = "peer";
35159
35392
  this.peerHandle = await startPeer({
35160
35393
  host: this.opts.host,
35161
35394
  port: this.opts.port,
35162
- identity: this.identity,
35163
- mcpId: this.mcpId,
35395
+ identity,
35396
+ mcpId,
35164
35397
  serverName: this.opts.serverName,
35165
35398
  version: this.opts.version,
35166
35399
  domains: this.opts.domains,
@@ -35174,8 +35407,19 @@ var FetchproxyServer = class {
35174
35407
  sessionStoragePointers: this.opts.sessionStoragePointers
35175
35408
  });
35176
35409
  this.peerHandle.onInner((inner) => this.onInner(inner));
35410
+ this.peerHandle.onRenegotiate(() => this.rejectAllPending());
35411
+ this.peerHandle.onPendingPair((code) => {
35412
+ this.rejectAllPending(this.pairingErrorMessage(code));
35413
+ });
35414
+ if (this.opts.onPairCode) {
35415
+ const cb = this.opts.onPairCode;
35416
+ this.peerHandle.onPendingPair((code) => cb(code));
35417
+ }
35177
35418
  }
35178
35419
  }
35420
+ pairingErrorMessage(code) {
35421
+ 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}`;
35422
+ }
35179
35423
  /**
35180
35424
  * Raw single-shot fetch through the bridge. Most callers should prefer
35181
35425
  * the verb shortcuts (`get` / `post` / `getJson` / `postJson` / `getHtml`)
@@ -35191,8 +35435,11 @@ var FetchproxyServer = class {
35191
35435
  * offline, etc.).
35192
35436
  */
35193
35437
  async fetch(init) {
35194
- if (!this.hostHandle && !this.peerHandle) {
35195
- throw new Error("FetchproxyServer.fetch called before listen() \u2014 not listening");
35438
+ await this.ensureConnected();
35439
+ const pendingCode = this.currentPendingPairCode();
35440
+ if (pendingCode !== null) {
35441
+ const error48 = this.pairingErrorMessage(pendingCode);
35442
+ return { ok: false, error: error48, kind: classifyFetchError(error48) };
35196
35443
  }
35197
35444
  const id = this.nextRequestId++;
35198
35445
  const inner = { type: "request", id, op: "fetch", init };
@@ -35333,9 +35580,8 @@ var FetchproxyServer = class {
35333
35580
  if (!this.opts.capabilities.includes("read_cookies")) {
35334
35581
  throw new Error('FetchproxyServer.readCookies(): MCP did not declare "read_cookies" in capabilities \u2014 add it to FetchproxyServerOpts.capabilities to enable this verb');
35335
35582
  }
35336
- if (!this.hostHandle && !this.peerHandle) {
35337
- throw new Error("FetchproxyServer.readCookies called before listen() \u2014 not listening");
35338
- }
35583
+ await this.ensureConnected();
35584
+ this.throwIfPendingPair();
35339
35585
  if (opts.subdomain !== void 0)
35340
35586
  assertSubdomainLabel(opts.subdomain);
35341
35587
  const baseDomain = this.resolveBaseDomain(opts.domain);
@@ -35392,9 +35638,8 @@ var FetchproxyServer = class {
35392
35638
  if (!this.opts.capabilities.includes(op)) {
35393
35639
  throw new Error(`FetchproxyServer.${op === "read_local_storage" ? "readLocalStorage" : "readSessionStorage"}(): MCP did not declare ${JSON.stringify(op)} in capabilities`);
35394
35640
  }
35395
- if (!this.hostHandle && !this.peerHandle) {
35396
- throw new Error(`FetchproxyServer.${op} called before listen() \u2014 not listening`);
35397
- }
35641
+ await this.ensureConnected();
35642
+ this.throwIfPendingPair();
35398
35643
  if (!Array.isArray(opts.keys) || opts.keys.length === 0) {
35399
35644
  throw new Error(`FetchproxyServer.${op}: opts.keys must be a non-empty array`);
35400
35645
  }
@@ -35449,9 +35694,8 @@ var FetchproxyServer = class {
35449
35694
  if (!this.opts.capabilities.includes("capture_request_header")) {
35450
35695
  throw new Error('FetchproxyServer.captureRequestHeader(): MCP did not declare "capture_request_header" in capabilities');
35451
35696
  }
35452
- if (!this.hostHandle && !this.peerHandle) {
35453
- throw new Error("FetchproxyServer.captureRequestHeader called before listen() \u2014 not listening");
35454
- }
35697
+ await this.ensureConnected();
35698
+ this.throwIfPendingPair();
35455
35699
  const declared = this.opts.captureHeaders.find((d) => d.urlPattern === opts.urlPattern && d.headerName === opts.headerName);
35456
35700
  if (!declared) {
35457
35701
  throw new Error(`FetchproxyServer.captureRequestHeader: (urlPattern=${JSON.stringify(opts.urlPattern)}, headerName=${JSON.stringify(opts.headerName)}) not declared in captureHeaders`);
@@ -35492,9 +35736,8 @@ var FetchproxyServer = class {
35492
35736
  if (!this.opts.capabilities.includes("read_indexed_db")) {
35493
35737
  throw new Error('FetchproxyServer.readIndexedDb(): MCP did not declare "read_indexed_db" in capabilities');
35494
35738
  }
35495
- if (!this.hostHandle && !this.peerHandle) {
35496
- throw new Error("FetchproxyServer.readIndexedDb called before listen() \u2014 not listening");
35497
- }
35739
+ await this.ensureConnected();
35740
+ this.throwIfPendingPair();
35498
35741
  if (!Array.isArray(opts.keys) || opts.keys.length === 0) {
35499
35742
  throw new Error("FetchproxyServer.readIndexedDb: opts.keys must be a non-empty array");
35500
35743
  }
@@ -35572,10 +35815,11 @@ var FetchproxyServer = class {
35572
35815
  if (inner.op === void 0 || inner.op === "fetch") {
35573
35816
  fetchCb({ ok: true, status: inner.status, url: inner.url, body: inner.body });
35574
35817
  } else {
35575
- fetchCb({ ok: false, error: `unexpected ${inner.op} response on fetch awaiter` });
35818
+ const error48 = `unexpected ${inner.op} response on fetch awaiter`;
35819
+ fetchCb({ ok: false, error: error48, kind: classifyFetchError(error48) });
35576
35820
  }
35577
35821
  } else {
35578
- fetchCb({ ok: false, error: inner.error });
35822
+ fetchCb({ ok: false, error: inner.error, kind: classifyFetchError(inner.error) });
35579
35823
  }
35580
35824
  return;
35581
35825
  }
@@ -35642,6 +35886,52 @@ var FetchproxyServer = class {
35642
35886
  }
35643
35887
  }
35644
35888
  }
35889
+ rejectAllPending(reason = "extension disconnected") {
35890
+ const err = new FetchproxyProtocolError(reason);
35891
+ for (const cb of this.pending.values()) {
35892
+ cb({ ok: false, error: err.message, kind: classifyFetchError(err.message) });
35893
+ }
35894
+ this.pending.clear();
35895
+ for (const cb of this.pendingReadCookies.values()) {
35896
+ cb({ ok: false, error: err.message });
35897
+ }
35898
+ this.pendingReadCookies.clear();
35899
+ for (const { reject } of this.pendingStorage.values())
35900
+ reject(err);
35901
+ this.pendingStorage.clear();
35902
+ for (const { reject } of this.pendingCapture.values())
35903
+ reject(err);
35904
+ this.pendingCapture.clear();
35905
+ for (const { reject } of this.pendingIdb.values())
35906
+ reject(err);
35907
+ this.pendingIdb.clear();
35908
+ }
35909
+ /**
35910
+ * 0.5.2+: read the current pair-pending pair code from whichever handle
35911
+ * is active, returning null when none is pending. Public verbs call this
35912
+ * at the top so that a tool invoked while the bridge is waiting on user
35913
+ * approval fails fast with the actionable error rather than hanging on a
35914
+ * sealed frame the extension will never process.
35915
+ */
35916
+ currentPendingPairCode() {
35917
+ if (this.hostHandle)
35918
+ return this.hostHandle.pendingPairCode();
35919
+ if (this.peerHandle)
35920
+ return this.peerHandle.pendingPairCode();
35921
+ return null;
35922
+ }
35923
+ /**
35924
+ * 0.5.2+: throw `FetchproxyProtocolError` with the actionable pair-code
35925
+ * message if the bridge is waiting on user approval. Used by the verb
35926
+ * methods (readCookies, readLocalStorage, etc.) that surface errors via
35927
+ * thrown exceptions rather than `ok:false` discriminated unions.
35928
+ */
35929
+ throwIfPendingPair() {
35930
+ const code = this.currentPendingPairCode();
35931
+ if (code !== null) {
35932
+ throw new FetchproxyProtocolError(this.pairingErrorMessage(code));
35933
+ }
35934
+ }
35645
35935
  /**
35646
35936
  * Shut down the bridge. Host: terminates the WebSocket server and any
35647
35937
  * still-attached extension/peer clients. Peer: closes the upstream
@@ -35649,6 +35939,10 @@ var FetchproxyServer = class {
35649
35939
  * twice in a row.
35650
35940
  */
35651
35941
  async close() {
35942
+ this.rejectAllPending();
35943
+ if (this.connectingPromise) {
35944
+ await this.connectingPromise.catch(() => void 0);
35945
+ }
35652
35946
  if (this.hostHandle)
35653
35947
  await this.hostHandle.close();
35654
35948
  if (this.peerHandle)
@@ -35656,6 +35950,7 @@ var FetchproxyServer = class {
35656
35950
  this.hostHandle = null;
35657
35951
  this.peerHandle = null;
35658
35952
  this.role = null;
35953
+ this.connectingPromise = null;
35659
35954
  }
35660
35955
  };
35661
35956
 
@@ -35744,8 +36039,7 @@ async function bootstrap(opts) {
35744
36039
  for (const p of localStoragePointers) {
35745
36040
  pointers[p.outputKey] = { storageKey: p.storageKey, jsonPointer: p.jsonPointer };
35746
36041
  }
35747
- const stub = server2;
35748
- localStorage = await stub.readLocalStorage({
36042
+ localStorage = await server2.readLocalStorage({
35749
36043
  keys: allKeys,
35750
36044
  ...storageDomainOpts,
35751
36045
  ...localStoragePointers.length > 0 ? { pointers } : {}
@@ -35758,8 +36052,7 @@ async function bootstrap(opts) {
35758
36052
  for (const p of sessionStoragePointers) {
35759
36053
  pointers[p.outputKey] = { storageKey: p.storageKey, jsonPointer: p.jsonPointer };
35760
36054
  }
35761
- const stub = server2;
35762
- sessionStorage = await stub.readSessionStorage({
36055
+ sessionStorage = await server2.readSessionStorage({
35763
36056
  keys: allKeys,
35764
36057
  ...storageDomainOpts,
35765
36058
  ...sessionStoragePointers.length > 0 ? { pointers } : {}
@@ -35782,9 +36075,6 @@ async function bootstrap(opts) {
35782
36075
  }
35783
36076
  const indexedDbBucket = {};
35784
36077
  for (const d of indexedDb) {
35785
- if (!server2.readIndexedDb) {
35786
- throw new Error("bootstrap: server factory does not implement readIndexedDb (declared indexedDb but server stub omits it)");
35787
- }
35788
36078
  const values = await server2.readIndexedDb({
35789
36079
  database: d.database,
35790
36080
  store: d.store,
@@ -35821,7 +36111,7 @@ var BootstrapDisabledError = class extends Error {
35821
36111
  // package.json
35822
36112
  var package_default = {
35823
36113
  name: "zola-mcp",
35824
- version: "1.1.3",
36114
+ version: "1.2.3",
35825
36115
  mcpName: "io.github.chrischall/zola-mcp",
35826
36116
  description: "Zola wedding MCP server for Claude",
35827
36117
  author: "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -35867,16 +36157,16 @@ var package_default = {
35867
36157
  "test:watch": "vitest"
35868
36158
  },
35869
36159
  dependencies: {
35870
- "@fetchproxy/bootstrap": "^0.4.2",
36160
+ "@fetchproxy/bootstrap": "^0.6.0",
35871
36161
  "@modelcontextprotocol/sdk": "^1.29.0",
35872
- dotenv: "^17.4.0"
36162
+ dotenv: "^17.4.2"
35873
36163
  },
35874
36164
  devDependencies: {
35875
- "@types/node": "^25.5.0",
35876
- "@vitest/coverage-v8": "^4.1.6",
36165
+ "@types/node": "^25.9.1",
36166
+ "@vitest/coverage-v8": "^4.1.7",
35877
36167
  esbuild: "^0.28.0",
35878
36168
  typescript: "^6.0.3",
35879
- vitest: "^4.1.6"
36169
+ vitest: "^4.1.7"
35880
36170
  }
35881
36171
  };
35882
36172
 
@@ -37618,7 +37908,8 @@ function registerRegistryItemTools(server2) {
37618
37908
  // src/index.ts
37619
37909
  var server = new McpServer({
37620
37910
  name: "zola-mcp",
37621
- version: "1.1.3"
37911
+ version: "1.2.3"
37912
+ // x-release-please-version
37622
37913
  });
37623
37914
  registerVendorTools(server);
37624
37915
  registerBudgetTools(server);
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ import { registerWebsiteThemeTools } from './tools/website-theme.js';
13
13
  import { registerRegistryItemTools } from './tools/registry-items.js';
14
14
  const server = new McpServer({
15
15
  name: 'zola-mcp',
16
- version: '1.1.3',
16
+ version: '1.2.3', // x-release-please-version
17
17
  });
18
18
  registerVendorTools(server);
19
19
  registerBudgetTools(server);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zola-mcp",
3
- "version": "1.1.3",
3
+ "version": "1.2.3",
4
4
  "mcpName": "io.github.chrischall/zola-mcp",
5
5
  "description": "Zola wedding MCP server for Claude",
6
6
  "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -46,15 +46,15 @@
46
46
  "test:watch": "vitest"
47
47
  },
48
48
  "dependencies": {
49
- "@fetchproxy/bootstrap": "^0.4.2",
49
+ "@fetchproxy/bootstrap": "^0.6.0",
50
50
  "@modelcontextprotocol/sdk": "^1.29.0",
51
- "dotenv": "^17.4.0"
51
+ "dotenv": "^17.4.2"
52
52
  },
53
53
  "devDependencies": {
54
- "@types/node": "^25.5.0",
55
- "@vitest/coverage-v8": "^4.1.6",
54
+ "@types/node": "^25.9.1",
55
+ "@vitest/coverage-v8": "^4.1.7",
56
56
  "esbuild": "^0.28.0",
57
57
  "typescript": "^6.0.3",
58
- "vitest": "^4.1.6"
58
+ "vitest": "^4.1.7"
59
59
  }
60
60
  }
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/chrischall/zola-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "1.1.3",
9
+ "version": "1.2.3",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "zola-mcp",
14
- "version": "1.1.3",
14
+ "version": "1.2.3",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },