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.
@@ -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.2"
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.2",
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.2",
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
+ [![CI](https://github.com/chrischall/signupgenius-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/chrischall/signupgenius-mcp/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/signupgenius-mcp)](https://www.npmjs.com/package/signupgenius-mcp)
5
+ [![license](https://img.shields.io/npm/l/signupgenius-mcp)](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
- const jar = new CookieJar();
19
- // Step 1: fetch login page, extract csrfToken
20
- const loginPage = await fetch(`${base}/login`, {
21
- headers: { 'User-Agent': USER_AGENT, Accept: 'text/html' },
22
- });
23
- if (!loginPage.ok) {
24
- throw new LoginFailedError(`login page returned ${loginPage.status}`);
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
- get(name) {
92
- return this.cookies.get(name);
93
- }
94
- header() {
95
- return Array.from(this.cookies.entries())
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) {