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.
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "metadata": {
9
9
  "description": "Zola wedding planning tools for Claude Code",
10
- "version": "1.3.2"
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.3.2",
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.3.2",
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
- "read_indexed_db"
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
- throw new ProtocolError(`inner.op: must be one of "fetch", "read_cookies", "read_local_storage", "read_session_storage", "capture_request_header", "read_indexed_db"; got ${JSON.stringify(raw.op)}`);
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.3.2",
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.0",
37739
- "@fetchproxy/bootstrap": "^1.0.0",
37740
- "@fetchproxy/server": "^1.0.0",
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.1",
37746
- "@vitest/coverage-v8": "^4.1.7",
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.7"
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((entry) => ({
38299
- ...entry.guest,
38300
- guest_id: entry.guest.guest_id,
38301
- address1: args.address1 ?? entry.guest.address1 ?? "",
38302
- address2: args.address2 !== void 0 ? args.address2 : entry.guest.address2 ?? "",
38303
- city: args.city ?? entry.guest.city ?? "",
38304
- state_province: args.state_province ?? entry.guest.state_province ?? "",
38305
- postal_code: args.postal_code ?? entry.guest.postal_code ?? "",
38306
- country_code: args.country_code ?? entry.guest.country_code ?? "US",
38307
- event_invitations: [],
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
- guest_group_request: {
38312
- wedding_account_id: weddingAccountId,
38313
- guest_group_id: args.guest_group_id,
38314
- guest_group_uuid: group.guest_group_uuid,
38315
- guest_group_affiliation: group.guest_group_affiliation,
38316
- guest_group_tier: group.guest_group_tier,
38317
- invited: group.invited,
38318
- invitation_sent: group.invitation_sent,
38319
- save_the_date_sent: group.save_the_date_sent,
38320
- envelope_recipient: group.envelope_recipient ?? "",
38321
- addressing_style: group.addressing_style,
38322
- guests: updatedGuests,
38323
- gift_count: 0
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/id/${weddingAccountId}/suite`,
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.3.2";
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
- const VERSION = '1.3.2'; // x-release-please-version
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
+ }
@@ -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
- const updatedGuests = group.guests.map((entry) => ({
84
- ...entry.guest,
85
- guest_id: entry.guest.guest_id,
86
- address1: args.address1 ?? entry.guest.address1 ?? '',
87
- address2: args.address2 !== undefined ? args.address2 : entry.guest.address2 ?? '',
88
- city: args.city ?? entry.guest.city ?? '',
89
- state_province: args.state_province ?? entry.guest.state_province ?? '',
90
- postal_code: args.postal_code ?? entry.guest.postal_code ?? '',
91
- country_code: args.country_code ?? entry.guest.country_code ?? 'US',
92
- event_invitations: [],
93
- tags: [],
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
- guest_group_request: {
97
- wedding_account_id: weddingAccountId,
98
- guest_group_id: args.guest_group_id,
99
- guest_group_uuid: group.guest_group_uuid,
100
- guest_group_affiliation: group.guest_group_affiliation,
101
- guest_group_tier: group.guest_group_tier,
102
- invited: group.invited,
103
- invitation_sent: group.invitation_sent,
104
- save_the_date_sent: group.save_the_date_sent,
105
- envelope_recipient: group.envelope_recipient ?? '',
106
- addressing_style: group.addressing_style,
107
- guests: updatedGuests,
108
- gift_count: 0,
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/id/${weddingAccountId}/suite`, body);
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.2",
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.0",
50
- "@fetchproxy/bootstrap": "^1.0.0",
51
- "@fetchproxy/server": "^1.0.0",
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.1",
57
- "@vitest/coverage-v8": "^4.1.7",
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.7"
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.3.2",
9
+ "version": "1.4.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "zola-mcp",
14
- "version": "1.3.2",
14
+ "version": "1.4.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
@@ -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 — 27 tools for managing your entire wedding via the Zola mobile API.
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