@pipeworx/mcp-latam-validate 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pipeworx
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # mcp-latam-validate
2
+
3
+ Validate LatAm banking & tax IDs: Mexican CLABE, Brazilian CNPJ/CPF checksums + BrasilAPI company/CEP/bank lookups
4
+
5
+ Part of [Pipeworx](https://pipeworx.io) — an MCP gateway connecting AI agents to 1299+ live data sources.
6
+
7
+ ## Tools
8
+
9
+ | Tool | Description |
10
+ |------|-------------|
11
+
12
+ ## Quick Start
13
+
14
+ Add to your MCP client (Claude Desktop, Cursor, Windsurf, etc.):
15
+
16
+ ```json
17
+ {
18
+ "mcpServers": {
19
+ "latam-validate": {
20
+ "url": "https://gateway.pipeworx.io/latam-validate/mcp"
21
+ }
22
+ }
23
+ }
24
+ ```
25
+
26
+ Or connect to the full Pipeworx gateway for access to all 1299+ data sources:
27
+
28
+ ```json
29
+ {
30
+ "mcpServers": {
31
+ "pipeworx": {
32
+ "url": "https://gateway.pipeworx.io/mcp"
33
+ }
34
+ }
35
+ }
36
+ ```
37
+
38
+ ## Using with ask_pipeworx
39
+
40
+ Instead of calling tools directly, you can ask questions in plain English:
41
+
42
+ ```
43
+ ask_pipeworx({ question: "your question about Latam Validate data" })
44
+ ```
45
+
46
+ The gateway picks the right tool and fills the arguments automatically.
47
+
48
+ ## More
49
+
50
+ - [All tools and guides](https://github.com/pipeworx-io/examples)
51
+ - [pipeworx.io](https://pipeworx.io)
52
+
53
+ ## License
54
+
55
+ MIT
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@pipeworx/mcp-latam-validate",
3
+ "version": "0.1.0",
4
+ "description": "Validate LatAm banking & tax IDs: Mexican CLABE, Brazilian CNPJ/CPF checksums + BrasilAPI company/CEP/bank lookups",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "types": "src/index.ts",
8
+ "keywords": ["mcp", "mcp-server", "model-context-protocol", "pipeworx", "latam-validate"],
9
+ "license": "MIT",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/pipeworx-io/mcp-latam-validate"
13
+ },
14
+ "scripts": {
15
+ "typecheck": "tsc --noEmit"
16
+ },
17
+ "devDependencies": {
18
+ "typescript": "^5.7.0"
19
+ }
20
+ }
package/server.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.pipeworx-io/latam-validate",
4
+ "title": "Latam Validate",
5
+ "description": "Validate LatAm banking & tax IDs: Mexican CLABE, Brazilian CNPJ/CPF checksums + BrasilAPI company/CEP/bank lookups",
6
+ "version": "0.1.0",
7
+ "websiteUrl": "https://pipeworx.io/packs/latam-validate",
8
+ "repository": {
9
+ "url": "https://github.com/pipeworx-io/mcp-latam-validate",
10
+ "source": "github"
11
+ },
12
+ "remotes": [
13
+ {
14
+ "type": "streamable-http",
15
+ "url": "https://gateway.pipeworx.io/latam-validate/mcp"
16
+ }
17
+ ]
18
+ }
package/src/index.ts ADDED
@@ -0,0 +1,390 @@
1
+ interface McpToolDefinition {
2
+ name: string;
3
+ description: string;
4
+ inputSchema: {
5
+ type: 'object';
6
+ properties: Record<string, unknown>;
7
+ required?: string[];
8
+ };
9
+ }
10
+
11
+ interface McpToolExport {
12
+ tools: McpToolDefinition[];
13
+ callTool: (name: string, args: Record<string, unknown>) => Promise<unknown>;
14
+ meter?: { credits: number };
15
+ cost?: Record<string, unknown>;
16
+ provider?: string;
17
+ }
18
+
19
+ /**
20
+ * LatAm Validate MCP — validate Latin-American banking and tax identifiers.
21
+ *
22
+ * - Mexican CLABE (18-digit interbank account number): pure checksum compute
23
+ * plus a local catalog of ABM bank codes (no network call).
24
+ * - Brazilian CNPJ (company tax ID): checksum (numeric and the 2026
25
+ * alphanumeric format) plus company registration lookup via BrasilAPI.
26
+ * - Brazilian CPF (personal tax ID): pure checksum compute (no lookup —
27
+ * personal data).
28
+ * - Brazilian CEP (postal code) → address, and the bank list, via BrasilAPI
29
+ * (brasilapi.com.br — keyless).
30
+ */
31
+
32
+
33
+ const BRASILAPI = 'https://brasilapi.com.br/api';
34
+ const UA = 'pipeworx/1.0 (+https://pipeworx.io)';
35
+
36
+ const tools: McpToolExport['tools'] = [
37
+ {
38
+ name: 'validate_clabe',
39
+ description:
40
+ 'Validate a Mexican CLABE (Clave Bancaria Estandarizada, the 18-digit interbank account number used for SPEI transfers). Checks the control-digit checksum and decodes the bank code, plaza (branch city) code, and account number. Resolves the bank name for major Mexican banks (BBVA, Banorte, Santander, Banamex, Banco Azteca, STP, Nu, Mercado Pago, ...). Pure computation — accepts spaces or dashes in the input.',
41
+ inputSchema: {
42
+ type: 'object',
43
+ properties: {
44
+ clabe: { type: 'string', description: 'The 18-digit CLABE, e.g. "002010077777777771". Spaces and dashes are allowed.' },
45
+ },
46
+ required: ['clabe'],
47
+ },
48
+ },
49
+ {
50
+ name: 'validate_cnpj',
51
+ description:
52
+ 'Validate a Brazilian CNPJ (Cadastro Nacional da Pessoa Jurídica, the 14-character company tax ID) and look up the company registration: legal name (razão social), trade name, CNAE activity, address, registration status (situação cadastral), and partners (QSA). Checksum supports both the classic numeric format and the alphanumeric CNPJ introduced in 2026. Punctuation (12.345.678/0001-95) is accepted. Set skip_lookup to validate the checksum only.',
53
+ inputSchema: {
54
+ type: 'object',
55
+ properties: {
56
+ cnpj: { type: 'string', description: 'The CNPJ, e.g. "33.000.167/0001-01" or "33000167000101".' },
57
+ skip_lookup: { type: 'boolean', description: 'If true, only run the checksum and skip the BrasilAPI company-registration lookup. Default false.' },
58
+ },
59
+ required: ['cnpj'],
60
+ },
61
+ },
62
+ {
63
+ name: 'validate_cpf',
64
+ description:
65
+ 'Validate a Brazilian CPF (Cadastro de Pessoas Físicas, the 11-digit personal tax ID) checksum. Pure computation, no lookup — returns whether the two check digits are correct and the formatted form. Punctuation (123.456.789-09) is accepted.',
66
+ inputSchema: {
67
+ type: 'object',
68
+ properties: {
69
+ cpf: { type: 'string', description: 'The CPF, e.g. "123.456.789-09" or "12345678909".' },
70
+ },
71
+ required: ['cpf'],
72
+ },
73
+ },
74
+ {
75
+ name: 'brasil_cep',
76
+ description:
77
+ 'Look up a Brazilian CEP (postal code) and return the address: street, neighborhood, city, and state, with coordinates when available. Example: CEP 01310-100 → Avenida Paulista, Bela Vista, São Paulo, SP.',
78
+ inputSchema: {
79
+ type: 'object',
80
+ properties: {
81
+ cep: { type: 'string', description: 'The 8-digit CEP, e.g. "01310-100" or "01310100".' },
82
+ },
83
+ required: ['cep'],
84
+ },
85
+ },
86
+ {
87
+ name: 'brasil_banks',
88
+ description:
89
+ 'List Brazilian banks (COMPE code, ISPB, and name) from the central-bank registry, optionally filtered by name or code. Use to resolve a bank code from a boleto or PIX/TED transfer to the institution name, e.g. code 1 → Banco do Brasil, 260 → Nubank.',
90
+ inputSchema: {
91
+ type: 'object',
92
+ properties: {
93
+ search: { type: 'string', description: 'Filter by bank name substring (case-insensitive), e.g. "nubank" or "itau".' },
94
+ code: { type: 'number', description: 'Exact COMPE bank code to look up, e.g. 341 for Itaú.' },
95
+ limit: { type: 'number', description: 'Max results when listing/filtering (default 25).' },
96
+ },
97
+ },
98
+ },
99
+ ];
100
+
101
+ async function callTool(name: string, args: Record<string, unknown>): Promise<unknown> {
102
+ try {
103
+ switch (name) {
104
+ case 'validate_clabe':
105
+ return validateClabe(args);
106
+ case 'validate_cnpj':
107
+ return await validateCnpj(args);
108
+ case 'validate_cpf':
109
+ return validateCpf(args);
110
+ case 'brasil_cep':
111
+ return await brasilCep(args);
112
+ case 'brasil_banks':
113
+ return await brasilBanks(args);
114
+ default:
115
+ return { error: `Unknown tool: ${name}` };
116
+ }
117
+ } catch (err) {
118
+ return { error: err instanceof Error ? err.message : String(err) };
119
+ }
120
+ }
121
+
122
+ // ── CLABE (Mexico) ───────────────────────────────────────────────────
123
+
124
+ // ABM 3-digit bank codes — major institutions. The full Banxico catalog has
125
+ // ~100 entries; unknown codes return bank_name: null with a note rather than
126
+ // failing validation (the checksum is authoritative, the name is a courtesy).
127
+ const CLABE_BANKS: Record<string, string> = {
128
+ '002': 'Citibanamex',
129
+ '006': 'Bancomext',
130
+ '009': 'Banobras',
131
+ '012': 'BBVA México',
132
+ '014': 'Santander México',
133
+ '019': 'Banjército',
134
+ '021': 'HSBC México',
135
+ '030': 'Banco del Bajío',
136
+ '036': 'Banco Inbursa',
137
+ '042': 'Banca Mifel',
138
+ '044': 'Scotiabank México',
139
+ '058': 'Banregio',
140
+ '059': 'Banco Invex',
141
+ '060': 'Bansí',
142
+ '062': 'Banca Afirme',
143
+ '072': 'Banorte',
144
+ '103': 'American Express Bank México',
145
+ '106': 'Bank of America México',
146
+ '108': 'MUFG Bank México',
147
+ '110': 'J.P. Morgan México',
148
+ '112': 'Banco Monex',
149
+ '113': 'Banco Ve por Más (Bx+)',
150
+ '127': 'Banco Azteca',
151
+ '128': 'Banco Autofin México',
152
+ '129': 'Barclays Bank México',
153
+ '130': 'Compartamos Banco',
154
+ '132': 'Banco Multiva',
155
+ '133': 'Banco Actinver',
156
+ '135': 'Nacional Financiera (Nafin)',
157
+ '136': 'Intercam Banco',
158
+ '137': 'BanCoppel',
159
+ '138': 'ABC Capital',
160
+ '140': 'Consubanco',
161
+ '141': 'Volkswagen Bank México',
162
+ '143': 'CIBanco',
163
+ '145': 'Banco Base',
164
+ '147': 'Bankaool',
165
+ '148': 'Banco PagaTodo',
166
+ '150': 'Banco Inmobiliario Mexicano',
167
+ '152': 'Banco Bancrea',
168
+ '155': 'ICBC México',
169
+ '156': 'Banco Sabadell México',
170
+ '157': 'Shinhan Bank México',
171
+ '158': 'Mizuho Bank México',
172
+ '159': 'Bank of China México',
173
+ '160': 'Banco S3 México',
174
+ '166': 'Banco del Bienestar',
175
+ '168': 'Sociedad Hipotecaria Federal',
176
+ '601': 'GBM Grupo Bursátil Mexicano',
177
+ '602': 'Masari Casa de Bolsa',
178
+ '605': 'Value Casa de Bolsa',
179
+ '608': 'Vector Casa de Bolsa',
180
+ '616': 'Finamex Casa de Bolsa',
181
+ '620': 'Profuturo',
182
+ '638': 'Nu México',
183
+ '646': 'STP (Sistema de Transferencias y Pagos)',
184
+ '653': 'Kuspit Casa de Bolsa',
185
+ '684': 'Operadora de Pagos Móviles (Transfer)',
186
+ '706': 'Arcus Financial Intelligence',
187
+ '710': 'NVIO Pagos México',
188
+ '722': 'Mercado Pago',
189
+ };
190
+
191
+ function validateClabe(args: Record<string, unknown>): unknown {
192
+ const raw = typeof args.clabe === 'string' ? args.clabe : String(args.clabe ?? '');
193
+ const clabe = raw.replace(/[\s-]/g, '');
194
+ if (!/^\d+$/.test(clabe)) {
195
+ return { valid: false, error: 'user_error', message: 'CLABE must contain only digits (spaces/dashes allowed). Example: 002010077777777771.' };
196
+ }
197
+ if (clabe.length !== 18) {
198
+ return { valid: false, error: 'user_error', message: `CLABE must be exactly 18 digits (got ${clabe.length}). Example: 002010077777777771.` };
199
+ }
200
+ // Control digit: weights 3,7,1 cycling over the first 17 digits;
201
+ // control = (10 − (sum mod 10)) mod 10.
202
+ const weights = [3, 7, 1];
203
+ let sum = 0;
204
+ for (let i = 0; i < 17; i++) {
205
+ sum += (Number(clabe[i]) * weights[i % 3]) % 10;
206
+ }
207
+ const expected = (10 - (sum % 10)) % 10;
208
+ const actual = Number(clabe[17]);
209
+ const bankCode = clabe.slice(0, 3);
210
+ const bankName = CLABE_BANKS[bankCode] ?? null;
211
+ return {
212
+ valid: expected === actual,
213
+ clabe,
214
+ bank_code: bankCode,
215
+ bank_name: bankName,
216
+ ...(bankName === null ? { bank_note: 'Bank code not in the local catalog of major banks — the checksum result is still authoritative.' } : {}),
217
+ plaza_code: clabe.slice(3, 6),
218
+ account_number: clabe.slice(6, 17),
219
+ control_digit: actual,
220
+ ...(expected !== actual ? { expected_control_digit: expected, message: 'Checksum failed — the CLABE is mistyped or invalid.' } : {}),
221
+ };
222
+ }
223
+
224
+ // ── CNPJ / CPF (Brazil) ──────────────────────────────────────────────
225
+
226
+ // Mod-11 check digit over char values (digit value, or ASCII−48 for the 2026
227
+ // alphanumeric CNPJ letters). Returns the expected check digit.
228
+ function cnpjDigit(values: number[], weights: number[]): number {
229
+ const sum = values.reduce((acc, v, i) => acc + v * weights[i], 0);
230
+ const mod = sum % 11;
231
+ return mod < 2 ? 0 : 11 - mod;
232
+ }
233
+
234
+ function validateCnpjChecksum(cnpj: string): { valid: boolean; reason?: string } {
235
+ if (!/^[A-Z0-9]{12}\d{2}$/.test(cnpj)) {
236
+ return { valid: false, reason: 'CNPJ must be 14 characters: 12 alphanumeric (digits, or letters in the 2026 format) followed by 2 numeric check digits.' };
237
+ }
238
+ if (/^(\d)\1{13}$/.test(cnpj)) return { valid: false, reason: 'All-repeated-digit CNPJs are invalid.' };
239
+ const vals = [...cnpj.slice(0, 12)].map((c) => c.charCodeAt(0) - 48);
240
+ const d1 = cnpjDigit(vals, [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]);
241
+ const d2 = cnpjDigit([...vals, d1], [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]);
242
+ if (Number(cnpj[12]) !== d1 || Number(cnpj[13]) !== d2) {
243
+ return { valid: false, reason: `Check digits failed (expected ${d1}${d2}).` };
244
+ }
245
+ return { valid: true };
246
+ }
247
+
248
+ async function validateCnpj(args: Record<string, unknown>): Promise<unknown> {
249
+ const raw = typeof args.cnpj === 'string' ? args.cnpj : String(args.cnpj ?? '');
250
+ const cnpj = raw.toUpperCase().replace(/[\s./-]/g, '');
251
+ const check = validateCnpjChecksum(cnpj);
252
+ const base: Record<string, unknown> = {
253
+ valid: check.valid,
254
+ cnpj,
255
+ formatted: cnpj.length === 14 ? `${cnpj.slice(0, 2)}.${cnpj.slice(2, 5)}.${cnpj.slice(5, 8)}/${cnpj.slice(8, 12)}-${cnpj.slice(12)}` : cnpj,
256
+ alphanumeric: /[A-Z]/.test(cnpj),
257
+ };
258
+ if (!check.valid) return { ...base, message: check.reason };
259
+ if (args.skip_lookup === true) return base;
260
+
261
+ // Company-registration lookup (best-effort — checksum result stands alone).
262
+ try {
263
+ const res = await fetch(`${BRASILAPI}/cnpj/v1/${encodeURIComponent(cnpj)}`, {
264
+ headers: { Accept: 'application/json', 'User-Agent': UA },
265
+ });
266
+ if (res.status === 404) return { ...base, company: null, company_note: 'Checksum valid, but no registration found in the Receita Federal mirror (new, unregistered, or alphanumeric-format CNPJ).' };
267
+ if (!res.ok) return { ...base, company: null, company_note: `Checksum valid; registration lookup unavailable (BrasilAPI ${res.status}).` };
268
+ const d = (await res.json()) as Record<string, any>;
269
+ return {
270
+ ...base,
271
+ company: {
272
+ legal_name: d.razao_social ?? null,
273
+ trade_name: d.nome_fantasia || null,
274
+ status: d.descricao_situacao_cadastral ?? null,
275
+ opened: d.data_inicio_atividade ?? null,
276
+ legal_nature: d.natureza_juridica ?? null,
277
+ main_activity: d.cnae_fiscal_descricao ?? null,
278
+ cnae_code: d.cnae_fiscal ?? null,
279
+ share_capital: d.capital_social ?? null,
280
+ company_size: d.porte ?? null,
281
+ address: {
282
+ street: [d.descricao_tipo_de_logradouro, d.logradouro].filter(Boolean).join(' ') || null,
283
+ number: d.numero ?? null,
284
+ neighborhood: d.bairro ?? null,
285
+ city: d.municipio ?? null,
286
+ state: d.uf ?? null,
287
+ cep: d.cep ?? null,
288
+ },
289
+ phone: d.ddd_telefone_1 || null,
290
+ partners: Array.isArray(d.qsa)
291
+ ? d.qsa.slice(0, 20).map((p: Record<string, any>) => ({
292
+ name: p.nome_socio ?? null,
293
+ role: p.qualificacao_socio ?? null,
294
+ joined: p.data_entrada_sociedade ?? null,
295
+ }))
296
+ : [],
297
+ },
298
+ };
299
+ } catch {
300
+ return { ...base, company: null, company_note: 'Checksum valid; registration lookup unavailable (BrasilAPI unreachable).' };
301
+ }
302
+ }
303
+
304
+ function validateCpf(args: Record<string, unknown>): unknown {
305
+ const raw = typeof args.cpf === 'string' ? args.cpf : String(args.cpf ?? '');
306
+ const cpf = raw.replace(/[\s.-]/g, '');
307
+ if (!/^\d{11}$/.test(cpf)) {
308
+ return { valid: false, error: 'user_error', message: `CPF must be exactly 11 digits (got ${cpf.length}). Example: 123.456.789-09.` };
309
+ }
310
+ if (/^(\d)\1{10}$/.test(cpf)) {
311
+ return { valid: false, cpf, message: 'All-repeated-digit CPFs are invalid.' };
312
+ }
313
+ const digits = [...cpf].map(Number);
314
+ const check = (count: number): number => {
315
+ let sum = 0;
316
+ for (let i = 0; i < count; i++) sum += digits[i] * (count + 1 - i);
317
+ const mod = (sum * 10) % 11;
318
+ return mod === 10 ? 0 : mod;
319
+ };
320
+ const d1 = check(9);
321
+ const d2 = check(10);
322
+ const valid = d1 === digits[9] && d2 === digits[10];
323
+ return {
324
+ valid,
325
+ cpf,
326
+ formatted: `${cpf.slice(0, 3)}.${cpf.slice(3, 6)}.${cpf.slice(6, 9)}-${cpf.slice(9)}`,
327
+ ...(valid ? {} : { message: `Check digits failed (expected ${d1}${d2}).` }),
328
+ };
329
+ }
330
+
331
+ // ── BrasilAPI lookups ────────────────────────────────────────────────
332
+
333
+ async function brasilFetch(path: string): Promise<Response> {
334
+ return fetch(`${BRASILAPI}${path}`, { headers: { Accept: 'application/json', 'User-Agent': UA } });
335
+ }
336
+
337
+ async function brasilCep(args: Record<string, unknown>): Promise<unknown> {
338
+ const raw = typeof args.cep === 'string' ? args.cep : String(args.cep ?? '');
339
+ const cep = raw.replace(/[\s.-]/g, '');
340
+ if (!/^\d{8}$/.test(cep)) {
341
+ return { error: 'user_error', message: `CEP must be 8 digits (got "${raw}"). Example: 01310-100.` };
342
+ }
343
+ const res = await brasilFetch(`/cep/v2/${cep}`);
344
+ if (res.status === 404) return { cep, found: false, message: 'CEP not found.' };
345
+ if (!res.ok) return { error: 'upstream_error', message: `BrasilAPI: ${res.status}` };
346
+ const d = (await res.json()) as Record<string, any>;
347
+ const coords = d.location?.coordinates ?? {};
348
+ return {
349
+ cep: d.cep ?? cep,
350
+ found: true,
351
+ street: d.street ?? null,
352
+ neighborhood: d.neighborhood ?? null,
353
+ city: d.city ?? null,
354
+ state: d.state ?? null,
355
+ latitude: coords.latitude ?? null,
356
+ longitude: coords.longitude ?? null,
357
+ };
358
+ }
359
+
360
+ // Accent-fold + lowercase for search ("itau" must match "ITAÚ"). Common brand
361
+ // names differ from registry names (Nubank = "NU PAGAMENTOS - IP"), so a small
362
+ // alias map keeps household-name searches working.
363
+ function foldBank(s: string): string {
364
+ return s.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
365
+ }
366
+ const BANK_ALIASES: Record<string, string> = {
367
+ nubank: 'nu pagamentos',
368
+ inter: 'bco inter',
369
+ bb: 'bco do brasil',
370
+ caixa: 'caixa economica',
371
+ };
372
+
373
+ async function brasilBanks(args: Record<string, unknown>): Promise<unknown> {
374
+ const res = await brasilFetch('/banks/v1');
375
+ if (!res.ok) return { error: 'upstream_error', message: `BrasilAPI: ${res.status}` };
376
+ let banks = (await res.json()) as Array<{ ispb: string; name: string | null; code: number | null; fullName: string | null }>;
377
+ const code = typeof args.code === 'number' ? args.code : undefined;
378
+ let search = typeof args.search === 'string' && args.search.trim() ? foldBank(args.search.trim()) : undefined;
379
+ if (search && BANK_ALIASES[search]) search = BANK_ALIASES[search];
380
+ if (code !== undefined) banks = banks.filter((b) => b.code === code);
381
+ if (search) banks = banks.filter((b) => foldBank(b.name ?? '').includes(search!) || foldBank(b.fullName ?? '').includes(search!));
382
+ const limit = typeof args.limit === 'number' && args.limit > 0 ? Math.min(args.limit, 200) : 25;
383
+ return {
384
+ total_matching: banks.length,
385
+ count: Math.min(banks.length, limit),
386
+ banks: banks.slice(0, limit).map((b) => ({ code: b.code, name: b.name, full_name: b.fullName, ispb: b.ispb })),
387
+ };
388
+ }
389
+
390
+ export default { tools, callTool, meter: { credits: 1 } } satisfies McpToolExport;
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "outDir": "dist",
10
+ "rootDir": "src",
11
+ "declaration": true
12
+ },
13
+ "include": ["src"]
14
+ }