@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 +120 -0
- package/dist/index.js +44 -6
- package/package.json +3 -2
- package/server.json +30 -0
- package/src/__tests__/index.test.ts +53 -0
- package/src/index.ts +30 -7
- package/tsconfig.json +1 -0
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
|
+
[](https://www.npmjs.com/package/@codespar/mcp-open-finance)
|
|
6
|
+
[](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:
|
|
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 (
|
|
262
|
-
|
|
263
|
-
|
|
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.
|
|
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:
|
|
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 (
|
|
264
|
-
|
|
265
|
-
|
|
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);
|