zola-mcp 1.3.2 → 1.4.0
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/bundle.js +502 -38
- package/dist/index.js +3 -1
- package/dist/tools/event-invitations.js +171 -0
- package/dist/tools/guests.js +31 -26
- package/package.json +9 -8
- package/server.json +2 -2
- package/skills/zola/SKILL.md +4 -1
|
@@ -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.4.0"
|
|
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.4.0",
|
|
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.4.0",
|
|
5
5
|
"description": "Zola wedding planning tools for Claude — vendors, budget, guests, seating, events, registry, inquiries, and more via MCP",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Chris Chall",
|
package/dist/bundle.js
CHANGED
|
@@ -34809,7 +34809,9 @@ var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
|
34809
34809
|
"read_local_storage",
|
|
34810
34810
|
"read_session_storage",
|
|
34811
34811
|
"capture_request_header",
|
|
34812
|
-
"
|
|
34812
|
+
"capture_redirect",
|
|
34813
|
+
"read_indexed_db",
|
|
34814
|
+
"download"
|
|
34813
34815
|
]);
|
|
34814
34816
|
|
|
34815
34817
|
// node_modules/@fetchproxy/protocol/dist/mcp-id.js
|
|
@@ -35369,6 +35371,31 @@ function validateInnerRequest(raw) {
|
|
|
35369
35371
|
}
|
|
35370
35372
|
return raw;
|
|
35371
35373
|
}
|
|
35374
|
+
if (raw.op === "capture_redirect") {
|
|
35375
|
+
assertObject(raw.init, "inner.init");
|
|
35376
|
+
if (raw.init.host === void 0) {
|
|
35377
|
+
throw new ProtocolError("inner.init.host: missing");
|
|
35378
|
+
}
|
|
35379
|
+
assertString(raw.init.host, "inner.init.host");
|
|
35380
|
+
if (!HOSTNAME_RE.test(raw.init.host)) {
|
|
35381
|
+
throw new ProtocolError(`inner.init.host: invalid hostname ${JSON.stringify(raw.init.host)}`);
|
|
35382
|
+
}
|
|
35383
|
+
if (raw.init.path !== void 0) {
|
|
35384
|
+
assertString(raw.init.path, "inner.init.path");
|
|
35385
|
+
if (!CAPTURE_PATH_RE.test(raw.init.path)) {
|
|
35386
|
+
throw new ProtocolError(`inner.init.path: must start with '/' ${JSON.stringify(raw.init.path)}`);
|
|
35387
|
+
}
|
|
35388
|
+
}
|
|
35389
|
+
if (raw.init.timeoutMs !== void 0) {
|
|
35390
|
+
assertPositiveInt(raw.init.timeoutMs, "inner.init.timeoutMs");
|
|
35391
|
+
}
|
|
35392
|
+
for (const k of Object.keys(raw.init)) {
|
|
35393
|
+
if (k !== "host" && k !== "path" && k !== "timeoutMs") {
|
|
35394
|
+
throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on capture_redirect`);
|
|
35395
|
+
}
|
|
35396
|
+
}
|
|
35397
|
+
return raw;
|
|
35398
|
+
}
|
|
35372
35399
|
if (raw.op === "read_indexed_db") {
|
|
35373
35400
|
assertObject(raw.init, "inner.init");
|
|
35374
35401
|
if (raw.init.origin === void 0)
|
|
@@ -35394,7 +35421,32 @@ function validateInnerRequest(raw) {
|
|
|
35394
35421
|
}
|
|
35395
35422
|
return raw;
|
|
35396
35423
|
}
|
|
35397
|
-
|
|
35424
|
+
if (raw.op === "download") {
|
|
35425
|
+
assertObject(raw.init, "inner.init");
|
|
35426
|
+
if (raw.init.url === void 0) {
|
|
35427
|
+
throw new ProtocolError("inner.init.url: missing");
|
|
35428
|
+
}
|
|
35429
|
+
assertHttpUrl(raw.init.url, "inner.init.url");
|
|
35430
|
+
if (raw.init.filename !== void 0) {
|
|
35431
|
+
assertString(raw.init.filename, "inner.init.filename");
|
|
35432
|
+
if (/^([/\\]|[A-Za-z]:)/.test(raw.init.filename)) {
|
|
35433
|
+
throw new ProtocolError(`inner.init.filename: must be relative ${JSON.stringify(raw.init.filename)}`);
|
|
35434
|
+
}
|
|
35435
|
+
if (raw.init.filename.split(/[/\\]/).includes("..")) {
|
|
35436
|
+
throw new ProtocolError(`inner.init.filename: must not contain '..' ${JSON.stringify(raw.init.filename)}`);
|
|
35437
|
+
}
|
|
35438
|
+
}
|
|
35439
|
+
if (raw.init.timeoutMs !== void 0) {
|
|
35440
|
+
assertPositiveInt(raw.init.timeoutMs, "inner.init.timeoutMs");
|
|
35441
|
+
}
|
|
35442
|
+
for (const k of Object.keys(raw.init)) {
|
|
35443
|
+
if (k !== "url" && k !== "filename" && k !== "timeoutMs") {
|
|
35444
|
+
throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on download`);
|
|
35445
|
+
}
|
|
35446
|
+
}
|
|
35447
|
+
return raw;
|
|
35448
|
+
}
|
|
35449
|
+
throw new ProtocolError(`inner.op: must be one of "fetch", "read_cookies", "read_local_storage", "read_session_storage", "capture_request_header", "capture_redirect", "read_indexed_db", "download"; got ${JSON.stringify(raw.op)}`);
|
|
35398
35450
|
}
|
|
35399
35451
|
function assertNonEmptyKeyArray(value, label) {
|
|
35400
35452
|
if (!Array.isArray(value)) {
|
|
@@ -35465,6 +35517,13 @@ function validateInnerResponse(raw) {
|
|
|
35465
35517
|
assertString(raw.value, "inner.value");
|
|
35466
35518
|
return raw;
|
|
35467
35519
|
}
|
|
35520
|
+
if (op === "capture_redirect") {
|
|
35521
|
+
if (raw.value === void 0) {
|
|
35522
|
+
throw new ProtocolError("inner.value: missing on capture_redirect response");
|
|
35523
|
+
}
|
|
35524
|
+
assertString(raw.value, "inner.value");
|
|
35525
|
+
return raw;
|
|
35526
|
+
}
|
|
35468
35527
|
if (op === "read_indexed_db") {
|
|
35469
35528
|
if (raw.values === void 0) {
|
|
35470
35529
|
throw new ProtocolError("inner.values: missing on read_indexed_db response");
|
|
@@ -35472,6 +35531,25 @@ function validateInnerResponse(raw) {
|
|
|
35472
35531
|
assertObject(raw.values, "inner.values");
|
|
35473
35532
|
return raw;
|
|
35474
35533
|
}
|
|
35534
|
+
if (op === "download") {
|
|
35535
|
+
assertObject(raw.value, "inner.value");
|
|
35536
|
+
assertString(raw.value.path, "inner.value.path");
|
|
35537
|
+
if (typeof raw.value.bytes !== "number" || !Number.isInteger(raw.value.bytes) || raw.value.bytes < 0) {
|
|
35538
|
+
throw new ProtocolError("inner.value.bytes: expected non-negative integer");
|
|
35539
|
+
}
|
|
35540
|
+
if (raw.value.mime !== void 0) {
|
|
35541
|
+
assertString(raw.value.mime, "inner.value.mime");
|
|
35542
|
+
}
|
|
35543
|
+
if (raw.value.finalUrl !== void 0) {
|
|
35544
|
+
assertString(raw.value.finalUrl, "inner.value.finalUrl");
|
|
35545
|
+
}
|
|
35546
|
+
for (const k of Object.keys(raw.value)) {
|
|
35547
|
+
if (k !== "path" && k !== "bytes" && k !== "mime" && k !== "finalUrl") {
|
|
35548
|
+
throw new ProtocolError(`inner.value: unexpected field ${JSON.stringify(k)} on download response`);
|
|
35549
|
+
}
|
|
35550
|
+
}
|
|
35551
|
+
return raw;
|
|
35552
|
+
}
|
|
35475
35553
|
throw new ProtocolError(`inner.op: unknown success-response op ${JSON.stringify(raw.op)}`);
|
|
35476
35554
|
}
|
|
35477
35555
|
if (raw.ok === false) {
|
|
@@ -36342,8 +36420,12 @@ var FetchproxyServer = class {
|
|
|
36342
36420
|
pendingStorage = /* @__PURE__ */ new Map();
|
|
36343
36421
|
// 0.3.0+: capture-header awaiters resolve a single string.
|
|
36344
36422
|
pendingCapture = /* @__PURE__ */ new Map();
|
|
36423
|
+
// capture_redirect awaiters resolve the captured redirect URL string.
|
|
36424
|
+
pendingRedirect = /* @__PURE__ */ new Map();
|
|
36345
36425
|
// 0.4.0+: read_indexed_db awaiters resolve a JSON-typed values map.
|
|
36346
36426
|
pendingIdb = /* @__PURE__ */ new Map();
|
|
36427
|
+
// download awaiters resolve the saved-file metadata (path + size + mime).
|
|
36428
|
+
pendingDownload = /* @__PURE__ */ new Map();
|
|
36347
36429
|
mcpId = null;
|
|
36348
36430
|
identity = null;
|
|
36349
36431
|
// 0.5.3+: in-flight role-election / handle-start promise. Set the
|
|
@@ -37275,6 +37357,181 @@ var FetchproxyServer = class {
|
|
|
37275
37357
|
}
|
|
37276
37358
|
return pending;
|
|
37277
37359
|
}
|
|
37360
|
+
/**
|
|
37361
|
+
* Snapshot the redirect target URL of the next request the browser
|
|
37362
|
+
* makes to `(host, path?)`. Single-shot: the extension registers a
|
|
37363
|
+
* one-time `chrome.webRequest.onBeforeRedirect` listener filtered on
|
|
37364
|
+
* `https://${host}${path ?? '/*'}`, captures `details.redirectUrl` on
|
|
37365
|
+
* the first match, removes itself, and resolves with the URL. Times out
|
|
37366
|
+
* after `timeoutMs` (default 30s on the extension).
|
|
37367
|
+
*
|
|
37368
|
+
* Use case: a Cloudflare-walled endpoint that 302-redirects cross-origin
|
|
37369
|
+
* to a presigned URL — a page-level fetch sees only an opaque redirect,
|
|
37370
|
+
* but `onBeforeRedirect` exposes the target. Capture is limited to the
|
|
37371
|
+
* MCP's own declared `domains`; no per-entry declared scope is required.
|
|
37372
|
+
*/
|
|
37373
|
+
async captureRedirect(opts) {
|
|
37374
|
+
if (!this.opts.capabilities.includes("capture_redirect")) {
|
|
37375
|
+
throw new Error('FetchproxyServer.captureRedirect(): MCP did not declare "capture_redirect" in capabilities');
|
|
37376
|
+
}
|
|
37377
|
+
await this.ensureConnected();
|
|
37378
|
+
this.throwIfPendingPair();
|
|
37379
|
+
try {
|
|
37380
|
+
const result = await this._captureRedirectOnce(opts);
|
|
37381
|
+
this.recordSuccess();
|
|
37382
|
+
return result;
|
|
37383
|
+
} catch (err) {
|
|
37384
|
+
const swDown = err instanceof FetchproxyProtocolError && classifyFetchError(err.message) === "content_script_unreachable";
|
|
37385
|
+
if (!swDown) {
|
|
37386
|
+
this.recordFailure(`capture_redirect: ${err.message ?? String(err)}`);
|
|
37387
|
+
throw err;
|
|
37388
|
+
}
|
|
37389
|
+
this.lastEvictionDetectedAt = Date.now();
|
|
37390
|
+
const reviveMs = this.opts.bridgeReviveDelayMs ?? 0;
|
|
37391
|
+
if (reviveMs > 0) {
|
|
37392
|
+
this.lazyReviveAttempts += 1;
|
|
37393
|
+
await new Promise((r) => setTimeout(r, reviveMs));
|
|
37394
|
+
try {
|
|
37395
|
+
const result = await this._captureRedirectOnce(opts);
|
|
37396
|
+
this.lazyReviveSuccesses += 1;
|
|
37397
|
+
this.recordSuccess();
|
|
37398
|
+
return result;
|
|
37399
|
+
} catch (retryErr) {
|
|
37400
|
+
const stillDown = retryErr instanceof FetchproxyProtocolError && classifyFetchError(retryErr.message) === "content_script_unreachable";
|
|
37401
|
+
if (!stillDown) {
|
|
37402
|
+
this.recordFailure(`capture_redirect: ${retryErr.message ?? String(retryErr)}`);
|
|
37403
|
+
throw retryErr;
|
|
37404
|
+
}
|
|
37405
|
+
this.recordFailure(`capture_redirect bridge-down: ${retryErr.message}`);
|
|
37406
|
+
throw new FetchproxyBridgeDownError({
|
|
37407
|
+
originalError: retryErr.message,
|
|
37408
|
+
retryAttempted: true,
|
|
37409
|
+
op: "capture_redirect",
|
|
37410
|
+
url: `https://${opts.host}${opts.path ?? "/*"}`,
|
|
37411
|
+
role: this.role,
|
|
37412
|
+
port: this.opts.port
|
|
37413
|
+
});
|
|
37414
|
+
}
|
|
37415
|
+
}
|
|
37416
|
+
this.recordFailure(`capture_redirect bridge-down: ${err.message}`);
|
|
37417
|
+
throw new FetchproxyBridgeDownError({
|
|
37418
|
+
originalError: err.message,
|
|
37419
|
+
retryAttempted: false,
|
|
37420
|
+
op: "capture_redirect",
|
|
37421
|
+
url: `https://${opts.host}${opts.path ?? "/*"}`,
|
|
37422
|
+
role: this.role,
|
|
37423
|
+
port: this.opts.port
|
|
37424
|
+
});
|
|
37425
|
+
}
|
|
37426
|
+
}
|
|
37427
|
+
async _captureRedirectOnce(opts) {
|
|
37428
|
+
const id = this.nextRequestId++;
|
|
37429
|
+
const inner = {
|
|
37430
|
+
type: "request",
|
|
37431
|
+
id,
|
|
37432
|
+
op: "capture_redirect",
|
|
37433
|
+
init: {
|
|
37434
|
+
host: opts.host,
|
|
37435
|
+
...opts.path !== void 0 ? { path: opts.path } : {},
|
|
37436
|
+
...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
|
|
37437
|
+
}
|
|
37438
|
+
};
|
|
37439
|
+
const pending = new Promise((resolve, reject) => {
|
|
37440
|
+
this.pendingRedirect.set(id, { resolve, reject });
|
|
37441
|
+
});
|
|
37442
|
+
if (this.hostHandle) {
|
|
37443
|
+
await this.hostHandle.sendOwnInner(inner);
|
|
37444
|
+
} else if (this.peerHandle) {
|
|
37445
|
+
await this.peerHandle.sendInner(inner);
|
|
37446
|
+
}
|
|
37447
|
+
return pending;
|
|
37448
|
+
}
|
|
37449
|
+
/**
|
|
37450
|
+
* Download `url` through the BROWSER's own network stack via
|
|
37451
|
+
* `chrome.downloads` (real cookies + TLS/JA3 fingerprint). Unlike a
|
|
37452
|
+
* page-level `fetch()` (cors mode), this clears a Cloudflare bot-challenge
|
|
37453
|
+
* on the endpoint and follows the cross-origin redirect to the final file.
|
|
37454
|
+
* Resolves the saved local file path + size; the bridge is loopback-only /
|
|
37455
|
+
* single-host, so the MCP reads the bytes from the same disk. Requires
|
|
37456
|
+
* `'download'` in capabilities and the `url` host to be a declared `domain`.
|
|
37457
|
+
*/
|
|
37458
|
+
async download(opts) {
|
|
37459
|
+
if (!this.opts.capabilities.includes("download")) {
|
|
37460
|
+
throw new Error('FetchproxyServer.download(): MCP did not declare "download" in capabilities');
|
|
37461
|
+
}
|
|
37462
|
+
assertUrlInDomains("download url", opts.url, this.opts.domains);
|
|
37463
|
+
await this.ensureConnected();
|
|
37464
|
+
this.throwIfPendingPair();
|
|
37465
|
+
try {
|
|
37466
|
+
const result = await this._downloadOnce(opts);
|
|
37467
|
+
this.recordSuccess();
|
|
37468
|
+
return result;
|
|
37469
|
+
} catch (err) {
|
|
37470
|
+
const swDown = err instanceof FetchproxyProtocolError && classifyFetchError(err.message) === "content_script_unreachable";
|
|
37471
|
+
if (!swDown) {
|
|
37472
|
+
this.recordFailure(`download: ${err.message ?? String(err)}`);
|
|
37473
|
+
throw err;
|
|
37474
|
+
}
|
|
37475
|
+
this.lastEvictionDetectedAt = Date.now();
|
|
37476
|
+
const reviveMs = this.opts.bridgeReviveDelayMs ?? 0;
|
|
37477
|
+
if (reviveMs > 0) {
|
|
37478
|
+
this.lazyReviveAttempts += 1;
|
|
37479
|
+
await new Promise((r) => setTimeout(r, reviveMs));
|
|
37480
|
+
try {
|
|
37481
|
+
const result = await this._downloadOnce(opts);
|
|
37482
|
+
this.lazyReviveSuccesses += 1;
|
|
37483
|
+
this.recordSuccess();
|
|
37484
|
+
return result;
|
|
37485
|
+
} catch (retryErr) {
|
|
37486
|
+
const stillDown = retryErr instanceof FetchproxyProtocolError && classifyFetchError(retryErr.message) === "content_script_unreachable";
|
|
37487
|
+
if (!stillDown) {
|
|
37488
|
+
this.recordFailure(`download: ${retryErr.message ?? String(retryErr)}`);
|
|
37489
|
+
throw retryErr;
|
|
37490
|
+
}
|
|
37491
|
+
this.recordFailure(`download bridge-down: ${retryErr.message}`);
|
|
37492
|
+
throw new FetchproxyBridgeDownError({
|
|
37493
|
+
originalError: retryErr.message,
|
|
37494
|
+
retryAttempted: true,
|
|
37495
|
+
op: "download",
|
|
37496
|
+
url: opts.url,
|
|
37497
|
+
role: this.role,
|
|
37498
|
+
port: this.opts.port
|
|
37499
|
+
});
|
|
37500
|
+
}
|
|
37501
|
+
}
|
|
37502
|
+
this.recordFailure(`download bridge-down: ${err.message}`);
|
|
37503
|
+
throw new FetchproxyBridgeDownError({
|
|
37504
|
+
originalError: err.message,
|
|
37505
|
+
retryAttempted: false,
|
|
37506
|
+
op: "download",
|
|
37507
|
+
url: opts.url,
|
|
37508
|
+
role: this.role,
|
|
37509
|
+
port: this.opts.port
|
|
37510
|
+
});
|
|
37511
|
+
}
|
|
37512
|
+
}
|
|
37513
|
+
async _downloadOnce(opts) {
|
|
37514
|
+
const id = this.nextRequestId++;
|
|
37515
|
+
const inner = {
|
|
37516
|
+
type: "request",
|
|
37517
|
+
id,
|
|
37518
|
+
op: "download",
|
|
37519
|
+
init: {
|
|
37520
|
+
url: opts.url,
|
|
37521
|
+
...opts.filename !== void 0 ? { filename: opts.filename } : {},
|
|
37522
|
+
...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
|
|
37523
|
+
}
|
|
37524
|
+
};
|
|
37525
|
+
const pending = new Promise((resolve, reject) => {
|
|
37526
|
+
this.pendingDownload.set(id, { resolve, reject });
|
|
37527
|
+
});
|
|
37528
|
+
if (this.hostHandle) {
|
|
37529
|
+
await this.hostHandle.sendOwnInner(inner);
|
|
37530
|
+
} else if (this.peerHandle) {
|
|
37531
|
+
await this.peerHandle.sendInner(inner);
|
|
37532
|
+
}
|
|
37533
|
+
return pending;
|
|
37534
|
+
}
|
|
37278
37535
|
/**
|
|
37279
37536
|
* 0.4.0+: read declared IndexedDB keys from the user's signed-in
|
|
37280
37537
|
* tab. Requires `'read_indexed_db'` in capabilities AND the
|
|
@@ -37422,6 +37679,20 @@ var FetchproxyServer = class {
|
|
|
37422
37679
|
}
|
|
37423
37680
|
return;
|
|
37424
37681
|
}
|
|
37682
|
+
const redirectCb = this.pendingRedirect.get(inner.id);
|
|
37683
|
+
if (redirectCb) {
|
|
37684
|
+
this.pendingRedirect.delete(inner.id);
|
|
37685
|
+
if (inner.ok) {
|
|
37686
|
+
if (inner.op === "capture_redirect" && typeof inner.value === "string") {
|
|
37687
|
+
redirectCb.resolve(inner.value);
|
|
37688
|
+
} else {
|
|
37689
|
+
redirectCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on capture_redirect awaiter`));
|
|
37690
|
+
}
|
|
37691
|
+
} else {
|
|
37692
|
+
redirectCb.reject(new FetchproxyProtocolError(inner.error));
|
|
37693
|
+
}
|
|
37694
|
+
return;
|
|
37695
|
+
}
|
|
37425
37696
|
const idbCb = this.pendingIdb.get(inner.id);
|
|
37426
37697
|
if (idbCb) {
|
|
37427
37698
|
this.pendingIdb.delete(inner.id);
|
|
@@ -37436,6 +37707,20 @@ var FetchproxyServer = class {
|
|
|
37436
37707
|
}
|
|
37437
37708
|
return;
|
|
37438
37709
|
}
|
|
37710
|
+
const downloadCb = this.pendingDownload.get(inner.id);
|
|
37711
|
+
if (downloadCb) {
|
|
37712
|
+
this.pendingDownload.delete(inner.id);
|
|
37713
|
+
if (inner.ok) {
|
|
37714
|
+
if (inner.op === "download" && inner.value && typeof inner.value === "object") {
|
|
37715
|
+
downloadCb.resolve({ ...inner.value });
|
|
37716
|
+
} else {
|
|
37717
|
+
downloadCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on download awaiter`));
|
|
37718
|
+
}
|
|
37719
|
+
} else {
|
|
37720
|
+
downloadCb.reject(new FetchproxyProtocolError(inner.error));
|
|
37721
|
+
}
|
|
37722
|
+
return;
|
|
37723
|
+
}
|
|
37439
37724
|
const cookiesCb = this.pendingReadCookies.get(inner.id);
|
|
37440
37725
|
if (cookiesCb) {
|
|
37441
37726
|
this.pendingReadCookies.delete(inner.id);
|
|
@@ -37478,9 +37763,15 @@ var FetchproxyServer = class {
|
|
|
37478
37763
|
for (const { reject } of this.pendingCapture.values())
|
|
37479
37764
|
reject(err);
|
|
37480
37765
|
this.pendingCapture.clear();
|
|
37766
|
+
for (const { reject } of this.pendingRedirect.values())
|
|
37767
|
+
reject(err);
|
|
37768
|
+
this.pendingRedirect.clear();
|
|
37481
37769
|
for (const { reject } of this.pendingIdb.values())
|
|
37482
37770
|
reject(err);
|
|
37483
37771
|
this.pendingIdb.clear();
|
|
37772
|
+
for (const { reject } of this.pendingDownload.values())
|
|
37773
|
+
reject(err);
|
|
37774
|
+
this.pendingDownload.clear();
|
|
37484
37775
|
}
|
|
37485
37776
|
/**
|
|
37486
37777
|
* 0.5.2+: read the current pair-pending pair code from whichever handle
|
|
@@ -37689,7 +37980,7 @@ var BootstrapDisabledError = class extends Error {
|
|
|
37689
37980
|
// package.json
|
|
37690
37981
|
var package_default = {
|
|
37691
37982
|
name: "zola-mcp",
|
|
37692
|
-
version: "1.
|
|
37983
|
+
version: "1.4.0",
|
|
37693
37984
|
mcpName: "io.github.chrischall/zola-mcp",
|
|
37694
37985
|
description: "Zola wedding MCP server for Claude",
|
|
37695
37986
|
author: "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -37735,18 +38026,19 @@ var package_default = {
|
|
|
37735
38026
|
"test:watch": "vitest"
|
|
37736
38027
|
},
|
|
37737
38028
|
dependencies: {
|
|
37738
|
-
"@chrischall/mcp-utils": "^0.5.
|
|
37739
|
-
"@fetchproxy/bootstrap": "^1.
|
|
37740
|
-
"@fetchproxy/server": "^1.
|
|
38029
|
+
"@chrischall/mcp-utils": "^0.5.2",
|
|
38030
|
+
"@fetchproxy/bootstrap": "^1.3.0",
|
|
38031
|
+
"@fetchproxy/server": "^1.3.0",
|
|
37741
38032
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
37742
|
-
dotenv: "^17.4.2"
|
|
38033
|
+
dotenv: "^17.4.2",
|
|
38034
|
+
zod: "^4.4.3"
|
|
37743
38035
|
},
|
|
37744
38036
|
devDependencies: {
|
|
37745
|
-
"@types/node": "^25.9.
|
|
37746
|
-
"@vitest/coverage-v8": "^4.1.
|
|
38037
|
+
"@types/node": "^25.9.2",
|
|
38038
|
+
"@vitest/coverage-v8": "^4.1.8",
|
|
37747
38039
|
esbuild: "^0.28.0",
|
|
37748
38040
|
typescript: "^6.0.3",
|
|
37749
|
-
vitest: "^4.1.
|
|
38041
|
+
vitest: "^4.1.8"
|
|
37750
38042
|
}
|
|
37751
38043
|
};
|
|
37752
38044
|
|
|
@@ -38295,37 +38587,39 @@ async function updateGuestAddress(args) {
|
|
|
38295
38587
|
if (!group) {
|
|
38296
38588
|
throw new Error(`Guest group with ID ${args.guest_group_id} not found`);
|
|
38297
38589
|
}
|
|
38298
|
-
const updatedGuests = group.guests.map((
|
|
38299
|
-
...
|
|
38300
|
-
|
|
38301
|
-
|
|
38302
|
-
|
|
38303
|
-
|
|
38304
|
-
|
|
38305
|
-
|
|
38306
|
-
|
|
38307
|
-
|
|
38308
|
-
tags: []
|
|
38590
|
+
const updatedGuests = group.guests.map((guest) => ({
|
|
38591
|
+
...guest,
|
|
38592
|
+
address1: args.address1 ?? guest.address1 ?? "",
|
|
38593
|
+
address2: args.address2 !== void 0 ? args.address2 : guest.address2 ?? "",
|
|
38594
|
+
city: args.city ?? guest.city ?? "",
|
|
38595
|
+
state_province: args.state_province ?? guest.state_province ?? "",
|
|
38596
|
+
postal_code: args.postal_code ?? guest.postal_code ?? "",
|
|
38597
|
+
country_code: args.country_code ?? guest.country_code ?? "US",
|
|
38598
|
+
event_invitations: guest.event_invitations ?? [],
|
|
38599
|
+
tags: guest.tags ?? []
|
|
38309
38600
|
}));
|
|
38310
38601
|
const body = {
|
|
38311
|
-
|
|
38312
|
-
|
|
38313
|
-
|
|
38314
|
-
|
|
38315
|
-
|
|
38316
|
-
|
|
38317
|
-
|
|
38318
|
-
|
|
38319
|
-
|
|
38320
|
-
|
|
38321
|
-
|
|
38322
|
-
|
|
38323
|
-
|
|
38324
|
-
|
|
38602
|
+
updated_guest_groups: [
|
|
38603
|
+
{
|
|
38604
|
+
...group,
|
|
38605
|
+
wedding_account_id: group.wedding_account_id ?? weddingAccountId,
|
|
38606
|
+
envelope_recipient: group.envelope_recipient ?? "",
|
|
38607
|
+
gift_group: group.gift_group ?? {
|
|
38608
|
+
gift_count: 0,
|
|
38609
|
+
modules: [],
|
|
38610
|
+
gift_groups: [],
|
|
38611
|
+
guest_group_uuid: ""
|
|
38612
|
+
},
|
|
38613
|
+
thank_you_note_status: group.thank_you_note_status ?? "NOT_STARTED",
|
|
38614
|
+
rsvp_question_answers: group.rsvp_question_answers ?? [],
|
|
38615
|
+
gift_count: group.gift_count ?? 0,
|
|
38616
|
+
guests: updatedGuests
|
|
38617
|
+
}
|
|
38618
|
+
]
|
|
38325
38619
|
};
|
|
38326
38620
|
const result = await client.requestMobile(
|
|
38327
38621
|
"PUT",
|
|
38328
|
-
`/v3/guestlists/groups/wedding-accounts
|
|
38622
|
+
`/v3/guestlists/groups/wedding-accounts/${weddingAccountId}/bulk/directory`,
|
|
38329
38623
|
body
|
|
38330
38624
|
);
|
|
38331
38625
|
return jsonResult(result.data);
|
|
@@ -39761,8 +40055,177 @@ function registerInvitationTools(server) {
|
|
|
39761
40055
|
}, setCardProjectQrcode);
|
|
39762
40056
|
}
|
|
39763
40057
|
|
|
40058
|
+
// src/tools/event-invitations.ts
|
|
40059
|
+
async function fetchDirectory(acct) {
|
|
40060
|
+
const resp = await client.requestMobile(
|
|
40061
|
+
"POST",
|
|
40062
|
+
`/v3/guestlists/directory/wedding-accounts/${acct}`,
|
|
40063
|
+
{ sort_by_name_asc: true }
|
|
40064
|
+
);
|
|
40065
|
+
return resp.data.guest_groups;
|
|
40066
|
+
}
|
|
40067
|
+
async function fetchEvents(acct) {
|
|
40068
|
+
const resp = await client.requestMobile(
|
|
40069
|
+
"GET",
|
|
40070
|
+
`/v3/websites/events/wedding-accounts/${acct}/groups`
|
|
40071
|
+
);
|
|
40072
|
+
return resp.data.flatMap((group) => group.events);
|
|
40073
|
+
}
|
|
40074
|
+
function computeNext(existing, eventId, invited) {
|
|
40075
|
+
const current = existing ?? [];
|
|
40076
|
+
if (invited) {
|
|
40077
|
+
if (current.some((e) => e.event_id === eventId)) return current;
|
|
40078
|
+
return [...current, { event_id: eventId, id: null, rsvp_type: "NO_RESPONSE" }];
|
|
40079
|
+
}
|
|
40080
|
+
return current.filter((e) => e.event_id !== eventId);
|
|
40081
|
+
}
|
|
40082
|
+
function toWriteGuest(guest, invitations) {
|
|
40083
|
+
return { ...guest, event_invitations: invitations, tags: guest.tags ?? [] };
|
|
40084
|
+
}
|
|
40085
|
+
function buildWriteGroup(group, guests, acct) {
|
|
40086
|
+
return {
|
|
40087
|
+
...group,
|
|
40088
|
+
wedding_account_id: group.wedding_account_id ?? acct,
|
|
40089
|
+
envelope_recipient: group.envelope_recipient ?? "",
|
|
40090
|
+
gift_group: group.gift_group ?? {
|
|
40091
|
+
gift_count: 0,
|
|
40092
|
+
modules: [],
|
|
40093
|
+
gift_groups: [],
|
|
40094
|
+
guest_group_uuid: ""
|
|
40095
|
+
},
|
|
40096
|
+
thank_you_note_status: group.thank_you_note_status ?? "NOT_STARTED",
|
|
40097
|
+
rsvp_question_answers: group.rsvp_question_answers ?? [],
|
|
40098
|
+
gift_count: group.gift_count ?? 0,
|
|
40099
|
+
guests
|
|
40100
|
+
};
|
|
40101
|
+
}
|
|
40102
|
+
async function writeGroups(acct, groups) {
|
|
40103
|
+
await client.requestMobile(
|
|
40104
|
+
"PUT",
|
|
40105
|
+
`/v3/guestlists/groups/wedding-accounts/${acct}/bulk/directory`,
|
|
40106
|
+
{ updated_guest_groups: groups }
|
|
40107
|
+
);
|
|
40108
|
+
}
|
|
40109
|
+
async function requireEvent(acct, eventId) {
|
|
40110
|
+
const events = await fetchEvents(acct);
|
|
40111
|
+
if (!events.some((e) => e.event_entity_id === eventId)) {
|
|
40112
|
+
throw new Error(`Event with ID ${eventId} not found`);
|
|
40113
|
+
}
|
|
40114
|
+
}
|
|
40115
|
+
async function setEventGuests(args) {
|
|
40116
|
+
const { weddingAccountId: acct } = await client.getContext();
|
|
40117
|
+
await requireEvent(acct, args.event_id);
|
|
40118
|
+
const byId = new Map(
|
|
40119
|
+
(await fetchDirectory(acct)).map((group) => [group.guest_group_id, group])
|
|
40120
|
+
);
|
|
40121
|
+
const updatedGroups = [];
|
|
40122
|
+
const summary = [];
|
|
40123
|
+
for (const req of args.guest_groups) {
|
|
40124
|
+
const group = byId.get(req.guest_group_id);
|
|
40125
|
+
if (!group) throw new Error(`Guest group with ID ${req.guest_group_id} not found`);
|
|
40126
|
+
let changed = 0;
|
|
40127
|
+
const newGuests = group.guests.map((guest) => {
|
|
40128
|
+
const before = (guest.event_invitations ?? []).length;
|
|
40129
|
+
const next = computeNext(guest.event_invitations ?? [], args.event_id, req.invited);
|
|
40130
|
+
if (next.length !== before) changed++;
|
|
40131
|
+
return toWriteGuest(guest, next);
|
|
40132
|
+
});
|
|
40133
|
+
updatedGroups.push(buildWriteGroup(group, newGuests, acct));
|
|
40134
|
+
summary.push({ guest_group_id: req.guest_group_id, invited: req.invited, guests_changed: changed });
|
|
40135
|
+
}
|
|
40136
|
+
await writeGroups(acct, updatedGroups);
|
|
40137
|
+
return jsonResult({ event_id: args.event_id, groups: summary });
|
|
40138
|
+
}
|
|
40139
|
+
async function mutateOne(opts) {
|
|
40140
|
+
const hasGroup = opts.guest_group_id !== void 0;
|
|
40141
|
+
const hasGuest = opts.guest_id !== void 0;
|
|
40142
|
+
if (hasGroup === hasGuest) {
|
|
40143
|
+
throw new Error("Provide exactly one of guest_group_id or guest_id");
|
|
40144
|
+
}
|
|
40145
|
+
const { weddingAccountId: acct } = await client.getContext();
|
|
40146
|
+
await requireEvent(acct, opts.event_id);
|
|
40147
|
+
const groups = await fetchDirectory(acct);
|
|
40148
|
+
let target;
|
|
40149
|
+
let appliesTo;
|
|
40150
|
+
if (hasGroup) {
|
|
40151
|
+
target = groups.find((g) => g.guest_group_id === opts.guest_group_id);
|
|
40152
|
+
if (!target) throw new Error(`Guest group with ID ${opts.guest_group_id} not found`);
|
|
40153
|
+
appliesTo = () => true;
|
|
40154
|
+
} else {
|
|
40155
|
+
target = groups.find((g) => g.guests.some((gu) => gu.guest_id === opts.guest_id));
|
|
40156
|
+
if (!target) throw new Error(`Guest with ID ${opts.guest_id} not found`);
|
|
40157
|
+
appliesTo = (guest) => guest.guest_id === opts.guest_id;
|
|
40158
|
+
}
|
|
40159
|
+
let changed = 0;
|
|
40160
|
+
const newGuests = target.guests.map((guest) => {
|
|
40161
|
+
const existing = guest.event_invitations ?? [];
|
|
40162
|
+
if (!appliesTo(guest)) return toWriteGuest(guest, existing);
|
|
40163
|
+
const next = computeNext(existing, opts.event_id, opts.invited);
|
|
40164
|
+
if (next.length !== existing.length) changed++;
|
|
40165
|
+
return toWriteGuest(guest, next);
|
|
40166
|
+
});
|
|
40167
|
+
await writeGroups(acct, [buildWriteGroup(target, newGuests, acct)]);
|
|
40168
|
+
return jsonResult({
|
|
40169
|
+
event_id: opts.event_id,
|
|
40170
|
+
invited: opts.invited,
|
|
40171
|
+
guest_group_id: target.guest_group_id,
|
|
40172
|
+
guests_changed: changed
|
|
40173
|
+
});
|
|
40174
|
+
}
|
|
40175
|
+
async function inviteGuestToEvent(args) {
|
|
40176
|
+
return mutateOne({ ...args, invited: true });
|
|
40177
|
+
}
|
|
40178
|
+
async function removeEventInvitation(args) {
|
|
40179
|
+
return mutateOne({ ...args, invited: false });
|
|
40180
|
+
}
|
|
40181
|
+
function registerEventInvitationTools(server) {
|
|
40182
|
+
server.registerTool(
|
|
40183
|
+
"set_event_guests",
|
|
40184
|
+
{
|
|
40185
|
+
description: "Set which guest groups are invited to an event (bulk). For each group, invited:true ensures every guest in the group is invited to the event; invited:false removes the invitation. Other events\u2019 invitations are preserved. Idempotent. Use this to assign guests to events in bulk (e.g. by tier/affiliation/location).",
|
|
40186
|
+
inputSchema: {
|
|
40187
|
+
event_id: external_exports.number().describe("Event entity ID from list_events (event_entity_id)"),
|
|
40188
|
+
guest_groups: external_exports.array(
|
|
40189
|
+
external_exports.object({
|
|
40190
|
+
guest_group_id: external_exports.number().describe("Guest group ID from list_guests"),
|
|
40191
|
+
invited: external_exports.boolean().describe("true = invite the whole group; false = uninvite")
|
|
40192
|
+
})
|
|
40193
|
+
).describe("Guest groups to set for this event. Only the listed groups are affected.")
|
|
40194
|
+
},
|
|
40195
|
+
annotations: { destructiveHint: false }
|
|
40196
|
+
},
|
|
40197
|
+
setEventGuests
|
|
40198
|
+
);
|
|
40199
|
+
server.registerTool(
|
|
40200
|
+
"invite_guest_to_event",
|
|
40201
|
+
{
|
|
40202
|
+
description: "Invite a single guest or guest group to an event (additive \u2014 does not affect other events). Pass exactly one of guest_group_id (invites all guests in the group) or guest_id (invites just that guest). Idempotent.",
|
|
40203
|
+
inputSchema: {
|
|
40204
|
+
event_id: external_exports.number().describe("Event entity ID from list_events (event_entity_id)"),
|
|
40205
|
+
guest_group_id: external_exports.number().optional().describe("Guest group ID \u2014 invites every guest in the group"),
|
|
40206
|
+
guest_id: external_exports.number().optional().describe("Single guest ID \u2014 invites just that guest")
|
|
40207
|
+
},
|
|
40208
|
+
annotations: { destructiveHint: false }
|
|
40209
|
+
},
|
|
40210
|
+
inviteGuestToEvent
|
|
40211
|
+
);
|
|
40212
|
+
server.registerTool(
|
|
40213
|
+
"remove_event_invitation",
|
|
40214
|
+
{
|
|
40215
|
+
description: "Remove an event invitation for a single guest or guest group. Pass exactly one of guest_group_id (removes for all guests in the group) or guest_id (removes for just that guest). Other events\u2019 invitations are preserved. Idempotent.",
|
|
40216
|
+
inputSchema: {
|
|
40217
|
+
event_id: external_exports.number().describe("Event entity ID from list_events (event_entity_id)"),
|
|
40218
|
+
guest_group_id: external_exports.number().optional().describe("Guest group ID \u2014 removes for every guest in the group"),
|
|
40219
|
+
guest_id: external_exports.number().optional().describe("Single guest ID \u2014 removes for just that guest")
|
|
40220
|
+
},
|
|
40221
|
+
annotations: { destructiveHint: false }
|
|
40222
|
+
},
|
|
40223
|
+
removeEventInvitation
|
|
40224
|
+
);
|
|
40225
|
+
}
|
|
40226
|
+
|
|
39764
40227
|
// src/index.ts
|
|
39765
|
-
var VERSION = "1.
|
|
40228
|
+
var VERSION = "1.4.0";
|
|
39766
40229
|
await runMcp({
|
|
39767
40230
|
name: "zola-mcp",
|
|
39768
40231
|
version: VERSION,
|
|
@@ -39779,6 +40242,7 @@ await runMcp({
|
|
|
39779
40242
|
registerWebsiteContentTools,
|
|
39780
40243
|
registerWebsiteThemeTools,
|
|
39781
40244
|
registerRegistryItemTools,
|
|
39782
|
-
registerInvitationTools
|
|
40245
|
+
registerInvitationTools,
|
|
40246
|
+
registerEventInvitationTools
|
|
39783
40247
|
]
|
|
39784
40248
|
});
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,8 @@ import { registerWebsiteContentTools } from './tools/website-content.js';
|
|
|
11
11
|
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
16
|
await runMcp({
|
|
16
17
|
name: 'zola-mcp',
|
|
17
18
|
version: VERSION,
|
|
@@ -29,5 +30,6 @@ await runMcp({
|
|
|
29
30
|
registerWebsiteThemeTools,
|
|
30
31
|
registerRegistryItemTools,
|
|
31
32
|
registerInvitationTools,
|
|
33
|
+
registerEventInvitationTools,
|
|
32
34
|
],
|
|
33
35
|
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { client } from '../client.js';
|
|
3
|
+
import { jsonResult } from '../types.js';
|
|
4
|
+
// ─── Live reads ────────────────────────────────────────────────────────────────
|
|
5
|
+
async function fetchDirectory(acct) {
|
|
6
|
+
const resp = await client.requestMobile('POST', `/v3/guestlists/directory/wedding-accounts/${acct}`, { sort_by_name_asc: true });
|
|
7
|
+
return resp.data.guest_groups;
|
|
8
|
+
}
|
|
9
|
+
async function fetchEvents(acct) {
|
|
10
|
+
const resp = await client.requestMobile('GET', `/v3/websites/events/wedding-accounts/${acct}/groups`);
|
|
11
|
+
return resp.data.flatMap((group) => group.events);
|
|
12
|
+
}
|
|
13
|
+
// ─── Read-modify-write helpers ──────────────────────────────────────────────────
|
|
14
|
+
/**
|
|
15
|
+
* Compute the new `event_invitations` array for one guest.
|
|
16
|
+
* Add → append `{ event_id, id: null, rsvp_type: "NO_RESPONSE" }` (idempotent).
|
|
17
|
+
* Remove → drop every element matching the event. Existing invitations to other
|
|
18
|
+
* events are preserved verbatim (id + rsvp intact) — this is what prevents the
|
|
19
|
+
* partial-update wipe.
|
|
20
|
+
*/
|
|
21
|
+
function computeNext(existing, eventId, invited) {
|
|
22
|
+
const current = existing ?? [];
|
|
23
|
+
if (invited) {
|
|
24
|
+
if (current.some((e) => e.event_id === eventId))
|
|
25
|
+
return current;
|
|
26
|
+
return [...current, { event_id: eventId, id: null, rsvp_type: 'NO_RESPONSE' }];
|
|
27
|
+
}
|
|
28
|
+
return current.filter((e) => e.event_id !== eventId);
|
|
29
|
+
}
|
|
30
|
+
/** Send each guest back as received (read-modify-write), with the new invitations. */
|
|
31
|
+
function toWriteGuest(guest, invitations) {
|
|
32
|
+
return { ...guest, event_invitations: invitations, tags: guest.tags ?? [] };
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Build the per-group payload the `bulk/directory` endpoint expects.
|
|
36
|
+
* Spread the group as read (faithful round-trip) and supply safe defaults for
|
|
37
|
+
* the few fields the endpoint requires non-null.
|
|
38
|
+
*/
|
|
39
|
+
function buildWriteGroup(group, guests, acct) {
|
|
40
|
+
return {
|
|
41
|
+
...group,
|
|
42
|
+
wedding_account_id: group.wedding_account_id ?? acct,
|
|
43
|
+
envelope_recipient: group.envelope_recipient ?? '',
|
|
44
|
+
gift_group: group.gift_group ?? {
|
|
45
|
+
gift_count: 0,
|
|
46
|
+
modules: [],
|
|
47
|
+
gift_groups: [],
|
|
48
|
+
guest_group_uuid: '',
|
|
49
|
+
},
|
|
50
|
+
thank_you_note_status: group.thank_you_note_status ?? 'NOT_STARTED',
|
|
51
|
+
rsvp_question_answers: group.rsvp_question_answers ?? [],
|
|
52
|
+
gift_count: group.gift_count ?? 0,
|
|
53
|
+
guests,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
async function writeGroups(acct, groups) {
|
|
57
|
+
await client.requestMobile('PUT', `/v3/guestlists/groups/wedding-accounts/${acct}/bulk/directory`, { updated_guest_groups: groups });
|
|
58
|
+
}
|
|
59
|
+
async function requireEvent(acct, eventId) {
|
|
60
|
+
const events = await fetchEvents(acct);
|
|
61
|
+
if (!events.some((e) => e.event_entity_id === eventId)) {
|
|
62
|
+
throw new Error(`Event with ID ${eventId} not found`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// ─── Tool: set_event_guests (bulk) ──────────────────────────────────────────────
|
|
66
|
+
export async function setEventGuests(args) {
|
|
67
|
+
const { weddingAccountId: acct } = await client.getContext();
|
|
68
|
+
await requireEvent(acct, args.event_id);
|
|
69
|
+
const byId = new Map((await fetchDirectory(acct)).map((group) => [group.guest_group_id, group]));
|
|
70
|
+
const updatedGroups = [];
|
|
71
|
+
const summary = [];
|
|
72
|
+
for (const req of args.guest_groups) {
|
|
73
|
+
const group = byId.get(req.guest_group_id);
|
|
74
|
+
if (!group)
|
|
75
|
+
throw new Error(`Guest group with ID ${req.guest_group_id} not found`);
|
|
76
|
+
let changed = 0;
|
|
77
|
+
const newGuests = group.guests.map((guest) => {
|
|
78
|
+
const before = (guest.event_invitations ?? []).length;
|
|
79
|
+
const next = computeNext(guest.event_invitations ?? [], args.event_id, req.invited);
|
|
80
|
+
if (next.length !== before)
|
|
81
|
+
changed++;
|
|
82
|
+
return toWriteGuest(guest, next);
|
|
83
|
+
});
|
|
84
|
+
updatedGroups.push(buildWriteGroup(group, newGuests, acct));
|
|
85
|
+
summary.push({ guest_group_id: req.guest_group_id, invited: req.invited, guests_changed: changed });
|
|
86
|
+
}
|
|
87
|
+
await writeGroups(acct, updatedGroups);
|
|
88
|
+
return jsonResult({ event_id: args.event_id, groups: summary });
|
|
89
|
+
}
|
|
90
|
+
// ─── Tools: invite_guest_to_event / remove_event_invitation (single) ─────────────
|
|
91
|
+
async function mutateOne(opts) {
|
|
92
|
+
const hasGroup = opts.guest_group_id !== undefined;
|
|
93
|
+
const hasGuest = opts.guest_id !== undefined;
|
|
94
|
+
if (hasGroup === hasGuest) {
|
|
95
|
+
throw new Error('Provide exactly one of guest_group_id or guest_id');
|
|
96
|
+
}
|
|
97
|
+
const { weddingAccountId: acct } = await client.getContext();
|
|
98
|
+
await requireEvent(acct, opts.event_id);
|
|
99
|
+
const groups = await fetchDirectory(acct);
|
|
100
|
+
let target;
|
|
101
|
+
let appliesTo;
|
|
102
|
+
if (hasGroup) {
|
|
103
|
+
target = groups.find((g) => g.guest_group_id === opts.guest_group_id);
|
|
104
|
+
if (!target)
|
|
105
|
+
throw new Error(`Guest group with ID ${opts.guest_group_id} not found`);
|
|
106
|
+
appliesTo = () => true;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
target = groups.find((g) => g.guests.some((gu) => gu.guest_id === opts.guest_id));
|
|
110
|
+
if (!target)
|
|
111
|
+
throw new Error(`Guest with ID ${opts.guest_id} not found`);
|
|
112
|
+
appliesTo = (guest) => guest.guest_id === opts.guest_id;
|
|
113
|
+
}
|
|
114
|
+
let changed = 0;
|
|
115
|
+
const newGuests = target.guests.map((guest) => {
|
|
116
|
+
const existing = guest.event_invitations ?? [];
|
|
117
|
+
if (!appliesTo(guest))
|
|
118
|
+
return toWriteGuest(guest, existing);
|
|
119
|
+
const next = computeNext(existing, opts.event_id, opts.invited);
|
|
120
|
+
if (next.length !== existing.length)
|
|
121
|
+
changed++;
|
|
122
|
+
return toWriteGuest(guest, next);
|
|
123
|
+
});
|
|
124
|
+
await writeGroups(acct, [buildWriteGroup(target, newGuests, acct)]);
|
|
125
|
+
return jsonResult({
|
|
126
|
+
event_id: opts.event_id,
|
|
127
|
+
invited: opts.invited,
|
|
128
|
+
guest_group_id: target.guest_group_id,
|
|
129
|
+
guests_changed: changed,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
export async function inviteGuestToEvent(args) {
|
|
133
|
+
return mutateOne({ ...args, invited: true });
|
|
134
|
+
}
|
|
135
|
+
export async function removeEventInvitation(args) {
|
|
136
|
+
return mutateOne({ ...args, invited: false });
|
|
137
|
+
}
|
|
138
|
+
// ─── MCP registration ────────────────────────────────────────────────────────
|
|
139
|
+
export function registerEventInvitationTools(server) {
|
|
140
|
+
server.registerTool('set_event_guests', {
|
|
141
|
+
description: 'Set which guest groups are invited to an event (bulk). For each group, invited:true ensures every guest in the group is invited to the event; invited:false removes the invitation. Other events’ invitations are preserved. Idempotent. Use this to assign guests to events in bulk (e.g. by tier/affiliation/location).',
|
|
142
|
+
inputSchema: {
|
|
143
|
+
event_id: z.number().describe('Event entity ID from list_events (event_entity_id)'),
|
|
144
|
+
guest_groups: z
|
|
145
|
+
.array(z.object({
|
|
146
|
+
guest_group_id: z.number().describe('Guest group ID from list_guests'),
|
|
147
|
+
invited: z.boolean().describe('true = invite the whole group; false = uninvite'),
|
|
148
|
+
}))
|
|
149
|
+
.describe('Guest groups to set for this event. Only the listed groups are affected.'),
|
|
150
|
+
},
|
|
151
|
+
annotations: { destructiveHint: false },
|
|
152
|
+
}, setEventGuests);
|
|
153
|
+
server.registerTool('invite_guest_to_event', {
|
|
154
|
+
description: 'Invite a single guest or guest group to an event (additive — does not affect other events). Pass exactly one of guest_group_id (invites all guests in the group) or guest_id (invites just that guest). Idempotent.',
|
|
155
|
+
inputSchema: {
|
|
156
|
+
event_id: z.number().describe('Event entity ID from list_events (event_entity_id)'),
|
|
157
|
+
guest_group_id: z.number().optional().describe('Guest group ID — invites every guest in the group'),
|
|
158
|
+
guest_id: z.number().optional().describe('Single guest ID — invites just that guest'),
|
|
159
|
+
},
|
|
160
|
+
annotations: { destructiveHint: false },
|
|
161
|
+
}, inviteGuestToEvent);
|
|
162
|
+
server.registerTool('remove_event_invitation', {
|
|
163
|
+
description: 'Remove an event invitation for a single guest or guest group. Pass exactly one of guest_group_id (removes for all guests in the group) or guest_id (removes for just that guest). Other events’ invitations are preserved. Idempotent.',
|
|
164
|
+
inputSchema: {
|
|
165
|
+
event_id: z.number().describe('Event entity ID from list_events (event_entity_id)'),
|
|
166
|
+
guest_group_id: z.number().optional().describe('Guest group ID — removes for every guest in the group'),
|
|
167
|
+
guest_id: z.number().optional().describe('Single guest ID — removes for just that guest'),
|
|
168
|
+
},
|
|
169
|
+
annotations: { destructiveHint: false },
|
|
170
|
+
}, removeEventInvitation);
|
|
171
|
+
}
|
package/dist/tools/guests.js
CHANGED
|
@@ -80,35 +80,40 @@ export async function updateGuestAddress(args) {
|
|
|
80
80
|
if (!group) {
|
|
81
81
|
throw new Error(`Guest group with ID ${args.guest_group_id} not found`);
|
|
82
82
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
83
|
+
// Full read-modify-write through the verified bulk/directory endpoint
|
|
84
|
+
// (docs/zola-api-quirks.md §5). Crucially, preserve each guest's existing
|
|
85
|
+
// event_invitations — blanking them here would wipe the group's invitations.
|
|
86
|
+
const updatedGuests = group.guests.map((guest) => ({
|
|
87
|
+
...guest,
|
|
88
|
+
address1: args.address1 ?? guest.address1 ?? '',
|
|
89
|
+
address2: args.address2 !== undefined ? args.address2 : guest.address2 ?? '',
|
|
90
|
+
city: args.city ?? guest.city ?? '',
|
|
91
|
+
state_province: args.state_province ?? guest.state_province ?? '',
|
|
92
|
+
postal_code: args.postal_code ?? guest.postal_code ?? '',
|
|
93
|
+
country_code: args.country_code ?? guest.country_code ?? 'US',
|
|
94
|
+
event_invitations: guest.event_invitations ?? [],
|
|
95
|
+
tags: guest.tags ?? [],
|
|
94
96
|
}));
|
|
95
97
|
const body = {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
98
|
+
updated_guest_groups: [
|
|
99
|
+
{
|
|
100
|
+
...group,
|
|
101
|
+
wedding_account_id: group.wedding_account_id ?? weddingAccountId,
|
|
102
|
+
envelope_recipient: group.envelope_recipient ?? '',
|
|
103
|
+
gift_group: group.gift_group ?? {
|
|
104
|
+
gift_count: 0,
|
|
105
|
+
modules: [],
|
|
106
|
+
gift_groups: [],
|
|
107
|
+
guest_group_uuid: '',
|
|
108
|
+
},
|
|
109
|
+
thank_you_note_status: group.thank_you_note_status ?? 'NOT_STARTED',
|
|
110
|
+
rsvp_question_answers: group.rsvp_question_answers ?? [],
|
|
111
|
+
gift_count: group.gift_count ?? 0,
|
|
112
|
+
guests: updatedGuests,
|
|
113
|
+
},
|
|
114
|
+
],
|
|
110
115
|
};
|
|
111
|
-
const result = await client.requestMobile('PUT', `/v3/guestlists/groups/wedding-accounts
|
|
116
|
+
const result = await client.requestMobile('PUT', `/v3/guestlists/groups/wedding-accounts/${weddingAccountId}/bulk/directory`, body);
|
|
112
117
|
return jsonResult(result.data);
|
|
113
118
|
}
|
|
114
119
|
export async function removeGuest(args) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zola-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
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,17 +46,18 @@
|
|
|
46
46
|
"test:watch": "vitest"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@chrischall/mcp-utils": "^0.5.
|
|
50
|
-
"@fetchproxy/bootstrap": "^1.
|
|
51
|
-
"@fetchproxy/server": "^1.
|
|
49
|
+
"@chrischall/mcp-utils": "^0.5.2",
|
|
50
|
+
"@fetchproxy/bootstrap": "^1.3.0",
|
|
51
|
+
"@fetchproxy/server": "^1.3.0",
|
|
52
52
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
53
|
-
"dotenv": "^17.4.2"
|
|
53
|
+
"dotenv": "^17.4.2",
|
|
54
|
+
"zod": "^4.4.3"
|
|
54
55
|
},
|
|
55
56
|
"devDependencies": {
|
|
56
|
-
"@types/node": "^25.9.
|
|
57
|
-
"@vitest/coverage-v8": "^4.1.
|
|
57
|
+
"@types/node": "^25.9.2",
|
|
58
|
+
"@vitest/coverage-v8": "^4.1.8",
|
|
58
59
|
"esbuild": "^0.28.0",
|
|
59
60
|
"typescript": "^6.0.3",
|
|
60
|
-
"vitest": "^4.1.
|
|
61
|
+
"vitest": "^4.1.8"
|
|
61
62
|
}
|
|
62
63
|
}
|
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.4.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "zola-mcp",
|
|
14
|
-
"version": "1.
|
|
14
|
+
"version": "1.4.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
package/skills/zola/SKILL.md
CHANGED
|
@@ -5,7 +5,7 @@ description: This skill should be used when the user asks about Zola wedding pla
|
|
|
5
5
|
|
|
6
6
|
# zola-mcp
|
|
7
7
|
|
|
8
|
-
MCP server for Zola —
|
|
8
|
+
MCP server for Zola — 30 tools for managing your entire wedding via the Zola mobile API.
|
|
9
9
|
|
|
10
10
|
## Tools
|
|
11
11
|
|
|
@@ -41,6 +41,9 @@ MCP server for Zola — 27 tools for managing your entire wedding via the Zola m
|
|
|
41
41
|
- `list_events` — All events with RSVP counts
|
|
42
42
|
- `track_rsvps` — RSVP tracking per event
|
|
43
43
|
- `update_event` — Update event details
|
|
44
|
+
- `set_event_guests` — Bulk set which guest groups are invited to an event
|
|
45
|
+
- `invite_guest_to_event` — Invite one guest or group to an event
|
|
46
|
+
- `remove_event_invitation` — Remove an event invitation for a guest or group
|
|
44
47
|
|
|
45
48
|
### Registry & Gifts
|
|
46
49
|
- `get_registry` — Registry categories and items
|