zola-mcp 1.4.0 → 1.4.2

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.0"
10
+ "version": "1.4.2"
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.0",
18
+ "version": "1.4.2",
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.0",
4
+ "version": "1.4.2",
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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chris Hall
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Zola MCP
2
2
 
3
+ [![CI](https://github.com/chrischall/zola-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/chrischall/zola-mcp/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/zola-mcp)](https://www.npmjs.com/package/zola-mcp)
5
+ [![license](https://img.shields.io/npm/l/zola-mcp)](LICENSE)
6
+
3
7
  A [Model Context Protocol](https://modelcontextprotocol.io) server that connects Claude to [Zola](https://www.zola.com), giving you natural-language access to your wedding vendors, budget, guest list, seating chart, events, registry, inquiries, and more.
4
8
 
5
9
  > [!WARNING]
@@ -301,3 +305,4 @@ Releases are automated via GitHub Actions:
301
305
  ## License
302
306
 
303
307
  MIT
308
+
package/dist/auth.js CHANGED
@@ -1,60 +1,63 @@
1
1
  // ────────────────────────────────────────────────────────────────────────────
2
- // Auth resolution — Pattern A template
2
+ // Auth resolution — createAuthResolver (shared skeleton from mcp-utils)
3
3
  // ────────────────────────────────────────────────────────────────────────────
4
4
  //
5
- // Mirrors the canonical "browser-bootstrap + Node-direct" shape from
6
- // ofw-mcp/src/auth.ts. Other MCPs in this family (resy-mcp, opentable-mcp,
7
- // signupgenius-mcp, …) use the same selector — keep the structure flat,
8
- // the path-selection explicit, and the error messages actionable.
5
+ // The canonical three-path "browser-bootstrap + Node-direct" resolver, now the
6
+ // shared `createAuthResolver` skeleton from `@chrischall/mcp-utils` (0.7.0+):
9
7
  //
10
- // THE THREE PATHS, in priority order:
8
+ // 1. Env-var credential ZOLA_REFRESH_TOKEN set → used directly. This is
9
+ // the ~1-year JWT that doubles as the `usr` cookie on zola.com. Reads go
10
+ // through the hardened `readEnvVar` (blank / 'undefined' / 'null' /
11
+ // unexpanded `${VAR}` treated as unset).
11
12
  //
12
- // 1. Env-var credential (existing behavior)
13
- // ZOLA_REFRESH_TOKEN set use it directly. This is the ~1-year JWT
14
- // that doubles as the `usr` cookie on zola.com. Legacy users keep
15
- // working without action.
13
+ // 2. fetchproxy fallback unless ZOLA_DISABLE_FETCHPROXY is truthy
14
+ // ('1'/'true'/'yes'/'on' via `parseBoolEnv` inside the resolver), lift
15
+ // the HttpOnly `usr` cookie out of the user's signed-in zola.com tab via
16
+ // `@fetchproxy/bootstrap` (one-shot WebSocket bridge; `chrome.cookies.get`
17
+ // sees HttpOnly cookies). All subsequent Zola API calls go direct to
18
+ // mobile-api.zola.com from Node — fetchproxy is NOT in the hot path.
19
+ // Bridge-down errors surface the FetchproxyBridgeDownError `.hint`
20
+ // verbatim (handled inside createAuthResolver since 0.7.0); a signed-out
21
+ // tab raises SessionNotAuthenticatedError naming Zola + zola.com.
16
22
  //
17
- // 2. fetchproxy fallback (new)
18
- // When no token is set, lift the user's session out of their
19
- // signed-in zola.com browser tab via the fetchproxy 0.3.0 extension.
20
- // The `@fetchproxy/bootstrap` helper spins up a one-shot WebSocket
21
- // bridge, asks the extension for the HttpOnly `usr` cookie via
22
- // `chrome.cookies.get`, then closes the bridge. From there, all
23
- // Zola API calls go out via plain Node `fetch()` to
24
- // mobile-api.zola.com — fetchproxy is NOT in the hot path.
23
+ // 3. Error nothing configured: an actionable message naming
24
+ // ZOLA_REFRESH_TOKEN, the browser sign-in fallback, and
25
+ // ZOLA_DISABLE_FETCHPROXY.
25
26
  //
26
- // Users opt out with ZOLA_DISABLE_FETCHPROXY=1 (anyone who wants the
27
- // old behavior of "fail loudly when creds are missing").
28
- //
29
- // 3. Error
30
- // Nothing to authenticate with. We throw a message that tells the
31
- // user exactly what to do: set the env var, OR install the extension
32
- // and sign in.
33
- //
34
- // Why fetchproxy is only a one-shot read:
35
- // The bootstrap call snapshots the `usr` cookie and returns. The MCP
36
- // then operates from Node with direct fetch — latency and reliability
37
- // are not coupled to the browser bridge for normal tool calls. The
38
- // captured refresh token is fed into the existing
39
- // `POST /v3/sessions/refresh` flow which mints 30-min session tokens
40
- // (also in pure Node).
41
- //
42
- // Testability:
43
- // - `@fetchproxy/bootstrap` is mocked at the module boundary in tests.
44
- // - This module exposes a single async `resolveRefreshToken()` that
45
- // returns the JWT plus the source — callers (the client) treat the
46
- // return value as opaque credentials.
27
+ // Testability: `@fetchproxy/bootstrap` is mocked at the module boundary in
28
+ // tests, exactly as before. This module still exposes a single async
29
+ // `resolveRefreshToken()` returning the JWT plus the source — callers (the
30
+ // client) treat the return value as opaque credentials.
47
31
  import { bootstrap } from '@fetchproxy/bootstrap';
48
- import { classifyBridgeError } from '@chrischall/mcp-utils/fetchproxy';
49
- import { readEnvVar } from '@chrischall/mcp-utils';
32
+ import { createAuthResolver } from '@chrischall/mcp-utils';
50
33
  import pkg from '../package.json' with { type: 'json' };
51
- /** True if the user has explicitly disabled the fetchproxy fallback. */
52
- function fetchproxyDisabled() {
53
- const raw = readEnvVar('ZOLA_DISABLE_FETCHPROXY');
54
- if (raw === undefined)
55
- return false;
56
- return ['1', 'true', 'yes', 'on'].includes(raw.toLowerCase());
57
- }
34
+ const resolveAuth = createAuthResolver({
35
+ envVar: 'ZOLA_REFRESH_TOKEN',
36
+ disableEnvVar: 'ZOLA_DISABLE_FETCHPROXY',
37
+ // The real bootstrap has a narrower opts type than the injected boundary;
38
+ // the cast keeps the heavy bridge dep out of the shared module's types.
39
+ bootstrap: bootstrap,
40
+ bootstrapOptions: {
41
+ serverName: pkg.name,
42
+ version: pkg.version,
43
+ // Zola serves www.zola.com (web app) and mobile-api.zola.com (API).
44
+ // The `usr` cookie lives on the web app's apex domain; the extension
45
+ // matches on suffix so listing the apex covers any subdomain.
46
+ domains: ['zola.com'],
47
+ declare: {
48
+ // `usr` is HttpOnly → invisible to page JS, but fetchproxy's
49
+ // read_cookies uses `chrome.cookies.get` which DOES see HttpOnly
50
+ // cookies. The value IS the ~1-year refresh JWT.
51
+ cookies: ['usr'],
52
+ localStorage: [],
53
+ sessionStorage: [],
54
+ captureHeaders: [],
55
+ },
56
+ },
57
+ parseTokens: (session) => session.cookies['usr'],
58
+ serviceName: 'Zola',
59
+ signInHost: 'zola.com',
60
+ });
58
61
  /**
59
62
  * Resolve the Zola refresh token using the three-path priority described
60
63
  * above. Throws with an actionable error message when no path succeeds.
@@ -64,58 +67,6 @@ function fetchproxyDisabled() {
64
67
  * logging / future cache-keying only.
65
68
  */
66
69
  export async function resolveRefreshToken() {
67
- // ── Path 1: env-var refresh token (unchanged from pre-fetchproxy behavior).
68
- const envToken = readEnvVar('ZOLA_REFRESH_TOKEN');
69
- if (envToken) {
70
- return { token: envToken, source: 'env' };
71
- }
72
- // ── Path 2: fetchproxy fallback (new).
73
- if (!fetchproxyDisabled()) {
74
- try {
75
- const session = await bootstrap({
76
- serverName: pkg.name,
77
- version: pkg.version,
78
- // Zola serves www.zola.com (web app) and mobile-api.zola.com (API).
79
- // The `usr` cookie lives on the web app's apex domain; the extension
80
- // matches on suffix so listing the apex covers any subdomain.
81
- domains: ['zola.com'],
82
- declare: {
83
- // `usr` is HttpOnly → invisible to page JS, but fetchproxy 0.3.0's
84
- // read_cookies uses `chrome.cookies.get` which DOES see HttpOnly
85
- // cookies. The value IS the ~1-year refresh JWT.
86
- cookies: ['usr'],
87
- localStorage: [],
88
- sessionStorage: [],
89
- captureHeaders: [],
90
- },
91
- });
92
- const token = session.cookies['usr'];
93
- if (!token) {
94
- throw new Error('zola: no `usr` cookie found. ' +
95
- 'Sign into zola.com in your browser (with the fetchproxy extension installed) and retry.');
96
- }
97
- return { token, source: 'fetchproxy' };
98
- }
99
- catch (e) {
100
- // 0.8.0+ typed-error discrimination. The fetchproxy server already
101
- // retries once on SW eviction (bridgeReviveDelayMs=2000 default), so
102
- // a thrown FetchproxyBridgeDownError means the retry also failed —
103
- // the extension's service worker is genuinely down and the user
104
- // needs to wake it. The `.hint` is the actionable copy
105
- // ("click the extension toolbar icon...") that we'd otherwise have
106
- // to hand-write here. Surface it verbatim so users in path 2 get
107
- // the same self-service guidance as path 3.
108
- if (classifyBridgeError(e) === 'bridge_down') {
109
- const downErr = e;
110
- throw new Error(`Zola auth: fetchproxy bridge is down (extension service worker unreachable after retry). ${downErr.hint}`);
111
- }
112
- const msg = e instanceof Error ? e.message : String(e);
113
- throw new Error(`Zola auth: no ZOLA_REFRESH_TOKEN set, and fetchproxy fallback failed: ${msg}`);
114
- }
115
- }
116
- // ── Path 3: nothing configured. Surface both fixes side-by-side so the
117
- // user can pick whichever fits their setup.
118
- throw new Error('Zola auth: set ZOLA_REFRESH_TOKEN, ' +
119
- 'or install the fetchproxy extension and sign into zola.com ' +
120
- '(unset ZOLA_DISABLE_FETCHPROXY if it is set).');
70
+ const { credential, source } = await resolveAuth();
71
+ return { token: credential, source };
121
72
  }
package/dist/bundle.js CHANGED
@@ -34680,24 +34680,52 @@ async function runMcp(opts) {
34680
34680
  return server;
34681
34681
  }
34682
34682
 
34683
- // node_modules/@chrischall/mcp-utils/dist/response/index.js
34684
- function textResult(data) {
34685
- return {
34686
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
34687
- };
34688
- }
34689
- function imageResult(base643, mimeType) {
34690
- return {
34691
- content: [{ type: "image", data: base643, mimeType }]
34692
- };
34693
- }
34694
-
34695
34683
  // node_modules/@chrischall/mcp-utils/dist/errors/index.js
34696
34684
  var DEFAULT_ERROR_MESSAGE_MAX = 500;
34685
+ var McpToolError = class extends Error {
34686
+ /** Actionable remediation text, when one applies. */
34687
+ hint;
34688
+ constructor(message, opts) {
34689
+ super(message, opts?.cause !== void 0 ? { cause: opts.cause } : void 0);
34690
+ this.name = "McpToolError";
34691
+ if (opts?.hint !== void 0)
34692
+ this.hint = opts.hint;
34693
+ Object.setPrototypeOf(this, new.target.prototype);
34694
+ }
34695
+ };
34696
+ var SessionNotAuthenticatedError = class extends McpToolError {
34697
+ constructor(service, signInHost) {
34698
+ const name = service ?? "the service";
34699
+ const where = signInHost ? `Open ${signInHost} in your browser and sign in, then try again.` : "Sign in in your browser, then try again.";
34700
+ super(`Not signed in to ${name}. ${where} Saved searches, saved homes, and other account data require a signed-in session.`, { hint: where });
34701
+ this.name = "SessionNotAuthenticatedError";
34702
+ }
34703
+ };
34704
+ function createHelpfulError(message, opts) {
34705
+ return new McpToolError(message, opts);
34706
+ }
34697
34707
  var BEARER_RE = /(bearer\s+)[A-Za-z0-9._~+/=-]{8,}/gi;
34698
34708
  var JWT_RE = /\b[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{8,}\b/g;
34709
+ var BASIC_AUTH_RE = /(authorization\s*:\s*basic\s+)[A-Za-z0-9+/=_-]{6,}/gi;
34710
+ var SET_COOKIE_RE = /(\bset-cookie\s*:\s*)([^=;,\s]+)=[^;,\s]*/gi;
34711
+ var COOKIE_HEADER_RE = /((?<!set-)\bcookie\s*:\s*)((?:[^=;,\s]+=[^;,\s]*)(?:;\s*[^=;,\s]+=[^;,\s]*)*)/gi;
34712
+ var API_KEY_RE = new RegExp([
34713
+ "sk-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])",
34714
+ // OpenAI / Anthropic (incl. sk-ant-…)
34715
+ "gh[pousr]_[A-Za-z0-9]{36,}\\b",
34716
+ // GitHub ghp_/gho_/ghu_/ghs_/ghr_
34717
+ "xox[baprs]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])",
34718
+ // Slack
34719
+ "AIza[0-9A-Za-z_-]{35}(?![0-9A-Za-z_-])",
34720
+ // Google API key (39 chars total)
34721
+ "AKIA[0-9A-Z]{16}\\b",
34722
+ // AWS access key id (20 chars total)
34723
+ "whsec_[A-Za-z0-9]{16,}\\b"
34724
+ // webhook signing secret (Stripe-style)
34725
+ ].map((p) => `\\b${p}`).join("|"), "g");
34726
+ var QUERY_SECRET_RE = /([?&](?:access_token|refresh_token|client_secret|api_?key|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
34699
34727
  function redactSecrets(text) {
34700
- return text.replace(BEARER_RE, "$1[REDACTED]").replace(JWT_RE, "[REDACTED]");
34728
+ 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]");
34701
34729
  }
34702
34730
  function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX) {
34703
34731
  const str = text === null || text === void 0 ? "" : String(text);
@@ -34707,6 +34735,18 @@ function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX) {
34707
34735
  return `${redacted.slice(0, max)}\u2026 [truncated]`;
34708
34736
  }
34709
34737
 
34738
+ // node_modules/@chrischall/mcp-utils/dist/response/index.js
34739
+ function textResult(data) {
34740
+ return {
34741
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
34742
+ };
34743
+ }
34744
+ function imageResult(base643, mimeType) {
34745
+ return {
34746
+ content: [{ type: "image", data: base643, mimeType }]
34747
+ };
34748
+ }
34749
+
34710
34750
  // node_modules/@chrischall/mcp-utils/dist/config/index.js
34711
34751
  var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
34712
34752
  function readEnvVar(key, opts = {}) {
@@ -34720,6 +34760,20 @@ function readEnvVar(key, opts = {}) {
34720
34760
  }
34721
34761
  return opts.default;
34722
34762
  }
34763
+ var TRUE_TOKENS = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
34764
+ var FALSE_TOKENS = /* @__PURE__ */ new Set(["0", "false", "no", "off"]);
34765
+ function parseBoolEnv(key, opts = {}) {
34766
+ const fallback = opts.default ?? false;
34767
+ const raw = readEnvVar(key, { env: opts.env });
34768
+ if (raw === void 0)
34769
+ return fallback;
34770
+ const token = raw.toLowerCase();
34771
+ if (TRUE_TOKENS.has(token))
34772
+ return true;
34773
+ if (FALSE_TOKENS.has(token))
34774
+ return false;
34775
+ return fallback;
34776
+ }
34723
34777
  async function loadDotenvSafely(opts = {}) {
34724
34778
  try {
34725
34779
  const mod = await import(
@@ -34738,6 +34792,15 @@ async function loadDotenvSafely(opts = {}) {
34738
34792
  }
34739
34793
 
34740
34794
  // node_modules/@chrischall/mcp-utils/dist/http/index.js
34795
+ function buildOptionalBody(args, optionalFields) {
34796
+ const body = {};
34797
+ for (const field of optionalFields) {
34798
+ if (args[field] !== void 0) {
34799
+ body[field] = args[field];
34800
+ }
34801
+ }
34802
+ return body;
34803
+ }
34741
34804
  function formatApiError(status, method, path, errorText, opts = {}) {
34742
34805
  const service = opts.service ?? "API";
34743
34806
  const head = `${service} error ${status} for ${method.toUpperCase()} ${path}`;
@@ -34796,6 +34859,47 @@ var pageSchema = {
34796
34859
  page_size: external_exports.number().int().min(1).max(200).default(50).describe("Number of items per page (1-200).")
34797
34860
  };
34798
34861
 
34862
+ // node_modules/@chrischall/mcp-utils/dist/auth/index.js
34863
+ function bridgeDownHintOf(err) {
34864
+ if (err instanceof Error && err.name === "FetchproxyBridgeDownError") {
34865
+ const hint = err.hint;
34866
+ if (typeof hint === "string" && hint.length > 0)
34867
+ return hint;
34868
+ }
34869
+ return void 0;
34870
+ }
34871
+ function createAuthResolver(opts) {
34872
+ const { envVar, disableEnvVar, bootstrap: bootstrap2, bootstrapOptions, parseTokens, serviceName, signInHost, env } = opts;
34873
+ return async function resolveAuth2() {
34874
+ const envCredential = readEnvVar(envVar, env ? { env } : {});
34875
+ if (envCredential) {
34876
+ return { credential: envCredential, source: "env" };
34877
+ }
34878
+ const disabled = disableEnvVar !== void 0 && parseBoolEnv(disableEnvVar, env ? { env } : {});
34879
+ if (!disabled) {
34880
+ let session;
34881
+ try {
34882
+ session = await bootstrap2(bootstrapOptions);
34883
+ } catch (e) {
34884
+ const bridgeHint = bridgeDownHintOf(e);
34885
+ if (bridgeHint !== void 0) {
34886
+ throw createHelpfulError(`Auth: no ${envVar} set, and the fetchproxy bridge is down (extension service worker unreachable). ${bridgeHint}`, { hint: bridgeHint });
34887
+ }
34888
+ throw createHelpfulError(`Auth: no ${envVar} set, and fetchproxy fallback failed: ${truncateErrorMessage(messageOf(e))}`, { hint: `Set ${envVar}, or sign in in your browser and retry.` });
34889
+ }
34890
+ const credential = parseTokens(session);
34891
+ if (credential) {
34892
+ return { credential, source: "fetchproxy" };
34893
+ }
34894
+ throw new SessionNotAuthenticatedError(serviceName, signInHost);
34895
+ }
34896
+ throw createHelpfulError(`Auth: set ${envVar}${signInHost ? `, or sign in at ${signInHost} in your browser` : ", or sign in in your browser"}${disableEnvVar ? ` (unset ${disableEnvVar} if it is set)` : ""}.`, { hint: `Set ${envVar} or sign in in your browser.` });
34897
+ };
34898
+ }
34899
+ function messageOf(err) {
34900
+ return err instanceof Error ? err.message : String(err);
34901
+ }
34902
+
34799
34903
  // src/client.ts
34800
34904
  import { dirname, join as join2 } from "path";
34801
34905
  import { fileURLToPath } from "url";
@@ -37980,7 +38084,7 @@ var BootstrapDisabledError = class extends Error {
37980
38084
  // package.json
37981
38085
  var package_default = {
37982
38086
  name: "zola-mcp",
37983
- version: "1.4.0",
38087
+ version: "1.4.2",
37984
38088
  mcpName: "io.github.chrischall/zola-mcp",
37985
38089
  description: "Zola wedding MCP server for Claude",
37986
38090
  author: "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -38026,7 +38130,7 @@ var package_default = {
38026
38130
  "test:watch": "vitest"
38027
38131
  },
38028
38132
  dependencies: {
38029
- "@chrischall/mcp-utils": "^0.5.2",
38133
+ "@chrischall/mcp-utils": "^0.7.0",
38030
38134
  "@fetchproxy/bootstrap": "^1.3.0",
38031
38135
  "@fetchproxy/server": "^1.3.0",
38032
38136
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -38043,58 +38147,36 @@ var package_default = {
38043
38147
  };
38044
38148
 
38045
38149
  // src/auth.ts
38046
- function fetchproxyDisabled() {
38047
- const raw = readEnvVar("ZOLA_DISABLE_FETCHPROXY");
38048
- if (raw === void 0) return false;
38049
- return ["1", "true", "yes", "on"].includes(raw.toLowerCase());
38050
- }
38051
- async function resolveRefreshToken() {
38052
- const envToken = readEnvVar("ZOLA_REFRESH_TOKEN");
38053
- if (envToken) {
38054
- return { token: envToken, source: "env" };
38055
- }
38056
- if (!fetchproxyDisabled()) {
38057
- try {
38058
- const session = await bootstrap({
38059
- serverName: package_default.name,
38060
- version: package_default.version,
38061
- // Zola serves www.zola.com (web app) and mobile-api.zola.com (API).
38062
- // The `usr` cookie lives on the web app's apex domain; the extension
38063
- // matches on suffix so listing the apex covers any subdomain.
38064
- domains: ["zola.com"],
38065
- declare: {
38066
- // `usr` is HttpOnly → invisible to page JS, but fetchproxy 0.3.0's
38067
- // read_cookies uses `chrome.cookies.get` which DOES see HttpOnly
38068
- // cookies. The value IS the ~1-year refresh JWT.
38069
- cookies: ["usr"],
38070
- localStorage: [],
38071
- sessionStorage: [],
38072
- captureHeaders: []
38073
- }
38074
- });
38075
- const token = session.cookies["usr"];
38076
- if (!token) {
38077
- throw new Error(
38078
- "zola: no `usr` cookie found. Sign into zola.com in your browser (with the fetchproxy extension installed) and retry."
38079
- );
38080
- }
38081
- return { token, source: "fetchproxy" };
38082
- } catch (e) {
38083
- if (classifyBridgeError(e) === "bridge_down") {
38084
- const downErr = e;
38085
- throw new Error(
38086
- `Zola auth: fetchproxy bridge is down (extension service worker unreachable after retry). ${downErr.hint}`
38087
- );
38088
- }
38089
- const msg = e instanceof Error ? e.message : String(e);
38090
- throw new Error(
38091
- `Zola auth: no ZOLA_REFRESH_TOKEN set, and fetchproxy fallback failed: ${msg}`
38092
- );
38150
+ var resolveAuth = createAuthResolver({
38151
+ envVar: "ZOLA_REFRESH_TOKEN",
38152
+ disableEnvVar: "ZOLA_DISABLE_FETCHPROXY",
38153
+ // The real bootstrap has a narrower opts type than the injected boundary;
38154
+ // the cast keeps the heavy bridge dep out of the shared module's types.
38155
+ bootstrap,
38156
+ bootstrapOptions: {
38157
+ serverName: package_default.name,
38158
+ version: package_default.version,
38159
+ // Zola serves www.zola.com (web app) and mobile-api.zola.com (API).
38160
+ // The `usr` cookie lives on the web app's apex domain; the extension
38161
+ // matches on suffix so listing the apex covers any subdomain.
38162
+ domains: ["zola.com"],
38163
+ declare: {
38164
+ // `usr` is HttpOnly → invisible to page JS, but fetchproxy's
38165
+ // read_cookies uses `chrome.cookies.get` which DOES see HttpOnly
38166
+ // cookies. The value IS the ~1-year refresh JWT.
38167
+ cookies: ["usr"],
38168
+ localStorage: [],
38169
+ sessionStorage: [],
38170
+ captureHeaders: []
38093
38171
  }
38094
- }
38095
- throw new Error(
38096
- "Zola auth: set ZOLA_REFRESH_TOKEN, or install the fetchproxy extension and sign into zola.com (unset ZOLA_DISABLE_FETCHPROXY if it is set)."
38097
- );
38172
+ },
38173
+ parseTokens: (session) => session.cookies["usr"],
38174
+ serviceName: "Zola",
38175
+ signInHost: "zola.com"
38176
+ });
38177
+ async function resolveRefreshToken() {
38178
+ const { credential, source } = await resolveAuth();
38179
+ return { token: credential, source };
38098
38180
  }
38099
38181
 
38100
38182
  // src/client.ts
@@ -38247,7 +38329,7 @@ var ZolaClient = class {
38247
38329
  if (!response.ok) {
38248
38330
  const text = await response.text();
38249
38331
  throw new Error(
38250
- `Zola session refresh failed (${response.status}): ${text}
38332
+ `Zola session refresh failed (${response.status}): ${truncateErrorMessage(text)}
38251
38333
  To fix: set ZOLA_REFRESH_TOKEN, or install the fetchproxy extension and sign into zola.com.`
38252
38334
  );
38253
38335
  }
@@ -38266,11 +38348,7 @@ function imageResult2(bytes, mimeType) {
38266
38348
  return imageResult(Buffer.from(bytes).toString("base64"), mimeType);
38267
38349
  }
38268
38350
  function pickDefined(base, args, keys) {
38269
- const body = { ...base };
38270
- for (const key of keys) {
38271
- if (args[key] !== void 0) body[key] = args[key];
38272
- }
38273
- return body;
38351
+ return { ...base, ...buildOptionalBody(args, keys) };
38274
38352
  }
38275
38353
 
38276
38354
  // src/tools/vendors.ts
@@ -40225,7 +40303,7 @@ function registerEventInvitationTools(server) {
40225
40303
  }
40226
40304
 
40227
40305
  // src/index.ts
40228
- var VERSION = "1.4.0";
40306
+ var VERSION = "1.4.2";
40229
40307
  await runMcp({
40230
40308
  name: "zola-mcp",
40231
40309
  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, } from '@chrischall/mcp-utils';
3
+ import { readEnvVar, loadDotenvSafely, decodeJwtExp, decodeJwtSessionId, formatApiError, truncateErrorMessage, } 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
@@ -159,7 +159,10 @@ export class ZolaClient {
159
159
  });
160
160
  if (!response.ok) {
161
161
  const text = await response.text();
162
- throw new Error(`Zola session refresh failed (${response.status}): ${text}\n` +
162
+ // The refresh request body carries the refresh JWT an echoing upstream
163
+ // or proxy could reflect it in the error body, so redact + truncate the
164
+ // untrusted body before it can reach a tool result.
165
+ throw new Error(`Zola session refresh failed (${response.status}): ${truncateErrorMessage(text)}\n` +
163
166
  'To fix: set ZOLA_REFRESH_TOKEN, or install the fetchproxy extension and sign into zola.com.');
164
167
  }
165
168
  const result = (await response.json());
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.0'; // x-release-please-version
15
+ const VERSION = '1.4.2'; // x-release-please-version
16
16
  await runMcp({
17
17
  name: 'zola-mcp',
18
18
  version: VERSION,
package/dist/types.js CHANGED
@@ -1,4 +1,4 @@
1
- import { textResult, imageResult as imageResultBase64 } from '@chrischall/mcp-utils';
1
+ import { buildOptionalBody, textResult, imageResult as imageResultBase64 } from '@chrischall/mcp-utils';
2
2
  /**
3
3
  * Wrap any value as the standard MCP text-content tool result with pretty-printed JSON.
4
4
  * Re-exported from `@chrischall/mcp-utils` (`textResult`) under the repo's
@@ -16,12 +16,9 @@ export function imageResult(bytes, mimeType) {
16
16
  /**
17
17
  * Build a partial-update body containing only keys from `args` that are not undefined.
18
18
  * Used by tools that send PATCH-style updates where omitting a key means "leave unchanged".
19
+ * The undefined-dropping is the shared `buildOptionalBody` from `@chrischall/mcp-utils`;
20
+ * this wrapper just merges the always-present `base` fields in front.
19
21
  */
20
22
  export function pickDefined(base, args, keys) {
21
- const body = { ...base };
22
- for (const key of keys) {
23
- if (args[key] !== undefined)
24
- body[key] = args[key];
25
- }
26
- return body;
23
+ return { ...base, ...buildOptionalBody(args, keys) };
27
24
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zola-mcp",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
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.5.2",
49
+ "@chrischall/mcp-utils": "^0.7.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.0",
9
+ "version": "1.4.2",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "zola-mcp",
14
- "version": "1.4.0",
14
+ "version": "1.4.2",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },