financy 0.2.0 → 0.4.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.
@@ -1,180 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- CliError,
4
- EXIT,
5
- fetchAccount,
6
- fetchAccounts,
7
- fetchBranches,
8
- fetchCategories,
9
- fetchConnection,
10
- fetchConnections,
11
- fetchProviders,
12
- fetchRefresh,
13
- fetchStatus,
14
- fetchTransaction,
15
- fetchTransactions,
16
- loadConfig,
17
- notConfigured
18
- } from "./chunk-QYWDNIM2.js";
19
-
20
- // src/mcp/tools.ts
21
- var str = (v) => v == null ? void 0 : String(v);
22
- var bool = (v) => v === true;
23
- function requireId(args) {
24
- const id = args.id;
25
- if (typeof id !== "string" || id.length === 0) {
26
- throw new CliError(EXIT.USAGE, "MISSING_ARGUMENT", "the `id` argument is required");
27
- }
28
- return id;
29
- }
30
- var PAGING = {
31
- limit: { type: "number", description: "page size (defaults to the API default)" },
32
- cursor: { type: "string", description: "opaque nextPage cursor from a prior call" },
33
- all: { type: "boolean", description: "auto-paginate to the end (ignores cursor/limit paging)" }
34
- };
35
- var NO_ARGS = { type: "object", properties: {} };
36
- var idSchema = (noun) => ({
37
- type: "object",
38
- properties: { id: { type: "string", description: `the ${noun} id` } },
39
- required: ["id"]
40
- });
41
- var listArgs = (a) => ({
42
- limit: str(a.limit),
43
- cursor: str(a.cursor),
44
- all: bool(a.all)
45
- });
46
- var TOOLS = [
47
- {
48
- name: "list_connections",
49
- description: "List the user's bank/card connections and their fetch state (status, data-through date, consent expiry, per-type counts). Start here to see what is connected. Supports limit/cursor paging and all.",
50
- inputSchema: { type: "object", properties: { ...PAGING } },
51
- run: (a, ctx) => fetchConnections(ctx.config, ctx.now, listArgs(a))
52
- },
53
- {
54
- name: "get_connection",
55
- description: "Read a single connection by id (from list_connections). Errors CONNECTION_NOT_FOUND if it is not in the user's scope.",
56
- inputSchema: idSchema("connection"),
57
- run: (a, ctx) => fetchConnection(ctx.config, ctx.now, requireId(a))
58
- },
59
- {
60
- name: "list_accounts",
61
- description: "List accounts with balances (securities embedded). Filter by type (CHECKING|CARD|LOAN|SAVINGS|SECURITY) or connection. Supports limit/cursor/all.",
62
- inputSchema: {
63
- type: "object",
64
- properties: {
65
- type: { type: "string", description: "CHECKING|CARD|LOAN|SAVINGS|SECURITY" },
66
- connection: { type: "string", description: "filter to one connection id" },
67
- ...PAGING
68
- }
69
- },
70
- run: (a, ctx) => fetchAccounts(ctx.config, ctx.now, {
71
- ...listArgs(a),
72
- type: str(a.type),
73
- connection: str(a.connection)
74
- })
75
- },
76
- {
77
- name: "get_account",
78
- description: "Read a single account by id (from list_accounts). Errors ACCOUNT_NOT_FOUND if not in scope.",
79
- inputSchema: idSchema("account"),
80
- run: (a, ctx) => fetchAccount(ctx.config, ctx.now, requireId(a))
81
- },
82
- {
83
- name: "list_transactions",
84
- description: "List transactions, most useful with filters: from/to (YYYY-MM-DD), account, connection, type (BANK|CARD). Page with limit/cursor or fetch everything with all. Hebrew merchant names are returned as-is. To then read one transaction, pass its `SK` (not its `id`) to get_transaction.",
85
- inputSchema: {
86
- type: "object",
87
- properties: {
88
- from: { type: "string", description: "earliest transaction date (YYYY-MM-DD)" },
89
- to: { type: "string", description: "latest transaction date (YYYY-MM-DD)" },
90
- account: { type: "string", description: "filter to one account id" },
91
- connection: { type: "string", description: "filter to one connection id" },
92
- type: { type: "string", description: "BANK|CARD" },
93
- ...PAGING
94
- }
95
- },
96
- run: (a, ctx) => fetchTransactions(ctx.config, ctx.now, {
97
- ...listArgs(a),
98
- from: str(a.from),
99
- to: str(a.to),
100
- account: str(a.account),
101
- connection: str(a.connection),
102
- type: str(a.type)
103
- })
104
- },
105
- {
106
- name: "get_transaction",
107
- description: "Read a single transaction by its `SK` \u2014 the composite key from list_transactions, NOT the short `id` field, which this route does not accept. Errors TRANSACTION_NOT_FOUND if not in scope.",
108
- inputSchema: {
109
- type: "object",
110
- properties: {
111
- id: {
112
- type: "string",
113
- description: 'the transaction\'s `SK` from list_transactions, e.g. "TRANSACTION#TYPE#CHECKING#PROVIDER#leumi#RESOURCE<uuid>#<ulid>". The short `id` field of the same transaction will NOT resolve.'
114
- }
115
- },
116
- required: ["id"]
117
- },
118
- run: (a, ctx) => fetchTransaction(ctx.config, ctx.now, requireId(a))
119
- },
120
- {
121
- name: "list_categories",
122
- description: "The static transaction-category taxonomy (English + Hebrew). Use to interpret transaction category fields. No arguments.",
123
- inputSchema: NO_ARGS,
124
- run: async (_a, ctx) => ({ data: await fetchCategories(ctx.config, ctx.now) })
125
- },
126
- {
127
- name: "list_providers",
128
- description: "Reference data: the list of banks/providers, to resolve providerId values. No arguments.",
129
- inputSchema: NO_ARGS,
130
- run: (_a, ctx) => fetchProviders(ctx.config, ctx.now)
131
- },
132
- {
133
- name: "list_bank_branches",
134
- description: "Reference data: bank branches. No arguments (this route takes no paging).",
135
- inputSchema: NO_ARGS,
136
- run: (_a, ctx) => fetchBranches(ctx.config, ctx.now)
137
- },
138
- {
139
- name: "get_status",
140
- description: "Per-connection freshness rollup: for each connection its status, data-through date, consent expiry, and account count, plus staleThresholdDays. Call this first to decide whether data is fresh enough before answering questions about balances or transactions.",
141
- inputSchema: NO_ARGS,
142
- run: async (_a, ctx) => {
143
- const { data, staleThresholdDays } = await fetchStatus(ctx.config, ctx.now);
144
- return { data, staleThresholdDays };
145
- }
146
- },
147
- {
148
- name: "refresh_connections",
149
- description: "Trigger an on-demand refresh of ALL connections (org-wide). This COSTS 20 credits \u2014 always confirm with the user before calling it. Data lands after a few minutes; poll get_status to watch progress. Returns the refresh outcome (accepted / already_running).",
150
- inputSchema: NO_ARGS,
151
- run: (_a, ctx) => fetchRefresh(ctx.config, ctx.now)
152
- }
153
- ];
154
- var BY_NAME = new Map(TOOLS.map((t) => [t.name, t]));
155
- function errorEnvelope(error) {
156
- const e = error instanceof CliError ? error : new CliError(EXIT.UNEXPECTED, "UNEXPECTED", String(error?.message ?? error));
157
- return { error: { code: e.code, message: e.message, ...e.extra } };
158
- }
159
- async function callTool(name, args, io) {
160
- try {
161
- const tool = BY_NAME.get(name);
162
- if (!tool) {
163
- throw new CliError(EXIT.USAGE, "UNKNOWN_TOOL", `no tool named '${name}'`);
164
- }
165
- let config = io.config;
166
- if (!config) {
167
- const result = await loadConfig(io.env);
168
- if (!result.ok) throw notConfigured(result);
169
- config = result.config;
170
- }
171
- return await tool.run(args ?? {}, { config, now: io.now ?? /* @__PURE__ */ new Date() });
172
- } catch (error) {
173
- return errorEnvelope(error);
174
- }
175
- }
176
-
177
- export {
178
- TOOLS,
179
- callTool
180
- };
@@ -1,179 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- TOOLS,
4
- callTool
5
- } from "../chunk-74AQOXZZ.js";
6
- import {
7
- VERSION,
8
- bearerConfig
9
- } from "../chunk-QYWDNIM2.js";
10
-
11
- // src/remote/server.ts
12
- import {
13
- createServer
14
- } from "http";
15
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
16
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
17
- import {
18
- ListToolsRequestSchema,
19
- CallToolRequestSchema
20
- } from "@modelcontextprotocol/sdk/types.js";
21
-
22
- // src/remote/auth.ts
23
- import { createRemoteJWKSet, jwtVerify } from "jose";
24
- var RemoteAuthError = class extends Error {
25
- };
26
- function remoteAuthFromEnv(env, getKey) {
27
- const domain = env.FINANCY_MCP_AUTH0_DOMAIN;
28
- if (!domain) {
29
- throw new Error("FINANCY_MCP_AUTH0_DOMAIN is required to run the remote MCP server");
30
- }
31
- const issuer = `https://${domain}/`;
32
- return {
33
- issuer,
34
- audience: env.FINANCY_MCP_AUDIENCE ?? "https://mcp.open-finance.ai/",
35
- getKey: getKey ?? createRemoteJWKSet(new URL(`${issuer}.well-known/jwks.json`))
36
- };
37
- }
38
- async function verifyBearer(auth, header) {
39
- const token = header?.startsWith("Bearer ") ? header.slice("Bearer ".length) : void 0;
40
- if (!token) throw new RemoteAuthError("missing bearer token");
41
- let scope;
42
- try {
43
- const { payload } = await jwtVerify(token, auth.getKey, {
44
- issuer: auth.issuer,
45
- audience: auth.audience,
46
- requiredClaims: ["exp"]
47
- });
48
- scope = payload.scope;
49
- } catch {
50
- throw new RemoteAuthError("invalid or expired token");
51
- }
52
- const scopes = typeof scope === "string" ? scope.split(" ") : [];
53
- if (!scopes.includes("mcp:read")) {
54
- throw new RemoteAuthError("token is missing the mcp:read scope \u2014 reconnect the Financy connector");
55
- }
56
- return token;
57
- }
58
-
59
- // src/remote/server.ts
60
- var METADATA_PATH = "/.well-known/oauth-protected-resource";
61
- var MCP_PATH = "/mcp";
62
- var REMOTE_TOOLS = TOOLS.filter((tool) => tool.name !== "refresh_connections");
63
- var REMOTE_TOOL_NAMES = new Set(REMOTE_TOOLS.map((tool) => tool.name));
64
- var sendJson = (res, status, body, headers = {}) => {
65
- res.writeHead(status, { "content-type": "application/json", ...headers });
66
- res.end(JSON.stringify(body));
67
- };
68
- var MAX_BODY_BYTES = 256 * 1024;
69
- async function readBoundedJson(req, res) {
70
- const chunks = [];
71
- let size = 0;
72
- for await (const chunk of req) {
73
- size += chunk.length;
74
- if (size > MAX_BODY_BYTES) {
75
- sendJson(res, 413, { error: "payload_too_large" });
76
- req.destroy();
77
- return { ok: false };
78
- }
79
- chunks.push(chunk);
80
- }
81
- let body;
82
- try {
83
- body = chunks.length ? JSON.parse(Buffer.concat(chunks).toString("utf8")) : void 0;
84
- } catch {
85
- sendJson(res, 400, { error: "invalid_json" });
86
- return { ok: false };
87
- }
88
- if (Array.isArray(body)) {
89
- sendJson(res, 400, { error: "batch_not_supported" });
90
- return { ok: false };
91
- }
92
- return { ok: true, body };
93
- }
94
- function createRemoteHandler(env, options = {}) {
95
- const auth = remoteAuthFromEnv(env, options.getKey);
96
- return async (req, res) => {
97
- const pathname = new URL(req.url ?? "/", "http://placeholder").pathname;
98
- if (req.method === "GET" && pathname === METADATA_PATH) {
99
- sendJson(res, 200, {
100
- resource: auth.audience,
101
- authorization_servers: [auth.issuer],
102
- scopes_supported: ["mcp:read"],
103
- bearer_methods_supported: ["header"]
104
- });
105
- return;
106
- }
107
- if (pathname !== MCP_PATH) {
108
- sendJson(res, 404, { error: "not_found" });
109
- return;
110
- }
111
- if (req.method !== "POST") {
112
- sendJson(res, 405, { error: "method_not_allowed" }, { allow: "POST" });
113
- return;
114
- }
115
- let token;
116
- try {
117
- token = await verifyBearer(auth, req.headers.authorization);
118
- } catch (error) {
119
- const proto = req.headers["x-forwarded-proto"] ?? "http";
120
- const host = req.headers["x-forwarded-host"] ?? req.headers.host;
121
- const message = error instanceof RemoteAuthError ? error.message : "unauthorized";
122
- sendJson(res, 401, { error: "unauthorized", message }, {
123
- "www-authenticate": `Bearer resource_metadata="${proto}://${host}${METADATA_PATH}"`
124
- });
125
- return;
126
- }
127
- const parsed = await readBoundedJson(req, res);
128
- if (!parsed.ok) return;
129
- const server = new Server(
130
- { name: "financy", version: VERSION },
131
- { capabilities: { tools: {} } }
132
- );
133
- server.setRequestHandler(ListToolsRequestSchema, () => ({
134
- tools: REMOTE_TOOLS.map((tool) => ({
135
- name: tool.name,
136
- description: tool.description,
137
- inputSchema: tool.inputSchema
138
- }))
139
- }));
140
- server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
141
- const name = request.params.name;
142
- const result = REMOTE_TOOL_NAMES.has(name) ? await callTool(name, request.params.arguments ?? {}, {
143
- env,
144
- config: bearerConfig(env, token, extra.signal)
145
- }) : { error: { code: "UNKNOWN_TOOL", message: `no tool named '${name}'` } };
146
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
147
- });
148
- const transport = new StreamableHTTPServerTransport({
149
- sessionIdGenerator: void 0,
150
- enableJsonResponse: true
151
- });
152
- res.on("close", () => {
153
- void transport.close();
154
- void server.close();
155
- });
156
- await server.connect(transport);
157
- await transport.handleRequest(req, res, parsed.body);
158
- };
159
- }
160
- function startRemoteServer(env, port2, options = {}) {
161
- const handler = createRemoteHandler(env, options);
162
- const httpServer = createServer((req, res) => {
163
- void handler(req, res).catch(() => {
164
- if (!res.headersSent) sendJson(res, 500, { error: "internal_error" });
165
- else res.end();
166
- });
167
- });
168
- return new Promise((resolve) => {
169
- httpServer.listen(port2, () => resolve(httpServer));
170
- });
171
- }
172
-
173
- // src/remote/start.ts
174
- var port = Number(process.env.PORT ?? 8790);
175
- startRemoteServer(process.env, port).then((server) => {
176
- const address = server.address();
177
- const shown = typeof address === "object" && address ? address.port : port;
178
- console.error(`financy remote MCP server listening on :${shown}`);
179
- });
@@ -1,37 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- TOOLS,
4
- callTool
5
- } from "./chunk-74AQOXZZ.js";
6
- import {
7
- VERSION
8
- } from "./chunk-QYWDNIM2.js";
9
-
10
- // src/mcp/server.ts
11
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
12
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
- import {
14
- ListToolsRequestSchema,
15
- CallToolRequestSchema
16
- } from "@modelcontextprotocol/sdk/types.js";
17
- async function startMcpServer(env) {
18
- const server = new Server(
19
- { name: "financy", version: VERSION },
20
- { capabilities: { tools: {} } }
21
- );
22
- server.setRequestHandler(ListToolsRequestSchema, () => ({
23
- tools: TOOLS.map((t) => ({
24
- name: t.name,
25
- description: t.description,
26
- inputSchema: t.inputSchema
27
- }))
28
- }));
29
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
30
- const result = await callTool(request.params.name, request.params.arguments ?? {}, { env });
31
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
32
- });
33
- await server.connect(new StdioServerTransport());
34
- }
35
- export {
36
- startMcpServer
37
- };