signupgenius-mcp 1.1.6 → 1.2.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": "MCP server for SignUpGenius — read sign-ups, slot reports, and groups; add group members.",
10
- "version": "1.1.6"
10
+ "version": "1.2.0"
11
11
  },
12
12
  "plugins": [
13
13
  {
@@ -15,7 +15,7 @@
15
15
  "displayName": "SignUpGenius",
16
16
  "source": "./",
17
17
  "description": "SignUpGenius MCP server for Claude — sign-ups, slot reports, and groups via natural language. Free or Pro accounts.",
18
- "version": "1.1.6",
18
+ "version": "1.2.0",
19
19
  "author": {
20
20
  "name": "Chris Hall"
21
21
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "signupgenius-mcp",
3
3
  "displayName": "SignUpGenius",
4
- "version": "1.1.6",
4
+ "version": "1.2.0",
5
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
6
  "author": {
7
7
  "name": "Chris Hall",
@@ -15,5 +15,6 @@
15
15
  "signups",
16
16
  "volunteers",
17
17
  "mcp"
18
- ]
18
+ ],
19
+ "skills": "./skills/"
19
20
  }
package/dist/bundle.js CHANGED
@@ -34948,6 +34948,7 @@ var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
34948
34948
  "capture_request_header",
34949
34949
  "capture_redirect",
34950
34950
  "read_indexed_db",
34951
+ "read_dom",
34951
34952
  "download"
34952
34953
  ]);
34953
34954
 
@@ -35249,6 +35250,44 @@ function assertIndexedDbScopesArray(value, label) {
35249
35250
  }
35250
35251
  }
35251
35252
  }
35253
+ var DOM_SELECTOR_RE = /^[^-]{1,512}$/;
35254
+ var DOM_ATTRIBUTE_RE = /^[A-Za-z_:][A-Za-z0-9_:.\-]{0,127}$/;
35255
+ function assertDomSelectorsArray(value, label) {
35256
+ if (!Array.isArray(value)) {
35257
+ throw new ProtocolError(`${label}: expected array, got ${typeof value}`);
35258
+ }
35259
+ const seen = /* @__PURE__ */ new Set();
35260
+ for (let i = 0; i < value.length; i++) {
35261
+ const entry = value[i];
35262
+ assertObject(entry, `${label}[${i}]`);
35263
+ if (entry.name === void 0) {
35264
+ throw new ProtocolError(`${label}[${i}].name: missing`);
35265
+ }
35266
+ if (entry.selector === void 0) {
35267
+ throw new ProtocolError(`${label}[${i}].selector: missing`);
35268
+ }
35269
+ if (typeof entry.name !== "string" || !SCOPE_KEY_RE.test(entry.name)) {
35270
+ throw new ProtocolError(`${label}[${i}].name: invalid ${JSON.stringify(entry.name)}`);
35271
+ }
35272
+ if (typeof entry.selector !== "string" || !DOM_SELECTOR_RE.test(entry.selector)) {
35273
+ throw new ProtocolError(`${label}[${i}].selector: invalid ${JSON.stringify(entry.selector)}`);
35274
+ }
35275
+ if (entry.attribute !== void 0) {
35276
+ if (typeof entry.attribute !== "string" || !DOM_ATTRIBUTE_RE.test(entry.attribute)) {
35277
+ throw new ProtocolError(`${label}[${i}].attribute: invalid ${JSON.stringify(entry.attribute)}`);
35278
+ }
35279
+ }
35280
+ if (seen.has(entry.name)) {
35281
+ throw new ProtocolError(`${label}: duplicate name ${JSON.stringify(entry.name)}`);
35282
+ }
35283
+ seen.add(entry.name);
35284
+ for (const k of Object.keys(entry)) {
35285
+ if (k !== "name" && k !== "selector" && k !== "attribute") {
35286
+ throw new ProtocolError(`${label}[${i}]: unexpected field ${JSON.stringify(k)}`);
35287
+ }
35288
+ }
35289
+ }
35290
+ }
35252
35291
  function validateFrame(raw) {
35253
35292
  assertObject(raw, "frame");
35254
35293
  const t = raw.type;
@@ -35324,6 +35363,9 @@ function validateHello(raw) {
35324
35363
  if (raw.sessionStoragePointers !== void 0) {
35325
35364
  assertStoragePointersArray(raw.sessionStoragePointers, "hello.sessionStoragePointers", raw.sessionStorageKeys);
35326
35365
  }
35366
+ if (raw.domSelectors !== void 0) {
35367
+ assertDomSelectorsArray(raw.domSelectors, "hello.domSelectors");
35368
+ }
35327
35369
  assertBase64(raw.identityX25519Pub, "hello.identityX25519Pub");
35328
35370
  assertBase64(raw.identityEd25519Pub, "hello.identityEd25519Pub");
35329
35371
  assertBase64(raw.sessionNonce, "hello.sessionNonce");
@@ -35558,6 +35600,21 @@ function validateInnerRequest(raw) {
35558
35600
  }
35559
35601
  return raw;
35560
35602
  }
35603
+ if (raw.op === "read_dom") {
35604
+ assertObject(raw.init, "inner.init");
35605
+ if (raw.init.origin === void 0)
35606
+ throw new ProtocolError("inner.init.origin: missing");
35607
+ if (raw.init.names === void 0)
35608
+ throw new ProtocolError("inner.init.names: missing");
35609
+ assertHttpsOriginOnly(raw.init.origin, "inner.init.origin");
35610
+ assertNonEmptyKeyArray(raw.init.names, "inner.init.names");
35611
+ for (const k of Object.keys(raw.init)) {
35612
+ if (k !== "origin" && k !== "names") {
35613
+ throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on read_dom`);
35614
+ }
35615
+ }
35616
+ return raw;
35617
+ }
35561
35618
  if (raw.op === "download") {
35562
35619
  assertObject(raw.init, "inner.init");
35563
35620
  if (raw.init.url === void 0) {
@@ -35583,7 +35640,7 @@ function validateInnerRequest(raw) {
35583
35640
  }
35584
35641
  return raw;
35585
35642
  }
35586
- 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)}`);
35643
+ 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", "read_dom", "download"; got ${JSON.stringify(raw.op)}`);
35587
35644
  }
35588
35645
  function assertNonEmptyKeyArray(value, label) {
35589
35646
  if (!Array.isArray(value)) {
@@ -35668,6 +35725,13 @@ function validateInnerResponse(raw) {
35668
35725
  assertObject(raw.values, "inner.values");
35669
35726
  return raw;
35670
35727
  }
35728
+ if (op === "read_dom") {
35729
+ if (raw.values === void 0) {
35730
+ throw new ProtocolError("inner.values: missing on read_dom response");
35731
+ }
35732
+ assertStringMap(raw.values, "inner.values");
35733
+ return raw;
35734
+ }
35671
35735
  if (op === "download") {
35672
35736
  assertObject(raw.value, "inner.value");
35673
35737
  assertString(raw.value.path, "inner.value.path");
@@ -36007,6 +36071,13 @@ async function buildServerHello(opts) {
36007
36071
  jsonPointer: d.jsonPointer
36008
36072
  }));
36009
36073
  }
36074
+ if (opts.domSelectors && opts.domSelectors.length > 0) {
36075
+ hello.domSelectors = opts.domSelectors.map((d) => ({
36076
+ name: d.name,
36077
+ selector: d.selector,
36078
+ ...d.attribute !== void 0 ? { attribute: d.attribute } : {}
36079
+ }));
36080
+ }
36010
36081
  return hello;
36011
36082
  }
36012
36083
 
@@ -36096,7 +36167,8 @@ async function startHost(opts) {
36096
36167
  captureHeaders: opts.ownCaptureHeaders,
36097
36168
  indexedDbScopes: opts.ownIndexedDbScopes,
36098
36169
  localStoragePointers: opts.ownLocalStoragePointers,
36099
- sessionStoragePointers: opts.ownSessionStoragePointers
36170
+ sessionStoragePointers: opts.ownSessionStoragePointers,
36171
+ domSelectors: opts.ownDomSelectors
36100
36172
  });
36101
36173
  const ownSessionNonce = fromB64(ownHello.sessionNonce);
36102
36174
  let extensionWs = null;
@@ -36331,6 +36403,7 @@ async function startPeer(opts) {
36331
36403
  sessionStorageKeys: opts.sessionStorageKeys,
36332
36404
  captureHeaders: opts.captureHeaders,
36333
36405
  indexedDbScopes: opts.indexedDbScopes,
36406
+ domSelectors: opts.domSelectors,
36334
36407
  localStoragePointers: opts.localStoragePointers,
36335
36408
  sessionStoragePointers: opts.sessionStoragePointers
36336
36409
  });
@@ -36738,6 +36811,11 @@ var FetchproxyServer = class {
36738
36811
  key: d.key,
36739
36812
  jsonPointer: d.jsonPointer
36740
36813
  })),
36814
+ domSelectors: (opts.domSelectors ?? []).map((d) => ({
36815
+ name: d.name,
36816
+ selector: d.selector,
36817
+ ...d.attribute !== void 0 ? { attribute: d.attribute } : {}
36818
+ })),
36741
36819
  // 0.8.0+: timer + lazy-revive default to ON. Every realty MCP
36742
36820
  // adapter was about to set these to the same numbers anyway; the
36743
36821
  // back-door is `0` (explicit opt-out) if a caller genuinely wants
@@ -36858,6 +36936,7 @@ var FetchproxyServer = class {
36858
36936
  ownIndexedDbScopes: this.opts.indexedDbScopes,
36859
36937
  ownLocalStoragePointers: this.opts.localStoragePointers,
36860
36938
  ownSessionStoragePointers: this.opts.sessionStoragePointers,
36939
+ ownDomSelectors: this.opts.domSelectors,
36861
36940
  onPairCode: this.opts.onPairCode
36862
36941
  });
36863
36942
  this.hostHandle.onOwnInner((inner) => this.onInner(inner));
@@ -36885,7 +36964,8 @@ var FetchproxyServer = class {
36885
36964
  captureHeaders: this.opts.captureHeaders,
36886
36965
  indexedDbScopes: this.opts.indexedDbScopes,
36887
36966
  localStoragePointers: this.opts.localStoragePointers,
36888
- sessionStoragePointers: this.opts.sessionStoragePointers
36967
+ sessionStoragePointers: this.opts.sessionStoragePointers,
36968
+ domSelectors: this.opts.domSelectors
36889
36969
  });
36890
36970
  this.peerHandle.onInner((inner) => this.onInner(inner));
36891
36971
  this.peerHandle.onRenegotiate(() => {
@@ -37860,6 +37940,46 @@ var FetchproxyServer = class {
37860
37940
  await this.sendInnerFrame(inner);
37861
37941
  return this._withVerbTimeout(pending, this.pendingIdb, id, origin);
37862
37942
  }
37943
+ /**
37944
+ * 1.4.0+: read declared DOM values from the user's signed-in tab.
37945
+ * Requires `'read_dom'` in capabilities AND every requested `name` to
37946
+ * match a declared `domSelectors` entry. The extension reads each
37947
+ * declared selector from the matched tab's DOM (isolated-world
37948
+ * `querySelector`, value or attribute) — no page-JS execution.
37949
+ *
37950
+ * Returns a `Record<string, string>` of `name → value`, with names
37951
+ * whose element (or attribute) was absent omitted. Throws
37952
+ * `FetchproxyProtocolError` on bridge failures and a plain `Error` on
37953
+ * developer mistakes (undeclared capability, undeclared name).
37954
+ */
37955
+ async readDom(opts) {
37956
+ if (!this.opts.capabilities.includes("read_dom")) {
37957
+ throw new Error('FetchproxyServer.readDom(): MCP did not declare "read_dom" in capabilities');
37958
+ }
37959
+ await this.ensureConnected();
37960
+ this.throwIfPendingPair();
37961
+ if (!Array.isArray(opts.names) || opts.names.length === 0) {
37962
+ throw new Error("FetchproxyServer.readDom: opts.names must be a non-empty array");
37963
+ }
37964
+ this.assertScopeSubset(opts.names, this.opts.domSelectors.map((d) => d.name), "domSelectors");
37965
+ if (opts.subdomain !== void 0)
37966
+ assertSubdomainLabel(opts.subdomain);
37967
+ const baseDomain = this.resolveBaseDomain(opts.domain);
37968
+ const host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
37969
+ const origin = `https://${host}`;
37970
+ const id = this.nextRequestId++;
37971
+ const inner = {
37972
+ type: "request",
37973
+ id,
37974
+ op: "read_dom",
37975
+ init: { origin, names: [...opts.names] }
37976
+ };
37977
+ const pending = new Promise((resolve, reject) => {
37978
+ this.pendingStorage.set(id, { resolve, reject });
37979
+ });
37980
+ await this.sendInnerFrame(inner);
37981
+ return this._withVerbTimeout(pending, this.pendingStorage, id, origin);
37982
+ }
37863
37983
  assertScopeSubset(requested, declared, label) {
37864
37984
  const undeclared = undeclaredKeys(requested, declared);
37865
37985
  if (undeclared.length > 0) {
@@ -37931,7 +38051,7 @@ var FetchproxyServer = class {
37931
38051
  if (storageCb) {
37932
38052
  this.pendingStorage.delete(inner.id);
37933
38053
  if (inner.ok) {
37934
- if ((inner.op === "read_local_storage" || inner.op === "read_session_storage") && inner.values) {
38054
+ if ((inner.op === "read_local_storage" || inner.op === "read_session_storage" || inner.op === "read_dom") && inner.values) {
37935
38055
  storageCb.resolve({ ...inner.values });
37936
38056
  } else {
37937
38057
  storageCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on storage awaiter`));
@@ -38326,7 +38446,7 @@ function loadAccount(env = process.env) {
38326
38446
  // package.json
38327
38447
  var package_default = {
38328
38448
  name: "signupgenius-mcp",
38329
- version: "1.1.6",
38449
+ version: "1.2.0",
38330
38450
  mcpName: "io.github.chrischall/signupgenius-mcp",
38331
38451
  description: "SignUpGenius MCP server \u2014 read sign-ups, reports, and groups; add group members.",
38332
38452
  author: "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -38363,7 +38483,7 @@ var package_default = {
38363
38483
  "test:watch": "vitest"
38364
38484
  },
38365
38485
  dependencies: {
38366
- "@chrischall/mcp-utils": "^0.12.0",
38486
+ "@chrischall/mcp-utils": "^0.13.0",
38367
38487
  "@fetchproxy/bootstrap": "^1.0.0",
38368
38488
  "@fetchproxy/server": "^1.0.0",
38369
38489
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -38374,7 +38494,7 @@ var package_default = {
38374
38494
  "@types/node": "^26.0.0",
38375
38495
  "@vitest/coverage-v8": "^4.1.7",
38376
38496
  esbuild: "^0.28.0",
38377
- typescript: "^6.0.3",
38497
+ typescript: "^7.0.2",
38378
38498
  vitest: "^4.1.7"
38379
38499
  }
38380
38500
  };
@@ -39387,7 +39507,7 @@ bannerLines.push(
39387
39507
  );
39388
39508
  await runMcp({
39389
39509
  name: "signupgenius",
39390
- version: "1.1.6",
39510
+ version: "1.2.0",
39391
39511
  // x-release-please-version
39392
39512
  banner: bannerLines.join("\n"),
39393
39513
  deps: client,
package/dist/index.js CHANGED
@@ -47,7 +47,7 @@ const bannerLines = account
47
47
  bannerLines.push('[signupgenius-mcp] Developed and maintained by AI (Claude). Use at your own discretion.');
48
48
  await runMcp({
49
49
  name: 'signupgenius',
50
- version: '1.1.6', // x-release-please-version
50
+ version: '1.2.0', // x-release-please-version
51
51
  banner: bannerLines.join('\n'),
52
52
  deps: client,
53
53
  tools: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signupgenius-mcp",
3
- "version": "1.1.6",
3
+ "version": "1.2.0",
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,7 +37,7 @@
37
37
  "test:watch": "vitest"
38
38
  },
39
39
  "dependencies": {
40
- "@chrischall/mcp-utils": "^0.12.0",
40
+ "@chrischall/mcp-utils": "^0.13.0",
41
41
  "@fetchproxy/bootstrap": "^1.0.0",
42
42
  "@fetchproxy/server": "^1.0.0",
43
43
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -48,7 +48,7 @@
48
48
  "@types/node": "^26.0.0",
49
49
  "@vitest/coverage-v8": "^4.1.7",
50
50
  "esbuild": "^0.28.0",
51
- "typescript": "^6.0.3",
51
+ "typescript": "^7.0.2",
52
52
  "vitest": "^4.1.7"
53
53
  }
54
54
  }
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.1.6",
9
+ "version": "1.2.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "signupgenius-mcp",
14
- "version": "1.1.6",
14
+ "version": "1.2.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
@@ -0,0 +1,116 @@
1
+ ---
2
+ name: signupgenius-mcp
3
+ description: Read sign-up sheets, slot reports, and groups on SignUpGenius — and add members to your groups. Triggers on phrases like "check SignUpGenius", "what am I signed up for", "what slots are left for [event]", "available slots", "list my SignUpGenius groups", "add [person] to my [group] group", or any request involving SignUpGenius sign-ups, RSVPs, volunteer slots, potlucks, carpools, classroom helpers, or PTA/HOA/Scout/team sign-ups. Works against your own signed-in account; supports Pro key for full slot reports.
4
+ ---
5
+
6
+ # signupgenius-mcp
7
+
8
+ MCP server for [SignUpGenius](https://www.signupgenius.com) — 14 read tools + 2 write across profile, groups, sign-ups, and reports.
9
+
10
+ - **npm:** [npmjs.com/package/signupgenius-mcp](https://www.npmjs.com/package/signupgenius-mcp)
11
+ - **Source:** [github.com/chrischall/signupgenius-mcp](https://github.com/chrischall/signupgenius-mcp)
12
+
13
+ ## Setup
14
+
15
+ Three auth modes, tried in priority order — first match wins. **You only need one.**
16
+
17
+ ### Mode 1 — fetchproxy fallback (zero env vars, recommended)
18
+
19
+ Install the [fetchproxy extension](https://github.com/chrischall/fetchproxy) once, sign into [signupgenius.com](https://www.signupgenius.com), and add to `.mcp.json` (project) or `~/.claude/mcp.json` (global):
20
+
21
+ ```json
22
+ {
23
+ "mcpServers": {
24
+ "signupgenius": {
25
+ "command": "npx",
26
+ "args": ["-y", "signupgenius-mcp"]
27
+ }
28
+ }
29
+ }
30
+ ```
31
+
32
+ At startup the MCP reads your `accessToken` / `cfid` / `cftoken` cookies once via the extension, then talks to SignUpGenius directly — the extension is **not** in the request hot path after that. Works with free accounts.
33
+
34
+ ### Mode 2 — session login (email + password)
35
+
36
+ Add an env block with your direct-login credentials (won't work with Google/Apple/Facebook/Microsoft SSO or 2FA):
37
+
38
+ ```json
39
+ {
40
+ "mcpServers": {
41
+ "signupgenius": {
42
+ "command": "npx",
43
+ "args": ["-y", "signupgenius-mcp"],
44
+ "env": {
45
+ "SIGNUPGENIUS_EMAIL": "you@example.com",
46
+ "SIGNUPGENIUS_PASSWORD": "your-password"
47
+ }
48
+ }
49
+ }
50
+ }
51
+ ```
52
+
53
+ ### Mode 3 — Pro API key (required for slot reports)
54
+
55
+ The three `signupgenius_report_*` tools that list filled / available / all participants for a given sign-up only work against the documented Pro v2 API. Get a key from **Pro Tools → API Management** in your SignUpGenius dashboard (Pro subscription required), then:
56
+
57
+ ```json
58
+ "env": { "SIGNUPGENIUS_USER_KEY": "your-api-key" }
59
+ ```
60
+
61
+ Modes can be combined; Pro key wins where it applies, session/fetchproxy handles everything else.
62
+
63
+ ## Tools
64
+
65
+ ### Profile
66
+
67
+ - **`signupgenius_get_profile`** — Your own profile (name, email, account type).
68
+
69
+ ### Groups
70
+
71
+ - **`signupgenius_list_groups`** — Every group you own or belong to.
72
+ - **`signupgenius_list_group_members`** — Members of one of your groups.
73
+ - **`signupgenius_get_group_member`** — One member's full record.
74
+ - **`signupgenius_add_group_member`** *(write)* — Add a person to one of your groups.
75
+
76
+ ### Sign-ups — created by you
77
+
78
+ - **`signupgenius_list_created_active`** — Sign-ups you've created that are still open.
79
+ - **`signupgenius_list_created_expired`** — Sign-ups you've created that have ended.
80
+ - **`signupgenius_list_created_all`** — Both active and expired in one call.
81
+
82
+ ### Sign-ups — others'
83
+
84
+ - **`signupgenius_list_invited`** — Sign-ups you've been invited to.
85
+ - **`signupgenius_list_signedupfor`** — Sign-ups you've taken a slot on. (Session-mode also includes the bonus `signupgenius_legacy_get_my_signups` which calls the same backend the SignUpGenius web wizard uses and sometimes returns fuller data.)
86
+ - **`signupgenius_legacy_get_my_signups`** *(session only)* — Bonus richer "what am I signed up for" lookup.
87
+
88
+ ### Public sign-up
89
+
90
+ - **`signupgenius_get_public_signup`** — Fetch a public sign-up page by URL or slug. No auth required.
91
+ - **`signupgenius_rsvp`** *(write)* — RSVP to a public sign-up slot.
92
+
93
+ ### Reports — slots for a sign-up (Pro key only)
94
+
95
+ - **`signupgenius_report_all`** — Every slot + participant on a sign-up.
96
+ - **`signupgenius_report_filled`** — Filled slots only.
97
+ - **`signupgenius_report_available`** — Available slots only.
98
+
99
+ Session-mode users hit a fast `ModeMismatchError` on the report tools with a clear instruction to set `SIGNUPGENIUS_USER_KEY`.
100
+
101
+ ## Trigger examples
102
+
103
+ - "Check SignUpGenius — what am I signed up for this week?" → `signupgenius_list_signedupfor` (+ `_legacy_get_my_signups` in session mode)
104
+ - "What slots are still open on the PTA potluck sign-up?" → `signupgenius_report_available` (Pro key)
105
+ - "List my SignUpGenius groups" → `signupgenius_list_groups`
106
+ - "Add Jordan Smith (<jordan@example.com>) to my Scouts group" → `signupgenius_add_group_member`
107
+ - "What sign-ups have I created that are still active?" → `signupgenius_list_created_active`
108
+ - "RSVP me to slot 3 on this SignUpGenius link" → `signupgenius_rsvp`
109
+
110
+ ## Gotchas
111
+
112
+ - **Reports require Pro.** `signupgenius_report_*` only work with `SIGNUPGENIUS_USER_KEY` — session/fetchproxy users get a clear error pointing at the key.
113
+ - **SSO accounts not supported.** Session mode is direct email/password only — no Google/Apple/Facebook/Microsoft SSO, no 2FA. Use fetchproxy mode instead if your account uses SSO.
114
+ - **Session listings collapse.** In session mode the v3 `signups/created` endpoint returns active + expired in one paginated call — the three `list_created_*` tools all hit the same endpoint and filter client-side. Pro key mode has separate endpoints and exposes the real distinction.
115
+ - **Write surface is small.** Only `signupgenius_add_group_member` and `signupgenius_rsvp` mutate; everything else is read-only.
116
+ - **ToS caveat.** SignUpGenius's terms generally prohibit scripted/automated access. Personal-account, personal-scale use is the intended audience; running this against accounts you don't own or at scale is your problem.
@@ -0,0 +1,124 @@
1
+ ---
2
+ name: signupgenius-api
3
+ description: "Access SignUpGenius (sign-ups, groups, RSVPs) from a shell with curl instead of running the signupgenius-mcp server — server-side email/password login to a JWT + cfid/cftoken cookies, then curl the v3 API and legacy /SUGboxAPI.cfm dispatcher directly. Use when you want SignUpGenius data without the MCP, in a script, or on a machine where the MCP isn't installed."
4
+ ---
5
+
6
+ # SignUpGenius via curl (no MCP)
7
+
8
+ SignUpGenius's session mode is a classic ColdFusion login: POST email+password
9
+ to the login form, get back a `accessToken` JWT cookie plus `cfid`/`cftoken`
10
+ session cookies. No browser or extension needed — everything here is a plain
11
+ `curl` call. This is the same auth path `signupgenius-mcp` uses in session
12
+ mode (`src/auth-session-login.ts`); its fetchproxy path (lifting the same
13
+ cookies out of a signed-in browser tab) is only a **fallback** for when you
14
+ don't want to put a password in `.env` — this skill always logs in directly.
15
+
16
+ ## One-time setup
17
+
18
+ ```sh
19
+ export SIGNUPGENIUS_EMAIL="you@example.com"
20
+ export SIGNUPGENIUS_PASSWORD="..."
21
+ # or: export SIGNUPGENIUS_PASSWORD="$(op read 'op://Private/SignUpGenius/password')"
22
+ ```
23
+
24
+ SSO accounts (Google/Apple/Facebook/Microsoft) and 2FA-enabled accounts can't
25
+ use this flow — same limitation as the MCP's session mode.
26
+
27
+ ## Log in: get the JWT + cookies
28
+
29
+ ```sh
30
+ COOKIEJAR=$(mktemp)
31
+
32
+ CSRF=$(curl -s -c "$COOKIEJAR" https://www.signupgenius.com/login \
33
+ | grep -oE 'name="csrfToken"[[:space:]]+value="[^"]+"' \
34
+ | sed -E 's/.*value="([^"]+)".*/\1/')
35
+
36
+ curl -s -D /tmp/sug-login-headers.txt -o /dev/null \
37
+ -b "$COOKIEJAR" -c "$COOKIEJAR" \
38
+ -A 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0 Safari/537.36' \
39
+ -X POST 'https://www.signupgenius.com/index.cfm?go=c.Login' \
40
+ --data-urlencode "csrfToken=$CSRF" \
41
+ --data-urlencode "loginemail=$SIGNUPGENIUS_EMAIL" \
42
+ --data-urlencode "pword=$SIGNUPGENIUS_PASSWORD" \
43
+ --data-urlencode "successpage=c.jump&jump=/index.cfm?go=c.MyAccount" \
44
+ --data-urlencode "failpage=c.Register" \
45
+ --data-urlencode "ScreenWidth=2000" \
46
+ --data-urlencode "ScreenHeight=1200" \
47
+ --data-urlencode "formaction=1" \
48
+ --data-urlencode "formName=loginform" \
49
+ --data-urlencode "refererUrl="
50
+
51
+ # Bad credentials 302 to c.Register instead of setting accessToken — check both:
52
+ grep -q 'c.Register' /tmp/sug-login-headers.txt && echo "LOGIN FAILED (bad credentials)" >&2
53
+
54
+ ACCESS_TOKEN=$(awk -F'\t' '$6=="accessToken"{print $7}' "$COOKIEJAR")
55
+ CFID=$(awk -F'\t' '$6=="cfid"{print $7}' "$COOKIEJAR")
56
+ CFTOKEN=$(awk -F'\t' '$6=="cftoken"{print $7}' "$COOKIEJAR")
57
+ [ -z "$ACCESS_TOKEN" ] && echo "LOGIN FAILED (no accessToken cookie set)" >&2
58
+ COOKIE_HEADER="accessToken=${ACCESS_TOKEN}; cfid=${CFID}; cftoken=${CFTOKEN}"
59
+ ```
60
+
61
+ This mirrors `sessionLoginFlow` exactly: GET `/login` for the CSRF token +
62
+ `cfid`/`cftoken`, POST the credentials with the exact same static form fields
63
+ the wizard sends, and read the `accessToken` cookie back out of the jar as
64
+ the success marker.
65
+
66
+ ## Core call pattern
67
+
68
+ Every authenticated call carries **both** headers — a Bearer JWT for the v3
69
+ API and the raw cookie header for the legacy dispatcher (the client sends
70
+ both on every request regardless of which surface it's hitting):
71
+
72
+ ```sh
73
+ curl -s 'https://api.signupgenius.com/v3/member/profile/' \
74
+ -H "Authorization: Bearer $ACCESS_TOKEN" \
75
+ -H "Cookie: $COOKIE_HEADER" | jq .
76
+ ```
77
+
78
+ ```sh
79
+ curl -s -X POST 'https://www.signupgenius.com/SUGboxAPI.cfm?go=t.getMySignups' \
80
+ -H "Authorization: Bearer $ACCESS_TOKEN" \
81
+ -H "Cookie: $COOKIE_HEADER" \
82
+ -H 'Content-Type: application/json' \
83
+ -d '{}' | jq .
84
+ ```
85
+
86
+ Ready-to-run request bodies for every tool endpoint (groups, sign-up
87
+ listings, public sign-up lookup, and the 3-step RSVP flow) are in
88
+ `references/sug-endpoints.md`.
89
+
90
+ ## The two envelope shapes
91
+
92
+ - v3 API (`api.signupgenius.com/v3/...`) returns **lower-case**
93
+ `{data, message, success}`.
94
+ - The legacy dispatcher (`/SUGboxAPI.cfm?go=...`) returns **upper-case**
95
+ `{DATA, MESSAGE, SUCCESS}` — `MESSAGE` may be a bare string instead of an
96
+ array.
97
+
98
+ `jq` recipes in the reference file account for the case difference per
99
+ endpoint.
100
+
101
+ ## Session expiry
102
+
103
+ Two signals mean the session lapsed — re-run the login step and retry:
104
+
105
+ - an HTTP `401` from either surface, or
106
+ - an HTTP `200` whose body is HTML containing `loginform`/`loginemail`/
107
+ `go=c.Login` (the server quietly bounced you to the login page instead of
108
+ returning JSON).
109
+
110
+ A `403` is a Pro-permission failure, not expiry — don't retry the login for
111
+ that one.
112
+
113
+ ## Out of scope for this skill
114
+
115
+ - **Pro API key mode** (`SIGNUPGENIUS_USER_KEY`, `Authorization: <key>` against
116
+ `api.signupgenius.com/v2/k`) is a different auth entirely — it's the only
117
+ mode that can call the slot-report endpoints
118
+ (`/signups/report/{all,filled,available}/{signupId}`), which are not
119
+ reachable in session mode at all (no v3 equivalent exists). Not covered
120
+ here since this skill is the email/password session path.
121
+ - **fetchproxy** (lifting these same cookies out of a signed-in browser tab)
122
+ is the MCP's fallback for when no env credentials are set. This skill
123
+ always logs in directly, so fetchproxy/the Transporter extension is never
124
+ needed.
@@ -0,0 +1,200 @@
1
+ # SignUpGenius session-mode endpoints for curl
2
+
3
+ All calls assume `$ACCESS_TOKEN` and `$COOKIE_HEADER` from the login step in
4
+ `../SKILL.md`. Every call sends both:
5
+
6
+ ```
7
+ -H "Authorization: Bearer $ACCESS_TOKEN" -H "Cookie: $COOKIE_HEADER"
8
+ ```
9
+
10
+ Paths under `api.signupgenius.com/v3` are the exact paths `signupgenius-mcp`'s
11
+ `client.ts` builds in session mode (it appends a trailing `/` to every v3
12
+ path — included below). Legacy calls POST JSON to
13
+ `https://www.signupgenius.com/SUGboxAPI.cfm?go=<action>` with
14
+ `Content-Type: application/json`.
15
+
16
+ ---
17
+
18
+ ## 1. Profile
19
+
20
+ ```sh
21
+ curl -s 'https://api.signupgenius.com/v3/member/profile/' \
22
+ -H "Authorization: Bearer $ACCESS_TOKEN" -H "Cookie: $COOKIE_HEADER" | jq '.data'
23
+ ```
24
+
25
+ ## 2. Groups
26
+
27
+ List groups (`sort` is optional, `asc`/`desc`):
28
+
29
+ ```sh
30
+ curl -s 'https://api.signupgenius.com/v3/groups/all/?sort=asc' \
31
+ -H "Authorization: Bearer $ACCESS_TOKEN" -H "Cookie: $COOKIE_HEADER" \
32
+ | jq -r '.data[] | "\(.groupid)\t\(.title)"'
33
+ ```
34
+
35
+ List a group's members (`GROUP_ID` from above):
36
+
37
+ ```sh
38
+ curl -s "https://api.signupgenius.com/v3/groups/${GROUP_ID}/members/" \
39
+ -H "Authorization: Bearer $ACCESS_TOKEN" -H "Cookie: $COOKIE_HEADER" \
40
+ | jq -r '.data[] | "\(.communitymemberid)\t\(.emailaddress)"'
41
+ ```
42
+
43
+ Get one member's detail (address/phone — only present if they supplied it on
44
+ a sign-up):
45
+
46
+ ```sh
47
+ curl -s "https://api.signupgenius.com/v3/groups/${GROUP_ID}/members/${MEMBER_ID}/details/" \
48
+ -H "Authorization: Bearer $ACCESS_TOKEN" -H "Cookie: $COOKIE_HEADER" | jq '.data'
49
+ ```
50
+
51
+ Add a member (**write** — confirm with the user first; `firstname`/`lastname`
52
+ are optional):
53
+
54
+ ```sh
55
+ curl -s -X POST "https://api.signupgenius.com/v3/groups/${GROUP_ID}/members/create/" \
56
+ -H "Authorization: Bearer $ACCESS_TOKEN" -H "Cookie: $COOKIE_HEADER" \
57
+ -H 'Content-Type: application/json' \
58
+ -d '{"emailaddress":"new-member@example.com","firstname":"Jane","lastname":"Doe"}' \
59
+ | jq '.success, .message'
60
+ ```
61
+
62
+ ## 3. Sign-up listings
63
+
64
+ In session mode `created`/`invited`/`signedupfor` each have **one** v3 path
65
+ each (unlike key mode's separate `/active`/`/expired`/`/all` paths) — filter
66
+ on `enddate` client-side if you only want active ones:
67
+
68
+ ```sh
69
+ # everything the account created (active + expired together)
70
+ curl -s 'https://api.signupgenius.com/v3/signups/created/' \
71
+ -H "Authorization: Bearer $ACCESS_TOKEN" -H "Cookie: $COOKIE_HEADER" \
72
+ | jq -r '.data[] | "\(.signupid)\t\(.enddate)\t\(.title)"'
73
+
74
+ # sign-ups invited to
75
+ curl -s 'https://api.signupgenius.com/v3/signups/invited/' \
76
+ -H "Authorization: Bearer $ACCESS_TOKEN" -H "Cookie: $COOKIE_HEADER" | jq '.data'
77
+
78
+ # sign-ups personally signed up for
79
+ curl -s 'https://api.signupgenius.com/v3/signups/signedupfor/' \
80
+ -H "Authorization: Bearer $ACCESS_TOKEN" -H "Cookie: $COOKIE_HEADER" | jq '.data'
81
+ ```
82
+
83
+ Legacy dispatcher equivalent — sometimes returns fuller data than v3
84
+ (note the **upper-case** envelope):
85
+
86
+ ```sh
87
+ curl -s -X POST 'https://www.signupgenius.com/SUGboxAPI.cfm?go=t.getMySignups' \
88
+ -H "Authorization: Bearer $ACCESS_TOKEN" -H "Cookie: $COOKIE_HEADER" \
89
+ -H 'Content-Type: application/json' -d '{}' \
90
+ | jq -r '.DATA[] | "\(.signupid)\t\(.title)"'
91
+ ```
92
+
93
+ ## 4. Public sign-up lookup (no auth)
94
+
95
+ Any sign-up's public page is server-rendered HTML at
96
+ `https://www.signupgenius.com/go/<urlid>-<signupid>[-<vanity>]` — no login
97
+ needed, works even without the login step above:
98
+
99
+ ```sh
100
+ curl -s 'https://www.signupgenius.com/go/10C054DA9AF2BA0FEC07-63774883-myers' -o /tmp/sug-page.html
101
+
102
+ grep -oE '<h1 class="SUGHeaderText">[^<]*' /tmp/sug-page.html | sed 's/.*>//' # title
103
+ grep -A2 '<strong>Date' /tmp/sug-page.html | head -1 # date (needs HTML stripping)
104
+ ```
105
+
106
+ There's no JSON surface for this page — `signupgenius-mcp`'s
107
+ `tools/public-signup.ts` regex-scrapes the same landmarks
108
+ (`h1.SUGHeaderText`, `<strong>Date/Time/Location</strong>`, the
109
+ `creator-info` table, and `Yes:/No:/Maybe:` response counts). Reproduce with
110
+ `grep`/`sed` for a quick look, or pull the regexes straight from that file for
111
+ anything more than a spot-check.
112
+
113
+ ## 5. RSVP (write, 3-step flow)
114
+
115
+ RSVP-only (Yes/No/Maybe headcount) sheets. **Confirm with the user before
116
+ running step 3** — it's the real submit. Get `URLID` (the full slug) by
117
+ parsing the public URL as in §4.
118
+
119
+ **Step 1 — PreProcessSignup** (sets a server-side session pointer; without
120
+ this every SUGboxAPI call below 404s with "none to be processed"). Expect
121
+ **301/302**, not 200:
122
+
123
+ ```sh
124
+ curl -s -o /dev/null -w '%{http_code}\n' -D - \
125
+ -H "Authorization: Bearer $ACCESS_TOKEN" -H "Cookie: $COOKIE_HEADER" \
126
+ -H 'Content-Type: application/x-www-form-urlencoded' -H 'Accept: text/html' \
127
+ -X POST "https://www.signupgenius.com/index.cfm?go=s.PreProcessSignup&URLID=${URLID}" \
128
+ --data 'ScreenWidth=2000&ScreenHeight=1200'
129
+ ```
130
+
131
+ **Step 2 — getSignupInfo** (fetch `useRSVP` + `rsvpdetails.slotid`; reject if
132
+ `useRSVP != 1` or `rsvpdetails.rsvpitems` is non-empty — that's an item-based
133
+ sheet, unsupported by this flow):
134
+
135
+ ```sh
136
+ curl -s -X POST 'https://www.signupgenius.com/SUGboxAPI.cfm?go=s.getSignupInfo' \
137
+ -H "Authorization: Bearer $ACCESS_TOKEN" -H "Cookie: $COOKIE_HEADER" \
138
+ -H 'Content-Type: application/json' \
139
+ -d "{\"urlid\":\"${URLID}\"}" | tee /tmp/sug-signupinfo.json | jq '.DATA | {useRSVP, owner, id, title, slotid: .rsvpdetails.slotid}'
140
+ ```
141
+
142
+ **Step 3 — processSignUpFormHandler** (the actual RSVP submit). `RSVPITEMS`
143
+ must always be present as `[]` — the CFML validator throws
144
+ `key [RSVPITEMS] doesn't exist` if it's omitted. `changemembermame` is
145
+ SignUpGenius's own typo — preserve it verbatim. `rsvpresponse` is a single
146
+ letter: `y`/`n`/`m`. A `n` response forces both guest counts to `0`
147
+ regardless of what you pass:
148
+
149
+ ```sh
150
+ OWNER=$(jq -r '.DATA.owner' /tmp/sug-signupinfo.json)
151
+ LISTID=$(jq -r '.DATA.id' /tmp/sug-signupinfo.json)
152
+ TITLE=$(jq -r '.DATA.title' /tmp/sug-signupinfo.json)
153
+ SLOTID=$(jq -r '.DATA.rsvpdetails.slotid' /tmp/sug-signupinfo.json)
154
+
155
+ curl -s -X POST 'https://www.signupgenius.com/SUGboxAPI.cfm?go=s.processSignUpFormHandler' \
156
+ -H "Authorization: Bearer $ACCESS_TOKEN" -H "Cookie: $COOKIE_HEADER" \
157
+ -H 'Content-Type: application/json' \
158
+ -d @- <<JSON | jq '.SUCCESS, .MESSAGE'
159
+ {
160
+ "listid": ${LISTID},
161
+ "owner": ${OWNER},
162
+ "urlid": "${URLID}",
163
+ "title": "${TITLE}",
164
+ "siid": "",
165
+ "rsvpid": 0,
166
+ "imid": 0,
167
+ "usealternatename": false,
168
+ "changemembermame": false,
169
+ "displayfirstname": "Jane",
170
+ "displaylastname": "Doe",
171
+ "firstname": "Jane",
172
+ "lastname": "Doe",
173
+ "email": "jane@example.com",
174
+ "optInStatus": false,
175
+ "savecontactinfo": false,
176
+ "rsvpresponse": "y",
177
+ "rsvpadult": 1,
178
+ "rsvpchildren": 0,
179
+ "rsvpitems": [],
180
+ "rsvpcomments": "",
181
+ "type": "rsvp",
182
+ "source": "main",
183
+ "slotid": ${SLOTID},
184
+ "isLoggedin": true,
185
+ "payLater": false,
186
+ "customFields": []
187
+ }
188
+ JSON
189
+ ```
190
+
191
+ ## Omitted — not reachable in session mode
192
+
193
+ Slot-report endpoints (`/signups/report/all|filled|available/{signupId}`)
194
+ require `SIGNUPGENIUS_USER_KEY` (Pro API key mode, `Authorization: <key>`
195
+ against `api.signupgenius.com/v2/k`, `user_key` in the query string) — a
196
+ different auth entirely from the session login this skill uses. No v3
197
+ equivalent was found during the MCP's recon (`src/tools/reports.ts`), so
198
+ there's nothing to transcribe for session mode. Slot-based (non-headcount)
199
+ sign-ups also have no submit flow here — the wizard's `s.getSignUpFormItems`
200
+ + per-item payload was never captured.