zola-mcp 1.4.2 → 1.4.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.4.2"
10
+ "version": "1.4.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.4.2",
18
+ "version": "1.4.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.4.2",
4
+ "version": "1.4.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/dist/bundle.js CHANGED
@@ -12,7 +12,11 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
12
12
  throw Error('Dynamic require of "' + x + '" is not supported');
13
13
  });
14
14
  var __commonJS = (cb, mod) => function __require2() {
15
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
15
+ try {
16
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
17
+ } catch (e) {
18
+ throw mod = 0, e;
19
+ }
16
20
  };
17
21
  var __export = (target, all) => {
18
22
  for (var name in all)
@@ -34848,6 +34852,8 @@ var NonNegInt = external_exports.number().int().nonnegative();
34848
34852
  var NonEmptyString = external_exports.string().min(1);
34849
34853
  var IsoDate = external_exports.iso.date();
34850
34854
  var IsoTime = external_exports.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, "must be HH:MM (24h), e.g. 19:30");
34855
+ var NumericIdString = external_exports.string().regex(/^\d+$/, "must be a numeric id (digits only)").describe("A numeric id string (digits only), safe to interpolate into a URL path.");
34856
+ var SafePathSegment = external_exports.string().min(1).regex(/^[^/?#\s]+$/, 'must not contain "/", "?", "#", or whitespace').refine((v) => !v.includes(".."), 'must not contain ".."').describe('A single URL path segment, safe to interpolate: no "/", "..", "?", "#", or whitespace.');
34851
34857
  var schemaOrigin = external_exports.string().optional().describe("Portal origin (e.g. https://<vendor>.example.co) selecting which active session to use. Optional when only one session is active.");
34852
34858
  var schemaConfirm = external_exports.boolean().optional().describe("Must be true to proceed. Without this, the tool returns a preview.");
34853
34859
  var paginationSchema = {
@@ -35860,10 +35866,20 @@ async function openEncryptedFrame(sessionKey, frame) {
35860
35866
 
35861
35867
  // node_modules/@fetchproxy/server/dist/election.js
35862
35868
  import { createServer, Server as HttpServer } from "node:http";
35869
+ var DEFAULT_BIND_TIMEOUT_MS = 5e3;
35863
35870
  async function electRole(opts) {
35864
35871
  const server = createServer();
35872
+ const bindTimeoutMs = opts.bindTimeoutMs ?? DEFAULT_BIND_TIMEOUT_MS;
35865
35873
  return new Promise((resolve, reject) => {
35874
+ let timer;
35875
+ const clearBindTimer = () => {
35876
+ if (timer) {
35877
+ clearTimeout(timer);
35878
+ timer = void 0;
35879
+ }
35880
+ };
35866
35881
  const onError = (e) => {
35882
+ clearBindTimer();
35867
35883
  server.removeListener("listening", onListening);
35868
35884
  if (e.code === "EADDRINUSE") {
35869
35885
  try {
@@ -35876,11 +35892,25 @@ async function electRole(opts) {
35876
35892
  }
35877
35893
  };
35878
35894
  const onListening = () => {
35895
+ clearBindTimer();
35879
35896
  server.removeListener("error", onError);
35880
35897
  resolve({ role: "host", server });
35881
35898
  };
35882
35899
  server.once("error", onError);
35883
35900
  server.once("listening", onListening);
35901
+ if (bindTimeoutMs > 0) {
35902
+ timer = setTimeout(() => {
35903
+ server.removeListener("error", onError);
35904
+ server.removeListener("listening", onListening);
35905
+ try {
35906
+ server.close();
35907
+ } catch {
35908
+ }
35909
+ reject(new Error(`fetchproxy: bind to ${opts.host}:${opts.port} timed out after ${bindTimeoutMs}ms (port may be in a bad state)`));
35910
+ }, bindTimeoutMs);
35911
+ if (typeof timer.unref === "function")
35912
+ timer.unref();
35913
+ }
35884
35914
  server.listen(opts.port, opts.host);
35885
35915
  });
35886
35916
  }
@@ -35973,6 +36003,44 @@ var SessionState = class {
35973
36003
  }
35974
36004
  };
35975
36005
 
36006
+ // node_modules/@fetchproxy/server/dist/session-ready.js
36007
+ var SESSION_READY_TIMEOUT_MS = 3e4;
36008
+ var FetchproxySessionNotReadyError = class extends Error {
36009
+ reason;
36010
+ pairCode;
36011
+ mcpId;
36012
+ hint;
36013
+ constructor(info) {
36014
+ const pairing = info.pairCode !== null && info.pairCode !== "";
36015
+ const hint = pairing ? `Open the Transporter extension popup and approve pair code ${info.pairCode} for "${info.mcpId}", then retry.` : `The extension is connected but hasn't confirmed a session for "${info.mcpId}" \u2014 sign in to the target site in that browser (and approve the requested scope if it changed), then retry.`;
36016
+ super(`fetchproxy: ${pairing ? "pairing not yet approved" : "no confirmed browser session"} for "${info.mcpId}". ${hint}`);
36017
+ this.name = "FetchproxySessionNotReadyError";
36018
+ this.reason = pairing ? "pair-required" : "not-ready";
36019
+ this.pairCode = pairing ? info.pairCode : null;
36020
+ this.mcpId = info.mcpId;
36021
+ this.hint = hint;
36022
+ Object.setPrototypeOf(this, new.target.prototype);
36023
+ }
36024
+ };
36025
+ async function awaitSessionReady(ready, opts) {
36026
+ const ms = opts.timeoutMs ?? SESSION_READY_TIMEOUT_MS;
36027
+ if (ms <= 0)
36028
+ return ready;
36029
+ let timer;
36030
+ const timeout = new Promise((_, reject) => {
36031
+ timer = setTimeout(() => {
36032
+ reject(new FetchproxySessionNotReadyError({ mcpId: opts.mcpId, pairCode: opts.pendingPairCode() }));
36033
+ }, ms);
36034
+ timer.unref?.();
36035
+ });
36036
+ try {
36037
+ return await Promise.race([ready, timeout]);
36038
+ } finally {
36039
+ if (timer)
36040
+ clearTimeout(timer);
36041
+ }
36042
+ }
36043
+
35976
36044
  // node_modules/@fetchproxy/server/dist/host.js
35977
36045
  var PUBLIC_ORIGIN_RE = /^https?:\/\/(?!(127\.0\.0\.1|localhost)(:|$))/i;
35978
36046
  var enc2 = new TextEncoder();
@@ -36060,6 +36128,29 @@ async function startHost(opts) {
36060
36128
  return;
36061
36129
  }
36062
36130
  if (frame.type === "hello" && frame.role === "server") {
36131
+ const peerEdPub = fromB64(frame.identityEd25519Pub);
36132
+ const peerSigMsg = concatBytes(enc2.encode(frame.mcpId), fromB64(frame.sessionNonce));
36133
+ const peerSig = fromB64(frame.sessionSig);
36134
+ let peerSigOk = false;
36135
+ try {
36136
+ peerSigOk = await ed25519Verify(peerEdPub, peerSigMsg, peerSig);
36137
+ } catch {
36138
+ peerSigOk = false;
36139
+ }
36140
+ if (!peerSigOk) {
36141
+ console.warn("[fetchproxy] peer hello signature invalid \u2014 refusing registration (possible squatter)");
36142
+ ws.close(1008, "peer hello signature invalid");
36143
+ return;
36144
+ }
36145
+ const existing = peers.get(frame.mcpId);
36146
+ if (existing && existing.ws !== ws) {
36147
+ const existingEdPub = existing.helloFrame.identityEd25519Pub;
36148
+ if (existingEdPub !== frame.identityEd25519Pub) {
36149
+ console.warn("[fetchproxy] peer mcpId already mapped to a different identity \u2014 refusing (mcpId squatting)");
36150
+ ws.close(1008, "mcpId already registered to another identity");
36151
+ return;
36152
+ }
36153
+ }
36063
36154
  identified = "peer";
36064
36155
  peerMcpId = frame.mcpId;
36065
36156
  peers.set(frame.mcpId, { ws, helloFrame: frame });
@@ -36152,8 +36243,10 @@ async function startHost(opts) {
36152
36243
  resetSessionPromise();
36153
36244
  disconnectListeners.forEach((cb) => cb());
36154
36245
  }
36155
- if (identified === "peer" && peerMcpId)
36156
- peers.delete(peerMcpId);
36246
+ if (identified === "peer" && peerMcpId) {
36247
+ if (peers.get(peerMcpId)?.ws === ws)
36248
+ peers.delete(peerMcpId);
36249
+ }
36157
36250
  });
36158
36251
  });
36159
36252
  return {
@@ -36169,7 +36262,10 @@ async function startHost(opts) {
36169
36262
  });
36170
36263
  }),
36171
36264
  sendOwnInner: async (inner) => {
36172
- const session = await ownSessionReady;
36265
+ const session = await awaitSessionReady(ownSessionReady, {
36266
+ mcpId: opts.ownMcpId,
36267
+ pendingPairCode: () => ownPendingPairCode
36268
+ });
36173
36269
  if (!extensionWs)
36174
36270
  throw new Error("host: no extension connected");
36175
36271
  const sealed = await sealInnerFrame(session.sessionKey, opts.ownMcpId, session.nextOutboundSeq(), inner);
@@ -36274,7 +36370,10 @@ async function startPeer(opts) {
36274
36370
  ws,
36275
36371
  session: sessionPromise,
36276
36372
  sendInner: async (inner) => {
36277
- await sessionPromise;
36373
+ await awaitSessionReady(sessionPromise, {
36374
+ mcpId: opts.mcpId,
36375
+ pendingPairCode: () => pendingPairCode
36376
+ });
36278
36377
  const s = session;
36279
36378
  const sealed = await sealInnerFrame(s.sessionKey, opts.mcpId, s.nextOutboundSeq(), inner);
36280
36379
  ws.send(JSON.stringify(sealed));
@@ -36940,6 +37039,35 @@ var FetchproxyServer = class {
36940
37039
  this.keepAliveTimer = null;
36941
37040
  }
36942
37041
  }
37042
+ /**
37043
+ * Send an inner request frame via whichever bridge handle is active. If the
37044
+ * send throws (e.g. `FetchproxySessionNotReadyError` — the session never
37045
+ * confirmed), the frame never reached the bridge, so no reply will arrive:
37046
+ * drop the just-registered pending resolver for this id (it lives in exactly
37047
+ * one of the op maps — request ids are unique) so it doesn't leak until the
37048
+ * server closes, then rethrow.
37049
+ */
37050
+ async sendInnerFrame(inner) {
37051
+ try {
37052
+ if (this.hostHandle) {
37053
+ await this.hostHandle.sendOwnInner(inner);
37054
+ } else if (this.peerHandle) {
37055
+ await this.peerHandle.sendInner(inner);
37056
+ }
37057
+ } catch (err) {
37058
+ if ("id" in inner && typeof inner.id === "number") {
37059
+ const { id } = inner;
37060
+ this.pending.delete(id);
37061
+ this.pendingReadCookies.delete(id);
37062
+ this.pendingStorage.delete(id);
37063
+ this.pendingCapture.delete(id);
37064
+ this.pendingRedirect.delete(id);
37065
+ this.pendingDownload.delete(id);
37066
+ this.pendingIdb.delete(id);
37067
+ }
37068
+ throw err;
37069
+ }
37070
+ }
36943
37071
  /**
36944
37072
  * Single bridge round-trip, wrapped by `fetchTimeoutMs` when set.
36945
37073
  * On timeout returns the `{ok:false, kind:'timeout'}` envelope —
@@ -36951,11 +37079,7 @@ var FetchproxyServer = class {
36951
37079
  const pending = new Promise((resolve) => {
36952
37080
  this.pending.set(id, resolve);
36953
37081
  });
36954
- if (this.hostHandle) {
36955
- await this.hostHandle.sendOwnInner(inner);
36956
- } else if (this.peerHandle) {
36957
- await this.peerHandle.sendInner(inner);
36958
- }
37082
+ await this.sendInnerFrame(inner);
36959
37083
  const timeoutMs = this.opts.fetchTimeoutMs;
36960
37084
  if (timeoutMs === void 0 || timeoutMs <= 0)
36961
37085
  return pending;
@@ -36984,6 +37108,51 @@ var FetchproxyServer = class {
36984
37108
  clearTimeout(timer);
36985
37109
  }
36986
37110
  }
37111
+ /**
37112
+ * FP-B2: bound a non-`fetch` verb's reply wait by `fetchTimeoutMs`.
37113
+ *
37114
+ * Before this, only `fetch()` raced its `pending` reply against a timer
37115
+ * (`_fetchOnceWithTimeout`); `readCookies`, the storage reads,
37116
+ * `capture_request_header`, `capture_redirect`, `download`, and
37117
+ * `read_indexed_db` awaited their `pending` promise with no race, so a
37118
+ * wedged extension hung the tool call indefinitely.
37119
+ *
37120
+ * `pending` is the already-registered reply promise. `pendingMap`/`id`
37121
+ * point at the op-specific map entry so we can drop it on expiry exactly
37122
+ * as `_fetchOnceWithTimeout` does — otherwise a late bridge reply would
37123
+ * resolve into a stale resolver / leak. On timeout we reject with a
37124
+ * `FetchproxyTimeoutError` (the same throwable the convenience methods
37125
+ * already surface for fetch timeouts). `0`/unset opts out (unbounded),
37126
+ * matching the fetch path.
37127
+ */
37128
+ async _withVerbTimeout(pending, pendingMap, id, url2) {
37129
+ const timeoutMs = this.opts.fetchTimeoutMs;
37130
+ if (timeoutMs === void 0 || timeoutMs <= 0)
37131
+ return pending;
37132
+ let timer;
37133
+ const start = Date.now();
37134
+ try {
37135
+ return await Promise.race([
37136
+ pending,
37137
+ new Promise((_resolve, reject) => {
37138
+ timer = setTimeout(() => {
37139
+ pendingMap.delete(id);
37140
+ reject(new FetchproxyTimeoutError({
37141
+ url: url2,
37142
+ timeoutMs,
37143
+ role: this.role,
37144
+ port: this.opts.port,
37145
+ elapsedMs: Date.now() - start,
37146
+ retryAttempted: false
37147
+ }));
37148
+ }, timeoutMs);
37149
+ })
37150
+ ]);
37151
+ } finally {
37152
+ if (timer)
37153
+ clearTimeout(timer);
37154
+ }
37155
+ }
36987
37156
  /**
36988
37157
  * Map an `ok:false` fetch result to its typed throwable. Centralizes
36989
37158
  * the kind-to-error-class switch so `request()` and (via the same
@@ -37274,12 +37443,8 @@ var FetchproxyServer = class {
37274
37443
  const pending = new Promise((resolve) => {
37275
37444
  this.pendingReadCookies.set(id, resolve);
37276
37445
  });
37277
- if (this.hostHandle) {
37278
- await this.hostHandle.sendOwnInner(inner);
37279
- } else if (this.peerHandle) {
37280
- await this.peerHandle.sendInner(inner);
37281
- }
37282
- const result = await pending;
37446
+ await this.sendInnerFrame(inner);
37447
+ const result = await this._withVerbTimeout(pending, this.pendingReadCookies, id, `https://${host}`);
37283
37448
  if (!result.ok) {
37284
37449
  throw new FetchproxyProtocolError(result.error);
37285
37450
  }
@@ -37345,12 +37510,8 @@ var FetchproxyServer = class {
37345
37510
  const pending = new Promise((resolve, reject) => {
37346
37511
  this.pendingStorage.set(id, { resolve, reject });
37347
37512
  });
37348
- if (this.hostHandle) {
37349
- await this.hostHandle.sendOwnInner(inner);
37350
- } else if (this.peerHandle) {
37351
- await this.peerHandle.sendInner(inner);
37352
- }
37353
- return pending;
37513
+ await this.sendInnerFrame(inner);
37514
+ return this._withVerbTimeout(pending, this.pendingStorage, id, `https://${host}`);
37354
37515
  }
37355
37516
  /**
37356
37517
  * 0.3.0+: snapshot the next outgoing request's named header. Single-
@@ -37454,12 +37615,8 @@ var FetchproxyServer = class {
37454
37615
  const pending = new Promise((resolve, reject) => {
37455
37616
  this.pendingCapture.set(id, { resolve, reject });
37456
37617
  });
37457
- if (this.hostHandle) {
37458
- await this.hostHandle.sendOwnInner(inner);
37459
- } else if (this.peerHandle) {
37460
- await this.peerHandle.sendInner(inner);
37461
- }
37462
- return pending;
37618
+ await this.sendInnerFrame(inner);
37619
+ return this._withVerbTimeout(pending, this.pendingCapture, id, `https://${opts.host}${opts.path ?? "/*"}`);
37463
37620
  }
37464
37621
  /**
37465
37622
  * Snapshot the redirect target URL of the next request the browser
@@ -37543,12 +37700,8 @@ var FetchproxyServer = class {
37543
37700
  const pending = new Promise((resolve, reject) => {
37544
37701
  this.pendingRedirect.set(id, { resolve, reject });
37545
37702
  });
37546
- if (this.hostHandle) {
37547
- await this.hostHandle.sendOwnInner(inner);
37548
- } else if (this.peerHandle) {
37549
- await this.peerHandle.sendInner(inner);
37550
- }
37551
- return pending;
37703
+ await this.sendInnerFrame(inner);
37704
+ return this._withVerbTimeout(pending, this.pendingRedirect, id, `https://${opts.host}${opts.path ?? "/*"}`);
37552
37705
  }
37553
37706
  /**
37554
37707
  * Download `url` through the BROWSER's own network stack via
@@ -37629,12 +37782,8 @@ var FetchproxyServer = class {
37629
37782
  const pending = new Promise((resolve, reject) => {
37630
37783
  this.pendingDownload.set(id, { resolve, reject });
37631
37784
  });
37632
- if (this.hostHandle) {
37633
- await this.hostHandle.sendOwnInner(inner);
37634
- } else if (this.peerHandle) {
37635
- await this.peerHandle.sendInner(inner);
37636
- }
37637
- return pending;
37785
+ await this.sendInnerFrame(inner);
37786
+ return this._withVerbTimeout(pending, this.pendingDownload, id, opts.url);
37638
37787
  }
37639
37788
  /**
37640
37789
  * 0.4.0+: read declared IndexedDB keys from the user's signed-in
@@ -37681,12 +37830,8 @@ var FetchproxyServer = class {
37681
37830
  const pending = new Promise((resolve, reject) => {
37682
37831
  this.pendingIdb.set(id, { resolve, reject });
37683
37832
  });
37684
- if (this.hostHandle) {
37685
- await this.hostHandle.sendOwnInner(inner);
37686
- } else if (this.peerHandle) {
37687
- await this.peerHandle.sendInner(inner);
37688
- }
37689
- return pending;
37833
+ await this.sendInnerFrame(inner);
37834
+ return this._withVerbTimeout(pending, this.pendingIdb, id, origin);
37690
37835
  }
37691
37836
  assertScopeSubset(requested, declared, label) {
37692
37837
  const undeclared = undeclaredKeys(requested, declared);
@@ -38084,7 +38229,7 @@ var BootstrapDisabledError = class extends Error {
38084
38229
  // package.json
38085
38230
  var package_default = {
38086
38231
  name: "zola-mcp",
38087
- version: "1.4.2",
38232
+ version: "1.4.3",
38088
38233
  mcpName: "io.github.chrischall/zola-mcp",
38089
38234
  description: "Zola wedding MCP server for Claude",
38090
38235
  author: "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -38130,7 +38275,7 @@ var package_default = {
38130
38275
  "test:watch": "vitest"
38131
38276
  },
38132
38277
  dependencies: {
38133
- "@chrischall/mcp-utils": "^0.7.0",
38278
+ "@chrischall/mcp-utils": "^0.10.3",
38134
38279
  "@fetchproxy/bootstrap": "^1.3.0",
38135
38280
  "@fetchproxy/server": "^1.3.0",
38136
38281
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -38138,7 +38283,7 @@ var package_default = {
38138
38283
  zod: "^4.4.3"
38139
38284
  },
38140
38285
  devDependencies: {
38141
- "@types/node": "^25.9.2",
38286
+ "@types/node": "^26.0.0",
38142
38287
  "@vitest/coverage-v8": "^4.1.8",
38143
38288
  esbuild: "^0.28.0",
38144
38289
  typescript: "^6.0.3",
@@ -40303,7 +40448,7 @@ function registerEventInvitationTools(server) {
40303
40448
  }
40304
40449
 
40305
40450
  // src/index.ts
40306
- var VERSION = "1.4.2";
40451
+ var VERSION = "1.4.3";
40307
40452
  await runMcp({
40308
40453
  name: "zola-mcp",
40309
40454
  version: VERSION,
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import { registerWebsiteThemeTools } from './tools/website-theme.js';
12
12
  import { registerRegistryItemTools } from './tools/registry-items.js';
13
13
  import { registerInvitationTools } from './tools/invitations.js';
14
14
  import { registerEventInvitationTools } from './tools/event-invitations.js';
15
- const VERSION = '1.4.2'; // x-release-please-version
15
+ const VERSION = '1.4.3'; // x-release-please-version
16
16
  await runMcp({
17
17
  name: 'zola-mcp',
18
18
  version: VERSION,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zola-mcp",
3
- "version": "1.4.2",
3
+ "version": "1.4.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,7 +46,7 @@
46
46
  "test:watch": "vitest"
47
47
  },
48
48
  "dependencies": {
49
- "@chrischall/mcp-utils": "^0.7.0",
49
+ "@chrischall/mcp-utils": "^0.10.3",
50
50
  "@fetchproxy/bootstrap": "^1.3.0",
51
51
  "@fetchproxy/server": "^1.3.0",
52
52
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -54,7 +54,7 @@
54
54
  "zod": "^4.4.3"
55
55
  },
56
56
  "devDependencies": {
57
- "@types/node": "^25.9.2",
57
+ "@types/node": "^26.0.0",
58
58
  "@vitest/coverage-v8": "^4.1.8",
59
59
  "esbuild": "^0.28.0",
60
60
  "typescript": "^6.0.3",
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.4.2",
9
+ "version": "1.4.3",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "zola-mcp",
14
- "version": "1.4.2",
14
+ "version": "1.4.3",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },