@codespar/mcp-banco-do-brasil 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,60 @@
1
+ # @codespar/mcp-banco-do-brasil
2
+
3
+ MCP server for [Banco do Brasil](https://developers.bb.com.br) — Brazil's top public bank.
4
+
5
+ BB exposes one of the broadest public-bank API surfaces in the country across Pix, Cobranças, Conta-Corrente, Open Finance, and Arrecadação. Merchants doing high-volume Pix and boleto operations integrate directly with BB instead of going through a PSP.
6
+
7
+ ## Status: alpha (`0.1.0-alpha.1`)
8
+
9
+ BB's Developer Portal is **contract-gated** — the full OpenAPI specs are visible only after merchant onboarding. Pix paths follow the BACEN Pix v2 standard; boleto, account, and statement paths are best-guesses based on BB's public marketing pages and conventions shared with peers (Itaú, Santander, Bradesco). Every unverified path is flagged `TODO(verify)` in `src/index.ts`. Pin to exact versions during `0.1.x`.
10
+
11
+ ## Tools (10)
12
+
13
+ | Tool | Purpose |
14
+ |---|---|
15
+ | `create_pix_cob` | Create immediate Pix charge (cob) |
16
+ | `get_pix_cob` | Get an immediate Pix charge by txid |
17
+ | `list_pix_cob` | List immediate Pix charges by date range |
18
+ | `create_pix_devolucao` | Refund a received Pix (devolução) |
19
+ | `get_pix_devolucao` | Retrieve a devolução by id |
20
+ | `resolve_dict_key` | Resolve a DICT key to account data |
21
+ | `register_dict_key` | Register a DICT key on a BB account |
22
+ | `delete_dict_key` | Delete a DICT key owned by the merchant |
23
+ | `register_boleto` | Issue a boleto via BB Cobranças |
24
+ | `get_boleto` | Retrieve a boleto by nosso_numero |
25
+ | `cancel_boleto` | Cancel (baixa) an outstanding boleto |
26
+ | `get_account_balance` | Conta-corrente balance |
27
+ | `get_statement` | Account statement transactions |
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ npm install @codespar/mcp-banco-do-brasil@0.1.0-alpha.1
33
+ ```
34
+
35
+ ## Environment
36
+
37
+ ```bash
38
+ BB_CLIENT_ID="..." # OAuth client_id from developers.bb.com.br
39
+ BB_CLIENT_SECRET="..." # OAuth client_secret
40
+ BB_DEVELOPER_APP_KEY="..." # gw-dev-app-key — required on most calls
41
+ BB_CERT_PATH="/abs/path/client.crt" # mTLS cert (production only)
42
+ BB_KEY_PATH="/abs/path/client.key" # mTLS key (production only)
43
+ BB_ENV="sandbox" # or "production" (default: sandbox)
44
+ ```
45
+
46
+ ## Authentication
47
+
48
+ - **OAuth2 `client_credentials`** — token endpoint at `oauth.{env}.bb.com.br`. Bearer cached until ~60s before expiry.
49
+ - **mTLS** — required by BACEN for Pix v2 in production. Sandbox typically allows TLS-only; cert/key are optional in sandbox and required in production.
50
+ - **`gw-dev-app-key`** — BB API gateway key, appended as a query param on all calls.
51
+
52
+ ## Run
53
+
54
+ ```bash
55
+ npx @codespar/mcp-banco-do-brasil
56
+ ```
57
+
58
+ ## License
59
+
60
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,560 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP Server for Banco do Brasil — Brazil's top public bank.
4
+ *
5
+ * BB exposes one of the broadest bank API surfaces in the country, covering
6
+ * Pix, Cobranças (boleto), Conta-Corrente, Open Finance, and Arrecadação
7
+ * via the Developer Portal at developers.bb.com.br.
8
+ *
9
+ * Tools (13):
10
+ * create_pix_cob — create immediate Pix charge (cob) with QR
11
+ * get_pix_cob — retrieve an immediate Pix charge by txid
12
+ * list_pix_cob — list immediate Pix charges by date range
13
+ * create_pix_devolucao — refund (devolução) a received Pix
14
+ * get_pix_devolucao — retrieve a devolução by id
15
+ * resolve_dict_key — resolve a DICT key to account data
16
+ * register_dict_key — register a DICT key on a BB account
17
+ * delete_dict_key — delete a DICT key owned by the merchant
18
+ * register_boleto — issue a boleto via BB Cobranças
19
+ * get_boleto — retrieve a boleto by nosso_numero
20
+ * cancel_boleto — cancel (baixa) an outstanding boleto
21
+ * get_account_balance — Conta-Corrente balance
22
+ * get_statement — Conta-Corrente statement / transactions
23
+ *
24
+ * Authentication
25
+ * OAuth 2.0 client_credentials + mandatory mTLS in production. BACEN
26
+ * requires mTLS for Pix v2; BB enforces it in production across product
27
+ * families. Sandbox typically accepts TLS-only — cert/key envs are
28
+ * therefore optional but recommended.
29
+ *
30
+ * Additionally, BB requires a developer-app-key (`gw-dev-app-key`)
31
+ * query param on every API call for traffic accounting.
32
+ *
33
+ * Version: 0.1.0-alpha.1
34
+ * developers.bb.com.br is contract-gated — full OpenAPI specs are visible
35
+ * only to onboarded merchants. Pix paths follow BACEN Pix v2 standard.
36
+ * Boleto / Conta-Corrente paths are best-guesses based on BB public docs
37
+ * and conventions shared with peers (Itaú, Santander, Bradesco). Every
38
+ * unverified path is marked TODO(verify). Treat 0.1.x as alpha and pin
39
+ * to exact versions.
40
+ *
41
+ * Environment
42
+ * BB_CLIENT_ID OAuth client id
43
+ * BB_CLIENT_SECRET OAuth client secret
44
+ * BB_DEVELOPER_APP_KEY gw-dev-app-key query param
45
+ * BB_CERT_PATH path to mTLS client cert (production)
46
+ * BB_KEY_PATH path to mTLS private key (production)
47
+ * BB_ENV "sandbox" | "production" (default: sandbox)
48
+ *
49
+ * Docs: https://developers.bb.com.br
50
+ */
51
+ import { readFileSync } from "node:fs";
52
+ import { Agent as HttpsAgent } from "node:https";
53
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
54
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
55
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
56
+ const CLIENT_ID = process.env.BB_CLIENT_ID || "";
57
+ const CLIENT_SECRET = process.env.BB_CLIENT_SECRET || "";
58
+ const DEVELOPER_APP_KEY = process.env.BB_DEVELOPER_APP_KEY || "";
59
+ const CERT_PATH = process.env.BB_CERT_PATH || "";
60
+ const KEY_PATH = process.env.BB_KEY_PATH || "";
61
+ const BB_ENV = (process.env.BB_ENV || "sandbox").toLowerCase();
62
+ const IS_PROD = BB_ENV === "production";
63
+ // TODO(verify): production base hostnames. BB documents distinct hosts per
64
+ // product family (api.bb.com.br for some, api-pix.bb.com.br for Pix). The
65
+ // sandbox subdomain is api.sandbox.bb.com.br for most products.
66
+ const BASE_URL = IS_PROD ? "https://api.bb.com.br" : "https://api.sandbox.bb.com.br";
67
+ const OAUTH_URL = IS_PROD
68
+ ? "https://oauth.bb.com.br/oauth/token"
69
+ : "https://oauth.sandbox.bb.com.br/oauth/token";
70
+ // Lazy-load the mTLS agent so `--help` / schema introspection doesn't crash
71
+ // when certs are missing. mTLS is required by BACEN in production but BB
72
+ // sandbox typically accepts TLS-only.
73
+ let mtlsAgent = null;
74
+ function getMtlsAgent() {
75
+ if (mtlsAgent)
76
+ return mtlsAgent;
77
+ if (!CERT_PATH || !KEY_PATH) {
78
+ if (IS_PROD) {
79
+ throw new Error("BB mTLS certificates are required in production. Set BB_CERT_PATH and BB_KEY_PATH " +
80
+ "to the client cert and private-key files issued by developers.bb.com.br.");
81
+ }
82
+ return null; // sandbox: fall back to system TLS
83
+ }
84
+ mtlsAgent = new HttpsAgent({
85
+ cert: readFileSync(CERT_PATH),
86
+ key: readFileSync(KEY_PATH),
87
+ keepAlive: true,
88
+ });
89
+ return mtlsAgent;
90
+ }
91
+ let tokenCache = null;
92
+ async function getAccessToken() {
93
+ const now = Date.now();
94
+ if (tokenCache && tokenCache.expiresAt > now + 60_000) {
95
+ return tokenCache.accessToken;
96
+ }
97
+ const basic = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString("base64");
98
+ const res = await fetchWithMtls(OAUTH_URL, {
99
+ method: "POST",
100
+ headers: {
101
+ Authorization: `Basic ${basic}`,
102
+ "Content-Type": "application/x-www-form-urlencoded",
103
+ },
104
+ // TODO(verify): scope. BB scopes are product-specific (cob.write,
105
+ // cob.read, pix.write, pix.read, dict.read, etc.). The sandbox
106
+ // commonly accepts a blank/all scope for client_credentials.
107
+ body: "grant_type=client_credentials",
108
+ });
109
+ if (!res.ok) {
110
+ throw new Error(`BB OAuth ${res.status}: ${await res.text()}`);
111
+ }
112
+ const data = (await res.json());
113
+ tokenCache = {
114
+ accessToken: data.access_token,
115
+ expiresAt: now + data.expires_in * 1000,
116
+ };
117
+ return data.access_token;
118
+ }
119
+ // Node's global fetch does not honour an https.Agent for mTLS; we drop down
120
+ // to node:https manually so the client cert is presented on every call.
121
+ async function fetchWithMtls(url, init) {
122
+ const agent = getMtlsAgent();
123
+ const { request } = await import("node:https");
124
+ return new Promise((resolve, reject) => {
125
+ const u = new URL(url);
126
+ const req = request({
127
+ ...(agent ? { agent } : {}),
128
+ method: init.method,
129
+ hostname: u.hostname,
130
+ port: u.port || 443,
131
+ path: u.pathname + u.search,
132
+ headers: init.headers ?? {},
133
+ }, (res) => {
134
+ const chunks = [];
135
+ res.on("data", (c) => chunks.push(c));
136
+ res.on("end", () => {
137
+ const buf = Buffer.concat(chunks);
138
+ const status = res.statusCode ?? 0;
139
+ resolve({
140
+ ok: status >= 200 && status < 300,
141
+ status,
142
+ text: async () => buf.toString("utf8"),
143
+ json: async () => JSON.parse(buf.toString("utf8")),
144
+ });
145
+ });
146
+ });
147
+ req.on("error", reject);
148
+ if (init.body)
149
+ req.write(init.body);
150
+ req.end();
151
+ });
152
+ }
153
+ function appendAppKey(path) {
154
+ if (!DEVELOPER_APP_KEY)
155
+ return path;
156
+ const sep = path.includes("?") ? "&" : "?";
157
+ return `${path}${sep}gw-dev-app-key=${encodeURIComponent(DEVELOPER_APP_KEY)}`;
158
+ }
159
+ async function bbRequest(method, path, body) {
160
+ const token = await getAccessToken();
161
+ const fullPath = appendAppKey(path);
162
+ const res = await fetchWithMtls(`${BASE_URL}${fullPath}`, {
163
+ method,
164
+ headers: {
165
+ "Content-Type": "application/json",
166
+ Authorization: `Bearer ${token}`,
167
+ },
168
+ body: body ? JSON.stringify(body) : undefined,
169
+ });
170
+ if (!res.ok) {
171
+ throw new Error(`BB API ${res.status}: ${await res.text()}`);
172
+ }
173
+ return res.json();
174
+ }
175
+ const server = new Server({ name: "mcp-banco-do-brasil", version: "0.1.0-alpha.1" }, { capabilities: { tools: {} } });
176
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
177
+ tools: [
178
+ {
179
+ name: "create_pix_cob",
180
+ description: "Create an immediate Pix charge (cob) with QR code. Returns txid, EMV copy-paste payload, and location URL. BACEN Pix v2 standard.",
181
+ inputSchema: {
182
+ type: "object",
183
+ properties: {
184
+ amount: { type: "string", description: "Amount in BRL major units, e.g. '99.90'" },
185
+ payer: {
186
+ type: "object",
187
+ description: "Payer identification (CPF/CNPJ + name)",
188
+ properties: {
189
+ document: { type: "string", description: "CPF or CNPJ digits only" },
190
+ name: { type: "string" },
191
+ },
192
+ },
193
+ expires_in: { type: "number", description: "QR lifetime in seconds (default 3600)" },
194
+ description: { type: "string", description: "Payer-visible description (solicitacaoPagador)" },
195
+ dict_key: { type: "string", description: "Recebedor DICT key — must be a key owned by the merchant" },
196
+ txid: { type: "string", description: "Optional merchant-supplied txid (26-35 alphanumeric). Omit to have BCB assign one." },
197
+ },
198
+ required: ["amount", "dict_key"],
199
+ },
200
+ },
201
+ {
202
+ name: "get_pix_cob",
203
+ description: "Retrieve an immediate Pix charge by its txid.",
204
+ inputSchema: {
205
+ type: "object",
206
+ properties: {
207
+ txid: { type: "string", description: "BACEN txid (26-35 alphanumeric)" },
208
+ },
209
+ required: ["txid"],
210
+ },
211
+ },
212
+ {
213
+ name: "list_pix_cob",
214
+ description: "List immediate Pix charges (cob) by date range. Paginated per BACEN Pix v2.",
215
+ inputSchema: {
216
+ type: "object",
217
+ properties: {
218
+ from: { type: "string", description: "Start date-time ISO-8601 (inicio)" },
219
+ to: { type: "string", description: "End date-time ISO-8601 (fim)" },
220
+ status: { type: "string", description: "Status filter: ATIVA | CONCLUIDA | REMOVIDA_PELO_USUARIO_RECEBEDOR | REMOVIDA_PELO_PSP" },
221
+ page: { type: "number", description: "paginacao.paginaAtual" },
222
+ page_size: { type: "number", description: "paginacao.itensPorPagina" },
223
+ },
224
+ required: ["from", "to"],
225
+ },
226
+ },
227
+ {
228
+ name: "create_pix_devolucao",
229
+ description: "Refund (devolução) a previously received Pix. Must reference the original endToEndId and a merchant-side refund id.",
230
+ inputSchema: {
231
+ type: "object",
232
+ properties: {
233
+ end_to_end_id: { type: "string", description: "Original Pix endToEndId" },
234
+ refund_id: { type: "string", description: "Merchant-side refund id (id da devolução, alphanumeric up to 35)" },
235
+ amount: { type: "string", description: "Refund amount in BRL major units. Omit for full refund." },
236
+ reason: { type: "string", description: "Free-text reason (descricao)" },
237
+ },
238
+ required: ["end_to_end_id", "refund_id"],
239
+ },
240
+ },
241
+ {
242
+ name: "get_pix_devolucao",
243
+ description: "Retrieve a Pix devolução by its endToEndId + refund id.",
244
+ inputSchema: {
245
+ type: "object",
246
+ properties: {
247
+ end_to_end_id: { type: "string", description: "Original Pix endToEndId" },
248
+ refund_id: { type: "string", description: "Merchant-side refund id" },
249
+ },
250
+ required: ["end_to_end_id", "refund_id"],
251
+ },
252
+ },
253
+ {
254
+ name: "resolve_dict_key",
255
+ description: "Resolve a DICT key (CPF, CNPJ, email, phone, EVP) to the owner's account data before sending a Pix. Subject to BCB rate limits per consenting payer.",
256
+ inputSchema: {
257
+ type: "object",
258
+ properties: {
259
+ key: { type: "string", description: "DICT key — CPF, CNPJ, email, phone (+55...), or EVP UUID" },
260
+ payer_document: { type: "string", description: "End-payer CPF/CNPJ for BCB audit logging" },
261
+ },
262
+ required: ["key"],
263
+ },
264
+ },
265
+ {
266
+ name: "register_dict_key",
267
+ description: "Register a DICT key on a BB account owned by the merchant. Some key types (email/phone) require BCB confirmation flows.",
268
+ inputSchema: {
269
+ type: "object",
270
+ properties: {
271
+ key_type: { type: "string", description: "DICT key type: CPF | CNPJ | EMAIL | PHONE | EVP" },
272
+ key: { type: "string", description: "Key value. Omit for EVP (BCB generates UUID)." },
273
+ account: {
274
+ type: "object",
275
+ properties: {
276
+ branch: { type: "string", description: "Agência" },
277
+ account: { type: "string", description: "Conta" },
278
+ account_type: { type: "string", description: "CACC | SVGS | SLRY | TRAN" },
279
+ },
280
+ required: ["branch", "account", "account_type"],
281
+ },
282
+ },
283
+ required: ["key_type", "account"],
284
+ },
285
+ },
286
+ {
287
+ name: "delete_dict_key",
288
+ description: "Delete a DICT key owned by the merchant. Irreversible — key becomes available for re-registration after BCB lockout window.",
289
+ inputSchema: {
290
+ type: "object",
291
+ properties: {
292
+ key: { type: "string", description: "DICT key value to delete" },
293
+ },
294
+ required: ["key"],
295
+ },
296
+ },
297
+ {
298
+ name: "register_boleto",
299
+ description: "Issue a boleto via BB Cobranças. Returns nosso_numero, linha digitável, barcode, and PDF URL.",
300
+ inputSchema: {
301
+ type: "object",
302
+ properties: {
303
+ amount: { type: "string", description: "Amount in BRL major units, e.g. '150.00'" },
304
+ due_date: { type: "string", description: "Due date ISO-8601 (YYYY-MM-DD)" },
305
+ convenio: { type: "string", description: "BB convênio (numeroConvenio) registered for the merchant" },
306
+ payer: {
307
+ type: "object",
308
+ properties: {
309
+ name: { type: "string" },
310
+ document: { type: "string", description: "CPF or CNPJ digits only" },
311
+ address: {
312
+ type: "object",
313
+ properties: {
314
+ street: { type: "string" },
315
+ number: { type: "string" },
316
+ district: { type: "string" },
317
+ city: { type: "string" },
318
+ state: { type: "string", description: "2-letter UF code" },
319
+ postal_code: { type: "string", description: "CEP digits only" },
320
+ },
321
+ },
322
+ },
323
+ required: ["name", "document"],
324
+ },
325
+ our_number: { type: "string", description: "Nosso_numero. Omit to have BB assign one." },
326
+ fine: { type: "object", description: "Multa: { percentage?, amount?, days_after_due? }" },
327
+ interest: { type: "object", description: "Juros: { percentage?, amount? }" },
328
+ discount: { type: "object", description: "Desconto: { percentage?, amount?, until? }" },
329
+ },
330
+ required: ["amount", "due_date", "convenio", "payer"],
331
+ },
332
+ },
333
+ {
334
+ name: "get_boleto",
335
+ description: "Retrieve a boleto by nosso_numero.",
336
+ inputSchema: {
337
+ type: "object",
338
+ properties: {
339
+ nosso_numero: { type: "string", description: "BB nosso_numero" },
340
+ convenio: { type: "string", description: "BB convênio" },
341
+ },
342
+ required: ["nosso_numero", "convenio"],
343
+ },
344
+ },
345
+ {
346
+ name: "cancel_boleto",
347
+ description: "Cancel (baixa) an outstanding boleto before payment.",
348
+ inputSchema: {
349
+ type: "object",
350
+ properties: {
351
+ nosso_numero: { type: "string", description: "BB nosso_numero" },
352
+ convenio: { type: "string", description: "BB convênio" },
353
+ reason: { type: "string", description: "Cancellation reason code or free text" },
354
+ },
355
+ required: ["nosso_numero", "convenio"],
356
+ },
357
+ },
358
+ {
359
+ name: "get_account_balance",
360
+ description: "Retrieve the current balance of a BB conta-corrente (checking) account.",
361
+ inputSchema: {
362
+ type: "object",
363
+ properties: {
364
+ branch: { type: "string", description: "Agência (4 digits)" },
365
+ account: { type: "string", description: "Conta (digits, no DV)" },
366
+ account_dv: { type: "string", description: "Conta DV (1 digit)" },
367
+ },
368
+ required: ["branch", "account"],
369
+ },
370
+ },
371
+ {
372
+ name: "get_statement",
373
+ description: "Retrieve account statement transactions for a BB conta-corrente over a date range. Paginated.",
374
+ inputSchema: {
375
+ type: "object",
376
+ properties: {
377
+ branch: { type: "string", description: "Agência" },
378
+ account: { type: "string", description: "Conta" },
379
+ from: { type: "string", description: "Start date ISO-8601 (YYYY-MM-DD)" },
380
+ to: { type: "string", description: "End date ISO-8601 (YYYY-MM-DD)" },
381
+ page: { type: "number", description: "Page number (1-indexed)" },
382
+ page_size: { type: "number", description: "Items per page (default 50)" },
383
+ },
384
+ required: ["branch", "account", "from", "to"],
385
+ },
386
+ },
387
+ ],
388
+ }));
389
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
390
+ const { name, arguments: args } = request.params;
391
+ const a = (args ?? {});
392
+ try {
393
+ switch (name) {
394
+ case "create_pix_cob": {
395
+ // TODO(verify): path. BACEN Pix v2 standard is PUT /pix/v2/cob/{txid}
396
+ // when txid is supplied, POST /pix/v2/cob otherwise. BB hosts Pix
397
+ // under api-pix.bb.com.br in production with /pix/v2 prefix.
398
+ const txid = a.txid ? `/${encodeURIComponent(String(a.txid))}` : "";
399
+ const method = a.txid ? "PUT" : "POST";
400
+ return {
401
+ content: [
402
+ { type: "text", text: JSON.stringify(await bbRequest(method, `/pix/v2/cob${txid}`, a), null, 2) },
403
+ ],
404
+ };
405
+ }
406
+ case "get_pix_cob": {
407
+ const txid = encodeURIComponent(String(a.txid ?? ""));
408
+ return {
409
+ content: [
410
+ { type: "text", text: JSON.stringify(await bbRequest("GET", `/pix/v2/cob/${txid}`), null, 2) },
411
+ ],
412
+ };
413
+ }
414
+ case "list_pix_cob": {
415
+ const params = new URLSearchParams();
416
+ if (a.from !== undefined)
417
+ params.set("inicio", String(a.from));
418
+ if (a.to !== undefined)
419
+ params.set("fim", String(a.to));
420
+ if (a.status !== undefined)
421
+ params.set("status", String(a.status));
422
+ if (a.page !== undefined)
423
+ params.set("paginacao.paginaAtual", String(a.page));
424
+ if (a.page_size !== undefined)
425
+ params.set("paginacao.itensPorPagina", String(a.page_size));
426
+ return {
427
+ content: [
428
+ { type: "text", text: JSON.stringify(await bbRequest("GET", `/pix/v2/cob?${params}`), null, 2) },
429
+ ],
430
+ };
431
+ }
432
+ case "create_pix_devolucao": {
433
+ const e2e = encodeURIComponent(String(a.end_to_end_id ?? ""));
434
+ const rid = encodeURIComponent(String(a.refund_id ?? ""));
435
+ const body = {};
436
+ if (a.amount !== undefined)
437
+ body.valor = a.amount;
438
+ if (a.reason !== undefined)
439
+ body.descricao = a.reason;
440
+ // TODO(verify): path. BACEN standard PUT /pix/v2/pix/{e2e}/devolucao/{id}.
441
+ return {
442
+ content: [
443
+ { type: "text", text: JSON.stringify(await bbRequest("PUT", `/pix/v2/pix/${e2e}/devolucao/${rid}`, body), null, 2) },
444
+ ],
445
+ };
446
+ }
447
+ case "get_pix_devolucao": {
448
+ const e2e = encodeURIComponent(String(a.end_to_end_id ?? ""));
449
+ const rid = encodeURIComponent(String(a.refund_id ?? ""));
450
+ return {
451
+ content: [
452
+ { type: "text", text: JSON.stringify(await bbRequest("GET", `/pix/v2/pix/${e2e}/devolucao/${rid}`), null, 2) },
453
+ ],
454
+ };
455
+ }
456
+ case "resolve_dict_key": {
457
+ const key = encodeURIComponent(String(a.key ?? ""));
458
+ const qs = a.payer_document ? `&payerDocument=${encodeURIComponent(String(a.payer_document))}` : "";
459
+ // TODO(verify): path. BACEN DICT consulta is GET /dict/v2/keys/{key}; BB
460
+ // may also expose it under /pix/v2/dict for client convenience.
461
+ return {
462
+ content: [
463
+ { type: "text", text: JSON.stringify(await bbRequest("GET", `/dict/v2/keys/${key}${qs ? `?${qs.slice(1)}` : ""}`), null, 2) },
464
+ ],
465
+ };
466
+ }
467
+ case "register_dict_key": {
468
+ // TODO(verify): path. BACEN DICT registration is POST /dict/v2/keys
469
+ // with a chaveEntries body; BB may add a confirmação flow for email/phone.
470
+ return {
471
+ content: [
472
+ { type: "text", text: JSON.stringify(await bbRequest("POST", "/dict/v2/keys", a), null, 2) },
473
+ ],
474
+ };
475
+ }
476
+ case "delete_dict_key": {
477
+ const key = encodeURIComponent(String(a.key ?? ""));
478
+ // TODO(verify): path. BACEN DICT delete is DELETE /dict/v2/keys/{key}.
479
+ return {
480
+ content: [
481
+ { type: "text", text: JSON.stringify(await bbRequest("DELETE", `/dict/v2/keys/${key}`), null, 2) },
482
+ ],
483
+ };
484
+ }
485
+ case "register_boleto": {
486
+ // TODO(verify): path. BB Cobranças v2 registration is commonly
487
+ // POST /cobrancas/v2/boletos with numeroConvenio carried in the body.
488
+ return {
489
+ content: [
490
+ { type: "text", text: JSON.stringify(await bbRequest("POST", "/cobrancas/v2/boletos", a), null, 2) },
491
+ ],
492
+ };
493
+ }
494
+ case "get_boleto": {
495
+ const nn = encodeURIComponent(String(a.nosso_numero ?? ""));
496
+ const conv = encodeURIComponent(String(a.convenio ?? ""));
497
+ // TODO(verify): path. BB exposes detail at GET /cobrancas/v2/boletos/{nosso_numero}?numeroConvenio=...
498
+ return {
499
+ content: [
500
+ { type: "text", text: JSON.stringify(await bbRequest("GET", `/cobrancas/v2/boletos/${nn}?numeroConvenio=${conv}`), null, 2) },
501
+ ],
502
+ };
503
+ }
504
+ case "cancel_boleto": {
505
+ const nn = encodeURIComponent(String(a.nosso_numero ?? ""));
506
+ const conv = encodeURIComponent(String(a.convenio ?? ""));
507
+ const body = { numeroConvenio: a.convenio };
508
+ if (a.reason !== undefined)
509
+ body.motivoBaixa = a.reason;
510
+ // TODO(verify): path. BB baixa is POST /cobrancas/v2/boletos/{nn}/baixar.
511
+ return {
512
+ content: [
513
+ { type: "text", text: JSON.stringify(await bbRequest("POST", `/cobrancas/v2/boletos/${nn}/baixar?numeroConvenio=${conv}`, body), null, 2) },
514
+ ],
515
+ };
516
+ }
517
+ case "get_account_balance": {
518
+ const branch = encodeURIComponent(String(a.branch ?? ""));
519
+ const account = encodeURIComponent(String(a.account ?? ""));
520
+ const dv = a.account_dv ? `&digitoConta=${encodeURIComponent(String(a.account_dv))}` : "";
521
+ // TODO(verify): path. BB Conta-Corrente saldo: GET /conta-corrente/v1/saldo?agencia=&conta=
522
+ return {
523
+ content: [
524
+ { type: "text", text: JSON.stringify(await bbRequest("GET", `/conta-corrente/v1/saldo?agencia=${branch}&conta=${account}${dv}`), null, 2) },
525
+ ],
526
+ };
527
+ }
528
+ case "get_statement": {
529
+ const params = new URLSearchParams();
530
+ params.set("agencia", String(a.branch ?? ""));
531
+ params.set("conta", String(a.account ?? ""));
532
+ params.set("dataInicio", String(a.from ?? ""));
533
+ params.set("dataFim", String(a.to ?? ""));
534
+ if (a.page !== undefined)
535
+ params.set("pagina", String(a.page));
536
+ if (a.page_size !== undefined)
537
+ params.set("tamanhoPagina", String(a.page_size));
538
+ // TODO(verify): path. BB extrato: GET /conta-corrente/v1/extrato.
539
+ return {
540
+ content: [
541
+ { type: "text", text: JSON.stringify(await bbRequest("GET", `/conta-corrente/v1/extrato?${params}`), null, 2) },
542
+ ],
543
+ };
544
+ }
545
+ default:
546
+ return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
547
+ }
548
+ }
549
+ catch (err) {
550
+ return {
551
+ content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
552
+ isError: true,
553
+ };
554
+ }
555
+ });
556
+ async function main() {
557
+ const transport = new StdioServerTransport();
558
+ await server.connect(transport);
559
+ }
560
+ main().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@codespar/mcp-banco-do-brasil",
3
+ "version": "0.1.0-alpha.1",
4
+ "description": "MCP server for Banco do Brasil — Brazil's top public bank. Pix, Cobrança (boleto), Conta-Corrente, and Arrecadação via BB's Developer Portal APIs (OAuth2 + mTLS).",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "bin": {
8
+ "mcp-banco-do-brasil": "./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
+ "zod": "^3.23.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^25.5.0",
20
+ "typescript": "^5.8.0"
21
+ },
22
+ "license": "MIT",
23
+ "keywords": [
24
+ "mcp",
25
+ "banco-do-brasil",
26
+ "bb",
27
+ "banking",
28
+ "pix",
29
+ "boleto",
30
+ "cobranca",
31
+ "open-finance",
32
+ "brazil"
33
+ ],
34
+ "mcpName": "io.github.codespar/banco-do-brasil"
35
+ }
package/server.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.codespar/banco-do-brasil",
4
+ "description": "MCP server for Banco do Brasil — Brazil's top public bank. Pix, Cobrança (boleto), Conta-Corrente, and Arrecadação via BB's Developer Portal APIs (OAuth2 + mTLS).",
5
+ "repository": {
6
+ "url": "https://github.com/codespar/mcp-dev-brasil",
7
+ "source": "github",
8
+ "subfolder": "packages/banking/banco-do-brasil"
9
+ },
10
+ "version": "0.1.0-alpha.1",
11
+ "packages": [
12
+ {
13
+ "registryType": "npm",
14
+ "identifier": "@codespar/mcp-banco-do-brasil",
15
+ "version": "0.1.0-alpha.1",
16
+ "transport": {
17
+ "type": "stdio"
18
+ },
19
+ "environmentVariables": [
20
+ {
21
+ "name": "BB_CLIENT_ID",
22
+ "description": "Banco do Brasil OAuth client_id issued via developers.bb.com.br after contract onboarding.",
23
+ "isRequired": true,
24
+ "format": "string",
25
+ "isSecret": false
26
+ },
27
+ {
28
+ "name": "BB_CLIENT_SECRET",
29
+ "description": "Banco do Brasil OAuth client_secret.",
30
+ "isRequired": true,
31
+ "format": "string",
32
+ "isSecret": true
33
+ },
34
+ {
35
+ "name": "BB_DEVELOPER_APP_KEY",
36
+ "description": "BB Developer App Key (gw-dev-app-key) — appended as a query param on most BB API calls.",
37
+ "isRequired": true,
38
+ "format": "string",
39
+ "isSecret": true
40
+ },
41
+ {
42
+ "name": "BB_CERT_PATH",
43
+ "description": "Absolute path to the mTLS client certificate (.crt or .pem). BACEN mandates mTLS for Pix v2 in production.",
44
+ "isRequired": false,
45
+ "format": "string",
46
+ "isSecret": false
47
+ },
48
+ {
49
+ "name": "BB_KEY_PATH",
50
+ "description": "Absolute path to the mTLS private key (.key or .pem).",
51
+ "isRequired": false,
52
+ "format": "string",
53
+ "isSecret": true
54
+ },
55
+ {
56
+ "name": "BB_ENV",
57
+ "description": "Environment: 'sandbox' or 'production'. Defaults to 'sandbox'.",
58
+ "isRequired": false,
59
+ "format": "string",
60
+ "isSecret": false
61
+ }
62
+ ]
63
+ }
64
+ ]
65
+ }
package/src/index.ts ADDED
@@ -0,0 +1,586 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * MCP Server for Banco do Brasil — Brazil's top public bank.
5
+ *
6
+ * BB exposes one of the broadest bank API surfaces in the country, covering
7
+ * Pix, Cobranças (boleto), Conta-Corrente, Open Finance, and Arrecadação
8
+ * via the Developer Portal at developers.bb.com.br.
9
+ *
10
+ * Tools (13):
11
+ * create_pix_cob — create immediate Pix charge (cob) with QR
12
+ * get_pix_cob — retrieve an immediate Pix charge by txid
13
+ * list_pix_cob — list immediate Pix charges by date range
14
+ * create_pix_devolucao — refund (devolução) a received Pix
15
+ * get_pix_devolucao — retrieve a devolução by id
16
+ * resolve_dict_key — resolve a DICT key to account data
17
+ * register_dict_key — register a DICT key on a BB account
18
+ * delete_dict_key — delete a DICT key owned by the merchant
19
+ * register_boleto — issue a boleto via BB Cobranças
20
+ * get_boleto — retrieve a boleto by nosso_numero
21
+ * cancel_boleto — cancel (baixa) an outstanding boleto
22
+ * get_account_balance — Conta-Corrente balance
23
+ * get_statement — Conta-Corrente statement / transactions
24
+ *
25
+ * Authentication
26
+ * OAuth 2.0 client_credentials + mandatory mTLS in production. BACEN
27
+ * requires mTLS for Pix v2; BB enforces it in production across product
28
+ * families. Sandbox typically accepts TLS-only — cert/key envs are
29
+ * therefore optional but recommended.
30
+ *
31
+ * Additionally, BB requires a developer-app-key (`gw-dev-app-key`)
32
+ * query param on every API call for traffic accounting.
33
+ *
34
+ * Version: 0.1.0-alpha.1
35
+ * developers.bb.com.br is contract-gated — full OpenAPI specs are visible
36
+ * only to onboarded merchants. Pix paths follow BACEN Pix v2 standard.
37
+ * Boleto / Conta-Corrente paths are best-guesses based on BB public docs
38
+ * and conventions shared with peers (Itaú, Santander, Bradesco). Every
39
+ * unverified path is marked TODO(verify). Treat 0.1.x as alpha and pin
40
+ * to exact versions.
41
+ *
42
+ * Environment
43
+ * BB_CLIENT_ID OAuth client id
44
+ * BB_CLIENT_SECRET OAuth client secret
45
+ * BB_DEVELOPER_APP_KEY gw-dev-app-key query param
46
+ * BB_CERT_PATH path to mTLS client cert (production)
47
+ * BB_KEY_PATH path to mTLS private key (production)
48
+ * BB_ENV "sandbox" | "production" (default: sandbox)
49
+ *
50
+ * Docs: https://developers.bb.com.br
51
+ */
52
+
53
+ import { readFileSync } from "node:fs";
54
+ import { Agent as HttpsAgent } from "node:https";
55
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
56
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
57
+ import {
58
+ CallToolRequestSchema,
59
+ ListToolsRequestSchema,
60
+ } from "@modelcontextprotocol/sdk/types.js";
61
+
62
+ const CLIENT_ID = process.env.BB_CLIENT_ID || "";
63
+ const CLIENT_SECRET = process.env.BB_CLIENT_SECRET || "";
64
+ const DEVELOPER_APP_KEY = process.env.BB_DEVELOPER_APP_KEY || "";
65
+ const CERT_PATH = process.env.BB_CERT_PATH || "";
66
+ const KEY_PATH = process.env.BB_KEY_PATH || "";
67
+ const BB_ENV = (process.env.BB_ENV || "sandbox").toLowerCase();
68
+ const IS_PROD = BB_ENV === "production";
69
+
70
+ // TODO(verify): production base hostnames. BB documents distinct hosts per
71
+ // product family (api.bb.com.br for some, api-pix.bb.com.br for Pix). The
72
+ // sandbox subdomain is api.sandbox.bb.com.br for most products.
73
+ const BASE_URL = IS_PROD ? "https://api.bb.com.br" : "https://api.sandbox.bb.com.br";
74
+ const OAUTH_URL = IS_PROD
75
+ ? "https://oauth.bb.com.br/oauth/token"
76
+ : "https://oauth.sandbox.bb.com.br/oauth/token";
77
+
78
+ // Lazy-load the mTLS agent so `--help` / schema introspection doesn't crash
79
+ // when certs are missing. mTLS is required by BACEN in production but BB
80
+ // sandbox typically accepts TLS-only.
81
+ let mtlsAgent: HttpsAgent | null = null;
82
+ function getMtlsAgent(): HttpsAgent | null {
83
+ if (mtlsAgent) return mtlsAgent;
84
+ if (!CERT_PATH || !KEY_PATH) {
85
+ if (IS_PROD) {
86
+ throw new Error(
87
+ "BB mTLS certificates are required in production. Set BB_CERT_PATH and BB_KEY_PATH " +
88
+ "to the client cert and private-key files issued by developers.bb.com.br."
89
+ );
90
+ }
91
+ return null; // sandbox: fall back to system TLS
92
+ }
93
+ mtlsAgent = new HttpsAgent({
94
+ cert: readFileSync(CERT_PATH),
95
+ key: readFileSync(KEY_PATH),
96
+ keepAlive: true,
97
+ });
98
+ return mtlsAgent;
99
+ }
100
+
101
+ interface TokenCache {
102
+ accessToken: string;
103
+ expiresAt: number;
104
+ }
105
+
106
+ let tokenCache: TokenCache | null = null;
107
+
108
+ async function getAccessToken(): Promise<string> {
109
+ const now = Date.now();
110
+ if (tokenCache && tokenCache.expiresAt > now + 60_000) {
111
+ return tokenCache.accessToken;
112
+ }
113
+ const basic = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString("base64");
114
+ const res = await fetchWithMtls(OAUTH_URL, {
115
+ method: "POST",
116
+ headers: {
117
+ Authorization: `Basic ${basic}`,
118
+ "Content-Type": "application/x-www-form-urlencoded",
119
+ },
120
+ // TODO(verify): scope. BB scopes are product-specific (cob.write,
121
+ // cob.read, pix.write, pix.read, dict.read, etc.). The sandbox
122
+ // commonly accepts a blank/all scope for client_credentials.
123
+ body: "grant_type=client_credentials",
124
+ });
125
+ if (!res.ok) {
126
+ throw new Error(`BB OAuth ${res.status}: ${await res.text()}`);
127
+ }
128
+ const data = (await res.json()) as { access_token: string; expires_in: number };
129
+ tokenCache = {
130
+ accessToken: data.access_token,
131
+ expiresAt: now + data.expires_in * 1000,
132
+ };
133
+ return data.access_token;
134
+ }
135
+
136
+ // Node's global fetch does not honour an https.Agent for mTLS; we drop down
137
+ // to node:https manually so the client cert is presented on every call.
138
+ async function fetchWithMtls(
139
+ url: string,
140
+ init: { method: string; headers?: Record<string, string>; body?: string }
141
+ ): Promise<{ ok: boolean; status: number; text: () => Promise<string>; json: () => Promise<unknown> }> {
142
+ const agent = getMtlsAgent();
143
+ const { request } = await import("node:https");
144
+ return new Promise((resolve, reject) => {
145
+ const u = new URL(url);
146
+ const req = request(
147
+ {
148
+ ...(agent ? { agent } : {}),
149
+ method: init.method,
150
+ hostname: u.hostname,
151
+ port: u.port || 443,
152
+ path: u.pathname + u.search,
153
+ headers: init.headers ?? {},
154
+ },
155
+ (res) => {
156
+ const chunks: Buffer[] = [];
157
+ res.on("data", (c: Buffer) => chunks.push(c));
158
+ res.on("end", () => {
159
+ const buf = Buffer.concat(chunks);
160
+ const status = res.statusCode ?? 0;
161
+ resolve({
162
+ ok: status >= 200 && status < 300,
163
+ status,
164
+ text: async () => buf.toString("utf8"),
165
+ json: async () => JSON.parse(buf.toString("utf8")),
166
+ });
167
+ });
168
+ }
169
+ );
170
+ req.on("error", reject);
171
+ if (init.body) req.write(init.body);
172
+ req.end();
173
+ });
174
+ }
175
+
176
+ function appendAppKey(path: string): string {
177
+ if (!DEVELOPER_APP_KEY) return path;
178
+ const sep = path.includes("?") ? "&" : "?";
179
+ return `${path}${sep}gw-dev-app-key=${encodeURIComponent(DEVELOPER_APP_KEY)}`;
180
+ }
181
+
182
+ async function bbRequest(method: string, path: string, body?: unknown): Promise<unknown> {
183
+ const token = await getAccessToken();
184
+ const fullPath = appendAppKey(path);
185
+ const res = await fetchWithMtls(`${BASE_URL}${fullPath}`, {
186
+ method,
187
+ headers: {
188
+ "Content-Type": "application/json",
189
+ Authorization: `Bearer ${token}`,
190
+ },
191
+ body: body ? JSON.stringify(body) : undefined,
192
+ });
193
+ if (!res.ok) {
194
+ throw new Error(`BB API ${res.status}: ${await res.text()}`);
195
+ }
196
+ return res.json();
197
+ }
198
+
199
+ const server = new Server(
200
+ { name: "mcp-banco-do-brasil", version: "0.1.0-alpha.1" },
201
+ { capabilities: { tools: {} } }
202
+ );
203
+
204
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
205
+ tools: [
206
+ {
207
+ name: "create_pix_cob",
208
+ description:
209
+ "Create an immediate Pix charge (cob) with QR code. Returns txid, EMV copy-paste payload, and location URL. BACEN Pix v2 standard.",
210
+ inputSchema: {
211
+ type: "object",
212
+ properties: {
213
+ amount: { type: "string", description: "Amount in BRL major units, e.g. '99.90'" },
214
+ payer: {
215
+ type: "object",
216
+ description: "Payer identification (CPF/CNPJ + name)",
217
+ properties: {
218
+ document: { type: "string", description: "CPF or CNPJ digits only" },
219
+ name: { type: "string" },
220
+ },
221
+ },
222
+ expires_in: { type: "number", description: "QR lifetime in seconds (default 3600)" },
223
+ description: { type: "string", description: "Payer-visible description (solicitacaoPagador)" },
224
+ dict_key: { type: "string", description: "Recebedor DICT key — must be a key owned by the merchant" },
225
+ txid: { type: "string", description: "Optional merchant-supplied txid (26-35 alphanumeric). Omit to have BCB assign one." },
226
+ },
227
+ required: ["amount", "dict_key"],
228
+ },
229
+ },
230
+ {
231
+ name: "get_pix_cob",
232
+ description: "Retrieve an immediate Pix charge by its txid.",
233
+ inputSchema: {
234
+ type: "object",
235
+ properties: {
236
+ txid: { type: "string", description: "BACEN txid (26-35 alphanumeric)" },
237
+ },
238
+ required: ["txid"],
239
+ },
240
+ },
241
+ {
242
+ name: "list_pix_cob",
243
+ description: "List immediate Pix charges (cob) by date range. Paginated per BACEN Pix v2.",
244
+ inputSchema: {
245
+ type: "object",
246
+ properties: {
247
+ from: { type: "string", description: "Start date-time ISO-8601 (inicio)" },
248
+ to: { type: "string", description: "End date-time ISO-8601 (fim)" },
249
+ status: { type: "string", description: "Status filter: ATIVA | CONCLUIDA | REMOVIDA_PELO_USUARIO_RECEBEDOR | REMOVIDA_PELO_PSP" },
250
+ page: { type: "number", description: "paginacao.paginaAtual" },
251
+ page_size: { type: "number", description: "paginacao.itensPorPagina" },
252
+ },
253
+ required: ["from", "to"],
254
+ },
255
+ },
256
+ {
257
+ name: "create_pix_devolucao",
258
+ description:
259
+ "Refund (devolução) a previously received Pix. Must reference the original endToEndId and a merchant-side refund id.",
260
+ inputSchema: {
261
+ type: "object",
262
+ properties: {
263
+ end_to_end_id: { type: "string", description: "Original Pix endToEndId" },
264
+ refund_id: { type: "string", description: "Merchant-side refund id (id da devolução, alphanumeric up to 35)" },
265
+ amount: { type: "string", description: "Refund amount in BRL major units. Omit for full refund." },
266
+ reason: { type: "string", description: "Free-text reason (descricao)" },
267
+ },
268
+ required: ["end_to_end_id", "refund_id"],
269
+ },
270
+ },
271
+ {
272
+ name: "get_pix_devolucao",
273
+ description: "Retrieve a Pix devolução by its endToEndId + refund id.",
274
+ inputSchema: {
275
+ type: "object",
276
+ properties: {
277
+ end_to_end_id: { type: "string", description: "Original Pix endToEndId" },
278
+ refund_id: { type: "string", description: "Merchant-side refund id" },
279
+ },
280
+ required: ["end_to_end_id", "refund_id"],
281
+ },
282
+ },
283
+ {
284
+ name: "resolve_dict_key",
285
+ description:
286
+ "Resolve a DICT key (CPF, CNPJ, email, phone, EVP) to the owner's account data before sending a Pix. Subject to BCB rate limits per consenting payer.",
287
+ inputSchema: {
288
+ type: "object",
289
+ properties: {
290
+ key: { type: "string", description: "DICT key — CPF, CNPJ, email, phone (+55...), or EVP UUID" },
291
+ payer_document: { type: "string", description: "End-payer CPF/CNPJ for BCB audit logging" },
292
+ },
293
+ required: ["key"],
294
+ },
295
+ },
296
+ {
297
+ name: "register_dict_key",
298
+ description:
299
+ "Register a DICT key on a BB account owned by the merchant. Some key types (email/phone) require BCB confirmation flows.",
300
+ inputSchema: {
301
+ type: "object",
302
+ properties: {
303
+ key_type: { type: "string", description: "DICT key type: CPF | CNPJ | EMAIL | PHONE | EVP" },
304
+ key: { type: "string", description: "Key value. Omit for EVP (BCB generates UUID)." },
305
+ account: {
306
+ type: "object",
307
+ properties: {
308
+ branch: { type: "string", description: "Agência" },
309
+ account: { type: "string", description: "Conta" },
310
+ account_type: { type: "string", description: "CACC | SVGS | SLRY | TRAN" },
311
+ },
312
+ required: ["branch", "account", "account_type"],
313
+ },
314
+ },
315
+ required: ["key_type", "account"],
316
+ },
317
+ },
318
+ {
319
+ name: "delete_dict_key",
320
+ description:
321
+ "Delete a DICT key owned by the merchant. Irreversible — key becomes available for re-registration after BCB lockout window.",
322
+ inputSchema: {
323
+ type: "object",
324
+ properties: {
325
+ key: { type: "string", description: "DICT key value to delete" },
326
+ },
327
+ required: ["key"],
328
+ },
329
+ },
330
+ {
331
+ name: "register_boleto",
332
+ description: "Issue a boleto via BB Cobranças. Returns nosso_numero, linha digitável, barcode, and PDF URL.",
333
+ inputSchema: {
334
+ type: "object",
335
+ properties: {
336
+ amount: { type: "string", description: "Amount in BRL major units, e.g. '150.00'" },
337
+ due_date: { type: "string", description: "Due date ISO-8601 (YYYY-MM-DD)" },
338
+ convenio: { type: "string", description: "BB convênio (numeroConvenio) registered for the merchant" },
339
+ payer: {
340
+ type: "object",
341
+ properties: {
342
+ name: { type: "string" },
343
+ document: { type: "string", description: "CPF or CNPJ digits only" },
344
+ address: {
345
+ type: "object",
346
+ properties: {
347
+ street: { type: "string" },
348
+ number: { type: "string" },
349
+ district: { type: "string" },
350
+ city: { type: "string" },
351
+ state: { type: "string", description: "2-letter UF code" },
352
+ postal_code: { type: "string", description: "CEP digits only" },
353
+ },
354
+ },
355
+ },
356
+ required: ["name", "document"],
357
+ },
358
+ our_number: { type: "string", description: "Nosso_numero. Omit to have BB assign one." },
359
+ fine: { type: "object", description: "Multa: { percentage?, amount?, days_after_due? }" },
360
+ interest: { type: "object", description: "Juros: { percentage?, amount? }" },
361
+ discount: { type: "object", description: "Desconto: { percentage?, amount?, until? }" },
362
+ },
363
+ required: ["amount", "due_date", "convenio", "payer"],
364
+ },
365
+ },
366
+ {
367
+ name: "get_boleto",
368
+ description: "Retrieve a boleto by nosso_numero.",
369
+ inputSchema: {
370
+ type: "object",
371
+ properties: {
372
+ nosso_numero: { type: "string", description: "BB nosso_numero" },
373
+ convenio: { type: "string", description: "BB convênio" },
374
+ },
375
+ required: ["nosso_numero", "convenio"],
376
+ },
377
+ },
378
+ {
379
+ name: "cancel_boleto",
380
+ description: "Cancel (baixa) an outstanding boleto before payment.",
381
+ inputSchema: {
382
+ type: "object",
383
+ properties: {
384
+ nosso_numero: { type: "string", description: "BB nosso_numero" },
385
+ convenio: { type: "string", description: "BB convênio" },
386
+ reason: { type: "string", description: "Cancellation reason code or free text" },
387
+ },
388
+ required: ["nosso_numero", "convenio"],
389
+ },
390
+ },
391
+ {
392
+ name: "get_account_balance",
393
+ description: "Retrieve the current balance of a BB conta-corrente (checking) account.",
394
+ inputSchema: {
395
+ type: "object",
396
+ properties: {
397
+ branch: { type: "string", description: "Agência (4 digits)" },
398
+ account: { type: "string", description: "Conta (digits, no DV)" },
399
+ account_dv: { type: "string", description: "Conta DV (1 digit)" },
400
+ },
401
+ required: ["branch", "account"],
402
+ },
403
+ },
404
+ {
405
+ name: "get_statement",
406
+ description: "Retrieve account statement transactions for a BB conta-corrente over a date range. Paginated.",
407
+ inputSchema: {
408
+ type: "object",
409
+ properties: {
410
+ branch: { type: "string", description: "Agência" },
411
+ account: { type: "string", description: "Conta" },
412
+ from: { type: "string", description: "Start date ISO-8601 (YYYY-MM-DD)" },
413
+ to: { type: "string", description: "End date ISO-8601 (YYYY-MM-DD)" },
414
+ page: { type: "number", description: "Page number (1-indexed)" },
415
+ page_size: { type: "number", description: "Items per page (default 50)" },
416
+ },
417
+ required: ["branch", "account", "from", "to"],
418
+ },
419
+ },
420
+ ],
421
+ }));
422
+
423
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
424
+ const { name, arguments: args } = request.params;
425
+ const a = (args ?? {}) as Record<string, unknown>;
426
+
427
+ try {
428
+ switch (name) {
429
+ case "create_pix_cob": {
430
+ // TODO(verify): path. BACEN Pix v2 standard is PUT /pix/v2/cob/{txid}
431
+ // when txid is supplied, POST /pix/v2/cob otherwise. BB hosts Pix
432
+ // under api-pix.bb.com.br in production with /pix/v2 prefix.
433
+ const txid = a.txid ? `/${encodeURIComponent(String(a.txid))}` : "";
434
+ const method = a.txid ? "PUT" : "POST";
435
+ return {
436
+ content: [
437
+ { type: "text", text: JSON.stringify(await bbRequest(method, `/pix/v2/cob${txid}`, a), null, 2) },
438
+ ],
439
+ };
440
+ }
441
+ case "get_pix_cob": {
442
+ const txid = encodeURIComponent(String(a.txid ?? ""));
443
+ return {
444
+ content: [
445
+ { type: "text", text: JSON.stringify(await bbRequest("GET", `/pix/v2/cob/${txid}`), null, 2) },
446
+ ],
447
+ };
448
+ }
449
+ case "list_pix_cob": {
450
+ const params = new URLSearchParams();
451
+ if (a.from !== undefined) params.set("inicio", String(a.from));
452
+ if (a.to !== undefined) params.set("fim", String(a.to));
453
+ if (a.status !== undefined) params.set("status", String(a.status));
454
+ if (a.page !== undefined) params.set("paginacao.paginaAtual", String(a.page));
455
+ if (a.page_size !== undefined) params.set("paginacao.itensPorPagina", String(a.page_size));
456
+ return {
457
+ content: [
458
+ { type: "text", text: JSON.stringify(await bbRequest("GET", `/pix/v2/cob?${params}`), null, 2) },
459
+ ],
460
+ };
461
+ }
462
+ case "create_pix_devolucao": {
463
+ const e2e = encodeURIComponent(String(a.end_to_end_id ?? ""));
464
+ const rid = encodeURIComponent(String(a.refund_id ?? ""));
465
+ const body: Record<string, unknown> = {};
466
+ if (a.amount !== undefined) body.valor = a.amount;
467
+ if (a.reason !== undefined) body.descricao = a.reason;
468
+ // TODO(verify): path. BACEN standard PUT /pix/v2/pix/{e2e}/devolucao/{id}.
469
+ return {
470
+ content: [
471
+ { type: "text", text: JSON.stringify(await bbRequest("PUT", `/pix/v2/pix/${e2e}/devolucao/${rid}`, body), null, 2) },
472
+ ],
473
+ };
474
+ }
475
+ case "get_pix_devolucao": {
476
+ const e2e = encodeURIComponent(String(a.end_to_end_id ?? ""));
477
+ const rid = encodeURIComponent(String(a.refund_id ?? ""));
478
+ return {
479
+ content: [
480
+ { type: "text", text: JSON.stringify(await bbRequest("GET", `/pix/v2/pix/${e2e}/devolucao/${rid}`), null, 2) },
481
+ ],
482
+ };
483
+ }
484
+ case "resolve_dict_key": {
485
+ const key = encodeURIComponent(String(a.key ?? ""));
486
+ const qs = a.payer_document ? `&payerDocument=${encodeURIComponent(String(a.payer_document))}` : "";
487
+ // TODO(verify): path. BACEN DICT consulta is GET /dict/v2/keys/{key}; BB
488
+ // may also expose it under /pix/v2/dict for client convenience.
489
+ return {
490
+ content: [
491
+ { type: "text", text: JSON.stringify(await bbRequest("GET", `/dict/v2/keys/${key}${qs ? `?${qs.slice(1)}` : ""}`), null, 2) },
492
+ ],
493
+ };
494
+ }
495
+ case "register_dict_key": {
496
+ // TODO(verify): path. BACEN DICT registration is POST /dict/v2/keys
497
+ // with a chaveEntries body; BB may add a confirmação flow for email/phone.
498
+ return {
499
+ content: [
500
+ { type: "text", text: JSON.stringify(await bbRequest("POST", "/dict/v2/keys", a), null, 2) },
501
+ ],
502
+ };
503
+ }
504
+ case "delete_dict_key": {
505
+ const key = encodeURIComponent(String(a.key ?? ""));
506
+ // TODO(verify): path. BACEN DICT delete is DELETE /dict/v2/keys/{key}.
507
+ return {
508
+ content: [
509
+ { type: "text", text: JSON.stringify(await bbRequest("DELETE", `/dict/v2/keys/${key}`), null, 2) },
510
+ ],
511
+ };
512
+ }
513
+ case "register_boleto": {
514
+ // TODO(verify): path. BB Cobranças v2 registration is commonly
515
+ // POST /cobrancas/v2/boletos with numeroConvenio carried in the body.
516
+ return {
517
+ content: [
518
+ { type: "text", text: JSON.stringify(await bbRequest("POST", "/cobrancas/v2/boletos", a), null, 2) },
519
+ ],
520
+ };
521
+ }
522
+ case "get_boleto": {
523
+ const nn = encodeURIComponent(String(a.nosso_numero ?? ""));
524
+ const conv = encodeURIComponent(String(a.convenio ?? ""));
525
+ // TODO(verify): path. BB exposes detail at GET /cobrancas/v2/boletos/{nosso_numero}?numeroConvenio=...
526
+ return {
527
+ content: [
528
+ { type: "text", text: JSON.stringify(await bbRequest("GET", `/cobrancas/v2/boletos/${nn}?numeroConvenio=${conv}`), null, 2) },
529
+ ],
530
+ };
531
+ }
532
+ case "cancel_boleto": {
533
+ const nn = encodeURIComponent(String(a.nosso_numero ?? ""));
534
+ const conv = encodeURIComponent(String(a.convenio ?? ""));
535
+ const body: Record<string, unknown> = { numeroConvenio: a.convenio };
536
+ if (a.reason !== undefined) body.motivoBaixa = a.reason;
537
+ // TODO(verify): path. BB baixa is POST /cobrancas/v2/boletos/{nn}/baixar.
538
+ return {
539
+ content: [
540
+ { type: "text", text: JSON.stringify(await bbRequest("POST", `/cobrancas/v2/boletos/${nn}/baixar?numeroConvenio=${conv}`, body), null, 2) },
541
+ ],
542
+ };
543
+ }
544
+ case "get_account_balance": {
545
+ const branch = encodeURIComponent(String(a.branch ?? ""));
546
+ const account = encodeURIComponent(String(a.account ?? ""));
547
+ const dv = a.account_dv ? `&digitoConta=${encodeURIComponent(String(a.account_dv))}` : "";
548
+ // TODO(verify): path. BB Conta-Corrente saldo: GET /conta-corrente/v1/saldo?agencia=&conta=
549
+ return {
550
+ content: [
551
+ { type: "text", text: JSON.stringify(await bbRequest("GET", `/conta-corrente/v1/saldo?agencia=${branch}&conta=${account}${dv}`), null, 2) },
552
+ ],
553
+ };
554
+ }
555
+ case "get_statement": {
556
+ const params = new URLSearchParams();
557
+ params.set("agencia", String(a.branch ?? ""));
558
+ params.set("conta", String(a.account ?? ""));
559
+ params.set("dataInicio", String(a.from ?? ""));
560
+ params.set("dataFim", String(a.to ?? ""));
561
+ if (a.page !== undefined) params.set("pagina", String(a.page));
562
+ if (a.page_size !== undefined) params.set("tamanhoPagina", String(a.page_size));
563
+ // TODO(verify): path. BB extrato: GET /conta-corrente/v1/extrato.
564
+ return {
565
+ content: [
566
+ { type: "text", text: JSON.stringify(await bbRequest("GET", `/conta-corrente/v1/extrato?${params}`), null, 2) },
567
+ ],
568
+ };
569
+ }
570
+ default:
571
+ return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
572
+ }
573
+ } catch (err) {
574
+ return {
575
+ content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
576
+ isError: true,
577
+ };
578
+ }
579
+ });
580
+
581
+ async function main() {
582
+ const transport = new StdioServerTransport();
583
+ await server.connect(transport);
584
+ }
585
+
586
+ 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
+ }