zola-mcp 1.4.3 → 1.4.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.
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "metadata": {
9
9
  "description": "Zola wedding planning tools for Claude Code",
10
- "version": "1.4.3"
10
+ "version": "1.4.4"
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.3",
18
+ "version": "1.4.4",
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.3",
4
+ "version": "1.4.4",
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
@@ -34728,8 +34728,12 @@ var API_KEY_RE = new RegExp([
34728
34728
  // webhook signing secret (Stripe-style)
34729
34729
  ].map((p) => `\\b${p}`).join("|"), "g");
34730
34730
  var QUERY_SECRET_RE = /([?&](?:access_token|refresh_token|client_secret|api_?key|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
34731
+ var AWS_SIGV4_RE = /([?&]X-Amz-(?:Signature|Security-Token|Credential)=)[^&#\s"'<>`]+/gi;
34732
+ var JSON_SECRET_KEYS = "access_token|refresh_token|client_secret|api_?key|password|passwd|secret|token";
34733
+ var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")[^"]*(")`, "gi");
34734
+ var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')[^']*(')`, "gi");
34731
34735
  function redactSecrets(text) {
34732
- return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(JWT_RE, "[REDACTED]");
34736
+ return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JWT_RE, "[REDACTED]");
34733
34737
  }
34734
34738
  function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX) {
34735
34739
  const str = text === null || text === void 0 ? "" : String(text);
@@ -34865,6 +34869,41 @@ var pageSchema = {
34865
34869
  page_size: external_exports.number().int().min(1).max(200).default(50).describe("Number of items per page (1-200).")
34866
34870
  };
34867
34871
 
34872
+ // node_modules/@chrischall/mcp-utils/dist/auth/cached-token.js
34873
+ function createCachedTokenSource(opts) {
34874
+ const now = opts.now ?? Date.now;
34875
+ const bufferMs = opts.bufferMs ?? 6e4;
34876
+ const defaultTtlMs = opts.defaultTtlMs ?? 36e5;
34877
+ let cached2 = null;
34878
+ let inFlight = null;
34879
+ const expiryOf = (minted) => {
34880
+ if (minted.expiresAt !== void 0) {
34881
+ return minted.expiresAt instanceof Date ? minted.expiresAt.getTime() : minted.expiresAt;
34882
+ }
34883
+ return now() + (minted.ttlMs ?? defaultTtlMs);
34884
+ };
34885
+ return {
34886
+ getToken() {
34887
+ if (cached2 && cached2.expiresAt - bufferMs > now())
34888
+ return Promise.resolve(cached2.token);
34889
+ if (inFlight)
34890
+ return inFlight;
34891
+ const p = opts.mint().then((minted) => {
34892
+ cached2 = { token: minted.token, expiresAt: expiryOf(minted) };
34893
+ return minted.token;
34894
+ }).finally(() => {
34895
+ if (inFlight === p)
34896
+ inFlight = null;
34897
+ });
34898
+ inFlight = p;
34899
+ return p;
34900
+ },
34901
+ invalidate() {
34902
+ cached2 = null;
34903
+ }
34904
+ };
34905
+ }
34906
+
34868
34907
  // node_modules/@chrischall/mcp-utils/dist/auth/index.js
34869
34908
  function bridgeDownHintOf(err) {
34870
34909
  if (err instanceof Error && err.name === "FetchproxyBridgeDownError") {
@@ -38229,7 +38268,7 @@ var BootstrapDisabledError = class extends Error {
38229
38268
  // package.json
38230
38269
  var package_default = {
38231
38270
  name: "zola-mcp",
38232
- version: "1.4.3",
38271
+ version: "1.4.4",
38233
38272
  mcpName: "io.github.chrischall/zola-mcp",
38234
38273
  description: "Zola wedding MCP server for Claude",
38235
38274
  author: "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -38275,7 +38314,7 @@ var package_default = {
38275
38314
  "test:watch": "vitest"
38276
38315
  },
38277
38316
  dependencies: {
38278
- "@chrischall/mcp-utils": "^0.10.3",
38317
+ "@chrischall/mcp-utils": "^0.12.0",
38279
38318
  "@fetchproxy/bootstrap": "^1.3.0",
38280
38319
  "@fetchproxy/server": "^1.3.0",
38281
38320
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -38328,18 +38367,26 @@ async function resolveRefreshToken() {
38328
38367
  var __dirname = dirname(fileURLToPath(import.meta.url));
38329
38368
  await loadDotenvSafely({ path: join2(__dirname, "..", ".env"), override: false });
38330
38369
  var MOBILE_BASE_URL = "https://mobile-api.zola.com";
38370
+ var SESSION_REFRESH_BUFFER_MS = 5 * 60 * 1e3;
38331
38371
  var ZolaClient = class {
38332
- sessionToken = null;
38333
- sessionExpiry = null;
38334
38372
  cachedContext = null;
38335
38373
  // WAF requires x-zola-session-id on all mobile-api.zola.com requests
38336
38374
  deviceSessionId = crypto.randomUUID().toUpperCase();
38375
+ // The `ZOLA_SESSION_TOKEN` seed is a cold-start-only shortcut, exactly as the
38376
+ // old `ensureSession` gated it on `sessionToken === null`; a 401 re-mint goes
38377
+ // straight to `refresh()`, never back to the env token.
38378
+ triedEnvSeed = false;
38379
+ // Single-flight cached mint: caches the 30-min session JWT until 5 min before
38380
+ // its `exp`, coalesces concurrent mints, and re-mints on demand after a 401.
38381
+ session = createCachedTokenSource({
38382
+ mint: () => this.mintSession(),
38383
+ bufferMs: SESSION_REFRESH_BUFFER_MS
38384
+ });
38337
38385
  /**
38338
38386
  * Make a request to the Zola mobile API (mobile-api.zola.com).
38339
38387
  * Uses Bearer JWT auth with x-zola-session-id header.
38340
38388
  */
38341
38389
  async requestMobile(method, path, body) {
38342
- await this.ensureSession();
38343
38390
  return this.doRequest(method, path, body);
38344
38391
  }
38345
38392
  /**
@@ -38349,7 +38396,6 @@ var ZolaClient = class {
38349
38396
  * raw bytes and the server's content-type so callers can pass it through.
38350
38397
  */
38351
38398
  async requestMobileBinary(method, path, body) {
38352
- await this.ensureSession();
38353
38399
  const response = await this.sendWithRetry(method, path, body, false, false, "*/*");
38354
38400
  const contentType = response.headers.get("content-type") ?? "application/octet-stream";
38355
38401
  const bytes = new Uint8Array(await response.arrayBuffer());
@@ -38393,10 +38439,11 @@ var ZolaClient = class {
38393
38439
  return text ? JSON.parse(text) : null;
38394
38440
  }
38395
38441
  async sendWithRetry(method, path, body, isAuthRetry = false, isRateRetry = false, accept = "application/json") {
38396
- const sessionId = decodeJwtSessionId(this.sessionToken);
38442
+ const token = await this.session.getToken();
38443
+ const sessionId = decodeJwtSessionId(token);
38397
38444
  const headers = {
38398
38445
  accept,
38399
- authorization: `Bearer ${this.sessionToken}`,
38446
+ authorization: `Bearer ${token}`,
38400
38447
  "x-zola-platform-type": "iphone_app",
38401
38448
  "x-zola-session-id": this.deviceSessionId,
38402
38449
  "user-agent": "Zola/42.5.0 (iPad; iOS 26.4; Scale/2.0)",
@@ -38409,9 +38456,7 @@ var ZolaClient = class {
38409
38456
  ...body !== void 0 ? { body: JSON.stringify(body) } : {}
38410
38457
  });
38411
38458
  if (response.status === 401 && !isAuthRetry) {
38412
- this.sessionToken = null;
38413
- this.sessionExpiry = null;
38414
- await this.refresh();
38459
+ this.session.invalidate();
38415
38460
  return this.sendWithRetry(method, path, body, true, isRateRetry, accept);
38416
38461
  }
38417
38462
  if (response.status === 429) {
@@ -38429,30 +38474,33 @@ var ZolaClient = class {
38429
38474
  }
38430
38475
  return response;
38431
38476
  }
38432
- async ensureSession() {
38433
- if (this.sessionToken && this.sessionExpiry) {
38434
- if (this.sessionExpiry.getTime() - Date.now() > 5 * 60 * 1e3) return;
38435
- }
38436
- if (this.sessionToken === null) {
38477
+ /**
38478
+ * Mint a session token for the cache. On the very first mint, use a valid
38479
+ * `ZOLA_SESSION_TOKEN` if present (a cold-start shortcut that skips the
38480
+ * initial refresh); otherwise, and on every later mint, refresh via the
38481
+ * mobile API. Returns `{ token, expiresAt }` so the cache serves it until
38482
+ * `SESSION_REFRESH_BUFFER_MS` before the JWT's `exp`.
38483
+ */
38484
+ async mintSession() {
38485
+ if (!this.triedEnvSeed) {
38486
+ this.triedEnvSeed = true;
38437
38487
  const envSession = readEnvVar("ZOLA_SESSION_TOKEN");
38438
38488
  if (envSession) {
38439
38489
  try {
38440
38490
  const exp = decodeJwtExp(envSession);
38441
- if (exp * 1e3 - Date.now() > 5 * 60 * 1e3) {
38442
- this.sessionToken = envSession;
38443
- this.sessionExpiry = new Date(exp * 1e3);
38444
- return;
38491
+ if (exp * 1e3 - Date.now() > SESSION_REFRESH_BUFFER_MS) {
38492
+ return { token: envSession, expiresAt: exp * 1e3 };
38445
38493
  }
38446
38494
  } catch {
38447
38495
  }
38448
38496
  }
38449
38497
  }
38450
- await this.refresh();
38498
+ return this.refresh();
38451
38499
  }
38452
38500
  /**
38453
38501
  * Refresh the session using the mobile API endpoint.
38454
38502
  * POST /v3/sessions/refresh with the refresh token JWT.
38455
- * Returns a new session_token (30-min) and optionally a new refresh_token.
38503
+ * Returns a new session_token (30-min) as a {@link MintedToken}.
38456
38504
  *
38457
38505
  * The refresh token comes from `resolveRefreshToken()`, which tries
38458
38506
  * (1) ZOLA_REFRESH_TOKEN env var, then (2) the fetchproxy 0.3.0 extension's
@@ -38481,8 +38529,7 @@ To fix: set ZOLA_REFRESH_TOKEN, or install the fetchproxy extension and sign int
38481
38529
  const result = await response.json();
38482
38530
  const { session_token } = result.data;
38483
38531
  const exp = decodeJwtExp(session_token);
38484
- this.sessionToken = session_token;
38485
- this.sessionExpiry = new Date(exp * 1e3);
38532
+ return { token: session_token, expiresAt: exp * 1e3 };
38486
38533
  }
38487
38534
  };
38488
38535
  var client = new ZolaClient();
@@ -40448,7 +40495,7 @@ function registerEventInvitationTools(server) {
40448
40495
  }
40449
40496
 
40450
40497
  // src/index.ts
40451
- var VERSION = "1.4.3";
40498
+ var VERSION = "1.4.4";
40452
40499
  await runMcp({
40453
40500
  name: "zola-mcp",
40454
40501
  version: VERSION,
package/dist/client.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { dirname, join } from 'path';
2
2
  import { fileURLToPath } from 'url';
3
- import { readEnvVar, loadDotenvSafely, decodeJwtExp, decodeJwtSessionId, formatApiError, truncateErrorMessage, } from '@chrischall/mcp-utils';
3
+ import { readEnvVar, loadDotenvSafely, decodeJwtExp, decodeJwtSessionId, formatApiError, truncateErrorMessage, createCachedTokenSource, } from '@chrischall/mcp-utils';
4
4
  import { resolveRefreshToken } from './auth.js';
5
5
  // Load `.env` next to the compiled entry point. `loadDotenvSafely` is a
6
6
  // no-throw loader: in bundled mode (no resolvable `dotenv`) it returns false
@@ -8,18 +8,29 @@ import { resolveRefreshToken } from './auth.js';
8
8
  const __dirname = dirname(fileURLToPath(import.meta.url));
9
9
  await loadDotenvSafely({ path: join(__dirname, '..', '.env'), override: false });
10
10
  const MOBILE_BASE_URL = 'https://mobile-api.zola.com';
11
+ // Refresh the session token this long before its JWT `exp` — the same 5-minute
12
+ // comfort margin the hand-rolled cache used for both the "still valid" check and
13
+ // the `ZOLA_SESSION_TOKEN` seed's freshness gate.
14
+ const SESSION_REFRESH_BUFFER_MS = 5 * 60 * 1000;
11
15
  export class ZolaClient {
12
- sessionToken = null;
13
- sessionExpiry = null;
14
16
  cachedContext = null;
15
17
  // WAF requires x-zola-session-id on all mobile-api.zola.com requests
16
18
  deviceSessionId = crypto.randomUUID().toUpperCase();
19
+ // The `ZOLA_SESSION_TOKEN` seed is a cold-start-only shortcut, exactly as the
20
+ // old `ensureSession` gated it on `sessionToken === null`; a 401 re-mint goes
21
+ // straight to `refresh()`, never back to the env token.
22
+ triedEnvSeed = false;
23
+ // Single-flight cached mint: caches the 30-min session JWT until 5 min before
24
+ // its `exp`, coalesces concurrent mints, and re-mints on demand after a 401.
25
+ session = createCachedTokenSource({
26
+ mint: () => this.mintSession(),
27
+ bufferMs: SESSION_REFRESH_BUFFER_MS,
28
+ });
17
29
  /**
18
30
  * Make a request to the Zola mobile API (mobile-api.zola.com).
19
31
  * Uses Bearer JWT auth with x-zola-session-id header.
20
32
  */
21
33
  async requestMobile(method, path, body) {
22
- await this.ensureSession();
23
34
  return this.doRequest(method, path, body);
24
35
  }
25
36
  /**
@@ -29,7 +40,6 @@ export class ZolaClient {
29
40
  * raw bytes and the server's content-type so callers can pass it through.
30
41
  */
31
42
  async requestMobileBinary(method, path, body) {
32
- await this.ensureSession();
33
43
  const response = await this.sendWithRetry(method, path, body, false, false, '*/*');
34
44
  const contentType = response.headers.get('content-type') ?? 'application/octet-stream';
35
45
  const bytes = new Uint8Array(await response.arrayBuffer());
@@ -75,10 +85,14 @@ export class ZolaClient {
75
85
  return (text ? JSON.parse(text) : null);
76
86
  }
77
87
  async sendWithRetry(method, path, body, isAuthRetry = false, isRateRetry = false, accept = 'application/json') {
78
- const sessionId = decodeJwtSessionId(this.sessionToken);
88
+ // Current session token from the cache (mints/refreshes as needed). The
89
+ // per-request `x-zola-user-session-id` is derived from it every call, so a
90
+ // re-minted token below carries its own session id on the replay.
91
+ const token = await this.session.getToken();
92
+ const sessionId = decodeJwtSessionId(token);
79
93
  const headers = {
80
94
  accept,
81
- authorization: `Bearer ${this.sessionToken}`,
95
+ authorization: `Bearer ${token}`,
82
96
  'x-zola-platform-type': 'iphone_app',
83
97
  'x-zola-session-id': this.deviceSessionId,
84
98
  'user-agent': 'Zola/42.5.0 (iPad; iOS 26.4; Scale/2.0)',
@@ -92,9 +106,8 @@ export class ZolaClient {
92
106
  ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
93
107
  });
94
108
  if (response.status === 401 && !isAuthRetry) {
95
- this.sessionToken = null;
96
- this.sessionExpiry = null;
97
- await this.refresh();
109
+ // Drop the cached token so the replay's getToken re-mints via refresh().
110
+ this.session.invalidate();
98
111
  return this.sendWithRetry(method, path, body, true, isRateRetry, accept);
99
112
  }
100
113
  if (response.status === 429) {
@@ -110,22 +123,24 @@ export class ZolaClient {
110
123
  }
111
124
  return response;
112
125
  }
113
- async ensureSession() {
114
- // Session still valid with comfortable margin
115
- if (this.sessionToken && this.sessionExpiry) {
116
- if (this.sessionExpiry.getTime() - Date.now() > 5 * 60 * 1000)
117
- return;
118
- }
119
- // Check for session token in env (first load only)
120
- if (this.sessionToken === null) {
126
+ /**
127
+ * Mint a session token for the cache. On the very first mint, use a valid
128
+ * `ZOLA_SESSION_TOKEN` if present (a cold-start shortcut that skips the
129
+ * initial refresh); otherwise, and on every later mint, refresh via the
130
+ * mobile API. Returns `{ token, expiresAt }` so the cache serves it until
131
+ * `SESSION_REFRESH_BUFFER_MS` before the JWT's `exp`.
132
+ */
133
+ async mintSession() {
134
+ // Cold-start seed: use ZOLA_SESSION_TOKEN if present and comfortably
135
+ // unexpired. Only attempted once — a 401 re-mint always goes to refresh().
136
+ if (!this.triedEnvSeed) {
137
+ this.triedEnvSeed = true;
121
138
  const envSession = readEnvVar('ZOLA_SESSION_TOKEN');
122
139
  if (envSession) {
123
140
  try {
124
141
  const exp = decodeJwtExp(envSession);
125
- if (exp * 1000 - Date.now() > 5 * 60 * 1000) {
126
- this.sessionToken = envSession;
127
- this.sessionExpiry = new Date(exp * 1000);
128
- return;
142
+ if (exp * 1000 - Date.now() > SESSION_REFRESH_BUFFER_MS) {
143
+ return { token: envSession, expiresAt: exp * 1000 };
129
144
  }
130
145
  }
131
146
  catch {
@@ -133,12 +148,12 @@ export class ZolaClient {
133
148
  }
134
149
  }
135
150
  }
136
- await this.refresh();
151
+ return this.refresh();
137
152
  }
138
153
  /**
139
154
  * Refresh the session using the mobile API endpoint.
140
155
  * POST /v3/sessions/refresh with the refresh token JWT.
141
- * Returns a new session_token (30-min) and optionally a new refresh_token.
156
+ * Returns a new session_token (30-min) as a {@link MintedToken}.
142
157
  *
143
158
  * The refresh token comes from `resolveRefreshToken()`, which tries
144
159
  * (1) ZOLA_REFRESH_TOKEN env var, then (2) the fetchproxy 0.3.0 extension's
@@ -168,8 +183,7 @@ export class ZolaClient {
168
183
  const result = (await response.json());
169
184
  const { session_token } = result.data;
170
185
  const exp = decodeJwtExp(session_token);
171
- this.sessionToken = session_token;
172
- this.sessionExpiry = new Date(exp * 1000);
186
+ return { token: session_token, expiresAt: exp * 1000 };
173
187
  }
174
188
  }
175
189
  export const client = new ZolaClient();
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.3'; // x-release-please-version
15
+ const VERSION = '1.4.4'; // 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.3",
3
+ "version": "1.4.4",
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.10.3",
49
+ "@chrischall/mcp-utils": "^0.12.0",
50
50
  "@fetchproxy/bootstrap": "^1.3.0",
51
51
  "@fetchproxy/server": "^1.3.0",
52
52
  "@modelcontextprotocol/sdk": "^1.29.0",
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.3",
9
+ "version": "1.4.4",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "zola-mcp",
14
- "version": "1.4.3",
14
+ "version": "1.4.4",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },