@codespar/mcp-open-finance 0.1.0 → 0.1.2

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,120 @@
1
+ # @codespar/mcp-open-finance
2
+
3
+ > MCP server for **Open Finance Brasil** — open banking standard for accounts, transactions, and consents
4
+
5
+ [![npm](https://img.shields.io/npm/v/@codespar/mcp-open-finance)](https://www.npmjs.com/package/@codespar/mcp-open-finance)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ ## Quick Start
9
+
10
+ ### Claude Desktop
11
+
12
+ Add to `~/.config/claude/claude_desktop_config.json`:
13
+
14
+ ```json
15
+ {
16
+ "mcpServers": {
17
+ "open-finance": {
18
+ "command": "npx",
19
+ "args": ["-y", "@codespar/mcp-open-finance"],
20
+ "env": {
21
+ "OPEN_FINANCE_BASE_URL": "https://api.institution.com.br",
22
+ "OPEN_FINANCE_CLIENT_ID": "your-client-id",
23
+ "OPEN_FINANCE_CLIENT_SECRET": "your-client-secret"
24
+ }
25
+ }
26
+ }
27
+ }
28
+ ```
29
+
30
+ ### Claude Code
31
+
32
+ ```bash
33
+ claude mcp add open-finance -- npx @codespar/mcp-open-finance
34
+ ```
35
+
36
+ ### Cursor / VS Code
37
+
38
+ Add to `.cursor/mcp.json` or `.vscode/mcp.json`:
39
+
40
+ ```json
41
+ {
42
+ "servers": {
43
+ "open-finance": {
44
+ "command": "npx",
45
+ "args": ["-y", "@codespar/mcp-open-finance"],
46
+ "env": {
47
+ "OPEN_FINANCE_BASE_URL": "https://api.institution.com.br",
48
+ "OPEN_FINANCE_CLIENT_ID": "your-client-id",
49
+ "OPEN_FINANCE_CLIENT_SECRET": "your-client-secret"
50
+ }
51
+ }
52
+ }
53
+ }
54
+ ```
55
+
56
+ ## Tools
57
+
58
+ | Tool | Description |
59
+ |------|-------------|
60
+ | `list_accounts` | List customer bank accounts via Open Finance |
61
+ | `get_account_balance` | Get account balance via Open Finance |
62
+ | `list_transactions` | List account transactions via Open Finance |
63
+ | `get_consent` | Get consent details by ID |
64
+ | `create_consent` | Create a new consent request for data access |
65
+ | `list_credit_cards` | List credit card accounts via Open Finance |
66
+ | `get_credit_card_transactions` | Get credit card transactions via Open Finance |
67
+ | `list_investments` | List investment products via Open Finance |
68
+
69
+ ## Authentication
70
+
71
+ Open Finance Brasil uses OAuth2 client credentials. Each financial institution provides its own base URL and credentials.
72
+
73
+ ## Sandbox / Testing
74
+
75
+ Sandbox availability varies by institution. Contact your financial institution for Open Finance sandbox access.
76
+
77
+ ### Get your credentials
78
+
79
+ 1. Go to [Open Finance Brasil](https://openfinancebrasil.org.br)
80
+ 2. Register with a participating financial institution
81
+ 3. Obtain your OAuth2 client credentials
82
+ 4. Set the environment variables
83
+
84
+ ## Environment Variables
85
+
86
+ | Variable | Required | Description |
87
+ |----------|----------|-------------|
88
+ | `OPEN_FINANCE_BASE_URL` | Yes | Institution API base URL |
89
+ | `OPEN_FINANCE_CLIENT_ID` | Yes | OAuth2 client ID |
90
+ | `OPEN_FINANCE_CLIENT_SECRET` | Yes | OAuth2 client secret |
91
+
92
+ ## Roadmap
93
+
94
+ ### v0.2 (planned)
95
+ - `revoke_consent` — Revoke a data sharing consent
96
+ - `list_payments` — List initiated payments
97
+ - `create_payment_consent` — Create a payment initiation consent
98
+ - `initiate_payment` — Initiate a payment via Open Finance
99
+ - `get_payment_status` — Get payment initiation status
100
+
101
+ ### v0.3 (planned)
102
+ - `insurance_products` — List insurance products from institutions
103
+ - `pension_products` — List pension products from institutions
104
+
105
+ Want to contribute? [Open a PR](https://github.com/codespar/mcp-dev-brasil) or [request a tool](https://github.com/codespar/mcp-dev-brasil/issues).
106
+
107
+ ## Links
108
+
109
+ - [Open Finance Brasil](https://openfinancebrasil.org.br)
110
+ - [Open Finance Brasil Developer Portal](https://openfinancebrasil.atlassian.net)
111
+ - [MCP Dev Brasil](https://github.com/codespar/mcp-dev-brasil)
112
+ - [Landing Page](https://codespar.dev/mcp)
113
+
114
+ ## Enterprise
115
+
116
+ Need governance, budget limits, and audit trails for agent payments? [CodeSpar Enterprise](https://codespar.dev/enterprise) adds policy engine, payment routing, and compliance templates on top of these MCP servers.
117
+
118
+ ## License
119
+
120
+ MIT
package/dist/index.js CHANGED
@@ -19,6 +19,8 @@
19
19
  */
20
20
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
21
21
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
22
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
23
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
22
24
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
23
25
  const BASE_URL = process.env.OPEN_FINANCE_BASE_URL || "";
24
26
  const CLIENT_ID = process.env.OPEN_FINANCE_CLIENT_ID || "";
@@ -182,7 +184,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
182
184
  ],
183
185
  }));
184
186
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
185
- const { name, arguments: args } = request.params;
187
+ const { name, arguments: rawArgs } = request.params;
188
+ const args = rawArgs;
186
189
  try {
187
190
  switch (name) {
188
191
  case "list_accounts": {
@@ -258,11 +261,46 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
258
261
  }
259
262
  });
260
263
  async function main() {
261
- if (!BASE_URL || !CLIENT_ID || !CLIENT_SECRET) {
262
- console.error("OPEN_FINANCE_BASE_URL, OPEN_FINANCE_CLIENT_ID, and OPEN_FINANCE_CLIENT_SECRET environment variables are required");
263
- process.exit(1);
264
+ if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") {
265
+ const { default: express } = await import("express");
266
+ const { randomUUID } = await import("node:crypto");
267
+ const app = express();
268
+ app.use(express.json());
269
+ const transports = new Map();
270
+ app.get("/health", (_req, res) => res.json({ status: "ok", sessions: transports.size }));
271
+ app.post("/mcp", async (req, res) => {
272
+ const sid = req.headers["mcp-session-id"];
273
+ if (sid && transports.has(sid)) {
274
+ await transports.get(sid).handleRequest(req, res, req.body);
275
+ return;
276
+ }
277
+ if (!sid && isInitializeRequest(req.body)) {
278
+ const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
279
+ t.onclose = () => { if (t.sessionId)
280
+ transports.delete(t.sessionId); };
281
+ const s = new Server({ name: "mcp-open-finance", version: "0.1.0" }, { capabilities: { tools: {} } });
282
+ server._requestHandlers.forEach((v, k) => s._requestHandlers.set(k, v));
283
+ server._notificationHandlers?.forEach((v, k) => s._notificationHandlers.set(k, v));
284
+ await s.connect(t);
285
+ await t.handleRequest(req, res, req.body);
286
+ return;
287
+ }
288
+ res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null });
289
+ });
290
+ app.get("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"]; if (sid && transports.has(sid))
291
+ await transports.get(sid).handleRequest(req, res);
292
+ else
293
+ res.status(400).send("Invalid session"); });
294
+ app.delete("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"]; if (sid && transports.has(sid))
295
+ await transports.get(sid).handleRequest(req, res);
296
+ else
297
+ res.status(400).send("Invalid session"); });
298
+ const port = Number(process.env.MCP_PORT) || 3000;
299
+ app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); });
300
+ }
301
+ else {
302
+ const transport = new StdioServerTransport();
303
+ await server.connect(transport);
264
304
  }
265
- const transport = new StdioServerTransport();
266
- await server.connect(transport);
267
305
  }
268
306
  main().catch(console.error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codespar/mcp-open-finance",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "MCP server for Open Finance Brasil — accounts, transactions, consents, investments",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -26,5 +26,6 @@
26
26
  "pix",
27
27
  "accounts",
28
28
  "brazil"
29
- ]
29
+ ],
30
+ "mcpName": "io.github.codespar/mcp-open-finance"
30
31
  }
package/server.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.codespar/mcp-open-finance",
4
+ "description": "MCP server for Open Finance Brasil — accounts, transactions, consents, investments",
5
+ "repository": {
6
+ "url": "https://github.com/codespar/mcp-dev-brasil",
7
+ "source": "github",
8
+ "subfolder": "packages/banking/open-finance"
9
+ },
10
+ "version": "0.1.2",
11
+ "packages": [
12
+ {
13
+ "registryType": "npm",
14
+ "identifier": "@codespar/mcp-open-finance",
15
+ "version": "0.1.2",
16
+ "transport": {
17
+ "type": "stdio"
18
+ },
19
+ "environmentVariables": [
20
+ {
21
+ "name": "OPEN_FINANCE_CLIENT_SECRET",
22
+ "description": "API key for open-finance",
23
+ "isRequired": true,
24
+ "format": "string",
25
+ "isSecret": true
26
+ }
27
+ ]
28
+ }
29
+ ]
30
+ }
@@ -0,0 +1,53 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
3
+ let listToolsHandler: Function;
4
+ let callToolHandler: Function;
5
+
6
+ vi.mock("@modelcontextprotocol/sdk/server/index.js", () => {
7
+ class FakeServer {
8
+ constructor() {}
9
+ setRequestHandler(schema: any, handler: Function) {
10
+ if (JSON.stringify(schema).includes("tools/list")) listToolsHandler = handler;
11
+ if (JSON.stringify(schema).includes("tools/call")) callToolHandler = handler;
12
+ }
13
+ connect() { return Promise.resolve(); }
14
+ }
15
+ return { Server: FakeServer };
16
+ });
17
+
18
+ vi.mock("@modelcontextprotocol/sdk/server/stdio.js", () => ({ StdioServerTransport: class {} }));
19
+
20
+ process.env.OPEN_FINANCE_BASE_URL = "https://api.bank.example.com";
21
+ process.env.OPEN_FINANCE_CLIENT_ID = "test-id";
22
+ process.env.OPEN_FINANCE_CLIENT_SECRET = "test-secret";
23
+
24
+ const mockFetch = vi.fn();
25
+ global.fetch = mockFetch as any;
26
+
27
+ beforeEach(async () => {
28
+ vi.resetModules();
29
+ listToolsHandler = undefined as any;
30
+ callToolHandler = undefined as any;
31
+ mockFetch.mockReset();
32
+ global.fetch = mockFetch as any;
33
+ mockFetch.mockResolvedValue({ ok: true, json: () => Promise.resolve({ access_token: "tok", expires_in: 3600 }) });
34
+ await import("../index.js");
35
+ });
36
+
37
+ describe("mcp-open-finance", () => {
38
+ it("should register 8 tools", async () => {
39
+ const result = await listToolsHandler();
40
+ expect(result.tools).toHaveLength(8);
41
+ });
42
+
43
+ it("should call correct API endpoint for get_account_balance", async () => {
44
+ mockFetch
45
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ access_token: "tok", expires_in: 3600 }) })
46
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ data: { availableAmount: 1000 } }) });
47
+
48
+ await callToolHandler({ params: { name: "get_account_balance", arguments: { accountId: "acc_123" } } });
49
+
50
+ const lastCall = mockFetch.mock.calls[mockFetch.mock.calls.length - 1];
51
+ expect(lastCall[0]).toContain("/open-banking/accounts/v2/accounts/acc_123/balances");
52
+ });
53
+ });
package/src/index.ts CHANGED
@@ -21,6 +21,8 @@
21
21
 
22
22
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
23
23
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
24
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
25
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
24
26
  import {
25
27
  CallToolRequestSchema,
26
28
  ListToolsRequestSchema,
@@ -197,7 +199,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
197
199
  }));
198
200
 
199
201
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
200
- const { name, arguments: args } = request.params;
202
+ const { name, arguments: rawArgs } = request.params;
203
+ const args = rawArgs as Record<string, unknown> | undefined;
201
204
 
202
205
  try {
203
206
  switch (name) {
@@ -245,7 +248,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
245
248
  return { content: [{ type: "text", text: JSON.stringify(await openFinanceRequest("GET", `/open-banking/credit-cards-accounts/v2/accounts/${args?.creditCardAccountId}/transactions?${params}`), null, 2) }] };
246
249
  }
247
250
  case "list_investments": {
248
- const investmentType = args?.investmentType || "BANK_FIXED_INCOMES";
251
+ const investmentType = (args?.investmentType as string) || "BANK_FIXED_INCOMES";
249
252
  const params = new URLSearchParams();
250
253
  if (args?.page) params.set("page", String(args.page));
251
254
  if (args?.pageSize) params.set("page-size", String(args.pageSize));
@@ -260,12 +263,32 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
260
263
  });
261
264
 
262
265
  async function main() {
263
- if (!BASE_URL || !CLIENT_ID || !CLIENT_SECRET) {
264
- console.error("OPEN_FINANCE_BASE_URL, OPEN_FINANCE_CLIENT_ID, and OPEN_FINANCE_CLIENT_SECRET environment variables are required");
265
- process.exit(1);
266
+ if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") {
267
+ const { default: express } = await import("express");
268
+ const { randomUUID } = await import("node:crypto");
269
+ const app = express();
270
+ app.use(express.json());
271
+ const transports = new Map<string, StreamableHTTPServerTransport>();
272
+ app.get("/health", (_req: any, res: any) => res.json({ status: "ok", sessions: transports.size }));
273
+ app.post("/mcp", async (req: any, res: any) => {
274
+ const sid = req.headers["mcp-session-id"] as string | undefined;
275
+ if (sid && transports.has(sid)) { await transports.get(sid)!.handleRequest(req, res, req.body); return; }
276
+ if (!sid && isInitializeRequest(req.body)) {
277
+ const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
278
+ t.onclose = () => { if (t.sessionId) transports.delete(t.sessionId); };
279
+ const s = new Server({ name: "mcp-open-finance", version: "0.1.0" }, { capabilities: { tools: {} } }); (server as any)._requestHandlers.forEach((v: any, k: any) => (s as any)._requestHandlers.set(k, v)); (server as any)._notificationHandlers?.forEach((v: any, k: any) => (s as any)._notificationHandlers.set(k, v)); await s.connect(t);
280
+ await t.handleRequest(req, res, req.body); return;
281
+ }
282
+ res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null });
283
+ });
284
+ app.get("/mcp", async (req: any, res: any) => { const sid = req.headers["mcp-session-id"] as string; if (sid && transports.has(sid)) await transports.get(sid)!.handleRequest(req, res); else res.status(400).send("Invalid session"); });
285
+ app.delete("/mcp", async (req: any, res: any) => { const sid = req.headers["mcp-session-id"] as string; if (sid && transports.has(sid)) await transports.get(sid)!.handleRequest(req, res); else res.status(400).send("Invalid session"); });
286
+ const port = Number(process.env.MCP_PORT) || 3000;
287
+ app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); });
288
+ } else {
289
+ const transport = new StdioServerTransport();
290
+ await server.connect(transport);
266
291
  }
267
- const transport = new StdioServerTransport();
268
- await server.connect(transport);
269
292
  }
270
293
 
271
294
  main().catch(console.error);
package/tsconfig.json CHANGED
@@ -6,6 +6,7 @@
6
6
  "outDir": "./dist",
7
7
  "rootDir": "./src",
8
8
  "strict": true,
9
+ "skipLibCheck": true,
9
10
  "esModuleInterop": true,
10
11
  "declaration": true
11
12
  },