signupgenius-mcp 1.1.5 → 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 +202 -37
- package/dist/client.js +16 -28
- package/dist/index.js +1 -1
- package/dist/tools/reports.js +3 -2
- 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
|
@@ -34701,6 +34701,32 @@ var McpToolError = class extends Error {
|
|
|
34701
34701
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
34702
34702
|
}
|
|
34703
34703
|
};
|
|
34704
|
+
var UnreachableError = class extends McpToolError {
|
|
34705
|
+
/** Upstream HTTP status, when one was observed. */
|
|
34706
|
+
status;
|
|
34707
|
+
constructor(service, status) {
|
|
34708
|
+
const suffix = status !== void 0 ? ` (status ${status})` : "";
|
|
34709
|
+
super(`${service} unreachable${suffix}. The service may be down \u2014 try again later.`, {
|
|
34710
|
+
hint: "The upstream service is temporarily unavailable; retry later."
|
|
34711
|
+
});
|
|
34712
|
+
this.name = "UnreachableError";
|
|
34713
|
+
if (status !== void 0)
|
|
34714
|
+
this.status = status;
|
|
34715
|
+
}
|
|
34716
|
+
};
|
|
34717
|
+
var ModeMismatchError = class extends McpToolError {
|
|
34718
|
+
currentMode;
|
|
34719
|
+
requiredMode;
|
|
34720
|
+
feature;
|
|
34721
|
+
constructor(currentMode, requiredMode, feature) {
|
|
34722
|
+
const hint = `Switch to ${requiredMode} mode to use ${feature}.`;
|
|
34723
|
+
super(`${feature} requires ${requiredMode} mode but the server is running in ${currentMode} mode. ${hint}`, { hint });
|
|
34724
|
+
this.currentMode = currentMode;
|
|
34725
|
+
this.requiredMode = requiredMode;
|
|
34726
|
+
this.feature = feature;
|
|
34727
|
+
this.name = "ModeMismatchError";
|
|
34728
|
+
}
|
|
34729
|
+
};
|
|
34704
34730
|
function createHelpfulError(message, opts) {
|
|
34705
34731
|
return new McpToolError(message, opts);
|
|
34706
34732
|
}
|
|
@@ -34718,6 +34744,9 @@ var API_KEY_RE = new RegExp([
|
|
|
34718
34744
|
"whsec_[A-Za-z0-9]{16,}\\b"
|
|
34719
34745
|
// webhook signing secret (Stripe-style)
|
|
34720
34746
|
].map((p) => `\\b${p}`).join("|"), "g");
|
|
34747
|
+
var JSON_SECRET_KEYS = "access_token|refresh_token|client_secret|api_?key|password|passwd|secret|token";
|
|
34748
|
+
var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")[^"]*(")`, "gi");
|
|
34749
|
+
var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')[^']*(')`, "gi");
|
|
34721
34750
|
|
|
34722
34751
|
// node_modules/@chrischall/mcp-utils/dist/response/index.js
|
|
34723
34752
|
function textResult(data) {
|
|
@@ -34919,6 +34948,7 @@ var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
|
34919
34948
|
"capture_request_header",
|
|
34920
34949
|
"capture_redirect",
|
|
34921
34950
|
"read_indexed_db",
|
|
34951
|
+
"read_dom",
|
|
34922
34952
|
"download"
|
|
34923
34953
|
]);
|
|
34924
34954
|
|
|
@@ -35220,6 +35250,44 @@ function assertIndexedDbScopesArray(value, label) {
|
|
|
35220
35250
|
}
|
|
35221
35251
|
}
|
|
35222
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
|
+
}
|
|
35223
35291
|
function validateFrame(raw) {
|
|
35224
35292
|
assertObject(raw, "frame");
|
|
35225
35293
|
const t = raw.type;
|
|
@@ -35295,6 +35363,9 @@ function validateHello(raw) {
|
|
|
35295
35363
|
if (raw.sessionStoragePointers !== void 0) {
|
|
35296
35364
|
assertStoragePointersArray(raw.sessionStoragePointers, "hello.sessionStoragePointers", raw.sessionStorageKeys);
|
|
35297
35365
|
}
|
|
35366
|
+
if (raw.domSelectors !== void 0) {
|
|
35367
|
+
assertDomSelectorsArray(raw.domSelectors, "hello.domSelectors");
|
|
35368
|
+
}
|
|
35298
35369
|
assertBase64(raw.identityX25519Pub, "hello.identityX25519Pub");
|
|
35299
35370
|
assertBase64(raw.identityEd25519Pub, "hello.identityEd25519Pub");
|
|
35300
35371
|
assertBase64(raw.sessionNonce, "hello.sessionNonce");
|
|
@@ -35529,6 +35600,21 @@ function validateInnerRequest(raw) {
|
|
|
35529
35600
|
}
|
|
35530
35601
|
return raw;
|
|
35531
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
|
+
}
|
|
35532
35618
|
if (raw.op === "download") {
|
|
35533
35619
|
assertObject(raw.init, "inner.init");
|
|
35534
35620
|
if (raw.init.url === void 0) {
|
|
@@ -35554,7 +35640,7 @@ function validateInnerRequest(raw) {
|
|
|
35554
35640
|
}
|
|
35555
35641
|
return raw;
|
|
35556
35642
|
}
|
|
35557
|
-
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)}`);
|
|
35558
35644
|
}
|
|
35559
35645
|
function assertNonEmptyKeyArray(value, label) {
|
|
35560
35646
|
if (!Array.isArray(value)) {
|
|
@@ -35639,6 +35725,13 @@ function validateInnerResponse(raw) {
|
|
|
35639
35725
|
assertObject(raw.values, "inner.values");
|
|
35640
35726
|
return raw;
|
|
35641
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
|
+
}
|
|
35642
35735
|
if (op === "download") {
|
|
35643
35736
|
assertObject(raw.value, "inner.value");
|
|
35644
35737
|
assertString(raw.value.path, "inner.value.path");
|
|
@@ -35978,6 +36071,13 @@ async function buildServerHello(opts) {
|
|
|
35978
36071
|
jsonPointer: d.jsonPointer
|
|
35979
36072
|
}));
|
|
35980
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
|
+
}
|
|
35981
36081
|
return hello;
|
|
35982
36082
|
}
|
|
35983
36083
|
|
|
@@ -36067,7 +36167,8 @@ async function startHost(opts) {
|
|
|
36067
36167
|
captureHeaders: opts.ownCaptureHeaders,
|
|
36068
36168
|
indexedDbScopes: opts.ownIndexedDbScopes,
|
|
36069
36169
|
localStoragePointers: opts.ownLocalStoragePointers,
|
|
36070
|
-
sessionStoragePointers: opts.ownSessionStoragePointers
|
|
36170
|
+
sessionStoragePointers: opts.ownSessionStoragePointers,
|
|
36171
|
+
domSelectors: opts.ownDomSelectors
|
|
36071
36172
|
});
|
|
36072
36173
|
const ownSessionNonce = fromB64(ownHello.sessionNonce);
|
|
36073
36174
|
let extensionWs = null;
|
|
@@ -36302,6 +36403,7 @@ async function startPeer(opts) {
|
|
|
36302
36403
|
sessionStorageKeys: opts.sessionStorageKeys,
|
|
36303
36404
|
captureHeaders: opts.captureHeaders,
|
|
36304
36405
|
indexedDbScopes: opts.indexedDbScopes,
|
|
36406
|
+
domSelectors: opts.domSelectors,
|
|
36305
36407
|
localStoragePointers: opts.localStoragePointers,
|
|
36306
36408
|
sessionStoragePointers: opts.sessionStoragePointers
|
|
36307
36409
|
});
|
|
@@ -36709,6 +36811,11 @@ var FetchproxyServer = class {
|
|
|
36709
36811
|
key: d.key,
|
|
36710
36812
|
jsonPointer: d.jsonPointer
|
|
36711
36813
|
})),
|
|
36814
|
+
domSelectors: (opts.domSelectors ?? []).map((d) => ({
|
|
36815
|
+
name: d.name,
|
|
36816
|
+
selector: d.selector,
|
|
36817
|
+
...d.attribute !== void 0 ? { attribute: d.attribute } : {}
|
|
36818
|
+
})),
|
|
36712
36819
|
// 0.8.0+: timer + lazy-revive default to ON. Every realty MCP
|
|
36713
36820
|
// adapter was about to set these to the same numbers anyway; the
|
|
36714
36821
|
// back-door is `0` (explicit opt-out) if a caller genuinely wants
|
|
@@ -36829,6 +36936,7 @@ var FetchproxyServer = class {
|
|
|
36829
36936
|
ownIndexedDbScopes: this.opts.indexedDbScopes,
|
|
36830
36937
|
ownLocalStoragePointers: this.opts.localStoragePointers,
|
|
36831
36938
|
ownSessionStoragePointers: this.opts.sessionStoragePointers,
|
|
36939
|
+
ownDomSelectors: this.opts.domSelectors,
|
|
36832
36940
|
onPairCode: this.opts.onPairCode
|
|
36833
36941
|
});
|
|
36834
36942
|
this.hostHandle.onOwnInner((inner) => this.onInner(inner));
|
|
@@ -36856,7 +36964,8 @@ var FetchproxyServer = class {
|
|
|
36856
36964
|
captureHeaders: this.opts.captureHeaders,
|
|
36857
36965
|
indexedDbScopes: this.opts.indexedDbScopes,
|
|
36858
36966
|
localStoragePointers: this.opts.localStoragePointers,
|
|
36859
|
-
sessionStoragePointers: this.opts.sessionStoragePointers
|
|
36967
|
+
sessionStoragePointers: this.opts.sessionStoragePointers,
|
|
36968
|
+
domSelectors: this.opts.domSelectors
|
|
36860
36969
|
});
|
|
36861
36970
|
this.peerHandle.onInner((inner) => this.onInner(inner));
|
|
36862
36971
|
this.peerHandle.onRenegotiate(() => {
|
|
@@ -37831,6 +37940,46 @@ var FetchproxyServer = class {
|
|
|
37831
37940
|
await this.sendInnerFrame(inner);
|
|
37832
37941
|
return this._withVerbTimeout(pending, this.pendingIdb, id, origin);
|
|
37833
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
|
+
}
|
|
37834
37983
|
assertScopeSubset(requested, declared, label) {
|
|
37835
37984
|
const undeclared = undeclaredKeys(requested, declared);
|
|
37836
37985
|
if (undeclared.length > 0) {
|
|
@@ -37902,7 +38051,7 @@ var FetchproxyServer = class {
|
|
|
37902
38051
|
if (storageCb) {
|
|
37903
38052
|
this.pendingStorage.delete(inner.id);
|
|
37904
38053
|
if (inner.ok) {
|
|
37905
|
-
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) {
|
|
37906
38055
|
storageCb.resolve({ ...inner.values });
|
|
37907
38056
|
} else {
|
|
37908
38057
|
storageCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on storage awaiter`));
|
|
@@ -38297,7 +38446,7 @@ function loadAccount(env = process.env) {
|
|
|
38297
38446
|
// package.json
|
|
38298
38447
|
var package_default = {
|
|
38299
38448
|
name: "signupgenius-mcp",
|
|
38300
|
-
version: "1.
|
|
38449
|
+
version: "1.2.0",
|
|
38301
38450
|
mcpName: "io.github.chrischall/signupgenius-mcp",
|
|
38302
38451
|
description: "SignUpGenius MCP server \u2014 read sign-ups, reports, and groups; add group members.",
|
|
38303
38452
|
author: "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -38334,7 +38483,7 @@ var package_default = {
|
|
|
38334
38483
|
"test:watch": "vitest"
|
|
38335
38484
|
},
|
|
38336
38485
|
dependencies: {
|
|
38337
|
-
"@chrischall/mcp-utils": "^0.
|
|
38486
|
+
"@chrischall/mcp-utils": "^0.13.0",
|
|
38338
38487
|
"@fetchproxy/bootstrap": "^1.0.0",
|
|
38339
38488
|
"@fetchproxy/server": "^1.0.0",
|
|
38340
38489
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
@@ -38345,7 +38494,7 @@ var package_default = {
|
|
|
38345
38494
|
"@types/node": "^26.0.0",
|
|
38346
38495
|
"@vitest/coverage-v8": "^4.1.7",
|
|
38347
38496
|
esbuild: "^0.28.0",
|
|
38348
|
-
typescript: "^
|
|
38497
|
+
typescript: "^7.0.2",
|
|
38349
38498
|
vitest: "^4.1.7"
|
|
38350
38499
|
}
|
|
38351
38500
|
};
|
|
@@ -38434,18 +38583,30 @@ var CookieSessionManager = class {
|
|
|
38434
38583
|
inFlight;
|
|
38435
38584
|
/** A cached *permanent* config error: once set, every `ensure` rethrows it. */
|
|
38436
38585
|
permanentError;
|
|
38586
|
+
/** When the current session was minted (login) or installed (seed) — for maxAgeMs. */
|
|
38587
|
+
sessionAt = 0;
|
|
38437
38588
|
loginFn;
|
|
38438
38589
|
isExpiredFn;
|
|
38439
38590
|
isPermanentErrorFn;
|
|
38591
|
+
maxAgeMs;
|
|
38592
|
+
now;
|
|
38593
|
+
onReplayLoginErrorFn;
|
|
38440
38594
|
constructor(opts) {
|
|
38441
38595
|
this.loginFn = opts.login;
|
|
38442
38596
|
this.isExpiredFn = opts.isExpired ?? (() => false);
|
|
38443
38597
|
this.isPermanentErrorFn = opts.isPermanentError ?? (() => false);
|
|
38598
|
+
this.maxAgeMs = opts.maxAgeMs;
|
|
38599
|
+
this.now = opts.now ?? Date.now;
|
|
38600
|
+
this.onReplayLoginErrorFn = opts.onReplayLoginError;
|
|
38444
38601
|
}
|
|
38445
38602
|
/** The current session, or `undefined` before the first successful login. */
|
|
38446
38603
|
get current() {
|
|
38447
38604
|
return this.session;
|
|
38448
38605
|
}
|
|
38606
|
+
/** True when {@link CookieSessionManagerOptions.maxAgeMs} says the session is too old. */
|
|
38607
|
+
isStale() {
|
|
38608
|
+
return this.maxAgeMs !== void 0 && this.now() - this.sessionAt >= this.maxAgeMs;
|
|
38609
|
+
}
|
|
38449
38610
|
/**
|
|
38450
38611
|
* Return the current session, or single-flight a login if there is none.
|
|
38451
38612
|
*
|
|
@@ -38454,10 +38615,16 @@ var CookieSessionManager = class {
|
|
|
38454
38615
|
* the next call retries. A login error classified permanent by
|
|
38455
38616
|
* {@link CookieSessionManagerOptions.isPermanentError} is cached and rethrown
|
|
38456
38617
|
* on every later call; a transient error is not cached (next call retries).
|
|
38618
|
+
* With {@link CookieSessionManagerOptions.maxAgeMs} set, a session past its
|
|
38619
|
+
* TTL is invalidated first, so the call falls through to a (single-flight)
|
|
38620
|
+
* re-login.
|
|
38457
38621
|
*/
|
|
38458
38622
|
async ensure() {
|
|
38459
|
-
if (this.session !== void 0)
|
|
38460
|
-
|
|
38623
|
+
if (this.session !== void 0) {
|
|
38624
|
+
if (!this.isStale())
|
|
38625
|
+
return this.session;
|
|
38626
|
+
this.invalidate();
|
|
38627
|
+
}
|
|
38461
38628
|
if (this.permanentError !== void 0)
|
|
38462
38629
|
throw this.permanentError;
|
|
38463
38630
|
if (this.inFlight === void 0) {
|
|
@@ -38465,6 +38632,21 @@ var CookieSessionManager = class {
|
|
|
38465
38632
|
}
|
|
38466
38633
|
return this.inFlight;
|
|
38467
38634
|
}
|
|
38635
|
+
/**
|
|
38636
|
+
* Install an externally-minted session (e.g. infinitecampus's CUPS linked-
|
|
38637
|
+
* district discovery, which mints sessions outside {@link
|
|
38638
|
+
* CookieSessionManagerOptions.login}). The seed becomes the current session
|
|
38639
|
+
* with a fresh {@link CookieSessionManagerOptions.maxAgeMs} clock, and any
|
|
38640
|
+
* in-flight login is DETACHED: its waiters still receive its result, but a
|
|
38641
|
+
* late resolution will not overwrite the seed. A cached permanent config
|
|
38642
|
+
* error is left intact — the login path is still misconfigured, and the next
|
|
38643
|
+
* post-seed re-login should keep saying so.
|
|
38644
|
+
*/
|
|
38645
|
+
seed(session) {
|
|
38646
|
+
this.session = session;
|
|
38647
|
+
this.sessionAt = this.now();
|
|
38648
|
+
this.inFlight = void 0;
|
|
38649
|
+
}
|
|
38468
38650
|
/**
|
|
38469
38651
|
* One login attempt. Self-clears `inFlight` on settle so a rejected login
|
|
38470
38652
|
* never sticks, and stamps the resulting session only while it's still the
|
|
@@ -38476,8 +38658,10 @@ var CookieSessionManager = class {
|
|
|
38476
38658
|
holder.p = (async () => {
|
|
38477
38659
|
try {
|
|
38478
38660
|
const session = await this.loginFn();
|
|
38479
|
-
if (this.inFlight === holder.p)
|
|
38661
|
+
if (this.inFlight === holder.p) {
|
|
38480
38662
|
this.session = session;
|
|
38663
|
+
this.sessionAt = this.now();
|
|
38664
|
+
}
|
|
38481
38665
|
return session;
|
|
38482
38666
|
} catch (err) {
|
|
38483
38667
|
if (this.isPermanentErrorFn(err))
|
|
@@ -38524,7 +38708,8 @@ var CookieSessionManager = class {
|
|
|
38524
38708
|
let fresh;
|
|
38525
38709
|
try {
|
|
38526
38710
|
fresh = await this.ensure();
|
|
38527
|
-
} catch {
|
|
38711
|
+
} catch (err) {
|
|
38712
|
+
this.onReplayLoginErrorFn?.(err);
|
|
38528
38713
|
return res;
|
|
38529
38714
|
}
|
|
38530
38715
|
return call(fresh);
|
|
@@ -38801,7 +38986,7 @@ async function parseEnvelope(res, context, normalize) {
|
|
|
38801
38986
|
const msg = parsed?.message.join("; ");
|
|
38802
38987
|
if (res.status === 401 || res.status === 403) throw new AuthError(res.status, msg);
|
|
38803
38988
|
if (res.status === 404) throw new Error(`SignUpGenius 404 ${context}`);
|
|
38804
|
-
if (res.status >= 500) throw new UnreachableError(res.status);
|
|
38989
|
+
if (res.status >= 500) throw new UnreachableError("SignUpGenius", res.status);
|
|
38805
38990
|
if (!res.ok) throw new Error(`SignUpGenius ${res.status} ${msg || res.statusText} for ${context}`);
|
|
38806
38991
|
if (!parsed) {
|
|
38807
38992
|
throw new Error(`SignUpGenius returned ${text === "" ? "empty" : "non-JSON"} body for ${context}`);
|
|
@@ -38819,37 +39004,17 @@ function parseJsonBody(text) {
|
|
|
38819
39004
|
return null;
|
|
38820
39005
|
}
|
|
38821
39006
|
}
|
|
38822
|
-
var AuthError = class extends
|
|
39007
|
+
var AuthError = class _AuthError extends McpToolError {
|
|
38823
39008
|
constructor(status, detail) {
|
|
38824
39009
|
super(
|
|
38825
|
-
`SignUpGenius rejected the request (${status}).
|
|
39010
|
+
`SignUpGenius rejected the request (${status}). ` + _AuthError.HINT + (detail ? ` (${detail})` : ""),
|
|
39011
|
+
{ hint: _AuthError.HINT }
|
|
38826
39012
|
);
|
|
38827
39013
|
this.status = status;
|
|
38828
39014
|
this.name = "AuthError";
|
|
38829
39015
|
}
|
|
38830
39016
|
status;
|
|
38831
|
-
|
|
38832
|
-
var UnreachableError = class extends Error {
|
|
38833
|
-
constructor(status) {
|
|
38834
|
-
super(`SignUpGenius unreachable (status ${status})`);
|
|
38835
|
-
this.status = status;
|
|
38836
|
-
this.name = "UnreachableError";
|
|
38837
|
-
}
|
|
38838
|
-
status;
|
|
38839
|
-
};
|
|
38840
|
-
var ModeMismatchError = class extends Error {
|
|
38841
|
-
constructor(currentMode, requiredMode, feature) {
|
|
38842
|
-
super(
|
|
38843
|
-
`${feature} requires ${requiredMode} mode but the server is running in ${currentMode} mode. ` + (requiredMode === "key" ? "Set SIGNUPGENIUS_USER_KEY (Pro subscription required for the documented v2/k API)." : "Set SIGNUPGENIUS_EMAIL + SIGNUPGENIUS_PASSWORD to enable this tool.")
|
|
38844
|
-
);
|
|
38845
|
-
this.currentMode = currentMode;
|
|
38846
|
-
this.requiredMode = requiredMode;
|
|
38847
|
-
this.feature = feature;
|
|
38848
|
-
this.name = "ModeMismatchError";
|
|
38849
|
-
}
|
|
38850
|
-
currentMode;
|
|
38851
|
-
requiredMode;
|
|
38852
|
-
feature;
|
|
39017
|
+
static HINT = "For key mode: check SIGNUPGENIUS_USER_KEY (it may be wrong, revoked, or the account no longer has Pro). For session mode: the session cookie may have been invalidated server-side.";
|
|
38853
39018
|
};
|
|
38854
39019
|
|
|
38855
39020
|
// src/tools/_shared.ts
|
|
@@ -39342,7 +39507,7 @@ bannerLines.push(
|
|
|
39342
39507
|
);
|
|
39343
39508
|
await runMcp({
|
|
39344
39509
|
name: "signupgenius",
|
|
39345
|
-
version: "1.
|
|
39510
|
+
version: "1.2.0",
|
|
39346
39511
|
// x-release-please-version
|
|
39347
39512
|
banner: bannerLines.join("\n"),
|
|
39348
39513
|
deps: client,
|
package/dist/client.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import { McpToolError, ModeMismatchError, UnreachableError } from '@chrischall/mcp-utils';
|
|
1
2
|
import { CookieSessionManager } from '@chrischall/mcp-utils/session';
|
|
3
|
+
// Re-exported so tools/tests keep importing the error types from the client
|
|
4
|
+
// module (the shared classes live in @chrischall/mcp-utils since 0.10.x).
|
|
5
|
+
export { ModeMismatchError, UnreachableError };
|
|
2
6
|
import { sessionLogin as defaultSessionLogin } from './auth-session-login.js';
|
|
3
7
|
/**
|
|
4
8
|
* Detect a session that lapsed server-side. SignUpGenius signals expiry two
|
|
@@ -245,7 +249,7 @@ async function parseEnvelope(res, context, normalize) {
|
|
|
245
249
|
if (res.status === 404)
|
|
246
250
|
throw new Error(`SignUpGenius 404 ${context}`);
|
|
247
251
|
if (res.status >= 500)
|
|
248
|
-
throw new UnreachableError(res.status);
|
|
252
|
+
throw new UnreachableError('SignUpGenius', res.status);
|
|
249
253
|
if (!res.ok)
|
|
250
254
|
throw new Error(`SignUpGenius ${res.status} ${msg || res.statusText} for ${context}`);
|
|
251
255
|
if (!parsed) {
|
|
@@ -266,37 +270,21 @@ function parseJsonBody(text) {
|
|
|
266
270
|
return null;
|
|
267
271
|
}
|
|
268
272
|
}
|
|
269
|
-
|
|
273
|
+
/**
|
|
274
|
+
* SignUpGenius rejected a request (401/403). Kept as a local class (rather
|
|
275
|
+
* than the shared `SessionNotAuthenticatedError`, whose fixed browser-sign-in
|
|
276
|
+
* message can't carry it) because the remediation is mode-dependent SUG
|
|
277
|
+
* guidance — surfaced as the `hint` per the `McpToolError` contract.
|
|
278
|
+
*/
|
|
279
|
+
export class AuthError extends McpToolError {
|
|
270
280
|
status;
|
|
281
|
+
static HINT = 'For key mode: check SIGNUPGENIUS_USER_KEY (it may be wrong, revoked, or the account no longer has Pro). ' +
|
|
282
|
+
'For session mode: the session cookie may have been invalidated server-side.';
|
|
271
283
|
constructor(status, detail) {
|
|
272
284
|
super(`SignUpGenius rejected the request (${status}). ` +
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
(detail ? ` (${detail})` : ''));
|
|
285
|
+
AuthError.HINT +
|
|
286
|
+
(detail ? ` (${detail})` : ''), { hint: AuthError.HINT });
|
|
276
287
|
this.status = status;
|
|
277
288
|
this.name = 'AuthError';
|
|
278
289
|
}
|
|
279
290
|
}
|
|
280
|
-
export class UnreachableError extends Error {
|
|
281
|
-
status;
|
|
282
|
-
constructor(status) {
|
|
283
|
-
super(`SignUpGenius unreachable (status ${status})`);
|
|
284
|
-
this.status = status;
|
|
285
|
-
this.name = 'UnreachableError';
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
export class ModeMismatchError extends Error {
|
|
289
|
-
currentMode;
|
|
290
|
-
requiredMode;
|
|
291
|
-
feature;
|
|
292
|
-
constructor(currentMode, requiredMode, feature) {
|
|
293
|
-
super(`${feature} requires ${requiredMode} mode but the server is running in ${currentMode} mode. ` +
|
|
294
|
-
(requiredMode === 'key'
|
|
295
|
-
? 'Set SIGNUPGENIUS_USER_KEY (Pro subscription required for the documented v2/k API).'
|
|
296
|
-
: 'Set SIGNUPGENIUS_EMAIL + SIGNUPGENIUS_PASSWORD to enable this tool.'));
|
|
297
|
-
this.currentMode = currentMode;
|
|
298
|
-
this.requiredMode = requiredMode;
|
|
299
|
-
this.feature = feature;
|
|
300
|
-
this.name = 'ModeMismatchError';
|
|
301
|
-
}
|
|
302
|
-
}
|
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/dist/tools/reports.js
CHANGED
|
@@ -7,8 +7,9 @@ const reportArgs = z.object({
|
|
|
7
7
|
* Report endpoints are Pro-only. The session-mode v3 web API has no
|
|
8
8
|
* equivalent — only the sign-up owner can pull report data, and the v3 paths
|
|
9
9
|
* for it were not discovered during recon. We still register the tools so
|
|
10
|
-
* Claude knows they exist, but in session mode they fail fast with
|
|
11
|
-
* ModeMismatchError
|
|
10
|
+
* Claude knows they exist, but in session mode they fail fast with the shared
|
|
11
|
+
* ModeMismatchError, whose hint says "Switch to key mode to use {tool}." —
|
|
12
|
+
* the SIGNUPGENIUS_USER_KEY pointer lives in each tool's description below.
|
|
12
13
|
*/
|
|
13
14
|
export function registerReportTools(server, client) {
|
|
14
15
|
const register = (toolName, path, blurb) => {
|
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.
|