creditkarma-mcp 2.0.8 → 2.0.10

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/README.md CHANGED
@@ -21,7 +21,7 @@ Ask Claude things like:
21
21
  - [Claude Desktop](https://claude.ai/download) or [Claude Code](https://claude.ai/code)
22
22
  - [Node.js](https://nodejs.org) 18 or later
23
23
  - A Credit Karma account
24
- - [Google Chrome](https://www.google.com/chrome/) — used once for the scripted auth flow (optional; you can copy the cookie manually instead)
24
+ - For the no-env-var path: the [fetchproxy 0.3.0 Chrome / Safari extension](https://github.com/chrischall/fetchproxy)
25
25
 
26
26
  ## Installation
27
27
 
@@ -80,38 +80,31 @@ Fully quit and relaunch. Then ask: *"Sync my Credit Karma transactions"*.
80
80
 
81
81
  Credit Karma uses short-lived JWTs. This server handles automatic token refresh — you only need to set up credentials once (or when your session expires).
82
82
 
83
- ### Getting your credentials
83
+ `creditkarma-mcp` tries three auth paths in priority order; whichever succeeds first is used. Existing setups keep working unchanged.
84
84
 
85
- #### Option A scripted (recommended)
85
+ 1. **`CK_COOKIES` env var (legacy).** Set the full Cookie header in your Claude Desktop config or `.env`. This is the path shown in the config above.
86
+ 2. **Cached session from `ck_set_session`.** Once called, the tool persists the Cookie header to `.env` as `CK_COOKIES` — so subsequent runs collapse into path 1.
87
+ 3. **fetchproxy fallback (no env vars needed — easiest onboarding).** When neither is configured, the server reads `CKAT` + `CKTRKID` cookies once at startup from your already-signed-in `creditkarma.com` tab via the [fetchproxy](https://github.com/chrischall/fetchproxy) browser extension. After that one read, all CK API calls go directly from Node — the extension is **not** in the request hot path. Install the fetchproxy extension (Chrome Web Store / Safari `.dmg`), sign into [creditkarma.com](https://www.creditkarma.com), and the MCP just works.
86
88
 
87
- ```bash
88
- npm run auth # prints the Cookie header to the console
89
- npm run auth -- .env # writes CK_COOKIES=<header> to .env
90
- ```
89
+ Set `CK_DISABLE_FETCHPROXY=1` to opt out of the fallback (turns missing credentials into a hard error — useful in headless CI).
91
90
 
92
- Launches Chrome with a dedicated profile at `~/.creditkarma-mcp/chrome-profile`, waits for you to sign in at creditkarma.com, then captures the full session Cookie header (CKAT carries the access + refresh JWTs; CKTRKID and friends are needed by the refresh endpoint). Either prints it (for pasting into Claude Desktop / MCPB) or writes it to the env file you pass at mode 0600 (owner-only). Requires Google Chrome installed locally; on first run the script installs `puppeteer-core`, `puppeteer-extra`, and `puppeteer-extra-plugin-stealth` (a few MB, not added to `package.json`).
91
+ ### Getting your credentials (env-var path)
93
92
 
94
- #### Option Bmanual paste (secure prompt)
93
+ #### Option Afetchproxy extension (recommended)
95
94
 
96
- ```bash
97
- npm run auth -- --manual # prompts for the cookie, prints CK_COOKIES
98
- npm run auth -- --manual .env # prompts for the cookie, writes to .env
99
- ```
95
+ 1. Install the [fetchproxy 0.3.0 extension](https://github.com/chrischall/fetchproxy) (Chrome Web Store or Safari `.dmg`).
96
+ 2. Sign into [creditkarma.com](https://www.creditkarma.com) in that browser.
97
+ 3. Leave `CK_COOKIES` **unset** in your Claude config.
100
98
 
101
- Use this if the scripted flow hits Intuit/Akamai bot detection (sign-in returns "A technical issue has unexpectedly occurred"). Grab the Cookie header from your normal Chrome (Option C below), then paste it at the prompt. Input is **not echoed** — paste, press Enter.
99
+ The MCP reads the HttpOnly `CKAT` + `CKTRKID` cookies via `chrome.cookies.get` on the first tool call, then operates direct-to-API from Node. To re-auth (e.g. after Credit Karma signs you out), just sign back in to creditkarma.com.
102
100
 
103
- #### Option C — manual (DevTools)
101
+ #### Option B — manual (DevTools)
104
102
 
105
103
  1. Log in to [creditkarma.com](https://www.creditkarma.com) in Chrome
106
104
  2. Open DevTools → **Network** → click any request to creditkarma.com → **Request Headers**
107
105
  3. Right-click the `cookie` header → **Copy value**
108
106
 
109
- ### Setting credentials
110
-
111
- Either of these works:
112
-
113
- - Paste the value from `npm run auth` into `CK_COOKIES` in your `.env` or Claude config
114
- - Or call `ck_set_session` from within Claude with the Cookie header value
107
+ Then either paste into `CK_COOKIES` in your Claude config / `.env`, or call `ck_set_session` from within Claude with the Cookie header value.
115
108
 
116
109
  The server extracts the access and refresh JWTs from the `CKAT` cookie inside the header and refreshes the access token automatically as needed.
117
110
 
@@ -119,7 +112,9 @@ The server extracts the access and refresh JWTs from the `CKAT` cookie inside th
119
112
 
120
113
  - **Access token**: ~15 minutes (auto-refreshed transparently)
121
114
  - **Refresh token**: ~8 hours
122
- - When the refresh token expires, re-run `npm run auth` (or grab a fresh Cookie header from DevTools) and either update `CK_COOKIES` or call `ck_set_session`
115
+ - When the refresh token expires:
116
+ - **fetchproxy path:** sign back into creditkarma.com — the MCP re-reads fresh cookies on the next tool call.
117
+ - **env-var path:** grab a fresh Cookie header from DevTools and update `CK_COOKIES` (or call `ck_set_session`).
123
118
 
124
119
  ## Available tools
125
120
 
@@ -156,14 +151,19 @@ sync_state (key, value)
156
151
 
157
152
  | Env var | Description | Default |
158
153
  |---------|-------------|---------|
159
- | `CK_COOKIES` | Full Cookie header from a signed-in creditkarma.com request | *(required)* |
154
+ | `CK_COOKIES` | Full Cookie header from a signed-in creditkarma.com request | *(unset — falls back to fetchproxy)* |
155
+ | `CK_DISABLE_FETCHPROXY` | Set to `1` to skip the fetchproxy fallback (headless / CI) | *(unset)* |
160
156
  | `CK_DB_PATH` | Path to SQLite database file | `~/.creditkarma-mcp/transactions.db` |
161
157
 
162
158
  ## Troubleshooting
163
159
 
164
- **"TOKEN_EXPIRED"** — your refresh token has expired. Re-run `npm run auth` (or grab a fresh Cookie header) and update `CK_COOKIES` or call `ck_set_session`.
160
+ **"CK auth: set CK_COOKIES, or call the ck_set_session MCP tool, or install the fetchproxy extension…"** — neither auth path is configured. Either fill in `CK_COOKIES` in your Claude config, or install the [fetchproxy extension](https://github.com/chrischall/fetchproxy) and sign into `creditkarma.com` in your browser.
161
+
162
+ **"TOKEN_EXPIRED"** — your refresh token has expired. Sign back into creditkarma.com (fetchproxy path) or grab a fresh Cookie header from DevTools and update `CK_COOKIES` / call `ck_set_session`.
163
+
164
+ **"fetchproxy fallback failed"** — the env-var path wasn't configured and the extension couldn't be reached. Confirm the fetchproxy extension is installed, signed into Credit Karma, and that it's running (open the extension popup). To disable the fallback, set `CK_DISABLE_FETCHPROXY=1`.
165
165
 
166
- **Sync returns 0 transactions** — check that your `CK_COOKIES` value is fresh. The refresh token inside the CKAT cookie expires after ~8 hours.
166
+ **Sync returns 0 transactions** — check that your auth is fresh. The refresh token inside the CKAT cookie expires after ~8 hours.
167
167
 
168
168
  **Tools not appearing** — fully quit and relaunch Claude Desktop. In Claude Code, run `/mcp` to check server status.
169
169
 
@@ -171,9 +171,10 @@ sync_state (key, value)
171
171
 
172
172
  ## Security
173
173
 
174
- - Credentials are stored only in your local `.env` file (gitignored) or Claude config
175
- - `.env` is written at mode 0600 (owner read/write only) by both `npm run auth` and `ck_set_session`
174
+ - Credentials are stored only in your local `.env` file (gitignored), Claude config, or your browser's cookie jar (fetchproxy path)
175
+ - `.env` is written at mode 0600 (owner read/write only) by `ck_set_session`
176
176
  - `ck_set_session` refuses to save a refresh token whose JWT `exp` is already in the past — prevents stale credentials from polluting `.env`
177
+ - The fetchproxy path doesn't persist anything to disk — cookies are read into memory once per MCP run, directly from the user's browser via `chrome.cookies.get`
177
178
  - The server never logs credentials; warnings go to stderr only (stdout is reserved for the MCP JSON-RPC stream)
178
179
  - Only `SELECT` queries are permitted via `ck_query_sql` — no writes to Credit Karma; the underlying `node:sqlite` `prepare()` also rejects multi-statement input
179
180
 
@@ -196,6 +197,7 @@ Changes land via PR, including for solo work — release notes are generated fro
196
197
 
197
198
  ```
198
199
  src/
200
+ auth.ts resolveAuth() — three-path priority (CK_COOKIES env / ck_set_session cache / fetchproxy), plus loadAuthIntoClient()
199
201
  client.ts Credit Karma GraphQL client (auto-refresh, JWT helpers, cookie parser)
200
202
  index.ts MCP server entry point; bootstraps tokens from CK_COOKIES
201
203
  db.ts SQLite schema, migrations, and upsert helpers
@@ -207,13 +209,11 @@ src/
207
209
  ck_get_spending_by_category, ck_get_spending_by_merchant,
208
210
  ck_get_account_summary
209
211
  sql.ts ck_query_sql — SELECT-only escape hatch
210
- scripts/
211
- setup-auth.mjs npm run auth — Puppeteer flow + manual paste fallback
212
212
  tests/
213
213
  helpers.ts Shared test helpers (fakeServer, makeJwt)
214
+ auth.test.ts resolveAuth + loadAuthIntoClient (mocks @fetchproxy/bootstrap)
214
215
  client.test.ts
215
216
  db.test.ts
216
- setup-auth.test.ts
217
217
  tools/
218
218
  auth.test.ts
219
219
  sync.test.ts
package/SKILL.md CHANGED
@@ -56,28 +56,29 @@ Then add to `.mcp.json`:
56
56
 
57
57
  Or use a `.env` file in the project directory with `CK_COOKIES=<value>`.
58
58
 
59
- ### Getting CK_COOKIES
59
+ ### Getting CK_COOKIES (optional)
60
60
 
61
- **Scripted (recommended source install):**
62
- ```bash
63
- npm run auth # prints the Cookie header to the console
64
- npm run auth -- .env # writes CK_COOKIES=<header> to .env
65
- ```
61
+ Three onboarding paths, in priority order:
62
+
63
+ **1. fetchproxy extension (easiest — no env vars):** Install the [fetchproxy 0.3.0 extension](https://github.com/chrischall/fetchproxy), sign into creditkarma.com once, and leave `CK_COOKIES` **unset**. The MCP reads HttpOnly `CKAT` + `CKTRKID` cookies on the first tool call via `chrome.cookies.get`, then operates direct-to-API from Node.
66
64
 
67
- Launches Chrome with a dedicated profile, waits for sign-in at creditkarma.com, then captures the full session Cookie header (CKAT carries the access + refresh JWTs; CKTRKID and friends are needed by the refresh endpoint). Use the printed value with Claude Desktop / MCPB, or the `.env` form when running from source.
65
+ **2. ck_set_session MCP tool:** From within Claude, call `ck_set_session` with a Cookie header you copied from DevTools (see below). The tool persists it to `.env`.
68
66
 
69
- **Manual (DevTools):**
67
+ **3. Manual (DevTools):**
70
68
  1. Log in to [creditkarma.com](https://www.creditkarma.com) in Chrome
71
69
  2. DevTools → **Network** → any creditkarma.com request → **Request Headers**
72
70
  3. Right-click the `cookie` header → **Copy value**
71
+ 4. Paste into `CK_COOKIES` in your Claude config
73
72
 
74
73
  ## Authentication
75
74
 
76
- Call `ck_set_session` with your Cookie header to store credentials and enable auto-refresh.
75
+ The MCP handles auth automatically once any of the three paths is configured.
77
76
 
78
77
  - Access token: ~15 min TTL, auto-refreshed transparently
79
78
  - Refresh token: ~8 hours TTL
80
- - When expired: re-run `npm run auth` (or grab a fresh Cookie header) and call `ck_set_session`
79
+ - When expired:
80
+ - **fetchproxy path:** sign back into creditkarma.com — the MCP reads fresh cookies on the next tool call
81
+ - **env-var / ck_set_session path:** grab a fresh Cookie header from DevTools and update `CK_COOKIES` (or call `ck_set_session` again)
81
82
 
82
83
  ## Tools
83
84
 
@@ -104,8 +105,8 @@ Call `ck_set_session` with your Cookie header to store credentials and enable au
104
105
  ## Workflows
105
106
 
106
107
  **First-time setup:**
107
- 1. Run `npm run auth` (or grab the Cookie header manually from a creditkarma.com request in DevTools)
108
- 2. Paste into `CK_COOKIES` env var, or call `ck_set_session(cookies)` from within Claude
108
+ 1. Easiest: install the [fetchproxy extension](https://github.com/chrischall/fetchproxy), sign into creditkarma.com, leave `CK_COOKIES` unset.
109
+ 2. Or: copy the Cookie header from DevTools and either set `CK_COOKIES` in your config or call `ck_set_session(cookies)` from within Claude.
109
110
  3. `ck_sync_transactions` → initial full sync
110
111
 
111
112
  **Regular use:**
package/dist/auth.js ADDED
@@ -0,0 +1,198 @@
1
+ // ────────────────────────────────────────────────────────────────────────────
2
+ // Auth resolution — Pattern A template
3
+ // ────────────────────────────────────────────────────────────────────────────
4
+ //
5
+ // Mirrors the canonical "browser-bootstrap + Node-direct" shape from
6
+ // ofw-mcp/src/auth.ts. Other MCPs in this family (resy-mcp, opentable-mcp,
7
+ // signupgenius-mcp, zola-mcp, …) use the same selector — keep the structure
8
+ // flat, the path-selection explicit, and the error messages actionable.
9
+ //
10
+ // THE PATHS, in priority order:
11
+ //
12
+ // 1. CK_COOKIES env var (existing behavior)
13
+ // A full Cookie header (e.g. `CKTRKID=...; CKAT=eyJ...%3BeyJ...; ...`)
14
+ // from a signed-in creditkarma.com request. The CKAT cookie contains
15
+ // `<accessJWT>%3B<refreshJWT>` URL-encoded, which the caller parses.
16
+ // Legacy users keep working without action.
17
+ //
18
+ // 2. Cached session from `ck_set_session` (existing behavior)
19
+ // The MCP tool `ck_set_session` accepts a pasted Cookie header and
20
+ // persists it to .env as CK_COOKIES — so once it's been called, this
21
+ // path collapses into path 1 on subsequent runs.
22
+ //
23
+ // 3. fetchproxy fallback (new)
24
+ // When no Cookie header is set, lift the user's session out of their
25
+ // signed-in creditkarma.com browser tab via the fetchproxy 0.3.0
26
+ // extension. `@fetchproxy/bootstrap` spins up a one-shot WebSocket
27
+ // bridge, asks the extension for the `CKAT` and `CKTRKID` cookies via
28
+ // `chrome.cookies.get`, then closes the bridge. The synthesized
29
+ // Cookie header has the same shape that ck_set_session produces, so
30
+ // the rest of the stack consumes it without branching.
31
+ //
32
+ // All subsequent API calls go out via plain Node `fetch()` —
33
+ // fetchproxy is NOT in the request hot path. Token refresh
34
+ // (`POST /member/oauth2/refresh`) is also a plain Node fetch.
35
+ //
36
+ // Users opt out with CK_DISABLE_FETCHPROXY=1 (anyone who wants the
37
+ // old behavior of "fail loudly when creds are missing").
38
+ //
39
+ // 4. Error
40
+ // Nothing to authenticate with. We throw a message that names all
41
+ // three onboarding paths so the user can pick whichever fits.
42
+ //
43
+ // Why fetchproxy is only a one-shot read:
44
+ // The bootstrap call snapshots the CKAT + CKTRKID cookies and returns.
45
+ // The MCP then operates from Node with direct fetch — latency and
46
+ // reliability are not coupled to the browser bridge for normal tool
47
+ // calls. If the access JWT inside CKAT expires, the refresh flow runs
48
+ // in pure Node against `creditkarma.com/member/oauth2/refresh`. If
49
+ // that 403s (Akamai gate / expired refresh JWT), the user re-signs into
50
+ // creditkarma.com in the browser and the next MCP run re-reads the
51
+ // fresh cookies.
52
+ //
53
+ // Testability:
54
+ // - `@fetchproxy/bootstrap` is mocked at the module boundary in tests.
55
+ // - This module exposes a single async `resolveAuth()` that returns a
56
+ // Cookie header string + a source label. Callers treat the cookies
57
+ // value as opaque — the existing parser in `src/index.ts` /
58
+ // `src/tools/auth.ts` extracts the CKAT JWTs the same way it does
59
+ // today.
60
+ import { bootstrap } from '@fetchproxy/bootstrap';
61
+ import pkg from '../package.json' with { type: 'json' };
62
+ import { extractCookieValue } from './client.js';
63
+ /**
64
+ * Read an env var, trim, and treat blank / `${UNEXPANDED}` placeholders as
65
+ * unset. Defends against MCP hosts that pass `.mcp.json` env blocks through
66
+ * without variable expansion.
67
+ */
68
+ function readEnv(key) {
69
+ const raw = process.env[key];
70
+ if (typeof raw !== 'string')
71
+ return undefined;
72
+ const trimmed = raw.trim();
73
+ if (trimmed.length === 0)
74
+ return undefined;
75
+ if (trimmed === 'undefined' || trimmed === 'null')
76
+ return undefined;
77
+ if (/^\$\{[^}]*\}$/.test(trimmed))
78
+ return undefined;
79
+ return trimmed;
80
+ }
81
+ /** True if the user has explicitly disabled the fetchproxy fallback. */
82
+ function fetchproxyDisabled() {
83
+ const raw = readEnv('CK_DISABLE_FETCHPROXY');
84
+ if (raw === undefined)
85
+ return false;
86
+ return ['1', 'true', 'yes', 'on'].includes(raw.toLowerCase());
87
+ }
88
+ /**
89
+ * Resolve CK auth using the path priority described at the top of this
90
+ * file. Throws with an actionable error message when no path succeeds.
91
+ *
92
+ * Callers should treat the return value as opaque credentials — they
93
+ * should not branch on `source`. The field exists for logging / future
94
+ * cache-keying only.
95
+ */
96
+ export async function resolveAuth() {
97
+ // ── Path 1: CK_COOKIES env var (unchanged from pre-fetchproxy behavior).
98
+ const envCookies = readEnv('CK_COOKIES');
99
+ if (envCookies) {
100
+ return { cookies: envCookies, source: 'env' };
101
+ }
102
+ // ── Path 2: fetchproxy fallback (new).
103
+ // (Path 2 — cached session from ck_set_session — also lands here at the
104
+ // env-var step on subsequent runs, since that tool writes CK_COOKIES
105
+ // to .env. So this branch only fires when neither env var nor cache
106
+ // has been seeded.)
107
+ if (!fetchproxyDisabled()) {
108
+ try {
109
+ const session = await bootstrap({
110
+ serverName: pkg.name,
111
+ version: pkg.version,
112
+ // CK serves www.creditkarma.com (web) and api.creditkarma.com
113
+ // (GraphQL). Both share the apex domain; the extension matches on
114
+ // suffix so listing the apex covers any subdomain.
115
+ domains: ['creditkarma.com'],
116
+ declare: {
117
+ // CKAT contains the access + refresh JWTs joined by `%3B`. CKTRKID
118
+ // is sent as the `ck-cookie-id` header on refresh requests; without
119
+ // it the refresh endpoint 403s. Both are HttpOnly — invisible to
120
+ // page JS — but fetchproxy 0.3.0's `read_cookies` uses
121
+ // `chrome.cookies.get` which sees HttpOnly cookies.
122
+ cookies: ['CKAT', 'CKTRKID'],
123
+ localStorage: [],
124
+ sessionStorage: [],
125
+ captureHeaders: [],
126
+ },
127
+ });
128
+ const ckat = session.cookies['CKAT'];
129
+ const cktrkid = session.cookies['CKTRKID'];
130
+ if (!ckat) {
131
+ throw new Error('CKAT cookie missing on creditkarma.com. ' +
132
+ 'Sign into creditkarma.com in your browser (with the fetchproxy extension installed) and retry.');
133
+ }
134
+ if (!cktrkid) {
135
+ throw new Error('CKTRKID cookie missing on creditkarma.com. ' +
136
+ 'Sign into creditkarma.com in your browser (with the fetchproxy extension installed) and retry.');
137
+ }
138
+ // Synthesize a Cookie header identical in shape to what `ck_set_session`
139
+ // accepts. The existing parser in `src/index.ts` / `src/tools/auth.ts`
140
+ // extracts CKAT and splits its `<accessJWT>%3B<refreshJWT>` payload
141
+ // without caring how the header was assembled.
142
+ const cookies = `CKTRKID=${cktrkid}; CKAT=${ckat}`;
143
+ return { cookies, source: 'fetchproxy' };
144
+ }
145
+ catch (e) {
146
+ const msg = e instanceof Error ? e.message : String(e);
147
+ throw new Error(`CK auth: no CK_COOKIES set, and fetchproxy fallback failed: ${msg}`);
148
+ }
149
+ }
150
+ // ── Path 4: nothing configured. Surface all three fixes side-by-side so
151
+ // the user can pick whichever fits their setup.
152
+ throw new Error('CK auth: set CK_COOKIES, ' +
153
+ 'or call the ck_set_session MCP tool with a Cookie header, ' +
154
+ 'or install the fetchproxy extension and sign into creditkarma.com ' +
155
+ '(unset CK_DISABLE_FETCHPROXY if it is set).');
156
+ }
157
+ /**
158
+ * Parse a Cookie header into the CK_COOKIES → (accessToken, refreshToken)
159
+ * shape, mirroring `src/index.ts` and `src/tools/auth.ts`. The CKAT cookie
160
+ * value is `<accessJWT>%3B<refreshJWT>` URL-encoded; we split on either
161
+ * the encoded or literal semicolon.
162
+ *
163
+ * Exported so both `src/index.ts` (startup) and `loadAuthIntoClient()`
164
+ * (lazy bootstrap) can share one parser. Returns nulls (not errors) when
165
+ * the input doesn't contain a CKAT — the caller decides whether absence
166
+ * is fatal.
167
+ */
168
+ export function parseCookieHeader(cookies) {
169
+ const ckat = extractCookieValue(cookies, 'CKAT') ?? cookies.trim();
170
+ const parts = ckat.replace('%3B', ';').split(';');
171
+ const accessToken = parts[0]?.trim() || null;
172
+ const refreshToken = parts[1]?.trim() || null;
173
+ return { accessToken, refreshToken };
174
+ }
175
+ /**
176
+ * Resolve CK auth via `resolveAuth()` and apply the result to a client.
177
+ *
178
+ * Used by tool handlers on the first request that needs auth but finds no
179
+ * credentials on the client — i.e. the user didn't set CK_COOKIES, didn't
180
+ * call ck_set_session, and the fetchproxy extension is the last hope.
181
+ *
182
+ * If `resolveAuth()` lands on the env-var path (path 1) the cookies are
183
+ * applied with no network round-trip. If it lands on fetchproxy (path 3)
184
+ * the bootstrap call snapshots the browser session once; afterwards the
185
+ * client has fresh CKAT + CKTRKID and the normal `refreshAccessToken()`
186
+ * flow takes over.
187
+ */
188
+ export async function loadAuthIntoClient(client) {
189
+ const { cookies } = await resolveAuth();
190
+ const { accessToken, refreshToken } = parseCookieHeader(cookies);
191
+ if (!accessToken) {
192
+ throw new Error('CK auth: resolved cookies did not contain a CKAT token.');
193
+ }
194
+ client.setToken(accessToken);
195
+ if (refreshToken)
196
+ client.setRefreshToken(refreshToken);
197
+ client.setCookies(cookies);
198
+ }