signupgenius-mcp 1.0.3 → 1.0.7

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.
@@ -0,0 +1,34 @@
1
+ {
2
+ "$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
3
+ "name": "signupgenius-mcp",
4
+ "owner": {
5
+ "name": "Chris Hall",
6
+ "email": "chris.c.hall@gmail.com"
7
+ },
8
+ "metadata": {
9
+ "description": "MCP server for SignUpGenius — read sign-ups, slot reports, and groups; add group members.",
10
+ "version": "1.0.7"
11
+ },
12
+ "plugins": [
13
+ {
14
+ "name": "signupgenius-mcp",
15
+ "displayName": "SignUpGenius",
16
+ "source": "./",
17
+ "description": "SignUpGenius MCP server for Claude — sign-ups, slot reports, and groups via natural language. Free or Pro accounts.",
18
+ "version": "1.0.7",
19
+ "author": {
20
+ "name": "Chris Hall"
21
+ },
22
+ "homepage": "https://github.com/chrischall/signupgenius-mcp",
23
+ "repository": "https://github.com/chrischall/signupgenius-mcp",
24
+ "license": "MIT",
25
+ "keywords": [
26
+ "signupgenius",
27
+ "signups",
28
+ "volunteers",
29
+ "mcp"
30
+ ],
31
+ "category": "productivity"
32
+ }
33
+ ]
34
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "signupgenius-mcp",
3
+ "displayName": "SignUpGenius",
4
+ "version": "1.0.7",
5
+ "description": "SignUpGenius MCP server for Claude — read sign-ups, slot reports, and groups; add group members. Three auth paths: Pro API key, email/password, or sign in via the fetchproxy browser extension.",
6
+ "author": {
7
+ "name": "Chris Hall",
8
+ "email": "chris.c.hall@gmail.com"
9
+ },
10
+ "homepage": "https://github.com/chrischall/signupgenius-mcp",
11
+ "repository": "https://github.com/chrischall/signupgenius-mcp",
12
+ "license": "MIT",
13
+ "keywords": [
14
+ "signupgenius",
15
+ "signups",
16
+ "volunteers",
17
+ "mcp"
18
+ ]
19
+ }
package/README.md CHANGED
@@ -88,3 +88,25 @@ Tests: vitest, 100% line/branch/function coverage. End-to-end tests against the
88
88
  - For testing the Pro v2/k surface without an account, SignUpGenius publishes a frozen demo key: `V0FzMkxZcmVOZlVnclZMVEl6dGhWQT09`.
89
89
 
90
90
  Developed and maintained by AI (Claude). Use at your own discretion.
91
+
92
+ ## Acknowledgement of Terms
93
+
94
+ By using this MCP server, you acknowledge and agree to the following:
95
+
96
+ **1. This server accesses your own SignUpGenius account.** Auth happens via your own credentials. It does not — and cannot — access anyone else's account or signups.
97
+
98
+ **2. [SignUpGenius's Terms of Service](https://www.signupgenius.com/terms-of-service) govern your use of this server**, just as they govern your direct use of signupgenius.com. The clauses most relevant here:
99
+
100
+ > Users may not bypass any robot exclusion headers or other measures we take to restrict access to the Services or use any software, technology, or device to scrape, spider, or crawl the Services.
101
+
102
+ And: *"You are responsible for maintaining the confidentiality of your account user name and password… You agree to accept responsibility for any and all activities or actions that occur in connection with your User Credentials."*
103
+
104
+ You are agreeing to those terms — read by the maintainer 2026-05-23 — every time you invoke a tool in this server. Notably, **SignUpGenius does offer an official API** for paid plans; where possible, prefer the official API over the endpoints this MCP exercises.
105
+
106
+ **3. Personal, organizer/participant use only.** This project is not affiliated with, endorsed by, sponsored by, or in partnership with SignUpGenius, Inc. It is a personal automation tool for an authenticated user to manage their own signups and groups. Do not use it to scrape other organizers' signups, spam participants, or bulk-add fake group members.
107
+
108
+ **4. Stability is not guaranteed.** This server may call internal endpoints that SignUpGenius can change without notice. If a tool here breaks, the canonical fix is to use the official API where available.
109
+
110
+ **5. You accept full responsibility** for any consequences of using this server in connection with your SignUpGenius account — rate limiting, account warnings, suspension, or any enforcement action. Per the ToS, everything done under your credentials is attributed to you. If SignUpGenius objects to your use, stop using this server.
111
+
112
+ This section is the maintainer's good-faith summary of the terms — it is not legal advice and does not modify or supersede SignUpGenius's actual ToS.
package/dist/bundle.js CHANGED
@@ -7659,6 +7659,10 @@ var require_receiver = __commonJS({
7659
7659
  * extensions
7660
7660
  * @param {Boolean} [options.isServer=false] Specifies whether to operate in
7661
7661
  * client or server mode
7662
+ * @param {Number} [options.maxBufferedChunks=0] The maximum number of
7663
+ * buffered data chunks
7664
+ * @param {Number} [options.maxFragments=0] The maximum number of message
7665
+ * fragments
7662
7666
  * @param {Number} [options.maxPayload=0] The maximum allowed message length
7663
7667
  * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
7664
7668
  * not to skip UTF-8 validation for text and close messages
@@ -7669,6 +7673,8 @@ var require_receiver = __commonJS({
7669
7673
  this._binaryType = options.binaryType || BINARY_TYPES[0];
7670
7674
  this._extensions = options.extensions || {};
7671
7675
  this._isServer = !!options.isServer;
7676
+ this._maxBufferedChunks = options.maxBufferedChunks | 0;
7677
+ this._maxFragments = options.maxFragments | 0;
7672
7678
  this._maxPayload = options.maxPayload | 0;
7673
7679
  this._skipUTF8Validation = !!options.skipUTF8Validation;
7674
7680
  this[kWebSocket] = void 0;
@@ -7698,6 +7704,18 @@ var require_receiver = __commonJS({
7698
7704
  */
7699
7705
  _write(chunk, encoding, cb) {
7700
7706
  if (this._opcode === 8 && this._state == GET_INFO) return cb();
7707
+ if (this._maxBufferedChunks > 0 && this._buffers.length >= this._maxBufferedChunks) {
7708
+ cb(
7709
+ this.createError(
7710
+ RangeError,
7711
+ "Too many buffered chunks",
7712
+ false,
7713
+ 1008,
7714
+ "WS_ERR_TOO_MANY_BUFFERED_PARTS"
7715
+ )
7716
+ );
7717
+ return;
7718
+ }
7701
7719
  this._bufferedBytes += chunk.length;
7702
7720
  this._buffers.push(chunk);
7703
7721
  this.startLoop(cb);
@@ -8027,6 +8045,17 @@ var require_receiver = __commonJS({
8027
8045
  return;
8028
8046
  }
8029
8047
  if (data.length) {
8048
+ if (this._maxFragments > 0 && this._fragments.length >= this._maxFragments) {
8049
+ const error51 = this.createError(
8050
+ RangeError,
8051
+ "Too many message fragments",
8052
+ false,
8053
+ 1008,
8054
+ "WS_ERR_TOO_MANY_BUFFERED_PARTS"
8055
+ );
8056
+ cb(error51);
8057
+ return;
8058
+ }
8030
8059
  this._messageLength = this._totalPayloadLength;
8031
8060
  this._fragments.push(data);
8032
8061
  }
@@ -8056,6 +8085,17 @@ var require_receiver = __commonJS({
8056
8085
  cb(error51);
8057
8086
  return;
8058
8087
  }
8088
+ if (this._maxFragments > 0 && this._fragments.length >= this._maxFragments) {
8089
+ const error51 = this.createError(
8090
+ RangeError,
8091
+ "Too many message fragments",
8092
+ false,
8093
+ 1008,
8094
+ "WS_ERR_TOO_MANY_BUFFERED_PARTS"
8095
+ );
8096
+ cb(error51);
8097
+ return;
8098
+ }
8059
8099
  this._fragments.push(buf);
8060
8100
  }
8061
8101
  this.dataMessage(cb);
@@ -9262,6 +9302,10 @@ var require_websocket = __commonJS({
9262
9302
  * multiple times in the same tick
9263
9303
  * @param {Function} [options.generateMask] The function used to generate the
9264
9304
  * masking key
9305
+ * @param {Number} [options.maxBufferedChunks=0] The maximum number of
9306
+ * buffered data chunks
9307
+ * @param {Number} [options.maxFragments=0] The maximum number of message
9308
+ * fragments
9265
9309
  * @param {Number} [options.maxPayload=0] The maximum allowed message size
9266
9310
  * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
9267
9311
  * not to skip UTF-8 validation for text and close messages
@@ -9273,6 +9317,8 @@ var require_websocket = __commonJS({
9273
9317
  binaryType: this.binaryType,
9274
9318
  extensions: this._extensions,
9275
9319
  isServer: this._isServer,
9320
+ maxBufferedChunks: options.maxBufferedChunks,
9321
+ maxFragments: options.maxFragments,
9276
9322
  maxPayload: options.maxPayload,
9277
9323
  skipUTF8Validation: options.skipUTF8Validation
9278
9324
  });
@@ -9572,6 +9618,8 @@ var require_websocket = __commonJS({
9572
9618
  autoPong: true,
9573
9619
  closeTimeout: CLOSE_TIMEOUT,
9574
9620
  protocolVersion: protocolVersions[1],
9621
+ maxBufferedChunks: 1024 * 1024,
9622
+ maxFragments: 128 * 1024,
9575
9623
  maxPayload: 100 * 1024 * 1024,
9576
9624
  skipUTF8Validation: false,
9577
9625
  perMessageDeflate: true,
@@ -9814,6 +9862,8 @@ var require_websocket = __commonJS({
9814
9862
  websocket.setSocket(socket, head, {
9815
9863
  allowSynchronousEvents: opts.allowSynchronousEvents,
9816
9864
  generateMask: opts.generateMask,
9865
+ maxBufferedChunks: opts.maxBufferedChunks,
9866
+ maxFragments: opts.maxFragments,
9817
9867
  maxPayload: opts.maxPayload,
9818
9868
  skipUTF8Validation: opts.skipUTF8Validation
9819
9869
  });
@@ -10156,6 +10206,10 @@ var require_websocket_server = __commonJS({
10156
10206
  * called
10157
10207
  * @param {Function} [options.handleProtocols] A hook to handle protocols
10158
10208
  * @param {String} [options.host] The hostname where to bind the server
10209
+ * @param {Number} [options.maxBufferedChunks=1048576] The maximum number of
10210
+ * buffered data chunks
10211
+ * @param {Number} [options.maxFragments=131072] The maximum number of message
10212
+ * fragments
10159
10213
  * @param {Number} [options.maxPayload=104857600] The maximum allowed message
10160
10214
  * size
10161
10215
  * @param {Boolean} [options.noServer=false] Enable no server mode
@@ -10177,6 +10231,8 @@ var require_websocket_server = __commonJS({
10177
10231
  options = {
10178
10232
  allowSynchronousEvents: true,
10179
10233
  autoPong: true,
10234
+ maxBufferedChunks: 1024 * 1024,
10235
+ maxFragments: 128 * 1024,
10180
10236
  maxPayload: 100 * 1024 * 1024,
10181
10237
  skipUTF8Validation: false,
10182
10238
  perMessageDeflate: false,
@@ -10456,6 +10512,8 @@ var require_websocket_server = __commonJS({
10456
10512
  socket.removeListener("error", socketOnError);
10457
10513
  ws.setSocket(socket, head, {
10458
10514
  allowSynchronousEvents: this.options.allowSynchronousEvents,
10515
+ maxBufferedChunks: this.options.maxBufferedChunks,
10516
+ maxFragments: this.options.maxFragments,
10459
10517
  maxPayload: this.options.maxPayload,
10460
10518
  skipUTF8Validation: this.options.skipUTF8Validation
10461
10519
  });
@@ -34582,6 +34640,7 @@ var StdioServerTransport = class {
34582
34640
 
34583
34641
  // node_modules/@fetchproxy/protocol/dist/frames.js
34584
34642
  var PROTOCOL_VERSION = 2;
34643
+ var HKDF_SESSION_INFO = "fetchproxy/1.0.0/session";
34585
34644
  var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
34586
34645
  "fetch",
34587
34646
  "read_cookies",
@@ -34656,7 +34715,7 @@ var ProtocolError = class extends Error {
34656
34715
  var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
34657
34716
  var BASE64_RE = /^[A-Za-z0-9+/]*={0,2}$/;
34658
34717
  var SCOPE_KEY_RE = /^[A-Za-z0-9_.\-]{1,256}$/;
34659
- var SCOPE_KEY_GLOB_RE = /^[A-Za-z0-9_.\-]{1,255}\*?$/;
34718
+ var SCOPE_KEY_GLOB_RE = /^[A-Za-z0-9_.\-]{1,256}\*?$/;
34660
34719
  var HEADER_NAME_RE = /^[A-Za-z0-9_\-]{1,128}$/;
34661
34720
  var HOSTNAME_RE = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$/i;
34662
34721
  function assertObject(x, label) {
@@ -34785,7 +34844,7 @@ function assertStoragePointersArray(value, label, declaredKeys) {
34785
34844
  if (entry.jsonPointer === void 0) {
34786
34845
  throw new ProtocolError(`${label}[${i}].jsonPointer: missing`);
34787
34846
  }
34788
- if (typeof entry.key !== "string" || !SCOPE_KEY_GLOB_RE.test(entry.key)) {
34847
+ if (typeof entry.key !== "string" || !SCOPE_KEY_RE.test(entry.key)) {
34789
34848
  throw new ProtocolError(`${label}[${i}].key: invalid key ${JSON.stringify(entry.key)}`);
34790
34849
  }
34791
34850
  if (typeof entry.jsonPointer !== "string" || !isValidJsonPointer(entry.jsonPointer)) {
@@ -34884,6 +34943,8 @@ function validateFrame(raw) {
34884
34943
  return validateReady(raw);
34885
34944
  if (t === "frame")
34886
34945
  return validateEncrypted(raw);
34946
+ if (t === "pair-pending")
34947
+ return validatePairPending(raw);
34887
34948
  throw new ProtocolError(`unknown frame type: ${String(t)}`);
34888
34949
  }
34889
34950
  function validateHello(raw) {
@@ -34985,6 +35046,17 @@ function validateEncrypted(raw) {
34985
35046
  assertBase64(raw.ciphertext, "frame.ciphertext");
34986
35047
  return raw;
34987
35048
  }
35049
+ var PAIR_CODE_RE = /^\d{3}-\d{3}$/;
35050
+ function validatePairPending(raw) {
35051
+ assertString(raw.mcpId, "pair-pending.mcpId");
35052
+ if (!isValidMcpId(raw.mcpId))
35053
+ throw new ProtocolError("pair-pending.mcpId: invalid format");
35054
+ assertString(raw.pairCode, "pair-pending.pairCode");
35055
+ if (!PAIR_CODE_RE.test(raw.pairCode)) {
35056
+ throw new ProtocolError(`pair-pending.pairCode: must match XXX-XXX, got ${String(raw.pairCode)}`);
35057
+ }
35058
+ return { type: "pair-pending", mcpId: raw.mcpId, pairCode: raw.pairCode };
35059
+ }
34988
35060
  function validateInnerFrame(raw) {
34989
35061
  assertObject(raw, "inner");
34990
35062
  const t = raw.type;
@@ -35567,15 +35639,22 @@ async function startHost(opts) {
35567
35639
  let extensionWs = null;
35568
35640
  const peers = /* @__PURE__ */ new Map();
35569
35641
  const ownInnerListeners = [];
35642
+ const disconnectListeners = [];
35643
+ const pendingPairListeners = [];
35570
35644
  let ownSession = null;
35645
+ let ownPendingPairCode = null;
35571
35646
  let resolveOwnSession;
35572
35647
  let rejectOwnSession;
35573
- const ownSessionReady = new Promise((resolve, reject) => {
35574
- resolveOwnSession = resolve;
35575
- rejectOwnSession = reject;
35576
- });
35577
- ownSessionReady.catch(() => {
35578
- });
35648
+ let ownSessionReady;
35649
+ function resetSessionPromise() {
35650
+ ownSessionReady = new Promise((resolve, reject) => {
35651
+ resolveOwnSession = resolve;
35652
+ rejectOwnSession = reject;
35653
+ });
35654
+ ownSessionReady.catch(() => {
35655
+ });
35656
+ }
35657
+ resetSessionPromise();
35579
35658
  let extensionHello = null;
35580
35659
  wss.on("connection", (ws) => {
35581
35660
  let identified = null;
@@ -35644,11 +35723,12 @@ async function startHost(opts) {
35644
35723
  }
35645
35724
  const extPub = fromB64(frame.extensionSessionPub);
35646
35725
  const shared = await ecdhX25519(opts.ownIdentity.x25519Priv, extPub);
35647
- const key = await hkdfSha256(shared, ownSessionNonce, enc2.encode("fetchproxy/0.1.0/session"), 32);
35648
- if (!ownSession) {
35649
- ownSession = new SessionState(key);
35650
- resolveOwnSession(ownSession);
35651
- }
35726
+ const key = await hkdfSha256(shared, ownSessionNonce, enc2.encode(HKDF_SESSION_INFO), 32);
35727
+ if (extensionWs !== ws)
35728
+ return;
35729
+ ownSession = new SessionState(key);
35730
+ ownPendingPairCode = null;
35731
+ resolveOwnSession(ownSession);
35652
35732
  } else {
35653
35733
  const slot = peers.get(frame.mcpId);
35654
35734
  if (slot)
@@ -35675,6 +35755,16 @@ async function startHost(opts) {
35675
35755
  extensionWs.send(JSON.stringify(frame));
35676
35756
  }
35677
35757
  }
35758
+ if (frame.type === "pair-pending" && identified === "extension") {
35759
+ if (frame.mcpId === opts.ownMcpId) {
35760
+ ownPendingPairCode = frame.pairCode;
35761
+ pendingPairListeners.forEach((cb) => cb(frame.pairCode));
35762
+ } else {
35763
+ const slot = peers.get(frame.mcpId);
35764
+ if (slot)
35765
+ slot.ws.send(JSON.stringify(frame));
35766
+ }
35767
+ }
35678
35768
  } catch (e) {
35679
35769
  console.error("[fetchproxy] host: message handler error:", e);
35680
35770
  try {
@@ -35690,6 +35780,9 @@ async function startHost(opts) {
35690
35780
  if (!ownSession) {
35691
35781
  rejectOwnSession(new Error("extension disconnected before ready"));
35692
35782
  }
35783
+ ownSession = null;
35784
+ resetSessionPromise();
35785
+ disconnectListeners.forEach((cb) => cb());
35693
35786
  }
35694
35787
  if (identified === "peer" && peerMcpId)
35695
35788
  peers.delete(peerMcpId);
@@ -35714,7 +35807,14 @@ async function startHost(opts) {
35714
35807
  },
35715
35808
  onOwnInner: (cb) => {
35716
35809
  ownInnerListeners.push(cb);
35717
- }
35810
+ },
35811
+ onExtensionDisconnect: (cb) => {
35812
+ disconnectListeners.push(cb);
35813
+ },
35814
+ onPendingPair: (cb) => {
35815
+ pendingPairListeners.push(cb);
35816
+ },
35817
+ pendingPairCode: () => ownPendingPairCode
35718
35818
  };
35719
35819
  }
35720
35820
 
@@ -35744,36 +35844,57 @@ async function startPeer(opts) {
35744
35844
  const sessionNonce = fromB64(hello.sessionNonce);
35745
35845
  ws.send(JSON.stringify(hello));
35746
35846
  const innerListeners = [];
35847
+ const renegotiateListeners = [];
35848
+ const pendingPairListeners = [];
35747
35849
  let session = null;
35850
+ let pendingPairCode = null;
35851
+ let resolveFirstReady;
35852
+ let rejectFirstReady;
35748
35853
  const sessionPromise = new Promise((resolve, reject) => {
35749
- const onMessage = async (data) => {
35750
- try {
35751
- const raw = JSON.parse(data.toString());
35752
- const frame = validateFrame(raw);
35753
- if (frame.type === "ready" && frame.mcpId === opts.mcpId) {
35754
- const extPub = fromB64(frame.extensionSessionPub);
35755
- const shared = await ecdhX25519(opts.identity.x25519Priv, extPub);
35756
- const sessionKey = await hkdfSha256(shared, sessionNonce, enc3.encode("fetchproxy/0.1.0/session"), 32);
35757
- session = new SessionState(sessionKey);
35758
- resolve(session);
35759
- return;
35854
+ resolveFirstReady = resolve;
35855
+ rejectFirstReady = reject;
35856
+ });
35857
+ const onMessage = async (data) => {
35858
+ try {
35859
+ const raw = JSON.parse(data.toString());
35860
+ const frame = validateFrame(raw);
35861
+ if (frame.type === "ready" && frame.mcpId === opts.mcpId) {
35862
+ const extPub = fromB64(frame.extensionSessionPub);
35863
+ const shared = await ecdhX25519(opts.identity.x25519Priv, extPub);
35864
+ const sessionKey = await hkdfSha256(shared, sessionNonce, enc3.encode(HKDF_SESSION_INFO), 32);
35865
+ const isRenegotiation = session !== null;
35866
+ session = new SessionState(sessionKey);
35867
+ pendingPairCode = null;
35868
+ if (isRenegotiation) {
35869
+ renegotiateListeners.forEach((cb) => cb());
35870
+ } else {
35871
+ resolveFirstReady(session);
35760
35872
  }
35761
- if (frame.type === "frame" && frame.mcpId === opts.mcpId) {
35762
- if (!session)
35763
- return;
35764
- if (!session.acceptInboundSeq(frame.seq))
35765
- return;
35873
+ return;
35874
+ }
35875
+ if (frame.type === "pair-pending" && frame.mcpId === opts.mcpId) {
35876
+ pendingPairCode = frame.pairCode;
35877
+ pendingPairListeners.forEach((cb) => cb(frame.pairCode));
35878
+ return;
35879
+ }
35880
+ if (frame.type === "frame" && frame.mcpId === opts.mcpId) {
35881
+ if (!session)
35882
+ return;
35883
+ if (!session.acceptInboundSeq(frame.seq))
35884
+ return;
35885
+ try {
35766
35886
  const inner = await openEncryptedFrame(session.sessionKey, frame);
35767
35887
  innerListeners.forEach((cb) => cb(inner));
35888
+ } catch {
35768
35889
  }
35769
- } catch (e) {
35770
- reject(e instanceof Error ? e : new Error(String(e)));
35771
35890
  }
35772
- };
35773
- ws.on("message", onMessage);
35774
- ws.once("close", () => {
35775
- reject(new Error("peer WS closed before ready"));
35776
- });
35891
+ } catch (e) {
35892
+ rejectFirstReady(e instanceof Error ? e : new Error(String(e)));
35893
+ }
35894
+ };
35895
+ ws.on("message", onMessage);
35896
+ ws.once("close", () => {
35897
+ rejectFirstReady(new Error("peer WS closed before ready"));
35777
35898
  });
35778
35899
  sessionPromise.catch(() => {
35779
35900
  });
@@ -35781,13 +35902,21 @@ async function startPeer(opts) {
35781
35902
  ws,
35782
35903
  session: sessionPromise,
35783
35904
  sendInner: async (inner) => {
35784
- const s = await sessionPromise;
35905
+ await sessionPromise;
35906
+ const s = session;
35785
35907
  const sealed = await sealInnerFrame(s.sessionKey, opts.mcpId, s.nextOutboundSeq(), inner);
35786
35908
  ws.send(JSON.stringify(sealed));
35787
35909
  },
35788
35910
  onInner: (cb) => {
35789
35911
  innerListeners.push(cb);
35790
35912
  },
35913
+ onRenegotiate: (cb) => {
35914
+ renegotiateListeners.push(cb);
35915
+ },
35916
+ onPendingPair: (cb) => {
35917
+ pendingPairListeners.push(cb);
35918
+ },
35919
+ pendingPairCode: () => pendingPairCode,
35791
35920
  close: () => ws.close()
35792
35921
  };
35793
35922
  return handle;
@@ -35844,6 +35973,32 @@ async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
35844
35973
  return id;
35845
35974
  }
35846
35975
 
35976
+ // node_modules/@fetchproxy/server/dist/error-kind.js
35977
+ function classifyFetchError(error51) {
35978
+ if (/Could not establish connection/i.test(error51) || /Receiving end does not exist/i.test(error51)) {
35979
+ return "content_script_unreachable";
35980
+ }
35981
+ if (/^tab fetch failed:/.test(error51)) {
35982
+ return "tab_fetch_failed";
35983
+ }
35984
+ if (/^fetch threw:/.test(error51)) {
35985
+ return "tab_fetch_failed";
35986
+ }
35987
+ if (/^no tab matching /.test(error51)) {
35988
+ return "no_tab";
35989
+ }
35990
+ if (/not in domains \[/.test(error51)) {
35991
+ return "domain_denied";
35992
+ }
35993
+ if (/^capability .+ not granted/.test(error51)) {
35994
+ return "capability_denied";
35995
+ }
35996
+ if (/^(request|response) body too large:/.test(error51)) {
35997
+ return "body_too_large";
35998
+ }
35999
+ return "other";
36000
+ }
36001
+
35847
36002
  // node_modules/@fetchproxy/server/dist/ws-server.js
35848
36003
  var FetchproxyProtocolError = class extends Error {
35849
36004
  constructor(message) {
@@ -35886,7 +36041,11 @@ function assertUrlInDomains(field, url2, domains) {
35886
36041
  }
35887
36042
  var DEFAULT_JSON_OK_STATUSES = [200, 201, 202, 204];
35888
36043
  var FetchproxyServer = class {
35889
- /** Set after `listen()` succeeds. Null while not listening. */
36044
+ /**
36045
+ * Bridge role. `null` until the first verb call (or an explicit
36046
+ * `connect()`) — `listen()` no longer triggers the role election
36047
+ * as of 0.5.3+. Reset to `null` on `close()`.
36048
+ */
35890
36049
  role = null;
35891
36050
  opts;
35892
36051
  hostHandle = null;
@@ -35908,6 +36067,12 @@ var FetchproxyServer = class {
35908
36067
  pendingIdb = /* @__PURE__ */ new Map();
35909
36068
  mcpId = null;
35910
36069
  identity = null;
36070
+ // 0.5.3+: in-flight role-election / handle-start promise. Set the
36071
+ // first time a verb call runs `ensureConnected`, awaited by concurrent
36072
+ // callers, cleared once the connection is up. Single source of truth
36073
+ // for "we're connecting right now" so two parallel first-calls don't
36074
+ // race the port bind.
36075
+ connectingPromise = null;
35911
36076
  constructor(opts) {
35912
36077
  if (!Array.isArray(opts.domains) || opts.domains.length === 0) {
35913
36078
  throw new Error("FetchproxyServer: opts.domains must be a non-empty array of hostnames");
@@ -35959,23 +36124,87 @@ var FetchproxyServer = class {
35959
36124
  };
35960
36125
  }
35961
36126
  /**
35962
- * Start the WebSocket bridge. Loads the long-term identity keypair
35963
- * from disk (creating it on first call), elects the host-vs-peer
35964
- * role by attempting to bind the configured port, and stands up the
35965
- * matching handshake machinery. Idempotent only insofar as it leaves
35966
- * `role` non-null on success; calling `listen()` twice without an
35967
- * intervening `close()` is a programming error.
36127
+ * Prepare the bridge for use. Loads the long-term identity keypair
36128
+ * from disk (creating it on first call) and computes this instance's
36129
+ * `mcpId`. Does NOT bind the bridge port or dial any WebSocket — the
36130
+ * connection is established lazily on the first verb call (see
36131
+ * `ensureConnected` / `getOrConnect`).
36132
+ *
36133
+ * Pre-0.5.3 behavior: `listen()` also did role election and started
36134
+ * the host/peer immediately, which meant every configured-but-unused
36135
+ * MCP claimed bridge resources at MCP-client boot. Several MCPs
36136
+ * starting in parallel under Claude Desktop also produced noisy
36137
+ * `ERR_CONNECTION_REFUSED` errors in the extension if it raced ahead
36138
+ * of the first MCP's port bind. Deferring keeps boot quiet and
36139
+ * leaves the port unowned until something actually needs it.
36140
+ *
36141
+ * Calling `listen()` twice without an intervening `close()` is a
36142
+ * no-op (the second call's identity load is idempotent).
35968
36143
  */
35969
36144
  async listen() {
35970
- this.identity = await loadOrCreateIdentity(this.opts.serverName, this.opts.identityDir);
35971
- this.mcpId = generateMcpId(this.opts.serverName, this.opts.version);
36145
+ if (!this.identity) {
36146
+ this.identity = await loadOrCreateIdentity(this.opts.serverName, this.opts.identityDir);
36147
+ }
36148
+ if (!this.mcpId) {
36149
+ this.mcpId = generateMcpId(this.opts.serverName, this.opts.version);
36150
+ }
36151
+ }
36152
+ /**
36153
+ * Force an eager bridge connection (role-election + host/peer handle
36154
+ * start + listener wiring) without waiting for the first verb call.
36155
+ * Useful for callers that want to surface the role / connection
36156
+ * outcome at boot, or for tests whose harness dials a mock extension
36157
+ * immediately after server construction. Production MCPs that just
36158
+ * answer tool calls should NOT call this — the lazy connect via
36159
+ * `ensureConnected` will do the right thing on first use, keeping
36160
+ * boot cheap and avoiding port-bind contention for MCPs that never
36161
+ * actually get invoked.
36162
+ *
36163
+ * Idempotent: a second call after the first has resolved is a no-op
36164
+ * (the existing handle is reused). Throws if `listen()` was never
36165
+ * called.
36166
+ */
36167
+ async connect() {
36168
+ await this.ensureConnected();
36169
+ }
36170
+ /**
36171
+ * Establish the bridge connection (role-election + host/peer handle
36172
+ * start + listener wiring) the first time a verb is invoked.
36173
+ * Idempotent after the connection is up; concurrent first-callers
36174
+ * share the same in-flight promise so only one election happens.
36175
+ *
36176
+ * Throws if `listen()` was never called — the contract is that the
36177
+ * MCP author still must wire `transport.start()` at boot to load
36178
+ * identity / set mcpId, even though the WS doesn't open until a
36179
+ * verb runs.
36180
+ */
36181
+ async ensureConnected() {
36182
+ if (this.hostHandle || this.peerHandle)
36183
+ return;
36184
+ if (this.connectingPromise) {
36185
+ await this.connectingPromise;
36186
+ return;
36187
+ }
36188
+ if (!this.identity || !this.mcpId) {
36189
+ throw new Error("FetchproxyServer: ensureConnected called before listen() \u2014 call listen() at MCP boot to load identity");
36190
+ }
36191
+ this.connectingPromise = this.doConnect();
36192
+ try {
36193
+ await this.connectingPromise;
36194
+ } finally {
36195
+ this.connectingPromise = null;
36196
+ }
36197
+ }
36198
+ async doConnect() {
36199
+ const identity = this.identity;
36200
+ const mcpId = this.mcpId;
35972
36201
  const el = await electRole({ host: this.opts.host, port: this.opts.port });
35973
36202
  if (el.role === "host") {
35974
36203
  this.role = "host";
35975
36204
  this.hostHandle = await startHost({
35976
36205
  httpServer: el.server,
35977
- ownIdentity: this.identity,
35978
- ownMcpId: this.mcpId,
36206
+ ownIdentity: identity,
36207
+ ownMcpId: mcpId,
35979
36208
  ownServerName: this.opts.serverName,
35980
36209
  ownVersion: this.opts.version,
35981
36210
  ownDomains: this.opts.domains,
@@ -35990,13 +36219,17 @@ var FetchproxyServer = class {
35990
36219
  onPairCode: this.opts.onPairCode
35991
36220
  });
35992
36221
  this.hostHandle.onOwnInner((inner) => this.onInner(inner));
36222
+ this.hostHandle.onExtensionDisconnect(() => this.rejectAllPending());
36223
+ this.hostHandle.onPendingPair((code) => {
36224
+ this.rejectAllPending(this.pairingErrorMessage(code));
36225
+ });
35993
36226
  } else {
35994
36227
  this.role = "peer";
35995
36228
  this.peerHandle = await startPeer({
35996
36229
  host: this.opts.host,
35997
36230
  port: this.opts.port,
35998
- identity: this.identity,
35999
- mcpId: this.mcpId,
36231
+ identity,
36232
+ mcpId,
36000
36233
  serverName: this.opts.serverName,
36001
36234
  version: this.opts.version,
36002
36235
  domains: this.opts.domains,
@@ -36010,8 +36243,19 @@ var FetchproxyServer = class {
36010
36243
  sessionStoragePointers: this.opts.sessionStoragePointers
36011
36244
  });
36012
36245
  this.peerHandle.onInner((inner) => this.onInner(inner));
36246
+ this.peerHandle.onRenegotiate(() => this.rejectAllPending());
36247
+ this.peerHandle.onPendingPair((code) => {
36248
+ this.rejectAllPending(this.pairingErrorMessage(code));
36249
+ });
36250
+ if (this.opts.onPairCode) {
36251
+ const cb = this.opts.onPairCode;
36252
+ this.peerHandle.onPendingPair((code) => cb(code));
36253
+ }
36013
36254
  }
36014
36255
  }
36256
+ pairingErrorMessage(code) {
36257
+ return `fetchproxy: pairing required for ${this.opts.serverName} \u2014 open the fetchproxy extension popup in Chrome and approve the pair request. Verify the pair code matches: ${code}`;
36258
+ }
36015
36259
  /**
36016
36260
  * Raw single-shot fetch through the bridge. Most callers should prefer
36017
36261
  * the verb shortcuts (`get` / `post` / `getJson` / `postJson` / `getHtml`)
@@ -36027,8 +36271,11 @@ var FetchproxyServer = class {
36027
36271
  * offline, etc.).
36028
36272
  */
36029
36273
  async fetch(init) {
36030
- if (!this.hostHandle && !this.peerHandle) {
36031
- throw new Error("FetchproxyServer.fetch called before listen() \u2014 not listening");
36274
+ await this.ensureConnected();
36275
+ const pendingCode = this.currentPendingPairCode();
36276
+ if (pendingCode !== null) {
36277
+ const error51 = this.pairingErrorMessage(pendingCode);
36278
+ return { ok: false, error: error51, kind: classifyFetchError(error51) };
36032
36279
  }
36033
36280
  const id = this.nextRequestId++;
36034
36281
  const inner = { type: "request", id, op: "fetch", init };
@@ -36169,9 +36416,8 @@ var FetchproxyServer = class {
36169
36416
  if (!this.opts.capabilities.includes("read_cookies")) {
36170
36417
  throw new Error('FetchproxyServer.readCookies(): MCP did not declare "read_cookies" in capabilities \u2014 add it to FetchproxyServerOpts.capabilities to enable this verb');
36171
36418
  }
36172
- if (!this.hostHandle && !this.peerHandle) {
36173
- throw new Error("FetchproxyServer.readCookies called before listen() \u2014 not listening");
36174
- }
36419
+ await this.ensureConnected();
36420
+ this.throwIfPendingPair();
36175
36421
  if (opts.subdomain !== void 0)
36176
36422
  assertSubdomainLabel(opts.subdomain);
36177
36423
  const baseDomain = this.resolveBaseDomain(opts.domain);
@@ -36228,9 +36474,8 @@ var FetchproxyServer = class {
36228
36474
  if (!this.opts.capabilities.includes(op)) {
36229
36475
  throw new Error(`FetchproxyServer.${op === "read_local_storage" ? "readLocalStorage" : "readSessionStorage"}(): MCP did not declare ${JSON.stringify(op)} in capabilities`);
36230
36476
  }
36231
- if (!this.hostHandle && !this.peerHandle) {
36232
- throw new Error(`FetchproxyServer.${op} called before listen() \u2014 not listening`);
36233
- }
36477
+ await this.ensureConnected();
36478
+ this.throwIfPendingPair();
36234
36479
  if (!Array.isArray(opts.keys) || opts.keys.length === 0) {
36235
36480
  throw new Error(`FetchproxyServer.${op}: opts.keys must be a non-empty array`);
36236
36481
  }
@@ -36285,9 +36530,8 @@ var FetchproxyServer = class {
36285
36530
  if (!this.opts.capabilities.includes("capture_request_header")) {
36286
36531
  throw new Error('FetchproxyServer.captureRequestHeader(): MCP did not declare "capture_request_header" in capabilities');
36287
36532
  }
36288
- if (!this.hostHandle && !this.peerHandle) {
36289
- throw new Error("FetchproxyServer.captureRequestHeader called before listen() \u2014 not listening");
36290
- }
36533
+ await this.ensureConnected();
36534
+ this.throwIfPendingPair();
36291
36535
  const declared = this.opts.captureHeaders.find((d) => d.urlPattern === opts.urlPattern && d.headerName === opts.headerName);
36292
36536
  if (!declared) {
36293
36537
  throw new Error(`FetchproxyServer.captureRequestHeader: (urlPattern=${JSON.stringify(opts.urlPattern)}, headerName=${JSON.stringify(opts.headerName)}) not declared in captureHeaders`);
@@ -36328,9 +36572,8 @@ var FetchproxyServer = class {
36328
36572
  if (!this.opts.capabilities.includes("read_indexed_db")) {
36329
36573
  throw new Error('FetchproxyServer.readIndexedDb(): MCP did not declare "read_indexed_db" in capabilities');
36330
36574
  }
36331
- if (!this.hostHandle && !this.peerHandle) {
36332
- throw new Error("FetchproxyServer.readIndexedDb called before listen() \u2014 not listening");
36333
- }
36575
+ await this.ensureConnected();
36576
+ this.throwIfPendingPair();
36334
36577
  if (!Array.isArray(opts.keys) || opts.keys.length === 0) {
36335
36578
  throw new Error("FetchproxyServer.readIndexedDb: opts.keys must be a non-empty array");
36336
36579
  }
@@ -36408,10 +36651,11 @@ var FetchproxyServer = class {
36408
36651
  if (inner.op === void 0 || inner.op === "fetch") {
36409
36652
  fetchCb({ ok: true, status: inner.status, url: inner.url, body: inner.body });
36410
36653
  } else {
36411
- fetchCb({ ok: false, error: `unexpected ${inner.op} response on fetch awaiter` });
36654
+ const error51 = `unexpected ${inner.op} response on fetch awaiter`;
36655
+ fetchCb({ ok: false, error: error51, kind: classifyFetchError(error51) });
36412
36656
  }
36413
36657
  } else {
36414
- fetchCb({ ok: false, error: inner.error });
36658
+ fetchCb({ ok: false, error: inner.error, kind: classifyFetchError(inner.error) });
36415
36659
  }
36416
36660
  return;
36417
36661
  }
@@ -36478,6 +36722,52 @@ var FetchproxyServer = class {
36478
36722
  }
36479
36723
  }
36480
36724
  }
36725
+ rejectAllPending(reason = "extension disconnected") {
36726
+ const err = new FetchproxyProtocolError(reason);
36727
+ for (const cb of this.pending.values()) {
36728
+ cb({ ok: false, error: err.message, kind: classifyFetchError(err.message) });
36729
+ }
36730
+ this.pending.clear();
36731
+ for (const cb of this.pendingReadCookies.values()) {
36732
+ cb({ ok: false, error: err.message });
36733
+ }
36734
+ this.pendingReadCookies.clear();
36735
+ for (const { reject } of this.pendingStorage.values())
36736
+ reject(err);
36737
+ this.pendingStorage.clear();
36738
+ for (const { reject } of this.pendingCapture.values())
36739
+ reject(err);
36740
+ this.pendingCapture.clear();
36741
+ for (const { reject } of this.pendingIdb.values())
36742
+ reject(err);
36743
+ this.pendingIdb.clear();
36744
+ }
36745
+ /**
36746
+ * 0.5.2+: read the current pair-pending pair code from whichever handle
36747
+ * is active, returning null when none is pending. Public verbs call this
36748
+ * at the top so that a tool invoked while the bridge is waiting on user
36749
+ * approval fails fast with the actionable error rather than hanging on a
36750
+ * sealed frame the extension will never process.
36751
+ */
36752
+ currentPendingPairCode() {
36753
+ if (this.hostHandle)
36754
+ return this.hostHandle.pendingPairCode();
36755
+ if (this.peerHandle)
36756
+ return this.peerHandle.pendingPairCode();
36757
+ return null;
36758
+ }
36759
+ /**
36760
+ * 0.5.2+: throw `FetchproxyProtocolError` with the actionable pair-code
36761
+ * message if the bridge is waiting on user approval. Used by the verb
36762
+ * methods (readCookies, readLocalStorage, etc.) that surface errors via
36763
+ * thrown exceptions rather than `ok:false` discriminated unions.
36764
+ */
36765
+ throwIfPendingPair() {
36766
+ const code = this.currentPendingPairCode();
36767
+ if (code !== null) {
36768
+ throw new FetchproxyProtocolError(this.pairingErrorMessage(code));
36769
+ }
36770
+ }
36481
36771
  /**
36482
36772
  * Shut down the bridge. Host: terminates the WebSocket server and any
36483
36773
  * still-attached extension/peer clients. Peer: closes the upstream
@@ -36485,6 +36775,10 @@ var FetchproxyServer = class {
36485
36775
  * twice in a row.
36486
36776
  */
36487
36777
  async close() {
36778
+ this.rejectAllPending();
36779
+ if (this.connectingPromise) {
36780
+ await this.connectingPromise.catch(() => void 0);
36781
+ }
36488
36782
  if (this.hostHandle)
36489
36783
  await this.hostHandle.close();
36490
36784
  if (this.peerHandle)
@@ -36492,6 +36786,7 @@ var FetchproxyServer = class {
36492
36786
  this.hostHandle = null;
36493
36787
  this.peerHandle = null;
36494
36788
  this.role = null;
36789
+ this.connectingPromise = null;
36495
36790
  }
36496
36791
  };
36497
36792
 
@@ -36580,8 +36875,7 @@ async function bootstrap(opts) {
36580
36875
  for (const p of localStoragePointers) {
36581
36876
  pointers[p.outputKey] = { storageKey: p.storageKey, jsonPointer: p.jsonPointer };
36582
36877
  }
36583
- const stub = server2;
36584
- localStorage = await stub.readLocalStorage({
36878
+ localStorage = await server2.readLocalStorage({
36585
36879
  keys: allKeys,
36586
36880
  ...storageDomainOpts,
36587
36881
  ...localStoragePointers.length > 0 ? { pointers } : {}
@@ -36594,8 +36888,7 @@ async function bootstrap(opts) {
36594
36888
  for (const p of sessionStoragePointers) {
36595
36889
  pointers[p.outputKey] = { storageKey: p.storageKey, jsonPointer: p.jsonPointer };
36596
36890
  }
36597
- const stub = server2;
36598
- sessionStorage = await stub.readSessionStorage({
36891
+ sessionStorage = await server2.readSessionStorage({
36599
36892
  keys: allKeys,
36600
36893
  ...storageDomainOpts,
36601
36894
  ...sessionStoragePointers.length > 0 ? { pointers } : {}
@@ -36618,9 +36911,6 @@ async function bootstrap(opts) {
36618
36911
  }
36619
36912
  const indexedDbBucket = {};
36620
36913
  for (const d of indexedDb) {
36621
- if (!server2.readIndexedDb) {
36622
- throw new Error("bootstrap: server factory does not implement readIndexedDb (declared indexedDb but server stub omits it)");
36623
- }
36624
36914
  const values = await server2.readIndexedDb({
36625
36915
  database: d.database,
36626
36916
  store: d.store,
@@ -36733,7 +37023,7 @@ function loadAccount(env = process.env) {
36733
37023
  // package.json
36734
37024
  var package_default = {
36735
37025
  name: "signupgenius-mcp",
36736
- version: "1.0.3",
37026
+ version: "1.0.7",
36737
37027
  mcpName: "io.github.chrischall/signupgenius-mcp",
36738
37028
  description: "SignUpGenius MCP server \u2014 read sign-ups, reports, and groups; add group members.",
36739
37029
  author: "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -36770,17 +37060,17 @@ var package_default = {
36770
37060
  "test:watch": "vitest"
36771
37061
  },
36772
37062
  dependencies: {
36773
- "@fetchproxy/bootstrap": "^0.4.2",
37063
+ "@fetchproxy/bootstrap": "^0.6.0",
36774
37064
  "@modelcontextprotocol/sdk": "^1.29.0",
36775
- dotenv: "^17.4.0",
36776
- zod: "^4.3.6"
37065
+ dotenv: "^17.4.2",
37066
+ zod: "^4.4.3"
36777
37067
  },
36778
37068
  devDependencies: {
36779
- "@types/node": "^25.5.2",
36780
- "@vitest/coverage-v8": "^4.1.2",
37069
+ "@types/node": "^25.9.1",
37070
+ "@vitest/coverage-v8": "^4.1.7",
36781
37071
  esbuild: "^0.28.0",
36782
- typescript: "^6.0.2",
36783
- vitest: "^4.1.2"
37072
+ typescript: "^6.0.3",
37073
+ vitest: "^4.1.7"
36784
37074
  }
36785
37075
  };
36786
37076
 
@@ -37575,23 +37865,39 @@ function buildRsvpPayload(parts, info, input) {
37575
37865
  const adults = isNo ? 0 : input.adults ?? 1;
37576
37866
  const children = isNo ? 0 : input.children ?? 0;
37577
37867
  return {
37578
- type: "rsvp",
37579
- source: "main",
37868
+ listid: info.id,
37869
+ owner: info.owner,
37580
37870
  urlid: parts.urlid,
37581
- signupid: parts.signupid,
37582
- slotid: info.rsvpdetails.slotid,
37583
- rsvpresponse: letter,
37584
- rsvpadult: adults,
37585
- rsvpchildren: children,
37871
+ title: info.title,
37872
+ siid: "",
37873
+ rsvpid: 0,
37874
+ imid: 0,
37875
+ usealternatename: false,
37876
+ changemembermame: false,
37877
+ displayfirstname: input.firstname,
37878
+ displaylastname: input.lastname,
37586
37879
  firstname: input.firstname,
37587
37880
  lastname: input.lastname,
37588
37881
  email: input.email,
37589
- comment: input.comment ?? "",
37882
+ optInStatus: false,
37883
+ savecontactinfo: false,
37884
+ rsvpresponse: letter,
37885
+ rsvpadult: adults,
37886
+ rsvpchildren: children,
37887
+ rsvpitems: [],
37888
+ rsvpcomments: input.comment ?? "",
37889
+ type: "rsvp",
37890
+ source: "main",
37891
+ slotid: info.rsvpdetails.slotid,
37590
37892
  isLoggedin: true,
37591
37893
  payLater: false,
37592
37894
  customFields: []
37593
37895
  };
37594
37896
  }
37897
+ function isItemBasedRsvp(info) {
37898
+ const items = info.rsvpdetails.rsvpitems;
37899
+ return Array.isArray(items) && items.length > 0;
37900
+ }
37595
37901
  function registerRsvpTool(server2, client2) {
37596
37902
  if (client2.mode !== "session") return;
37597
37903
  server2.registerTool(
@@ -37615,6 +37921,12 @@ function registerRsvpTool(server2, client2) {
37615
37921
  `Sign-up ${parts.urlid} is not an RSVP-style sheet (useRSVP=${info.useRSVP}). This tool only handles Yes/No/Maybe responses. Slot-based sign-ups need a separate tool.`
37616
37922
  );
37617
37923
  }
37924
+ if (isItemBasedRsvp(info)) {
37925
+ const itemCount = info.rsvpdetails.rsvpitems.length;
37926
+ throw new Error(
37927
+ `Sign-up ${parts.urlid} is an item-based RSVP \u2014 guests must pick from ${itemCount} item slot(s) (e.g. "Yes, I'll bring lasagna"). This tool only handles the headcount-only RSVP variant. Use the SignUpGenius web UI for item-based responses until a dedicated tool ships.`
37928
+ );
37929
+ }
37618
37930
  const payload = buildRsvpPayload(parts, info, args);
37619
37931
  const result = await client2.request("", {
37620
37932
  legacyAction: "s.processSignUpFormHandler",
@@ -37632,7 +37944,7 @@ function registerRsvpTool(server2, client2) {
37632
37944
  response: args.response,
37633
37945
  adults: payload.rsvpadult,
37634
37946
  children: payload.rsvpchildren,
37635
- comment: payload.comment,
37947
+ comment: payload.rsvpcomments,
37636
37948
  server: result.data
37637
37949
  });
37638
37950
  }
@@ -37662,7 +37974,7 @@ var client = new SignUpGeniusClient(account, {
37662
37974
  configError: configError ?? void 0,
37663
37975
  preloaded
37664
37976
  });
37665
- var server = new McpServer({ name: "signupgenius", version: "1.0.3" });
37977
+ var server = new McpServer({ name: "signupgenius", version: "1.0.7" });
37666
37978
  registerUserTools(server, client);
37667
37979
  registerGroupTools(server, client);
37668
37980
  registerSignUpTools(server, client);
package/dist/index.js CHANGED
@@ -43,7 +43,7 @@ const client = new SignUpGeniusClient(account, {
43
43
  configError: configError ?? undefined,
44
44
  preloaded,
45
45
  });
46
- const server = new McpServer({ name: 'signupgenius', version: '1.0.3' });
46
+ const server = new McpServer({ name: 'signupgenius', version: '1.0.7' }); // x-release-please-version
47
47
  registerUserTools(server, client);
48
48
  registerGroupTools(server, client);
49
49
  registerSignUpTools(server, client);
@@ -43,23 +43,46 @@ export function buildRsvpPayload(parts, info, input) {
43
43
  const adults = isNo ? 0 : input.adults ?? 1;
44
44
  const children = isNo ? 0 : input.children ?? 0;
45
45
  return {
46
- type: 'rsvp',
47
- source: 'main',
46
+ listid: info.id,
47
+ owner: info.owner,
48
48
  urlid: parts.urlid,
49
- signupid: parts.signupid,
50
- slotid: info.rsvpdetails.slotid,
51
- rsvpresponse: letter,
52
- rsvpadult: adults,
53
- rsvpchildren: children,
49
+ title: info.title,
50
+ siid: '',
51
+ rsvpid: 0,
52
+ imid: 0,
53
+ usealternatename: false,
54
+ changemembermame: false,
55
+ displayfirstname: input.firstname,
56
+ displaylastname: input.lastname,
54
57
  firstname: input.firstname,
55
58
  lastname: input.lastname,
56
59
  email: input.email,
57
- comment: input.comment ?? '',
60
+ optInStatus: false,
61
+ savecontactinfo: false,
62
+ rsvpresponse: letter,
63
+ rsvpadult: adults,
64
+ rsvpchildren: children,
65
+ rsvpitems: [],
66
+ rsvpcomments: input.comment ?? '',
67
+ type: 'rsvp',
68
+ source: 'main',
69
+ slotid: info.rsvpdetails.slotid,
58
70
  isLoggedin: true,
59
71
  payLater: false,
60
72
  customFields: [],
61
73
  };
62
74
  }
75
+ /**
76
+ * True when `getSignupInfo` reports per-item slots on an RSVP-style sheet
77
+ * (e.g. "Yes, I'll bring lasagna"). Headcount-only RSVPs leave `rsvpitems`
78
+ * empty or unset. We only handle the headcount variant — the item variant
79
+ * needs a separate input surface (per-slot quantities + comments) that this
80
+ * tool doesn't expose.
81
+ */
82
+ export function isItemBasedRsvp(info) {
83
+ const items = info.rsvpdetails.rsvpitems;
84
+ return Array.isArray(items) && items.length > 0;
85
+ }
63
86
  export function registerRsvpTool(server, client) {
64
87
  // Key mode doesn't have the cookie/JWT surface this flow needs, and the
65
88
  // documented Pro API has no equivalent. Skip registration entirely so the
@@ -88,6 +111,15 @@ export function registerRsvpTool(server, client) {
88
111
  throw new Error(`Sign-up ${parts.urlid} is not an RSVP-style sheet (useRSVP=${info.useRSVP}). ` +
89
112
  'This tool only handles Yes/No/Maybe responses. Slot-based sign-ups need a separate tool.');
90
113
  }
114
+ if (isItemBasedRsvp(info)) {
115
+ // Length is safe to read because isItemBasedRsvp confirmed the array.
116
+ const itemCount = info.rsvpdetails.rsvpitems.length;
117
+ throw new Error(`Sign-up ${parts.urlid} is an item-based RSVP — guests must pick from ` +
118
+ `${itemCount} item slot(s) ` +
119
+ "(e.g. \"Yes, I'll bring lasagna\"). This tool only handles the " +
120
+ 'headcount-only RSVP variant. Use the SignUpGenius web UI for ' +
121
+ 'item-based responses until a dedicated tool ships.');
122
+ }
91
123
  const payload = buildRsvpPayload(parts, info, args);
92
124
  const result = await client.request('', {
93
125
  legacyAction: 's.processSignUpFormHandler',
@@ -105,7 +137,7 @@ export function registerRsvpTool(server, client) {
105
137
  response: args.response,
106
138
  adults: payload.rsvpadult,
107
139
  children: payload.rsvpchildren,
108
- comment: payload.comment,
140
+ comment: payload.rsvpcomments,
109
141
  server: result.data,
110
142
  });
111
143
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signupgenius-mcp",
3
- "version": "1.0.3",
3
+ "version": "1.0.7",
4
4
  "mcpName": "io.github.chrischall/signupgenius-mcp",
5
5
  "description": "SignUpGenius MCP server — read sign-ups, reports, and groups; add group members.",
6
6
  "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -37,16 +37,16 @@
37
37
  "test:watch": "vitest"
38
38
  },
39
39
  "dependencies": {
40
- "@fetchproxy/bootstrap": "^0.4.2",
40
+ "@fetchproxy/bootstrap": "^0.6.0",
41
41
  "@modelcontextprotocol/sdk": "^1.29.0",
42
- "dotenv": "^17.4.0",
43
- "zod": "^4.3.6"
42
+ "dotenv": "^17.4.2",
43
+ "zod": "^4.4.3"
44
44
  },
45
45
  "devDependencies": {
46
- "@types/node": "^25.5.2",
47
- "@vitest/coverage-v8": "^4.1.2",
46
+ "@types/node": "^25.9.1",
47
+ "@vitest/coverage-v8": "^4.1.7",
48
48
  "esbuild": "^0.28.0",
49
- "typescript": "^6.0.2",
50
- "vitest": "^4.1.2"
49
+ "typescript": "^6.0.3",
50
+ "vitest": "^4.1.7"
51
51
  }
52
52
  }
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/chrischall/signupgenius-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "1.0.3",
9
+ "version": "1.0.7",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "signupgenius-mcp",
14
- "version": "1.0.3",
14
+ "version": "1.0.7",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },