signupgenius-mcp 1.1.4 → 1.1.6
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 +1 -1
- package/dist/bundle.js +272 -80
- 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
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "MCP server for SignUpGenius — read sign-ups, slot reports, and groups; add group members.",
|
|
10
|
-
"version": "1.1.
|
|
10
|
+
"version": "1.1.6"
|
|
11
11
|
},
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"displayName": "SignUpGenius",
|
|
16
16
|
"source": "./",
|
|
17
17
|
"description": "SignUpGenius MCP server for Claude — sign-ups, slot reports, and groups via natural language. Free or Pro accounts.",
|
|
18
|
-
"version": "1.1.
|
|
18
|
+
"version": "1.1.6",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "signupgenius-mcp",
|
|
3
3
|
"displayName": "SignUpGenius",
|
|
4
|
-
"version": "1.1.
|
|
4
|
+
"version": "1.1.6",
|
|
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",
|
package/dist/bundle.js
CHANGED
|
@@ -13,7 +13,11 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
13
13
|
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
14
14
|
});
|
|
15
15
|
var __commonJS = (cb, mod) => function __require2() {
|
|
16
|
-
|
|
16
|
+
try {
|
|
17
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
18
|
+
} catch (e) {
|
|
19
|
+
throw mod = 0, e;
|
|
20
|
+
}
|
|
17
21
|
};
|
|
18
22
|
var __export = (target, all) => {
|
|
19
23
|
for (var name in all)
|
|
@@ -34697,6 +34701,32 @@ var McpToolError = class extends Error {
|
|
|
34697
34701
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
34698
34702
|
}
|
|
34699
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
|
+
};
|
|
34700
34730
|
function createHelpfulError(message, opts) {
|
|
34701
34731
|
return new McpToolError(message, opts);
|
|
34702
34732
|
}
|
|
@@ -34714,6 +34744,9 @@ var API_KEY_RE = new RegExp([
|
|
|
34714
34744
|
"whsec_[A-Za-z0-9]{16,}\\b"
|
|
34715
34745
|
// webhook signing secret (Stripe-style)
|
|
34716
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");
|
|
34717
34750
|
|
|
34718
34751
|
// node_modules/@chrischall/mcp-utils/dist/response/index.js
|
|
34719
34752
|
function textResult(data) {
|
|
@@ -35860,10 +35893,20 @@ async function openEncryptedFrame(sessionKey, frame) {
|
|
|
35860
35893
|
|
|
35861
35894
|
// node_modules/@fetchproxy/server/dist/election.js
|
|
35862
35895
|
import { createServer, Server as HttpServer } from "node:http";
|
|
35896
|
+
var DEFAULT_BIND_TIMEOUT_MS = 5e3;
|
|
35863
35897
|
async function electRole(opts) {
|
|
35864
35898
|
const server = createServer();
|
|
35899
|
+
const bindTimeoutMs = opts.bindTimeoutMs ?? DEFAULT_BIND_TIMEOUT_MS;
|
|
35865
35900
|
return new Promise((resolve, reject) => {
|
|
35901
|
+
let timer;
|
|
35902
|
+
const clearBindTimer = () => {
|
|
35903
|
+
if (timer) {
|
|
35904
|
+
clearTimeout(timer);
|
|
35905
|
+
timer = void 0;
|
|
35906
|
+
}
|
|
35907
|
+
};
|
|
35866
35908
|
const onError = (e) => {
|
|
35909
|
+
clearBindTimer();
|
|
35867
35910
|
server.removeListener("listening", onListening);
|
|
35868
35911
|
if (e.code === "EADDRINUSE") {
|
|
35869
35912
|
try {
|
|
@@ -35876,11 +35919,25 @@ async function electRole(opts) {
|
|
|
35876
35919
|
}
|
|
35877
35920
|
};
|
|
35878
35921
|
const onListening = () => {
|
|
35922
|
+
clearBindTimer();
|
|
35879
35923
|
server.removeListener("error", onError);
|
|
35880
35924
|
resolve({ role: "host", server });
|
|
35881
35925
|
};
|
|
35882
35926
|
server.once("error", onError);
|
|
35883
35927
|
server.once("listening", onListening);
|
|
35928
|
+
if (bindTimeoutMs > 0) {
|
|
35929
|
+
timer = setTimeout(() => {
|
|
35930
|
+
server.removeListener("error", onError);
|
|
35931
|
+
server.removeListener("listening", onListening);
|
|
35932
|
+
try {
|
|
35933
|
+
server.close();
|
|
35934
|
+
} catch {
|
|
35935
|
+
}
|
|
35936
|
+
reject(new Error(`fetchproxy: bind to ${opts.host}:${opts.port} timed out after ${bindTimeoutMs}ms (port may be in a bad state)`));
|
|
35937
|
+
}, bindTimeoutMs);
|
|
35938
|
+
if (typeof timer.unref === "function")
|
|
35939
|
+
timer.unref();
|
|
35940
|
+
}
|
|
35884
35941
|
server.listen(opts.port, opts.host);
|
|
35885
35942
|
});
|
|
35886
35943
|
}
|
|
@@ -35973,6 +36030,44 @@ var SessionState = class {
|
|
|
35973
36030
|
}
|
|
35974
36031
|
};
|
|
35975
36032
|
|
|
36033
|
+
// node_modules/@fetchproxy/server/dist/session-ready.js
|
|
36034
|
+
var SESSION_READY_TIMEOUT_MS = 3e4;
|
|
36035
|
+
var FetchproxySessionNotReadyError = class extends Error {
|
|
36036
|
+
reason;
|
|
36037
|
+
pairCode;
|
|
36038
|
+
mcpId;
|
|
36039
|
+
hint;
|
|
36040
|
+
constructor(info) {
|
|
36041
|
+
const pairing = info.pairCode !== null && info.pairCode !== "";
|
|
36042
|
+
const hint = pairing ? `Open the Transporter extension popup and approve pair code ${info.pairCode} for "${info.mcpId}", then retry.` : `The extension is connected but hasn't confirmed a session for "${info.mcpId}" \u2014 sign in to the target site in that browser (and approve the requested scope if it changed), then retry.`;
|
|
36043
|
+
super(`fetchproxy: ${pairing ? "pairing not yet approved" : "no confirmed browser session"} for "${info.mcpId}". ${hint}`);
|
|
36044
|
+
this.name = "FetchproxySessionNotReadyError";
|
|
36045
|
+
this.reason = pairing ? "pair-required" : "not-ready";
|
|
36046
|
+
this.pairCode = pairing ? info.pairCode : null;
|
|
36047
|
+
this.mcpId = info.mcpId;
|
|
36048
|
+
this.hint = hint;
|
|
36049
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
36050
|
+
}
|
|
36051
|
+
};
|
|
36052
|
+
async function awaitSessionReady(ready, opts) {
|
|
36053
|
+
const ms = opts.timeoutMs ?? SESSION_READY_TIMEOUT_MS;
|
|
36054
|
+
if (ms <= 0)
|
|
36055
|
+
return ready;
|
|
36056
|
+
let timer;
|
|
36057
|
+
const timeout = new Promise((_, reject) => {
|
|
36058
|
+
timer = setTimeout(() => {
|
|
36059
|
+
reject(new FetchproxySessionNotReadyError({ mcpId: opts.mcpId, pairCode: opts.pendingPairCode() }));
|
|
36060
|
+
}, ms);
|
|
36061
|
+
timer.unref?.();
|
|
36062
|
+
});
|
|
36063
|
+
try {
|
|
36064
|
+
return await Promise.race([ready, timeout]);
|
|
36065
|
+
} finally {
|
|
36066
|
+
if (timer)
|
|
36067
|
+
clearTimeout(timer);
|
|
36068
|
+
}
|
|
36069
|
+
}
|
|
36070
|
+
|
|
35976
36071
|
// node_modules/@fetchproxy/server/dist/host.js
|
|
35977
36072
|
var PUBLIC_ORIGIN_RE = /^https?:\/\/(?!(127\.0\.0\.1|localhost)(:|$))/i;
|
|
35978
36073
|
var enc2 = new TextEncoder();
|
|
@@ -36060,6 +36155,29 @@ async function startHost(opts) {
|
|
|
36060
36155
|
return;
|
|
36061
36156
|
}
|
|
36062
36157
|
if (frame.type === "hello" && frame.role === "server") {
|
|
36158
|
+
const peerEdPub = fromB64(frame.identityEd25519Pub);
|
|
36159
|
+
const peerSigMsg = concatBytes(enc2.encode(frame.mcpId), fromB64(frame.sessionNonce));
|
|
36160
|
+
const peerSig = fromB64(frame.sessionSig);
|
|
36161
|
+
let peerSigOk = false;
|
|
36162
|
+
try {
|
|
36163
|
+
peerSigOk = await ed25519Verify(peerEdPub, peerSigMsg, peerSig);
|
|
36164
|
+
} catch {
|
|
36165
|
+
peerSigOk = false;
|
|
36166
|
+
}
|
|
36167
|
+
if (!peerSigOk) {
|
|
36168
|
+
console.warn("[fetchproxy] peer hello signature invalid \u2014 refusing registration (possible squatter)");
|
|
36169
|
+
ws.close(1008, "peer hello signature invalid");
|
|
36170
|
+
return;
|
|
36171
|
+
}
|
|
36172
|
+
const existing = peers.get(frame.mcpId);
|
|
36173
|
+
if (existing && existing.ws !== ws) {
|
|
36174
|
+
const existingEdPub = existing.helloFrame.identityEd25519Pub;
|
|
36175
|
+
if (existingEdPub !== frame.identityEd25519Pub) {
|
|
36176
|
+
console.warn("[fetchproxy] peer mcpId already mapped to a different identity \u2014 refusing (mcpId squatting)");
|
|
36177
|
+
ws.close(1008, "mcpId already registered to another identity");
|
|
36178
|
+
return;
|
|
36179
|
+
}
|
|
36180
|
+
}
|
|
36063
36181
|
identified = "peer";
|
|
36064
36182
|
peerMcpId = frame.mcpId;
|
|
36065
36183
|
peers.set(frame.mcpId, { ws, helloFrame: frame });
|
|
@@ -36152,8 +36270,10 @@ async function startHost(opts) {
|
|
|
36152
36270
|
resetSessionPromise();
|
|
36153
36271
|
disconnectListeners.forEach((cb) => cb());
|
|
36154
36272
|
}
|
|
36155
|
-
if (identified === "peer" && peerMcpId)
|
|
36156
|
-
peers.
|
|
36273
|
+
if (identified === "peer" && peerMcpId) {
|
|
36274
|
+
if (peers.get(peerMcpId)?.ws === ws)
|
|
36275
|
+
peers.delete(peerMcpId);
|
|
36276
|
+
}
|
|
36157
36277
|
});
|
|
36158
36278
|
});
|
|
36159
36279
|
return {
|
|
@@ -36169,7 +36289,10 @@ async function startHost(opts) {
|
|
|
36169
36289
|
});
|
|
36170
36290
|
}),
|
|
36171
36291
|
sendOwnInner: async (inner) => {
|
|
36172
|
-
const session = await ownSessionReady
|
|
36292
|
+
const session = await awaitSessionReady(ownSessionReady, {
|
|
36293
|
+
mcpId: opts.ownMcpId,
|
|
36294
|
+
pendingPairCode: () => ownPendingPairCode
|
|
36295
|
+
});
|
|
36173
36296
|
if (!extensionWs)
|
|
36174
36297
|
throw new Error("host: no extension connected");
|
|
36175
36298
|
const sealed = await sealInnerFrame(session.sessionKey, opts.ownMcpId, session.nextOutboundSeq(), inner);
|
|
@@ -36274,7 +36397,10 @@ async function startPeer(opts) {
|
|
|
36274
36397
|
ws,
|
|
36275
36398
|
session: sessionPromise,
|
|
36276
36399
|
sendInner: async (inner) => {
|
|
36277
|
-
await sessionPromise
|
|
36400
|
+
await awaitSessionReady(sessionPromise, {
|
|
36401
|
+
mcpId: opts.mcpId,
|
|
36402
|
+
pendingPairCode: () => pendingPairCode
|
|
36403
|
+
});
|
|
36278
36404
|
const s = session;
|
|
36279
36405
|
const sealed = await sealInnerFrame(s.sessionKey, opts.mcpId, s.nextOutboundSeq(), inner);
|
|
36280
36406
|
ws.send(JSON.stringify(sealed));
|
|
@@ -36940,6 +37066,35 @@ var FetchproxyServer = class {
|
|
|
36940
37066
|
this.keepAliveTimer = null;
|
|
36941
37067
|
}
|
|
36942
37068
|
}
|
|
37069
|
+
/**
|
|
37070
|
+
* Send an inner request frame via whichever bridge handle is active. If the
|
|
37071
|
+
* send throws (e.g. `FetchproxySessionNotReadyError` — the session never
|
|
37072
|
+
* confirmed), the frame never reached the bridge, so no reply will arrive:
|
|
37073
|
+
* drop the just-registered pending resolver for this id (it lives in exactly
|
|
37074
|
+
* one of the op maps — request ids are unique) so it doesn't leak until the
|
|
37075
|
+
* server closes, then rethrow.
|
|
37076
|
+
*/
|
|
37077
|
+
async sendInnerFrame(inner) {
|
|
37078
|
+
try {
|
|
37079
|
+
if (this.hostHandle) {
|
|
37080
|
+
await this.hostHandle.sendOwnInner(inner);
|
|
37081
|
+
} else if (this.peerHandle) {
|
|
37082
|
+
await this.peerHandle.sendInner(inner);
|
|
37083
|
+
}
|
|
37084
|
+
} catch (err) {
|
|
37085
|
+
if ("id" in inner && typeof inner.id === "number") {
|
|
37086
|
+
const { id } = inner;
|
|
37087
|
+
this.pending.delete(id);
|
|
37088
|
+
this.pendingReadCookies.delete(id);
|
|
37089
|
+
this.pendingStorage.delete(id);
|
|
37090
|
+
this.pendingCapture.delete(id);
|
|
37091
|
+
this.pendingRedirect.delete(id);
|
|
37092
|
+
this.pendingDownload.delete(id);
|
|
37093
|
+
this.pendingIdb.delete(id);
|
|
37094
|
+
}
|
|
37095
|
+
throw err;
|
|
37096
|
+
}
|
|
37097
|
+
}
|
|
36943
37098
|
/**
|
|
36944
37099
|
* Single bridge round-trip, wrapped by `fetchTimeoutMs` when set.
|
|
36945
37100
|
* On timeout returns the `{ok:false, kind:'timeout'}` envelope —
|
|
@@ -36951,11 +37106,7 @@ var FetchproxyServer = class {
|
|
|
36951
37106
|
const pending = new Promise((resolve) => {
|
|
36952
37107
|
this.pending.set(id, resolve);
|
|
36953
37108
|
});
|
|
36954
|
-
|
|
36955
|
-
await this.hostHandle.sendOwnInner(inner);
|
|
36956
|
-
} else if (this.peerHandle) {
|
|
36957
|
-
await this.peerHandle.sendInner(inner);
|
|
36958
|
-
}
|
|
37109
|
+
await this.sendInnerFrame(inner);
|
|
36959
37110
|
const timeoutMs = this.opts.fetchTimeoutMs;
|
|
36960
37111
|
if (timeoutMs === void 0 || timeoutMs <= 0)
|
|
36961
37112
|
return pending;
|
|
@@ -36984,6 +37135,51 @@ var FetchproxyServer = class {
|
|
|
36984
37135
|
clearTimeout(timer);
|
|
36985
37136
|
}
|
|
36986
37137
|
}
|
|
37138
|
+
/**
|
|
37139
|
+
* FP-B2: bound a non-`fetch` verb's reply wait by `fetchTimeoutMs`.
|
|
37140
|
+
*
|
|
37141
|
+
* Before this, only `fetch()` raced its `pending` reply against a timer
|
|
37142
|
+
* (`_fetchOnceWithTimeout`); `readCookies`, the storage reads,
|
|
37143
|
+
* `capture_request_header`, `capture_redirect`, `download`, and
|
|
37144
|
+
* `read_indexed_db` awaited their `pending` promise with no race, so a
|
|
37145
|
+
* wedged extension hung the tool call indefinitely.
|
|
37146
|
+
*
|
|
37147
|
+
* `pending` is the already-registered reply promise. `pendingMap`/`id`
|
|
37148
|
+
* point at the op-specific map entry so we can drop it on expiry exactly
|
|
37149
|
+
* as `_fetchOnceWithTimeout` does — otherwise a late bridge reply would
|
|
37150
|
+
* resolve into a stale resolver / leak. On timeout we reject with a
|
|
37151
|
+
* `FetchproxyTimeoutError` (the same throwable the convenience methods
|
|
37152
|
+
* already surface for fetch timeouts). `0`/unset opts out (unbounded),
|
|
37153
|
+
* matching the fetch path.
|
|
37154
|
+
*/
|
|
37155
|
+
async _withVerbTimeout(pending, pendingMap, id, url2) {
|
|
37156
|
+
const timeoutMs = this.opts.fetchTimeoutMs;
|
|
37157
|
+
if (timeoutMs === void 0 || timeoutMs <= 0)
|
|
37158
|
+
return pending;
|
|
37159
|
+
let timer;
|
|
37160
|
+
const start = Date.now();
|
|
37161
|
+
try {
|
|
37162
|
+
return await Promise.race([
|
|
37163
|
+
pending,
|
|
37164
|
+
new Promise((_resolve, reject) => {
|
|
37165
|
+
timer = setTimeout(() => {
|
|
37166
|
+
pendingMap.delete(id);
|
|
37167
|
+
reject(new FetchproxyTimeoutError({
|
|
37168
|
+
url: url2,
|
|
37169
|
+
timeoutMs,
|
|
37170
|
+
role: this.role,
|
|
37171
|
+
port: this.opts.port,
|
|
37172
|
+
elapsedMs: Date.now() - start,
|
|
37173
|
+
retryAttempted: false
|
|
37174
|
+
}));
|
|
37175
|
+
}, timeoutMs);
|
|
37176
|
+
})
|
|
37177
|
+
]);
|
|
37178
|
+
} finally {
|
|
37179
|
+
if (timer)
|
|
37180
|
+
clearTimeout(timer);
|
|
37181
|
+
}
|
|
37182
|
+
}
|
|
36987
37183
|
/**
|
|
36988
37184
|
* Map an `ok:false` fetch result to its typed throwable. Centralizes
|
|
36989
37185
|
* the kind-to-error-class switch so `request()` and (via the same
|
|
@@ -37274,12 +37470,8 @@ var FetchproxyServer = class {
|
|
|
37274
37470
|
const pending = new Promise((resolve) => {
|
|
37275
37471
|
this.pendingReadCookies.set(id, resolve);
|
|
37276
37472
|
});
|
|
37277
|
-
|
|
37278
|
-
|
|
37279
|
-
} else if (this.peerHandle) {
|
|
37280
|
-
await this.peerHandle.sendInner(inner);
|
|
37281
|
-
}
|
|
37282
|
-
const result = await pending;
|
|
37473
|
+
await this.sendInnerFrame(inner);
|
|
37474
|
+
const result = await this._withVerbTimeout(pending, this.pendingReadCookies, id, `https://${host}`);
|
|
37283
37475
|
if (!result.ok) {
|
|
37284
37476
|
throw new FetchproxyProtocolError(result.error);
|
|
37285
37477
|
}
|
|
@@ -37345,12 +37537,8 @@ var FetchproxyServer = class {
|
|
|
37345
37537
|
const pending = new Promise((resolve, reject) => {
|
|
37346
37538
|
this.pendingStorage.set(id, { resolve, reject });
|
|
37347
37539
|
});
|
|
37348
|
-
|
|
37349
|
-
|
|
37350
|
-
} else if (this.peerHandle) {
|
|
37351
|
-
await this.peerHandle.sendInner(inner);
|
|
37352
|
-
}
|
|
37353
|
-
return pending;
|
|
37540
|
+
await this.sendInnerFrame(inner);
|
|
37541
|
+
return this._withVerbTimeout(pending, this.pendingStorage, id, `https://${host}`);
|
|
37354
37542
|
}
|
|
37355
37543
|
/**
|
|
37356
37544
|
* 0.3.0+: snapshot the next outgoing request's named header. Single-
|
|
@@ -37454,12 +37642,8 @@ var FetchproxyServer = class {
|
|
|
37454
37642
|
const pending = new Promise((resolve, reject) => {
|
|
37455
37643
|
this.pendingCapture.set(id, { resolve, reject });
|
|
37456
37644
|
});
|
|
37457
|
-
|
|
37458
|
-
|
|
37459
|
-
} else if (this.peerHandle) {
|
|
37460
|
-
await this.peerHandle.sendInner(inner);
|
|
37461
|
-
}
|
|
37462
|
-
return pending;
|
|
37645
|
+
await this.sendInnerFrame(inner);
|
|
37646
|
+
return this._withVerbTimeout(pending, this.pendingCapture, id, `https://${opts.host}${opts.path ?? "/*"}`);
|
|
37463
37647
|
}
|
|
37464
37648
|
/**
|
|
37465
37649
|
* Snapshot the redirect target URL of the next request the browser
|
|
@@ -37543,12 +37727,8 @@ var FetchproxyServer = class {
|
|
|
37543
37727
|
const pending = new Promise((resolve, reject) => {
|
|
37544
37728
|
this.pendingRedirect.set(id, { resolve, reject });
|
|
37545
37729
|
});
|
|
37546
|
-
|
|
37547
|
-
|
|
37548
|
-
} else if (this.peerHandle) {
|
|
37549
|
-
await this.peerHandle.sendInner(inner);
|
|
37550
|
-
}
|
|
37551
|
-
return pending;
|
|
37730
|
+
await this.sendInnerFrame(inner);
|
|
37731
|
+
return this._withVerbTimeout(pending, this.pendingRedirect, id, `https://${opts.host}${opts.path ?? "/*"}`);
|
|
37552
37732
|
}
|
|
37553
37733
|
/**
|
|
37554
37734
|
* Download `url` through the BROWSER's own network stack via
|
|
@@ -37629,12 +37809,8 @@ var FetchproxyServer = class {
|
|
|
37629
37809
|
const pending = new Promise((resolve, reject) => {
|
|
37630
37810
|
this.pendingDownload.set(id, { resolve, reject });
|
|
37631
37811
|
});
|
|
37632
|
-
|
|
37633
|
-
|
|
37634
|
-
} else if (this.peerHandle) {
|
|
37635
|
-
await this.peerHandle.sendInner(inner);
|
|
37636
|
-
}
|
|
37637
|
-
return pending;
|
|
37812
|
+
await this.sendInnerFrame(inner);
|
|
37813
|
+
return this._withVerbTimeout(pending, this.pendingDownload, id, opts.url);
|
|
37638
37814
|
}
|
|
37639
37815
|
/**
|
|
37640
37816
|
* 0.4.0+: read declared IndexedDB keys from the user's signed-in
|
|
@@ -37681,12 +37857,8 @@ var FetchproxyServer = class {
|
|
|
37681
37857
|
const pending = new Promise((resolve, reject) => {
|
|
37682
37858
|
this.pendingIdb.set(id, { resolve, reject });
|
|
37683
37859
|
});
|
|
37684
|
-
|
|
37685
|
-
|
|
37686
|
-
} else if (this.peerHandle) {
|
|
37687
|
-
await this.peerHandle.sendInner(inner);
|
|
37688
|
-
}
|
|
37689
|
-
return pending;
|
|
37860
|
+
await this.sendInnerFrame(inner);
|
|
37861
|
+
return this._withVerbTimeout(pending, this.pendingIdb, id, origin);
|
|
37690
37862
|
}
|
|
37691
37863
|
assertScopeSubset(requested, declared, label) {
|
|
37692
37864
|
const undeclared = undeclaredKeys(requested, declared);
|
|
@@ -38154,7 +38326,7 @@ function loadAccount(env = process.env) {
|
|
|
38154
38326
|
// package.json
|
|
38155
38327
|
var package_default = {
|
|
38156
38328
|
name: "signupgenius-mcp",
|
|
38157
|
-
version: "1.1.
|
|
38329
|
+
version: "1.1.6",
|
|
38158
38330
|
mcpName: "io.github.chrischall/signupgenius-mcp",
|
|
38159
38331
|
description: "SignUpGenius MCP server \u2014 read sign-ups, reports, and groups; add group members.",
|
|
38160
38332
|
author: "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -38191,7 +38363,7 @@ var package_default = {
|
|
|
38191
38363
|
"test:watch": "vitest"
|
|
38192
38364
|
},
|
|
38193
38365
|
dependencies: {
|
|
38194
|
-
"@chrischall/mcp-utils": "^0.
|
|
38366
|
+
"@chrischall/mcp-utils": "^0.12.0",
|
|
38195
38367
|
"@fetchproxy/bootstrap": "^1.0.0",
|
|
38196
38368
|
"@fetchproxy/server": "^1.0.0",
|
|
38197
38369
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
@@ -38199,7 +38371,7 @@ var package_default = {
|
|
|
38199
38371
|
zod: "^4.4.3"
|
|
38200
38372
|
},
|
|
38201
38373
|
devDependencies: {
|
|
38202
|
-
"@types/node": "^
|
|
38374
|
+
"@types/node": "^26.0.0",
|
|
38203
38375
|
"@vitest/coverage-v8": "^4.1.7",
|
|
38204
38376
|
esbuild: "^0.28.0",
|
|
38205
38377
|
typescript: "^6.0.3",
|
|
@@ -38291,18 +38463,30 @@ var CookieSessionManager = class {
|
|
|
38291
38463
|
inFlight;
|
|
38292
38464
|
/** A cached *permanent* config error: once set, every `ensure` rethrows it. */
|
|
38293
38465
|
permanentError;
|
|
38466
|
+
/** When the current session was minted (login) or installed (seed) — for maxAgeMs. */
|
|
38467
|
+
sessionAt = 0;
|
|
38294
38468
|
loginFn;
|
|
38295
38469
|
isExpiredFn;
|
|
38296
38470
|
isPermanentErrorFn;
|
|
38471
|
+
maxAgeMs;
|
|
38472
|
+
now;
|
|
38473
|
+
onReplayLoginErrorFn;
|
|
38297
38474
|
constructor(opts) {
|
|
38298
38475
|
this.loginFn = opts.login;
|
|
38299
|
-
this.isExpiredFn = opts.isExpired;
|
|
38476
|
+
this.isExpiredFn = opts.isExpired ?? (() => false);
|
|
38300
38477
|
this.isPermanentErrorFn = opts.isPermanentError ?? (() => false);
|
|
38478
|
+
this.maxAgeMs = opts.maxAgeMs;
|
|
38479
|
+
this.now = opts.now ?? Date.now;
|
|
38480
|
+
this.onReplayLoginErrorFn = opts.onReplayLoginError;
|
|
38301
38481
|
}
|
|
38302
38482
|
/** The current session, or `undefined` before the first successful login. */
|
|
38303
38483
|
get current() {
|
|
38304
38484
|
return this.session;
|
|
38305
38485
|
}
|
|
38486
|
+
/** True when {@link CookieSessionManagerOptions.maxAgeMs} says the session is too old. */
|
|
38487
|
+
isStale() {
|
|
38488
|
+
return this.maxAgeMs !== void 0 && this.now() - this.sessionAt >= this.maxAgeMs;
|
|
38489
|
+
}
|
|
38306
38490
|
/**
|
|
38307
38491
|
* Return the current session, or single-flight a login if there is none.
|
|
38308
38492
|
*
|
|
@@ -38311,10 +38495,16 @@ var CookieSessionManager = class {
|
|
|
38311
38495
|
* the next call retries. A login error classified permanent by
|
|
38312
38496
|
* {@link CookieSessionManagerOptions.isPermanentError} is cached and rethrown
|
|
38313
38497
|
* on every later call; a transient error is not cached (next call retries).
|
|
38498
|
+
* With {@link CookieSessionManagerOptions.maxAgeMs} set, a session past its
|
|
38499
|
+
* TTL is invalidated first, so the call falls through to a (single-flight)
|
|
38500
|
+
* re-login.
|
|
38314
38501
|
*/
|
|
38315
38502
|
async ensure() {
|
|
38316
|
-
if (this.session !== void 0)
|
|
38317
|
-
|
|
38503
|
+
if (this.session !== void 0) {
|
|
38504
|
+
if (!this.isStale())
|
|
38505
|
+
return this.session;
|
|
38506
|
+
this.invalidate();
|
|
38507
|
+
}
|
|
38318
38508
|
if (this.permanentError !== void 0)
|
|
38319
38509
|
throw this.permanentError;
|
|
38320
38510
|
if (this.inFlight === void 0) {
|
|
@@ -38322,6 +38512,21 @@ var CookieSessionManager = class {
|
|
|
38322
38512
|
}
|
|
38323
38513
|
return this.inFlight;
|
|
38324
38514
|
}
|
|
38515
|
+
/**
|
|
38516
|
+
* Install an externally-minted session (e.g. infinitecampus's CUPS linked-
|
|
38517
|
+
* district discovery, which mints sessions outside {@link
|
|
38518
|
+
* CookieSessionManagerOptions.login}). The seed becomes the current session
|
|
38519
|
+
* with a fresh {@link CookieSessionManagerOptions.maxAgeMs} clock, and any
|
|
38520
|
+
* in-flight login is DETACHED: its waiters still receive its result, but a
|
|
38521
|
+
* late resolution will not overwrite the seed. A cached permanent config
|
|
38522
|
+
* error is left intact — the login path is still misconfigured, and the next
|
|
38523
|
+
* post-seed re-login should keep saying so.
|
|
38524
|
+
*/
|
|
38525
|
+
seed(session) {
|
|
38526
|
+
this.session = session;
|
|
38527
|
+
this.sessionAt = this.now();
|
|
38528
|
+
this.inFlight = void 0;
|
|
38529
|
+
}
|
|
38325
38530
|
/**
|
|
38326
38531
|
* One login attempt. Self-clears `inFlight` on settle so a rejected login
|
|
38327
38532
|
* never sticks, and stamps the resulting session only while it's still the
|
|
@@ -38333,8 +38538,10 @@ var CookieSessionManager = class {
|
|
|
38333
38538
|
holder.p = (async () => {
|
|
38334
38539
|
try {
|
|
38335
38540
|
const session = await this.loginFn();
|
|
38336
|
-
if (this.inFlight === holder.p)
|
|
38541
|
+
if (this.inFlight === holder.p) {
|
|
38337
38542
|
this.session = session;
|
|
38543
|
+
this.sessionAt = this.now();
|
|
38544
|
+
}
|
|
38338
38545
|
return session;
|
|
38339
38546
|
} catch (err) {
|
|
38340
38547
|
if (this.isPermanentErrorFn(err))
|
|
@@ -38366,6 +38573,10 @@ var CookieSessionManager = class {
|
|
|
38366
38573
|
* rather than looping. If the re-login itself fails, the original
|
|
38367
38574
|
* (expired-looking) response is returned so the caller can surface a clean
|
|
38368
38575
|
* sign-in error rather than the login failure.
|
|
38576
|
+
*
|
|
38577
|
+
* `call` resolves to `R` (the generic response type, default {@link Response});
|
|
38578
|
+
* the manager passes it untouched to {@link CookieSessionManagerOptions.isExpired}
|
|
38579
|
+
* and returns it untouched, so a custom transport type flows through cleanly.
|
|
38369
38580
|
*/
|
|
38370
38581
|
async withSession(call) {
|
|
38371
38582
|
const session = await this.ensure();
|
|
@@ -38377,7 +38588,8 @@ var CookieSessionManager = class {
|
|
|
38377
38588
|
let fresh;
|
|
38378
38589
|
try {
|
|
38379
38590
|
fresh = await this.ensure();
|
|
38380
|
-
} catch {
|
|
38591
|
+
} catch (err) {
|
|
38592
|
+
this.onReplayLoginErrorFn?.(err);
|
|
38381
38593
|
return res;
|
|
38382
38594
|
}
|
|
38383
38595
|
return call(fresh);
|
|
@@ -38654,7 +38866,7 @@ async function parseEnvelope(res, context, normalize) {
|
|
|
38654
38866
|
const msg = parsed?.message.join("; ");
|
|
38655
38867
|
if (res.status === 401 || res.status === 403) throw new AuthError(res.status, msg);
|
|
38656
38868
|
if (res.status === 404) throw new Error(`SignUpGenius 404 ${context}`);
|
|
38657
|
-
if (res.status >= 500) throw new UnreachableError(res.status);
|
|
38869
|
+
if (res.status >= 500) throw new UnreachableError("SignUpGenius", res.status);
|
|
38658
38870
|
if (!res.ok) throw new Error(`SignUpGenius ${res.status} ${msg || res.statusText} for ${context}`);
|
|
38659
38871
|
if (!parsed) {
|
|
38660
38872
|
throw new Error(`SignUpGenius returned ${text === "" ? "empty" : "non-JSON"} body for ${context}`);
|
|
@@ -38672,37 +38884,17 @@ function parseJsonBody(text) {
|
|
|
38672
38884
|
return null;
|
|
38673
38885
|
}
|
|
38674
38886
|
}
|
|
38675
|
-
var AuthError = class extends
|
|
38887
|
+
var AuthError = class _AuthError extends McpToolError {
|
|
38676
38888
|
constructor(status, detail) {
|
|
38677
38889
|
super(
|
|
38678
|
-
`SignUpGenius rejected the request (${status}).
|
|
38890
|
+
`SignUpGenius rejected the request (${status}). ` + _AuthError.HINT + (detail ? ` (${detail})` : ""),
|
|
38891
|
+
{ hint: _AuthError.HINT }
|
|
38679
38892
|
);
|
|
38680
38893
|
this.status = status;
|
|
38681
38894
|
this.name = "AuthError";
|
|
38682
38895
|
}
|
|
38683
38896
|
status;
|
|
38684
|
-
|
|
38685
|
-
var UnreachableError = class extends Error {
|
|
38686
|
-
constructor(status) {
|
|
38687
|
-
super(`SignUpGenius unreachable (status ${status})`);
|
|
38688
|
-
this.status = status;
|
|
38689
|
-
this.name = "UnreachableError";
|
|
38690
|
-
}
|
|
38691
|
-
status;
|
|
38692
|
-
};
|
|
38693
|
-
var ModeMismatchError = class extends Error {
|
|
38694
|
-
constructor(currentMode, requiredMode, feature) {
|
|
38695
|
-
super(
|
|
38696
|
-
`${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.")
|
|
38697
|
-
);
|
|
38698
|
-
this.currentMode = currentMode;
|
|
38699
|
-
this.requiredMode = requiredMode;
|
|
38700
|
-
this.feature = feature;
|
|
38701
|
-
this.name = "ModeMismatchError";
|
|
38702
|
-
}
|
|
38703
|
-
currentMode;
|
|
38704
|
-
requiredMode;
|
|
38705
|
-
feature;
|
|
38897
|
+
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.";
|
|
38706
38898
|
};
|
|
38707
38899
|
|
|
38708
38900
|
// src/tools/_shared.ts
|
|
@@ -39195,7 +39387,7 @@ bannerLines.push(
|
|
|
39195
39387
|
);
|
|
39196
39388
|
await runMcp({
|
|
39197
39389
|
name: "signupgenius",
|
|
39198
|
-
version: "1.1.
|
|
39390
|
+
version: "1.1.6",
|
|
39199
39391
|
// x-release-please-version
|
|
39200
39392
|
banner: bannerLines.join("\n"),
|
|
39201
39393
|
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.1.
|
|
50
|
+
version: '1.1.6', // 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.1.
|
|
3
|
+
"version": "1.1.6",
|
|
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.12.0",
|
|
41
41
|
"@fetchproxy/bootstrap": "^1.0.0",
|
|
42
42
|
"@fetchproxy/server": "^1.0.0",
|
|
43
43
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"zod": "^4.4.3"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@types/node": "^
|
|
48
|
+
"@types/node": "^26.0.0",
|
|
49
49
|
"@vitest/coverage-v8": "^4.1.7",
|
|
50
50
|
"esbuild": "^0.28.0",
|
|
51
51
|
"typescript": "^6.0.3",
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/signupgenius-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "1.1.
|
|
9
|
+
"version": "1.1.6",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "signupgenius-mcp",
|
|
14
|
-
"version": "1.1.
|
|
14
|
+
"version": "1.1.6",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|