signupgenius-mcp 1.1.2 → 1.1.3
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/auth-session-login.js +48 -95
- package/dist/bundle.js +667 -138
- package/dist/client.js +97 -58
- package/dist/index.js +1 -1
- package/package.json +2 -2
- 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.3"
|
|
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.3",
|
|
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.3",
|
|
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",
|
|
@@ -9,111 +9,64 @@
|
|
|
9
9
|
* including the scraped csrfToken. On success the server 302s and sets
|
|
10
10
|
* an `accessToken` JWT cookie scoped to .signupgenius.com.
|
|
11
11
|
*
|
|
12
|
+
* The mechanics (CSRF scrape, cookie jar, form POST, success-marker cookie)
|
|
13
|
+
* are delegated to `sessionLoginFlow` from `@chrischall/mcp-utils` — this
|
|
14
|
+
* module was the donor that primitive was extracted from, so only the
|
|
15
|
+
* SUG-specific bits remain here: the form-field names, the static form
|
|
16
|
+
* params, and the `c.Register` failure-redirect classification (the shared
|
|
17
|
+
* flow has no failure-classifier hook, so we sniff the credential POST's
|
|
18
|
+
* `Location` header via the injectable fetch and translate in a catch).
|
|
19
|
+
*
|
|
12
20
|
* SSO accounts (Google/Apple/Facebook/Microsoft) and 2FA-enabled accounts are
|
|
13
21
|
* unsupported — same caveat as canvas-parent-mcp's session mode.
|
|
14
22
|
*/
|
|
23
|
+
import { sessionLoginFlow } from '@chrischall/mcp-utils';
|
|
15
24
|
const DEFAULT_LOGIN_BASE = 'https://www.signupgenius.com';
|
|
16
25
|
export async function sessionLogin(input) {
|
|
17
26
|
const base = input.loginUrl ?? DEFAULT_LOGIN_BASE;
|
|
18
|
-
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
jar.absorb(loginPage.headers);
|
|
27
|
-
const html = await loginPage.text();
|
|
28
|
-
const csrfMatch = /name="csrfToken"\s+value="([^"]+)"/.exec(html);
|
|
29
|
-
if (!csrfMatch) {
|
|
30
|
-
throw new LoginFailedError('csrfToken not found on login page');
|
|
31
|
-
}
|
|
32
|
-
const csrfToken = csrfMatch[1];
|
|
33
|
-
// Step 2: POST credentials
|
|
34
|
-
const body = new URLSearchParams({
|
|
35
|
-
csrfToken,
|
|
36
|
-
loginemail: input.email,
|
|
37
|
-
pword: input.password,
|
|
38
|
-
successpage: 'c.jump&jump=/index.cfm?go=c.MyAccount',
|
|
39
|
-
failpage: 'c.Register',
|
|
40
|
-
ScreenWidth: '2000',
|
|
41
|
-
ScreenHeight: '1200',
|
|
42
|
-
formaction: '1',
|
|
43
|
-
formName: 'loginform',
|
|
44
|
-
refererUrl: '',
|
|
45
|
-
}).toString();
|
|
46
|
-
const postRes = await fetch(`${base}/index.cfm?go=c.Login`, {
|
|
47
|
-
method: 'POST',
|
|
48
|
-
redirect: 'manual',
|
|
49
|
-
headers: {
|
|
50
|
-
'User-Agent': USER_AGENT,
|
|
51
|
-
Accept: 'text/html',
|
|
52
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
53
|
-
Referer: `${base}/login`,
|
|
54
|
-
Cookie: jar.header(),
|
|
55
|
-
},
|
|
56
|
-
body,
|
|
57
|
-
});
|
|
58
|
-
jar.absorb(postRes.headers);
|
|
59
|
-
// A successful login redirects to /index.cfm?go=c.MyAccount. A failed login
|
|
60
|
-
// 302s to /index.cfm?go=c.Register (the configured failpage) or returns 200
|
|
61
|
-
// with the login page rerendered. Either way, no accessToken cookie is set.
|
|
62
|
-
const accessToken = jar.get('accessToken');
|
|
63
|
-
if (!accessToken) {
|
|
64
|
-
const location = postRes.headers.get('location') ?? '';
|
|
65
|
-
const detail = location.includes('c.Register')
|
|
66
|
-
? 'login form rejected the credentials'
|
|
67
|
-
: `login did not yield an accessToken (status ${postRes.status})`;
|
|
68
|
-
throw new LoginFailedError(detail);
|
|
69
|
-
}
|
|
70
|
-
return { accessToken, cookieHeader: jar.header() };
|
|
71
|
-
}
|
|
72
|
-
const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0 Safari/537.36';
|
|
73
|
-
/** Minimal Netscape-cookie-style jar — only tracks name/value, no domain matching. */
|
|
74
|
-
class CookieJar {
|
|
75
|
-
cookies = new Map();
|
|
76
|
-
absorb(headers) {
|
|
77
|
-
const setCookies = readSetCookieHeaders(headers);
|
|
78
|
-
for (const raw of setCookies) {
|
|
79
|
-
const semi = raw.indexOf(';');
|
|
80
|
-
const pair = semi >= 0 ? raw.slice(0, semi) : raw;
|
|
81
|
-
const eq = pair.indexOf('=');
|
|
82
|
-
if (eq <= 0)
|
|
83
|
-
continue;
|
|
84
|
-
const name = pair.slice(0, eq).trim();
|
|
85
|
-
const value = pair.slice(eq + 1).trim();
|
|
86
|
-
if (!name)
|
|
87
|
-
continue;
|
|
88
|
-
this.cookies.set(name, value);
|
|
27
|
+
// A failed login 302s to /index.cfm?go=c.Register (the configured failpage).
|
|
28
|
+
// The shared flow only reports "no accessToken cookie" — record the credential
|
|
29
|
+
// POST's redirect target so we can give the clearer bad-credentials message.
|
|
30
|
+
let postLocation = '';
|
|
31
|
+
const locationRecordingFetch = async (url, init) => {
|
|
32
|
+
const res = await fetch(url, init);
|
|
33
|
+
if (init?.method === 'POST') {
|
|
34
|
+
postLocation = res.headers.get('location') ?? '';
|
|
89
35
|
}
|
|
36
|
+
return res;
|
|
37
|
+
};
|
|
38
|
+
try {
|
|
39
|
+
const { token, cookies } = await sessionLoginFlow({
|
|
40
|
+
loginUrl: `${base}/login`,
|
|
41
|
+
postUrl: `${base}/index.cfm?go=c.Login`,
|
|
42
|
+
csrfRegex: /name="csrfToken"\s+value="([^"]+)"/,
|
|
43
|
+
tokenField: 'accessToken',
|
|
44
|
+
emailField: 'loginemail',
|
|
45
|
+
passwordField: 'pword',
|
|
46
|
+
email: input.email,
|
|
47
|
+
password: input.password,
|
|
48
|
+
extraFields: {
|
|
49
|
+
successpage: 'c.jump&jump=/index.cfm?go=c.MyAccount',
|
|
50
|
+
failpage: 'c.Register',
|
|
51
|
+
ScreenWidth: '2000',
|
|
52
|
+
ScreenHeight: '1200',
|
|
53
|
+
formaction: '1',
|
|
54
|
+
formName: 'loginform',
|
|
55
|
+
refererUrl: '',
|
|
56
|
+
},
|
|
57
|
+
userAgent: USER_AGENT,
|
|
58
|
+
fetchImpl: locationRecordingFetch,
|
|
59
|
+
});
|
|
60
|
+
return { accessToken: token, cookieHeader: cookies };
|
|
90
61
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
.map(([k, v]) => `${k}=${v}`)
|
|
97
|
-
.join('; ');
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Node's Headers can carry multiple Set-Cookie values; Headers.get joins them
|
|
102
|
-
* with ", " which breaks parsing because cookie attributes also use commas
|
|
103
|
-
* (Expires=Wed, 17 Jun 2026 ...). Headers.getSetCookie() is the spec'd way
|
|
104
|
-
* (Node ≥19, undici ≥5.2) — fall back to the single-string getter for older
|
|
105
|
-
* runtimes by splitting only on commas that precede a `name=` token.
|
|
106
|
-
*/
|
|
107
|
-
function readSetCookieHeaders(headers) {
|
|
108
|
-
const anyHeaders = headers;
|
|
109
|
-
if (typeof anyHeaders.getSetCookie === 'function') {
|
|
110
|
-
return anyHeaders.getSetCookie();
|
|
62
|
+
catch (err) {
|
|
63
|
+
if (postLocation.includes('c.Register')) {
|
|
64
|
+
throw new LoginFailedError('login form rejected the credentials');
|
|
65
|
+
}
|
|
66
|
+
throw new LoginFailedError(err instanceof Error ? err.message : String(err));
|
|
111
67
|
}
|
|
112
|
-
const raw = headers.get('set-cookie');
|
|
113
|
-
if (!raw)
|
|
114
|
-
return [];
|
|
115
|
-
return raw.split(/,(?=\s*[A-Za-z0-9_-]+=)/);
|
|
116
68
|
}
|
|
69
|
+
const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0 Safari/537.36';
|
|
117
70
|
export class LoginFailedError extends Error {
|
|
118
71
|
detail;
|
|
119
72
|
constructor(detail) {
|