signupgenius-mcp 1.1.2 → 1.1.4
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/LICENSE +21 -0
- package/README.md +4 -0
- 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.4"
|
|
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.4",
|
|
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.4",
|
|
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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Chris Hall
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# signupgenius-mcp
|
|
2
2
|
|
|
3
|
+
[](https://github.com/chrischall/signupgenius-mcp/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/signupgenius-mcp)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
3
7
|
MCP server for [SignUpGenius](https://www.signupgenius.com). 13 read tools and 1 write across profile, groups, sign-ups, and reports.
|
|
4
8
|
|
|
5
9
|
Three auth modes (tried in this priority order — first match wins):
|
|
@@ -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) {
|