bankmcp 0.1.3 → 0.1.5

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
@@ -71,6 +71,12 @@ Your browser will warn once about the certificate on localhost, which the
71
71
  server made for itself because Enable Banking requires https for the bank
72
72
  redirect. Continue past it. Say "connect my bank" and log in at your bank.
73
73
 
74
+ To avoid the warning altogether, use a certificate your browser already
75
+ trusts: [mkcert](https://github.com/FiloSottile/mkcert) makes one with
76
+ `mkcert localhost 127.0.0.1`; point `TLS_CERT_PATH` and `TLS_KEY_PATH` at the
77
+ two files and the server uses them instead of generating its own. BankMCP™
78
+ itself does not touch your system's trust store.
79
+
74
80
  State lives in `~/.bankmcp`. Delete the folder to forget everything.
75
81
 
76
82
  ### On a server
@@ -286,20 +292,26 @@ Requires Node 24 or newer (runs TypeScript directly, no build step).
286
292
  ## Layout
287
293
 
288
294
  ```
289
- src/server.ts Express: /mcp behind OAuth, OAuth endpoints, /callback, status page
290
- src/auth.ts single-user OAuth provider, password login page
291
- src/mcp.ts McpServer factory (tools + prompts + instructions)
295
+ src/stdio.ts entry point for npx bankmcp: stdio server plus the localhost https side
296
+ src/server.ts entry point for a hosted deployment: OAuth, /mcp, callback, status page
297
+ src/app.ts the Express app shared by both entry points
298
+ src/local.ts localhost https server and self-signed certificate for local mode
299
+ src/mcp.ts McpServer factory (tools, prompts, instructions)
292
300
  src/tools.ts the MCP tools
293
301
  src/prompts.ts the MCP prompts
294
302
  src/watcher.ts background rule checks and notifications
303
+ src/auth.ts single-user OAuth provider
304
+ src/setup.ts first-run setup: validates and stores id, key and password
305
+ src/pages.ts the HTML pages: setup, sign-in, connected, status, privacy, terms
306
+ src/config.ts environment and data-directory configuration
295
307
  src/enablebanking.ts JWT signing and a thin typed API client
296
308
  src/store.ts the JSON state file
297
309
  src/data.ts shaping balances and transactions for an assistant
298
- src/stdio.ts local entry point (stdio, used by `npx bankmcp`)
299
- src/local.ts localhost https server for setup and the bank redirect in local mode
300
- src/app.ts the Express app shared by both modes
301
310
  src/cli.ts check, hash-password, watch
302
- plugin/ Claude Code plugin with the bank skill
311
+ bin/bankmcp.js the npx entry; dispatches to stdio or cli
312
+ scripts/ build-mcpb.sh (Claude Desktop bundle), local-chat.ts (Ollama bridge)
313
+ plugin/ Claude Code plugin: setup and deploy skills, bank skill
314
+ docs/ landing page (GitHub Pages, bankmcp.dk)
303
315
  ```
304
316
 
305
317
  ## What this is, and is not
package/bin/bankmcp.js CHANGED
@@ -5,4 +5,12 @@ if (major < 24) {
5
5
  console.error(`BankMCP needs Node 24 or newer (you have ${process.versions.node}).`);
6
6
  process.exit(1);
7
7
  }
8
- await import("../dist/lib/stdio.js");
8
+ // A bare `bankmcp` is the stdio server an MCP client launches. Anything with
9
+ // arguments is a command for the person at the keyboard, and those look in the
10
+ // same place the local server keeps its state.
11
+ if (process.argv.length > 2) {
12
+ process.env.BANKMCP_LOCAL ??= "1";
13
+ await import("../dist/lib/cli.js");
14
+ } else {
15
+ await import("../dist/lib/stdio.js");
16
+ }
package/dist/lib/cli.js CHANGED
@@ -94,6 +94,6 @@ switch (command) {
94
94
  break;
95
95
  }
96
96
  default:
97
- console.log("Usage: node src/cli.ts <hash-password [password] | check | watch [--force]>");
97
+ console.log("Usage: bankmcp <hash-password [password] | check | watch [--force]>");
98
98
  process.exit(command ? 1 : 0);
99
99
  }
package/dist/lib/local.js CHANGED
@@ -2,24 +2,60 @@
2
2
  // page and receives the bank redirect. Enable Banking requires https redirect
3
3
  // URLs, so a self-signed certificate is created on first run.
4
4
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { X509Certificate } from "node:crypto";
5
6
  import { join } from "node:path";
6
7
  import { createServer as createHttpsServer } from "node:https";
7
8
  import selfsigned from "selfsigned";
8
- import { config } from "./config.js";
9
+ import { config, tlsOptions } from "./config.js";
9
10
  import { createApp } from "./app.js";
10
11
  let server;
11
12
  let starting;
13
+ /** Browsers reject server certificates valid for more than 398 days; earlier versions issued 10-year ones. */
14
+ export function certificateStillGood(pem, now = Date.now()) {
15
+ try {
16
+ const x = new X509Certificate(pem);
17
+ const from = Date.parse(x.validFrom);
18
+ const to = Date.parse(x.validTo);
19
+ const day = 86_400_000;
20
+ return to - from <= 398 * day && to - now > 30 * day;
21
+ }
22
+ catch {
23
+ return false;
24
+ }
25
+ }
12
26
  async function certificate() {
27
+ // An explicitly configured certificate wins, so anyone who would rather use
28
+ // one their browser already trusts (mkcert and friends) can point at it
29
+ // instead of trusting the generated one. `server.ts` already honours these.
30
+ const configured = tlsOptions();
31
+ if (configured)
32
+ return configured;
13
33
  const certPath = join(config.dataDir, "localhost-cert.pem");
14
34
  const keyPath = join(config.dataDir, "localhost-key.pem");
15
- if (existsSync(certPath) && existsSync(keyPath))
16
- return { cert: readFileSync(certPath, "utf8"), key: readFileSync(keyPath, "utf8") };
35
+ if (existsSync(certPath) && existsSync(keyPath)) {
36
+ const cert = readFileSync(certPath, "utf8");
37
+ if (certificateStillGood(cert))
38
+ return { cert, key: readFileSync(keyPath, "utf8") };
39
+ console.error("[bank] replacing the stored localhost certificate (too long-lived or about to expire)");
40
+ }
41
+ // Apple caps TLS server certificate lifetime at 398 days; Chrome on macOS
42
+ // defers to the system verifier and rejects anything longer as ERR_CERT_INVALID,
43
+ // which offers no click-through. Stay just under the limit.
17
44
  const notAfterDate = new Date();
18
- notAfterDate.setFullYear(notAfterDate.getFullYear() + 10);
45
+ notAfterDate.setDate(notAfterDate.getDate() + 397);
19
46
  const pems = await selfsigned.generate([{ name: "commonName", value: "localhost" }], {
20
47
  keySize: 2048,
21
48
  notAfterDate,
22
- extensions: [{ name: "subjectAltName", altNames: [{ type: 2, value: "localhost" }, { type: 7, ip: "127.0.0.1" }] }],
49
+ // selfsigned defaults to sha1, which browsers reject outright as
50
+ // ERR_CERT_INVALID with no click-through. macOS additionally requires
51
+ // basicConstraints and an extendedKeyUsage of serverAuth.
52
+ algorithm: "sha256",
53
+ extensions: [
54
+ { name: "basicConstraints", cA: false, critical: true },
55
+ { name: "keyUsage", digitalSignature: true, keyEncipherment: true, critical: true },
56
+ { name: "extKeyUsage", serverAuth: true },
57
+ { name: "subjectAltName", altNames: [{ type: 2, value: "localhost" }, { type: 7, ip: "127.0.0.1" }] },
58
+ ],
23
59
  });
24
60
  mkdirSync(config.dataDir, { recursive: true });
25
61
  writeFileSync(certPath, pems.cert, { mode: 0o600 });
package/dist/lib/mcp.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { registerTools } from "./tools.js";
3
3
  import { registerPrompts } from "./prompts.js";
4
- export const VERSION = "0.1.3";
4
+ export const VERSION = "0.1.5";
5
5
  export function createServer() {
6
6
  const server = new McpServer({ name: "bank", version: VERSION }, {
7
7
  instructions: [
@@ -6,6 +6,15 @@ import { createApp } from "./app.js";
6
6
  import { setupAvailable } from "./setup.js";
7
7
  const app = createApp({ remote: true });
8
8
  const tls = tlsOptions();
9
+ // In local mode config.baseUrl is https://localhost:PORT, but this entry point
10
+ // only speaks TLS when a certificate is configured. Serving plain http while
11
+ // advertising https leaves the setup page and the bank redirect unreachable,
12
+ // so say what is wrong instead of starting into a broken state.
13
+ if (config.localMode && !tls) {
14
+ console.error(`[bank] BANKMCP_LOCAL=1 makes the public URL ${config.baseUrl}, but no TLS certificate is configured, so this process can only serve http.\n` +
15
+ `[bank] Run the stdio entry point instead (npx bankmcp), which terminates TLS itself, or set TLS_CERT_PATH and TLS_KEY_PATH.`);
16
+ process.exit(1);
17
+ }
9
18
  const httpServer = tls ? createHttpsServer(tls, app) : createHttpServer(app);
10
19
  httpServer.listen(config.port, () => {
11
20
  console.log(`[bank ${new Date().toISOString()}] listening on ${tls ? "https" : "http"}://0.0.0.0:${config.port}, public URL ${config.baseUrl}`);
package/dist/lib/store.js CHANGED
@@ -4,6 +4,10 @@
4
4
  import { mkdirSync, readFileSync, renameSync, writeFileSync, existsSync } from "node:fs";
5
5
  import { dirname, join } from "node:path";
6
6
  import { config } from "./config.js";
7
+ /** How long a started-but-unfinished bank login stays interesting. */
8
+ export const PENDING_AUTH_TTL_MS = 60 * 60 * 1000;
9
+ /** True while a pending authorization is recent enough to still be completed. */
10
+ export const pendingAuthIsLive = (p, now = Date.now()) => Date.parse(p.started) >= now - PENDING_AUTH_TTL_MS;
7
11
  const empty = () => ({
8
12
  version: 1,
9
13
  sessions: {},
@@ -109,9 +113,8 @@ export class Store {
109
113
  // --- Pending bank authorizations ---
110
114
  addPendingAuth(p) {
111
115
  this.update((d) => {
112
- const cutoff = Date.now() - 60 * 60 * 1000;
113
116
  for (const [k, v] of Object.entries(d.pending_auth))
114
- if (Date.parse(v.started) < cutoff)
117
+ if (!pendingAuthIsLive(v))
115
118
  delete d.pending_auth[k];
116
119
  d.pending_auth[p.state] = p;
117
120
  });
package/dist/lib/tools.js CHANGED
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { z } from "zod";
3
3
  import { config, isConfigured } from "./config.js";
4
4
  import { eb, EnableBankingError } from "./enablebanking.js";
5
- import { store } from "./store.js";
5
+ import { pendingAuthIsLive, store } from "./store.js";
6
6
  import { daysAgo, daysLeft, describeAccount, isoDate, simplifyBalances, simplifyTransaction } from "./data.js";
7
7
  import { runWatches } from "./watcher.js";
8
8
  const MAX_CONSENT_DAYS = 180;
@@ -135,7 +135,11 @@ export function registerTools(server) {
135
135
  accounts: s.accounts().filter((a) => a.session_id === session.id).length,
136
136
  });
137
137
  }
138
- const pending = Object.values(s.data.pending_auth).map((p) => ({ bank: p.bank.name, started: p.started }));
138
+ // Expired ones are only swept when the next login starts, so filter here
139
+ // too; listing logins that can no longer be completed just misleads.
140
+ const pending = Object.values(s.data.pending_auth)
141
+ .filter((p) => pendingAuthIsLive(p))
142
+ .map((p) => ({ bank: p.bank.name, started: p.started }));
139
143
  return json({ banks, pending_logins: pending, hint: banks.length ? undefined : "No bank connected yet. Use start_consent." });
140
144
  }));
141
145
  server.registerTool("disconnect_bank", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bankmcp",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "BankMCP™: self-hosted, read-only MCP server that lets any AI assistant (Claude, ChatGPT, Mistral, Cursor, or a local model) answer questions about your own bank accounts via open banking (Enable Banking, PSD2)",
5
5
  "license": "MIT",
6
6
  "type": "module",