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 +19 -7
- package/bin/bankmcp.js +9 -1
- package/dist/lib/cli.js +1 -1
- package/dist/lib/local.js +41 -5
- package/dist/lib/mcp.js +1 -1
- package/dist/lib/server.js +9 -0
- package/dist/lib/store.js +5 -2
- package/dist/lib/tools.js +6 -2
- package/package.json +1 -1
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/
|
|
290
|
-
src/
|
|
291
|
-
src/
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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: [
|
package/dist/lib/server.js
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
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
|
+
"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",
|