@striderlabs/mcp-chase 1.0.0

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 ADDED
@@ -0,0 +1,100 @@
1
+ # @striderlabs/mcp-chase
2
+
3
+ MCP server for Chase Bank - let AI agents check accounts, view transactions, and manage finances.
4
+
5
+ Built by [Strider Labs](https://striderlabs.ai) for the agentic commerce era.
6
+
7
+ ## Features
8
+
9
+ - **Accounts** - View all checking, savings, credit, investment, and loan accounts
10
+ - **Balances** - Get current and available balances
11
+ - **Transactions** - View recent transaction history
12
+ - **Bills** - See payees and scheduled payments
13
+ - **Transfers** - View transfer history and preview new transfers
14
+ - **Statements** - Access account statements
15
+ - **Rewards** - Check Ultimate Rewards points and cash back
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install @striderlabs/mcp-chase
21
+ ```
22
+
23
+ ## Usage with Claude Desktop
24
+
25
+ Add to your `claude_desktop_config.json`:
26
+
27
+ ```json
28
+ {
29
+ "mcpServers": {
30
+ "chase": {
31
+ "command": "npx",
32
+ "args": ["@striderlabs/mcp-chase"]
33
+ }
34
+ }
35
+ }
36
+ ```
37
+
38
+ ## Authentication
39
+
40
+ This connector uses browser automation and requires stored cookies:
41
+
42
+ 1. Log in to Chase in your browser
43
+ 2. Export cookies to `~/.strider/chase/cookies.json`
44
+ 3. Use a browser extension like "Cookie-Editor" to export cookies in JSON format
45
+ 4. Note: Chase uses MFA - you may need to re-export cookies periodically
46
+
47
+ ## Security Notes
48
+
49
+ - This connector operates in **read-only mode** for sensitive operations
50
+ - Transfers and bill payments provide **preview only** with manual completion instructions
51
+ - No actual financial transactions are executed by this connector
52
+ - All credentials are stored locally in `~/.strider/chase/`
53
+
54
+ ## Available Tools
55
+
56
+ | Tool | Description |
57
+ |------|-------------|
58
+ | `chase_auth_check` | Check login status |
59
+ | `chase_auth_clear` | Clear stored session |
60
+ | `chase_accounts` | List all accounts |
61
+ | `chase_balance` | Get account balance |
62
+ | `chase_transactions` | Get transaction history |
63
+ | `chase_bills` | View bill pay payees |
64
+ | `chase_transfers` | View transfer history |
65
+ | `chase_statements` | Get account statements |
66
+ | `chase_rewards` | Check rewards balance |
67
+ | `chase_transfer_preview` | Preview a transfer (manual completion) |
68
+ | `chase_bill_pay_preview` | Preview a bill payment (manual completion) |
69
+
70
+ ## Example
71
+
72
+ ```typescript
73
+ // Check if logged in
74
+ const auth = await chase_auth_check();
75
+
76
+ // Get all accounts
77
+ const accounts = await chase_accounts();
78
+
79
+ // Get transactions for an account
80
+ const txns = await chase_transactions({ accountId: accounts[0].id, limit: 10 });
81
+
82
+ // Check rewards
83
+ const rewards = await chase_rewards();
84
+ ```
85
+
86
+ ## Requirements
87
+
88
+ - Node.js 18+
89
+ - Chase bank account
90
+ - Stored session cookies
91
+
92
+ ## License
93
+
94
+ MIT - Strider Labs
95
+
96
+ ## Links
97
+
98
+ - [Strider Labs](https://striderlabs.ai)
99
+ - [GitHub](https://github.com/markswendsen-code/mcp-chase)
100
+ - [npm](https://www.npmjs.com/package/@striderlabs/mcp-chase)
package/dist/auth.js ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Chase Bank Authentication Utilities
3
+ *
4
+ * Manages cookie storage and session persistence.
5
+ */
6
+ import * as fs from "fs";
7
+ import * as path from "path";
8
+ import * as os from "os";
9
+ const COOKIES_DIR = path.join(os.homedir(), ".strider", "chase");
10
+ const COOKIES_PATH = path.join(COOKIES_DIR, "cookies.json");
11
+ /**
12
+ * Get the path to the cookies file
13
+ */
14
+ export function getCookiesPath() {
15
+ return COOKIES_PATH;
16
+ }
17
+ /**
18
+ * Check if cookies file exists
19
+ */
20
+ export function hasStoredCookies() {
21
+ return fs.existsSync(COOKIES_PATH);
22
+ }
23
+ /**
24
+ * Save cookies to disk
25
+ */
26
+ export async function saveCookies(context) {
27
+ try {
28
+ if (!fs.existsSync(COOKIES_DIR)) {
29
+ fs.mkdirSync(COOKIES_DIR, { recursive: true });
30
+ }
31
+ const cookies = await context.cookies();
32
+ fs.writeFileSync(COOKIES_PATH, JSON.stringify(cookies, null, 2));
33
+ }
34
+ catch (error) {
35
+ console.error("Failed to save cookies:", error);
36
+ }
37
+ }
38
+ /**
39
+ * Load cookies from disk into context
40
+ */
41
+ export async function loadCookies(context) {
42
+ try {
43
+ if (fs.existsSync(COOKIES_PATH)) {
44
+ const cookies = JSON.parse(fs.readFileSync(COOKIES_PATH, "utf-8"));
45
+ await context.addCookies(cookies);
46
+ }
47
+ }
48
+ catch (error) {
49
+ console.error("Failed to load cookies:", error);
50
+ }
51
+ }
52
+ /**
53
+ * Clear stored cookies
54
+ */
55
+ export function clearCookies() {
56
+ try {
57
+ if (fs.existsSync(COOKIES_PATH)) {
58
+ fs.unlinkSync(COOKIES_PATH);
59
+ }
60
+ }
61
+ catch (error) {
62
+ console.error("Failed to clear cookies:", error);
63
+ }
64
+ }
65
+ /**
66
+ * Get current auth state
67
+ */
68
+ export function getAuthState() {
69
+ return {
70
+ isLoggedIn: hasStoredCookies(),
71
+ };
72
+ }
@@ -0,0 +1,394 @@
1
+ /**
2
+ * Chase Bank Browser Automation
3
+ *
4
+ * Patchright-based stealth automation for Chase banking operations.
5
+ */
6
+ import { chromium } from "patchright";
7
+ import { saveCookies, loadCookies } from "./auth.js";
8
+ const CHASE_BASE_URL = "https://www.chase.com";
9
+ const DEFAULT_TIMEOUT = 60000;
10
+ // Singleton browser instance
11
+ let browser = null;
12
+ let context = null;
13
+ let page = null;
14
+ /**
15
+ * Initialize browser with stealth settings
16
+ */
17
+ async function initBrowser() {
18
+ if (browser)
19
+ return;
20
+ browser = await chromium.launch({
21
+ headless: true,
22
+ args: [
23
+ "--disable-blink-features=AutomationControlled",
24
+ "--no-sandbox",
25
+ "--disable-setuid-sandbox",
26
+ "--disable-web-security",
27
+ "--disable-features=IsolateOrigins,site-per-process",
28
+ ],
29
+ });
30
+ context = await browser.newContext({
31
+ userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
32
+ viewport: { width: 1280, height: 800 },
33
+ locale: "en-US",
34
+ timezoneId: "America/Los_Angeles",
35
+ });
36
+ // Load saved cookies
37
+ await loadCookies(context);
38
+ page = await context.newPage();
39
+ page.setDefaultTimeout(DEFAULT_TIMEOUT);
40
+ }
41
+ /**
42
+ * Get the current page, initializing if needed
43
+ */
44
+ async function getPage() {
45
+ await initBrowser();
46
+ if (!page)
47
+ throw new Error("Failed to initialize browser");
48
+ return page;
49
+ }
50
+ /**
51
+ * Clean up browser resources
52
+ */
53
+ export async function cleanup() {
54
+ if (context) {
55
+ await saveCookies(context);
56
+ }
57
+ if (browser) {
58
+ await browser.close();
59
+ browser = null;
60
+ context = null;
61
+ page = null;
62
+ }
63
+ }
64
+ /**
65
+ * Get Chase login URL and instructions
66
+ */
67
+ export async function getLoginUrl() {
68
+ return {
69
+ url: "https://secure.chase.com/web/auth/dashboard",
70
+ instructions: "Please log in to Chase in a browser, then export your cookies to ~/.strider/chase/cookies.json. Use a browser extension like 'Cookie-Editor' to export cookies in JSON format. Note: Chase uses MFA, so you may need to re-export cookies after each session.",
71
+ };
72
+ }
73
+ /**
74
+ * Check authentication status
75
+ */
76
+ export async function checkAuth() {
77
+ try {
78
+ const p = await getPage();
79
+ await p.goto(`${CHASE_BASE_URL}/web/auth/dashboard`, { waitUntil: "networkidle" });
80
+ // Check if we're on the dashboard or redirected to login
81
+ const currentUrl = p.url();
82
+ if (currentUrl.includes("/auth/logon") || currentUrl.includes("/login")) {
83
+ return { isLoggedIn: false };
84
+ }
85
+ // Try to get account holder name
86
+ const holderName = await p.$eval('.mds-header-user-name, .account-holder-name, [data-testid="user-greeting"]', (el) => el.textContent?.trim() || "").catch(() => "");
87
+ return {
88
+ isLoggedIn: true,
89
+ accountHolder: holderName || undefined,
90
+ };
91
+ }
92
+ catch (error) {
93
+ return { isLoggedIn: false };
94
+ }
95
+ }
96
+ /**
97
+ * Get all accounts
98
+ */
99
+ export async function getAccounts() {
100
+ try {
101
+ const p = await getPage();
102
+ await p.goto(`${CHASE_BASE_URL}/web/auth/dashboard`, { waitUntil: "networkidle" });
103
+ await p.waitForTimeout(2000);
104
+ const accounts = await p.$$eval('.account-tile, .account-card, [data-testid="account-tile"]', (elements) => elements.map((el, index) => {
105
+ const nameEl = el.querySelector('.account-name, .tile-header, h3');
106
+ const balanceEl = el.querySelector('.account-balance, .balance, [data-testid="balance"]');
107
+ const lastFourEl = el.querySelector('.account-last-four, .masked-number');
108
+ const typeEl = el.querySelector('.account-type');
109
+ const name = nameEl?.textContent?.trim() || `Account ${index + 1}`;
110
+ const balanceText = balanceEl?.textContent?.trim() || '0';
111
+ const balance = parseFloat(balanceText.replace(/[$,]/g, '')) || 0;
112
+ const lastFour = lastFourEl?.textContent?.trim().replace(/[^0-9]/g, '').slice(-4) || undefined;
113
+ let type = 'checking';
114
+ const typeText = (typeEl?.textContent || name).toLowerCase();
115
+ if (typeText.includes('saving'))
116
+ type = 'savings';
117
+ else if (typeText.includes('credit') || typeText.includes('card'))
118
+ type = 'credit';
119
+ else if (typeText.includes('invest') || typeText.includes('brokerage'))
120
+ type = 'investment';
121
+ else if (typeText.includes('loan') || typeText.includes('mortgage') || typeText.includes('auto'))
122
+ type = 'loan';
123
+ return {
124
+ id: el.getAttribute('data-account-id') || `account-${index}`,
125
+ name,
126
+ type,
127
+ balance,
128
+ lastFour,
129
+ };
130
+ }));
131
+ return { success: true, accounts };
132
+ }
133
+ catch (error) {
134
+ return {
135
+ success: false,
136
+ error: error instanceof Error ? error.message : "Failed to get accounts",
137
+ };
138
+ }
139
+ }
140
+ /**
141
+ * Get account transactions
142
+ */
143
+ export async function getTransactions(accountId, limit = 25) {
144
+ try {
145
+ const p = await getPage();
146
+ // Navigate to account details
147
+ await p.goto(`${CHASE_BASE_URL}/web/auth/dashboard`, { waitUntil: "networkidle" });
148
+ await p.waitForTimeout(1000);
149
+ // Click on the account
150
+ const accountTile = await p.$(`[data-account-id="${accountId}"], .account-tile:nth-child(${parseInt(accountId.replace('account-', '')) + 1})`);
151
+ if (accountTile) {
152
+ await accountTile.click();
153
+ await p.waitForNavigation({ waitUntil: "networkidle" }).catch(() => { });
154
+ }
155
+ await p.waitForTimeout(2000);
156
+ const transactions = await p.$$eval('.transaction-row, .activity-row, [data-testid="transaction"]', (elements, maxItems) => elements.slice(0, maxItems).map((el, index) => {
157
+ const dateEl = el.querySelector('.transaction-date, .date, td:first-child');
158
+ const descEl = el.querySelector('.transaction-description, .description, .merchant-name');
159
+ const amountEl = el.querySelector('.transaction-amount, .amount');
160
+ const pendingEl = el.querySelector('.pending, .pending-badge');
161
+ const categoryEl = el.querySelector('.category, .transaction-category');
162
+ const amountText = amountEl?.textContent?.trim() || '0';
163
+ const amount = Math.abs(parseFloat(amountText.replace(/[$,]/g, ''))) || 0;
164
+ const isCredit = amountText.includes('+') || el.classList.contains('credit');
165
+ return {
166
+ id: el.getAttribute('data-transaction-id') || `txn-${index}`,
167
+ date: dateEl?.textContent?.trim() || '',
168
+ description: descEl?.textContent?.trim() || 'Unknown',
169
+ amount,
170
+ type: isCredit ? 'credit' : 'debit',
171
+ category: categoryEl?.textContent?.trim() || undefined,
172
+ pending: !!pendingEl,
173
+ };
174
+ }), limit);
175
+ return { success: true, transactions };
176
+ }
177
+ catch (error) {
178
+ return {
179
+ success: false,
180
+ error: error instanceof Error ? error.message : "Failed to get transactions",
181
+ };
182
+ }
183
+ }
184
+ /**
185
+ * Get account balance
186
+ */
187
+ export async function getBalance(accountId) {
188
+ try {
189
+ const accountsResult = await getAccounts();
190
+ if (!accountsResult.success || !accountsResult.accounts) {
191
+ return { success: false, error: "Failed to get accounts" };
192
+ }
193
+ const account = accountsResult.accounts.find(a => a.id === accountId);
194
+ if (!account) {
195
+ return { success: false, error: `Account not found: ${accountId}` };
196
+ }
197
+ return {
198
+ success: true,
199
+ balance: account.balance,
200
+ availableBalance: account.availableBalance || account.balance,
201
+ };
202
+ }
203
+ catch (error) {
204
+ return {
205
+ success: false,
206
+ error: error instanceof Error ? error.message : "Failed to get balance",
207
+ };
208
+ }
209
+ }
210
+ /**
211
+ * Get bills/payees
212
+ */
213
+ export async function getBills() {
214
+ try {
215
+ const p = await getPage();
216
+ await p.goto(`${CHASE_BASE_URL}/web/auth/billpay`, { waitUntil: "networkidle" });
217
+ await p.waitForTimeout(2000);
218
+ const bills = await p.$$eval('.payee-row, .bill-row, [data-testid="payee"]', (elements) => elements.map((el, index) => {
219
+ const payeeEl = el.querySelector('.payee-name, .name');
220
+ const amountEl = el.querySelector('.amount, .payment-amount');
221
+ const dueDateEl = el.querySelector('.due-date, .date');
222
+ const statusEl = el.querySelector('.status, .payment-status');
223
+ const autopayEl = el.querySelector('.autopay, .auto-pay-enabled');
224
+ const amountText = amountEl?.textContent?.trim() || '0';
225
+ const amount = parseFloat(amountText.replace(/[$,]/g, '')) || 0;
226
+ let status = 'scheduled';
227
+ const statusText = statusEl?.textContent?.toLowerCase() || '';
228
+ if (statusText.includes('paid'))
229
+ status = 'paid';
230
+ else if (statusText.includes('overdue') || statusText.includes('past due'))
231
+ status = 'overdue';
232
+ return {
233
+ id: el.getAttribute('data-payee-id') || `bill-${index}`,
234
+ payee: payeeEl?.textContent?.trim() || 'Unknown Payee',
235
+ amount,
236
+ dueDate: dueDateEl?.textContent?.trim() || '',
237
+ status,
238
+ autopay: !!autopayEl,
239
+ };
240
+ }));
241
+ return { success: true, bills };
242
+ }
243
+ catch (error) {
244
+ return {
245
+ success: false,
246
+ error: error instanceof Error ? error.message : "Failed to get bills",
247
+ };
248
+ }
249
+ }
250
+ /**
251
+ * Get transfer history
252
+ */
253
+ export async function getTransfers() {
254
+ try {
255
+ const p = await getPage();
256
+ await p.goto(`${CHASE_BASE_URL}/web/auth/transfer`, { waitUntil: "networkidle" });
257
+ await p.waitForTimeout(2000);
258
+ const transfers = await p.$$eval('.transfer-row, .activity-row, [data-testid="transfer"]', (elements) => elements.slice(0, 20).map((el, index) => {
259
+ const fromEl = el.querySelector('.from-account, .source');
260
+ const toEl = el.querySelector('.to-account, .destination');
261
+ const amountEl = el.querySelector('.amount');
262
+ const dateEl = el.querySelector('.date, .transfer-date');
263
+ const statusEl = el.querySelector('.status');
264
+ const amountText = amountEl?.textContent?.trim() || '0';
265
+ const amount = parseFloat(amountText.replace(/[$,]/g, '')) || 0;
266
+ let status = 'completed';
267
+ const statusText = statusEl?.textContent?.toLowerCase() || '';
268
+ if (statusText.includes('pending'))
269
+ status = 'pending';
270
+ else if (statusText.includes('failed') || statusText.includes('cancelled'))
271
+ status = 'failed';
272
+ return {
273
+ id: el.getAttribute('data-transfer-id') || `transfer-${index}`,
274
+ fromAccount: fromEl?.textContent?.trim() || 'Unknown',
275
+ toAccount: toEl?.textContent?.trim() || 'Unknown',
276
+ amount,
277
+ date: dateEl?.textContent?.trim() || '',
278
+ status,
279
+ };
280
+ }));
281
+ return { success: true, transfers };
282
+ }
283
+ catch (error) {
284
+ return {
285
+ success: false,
286
+ error: error instanceof Error ? error.message : "Failed to get transfers",
287
+ };
288
+ }
289
+ }
290
+ /**
291
+ * Get statements
292
+ */
293
+ export async function getStatements(accountId) {
294
+ try {
295
+ const p = await getPage();
296
+ // Navigate to account and then statements
297
+ await p.goto(`${CHASE_BASE_URL}/web/auth/dashboard`, { waitUntil: "networkidle" });
298
+ await p.waitForTimeout(1000);
299
+ // This would need to navigate to the specific account's statements section
300
+ // For now, return a message about navigation
301
+ const statements = await p.$$eval('.statement-row, [data-testid="statement"]', (elements, acctId) => elements.slice(0, 12).map((el, index) => {
302
+ const periodEl = el.querySelector('.period, .statement-period');
303
+ const dateEl = el.querySelector('.date, .statement-date');
304
+ return {
305
+ id: el.getAttribute('data-statement-id') || `stmt-${index}`,
306
+ accountId: acctId,
307
+ period: periodEl?.textContent?.trim() || '',
308
+ date: dateEl?.textContent?.trim() || '',
309
+ };
310
+ }), accountId);
311
+ return { success: true, statements };
312
+ }
313
+ catch (error) {
314
+ return {
315
+ success: false,
316
+ error: error instanceof Error ? error.message : "Failed to get statements",
317
+ };
318
+ }
319
+ }
320
+ /**
321
+ * Get rewards/points info
322
+ */
323
+ export async function getRewards() {
324
+ try {
325
+ const p = await getPage();
326
+ await p.goto(`${CHASE_BASE_URL}/web/auth/dashboard`, { waitUntil: "networkidle" });
327
+ await p.waitForTimeout(2000);
328
+ const rewards = await p.evaluate(() => {
329
+ const pointsEl = document.querySelector('.points-balance, .ultimate-rewards, [data-testid="points"]');
330
+ const cashBackEl = document.querySelector('.cash-back-balance, [data-testid="cashback"]');
331
+ const tierEl = document.querySelector('.rewards-tier, .membership-level');
332
+ const pointsText = pointsEl?.textContent?.trim() || '0';
333
+ const cashBackText = cashBackEl?.textContent?.trim() || '0';
334
+ return {
335
+ pointsBalance: parseInt(pointsText.replace(/[^0-9]/g, '')) || 0,
336
+ cashBackBalance: parseFloat(cashBackText.replace(/[$,]/g, '')) || 0,
337
+ pendingRewards: 0,
338
+ tier: tierEl?.textContent?.trim() || undefined,
339
+ };
340
+ });
341
+ return { success: true, rewards };
342
+ }
343
+ catch (error) {
344
+ return {
345
+ success: false,
346
+ error: error instanceof Error ? error.message : "Failed to get rewards",
347
+ };
348
+ }
349
+ }
350
+ /**
351
+ * Initiate a transfer (preview only - does not execute)
352
+ */
353
+ export async function initiateTransfer(fromAccountId, toAccountId, amount) {
354
+ // For security, this only provides a preview and instructions
355
+ // The actual transfer would need to be confirmed by the user
356
+ return {
357
+ success: true,
358
+ preview: {
359
+ fromAccountId,
360
+ toAccountId,
361
+ amount,
362
+ message: "For security reasons, transfers must be completed manually in Chase. Navigate to Transfers in your Chase account to complete this transfer.",
363
+ instructions: [
364
+ "1. Log in to Chase.com or Chase mobile app",
365
+ "2. Go to 'Pay & Transfer' > 'Transfers'",
366
+ "3. Select your from and to accounts",
367
+ `4. Enter amount: $${amount.toFixed(2)}`,
368
+ "5. Review and confirm the transfer"
369
+ ]
370
+ }
371
+ };
372
+ }
373
+ /**
374
+ * Pay a bill (preview only - does not execute)
375
+ */
376
+ export async function payBill(payeeId, amount, date) {
377
+ // For security, this only provides a preview and instructions
378
+ return {
379
+ success: true,
380
+ preview: {
381
+ payeeId,
382
+ amount,
383
+ scheduledDate: date || "Immediately",
384
+ message: "For security reasons, bill payments must be completed manually in Chase.",
385
+ instructions: [
386
+ "1. Log in to Chase.com or Chase mobile app",
387
+ "2. Go to 'Pay & Transfer' > 'Pay Bills'",
388
+ "3. Select the payee",
389
+ `4. Enter amount: $${amount.toFixed(2)}`,
390
+ "5. Choose payment date and confirm"
391
+ ]
392
+ }
393
+ };
394
+ }