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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +3 -2
- package/dist/bundle.js +128 -8
- package/dist/index.js +1 -1
- package/package.json +3 -3
- package/server.json +2 -2
- package/skills/signupgenius/SKILL.md +116 -0
- package/skills/signupgenius-api/SKILL.md +124 -0
- package/skills/signupgenius-api/references/sug-endpoints.md +200 -0
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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: "^
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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": "^
|
|
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.
|
|
9
|
+
"version": "1.2.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "signupgenius-mcp",
|
|
14
|
-
"version": "1.
|
|
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.
|