zola-mcp 1.2.3 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/auth.js +13 -0
- package/dist/bundle.js +639 -25
- package/dist/client.js +23 -6
- package/dist/index.js +3 -1
- package/dist/tools/invitations.js +256 -0
- package/dist/types.js +4 -0
- package/package.json +3 -2
- package/server.json +2 -2
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "Zola wedding planning tools for Claude Code",
|
|
10
|
-
"version": "1.
|
|
10
|
+
"version": "1.3.1"
|
|
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.
|
|
18
|
+
"version": "1.3.1",
|
|
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
|
+
"version": "1.3.1",
|
|
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/auth.js
CHANGED
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
// returns the JWT plus the source — callers (the client) treat the
|
|
46
46
|
// return value as opaque credentials.
|
|
47
47
|
import { bootstrap } from '@fetchproxy/bootstrap';
|
|
48
|
+
import { classifyBridgeError } from '@fetchproxy/server';
|
|
48
49
|
import pkg from '../package.json' with { type: 'json' };
|
|
49
50
|
/**
|
|
50
51
|
* Read an env var, trim, and treat blank / `${UNEXPANDED}` placeholders as
|
|
@@ -113,6 +114,18 @@ export async function resolveRefreshToken() {
|
|
|
113
114
|
return { token, source: 'fetchproxy' };
|
|
114
115
|
}
|
|
115
116
|
catch (e) {
|
|
117
|
+
// 0.8.0+ typed-error discrimination. The fetchproxy server already
|
|
118
|
+
// retries once on SW eviction (bridgeReviveDelayMs=2000 default), so
|
|
119
|
+
// a thrown FetchproxyBridgeDownError means the retry also failed —
|
|
120
|
+
// the extension's service worker is genuinely down and the user
|
|
121
|
+
// needs to wake it. The `.hint` is the actionable copy
|
|
122
|
+
// ("click the extension toolbar icon...") that we'd otherwise have
|
|
123
|
+
// to hand-write here. Surface it verbatim so users in path 2 get
|
|
124
|
+
// the same self-service guidance as path 3.
|
|
125
|
+
if (classifyBridgeError(e) === 'bridge_down') {
|
|
126
|
+
const downErr = e;
|
|
127
|
+
throw new Error(`Zola auth: fetchproxy bridge is down (extension service worker unreachable after retry). ${downErr.hint}`);
|
|
128
|
+
}
|
|
116
129
|
const msg = e instanceof Error ? e.message : String(e);
|
|
117
130
|
throw new Error(`Zola auth: no ZOLA_REFRESH_TOKEN set, and fetchproxy fallback failed: ${msg}`);
|
|
118
131
|
}
|
package/dist/bundle.js
CHANGED
|
@@ -35178,6 +35178,52 @@ var FetchproxyHttpError = class extends Error {
|
|
|
35178
35178
|
this.name = "FetchproxyHttpError";
|
|
35179
35179
|
}
|
|
35180
35180
|
};
|
|
35181
|
+
var FetchproxyBridgeDownError = class extends FetchproxyProtocolError {
|
|
35182
|
+
originalError;
|
|
35183
|
+
retryAttempted;
|
|
35184
|
+
op;
|
|
35185
|
+
url;
|
|
35186
|
+
/** 0.8.0+: bridge role at throw time; `null` if listen() hadn't bound yet. */
|
|
35187
|
+
role;
|
|
35188
|
+
/** 0.8.0+: bridge port at throw time (the same port `listen()` bound to). */
|
|
35189
|
+
port;
|
|
35190
|
+
hint;
|
|
35191
|
+
constructor(args) {
|
|
35192
|
+
const retryAttempted = args.retryAttempted ?? false;
|
|
35193
|
+
const op = args.op ?? "fetch";
|
|
35194
|
+
const retryClause = retryAttempted ? `Server already burned a one-shot lazy-revive retry; SW is still down. ` : `Server lazy-revive retry was disabled (bridgeReviveDelayMs unset/0). `;
|
|
35195
|
+
const hint = `the fetchproxy extension's service worker is not responding ("${args.originalError}"). Chrome evicts extension service workers after ~30s idle by default. ${retryClause}Wake it by clicking the fetchproxy extension toolbar icon, then retry. If it keeps happening, reload the extension from chrome://extensions.`;
|
|
35196
|
+
super(`fetchproxy bridge down during ${op}${args.url ? ` (${args.url})` : ""}. ${hint}`);
|
|
35197
|
+
this.name = "FetchproxyBridgeDownError";
|
|
35198
|
+
this.originalError = args.originalError;
|
|
35199
|
+
this.retryAttempted = retryAttempted;
|
|
35200
|
+
this.op = op;
|
|
35201
|
+
if (args.url !== void 0)
|
|
35202
|
+
this.url = args.url;
|
|
35203
|
+
this.role = args.role ?? null;
|
|
35204
|
+
this.port = args.port ?? 0;
|
|
35205
|
+
this.hint = hint;
|
|
35206
|
+
}
|
|
35207
|
+
};
|
|
35208
|
+
var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
|
|
35209
|
+
url;
|
|
35210
|
+
timeoutMs;
|
|
35211
|
+
/** 0.8.0+: bridge role at throw time; `null` if listen() hadn't bound yet. */
|
|
35212
|
+
role;
|
|
35213
|
+
/** 0.8.0+: bridge port at throw time. */
|
|
35214
|
+
port;
|
|
35215
|
+
/** 0.8.0+: actual elapsed milliseconds when the timer won the race. */
|
|
35216
|
+
elapsedMs;
|
|
35217
|
+
constructor(args) {
|
|
35218
|
+
super(`fetchproxy: ${args.url} did not respond within ${args.timeoutMs}ms`);
|
|
35219
|
+
this.name = "FetchproxyTimeoutError";
|
|
35220
|
+
this.url = args.url;
|
|
35221
|
+
this.timeoutMs = args.timeoutMs;
|
|
35222
|
+
this.role = args.role ?? null;
|
|
35223
|
+
this.port = args.port ?? 0;
|
|
35224
|
+
this.elapsedMs = args.elapsedMs ?? args.timeoutMs;
|
|
35225
|
+
}
|
|
35226
|
+
};
|
|
35181
35227
|
var SUBDOMAIN_LABEL_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
|
|
35182
35228
|
function assertSubdomainLabel(label) {
|
|
35183
35229
|
if (!SUBDOMAIN_LABEL_RE.test(label)) {
|
|
@@ -35215,6 +35261,18 @@ var FetchproxyServer = class {
|
|
|
35215
35261
|
hostHandle = null;
|
|
35216
35262
|
peerHandle = null;
|
|
35217
35263
|
nextRequestId = 1;
|
|
35264
|
+
// 0.8.0+: process-wide freshness counters surfaced via bridgeHealth().
|
|
35265
|
+
// Replaces the local copies every downstream MCP was rolling on top
|
|
35266
|
+
// of its own transport adapter — see realty-mcp cohort drift notes.
|
|
35267
|
+
// Updated by recordSuccess / recordFailure from fetch + capture paths.
|
|
35268
|
+
// `lastExtensionMessageAt` (#23 ask 4) is updated whenever any inner
|
|
35269
|
+
// frame from the extension arrives — gives extension-side liveness
|
|
35270
|
+
// distinct from per-call success/failure.
|
|
35271
|
+
lastSuccessAt = null;
|
|
35272
|
+
lastFailureAt = null;
|
|
35273
|
+
lastFailureReason = null;
|
|
35274
|
+
consecutiveFailures = 0;
|
|
35275
|
+
lastExtensionMessageAt = null;
|
|
35218
35276
|
pending = /* @__PURE__ */ new Map();
|
|
35219
35277
|
// Separate pending map for read_cookies so the response shape (cookies
|
|
35220
35278
|
// string vs status/body) doesn't have to share a union type with fetch.
|
|
@@ -35283,6 +35341,12 @@ var FetchproxyServer = class {
|
|
|
35283
35341
|
key: d.key,
|
|
35284
35342
|
jsonPointer: d.jsonPointer
|
|
35285
35343
|
})),
|
|
35344
|
+
// 0.8.0+: timer + lazy-revive default to ON. Every realty MCP
|
|
35345
|
+
// adapter was about to set these to the same numbers anyway; the
|
|
35346
|
+
// back-door is `0` (explicit opt-out) if a caller genuinely wants
|
|
35347
|
+
// the legacy hang-forever / fail-once-on-SW-eviction behavior.
|
|
35348
|
+
fetchTimeoutMs: opts.fetchTimeoutMs ?? 3e4,
|
|
35349
|
+
bridgeReviveDelayMs: opts.bridgeReviveDelayMs ?? 2e3,
|
|
35286
35350
|
identityDir: opts.identityDir,
|
|
35287
35351
|
onPairCode: opts.onPairCode
|
|
35288
35352
|
};
|
|
@@ -35418,7 +35482,7 @@ var FetchproxyServer = class {
|
|
|
35418
35482
|
}
|
|
35419
35483
|
}
|
|
35420
35484
|
pairingErrorMessage(code) {
|
|
35421
|
-
return `fetchproxy: pairing required for ${this.opts.serverName}
|
|
35485
|
+
return `fetchproxy transport error: pairing required for ${this.opts.serverName}. Tell the user to open the Transporter browser extension popup and approve the pair request. The pair code is: ${code} \u2014 display this code to the user so they can verify it matches.`;
|
|
35422
35486
|
}
|
|
35423
35487
|
/**
|
|
35424
35488
|
* Raw single-shot fetch through the bridge. Most callers should prefer
|
|
@@ -35439,8 +35503,71 @@ var FetchproxyServer = class {
|
|
|
35439
35503
|
const pendingCode = this.currentPendingPairCode();
|
|
35440
35504
|
if (pendingCode !== null) {
|
|
35441
35505
|
const error48 = this.pairingErrorMessage(pendingCode);
|
|
35442
|
-
return {
|
|
35506
|
+
return {
|
|
35507
|
+
ok: false,
|
|
35508
|
+
error: error48,
|
|
35509
|
+
kind: classifyFetchError(error48),
|
|
35510
|
+
retryAttempted: false
|
|
35511
|
+
};
|
|
35443
35512
|
}
|
|
35513
|
+
const first = await this._fetchOnceWithTimeout(init);
|
|
35514
|
+
const reviveMs = this.opts.bridgeReviveDelayMs;
|
|
35515
|
+
let final = first;
|
|
35516
|
+
if (!first.ok && first.kind === "content_script_unreachable" && reviveMs !== void 0 && reviveMs > 0) {
|
|
35517
|
+
await new Promise((r) => setTimeout(r, reviveMs));
|
|
35518
|
+
const second = await this._fetchOnceWithTimeout(init);
|
|
35519
|
+
if (second.ok)
|
|
35520
|
+
this.recordSuccess();
|
|
35521
|
+
else
|
|
35522
|
+
this.recordFailure(`${second.kind ?? "other"}: ${second.error}`);
|
|
35523
|
+
return { ...second, retryAttempted: true };
|
|
35524
|
+
}
|
|
35525
|
+
if (first.ok)
|
|
35526
|
+
this.recordSuccess();
|
|
35527
|
+
else
|
|
35528
|
+
this.recordFailure(`${first.kind ?? "other"}: ${first.error}`);
|
|
35529
|
+
return { ...first, retryAttempted: false };
|
|
35530
|
+
}
|
|
35531
|
+
/**
|
|
35532
|
+
* 0.8.0+: snapshot of the bridge's process-wide freshness counters,
|
|
35533
|
+
* suitable for surfacing through a downstream MCP's healthcheck tool.
|
|
35534
|
+
* Counters reset on a success (consecutiveFailures), accumulate
|
|
35535
|
+
* across the process lifetime otherwise. Replaces the per-MCP
|
|
35536
|
+
* duplication the realty cohort had been rolling in their adapters.
|
|
35537
|
+
* `lastExtensionMessageAt` is updated whenever ANY inner frame
|
|
35538
|
+
* arrives from the extension — gives extension-side liveness
|
|
35539
|
+
* distinct from server-side success/failure of the user-visible
|
|
35540
|
+
* call (addresses #23 ask 4).
|
|
35541
|
+
*/
|
|
35542
|
+
bridgeHealth() {
|
|
35543
|
+
return {
|
|
35544
|
+
role: this.role,
|
|
35545
|
+
port: this.opts.port,
|
|
35546
|
+
serverVersion: this.opts.version,
|
|
35547
|
+
fetchTimeoutMs: this.opts.fetchTimeoutMs ?? 0,
|
|
35548
|
+
bridgeReviveDelayMs: this.opts.bridgeReviveDelayMs ?? 0,
|
|
35549
|
+
lastSuccessAt: this.lastSuccessAt,
|
|
35550
|
+
lastFailureAt: this.lastFailureAt,
|
|
35551
|
+
lastFailureReason: this.lastFailureReason,
|
|
35552
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
35553
|
+
lastExtensionMessageAt: this.lastExtensionMessageAt
|
|
35554
|
+
};
|
|
35555
|
+
}
|
|
35556
|
+
recordSuccess() {
|
|
35557
|
+
this.lastSuccessAt = Date.now();
|
|
35558
|
+
this.consecutiveFailures = 0;
|
|
35559
|
+
}
|
|
35560
|
+
recordFailure(reason) {
|
|
35561
|
+
this.lastFailureAt = Date.now();
|
|
35562
|
+
this.lastFailureReason = reason;
|
|
35563
|
+
this.consecutiveFailures += 1;
|
|
35564
|
+
}
|
|
35565
|
+
/**
|
|
35566
|
+
* Single bridge round-trip, wrapped by `fetchTimeoutMs` when set.
|
|
35567
|
+
* On timeout returns the `{ok:false, kind:'timeout'}` envelope —
|
|
35568
|
+
* the throwing surface is the convenience methods.
|
|
35569
|
+
*/
|
|
35570
|
+
async _fetchOnceWithTimeout(init) {
|
|
35444
35571
|
const id = this.nextRequestId++;
|
|
35445
35572
|
const inner = { type: "request", id, op: "fetch", init };
|
|
35446
35573
|
const pending = new Promise((resolve) => {
|
|
@@ -35451,7 +35578,61 @@ var FetchproxyServer = class {
|
|
|
35451
35578
|
} else if (this.peerHandle) {
|
|
35452
35579
|
await this.peerHandle.sendInner(inner);
|
|
35453
35580
|
}
|
|
35454
|
-
|
|
35581
|
+
const timeoutMs = this.opts.fetchTimeoutMs;
|
|
35582
|
+
if (timeoutMs === void 0 || timeoutMs <= 0)
|
|
35583
|
+
return pending;
|
|
35584
|
+
let timer;
|
|
35585
|
+
const start = Date.now();
|
|
35586
|
+
try {
|
|
35587
|
+
return await Promise.race([
|
|
35588
|
+
pending,
|
|
35589
|
+
new Promise((resolve) => {
|
|
35590
|
+
timer = setTimeout(() => {
|
|
35591
|
+
this.pending.delete(id);
|
|
35592
|
+
const elapsedMs = Date.now() - start;
|
|
35593
|
+
const error48 = `fetchproxy: ${init.url} did not respond within ${timeoutMs}ms`;
|
|
35594
|
+
resolve({
|
|
35595
|
+
ok: false,
|
|
35596
|
+
error: error48,
|
|
35597
|
+
kind: "timeout",
|
|
35598
|
+
retryAttempted: false,
|
|
35599
|
+
elapsedMs
|
|
35600
|
+
});
|
|
35601
|
+
}, timeoutMs);
|
|
35602
|
+
})
|
|
35603
|
+
]);
|
|
35604
|
+
} finally {
|
|
35605
|
+
if (timer)
|
|
35606
|
+
clearTimeout(timer);
|
|
35607
|
+
}
|
|
35608
|
+
}
|
|
35609
|
+
/**
|
|
35610
|
+
* Map an `ok:false` fetch result to its typed throwable. Centralizes
|
|
35611
|
+
* the kind-to-error-class switch so `request()` and (via the same
|
|
35612
|
+
* logic re-implemented inline) `captureRequestHeader()` agree on what
|
|
35613
|
+
* to throw.
|
|
35614
|
+
*/
|
|
35615
|
+
_typedErrorFor(result, url2, op, retryAttempted) {
|
|
35616
|
+
if (result.kind === "timeout") {
|
|
35617
|
+
return new FetchproxyTimeoutError({
|
|
35618
|
+
url: url2,
|
|
35619
|
+
timeoutMs: this.opts.fetchTimeoutMs ?? 0,
|
|
35620
|
+
role: this.role,
|
|
35621
|
+
port: this.opts.port,
|
|
35622
|
+
elapsedMs: result.elapsedMs
|
|
35623
|
+
});
|
|
35624
|
+
}
|
|
35625
|
+
if (result.kind === "content_script_unreachable") {
|
|
35626
|
+
return new FetchproxyBridgeDownError({
|
|
35627
|
+
originalError: result.error,
|
|
35628
|
+
retryAttempted,
|
|
35629
|
+
op,
|
|
35630
|
+
url: url2,
|
|
35631
|
+
role: this.role,
|
|
35632
|
+
port: this.opts.port
|
|
35633
|
+
});
|
|
35634
|
+
}
|
|
35635
|
+
return new FetchproxyProtocolError(result.error);
|
|
35455
35636
|
}
|
|
35456
35637
|
/**
|
|
35457
35638
|
* Convenience wrapper around `fetch()`. Builds the URL from a path
|
|
@@ -35474,8 +35655,18 @@ var FetchproxyServer = class {
|
|
|
35474
35655
|
if (opts.subdomain !== void 0)
|
|
35475
35656
|
assertSubdomainLabel(opts.subdomain);
|
|
35476
35657
|
const baseDomain = this.resolveBaseDomain(opts.domain);
|
|
35477
|
-
const
|
|
35478
|
-
|
|
35658
|
+
const isAbsolute = path.startsWith("http://") || path.startsWith("https://");
|
|
35659
|
+
let host;
|
|
35660
|
+
if (isAbsolute) {
|
|
35661
|
+
try {
|
|
35662
|
+
host = new URL(path).host;
|
|
35663
|
+
} catch {
|
|
35664
|
+
throw new Error(`FetchproxyServer.request: absolute path is not a valid URL: ${JSON.stringify(path)}`);
|
|
35665
|
+
}
|
|
35666
|
+
} else {
|
|
35667
|
+
host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
|
|
35668
|
+
}
|
|
35669
|
+
const url2 = isAbsolute ? path : `https://${host}${path}`;
|
|
35479
35670
|
assertUrlInDomains("request url", url2, this.opts.domains);
|
|
35480
35671
|
const init = {
|
|
35481
35672
|
url: url2,
|
|
@@ -35486,7 +35677,7 @@ var FetchproxyServer = class {
|
|
|
35486
35677
|
};
|
|
35487
35678
|
const result = await this.fetch(init);
|
|
35488
35679
|
if (!result.ok) {
|
|
35489
|
-
throw
|
|
35680
|
+
throw this._typedErrorFor(result, init.url, "fetch", result.retryAttempted ?? false);
|
|
35490
35681
|
}
|
|
35491
35682
|
const response = {
|
|
35492
35683
|
status: result.status,
|
|
@@ -35696,10 +35887,73 @@ var FetchproxyServer = class {
|
|
|
35696
35887
|
}
|
|
35697
35888
|
await this.ensureConnected();
|
|
35698
35889
|
this.throwIfPendingPair();
|
|
35699
|
-
const
|
|
35700
|
-
|
|
35701
|
-
|
|
35890
|
+
const decls = this.opts.captureHeaders;
|
|
35891
|
+
let resolved;
|
|
35892
|
+
if (opts?.urlPattern !== void 0 && opts?.headerName !== void 0) {
|
|
35893
|
+
const found = decls.find((d) => d.urlPattern === opts.urlPattern && d.headerName === opts.headerName);
|
|
35894
|
+
if (!found) {
|
|
35895
|
+
throw new Error(`FetchproxyServer.captureRequestHeader: (urlPattern=${JSON.stringify(opts.urlPattern)}, headerName=${JSON.stringify(opts.headerName)}) not declared in captureHeaders`);
|
|
35896
|
+
}
|
|
35897
|
+
resolved = found;
|
|
35898
|
+
} else if (opts?.urlPattern === void 0 && opts?.headerName === void 0) {
|
|
35899
|
+
if (decls.length === 0) {
|
|
35900
|
+
throw new Error("FetchproxyServer.captureRequestHeader: no captureHeaders declared on this server \u2014 declare at least one entry in FetchproxyServerOpts.captureHeaders, or pass {urlPattern, headerName} explicitly");
|
|
35901
|
+
}
|
|
35902
|
+
if (decls.length > 1) {
|
|
35903
|
+
const list = decls.map((d) => `${JSON.stringify(d.urlPattern)}/${JSON.stringify(d.headerName)}`).join(", ");
|
|
35904
|
+
throw new Error(`FetchproxyServer.captureRequestHeader: multiple captureHeaders declared (${decls.length}: ${list}); pass {urlPattern, headerName} to disambiguate`);
|
|
35905
|
+
}
|
|
35906
|
+
resolved = decls[0];
|
|
35907
|
+
} else {
|
|
35908
|
+
throw new Error("FetchproxyServer.captureRequestHeader: pass both urlPattern AND headerName, or neither (which defaults to the single declared entry)");
|
|
35909
|
+
}
|
|
35910
|
+
const callOpts = { ...resolved, ...opts?.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {} };
|
|
35911
|
+
try {
|
|
35912
|
+
const result = await this._captureRequestHeaderOnce(callOpts);
|
|
35913
|
+
this.recordSuccess();
|
|
35914
|
+
return result;
|
|
35915
|
+
} catch (err) {
|
|
35916
|
+
const swDown = err instanceof FetchproxyProtocolError && classifyFetchError(err.message) === "content_script_unreachable";
|
|
35917
|
+
if (!swDown) {
|
|
35918
|
+
this.recordFailure(`capture_request_header: ${err.message ?? String(err)}`);
|
|
35919
|
+
throw err;
|
|
35920
|
+
}
|
|
35921
|
+
const reviveMs = this.opts.bridgeReviveDelayMs ?? 0;
|
|
35922
|
+
if (reviveMs > 0) {
|
|
35923
|
+
await new Promise((r) => setTimeout(r, reviveMs));
|
|
35924
|
+
try {
|
|
35925
|
+
const result = await this._captureRequestHeaderOnce(callOpts);
|
|
35926
|
+
this.recordSuccess();
|
|
35927
|
+
return result;
|
|
35928
|
+
} catch (retryErr) {
|
|
35929
|
+
const stillDown = retryErr instanceof FetchproxyProtocolError && classifyFetchError(retryErr.message) === "content_script_unreachable";
|
|
35930
|
+
if (!stillDown) {
|
|
35931
|
+
this.recordFailure(`capture_request_header: ${retryErr.message ?? String(retryErr)}`);
|
|
35932
|
+
throw retryErr;
|
|
35933
|
+
}
|
|
35934
|
+
this.recordFailure(`capture_request_header bridge-down: ${retryErr.message}`);
|
|
35935
|
+
throw new FetchproxyBridgeDownError({
|
|
35936
|
+
originalError: retryErr.message,
|
|
35937
|
+
retryAttempted: true,
|
|
35938
|
+
op: "capture_request_header",
|
|
35939
|
+
url: resolved.urlPattern,
|
|
35940
|
+
role: this.role,
|
|
35941
|
+
port: this.opts.port
|
|
35942
|
+
});
|
|
35943
|
+
}
|
|
35944
|
+
}
|
|
35945
|
+
this.recordFailure(`capture_request_header bridge-down: ${err.message}`);
|
|
35946
|
+
throw new FetchproxyBridgeDownError({
|
|
35947
|
+
originalError: err.message,
|
|
35948
|
+
retryAttempted: false,
|
|
35949
|
+
op: "capture_request_header",
|
|
35950
|
+
url: resolved.urlPattern,
|
|
35951
|
+
role: this.role,
|
|
35952
|
+
port: this.opts.port
|
|
35953
|
+
});
|
|
35702
35954
|
}
|
|
35955
|
+
}
|
|
35956
|
+
async _captureRequestHeaderOnce(opts) {
|
|
35703
35957
|
const id = this.nextRequestId++;
|
|
35704
35958
|
const inner = {
|
|
35705
35959
|
type: "request",
|
|
@@ -35808,18 +36062,35 @@ var FetchproxyServer = class {
|
|
|
35808
36062
|
onInner(inner) {
|
|
35809
36063
|
if (inner.type !== "response")
|
|
35810
36064
|
return;
|
|
36065
|
+
this.lastExtensionMessageAt = Date.now();
|
|
35811
36066
|
const fetchCb = this.pending.get(inner.id);
|
|
35812
36067
|
if (fetchCb) {
|
|
35813
36068
|
this.pending.delete(inner.id);
|
|
35814
36069
|
if (inner.ok) {
|
|
35815
36070
|
if (inner.op === void 0 || inner.op === "fetch") {
|
|
35816
|
-
fetchCb({
|
|
36071
|
+
fetchCb({
|
|
36072
|
+
ok: true,
|
|
36073
|
+
status: inner.status,
|
|
36074
|
+
url: inner.url,
|
|
36075
|
+
body: inner.body,
|
|
36076
|
+
retryAttempted: false
|
|
36077
|
+
});
|
|
35817
36078
|
} else {
|
|
35818
36079
|
const error48 = `unexpected ${inner.op} response on fetch awaiter`;
|
|
35819
|
-
fetchCb({
|
|
36080
|
+
fetchCb({
|
|
36081
|
+
ok: false,
|
|
36082
|
+
error: error48,
|
|
36083
|
+
kind: classifyFetchError(error48),
|
|
36084
|
+
retryAttempted: false
|
|
36085
|
+
});
|
|
35820
36086
|
}
|
|
35821
36087
|
} else {
|
|
35822
|
-
fetchCb({
|
|
36088
|
+
fetchCb({
|
|
36089
|
+
ok: false,
|
|
36090
|
+
error: inner.error,
|
|
36091
|
+
kind: classifyFetchError(inner.error),
|
|
36092
|
+
retryAttempted: false
|
|
36093
|
+
});
|
|
35823
36094
|
}
|
|
35824
36095
|
return;
|
|
35825
36096
|
}
|
|
@@ -35889,7 +36160,12 @@ var FetchproxyServer = class {
|
|
|
35889
36160
|
rejectAllPending(reason = "extension disconnected") {
|
|
35890
36161
|
const err = new FetchproxyProtocolError(reason);
|
|
35891
36162
|
for (const cb of this.pending.values()) {
|
|
35892
|
-
cb({
|
|
36163
|
+
cb({
|
|
36164
|
+
ok: false,
|
|
36165
|
+
error: err.message,
|
|
36166
|
+
kind: classifyFetchError(err.message),
|
|
36167
|
+
retryAttempted: false
|
|
36168
|
+
});
|
|
35893
36169
|
}
|
|
35894
36170
|
this.pending.clear();
|
|
35895
36171
|
for (const cb of this.pendingReadCookies.values()) {
|
|
@@ -35954,6 +36230,19 @@ var FetchproxyServer = class {
|
|
|
35954
36230
|
}
|
|
35955
36231
|
};
|
|
35956
36232
|
|
|
36233
|
+
// node_modules/@fetchproxy/server/dist/classify-bridge-error.js
|
|
36234
|
+
function classifyBridgeError(err) {
|
|
36235
|
+
if (err instanceof FetchproxyTimeoutError)
|
|
36236
|
+
return "timeout";
|
|
36237
|
+
if (err instanceof FetchproxyBridgeDownError)
|
|
36238
|
+
return "bridge_down";
|
|
36239
|
+
if (err instanceof FetchproxyHttpError)
|
|
36240
|
+
return "http";
|
|
36241
|
+
if (err instanceof FetchproxyProtocolError)
|
|
36242
|
+
return "protocol";
|
|
36243
|
+
return "other";
|
|
36244
|
+
}
|
|
36245
|
+
|
|
35957
36246
|
// node_modules/@fetchproxy/bootstrap/dist/index.js
|
|
35958
36247
|
var defaultFactory = (opts) => new FetchproxyServer(opts);
|
|
35959
36248
|
async function bootstrap(opts) {
|
|
@@ -36008,7 +36297,11 @@ async function bootstrap(opts) {
|
|
|
36008
36297
|
key: p.storageKey,
|
|
36009
36298
|
jsonPointer: p.jsonPointer
|
|
36010
36299
|
})),
|
|
36011
|
-
onPairCode: opts.onPairCode
|
|
36300
|
+
onPairCode: opts.onPairCode,
|
|
36301
|
+
// 0.8.0+ pass-through. Only forwarded when the caller set them;
|
|
36302
|
+
// unset → server defaults apply (30000 / 2000 in 0.8.0).
|
|
36303
|
+
...opts.fetchTimeoutMs !== void 0 ? { fetchTimeoutMs: opts.fetchTimeoutMs } : {},
|
|
36304
|
+
...opts.bridgeReviveDelayMs !== void 0 ? { bridgeReviveDelayMs: opts.bridgeReviveDelayMs } : {}
|
|
36012
36305
|
});
|
|
36013
36306
|
const storageDomainOpts = {};
|
|
36014
36307
|
if (opts.storageDomain !== void 0)
|
|
@@ -36111,7 +36404,7 @@ var BootstrapDisabledError = class extends Error {
|
|
|
36111
36404
|
// package.json
|
|
36112
36405
|
var package_default = {
|
|
36113
36406
|
name: "zola-mcp",
|
|
36114
|
-
version: "1.
|
|
36407
|
+
version: "1.3.1",
|
|
36115
36408
|
mcpName: "io.github.chrischall/zola-mcp",
|
|
36116
36409
|
description: "Zola wedding MCP server for Claude",
|
|
36117
36410
|
author: "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -36157,7 +36450,8 @@ var package_default = {
|
|
|
36157
36450
|
"test:watch": "vitest"
|
|
36158
36451
|
},
|
|
36159
36452
|
dependencies: {
|
|
36160
|
-
"@fetchproxy/bootstrap": "^0.
|
|
36453
|
+
"@fetchproxy/bootstrap": "^0.8.0",
|
|
36454
|
+
"@fetchproxy/server": "^0.8.0",
|
|
36161
36455
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
36162
36456
|
dotenv: "^17.4.2"
|
|
36163
36457
|
},
|
|
@@ -36217,6 +36511,12 @@ async function resolveRefreshToken() {
|
|
|
36217
36511
|
}
|
|
36218
36512
|
return { token, source: "fetchproxy" };
|
|
36219
36513
|
} catch (e) {
|
|
36514
|
+
if (classifyBridgeError(e) === "bridge_down") {
|
|
36515
|
+
const downErr = e;
|
|
36516
|
+
throw new Error(
|
|
36517
|
+
`Zola auth: fetchproxy bridge is down (extension service worker unreachable after retry). ${downErr.hint}`
|
|
36518
|
+
);
|
|
36519
|
+
}
|
|
36220
36520
|
const msg = e instanceof Error ? e.message : String(e);
|
|
36221
36521
|
throw new Error(
|
|
36222
36522
|
`Zola auth: no ZOLA_REFRESH_TOKEN set, and fetchproxy fallback failed: ${msg}`
|
|
@@ -36280,6 +36580,19 @@ var ZolaClient = class {
|
|
|
36280
36580
|
await this.ensureSession();
|
|
36281
36581
|
return this.doRequest(method, path, body);
|
|
36282
36582
|
}
|
|
36583
|
+
/**
|
|
36584
|
+
* Like `requestMobile` but for endpoints that return a non-JSON body
|
|
36585
|
+
* (e.g. the QR-code preview which returns image bytes).
|
|
36586
|
+
* Sends `Accept: *\/*` (the JSON default would 406), and returns both the
|
|
36587
|
+
* raw bytes and the server's content-type so callers can pass it through.
|
|
36588
|
+
*/
|
|
36589
|
+
async requestMobileBinary(method, path, body) {
|
|
36590
|
+
await this.ensureSession();
|
|
36591
|
+
const response = await this.sendWithRetry(method, path, body, false, false, "*/*");
|
|
36592
|
+
const contentType = response.headers.get("content-type") ?? "application/octet-stream";
|
|
36593
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
36594
|
+
return { bytes, contentType };
|
|
36595
|
+
}
|
|
36283
36596
|
/**
|
|
36284
36597
|
* Get user context (wedding account ID, registry ID, etc.).
|
|
36285
36598
|
* Uses env vars as overrides; falls back to GET /v3/users/me/context.
|
|
@@ -36312,10 +36625,15 @@ var ZolaClient = class {
|
|
|
36312
36625
|
};
|
|
36313
36626
|
return this.cachedContext;
|
|
36314
36627
|
}
|
|
36315
|
-
async doRequest(method, path, body
|
|
36628
|
+
async doRequest(method, path, body) {
|
|
36629
|
+
const response = await this.sendWithRetry(method, path, body);
|
|
36630
|
+
const text = await response.text();
|
|
36631
|
+
return text ? JSON.parse(text) : null;
|
|
36632
|
+
}
|
|
36633
|
+
async sendWithRetry(method, path, body, isAuthRetry = false, isRateRetry = false, accept = "application/json") {
|
|
36316
36634
|
const sessionId = decodeJwtSessionId(this.sessionToken);
|
|
36317
36635
|
const headers = {
|
|
36318
|
-
accept
|
|
36636
|
+
accept,
|
|
36319
36637
|
authorization: `Bearer ${this.sessionToken}`,
|
|
36320
36638
|
"x-zola-platform-type": "iphone_app",
|
|
36321
36639
|
"x-zola-session-id": this.deviceSessionId,
|
|
@@ -36332,21 +36650,20 @@ var ZolaClient = class {
|
|
|
36332
36650
|
this.sessionToken = null;
|
|
36333
36651
|
this.sessionExpiry = null;
|
|
36334
36652
|
await this.refresh();
|
|
36335
|
-
return this.
|
|
36653
|
+
return this.sendWithRetry(method, path, body, true, isRateRetry, accept);
|
|
36336
36654
|
}
|
|
36337
36655
|
if (response.status === 429) {
|
|
36338
36656
|
if (!isRateRetry) {
|
|
36339
36657
|
await new Promise((r) => setTimeout(r, 2e3));
|
|
36340
|
-
return this.
|
|
36658
|
+
return this.sendWithRetry(method, path, body, isAuthRetry, true, accept);
|
|
36341
36659
|
}
|
|
36342
36660
|
throw new Error("Rate limited by Zola API");
|
|
36343
36661
|
}
|
|
36344
36662
|
if (!response.ok) {
|
|
36345
|
-
const
|
|
36346
|
-
throw new Error(`Zola API error: ${response.status} ${response.statusText} for ${method} ${path}: ${
|
|
36663
|
+
const text = await response.text();
|
|
36664
|
+
throw new Error(`Zola API error: ${response.status} ${response.statusText} for ${method} ${path}: ${text}`);
|
|
36347
36665
|
}
|
|
36348
|
-
|
|
36349
|
-
return text ? JSON.parse(text) : null;
|
|
36666
|
+
return response;
|
|
36350
36667
|
}
|
|
36351
36668
|
async ensureSession() {
|
|
36352
36669
|
if (this.sessionToken && this.sessionExpiry) {
|
|
@@ -36410,6 +36727,9 @@ var client = new ZolaClient();
|
|
|
36410
36727
|
function jsonResult(data) {
|
|
36411
36728
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
36412
36729
|
}
|
|
36730
|
+
function imageResult(bytes, mimeType) {
|
|
36731
|
+
return { content: [{ type: "image", data: Buffer.from(bytes).toString("base64"), mimeType }] };
|
|
36732
|
+
}
|
|
36413
36733
|
function pickDefined(base, args, keys) {
|
|
36414
36734
|
const body = { ...base };
|
|
36415
36735
|
for (const key of keys) {
|
|
@@ -37905,10 +38225,303 @@ function registerRegistryItemTools(server2) {
|
|
|
37905
38225
|
}, removeRegistryItem);
|
|
37906
38226
|
}
|
|
37907
38227
|
|
|
38228
|
+
// src/tools/invitations.ts
|
|
38229
|
+
async function listCardProjects(args) {
|
|
38230
|
+
const response = await client.requestMobile(
|
|
38231
|
+
"POST",
|
|
38232
|
+
"/v3/card-projects/search_request",
|
|
38233
|
+
{
|
|
38234
|
+
completed: args.include_completed ?? false,
|
|
38235
|
+
limit: args.limit ?? 30,
|
|
38236
|
+
card_suite_uuids: [],
|
|
38237
|
+
card_suite_ids: [],
|
|
38238
|
+
offset: 0,
|
|
38239
|
+
fetch_customizations: true,
|
|
38240
|
+
include_deleted: false,
|
|
38241
|
+
medium: ["PAPER", "MAGNET", "DIGITAL"],
|
|
38242
|
+
single_sample: false
|
|
38243
|
+
}
|
|
38244
|
+
);
|
|
38245
|
+
return jsonResult(response.data);
|
|
38246
|
+
}
|
|
38247
|
+
async function getCardProject(args) {
|
|
38248
|
+
const response = await client.requestMobile(
|
|
38249
|
+
"GET",
|
|
38250
|
+
`/v3/card-projects/${args.project_uuid}`
|
|
38251
|
+
);
|
|
38252
|
+
return jsonResult(response.data);
|
|
38253
|
+
}
|
|
38254
|
+
async function validateCardProject(args) {
|
|
38255
|
+
const response = await client.requestMobile(
|
|
38256
|
+
"GET",
|
|
38257
|
+
`/v3/card-projects/${args.project_uuid}/validate`
|
|
38258
|
+
);
|
|
38259
|
+
return jsonResult(response.data);
|
|
38260
|
+
}
|
|
38261
|
+
async function getCardProjectGuests(args) {
|
|
38262
|
+
const response = await client.requestMobile(
|
|
38263
|
+
"GET",
|
|
38264
|
+
`/v3/card-projects/${args.project_uuid}/project-guest-groups`
|
|
38265
|
+
);
|
|
38266
|
+
return jsonResult(response.data);
|
|
38267
|
+
}
|
|
38268
|
+
async function getCardSuite(args) {
|
|
38269
|
+
const response = await client.requestMobile(
|
|
38270
|
+
"POST",
|
|
38271
|
+
`/v4/card-catalog/suites/details/${args.suite_uuid}`
|
|
38272
|
+
);
|
|
38273
|
+
return jsonResult(response.data);
|
|
38274
|
+
}
|
|
38275
|
+
async function searchCardCatalog(args) {
|
|
38276
|
+
const response = await client.requestMobile(
|
|
38277
|
+
"POST",
|
|
38278
|
+
"/v3/card-catalog/search/faceted",
|
|
38279
|
+
{
|
|
38280
|
+
lead_card_types: [],
|
|
38281
|
+
include_updated_proof_module: true,
|
|
38282
|
+
limit: args.limit ?? 50,
|
|
38283
|
+
offset: 0,
|
|
38284
|
+
digital_suite: false,
|
|
38285
|
+
include_lead_card_type_metadata: true,
|
|
38286
|
+
lead_card_type: args.card_type ?? "INVITATION",
|
|
38287
|
+
include_module: true
|
|
38288
|
+
}
|
|
38289
|
+
);
|
|
38290
|
+
return jsonResult(response.data);
|
|
38291
|
+
}
|
|
38292
|
+
async function listFavoriteCardSuites() {
|
|
38293
|
+
const response = await client.requestMobile(
|
|
38294
|
+
"GET",
|
|
38295
|
+
"/v3/favorites/card-suites/"
|
|
38296
|
+
);
|
|
38297
|
+
return jsonResult(response.data);
|
|
38298
|
+
}
|
|
38299
|
+
async function getRsvpPage() {
|
|
38300
|
+
const { weddingAccountId } = await client.getContext();
|
|
38301
|
+
const response = await client.requestMobile(
|
|
38302
|
+
"GET",
|
|
38303
|
+
`/v3/websites/rsvps/wedding-accounts/${weddingAccountId}`
|
|
38304
|
+
);
|
|
38305
|
+
return jsonResult(response.data);
|
|
38306
|
+
}
|
|
38307
|
+
async function createCardProject(args) {
|
|
38308
|
+
const { weddingAccountId } = await client.getContext();
|
|
38309
|
+
const response = await client.requestMobile(
|
|
38310
|
+
"POST",
|
|
38311
|
+
"/v3/card-projects",
|
|
38312
|
+
{
|
|
38313
|
+
quantity: args.quantity ?? 150,
|
|
38314
|
+
lead_variation_uuid: args.lead_variation_uuid,
|
|
38315
|
+
extra_customizable: false,
|
|
38316
|
+
account_id: weddingAccountId,
|
|
38317
|
+
suite_uuid: args.suite_uuid
|
|
38318
|
+
}
|
|
38319
|
+
);
|
|
38320
|
+
return jsonResult(response.data);
|
|
38321
|
+
}
|
|
38322
|
+
async function swapCardProjectVariation(args) {
|
|
38323
|
+
const customizations = {};
|
|
38324
|
+
for (const [customizationUuid, variationUuid] of Object.entries(args.customizations)) {
|
|
38325
|
+
customizations[customizationUuid] = { variation_uuid: variationUuid };
|
|
38326
|
+
}
|
|
38327
|
+
const response = await client.requestMobile(
|
|
38328
|
+
"PUT",
|
|
38329
|
+
`/v3/card-projects/${args.project_uuid}`,
|
|
38330
|
+
{ customizations }
|
|
38331
|
+
);
|
|
38332
|
+
return jsonResult(response.data);
|
|
38333
|
+
}
|
|
38334
|
+
async function setCardProjectGuests(args) {
|
|
38335
|
+
const response = await client.requestMobile(
|
|
38336
|
+
"PUT",
|
|
38337
|
+
`/v3/card-projects/${args.project_uuid}/project-guest-groups`,
|
|
38338
|
+
{ guest_group_requests: args.guest_groups }
|
|
38339
|
+
);
|
|
38340
|
+
return jsonResult(response.data);
|
|
38341
|
+
}
|
|
38342
|
+
async function previewCardTemplate(args) {
|
|
38343
|
+
const substitutions = {};
|
|
38344
|
+
if (args.first_name !== void 0) substitutions.first_name = args.first_name;
|
|
38345
|
+
if (args.last_name !== void 0) substitutions.last_name = args.last_name;
|
|
38346
|
+
if (args.partner_first_name !== void 0) substitutions.partner_first_name = args.partner_first_name;
|
|
38347
|
+
if (args.partner_last_name !== void 0) substitutions.partner_last_name = args.partner_last_name;
|
|
38348
|
+
if (args.wedding_date !== void 0) {
|
|
38349
|
+
substitutions.wedding_date = args.wedding_date;
|
|
38350
|
+
} else {
|
|
38351
|
+
const { weddingDate } = await client.getContext();
|
|
38352
|
+
if (weddingDate) substitutions.wedding_date = weddingDate;
|
|
38353
|
+
}
|
|
38354
|
+
const response = await client.requestMobile(
|
|
38355
|
+
"POST",
|
|
38356
|
+
"/v3/card-templates/preview",
|
|
38357
|
+
{
|
|
38358
|
+
variation_uuids: args.variation_uuids,
|
|
38359
|
+
customizable: true,
|
|
38360
|
+
substitutions
|
|
38361
|
+
}
|
|
38362
|
+
);
|
|
38363
|
+
return jsonResult(response.data);
|
|
38364
|
+
}
|
|
38365
|
+
async function previewQrcode(args) {
|
|
38366
|
+
const { bytes, contentType } = await client.requestMobileBinary(
|
|
38367
|
+
"PUT",
|
|
38368
|
+
"/v3/card-projects/qrcode/preview",
|
|
38369
|
+
{
|
|
38370
|
+
dimension: args.dimension ?? "MEDIUM",
|
|
38371
|
+
url_type: args.url_type ?? "CUSTOM",
|
|
38372
|
+
enabled: args.enabled ?? true,
|
|
38373
|
+
url: args.url
|
|
38374
|
+
}
|
|
38375
|
+
);
|
|
38376
|
+
return imageResult(bytes, sniffImageType(bytes) ?? contentType);
|
|
38377
|
+
}
|
|
38378
|
+
function sniffImageType(bytes) {
|
|
38379
|
+
if (bytes.length >= 8 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) {
|
|
38380
|
+
return "image/png";
|
|
38381
|
+
}
|
|
38382
|
+
if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) {
|
|
38383
|
+
return "image/jpeg";
|
|
38384
|
+
}
|
|
38385
|
+
return null;
|
|
38386
|
+
}
|
|
38387
|
+
async function setCardProjectQrcode(args) {
|
|
38388
|
+
const response = await client.requestMobile(
|
|
38389
|
+
"PUT",
|
|
38390
|
+
`/v3/card-projects/${args.project_uuid}/customization/page/${args.page_uuid}/qrcode`,
|
|
38391
|
+
{
|
|
38392
|
+
dimension: args.dimension ?? "MEDIUM",
|
|
38393
|
+
url_type: args.url_type ?? "CUSTOM",
|
|
38394
|
+
color: args.color ?? "000000",
|
|
38395
|
+
enabled: args.enabled ?? true,
|
|
38396
|
+
url: args.url
|
|
38397
|
+
}
|
|
38398
|
+
);
|
|
38399
|
+
return jsonResult(response.data);
|
|
38400
|
+
}
|
|
38401
|
+
function registerInvitationTools(server2) {
|
|
38402
|
+
server2.registerTool("list_card_projects", {
|
|
38403
|
+
description: 'List your invitation / save-the-date / shower-invite "card" projects (paper or digital). Returns project UUID, name, customizations, suite, and quantity.',
|
|
38404
|
+
inputSchema: {
|
|
38405
|
+
include_completed: external_exports3.boolean().optional().describe("Include orders that have already been placed. Default: false (drafts only)."),
|
|
38406
|
+
limit: external_exports3.number().optional().describe("Max projects to return. Default: 30.")
|
|
38407
|
+
},
|
|
38408
|
+
annotations: { readOnlyHint: true }
|
|
38409
|
+
}, listCardProjects);
|
|
38410
|
+
server2.registerTool("get_card_project", {
|
|
38411
|
+
description: "Get full details for one invitation project including all customizations (invitation, envelope, RSVP card, details card), paper/color options, and per-customization pages.",
|
|
38412
|
+
inputSchema: {
|
|
38413
|
+
project_uuid: external_exports3.string().describe("Project UUID from list_card_projects")
|
|
38414
|
+
},
|
|
38415
|
+
annotations: { readOnlyHint: true }
|
|
38416
|
+
}, getCardProject);
|
|
38417
|
+
server2.registerTool("validate_card_project", {
|
|
38418
|
+
description: "Validate an invitation project \u2014 reports any text-fit, image, or guest-addressing errors per customization. Use before placing an order.",
|
|
38419
|
+
inputSchema: {
|
|
38420
|
+
project_uuid: external_exports3.string().describe("Project UUID from list_card_projects")
|
|
38421
|
+
},
|
|
38422
|
+
annotations: { readOnlyHint: true }
|
|
38423
|
+
}, validateCardProject);
|
|
38424
|
+
server2.registerTool("get_card_project_guests", {
|
|
38425
|
+
description: "List the guest groups assigned to an invitation project, including per-group font-size overrides for printed addressing.",
|
|
38426
|
+
inputSchema: {
|
|
38427
|
+
project_uuid: external_exports3.string().describe("Project UUID from list_card_projects")
|
|
38428
|
+
},
|
|
38429
|
+
annotations: { readOnlyHint: true }
|
|
38430
|
+
}, getCardProjectGuests);
|
|
38431
|
+
server2.registerTool("get_card_suite", {
|
|
38432
|
+
description: 'Get details for an invitation design "suite" (the family of matching invitation + RSVP + details cards), including paper types, sizes, and price range.',
|
|
38433
|
+
inputSchema: {
|
|
38434
|
+
suite_uuid: external_exports3.string().describe("Suite UUID (e.g. from search_card_catalog or list_favorite_card_suites)")
|
|
38435
|
+
},
|
|
38436
|
+
annotations: { readOnlyHint: true }
|
|
38437
|
+
}, getCardSuite);
|
|
38438
|
+
server2.registerTool("search_card_catalog", {
|
|
38439
|
+
description: "Search the invitation design catalog. Faceted search returning suites matching the requested card type.",
|
|
38440
|
+
inputSchema: {
|
|
38441
|
+
card_type: external_exports3.string().optional().describe("Lead card type: INVITATION (default), SAVE_THE_DATE, WEDDING_SHOWER_INVITATION, REHEARSAL_DINNER_INVITATION, THANK_YOU_CARD, etc."),
|
|
38442
|
+
limit: external_exports3.number().optional().describe("Max suites to return. Default: 50.")
|
|
38443
|
+
},
|
|
38444
|
+
annotations: { readOnlyHint: true }
|
|
38445
|
+
}, searchCardCatalog);
|
|
38446
|
+
server2.registerTool("list_favorite_card_suites", {
|
|
38447
|
+
description: "List invitation design suites you have favorited (hearted).",
|
|
38448
|
+
annotations: { readOnlyHint: true }
|
|
38449
|
+
}, listFavoriteCardSuites);
|
|
38450
|
+
server2.registerTool("get_rsvp_page", {
|
|
38451
|
+
description: "Get the RSVP page settings on the wedding website (title, intro copy, visibility, customization).",
|
|
38452
|
+
annotations: { readOnlyHint: true }
|
|
38453
|
+
}, getRsvpPage);
|
|
38454
|
+
server2.registerTool("create_card_project", {
|
|
38455
|
+
description: "Create a new invitation project from a design suite and a lead variation (specific size/paper).",
|
|
38456
|
+
inputSchema: {
|
|
38457
|
+
suite_uuid: external_exports3.string().describe("Suite UUID from search_card_catalog or get_card_suite"),
|
|
38458
|
+
lead_variation_uuid: external_exports3.string().describe("Lead variation UUID (specific size/paper/color from the suite)"),
|
|
38459
|
+
quantity: external_exports3.number().optional().describe("Quantity to order. Default: 150.")
|
|
38460
|
+
},
|
|
38461
|
+
annotations: { destructiveHint: false }
|
|
38462
|
+
}, createCardProject);
|
|
38463
|
+
server2.registerTool("swap_card_project_variation", {
|
|
38464
|
+
description: "Swap one or more customizations on a project to different variations (e.g. switch paper type, color, or size). Pass a map of customization UUID \u2192 new variation UUID.",
|
|
38465
|
+
inputSchema: {
|
|
38466
|
+
project_uuid: external_exports3.string().describe("Project UUID from list_card_projects"),
|
|
38467
|
+
customizations: external_exports3.record(external_exports3.string(), external_exports3.string()).describe("{ customization_uuid: new_variation_uuid }")
|
|
38468
|
+
},
|
|
38469
|
+
annotations: { destructiveHint: false }
|
|
38470
|
+
}, swapCardProjectVariation);
|
|
38471
|
+
server2.registerTool("set_card_project_guests", {
|
|
38472
|
+
description: "Enable / disable guest groups for an invitation project and optionally override font sizes for printed names and addresses. Pass the full list of guest groups you want recorded.",
|
|
38473
|
+
inputSchema: {
|
|
38474
|
+
project_uuid: external_exports3.string().describe("Project UUID from list_card_projects"),
|
|
38475
|
+
guest_groups: external_exports3.array(external_exports3.object({
|
|
38476
|
+
guest_group_id: external_exports3.number().describe("Guest group ID from list_guests / get_card_project_guests"),
|
|
38477
|
+
enabled: external_exports3.boolean().describe("Whether to include this group in printed addressing"),
|
|
38478
|
+
guest_name_font_size_override: external_exports3.number().optional().describe("Override font size for the guest name (pt)"),
|
|
38479
|
+
address_font_size_override: external_exports3.number().optional().describe("Override font size for the address (pt)")
|
|
38480
|
+
})).describe("Full list of guest groups to record. Omitted groups are not affected by this call.")
|
|
38481
|
+
},
|
|
38482
|
+
annotations: { destructiveHint: false }
|
|
38483
|
+
}, setCardProjectGuests);
|
|
38484
|
+
server2.registerTool("preview_card_template", {
|
|
38485
|
+
description: "Render an invitation template preview with text substituted in. Use to see how a design would look with your couple's names and wedding date. Returns the template structure including page layouts.",
|
|
38486
|
+
inputSchema: {
|
|
38487
|
+
variation_uuids: external_exports3.array(external_exports3.string()).describe("One or more variation UUIDs to preview (e.g. a specific size+paper of an invitation)"),
|
|
38488
|
+
first_name: external_exports3.string().optional().describe("Substitute for {{first_name}} placeholders"),
|
|
38489
|
+
last_name: external_exports3.string().optional().describe("Substitute for {{last_name}}"),
|
|
38490
|
+
partner_first_name: external_exports3.string().optional().describe("Substitute for {{partner_first_name}}"),
|
|
38491
|
+
partner_last_name: external_exports3.string().optional().describe("Substitute for {{partner_last_name}}"),
|
|
38492
|
+
wedding_date: external_exports3.string().optional().describe("Substitute for {{wedding_date}}, YYYY-MM-DD. Defaults to the wedding date on file.")
|
|
38493
|
+
},
|
|
38494
|
+
annotations: { readOnlyHint: true }
|
|
38495
|
+
}, previewCardTemplate);
|
|
38496
|
+
server2.registerTool("preview_qrcode", {
|
|
38497
|
+
description: "Generate a QR-code PNG for an invitation. Returns the image so it can be inspected. Use set_card_project_qrcode to actually place it on a card.",
|
|
38498
|
+
inputSchema: {
|
|
38499
|
+
url: external_exports3.string().describe("URL the QR code will resolve to"),
|
|
38500
|
+
dimension: external_exports3.string().optional().describe("SMALL | MEDIUM (default) | LARGE"),
|
|
38501
|
+
url_type: external_exports3.string().optional().describe("CUSTOM (default) | WEDDING_WEBSITE | WEDDING_WEBSITE_RSVP"),
|
|
38502
|
+
enabled: external_exports3.boolean().optional().describe("Whether the QR code is enabled. Default: true.")
|
|
38503
|
+
},
|
|
38504
|
+
annotations: { readOnlyHint: true }
|
|
38505
|
+
}, previewQrcode);
|
|
38506
|
+
server2.registerTool("set_card_project_qrcode", {
|
|
38507
|
+
description: "Place (or update) a QR code on a specific page of an invitation project. The page UUID comes from get_card_project (look under the customization's pages array).",
|
|
38508
|
+
inputSchema: {
|
|
38509
|
+
project_uuid: external_exports3.string().describe("Project UUID from list_card_projects"),
|
|
38510
|
+
page_uuid: external_exports3.string().describe("Page UUID of the customization to put the QR on"),
|
|
38511
|
+
url: external_exports3.string().describe("URL the QR code will resolve to"),
|
|
38512
|
+
dimension: external_exports3.string().optional().describe("SMALL | MEDIUM (default) | LARGE"),
|
|
38513
|
+
url_type: external_exports3.string().optional().describe("CUSTOM (default) | WEDDING_WEBSITE | WEDDING_WEBSITE_RSVP"),
|
|
38514
|
+
color: external_exports3.string().optional().describe("Hex color (no #). Default: 000000"),
|
|
38515
|
+
enabled: external_exports3.boolean().optional().describe("Whether to enable the QR code. Default: true.")
|
|
38516
|
+
},
|
|
38517
|
+
annotations: { destructiveHint: false }
|
|
38518
|
+
}, setCardProjectQrcode);
|
|
38519
|
+
}
|
|
38520
|
+
|
|
37908
38521
|
// src/index.ts
|
|
37909
38522
|
var server = new McpServer({
|
|
37910
38523
|
name: "zola-mcp",
|
|
37911
|
-
version: "1.
|
|
38524
|
+
version: "1.3.1"
|
|
37912
38525
|
// x-release-please-version
|
|
37913
38526
|
});
|
|
37914
38527
|
registerVendorTools(server);
|
|
@@ -37922,5 +38535,6 @@ registerWebsiteTools(server);
|
|
|
37922
38535
|
registerWebsiteContentTools(server);
|
|
37923
38536
|
registerWebsiteThemeTools(server);
|
|
37924
38537
|
registerRegistryItemTools(server);
|
|
38538
|
+
registerInvitationTools(server);
|
|
37925
38539
|
var transport = new StdioServerTransport();
|
|
37926
38540
|
await server.connect(transport);
|
package/dist/client.js
CHANGED
|
@@ -65,6 +65,19 @@ export class ZolaClient {
|
|
|
65
65
|
await this.ensureSession();
|
|
66
66
|
return this.doRequest(method, path, body);
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Like `requestMobile` but for endpoints that return a non-JSON body
|
|
70
|
+
* (e.g. the QR-code preview which returns image bytes).
|
|
71
|
+
* Sends `Accept: *\/*` (the JSON default would 406), and returns both the
|
|
72
|
+
* raw bytes and the server's content-type so callers can pass it through.
|
|
73
|
+
*/
|
|
74
|
+
async requestMobileBinary(method, path, body) {
|
|
75
|
+
await this.ensureSession();
|
|
76
|
+
const response = await this.sendWithRetry(method, path, body, false, false, '*/*');
|
|
77
|
+
const contentType = response.headers.get('content-type') ?? 'application/octet-stream';
|
|
78
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
79
|
+
return { bytes, contentType };
|
|
80
|
+
}
|
|
68
81
|
/**
|
|
69
82
|
* Get user context (wedding account ID, registry ID, etc.).
|
|
70
83
|
* Uses env vars as overrides; falls back to GET /v3/users/me/context.
|
|
@@ -99,10 +112,15 @@ export class ZolaClient {
|
|
|
99
112
|
};
|
|
100
113
|
return this.cachedContext;
|
|
101
114
|
}
|
|
102
|
-
async doRequest(method, path, body
|
|
115
|
+
async doRequest(method, path, body) {
|
|
116
|
+
const response = await this.sendWithRetry(method, path, body);
|
|
117
|
+
const text = await response.text();
|
|
118
|
+
return (text ? JSON.parse(text) : null);
|
|
119
|
+
}
|
|
120
|
+
async sendWithRetry(method, path, body, isAuthRetry = false, isRateRetry = false, accept = 'application/json') {
|
|
103
121
|
const sessionId = decodeJwtSessionId(this.sessionToken);
|
|
104
122
|
const headers = {
|
|
105
|
-
accept
|
|
123
|
+
accept,
|
|
106
124
|
authorization: `Bearer ${this.sessionToken}`,
|
|
107
125
|
'x-zola-platform-type': 'iphone_app',
|
|
108
126
|
'x-zola-session-id': this.deviceSessionId,
|
|
@@ -120,12 +138,12 @@ export class ZolaClient {
|
|
|
120
138
|
this.sessionToken = null;
|
|
121
139
|
this.sessionExpiry = null;
|
|
122
140
|
await this.refresh();
|
|
123
|
-
return this.
|
|
141
|
+
return this.sendWithRetry(method, path, body, true, isRateRetry, accept);
|
|
124
142
|
}
|
|
125
143
|
if (response.status === 429) {
|
|
126
144
|
if (!isRateRetry) {
|
|
127
145
|
await new Promise((r) => setTimeout(r, 2000));
|
|
128
|
-
return this.
|
|
146
|
+
return this.sendWithRetry(method, path, body, isAuthRetry, true, accept);
|
|
129
147
|
}
|
|
130
148
|
throw new Error('Rate limited by Zola API');
|
|
131
149
|
}
|
|
@@ -133,8 +151,7 @@ export class ZolaClient {
|
|
|
133
151
|
const text = await response.text();
|
|
134
152
|
throw new Error(`Zola API error: ${response.status} ${response.statusText} for ${method} ${path}: ${text}`);
|
|
135
153
|
}
|
|
136
|
-
|
|
137
|
-
return (text ? JSON.parse(text) : null);
|
|
154
|
+
return response;
|
|
138
155
|
}
|
|
139
156
|
async ensureSession() {
|
|
140
157
|
// Session still valid with comfortable margin
|
package/dist/index.js
CHANGED
|
@@ -11,9 +11,10 @@ import { registerWebsiteTools } from './tools/website.js';
|
|
|
11
11
|
import { registerWebsiteContentTools } from './tools/website-content.js';
|
|
12
12
|
import { registerWebsiteThemeTools } from './tools/website-theme.js';
|
|
13
13
|
import { registerRegistryItemTools } from './tools/registry-items.js';
|
|
14
|
+
import { registerInvitationTools } from './tools/invitations.js';
|
|
14
15
|
const server = new McpServer({
|
|
15
16
|
name: 'zola-mcp',
|
|
16
|
-
version: '1.
|
|
17
|
+
version: '1.3.1', // x-release-please-version
|
|
17
18
|
});
|
|
18
19
|
registerVendorTools(server);
|
|
19
20
|
registerBudgetTools(server);
|
|
@@ -26,5 +27,6 @@ registerWebsiteTools(server);
|
|
|
26
27
|
registerWebsiteContentTools(server);
|
|
27
28
|
registerWebsiteThemeTools(server);
|
|
28
29
|
registerRegistryItemTools(server);
|
|
30
|
+
registerInvitationTools(server);
|
|
29
31
|
const transport = new StdioServerTransport();
|
|
30
32
|
await server.connect(transport);
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { client } from '../client.js';
|
|
3
|
+
import { jsonResult, imageResult } from '../types.js';
|
|
4
|
+
// ─── Tier 1: read-only ───────────────────────────────────────────────────────
|
|
5
|
+
export async function listCardProjects(args) {
|
|
6
|
+
const response = await client.requestMobile('POST', '/v3/card-projects/search_request', {
|
|
7
|
+
completed: args.include_completed ?? false,
|
|
8
|
+
limit: args.limit ?? 30,
|
|
9
|
+
card_suite_uuids: [],
|
|
10
|
+
card_suite_ids: [],
|
|
11
|
+
offset: 0,
|
|
12
|
+
fetch_customizations: true,
|
|
13
|
+
include_deleted: false,
|
|
14
|
+
medium: ['PAPER', 'MAGNET', 'DIGITAL'],
|
|
15
|
+
single_sample: false,
|
|
16
|
+
});
|
|
17
|
+
return jsonResult(response.data);
|
|
18
|
+
}
|
|
19
|
+
export async function getCardProject(args) {
|
|
20
|
+
const response = await client.requestMobile('GET', `/v3/card-projects/${args.project_uuid}`);
|
|
21
|
+
return jsonResult(response.data);
|
|
22
|
+
}
|
|
23
|
+
export async function validateCardProject(args) {
|
|
24
|
+
const response = await client.requestMobile('GET', `/v3/card-projects/${args.project_uuid}/validate`);
|
|
25
|
+
return jsonResult(response.data);
|
|
26
|
+
}
|
|
27
|
+
export async function getCardProjectGuests(args) {
|
|
28
|
+
const response = await client.requestMobile('GET', `/v3/card-projects/${args.project_uuid}/project-guest-groups`);
|
|
29
|
+
return jsonResult(response.data);
|
|
30
|
+
}
|
|
31
|
+
export async function getCardSuite(args) {
|
|
32
|
+
const response = await client.requestMobile('POST', `/v4/card-catalog/suites/details/${args.suite_uuid}`);
|
|
33
|
+
return jsonResult(response.data);
|
|
34
|
+
}
|
|
35
|
+
export async function searchCardCatalog(args) {
|
|
36
|
+
const response = await client.requestMobile('POST', '/v3/card-catalog/search/faceted', {
|
|
37
|
+
lead_card_types: [],
|
|
38
|
+
include_updated_proof_module: true,
|
|
39
|
+
limit: args.limit ?? 50,
|
|
40
|
+
offset: 0,
|
|
41
|
+
digital_suite: false,
|
|
42
|
+
include_lead_card_type_metadata: true,
|
|
43
|
+
lead_card_type: args.card_type ?? 'INVITATION',
|
|
44
|
+
include_module: true,
|
|
45
|
+
});
|
|
46
|
+
return jsonResult(response.data);
|
|
47
|
+
}
|
|
48
|
+
export async function listFavoriteCardSuites() {
|
|
49
|
+
const response = await client.requestMobile('GET', '/v3/favorites/card-suites/');
|
|
50
|
+
return jsonResult(response.data);
|
|
51
|
+
}
|
|
52
|
+
export async function getRsvpPage() {
|
|
53
|
+
const { weddingAccountId } = await client.getContext();
|
|
54
|
+
const response = await client.requestMobile('GET', `/v3/websites/rsvps/wedding-accounts/${weddingAccountId}`);
|
|
55
|
+
return jsonResult(response.data);
|
|
56
|
+
}
|
|
57
|
+
// ─── Tier 2: writes ──────────────────────────────────────────────────────────
|
|
58
|
+
export async function createCardProject(args) {
|
|
59
|
+
const { weddingAccountId } = await client.getContext();
|
|
60
|
+
const response = await client.requestMobile('POST', '/v3/card-projects', {
|
|
61
|
+
quantity: args.quantity ?? 150,
|
|
62
|
+
lead_variation_uuid: args.lead_variation_uuid,
|
|
63
|
+
extra_customizable: false,
|
|
64
|
+
account_id: weddingAccountId,
|
|
65
|
+
suite_uuid: args.suite_uuid,
|
|
66
|
+
});
|
|
67
|
+
return jsonResult(response.data);
|
|
68
|
+
}
|
|
69
|
+
export async function swapCardProjectVariation(args) {
|
|
70
|
+
const customizations = {};
|
|
71
|
+
for (const [customizationUuid, variationUuid] of Object.entries(args.customizations)) {
|
|
72
|
+
customizations[customizationUuid] = { variation_uuid: variationUuid };
|
|
73
|
+
}
|
|
74
|
+
const response = await client.requestMobile('PUT', `/v3/card-projects/${args.project_uuid}`, { customizations });
|
|
75
|
+
return jsonResult(response.data);
|
|
76
|
+
}
|
|
77
|
+
export async function setCardProjectGuests(args) {
|
|
78
|
+
const response = await client.requestMobile('PUT', `/v3/card-projects/${args.project_uuid}/project-guest-groups`, { guest_group_requests: args.guest_groups });
|
|
79
|
+
return jsonResult(response.data);
|
|
80
|
+
}
|
|
81
|
+
export async function previewCardTemplate(args) {
|
|
82
|
+
const substitutions = {};
|
|
83
|
+
if (args.first_name !== undefined)
|
|
84
|
+
substitutions.first_name = args.first_name;
|
|
85
|
+
if (args.last_name !== undefined)
|
|
86
|
+
substitutions.last_name = args.last_name;
|
|
87
|
+
if (args.partner_first_name !== undefined)
|
|
88
|
+
substitutions.partner_first_name = args.partner_first_name;
|
|
89
|
+
if (args.partner_last_name !== undefined)
|
|
90
|
+
substitutions.partner_last_name = args.partner_last_name;
|
|
91
|
+
if (args.wedding_date !== undefined) {
|
|
92
|
+
substitutions.wedding_date = args.wedding_date;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
const { weddingDate } = await client.getContext();
|
|
96
|
+
if (weddingDate)
|
|
97
|
+
substitutions.wedding_date = weddingDate;
|
|
98
|
+
}
|
|
99
|
+
const response = await client.requestMobile('POST', '/v3/card-templates/preview', {
|
|
100
|
+
variation_uuids: args.variation_uuids,
|
|
101
|
+
customizable: true,
|
|
102
|
+
substitutions,
|
|
103
|
+
});
|
|
104
|
+
return jsonResult(response.data);
|
|
105
|
+
}
|
|
106
|
+
// ─── QR code ─────────────────────────────────────────────────────────────────
|
|
107
|
+
export async function previewQrcode(args) {
|
|
108
|
+
const { bytes, contentType } = await client.requestMobileBinary('PUT', '/v3/card-projects/qrcode/preview', {
|
|
109
|
+
dimension: args.dimension ?? 'MEDIUM',
|
|
110
|
+
url_type: args.url_type ?? 'CUSTOM',
|
|
111
|
+
enabled: args.enabled ?? true,
|
|
112
|
+
url: args.url,
|
|
113
|
+
});
|
|
114
|
+
// Zola's qrcode/preview returns PNG bytes with `Content-Type: image/jpeg`. Sniff magic bytes.
|
|
115
|
+
return imageResult(bytes, sniffImageType(bytes) ?? contentType);
|
|
116
|
+
}
|
|
117
|
+
function sniffImageType(bytes) {
|
|
118
|
+
if (bytes.length >= 8 &&
|
|
119
|
+
bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) {
|
|
120
|
+
return 'image/png';
|
|
121
|
+
}
|
|
122
|
+
if (bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) {
|
|
123
|
+
return 'image/jpeg';
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
export async function setCardProjectQrcode(args) {
|
|
128
|
+
const response = await client.requestMobile('PUT', `/v3/card-projects/${args.project_uuid}/customization/page/${args.page_uuid}/qrcode`, {
|
|
129
|
+
dimension: args.dimension ?? 'MEDIUM',
|
|
130
|
+
url_type: args.url_type ?? 'CUSTOM',
|
|
131
|
+
color: args.color ?? '000000',
|
|
132
|
+
enabled: args.enabled ?? true,
|
|
133
|
+
url: args.url,
|
|
134
|
+
});
|
|
135
|
+
return jsonResult(response.data);
|
|
136
|
+
}
|
|
137
|
+
// ─── MCP registration ────────────────────────────────────────────────────────
|
|
138
|
+
export function registerInvitationTools(server) {
|
|
139
|
+
server.registerTool('list_card_projects', {
|
|
140
|
+
description: 'List your invitation / save-the-date / shower-invite "card" projects (paper or digital). Returns project UUID, name, customizations, suite, and quantity.',
|
|
141
|
+
inputSchema: {
|
|
142
|
+
include_completed: z.boolean().optional().describe('Include orders that have already been placed. Default: false (drafts only).'),
|
|
143
|
+
limit: z.number().optional().describe('Max projects to return. Default: 30.'),
|
|
144
|
+
},
|
|
145
|
+
annotations: { readOnlyHint: true },
|
|
146
|
+
}, listCardProjects);
|
|
147
|
+
server.registerTool('get_card_project', {
|
|
148
|
+
description: 'Get full details for one invitation project including all customizations (invitation, envelope, RSVP card, details card), paper/color options, and per-customization pages.',
|
|
149
|
+
inputSchema: {
|
|
150
|
+
project_uuid: z.string().describe('Project UUID from list_card_projects'),
|
|
151
|
+
},
|
|
152
|
+
annotations: { readOnlyHint: true },
|
|
153
|
+
}, getCardProject);
|
|
154
|
+
server.registerTool('validate_card_project', {
|
|
155
|
+
description: 'Validate an invitation project — reports any text-fit, image, or guest-addressing errors per customization. Use before placing an order.',
|
|
156
|
+
inputSchema: {
|
|
157
|
+
project_uuid: z.string().describe('Project UUID from list_card_projects'),
|
|
158
|
+
},
|
|
159
|
+
annotations: { readOnlyHint: true },
|
|
160
|
+
}, validateCardProject);
|
|
161
|
+
server.registerTool('get_card_project_guests', {
|
|
162
|
+
description: 'List the guest groups assigned to an invitation project, including per-group font-size overrides for printed addressing.',
|
|
163
|
+
inputSchema: {
|
|
164
|
+
project_uuid: z.string().describe('Project UUID from list_card_projects'),
|
|
165
|
+
},
|
|
166
|
+
annotations: { readOnlyHint: true },
|
|
167
|
+
}, getCardProjectGuests);
|
|
168
|
+
server.registerTool('get_card_suite', {
|
|
169
|
+
description: 'Get details for an invitation design "suite" (the family of matching invitation + RSVP + details cards), including paper types, sizes, and price range.',
|
|
170
|
+
inputSchema: {
|
|
171
|
+
suite_uuid: z.string().describe('Suite UUID (e.g. from search_card_catalog or list_favorite_card_suites)'),
|
|
172
|
+
},
|
|
173
|
+
annotations: { readOnlyHint: true },
|
|
174
|
+
}, getCardSuite);
|
|
175
|
+
server.registerTool('search_card_catalog', {
|
|
176
|
+
description: 'Search the invitation design catalog. Faceted search returning suites matching the requested card type.',
|
|
177
|
+
inputSchema: {
|
|
178
|
+
card_type: z.string().optional().describe('Lead card type: INVITATION (default), SAVE_THE_DATE, WEDDING_SHOWER_INVITATION, REHEARSAL_DINNER_INVITATION, THANK_YOU_CARD, etc.'),
|
|
179
|
+
limit: z.number().optional().describe('Max suites to return. Default: 50.'),
|
|
180
|
+
},
|
|
181
|
+
annotations: { readOnlyHint: true },
|
|
182
|
+
}, searchCardCatalog);
|
|
183
|
+
server.registerTool('list_favorite_card_suites', {
|
|
184
|
+
description: 'List invitation design suites you have favorited (hearted).',
|
|
185
|
+
annotations: { readOnlyHint: true },
|
|
186
|
+
}, listFavoriteCardSuites);
|
|
187
|
+
server.registerTool('get_rsvp_page', {
|
|
188
|
+
description: 'Get the RSVP page settings on the wedding website (title, intro copy, visibility, customization).',
|
|
189
|
+
annotations: { readOnlyHint: true },
|
|
190
|
+
}, getRsvpPage);
|
|
191
|
+
server.registerTool('create_card_project', {
|
|
192
|
+
description: 'Create a new invitation project from a design suite and a lead variation (specific size/paper).',
|
|
193
|
+
inputSchema: {
|
|
194
|
+
suite_uuid: z.string().describe('Suite UUID from search_card_catalog or get_card_suite'),
|
|
195
|
+
lead_variation_uuid: z.string().describe('Lead variation UUID (specific size/paper/color from the suite)'),
|
|
196
|
+
quantity: z.number().optional().describe('Quantity to order. Default: 150.'),
|
|
197
|
+
},
|
|
198
|
+
annotations: { destructiveHint: false },
|
|
199
|
+
}, createCardProject);
|
|
200
|
+
server.registerTool('swap_card_project_variation', {
|
|
201
|
+
description: 'Swap one or more customizations on a project to different variations (e.g. switch paper type, color, or size). Pass a map of customization UUID → new variation UUID.',
|
|
202
|
+
inputSchema: {
|
|
203
|
+
project_uuid: z.string().describe('Project UUID from list_card_projects'),
|
|
204
|
+
customizations: z.record(z.string(), z.string()).describe('{ customization_uuid: new_variation_uuid }'),
|
|
205
|
+
},
|
|
206
|
+
annotations: { destructiveHint: false },
|
|
207
|
+
}, swapCardProjectVariation);
|
|
208
|
+
server.registerTool('set_card_project_guests', {
|
|
209
|
+
description: 'Enable / disable guest groups for an invitation project and optionally override font sizes for printed names and addresses. Pass the full list of guest groups you want recorded.',
|
|
210
|
+
inputSchema: {
|
|
211
|
+
project_uuid: z.string().describe('Project UUID from list_card_projects'),
|
|
212
|
+
guest_groups: z.array(z.object({
|
|
213
|
+
guest_group_id: z.number().describe('Guest group ID from list_guests / get_card_project_guests'),
|
|
214
|
+
enabled: z.boolean().describe('Whether to include this group in printed addressing'),
|
|
215
|
+
guest_name_font_size_override: z.number().optional().describe('Override font size for the guest name (pt)'),
|
|
216
|
+
address_font_size_override: z.number().optional().describe('Override font size for the address (pt)'),
|
|
217
|
+
})).describe('Full list of guest groups to record. Omitted groups are not affected by this call.'),
|
|
218
|
+
},
|
|
219
|
+
annotations: { destructiveHint: false },
|
|
220
|
+
}, setCardProjectGuests);
|
|
221
|
+
server.registerTool('preview_card_template', {
|
|
222
|
+
description: 'Render an invitation template preview with text substituted in. Use to see how a design would look with your couple\'s names and wedding date. Returns the template structure including page layouts.',
|
|
223
|
+
inputSchema: {
|
|
224
|
+
variation_uuids: z.array(z.string()).describe('One or more variation UUIDs to preview (e.g. a specific size+paper of an invitation)'),
|
|
225
|
+
first_name: z.string().optional().describe('Substitute for {{first_name}} placeholders'),
|
|
226
|
+
last_name: z.string().optional().describe('Substitute for {{last_name}}'),
|
|
227
|
+
partner_first_name: z.string().optional().describe('Substitute for {{partner_first_name}}'),
|
|
228
|
+
partner_last_name: z.string().optional().describe('Substitute for {{partner_last_name}}'),
|
|
229
|
+
wedding_date: z.string().optional().describe('Substitute for {{wedding_date}}, YYYY-MM-DD. Defaults to the wedding date on file.'),
|
|
230
|
+
},
|
|
231
|
+
annotations: { readOnlyHint: true },
|
|
232
|
+
}, previewCardTemplate);
|
|
233
|
+
server.registerTool('preview_qrcode', {
|
|
234
|
+
description: 'Generate a QR-code PNG for an invitation. Returns the image so it can be inspected. Use set_card_project_qrcode to actually place it on a card.',
|
|
235
|
+
inputSchema: {
|
|
236
|
+
url: z.string().describe('URL the QR code will resolve to'),
|
|
237
|
+
dimension: z.string().optional().describe('SMALL | MEDIUM (default) | LARGE'),
|
|
238
|
+
url_type: z.string().optional().describe('CUSTOM (default) | WEDDING_WEBSITE | WEDDING_WEBSITE_RSVP'),
|
|
239
|
+
enabled: z.boolean().optional().describe('Whether the QR code is enabled. Default: true.'),
|
|
240
|
+
},
|
|
241
|
+
annotations: { readOnlyHint: true },
|
|
242
|
+
}, previewQrcode);
|
|
243
|
+
server.registerTool('set_card_project_qrcode', {
|
|
244
|
+
description: 'Place (or update) a QR code on a specific page of an invitation project. The page UUID comes from get_card_project (look under the customization\'s pages array).',
|
|
245
|
+
inputSchema: {
|
|
246
|
+
project_uuid: z.string().describe('Project UUID from list_card_projects'),
|
|
247
|
+
page_uuid: z.string().describe('Page UUID of the customization to put the QR on'),
|
|
248
|
+
url: z.string().describe('URL the QR code will resolve to'),
|
|
249
|
+
dimension: z.string().optional().describe('SMALL | MEDIUM (default) | LARGE'),
|
|
250
|
+
url_type: z.string().optional().describe('CUSTOM (default) | WEDDING_WEBSITE | WEDDING_WEBSITE_RSVP'),
|
|
251
|
+
color: z.string().optional().describe('Hex color (no #). Default: 000000'),
|
|
252
|
+
enabled: z.boolean().optional().describe('Whether to enable the QR code. Default: true.'),
|
|
253
|
+
},
|
|
254
|
+
annotations: { destructiveHint: false },
|
|
255
|
+
}, setCardProjectQrcode);
|
|
256
|
+
}
|
package/dist/types.js
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
export function jsonResult(data) {
|
|
3
3
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
4
4
|
}
|
|
5
|
+
/** Wrap raw image bytes as an MCP image-content tool result. */
|
|
6
|
+
export function imageResult(bytes, mimeType) {
|
|
7
|
+
return { content: [{ type: 'image', data: Buffer.from(bytes).toString('base64'), mimeType }] };
|
|
8
|
+
}
|
|
5
9
|
/**
|
|
6
10
|
* Build a partial-update body containing only keys from `args` that are not undefined.
|
|
7
11
|
* Used by tools that send PATCH-style updates where omitting a key means "leave unchanged".
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zola-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
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,8 @@
|
|
|
46
46
|
"test:watch": "vitest"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@fetchproxy/bootstrap": "^0.
|
|
49
|
+
"@fetchproxy/bootstrap": "^0.8.0",
|
|
50
|
+
"@fetchproxy/server": "^0.8.0",
|
|
50
51
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
51
52
|
"dotenv": "^17.4.2"
|
|
52
53
|
},
|
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.
|
|
9
|
+
"version": "1.3.1",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "zola-mcp",
|
|
14
|
-
"version": "1.
|
|
14
|
+
"version": "1.3.1",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|