@codespar/mcp-matera 0.1.0-alpha.1

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,127 @@
1
+ # @codespar/mcp-matera
2
+
3
+ > MCP server for **Matera** — Brazilian core-banking infrastructure (BaaS) for fintechs building on top of Pix, DICT, and Pix Automático
4
+
5
+ [![npm](https://img.shields.io/npm/v/@codespar/mcp-matera)](https://www.npmjs.com/package/@codespar/mcp-matera)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ ## Why Matera
9
+
10
+ Matera is **core-banking infrastructure**, not a PSP. Per vendor case studies it processes roughly 10% of Brazil's Pix transactions. Its customer is a **fintech building on top of Pix** — issuing accounts, moving money through DICT, registering Pix Automático agreements — **not a merchant accepting Pix** (that's what Zoop / Asaas / Mercado Pago are for).
11
+
12
+ This opens a segment in the CodeSpar catalog distinct from PSP servers: **fintech-building-on-top-of-Pix**. Matera sits under `banking` alongside Stark Bank and Open Finance, not under `payments`.
13
+
14
+ Use Matera when an agent needs to:
15
+ - Spin up Pix charges against accounts the fintech itself issued
16
+ - Do DICT lookups to resolve a Pix key before moving money
17
+ - Register recurring **Pix Automático** agreements (BCB 2025 product — few providers are live with this)
18
+ - Move money bank-to-bank through the fintech's own Matera rails
19
+
20
+ ## Quick Start
21
+
22
+ ### Claude Desktop
23
+
24
+ Add to `~/.config/claude/claude_desktop_config.json`:
25
+
26
+ ```json
27
+ {
28
+ "mcpServers": {
29
+ "matera": {
30
+ "command": "npx",
31
+ "args": ["-y", "@codespar/mcp-matera"],
32
+ "env": {
33
+ "MATERA_CLIENT_ID": "your-client-id",
34
+ "MATERA_CLIENT_SECRET": "your-client-secret"
35
+ }
36
+ }
37
+ }
38
+ }
39
+ ```
40
+
41
+ ### Claude Code
42
+
43
+ ```bash
44
+ claude mcp add matera -- npx @codespar/mcp-matera
45
+ ```
46
+
47
+ ### Cursor / VS Code
48
+
49
+ Add to `.cursor/mcp.json` or `.vscode/mcp.json`:
50
+
51
+ ```json
52
+ {
53
+ "servers": {
54
+ "matera": {
55
+ "command": "npx",
56
+ "args": ["-y", "@codespar/mcp-matera"],
57
+ "env": {
58
+ "MATERA_CLIENT_ID": "your-client-id",
59
+ "MATERA_CLIENT_SECRET": "your-client-secret"
60
+ }
61
+ }
62
+ }
63
+ }
64
+ ```
65
+
66
+ ## Tools
67
+
68
+ | Tool | Description |
69
+ |------|-------------|
70
+ | `create_pix_charge_static` | Static Pix QR code (reusable, tied to a merchant Pix key) |
71
+ | `create_pix_charge_dynamic` | Dynamic Pix QR code (single-use, expiring) |
72
+ | `get_pix_charge` | Retrieve a Pix charge by txid |
73
+ | `create_pix_payment` | Initiate an outbound Pix transfer |
74
+ | `get_pix_payment` | Retrieve an outbound Pix payment by endToEndId |
75
+ | `refund_pix_payment` | Refund (devolução) a Pix payment |
76
+ | `list_pix_payments` | List Pix payments with filters (start, end, status) |
77
+ | `resolve_pix_key` | DICT lookup — resolve a Pix key to account info |
78
+ | `list_dict_keys` | List DICT keys registered to merchant accounts |
79
+ | `create_pix_automatico` | Register a recurring Pix Automático agreement (BCB 2025) |
80
+
81
+ ## Authentication
82
+
83
+ Matera uses **OAuth 2.0 client_credentials**. The server calls `POST /auth/token` with HTTP Basic auth and caches the bearer token in memory until a minute before expiry.
84
+
85
+ Matera also supports `secret-key` + `data-signature` headers for signed server-to-server calls. That path is not implemented in v0.1; OAuth2 is sufficient for every tool above.
86
+
87
+ ## Environment Variables
88
+
89
+ | Variable | Required | Description |
90
+ |----------|----------|-------------|
91
+ | `MATERA_CLIENT_ID` | Yes | OAuth2 client_id issued by Matera |
92
+ | `MATERA_CLIENT_SECRET` | Yes | OAuth2 client_secret (secret) |
93
+ | `MATERA_BASE_URL` | No | API base URL. Defaults to `https://api.matera.com`. Sandbox URL varies per product line — ask your Matera contact. |
94
+
95
+ ## Status
96
+
97
+ v0.1 — scaffold. Endpoint paths follow Matera's published doc structure but were not validated against a live sandbox. Expect small adjustments to paths and request shapes once the team has credentials. Schemas are deliberately lightweight — only required fields are marked `required`; nested objects accept any shape so agents can pass through fields we haven't modeled yet.
98
+
99
+ ## Roadmap
100
+
101
+ ### v0.2 (planned)
102
+ - Signed-request auth path (`secret-key` + `data-signature`) for endpoints that require it
103
+ - Account opening (abertura de conta) — Matera IB product
104
+ - TED / bank transfers (non-Pix rails)
105
+ - Webhook event helpers
106
+
107
+ ### v0.3 (planned)
108
+ - Internet Banking Server tools (statements, balances, card management)
109
+ - Boleto issuance
110
+ - Pix MED (Mecanismo Especial de Devolução) flow
111
+
112
+ 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).
113
+
114
+ ## Links
115
+
116
+ - [Matera](https://matera.com)
117
+ - [Matera API Documentation](https://doc-api.matera.com)
118
+ - [MCP Dev Brasil](https://github.com/codespar/mcp-dev-brasil)
119
+ - [Landing Page](https://codespar.dev/mcp)
120
+
121
+ ## Enterprise
122
+
123
+ Need governance, budget limits, and audit trails for agent-initiated bank transfers? [CodeSpar Enterprise](https://codespar.dev/enterprise) adds policy engine, payment routing, and compliance templates on top of these MCP servers.
124
+
125
+ ## License
126
+
127
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,358 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP Server for Matera — Brazilian core-banking infrastructure (BaaS).
4
+ *
5
+ * Matera is core-banking rails underneath fintechs, not a PSP. Per vendor
6
+ * case studies it processes ~10% of Brazil's Pix transactions. Its customer
7
+ * is a fintech building on top of Pix (issuing accounts, moving money through
8
+ * DICT, registering recurring Pix Automático agreements) — distinct from PSPs
9
+ * like Zoop/Asaas/Mercado Pago which serve merchants accepting Pix.
10
+ *
11
+ * Tools (10) — Pix focus for v0.1:
12
+ * create_pix_charge_static — static QR code (merchant Pix key, reusable)
13
+ * create_pix_charge_dynamic — dynamic QR code (single-use, expiring)
14
+ * get_pix_charge — fetch a charge by txid
15
+ * create_pix_payment — initiate an outbound Pix transfer
16
+ * get_pix_payment — fetch a payment by endToEndId
17
+ * refund_pix_payment — refund a Pix payment (MED / devolução)
18
+ * list_pix_payments — list payments with start/end/status filters
19
+ * resolve_pix_key — DICT lookup (CPF/CNPJ/email/phone/random → account)
20
+ * list_dict_keys — list DICT keys registered to merchant accounts
21
+ * create_pix_automatico — register a recurring Pix agreement (BCB 2025)
22
+ *
23
+ * Authentication
24
+ * OAuth 2.0 Client Credentials. POST /auth/token with Basic auth
25
+ * (client_id:client_secret) + grant_type=client_credentials. Bearer token
26
+ * cached in memory until a minute before expiry.
27
+ * Matera also supports secret-key + data-signature headers for signed
28
+ * server-to-server calls; not used in v0.1.
29
+ *
30
+ * Environment
31
+ * MATERA_CLIENT_ID OAuth2 client_id
32
+ * MATERA_CLIENT_SECRET OAuth2 client_secret
33
+ * MATERA_BASE_URL optional; defaults to https://api.matera.com
34
+ * (sandbox URL varies per product line)
35
+ *
36
+ * Docs: https://doc-api.matera.com
37
+ */
38
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
39
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
40
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
41
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
42
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
43
+ const CLIENT_ID = process.env.MATERA_CLIENT_ID || "";
44
+ const CLIENT_SECRET = process.env.MATERA_CLIENT_SECRET || "";
45
+ const BASE_URL = process.env.MATERA_BASE_URL || "https://api.matera.com";
46
+ let tokenCache = null;
47
+ async function getAccessToken() {
48
+ const now = Date.now();
49
+ if (tokenCache && tokenCache.expiresAt > now + 60_000) {
50
+ return tokenCache.accessToken;
51
+ }
52
+ const basic = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString("base64");
53
+ const res = await fetch(`${BASE_URL}/auth/token`, {
54
+ method: "POST",
55
+ headers: {
56
+ "Authorization": `Basic ${basic}`,
57
+ "Content-Type": "application/x-www-form-urlencoded",
58
+ },
59
+ body: "grant_type=client_credentials",
60
+ });
61
+ if (!res.ok) {
62
+ throw new Error(`Matera OAuth ${res.status}: ${await res.text()}`);
63
+ }
64
+ const data = (await res.json());
65
+ tokenCache = {
66
+ accessToken: data.access_token,
67
+ expiresAt: now + data.expires_in * 1000,
68
+ };
69
+ return data.access_token;
70
+ }
71
+ async function materaRequest(method, path, body) {
72
+ const token = await getAccessToken();
73
+ const res = await fetch(`${BASE_URL}${path}`, {
74
+ method,
75
+ headers: {
76
+ "Content-Type": "application/json",
77
+ "Authorization": `Bearer ${token}`,
78
+ },
79
+ body: body ? JSON.stringify(body) : undefined,
80
+ });
81
+ if (!res.ok) {
82
+ throw new Error(`Matera API ${res.status}: ${await res.text()}`);
83
+ }
84
+ // Some endpoints (e.g. 204 No Content on refund) may return empty body
85
+ const text = await res.text();
86
+ return text ? JSON.parse(text) : {};
87
+ }
88
+ const server = new Server({ name: "mcp-matera", version: "0.1.0" }, { capabilities: { tools: {} } });
89
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
90
+ tools: [
91
+ {
92
+ name: "create_pix_charge_static",
93
+ description: "Create a static Pix charge (reusable QR code tied to a merchant Pix key). Returns EMV copy-paste payload and QR code image. Use for points-of-sale or donations where the same QR is shown to many payers.",
94
+ inputSchema: {
95
+ type: "object",
96
+ properties: {
97
+ pix_key: { type: "string", description: "Merchant Pix key (CPF, CNPJ, email, phone, or random UUID)" },
98
+ amount: { type: "number", description: "Amount in BRL (decimal, e.g. 10.50). Omit for open-amount QR." },
99
+ description: { type: "string", description: "Free-text description shown to payer" },
100
+ merchant_name: { type: "string", description: "Merchant name as it will appear on the QR payload" },
101
+ merchant_city: { type: "string", description: "Merchant city" },
102
+ txid: { type: "string", description: "Optional merchant-side transaction identifier (26-35 alphanumerics)" },
103
+ },
104
+ required: ["pix_key", "description"],
105
+ },
106
+ },
107
+ {
108
+ name: "create_pix_charge_dynamic",
109
+ description: "Create a dynamic Pix charge (single-use QR with expiration). Returns txid, EMV copy-paste, and QR image. Preferred for e-commerce checkouts and invoices.",
110
+ inputSchema: {
111
+ type: "object",
112
+ properties: {
113
+ pix_key: { type: "string", description: "Merchant Pix key the charge settles to" },
114
+ amount: { type: "number", description: "Amount in BRL (decimal)" },
115
+ description: { type: "string", description: "Description shown to payer" },
116
+ expiration: { type: "number", description: "QR lifetime in seconds (e.g. 3600 = 1 hour)" },
117
+ debtor: {
118
+ type: "object",
119
+ description: "Optional payer identification (BCB requires CPF/CNPJ to be pre-known for some flows)",
120
+ properties: {
121
+ cpf: { type: "string" },
122
+ cnpj: { type: "string" },
123
+ name: { type: "string" },
124
+ },
125
+ },
126
+ txid: { type: "string", description: "Optional merchant-side transaction identifier" },
127
+ },
128
+ required: ["pix_key", "amount", "description", "expiration"],
129
+ },
130
+ },
131
+ {
132
+ name: "get_pix_charge",
133
+ description: "Retrieve a Pix charge (static or dynamic) by txid.",
134
+ inputSchema: {
135
+ type: "object",
136
+ properties: {
137
+ txid: { type: "string", description: "Matera txid returned by create_pix_charge_*" },
138
+ },
139
+ required: ["txid"],
140
+ },
141
+ },
142
+ {
143
+ name: "create_pix_payment",
144
+ description: "Initiate an outbound Pix transfer (ordem de pagamento). Moves money from a debtor account held on Matera to any Pix key in BR. Returns endToEndId once the BCB SPI confirms.",
145
+ inputSchema: {
146
+ type: "object",
147
+ properties: {
148
+ debtor_account: {
149
+ type: "object",
150
+ description: "Source account held on Matera (ispb, branch, account, account type)",
151
+ properties: {
152
+ ispb: { type: "string", description: "ISPB of the debtor bank" },
153
+ branch: { type: "string", description: "Branch (agência)" },
154
+ account: { type: "string", description: "Account number" },
155
+ account_type: { type: "string", enum: ["CACC", "SLRY", "SVGS", "TRAN"], description: "ISO 20022 account type (CACC=corrente, SVGS=poupança)" },
156
+ },
157
+ required: ["ispb", "branch", "account", "account_type"],
158
+ },
159
+ creditor_pix_key: { type: "string", description: "Destination Pix key (CPF/CNPJ/email/phone/random)" },
160
+ amount: { type: "number", description: "Amount in BRL (decimal)" },
161
+ description: { type: "string", description: "Message shown to the recipient (optional)" },
162
+ idempotency_key: { type: "string", description: "Merchant-side unique id to prevent double-send on retry" },
163
+ },
164
+ required: ["debtor_account", "creditor_pix_key", "amount"],
165
+ },
166
+ },
167
+ {
168
+ name: "get_pix_payment",
169
+ description: "Retrieve an outbound Pix payment by endToEndId.",
170
+ inputSchema: {
171
+ type: "object",
172
+ properties: {
173
+ end_to_end_id: { type: "string", description: "32-char BCB endToEndId (E<ispb><yyyyMMddHHmm><random>)" },
174
+ },
175
+ required: ["end_to_end_id"],
176
+ },
177
+ },
178
+ {
179
+ name: "refund_pix_payment",
180
+ description: "Refund (devolução) a Pix payment. Supports full or partial amount. Use reason codes per BCB MED catalog.",
181
+ inputSchema: {
182
+ type: "object",
183
+ properties: {
184
+ end_to_end_id: { type: "string", description: "endToEndId of the payment to refund" },
185
+ amount: { type: "number", description: "Refund amount in BRL. Must be <= original." },
186
+ reason: { type: "string", description: "Reason for refund (BCB MED code or free text)" },
187
+ },
188
+ required: ["end_to_end_id", "amount", "reason"],
189
+ },
190
+ },
191
+ {
192
+ name: "list_pix_payments",
193
+ description: "List outbound Pix payments with optional filters. Useful for reconciliation and agent-driven audit.",
194
+ inputSchema: {
195
+ type: "object",
196
+ properties: {
197
+ start: { type: "string", description: "ISO-8601 start timestamp (inclusive)" },
198
+ end: { type: "string", description: "ISO-8601 end timestamp (exclusive)" },
199
+ status: { type: "string", description: "Filter by status (e.g. ACSC, RJCT, PDNG)" },
200
+ page: { type: "number", description: "Page number (starts at 1)" },
201
+ limit: { type: "number", description: "Page size" },
202
+ },
203
+ },
204
+ },
205
+ {
206
+ name: "resolve_pix_key",
207
+ description: "Resolve a Pix DICT key to the account holder's identity and ISPB/branch/account. Use before sending large transfers to verify the counterparty. Note: DICT queries are rate-limited and logged by BCB.",
208
+ inputSchema: {
209
+ type: "object",
210
+ properties: {
211
+ key: { type: "string", description: "Pix key to resolve (CPF, CNPJ, email, phone, or random UUID)" },
212
+ },
213
+ required: ["key"],
214
+ },
215
+ },
216
+ {
217
+ name: "list_dict_keys",
218
+ description: "List DICT keys registered to the merchant's accounts on Matera.",
219
+ inputSchema: {
220
+ type: "object",
221
+ properties: {
222
+ account: { type: "string", description: "Optional filter: return only keys for this account number" },
223
+ },
224
+ },
225
+ },
226
+ {
227
+ name: "create_pix_automatico",
228
+ description: "Register a Pix Automático agreement (BCB 2025 recurring Pix product). The payer authorizes the merchant to pull recurring amounts on a schedule. Matera is one of the few providers live with this.",
229
+ inputSchema: {
230
+ type: "object",
231
+ properties: {
232
+ payer: {
233
+ type: "object",
234
+ description: "Payer identity + bank",
235
+ properties: {
236
+ cpf: { type: "string" },
237
+ cnpj: { type: "string" },
238
+ name: { type: "string" },
239
+ ispb: { type: "string", description: "Payer bank ISPB" },
240
+ },
241
+ required: ["name"],
242
+ },
243
+ merchant_pix_key: { type: "string", description: "Merchant Pix key receiving the recurring payments" },
244
+ frequency: { type: "string", enum: ["WEEKLY", "MONTHLY", "QUARTERLY", "SEMESTRAL", "ANNUAL"], description: "Recurrence frequency" },
245
+ amount: { type: "number", description: "Amount per charge in BRL (fixed schedule)" },
246
+ first_charge_date: { type: "string", description: "ISO-8601 date of the first charge" },
247
+ end_date: { type: "string", description: "Optional ISO-8601 date to stop the recurrence" },
248
+ description: { type: "string", description: "Description shown to the payer on the authorization screen" },
249
+ },
250
+ required: ["payer", "merchant_pix_key", "frequency", "amount", "first_charge_date", "description"],
251
+ },
252
+ },
253
+ ],
254
+ }));
255
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
256
+ const { name, arguments: args } = request.params;
257
+ const a = (args ?? {});
258
+ try {
259
+ switch (name) {
260
+ case "create_pix_charge_static":
261
+ return { content: [{ type: "text", text: JSON.stringify(await materaRequest("POST", "/pix/charges/static", a), null, 2) }] };
262
+ case "create_pix_charge_dynamic":
263
+ return { content: [{ type: "text", text: JSON.stringify(await materaRequest("POST", "/pix/charges/dynamic", a), null, 2) }] };
264
+ case "get_pix_charge": {
265
+ const txid = encodeURIComponent(String(a.txid ?? ""));
266
+ return { content: [{ type: "text", text: JSON.stringify(await materaRequest("GET", `/pix/charges/${txid}`), null, 2) }] };
267
+ }
268
+ case "create_pix_payment":
269
+ return { content: [{ type: "text", text: JSON.stringify(await materaRequest("POST", "/pix/payments", a), null, 2) }] };
270
+ case "get_pix_payment": {
271
+ const e2e = encodeURIComponent(String(a.end_to_end_id ?? ""));
272
+ return { content: [{ type: "text", text: JSON.stringify(await materaRequest("GET", `/pix/payments/${e2e}`), null, 2) }] };
273
+ }
274
+ case "refund_pix_payment": {
275
+ const e2e = encodeURIComponent(String(a.end_to_end_id ?? ""));
276
+ const body = { amount: a.amount, reason: a.reason };
277
+ return { content: [{ type: "text", text: JSON.stringify(await materaRequest("POST", `/pix/payments/${e2e}/refund`, body), null, 2) }] };
278
+ }
279
+ case "list_pix_payments": {
280
+ const params = new URLSearchParams();
281
+ if (a.start)
282
+ params.set("start", String(a.start));
283
+ if (a.end)
284
+ params.set("end", String(a.end));
285
+ if (a.status)
286
+ params.set("status", String(a.status));
287
+ if (a.page)
288
+ params.set("page", String(a.page));
289
+ if (a.limit)
290
+ params.set("limit", String(a.limit));
291
+ const qs = params.toString();
292
+ return { content: [{ type: "text", text: JSON.stringify(await materaRequest("GET", `/pix/payments${qs ? `?${qs}` : ""}`), null, 2) }] };
293
+ }
294
+ case "resolve_pix_key": {
295
+ const key = encodeURIComponent(String(a.key ?? ""));
296
+ return { content: [{ type: "text", text: JSON.stringify(await materaRequest("GET", `/pix/dict/${key}`), null, 2) }] };
297
+ }
298
+ case "list_dict_keys": {
299
+ const params = new URLSearchParams();
300
+ if (a.account)
301
+ params.set("account", String(a.account));
302
+ const qs = params.toString();
303
+ return { content: [{ type: "text", text: JSON.stringify(await materaRequest("GET", `/pix/dict/keys${qs ? `?${qs}` : ""}`), null, 2) }] };
304
+ }
305
+ case "create_pix_automatico":
306
+ return { content: [{ type: "text", text: JSON.stringify(await materaRequest("POST", "/pix/automatico", a), null, 2) }] };
307
+ default:
308
+ return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
309
+ }
310
+ }
311
+ catch (err) {
312
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
313
+ }
314
+ });
315
+ async function main() {
316
+ if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") {
317
+ const { default: express } = await import("express");
318
+ const { randomUUID } = await import("node:crypto");
319
+ const app = express();
320
+ app.use(express.json());
321
+ const transports = new Map();
322
+ app.get("/health", (_req, res) => res.json({ status: "ok", sessions: transports.size }));
323
+ app.post("/mcp", async (req, res) => {
324
+ const sid = req.headers["mcp-session-id"];
325
+ if (sid && transports.has(sid)) {
326
+ await transports.get(sid).handleRequest(req, res, req.body);
327
+ return;
328
+ }
329
+ if (!sid && isInitializeRequest(req.body)) {
330
+ const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
331
+ t.onclose = () => { if (t.sessionId)
332
+ transports.delete(t.sessionId); };
333
+ const s = new Server({ name: "mcp-matera", version: "0.1.0" }, { capabilities: { tools: {} } });
334
+ server._requestHandlers.forEach((v, k) => s._requestHandlers.set(k, v));
335
+ server._notificationHandlers?.forEach((v, k) => s._notificationHandlers.set(k, v));
336
+ await s.connect(t);
337
+ await t.handleRequest(req, res, req.body);
338
+ return;
339
+ }
340
+ res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null });
341
+ });
342
+ app.get("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"]; if (sid && transports.has(sid))
343
+ await transports.get(sid).handleRequest(req, res);
344
+ else
345
+ res.status(400).send("Invalid session"); });
346
+ app.delete("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"]; if (sid && transports.has(sid))
347
+ await transports.get(sid).handleRequest(req, res);
348
+ else
349
+ res.status(400).send("Invalid session"); });
350
+ const port = Number(process.env.MCP_PORT) || 3000;
351
+ app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); });
352
+ }
353
+ else {
354
+ const transport = new StdioServerTransport();
355
+ await server.connect(transport);
356
+ }
357
+ }
358
+ main().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@codespar/mcp-matera",
3
+ "version": "0.1.0-alpha.1",
4
+ "description": "MCP server for Matera — Brazilian core-banking infrastructure (BaaS) for fintechs building on top of Pix, DICT, and Pix Automático",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "bin": {
8
+ "mcp-matera": "./dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "start": "node dist/index.js"
13
+ },
14
+ "dependencies": {
15
+ "@modelcontextprotocol/sdk": "^1.0.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^25.5.0",
19
+ "typescript": "^5.8.0"
20
+ },
21
+ "license": "MIT",
22
+ "keywords": [
23
+ "mcp",
24
+ "matera",
25
+ "banking",
26
+ "baas",
27
+ "core-banking",
28
+ "pix",
29
+ "pix-automatico",
30
+ "dict",
31
+ "brazil",
32
+ "fintech"
33
+ ],
34
+ "mcpName": "io.github.codespar/mcp-matera"
35
+ }
package/server.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.codespar/mcp-matera",
4
+ "description": "MCP server for Matera — Brazilian core-banking infrastructure (BaaS) for fintechs building on top of Pix, DICT, and Pix Automático",
5
+ "repository": {
6
+ "url": "https://github.com/codespar/mcp-dev-brasil",
7
+ "source": "github",
8
+ "subfolder": "packages/banking/matera"
9
+ },
10
+ "version": "0.1.0",
11
+ "packages": [
12
+ {
13
+ "registryType": "npm",
14
+ "identifier": "@codespar/mcp-matera",
15
+ "version": "0.1.0",
16
+ "transport": {
17
+ "type": "stdio"
18
+ },
19
+ "environmentVariables": [
20
+ {
21
+ "name": "MATERA_CLIENT_ID",
22
+ "description": "OAuth2 client_id issued by Matera",
23
+ "isRequired": true,
24
+ "format": "string",
25
+ "isSecret": false
26
+ },
27
+ {
28
+ "name": "MATERA_CLIENT_SECRET",
29
+ "description": "OAuth2 client_secret issued by Matera",
30
+ "isRequired": true,
31
+ "format": "string",
32
+ "isSecret": true
33
+ },
34
+ {
35
+ "name": "MATERA_BASE_URL",
36
+ "description": "API base URL. Defaults to https://api.matera.com (production)",
37
+ "isRequired": false,
38
+ "format": "string",
39
+ "isSecret": false
40
+ }
41
+ ]
42
+ }
43
+ ]
44
+ }
package/src/index.ts ADDED
@@ -0,0 +1,357 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP Server for Matera — Brazilian core-banking infrastructure (BaaS).
4
+ *
5
+ * Matera is core-banking rails underneath fintechs, not a PSP. Per vendor
6
+ * case studies it processes ~10% of Brazil's Pix transactions. Its customer
7
+ * is a fintech building on top of Pix (issuing accounts, moving money through
8
+ * DICT, registering recurring Pix Automático agreements) — distinct from PSPs
9
+ * like Zoop/Asaas/Mercado Pago which serve merchants accepting Pix.
10
+ *
11
+ * Tools (10) — Pix focus for v0.1:
12
+ * create_pix_charge_static — static QR code (merchant Pix key, reusable)
13
+ * create_pix_charge_dynamic — dynamic QR code (single-use, expiring)
14
+ * get_pix_charge — fetch a charge by txid
15
+ * create_pix_payment — initiate an outbound Pix transfer
16
+ * get_pix_payment — fetch a payment by endToEndId
17
+ * refund_pix_payment — refund a Pix payment (MED / devolução)
18
+ * list_pix_payments — list payments with start/end/status filters
19
+ * resolve_pix_key — DICT lookup (CPF/CNPJ/email/phone/random → account)
20
+ * list_dict_keys — list DICT keys registered to merchant accounts
21
+ * create_pix_automatico — register a recurring Pix agreement (BCB 2025)
22
+ *
23
+ * Authentication
24
+ * OAuth 2.0 Client Credentials. POST /auth/token with Basic auth
25
+ * (client_id:client_secret) + grant_type=client_credentials. Bearer token
26
+ * cached in memory until a minute before expiry.
27
+ * Matera also supports secret-key + data-signature headers for signed
28
+ * server-to-server calls; not used in v0.1.
29
+ *
30
+ * Environment
31
+ * MATERA_CLIENT_ID OAuth2 client_id
32
+ * MATERA_CLIENT_SECRET OAuth2 client_secret
33
+ * MATERA_BASE_URL optional; defaults to https://api.matera.com
34
+ * (sandbox URL varies per product line)
35
+ *
36
+ * Docs: https://doc-api.matera.com
37
+ */
38
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
39
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
40
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
41
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
42
+ import {
43
+ CallToolRequestSchema,
44
+ ListToolsRequestSchema,
45
+ } from "@modelcontextprotocol/sdk/types.js";
46
+
47
+ const CLIENT_ID = process.env.MATERA_CLIENT_ID || "";
48
+ const CLIENT_SECRET = process.env.MATERA_CLIENT_SECRET || "";
49
+ const BASE_URL = process.env.MATERA_BASE_URL || "https://api.matera.com";
50
+
51
+ let tokenCache: { accessToken: string; expiresAt: number } | null = null;
52
+
53
+ async function getAccessToken(): Promise<string> {
54
+ const now = Date.now();
55
+ if (tokenCache && tokenCache.expiresAt > now + 60_000) {
56
+ return tokenCache.accessToken;
57
+ }
58
+ const basic = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString("base64");
59
+ const res = await fetch(`${BASE_URL}/auth/token`, {
60
+ method: "POST",
61
+ headers: {
62
+ "Authorization": `Basic ${basic}`,
63
+ "Content-Type": "application/x-www-form-urlencoded",
64
+ },
65
+ body: "grant_type=client_credentials",
66
+ });
67
+ if (!res.ok) {
68
+ throw new Error(`Matera OAuth ${res.status}: ${await res.text()}`);
69
+ }
70
+ const data = (await res.json()) as { access_token: string; expires_in: number };
71
+ tokenCache = {
72
+ accessToken: data.access_token,
73
+ expiresAt: now + data.expires_in * 1000,
74
+ };
75
+ return data.access_token;
76
+ }
77
+
78
+ async function materaRequest(method: string, path: string, body?: unknown): Promise<unknown> {
79
+ const token = await getAccessToken();
80
+ const res = await fetch(`${BASE_URL}${path}`, {
81
+ method,
82
+ headers: {
83
+ "Content-Type": "application/json",
84
+ "Authorization": `Bearer ${token}`,
85
+ },
86
+ body: body ? JSON.stringify(body) : undefined,
87
+ });
88
+ if (!res.ok) {
89
+ throw new Error(`Matera API ${res.status}: ${await res.text()}`);
90
+ }
91
+ // Some endpoints (e.g. 204 No Content on refund) may return empty body
92
+ const text = await res.text();
93
+ return text ? JSON.parse(text) : {};
94
+ }
95
+
96
+ const server = new Server(
97
+ { name: "mcp-matera", version: "0.1.0" },
98
+ { capabilities: { tools: {} } },
99
+ );
100
+
101
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
102
+ tools: [
103
+ {
104
+ name: "create_pix_charge_static",
105
+ description: "Create a static Pix charge (reusable QR code tied to a merchant Pix key). Returns EMV copy-paste payload and QR code image. Use for points-of-sale or donations where the same QR is shown to many payers.",
106
+ inputSchema: {
107
+ type: "object",
108
+ properties: {
109
+ pix_key: { type: "string", description: "Merchant Pix key (CPF, CNPJ, email, phone, or random UUID)" },
110
+ amount: { type: "number", description: "Amount in BRL (decimal, e.g. 10.50). Omit for open-amount QR." },
111
+ description: { type: "string", description: "Free-text description shown to payer" },
112
+ merchant_name: { type: "string", description: "Merchant name as it will appear on the QR payload" },
113
+ merchant_city: { type: "string", description: "Merchant city" },
114
+ txid: { type: "string", description: "Optional merchant-side transaction identifier (26-35 alphanumerics)" },
115
+ },
116
+ required: ["pix_key", "description"],
117
+ },
118
+ },
119
+ {
120
+ name: "create_pix_charge_dynamic",
121
+ description: "Create a dynamic Pix charge (single-use QR with expiration). Returns txid, EMV copy-paste, and QR image. Preferred for e-commerce checkouts and invoices.",
122
+ inputSchema: {
123
+ type: "object",
124
+ properties: {
125
+ pix_key: { type: "string", description: "Merchant Pix key the charge settles to" },
126
+ amount: { type: "number", description: "Amount in BRL (decimal)" },
127
+ description: { type: "string", description: "Description shown to payer" },
128
+ expiration: { type: "number", description: "QR lifetime in seconds (e.g. 3600 = 1 hour)" },
129
+ debtor: {
130
+ type: "object",
131
+ description: "Optional payer identification (BCB requires CPF/CNPJ to be pre-known for some flows)",
132
+ properties: {
133
+ cpf: { type: "string" },
134
+ cnpj: { type: "string" },
135
+ name: { type: "string" },
136
+ },
137
+ },
138
+ txid: { type: "string", description: "Optional merchant-side transaction identifier" },
139
+ },
140
+ required: ["pix_key", "amount", "description", "expiration"],
141
+ },
142
+ },
143
+ {
144
+ name: "get_pix_charge",
145
+ description: "Retrieve a Pix charge (static or dynamic) by txid.",
146
+ inputSchema: {
147
+ type: "object",
148
+ properties: {
149
+ txid: { type: "string", description: "Matera txid returned by create_pix_charge_*" },
150
+ },
151
+ required: ["txid"],
152
+ },
153
+ },
154
+ {
155
+ name: "create_pix_payment",
156
+ description: "Initiate an outbound Pix transfer (ordem de pagamento). Moves money from a debtor account held on Matera to any Pix key in BR. Returns endToEndId once the BCB SPI confirms.",
157
+ inputSchema: {
158
+ type: "object",
159
+ properties: {
160
+ debtor_account: {
161
+ type: "object",
162
+ description: "Source account held on Matera (ispb, branch, account, account type)",
163
+ properties: {
164
+ ispb: { type: "string", description: "ISPB of the debtor bank" },
165
+ branch: { type: "string", description: "Branch (agência)" },
166
+ account: { type: "string", description: "Account number" },
167
+ account_type: { type: "string", enum: ["CACC", "SLRY", "SVGS", "TRAN"], description: "ISO 20022 account type (CACC=corrente, SVGS=poupança)" },
168
+ },
169
+ required: ["ispb", "branch", "account", "account_type"],
170
+ },
171
+ creditor_pix_key: { type: "string", description: "Destination Pix key (CPF/CNPJ/email/phone/random)" },
172
+ amount: { type: "number", description: "Amount in BRL (decimal)" },
173
+ description: { type: "string", description: "Message shown to the recipient (optional)" },
174
+ idempotency_key: { type: "string", description: "Merchant-side unique id to prevent double-send on retry" },
175
+ },
176
+ required: ["debtor_account", "creditor_pix_key", "amount"],
177
+ },
178
+ },
179
+ {
180
+ name: "get_pix_payment",
181
+ description: "Retrieve an outbound Pix payment by endToEndId.",
182
+ inputSchema: {
183
+ type: "object",
184
+ properties: {
185
+ end_to_end_id: { type: "string", description: "32-char BCB endToEndId (E<ispb><yyyyMMddHHmm><random>)" },
186
+ },
187
+ required: ["end_to_end_id"],
188
+ },
189
+ },
190
+ {
191
+ name: "refund_pix_payment",
192
+ description: "Refund (devolução) a Pix payment. Supports full or partial amount. Use reason codes per BCB MED catalog.",
193
+ inputSchema: {
194
+ type: "object",
195
+ properties: {
196
+ end_to_end_id: { type: "string", description: "endToEndId of the payment to refund" },
197
+ amount: { type: "number", description: "Refund amount in BRL. Must be <= original." },
198
+ reason: { type: "string", description: "Reason for refund (BCB MED code or free text)" },
199
+ },
200
+ required: ["end_to_end_id", "amount", "reason"],
201
+ },
202
+ },
203
+ {
204
+ name: "list_pix_payments",
205
+ description: "List outbound Pix payments with optional filters. Useful for reconciliation and agent-driven audit.",
206
+ inputSchema: {
207
+ type: "object",
208
+ properties: {
209
+ start: { type: "string", description: "ISO-8601 start timestamp (inclusive)" },
210
+ end: { type: "string", description: "ISO-8601 end timestamp (exclusive)" },
211
+ status: { type: "string", description: "Filter by status (e.g. ACSC, RJCT, PDNG)" },
212
+ page: { type: "number", description: "Page number (starts at 1)" },
213
+ limit: { type: "number", description: "Page size" },
214
+ },
215
+ },
216
+ },
217
+ {
218
+ name: "resolve_pix_key",
219
+ description: "Resolve a Pix DICT key to the account holder's identity and ISPB/branch/account. Use before sending large transfers to verify the counterparty. Note: DICT queries are rate-limited and logged by BCB.",
220
+ inputSchema: {
221
+ type: "object",
222
+ properties: {
223
+ key: { type: "string", description: "Pix key to resolve (CPF, CNPJ, email, phone, or random UUID)" },
224
+ },
225
+ required: ["key"],
226
+ },
227
+ },
228
+ {
229
+ name: "list_dict_keys",
230
+ description: "List DICT keys registered to the merchant's accounts on Matera.",
231
+ inputSchema: {
232
+ type: "object",
233
+ properties: {
234
+ account: { type: "string", description: "Optional filter: return only keys for this account number" },
235
+ },
236
+ },
237
+ },
238
+ {
239
+ name: "create_pix_automatico",
240
+ description: "Register a Pix Automático agreement (BCB 2025 recurring Pix product). The payer authorizes the merchant to pull recurring amounts on a schedule. Matera is one of the few providers live with this.",
241
+ inputSchema: {
242
+ type: "object",
243
+ properties: {
244
+ payer: {
245
+ type: "object",
246
+ description: "Payer identity + bank",
247
+ properties: {
248
+ cpf: { type: "string" },
249
+ cnpj: { type: "string" },
250
+ name: { type: "string" },
251
+ ispb: { type: "string", description: "Payer bank ISPB" },
252
+ },
253
+ required: ["name"],
254
+ },
255
+ merchant_pix_key: { type: "string", description: "Merchant Pix key receiving the recurring payments" },
256
+ frequency: { type: "string", enum: ["WEEKLY", "MONTHLY", "QUARTERLY", "SEMESTRAL", "ANNUAL"], description: "Recurrence frequency" },
257
+ amount: { type: "number", description: "Amount per charge in BRL (fixed schedule)" },
258
+ first_charge_date: { type: "string", description: "ISO-8601 date of the first charge" },
259
+ end_date: { type: "string", description: "Optional ISO-8601 date to stop the recurrence" },
260
+ description: { type: "string", description: "Description shown to the payer on the authorization screen" },
261
+ },
262
+ required: ["payer", "merchant_pix_key", "frequency", "amount", "first_charge_date", "description"],
263
+ },
264
+ },
265
+ ],
266
+ }));
267
+
268
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
269
+ const { name, arguments: args } = request.params;
270
+ const a = (args ?? {}) as Record<string, unknown>;
271
+ try {
272
+ switch (name) {
273
+ case "create_pix_charge_static":
274
+ return { content: [{ type: "text", text: JSON.stringify(await materaRequest("POST", "/pix/charges/static", a), null, 2) }] };
275
+ case "create_pix_charge_dynamic":
276
+ return { content: [{ type: "text", text: JSON.stringify(await materaRequest("POST", "/pix/charges/dynamic", a), null, 2) }] };
277
+ case "get_pix_charge": {
278
+ const txid = encodeURIComponent(String(a.txid ?? ""));
279
+ return { content: [{ type: "text", text: JSON.stringify(await materaRequest("GET", `/pix/charges/${txid}`), null, 2) }] };
280
+ }
281
+ case "create_pix_payment":
282
+ return { content: [{ type: "text", text: JSON.stringify(await materaRequest("POST", "/pix/payments", a), null, 2) }] };
283
+ case "get_pix_payment": {
284
+ const e2e = encodeURIComponent(String(a.end_to_end_id ?? ""));
285
+ return { content: [{ type: "text", text: JSON.stringify(await materaRequest("GET", `/pix/payments/${e2e}`), null, 2) }] };
286
+ }
287
+ case "refund_pix_payment": {
288
+ const e2e = encodeURIComponent(String(a.end_to_end_id ?? ""));
289
+ const body: Record<string, unknown> = { amount: a.amount, reason: a.reason };
290
+ return { content: [{ type: "text", text: JSON.stringify(await materaRequest("POST", `/pix/payments/${e2e}/refund`, body), null, 2) }] };
291
+ }
292
+ case "list_pix_payments": {
293
+ const params = new URLSearchParams();
294
+ if (a.start) params.set("start", String(a.start));
295
+ if (a.end) params.set("end", String(a.end));
296
+ if (a.status) params.set("status", String(a.status));
297
+ if (a.page) params.set("page", String(a.page));
298
+ if (a.limit) params.set("limit", String(a.limit));
299
+ const qs = params.toString();
300
+ return { content: [{ type: "text", text: JSON.stringify(await materaRequest("GET", `/pix/payments${qs ? `?${qs}` : ""}`), null, 2) }] };
301
+ }
302
+ case "resolve_pix_key": {
303
+ const key = encodeURIComponent(String(a.key ?? ""));
304
+ return { content: [{ type: "text", text: JSON.stringify(await materaRequest("GET", `/pix/dict/${key}`), null, 2) }] };
305
+ }
306
+ case "list_dict_keys": {
307
+ const params = new URLSearchParams();
308
+ if (a.account) params.set("account", String(a.account));
309
+ const qs = params.toString();
310
+ return { content: [{ type: "text", text: JSON.stringify(await materaRequest("GET", `/pix/dict/keys${qs ? `?${qs}` : ""}`), null, 2) }] };
311
+ }
312
+ case "create_pix_automatico":
313
+ return { content: [{ type: "text", text: JSON.stringify(await materaRequest("POST", "/pix/automatico", a), null, 2) }] };
314
+ default:
315
+ return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
316
+ }
317
+ } catch (err) {
318
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
319
+ }
320
+ });
321
+
322
+ async function main() {
323
+ if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") {
324
+ const { default: express } = await import("express");
325
+ const { randomUUID } = await import("node:crypto");
326
+ const app = express();
327
+ app.use(express.json());
328
+ const transports = new Map<string, StreamableHTTPServerTransport>();
329
+ app.get("/health", (_req, res) => res.json({ status: "ok", sessions: transports.size }));
330
+ app.post("/mcp", async (req, res) => {
331
+ const sid = req.headers["mcp-session-id"] as string | undefined;
332
+ if (sid && transports.has(sid)) {
333
+ await transports.get(sid)!.handleRequest(req, res, req.body);
334
+ return;
335
+ }
336
+ if (!sid && isInitializeRequest(req.body)) {
337
+ const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
338
+ t.onclose = () => { if (t.sessionId) transports.delete(t.sessionId); };
339
+ const s = new Server({ name: "mcp-matera", version: "0.1.0" }, { capabilities: { tools: {} } });
340
+ (server as any)._requestHandlers.forEach((v: any, k: any) => (s as any)._requestHandlers.set(k, v));
341
+ (server as any)._notificationHandlers?.forEach((v: any, k: any) => (s as any)._notificationHandlers.set(k, v));
342
+ await s.connect(t);
343
+ await t.handleRequest(req, res, req.body);
344
+ return;
345
+ }
346
+ res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null });
347
+ });
348
+ app.get("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"] as string | undefined; if (sid && transports.has(sid)) await transports.get(sid)!.handleRequest(req, res); else res.status(400).send("Invalid session"); });
349
+ app.delete("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"] as string | undefined; if (sid && transports.has(sid)) await transports.get(sid)!.handleRequest(req, res); else res.status(400).send("Invalid session"); });
350
+ const port = Number(process.env.MCP_PORT) || 3000;
351
+ app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); });
352
+ } else {
353
+ const transport = new StdioServerTransport();
354
+ await server.connect(transport);
355
+ }
356
+ }
357
+ main().catch(console.error);
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "esModuleInterop": true,
11
+ "declaration": true
12
+ },
13
+ "include": ["src"]
14
+ }