brasil-data-mcp 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 Alan Castriola
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,148 @@
1
+ # brasil-data-mcp
2
+
3
+ [![npm version](https://img.shields.io/npm/v/brasil-data-mcp.svg)](https://www.npmjs.com/package/brasil-data-mcp)
4
+ [![license](https://img.shields.io/npm/l/brasil-data-mcp.svg)](./LICENSE)
5
+ [![node](https://img.shields.io/node/v/brasil-data-mcp.svg)](https://nodejs.org)
6
+
7
+ > MCP server que expõe dados públicos brasileiros (CNPJ, CEP, bancos, feriados) como tools pra Claude Desktop, Claude Code, Cursor, Windsurf e qualquer cliente compatível com [Model Context Protocol](https://modelcontextprotocol.io).
8
+ >
9
+ > _MCP server exposing Brazilian public data (CNPJ, CEP, banks, holidays) as tools for Claude Desktop, Claude Code, Cursor, Windsurf and any MCP-compatible client._
10
+
11
+ Powered by [BrasilAPI](https://brasilapi.com.br) — sem chave, sem auth, dados oficiais.
12
+
13
+ ---
14
+
15
+ ## 🇧🇷 PT — O que é?
16
+
17
+ Conecte seu cliente de IA aos dados públicos brasileiros sem escrever uma linha de código. Pergunte em linguagem natural:
18
+
19
+ - _"Qual a razão social do CNPJ 33.000.167/0001-01?"_
20
+ - _"Esse CEP 01310-100 é em qual cidade?"_
21
+ - _"Quem é o banco com código 341?"_
22
+ - _"Quais os feriados nacionais de 2026?"_
23
+
24
+ O Claude (ou outro cliente MCP) chama a tool, retorna o JSON estruturado, e você lê a resposta em português direto na conversa.
25
+
26
+ ### Tools disponíveis
27
+
28
+ | Tool | O que faz |
29
+ | --------------------- | ------------------------------------------------------------------------------- |
30
+ | `consultar_cnpj` | Dados cadastrais de empresa: razão social, situação, endereço, sócios, CNAE |
31
+ | `consultar_cep` | Endereço completo a partir de CEP (logradouro, bairro, cidade, UF, coordenadas) |
32
+ | `consultar_banco` | Nome e ISPB de banco brasileiro pelo código COMPE (ex: 341 = Itaú, 260 = Nubank) |
33
+ | `listar_bancos` | Lista completa de bancos brasileiros cadastrados no BACEN (~250 instituições) |
34
+ | `consultar_feriados` | Feriados nacionais de um ano (datas, nome, tipo) — inclui Carnaval e Páscoa |
35
+
36
+ ---
37
+
38
+ ## 🇺🇸 EN — What is it?
39
+
40
+ Plug your AI client into Brazilian public data with zero code. Ask in natural language and the LLM picks the right tool, calls it, and answers you with structured data from official sources (Receita Federal, ViaCEP, BACEN).
41
+
42
+ Currently ships with `consultar_cnpj`. CEP, banks and holidays are landing next.
43
+
44
+ ---
45
+
46
+ ## 🚀 Instalação / Installation
47
+
48
+ Todas as instruções abaixo usam `npx -y brasil-data-mcp`, o que baixa e roda a última versão sem instalação global.
49
+
50
+ ### Claude Desktop
51
+
52
+ Edite o arquivo de configuração:
53
+
54
+ - **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
55
+ - **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
56
+
57
+ ```json
58
+ {
59
+ "mcpServers": {
60
+ "brasil-data": {
61
+ "command": "npx",
62
+ "args": ["-y", "brasil-data-mcp"]
63
+ }
64
+ }
65
+ }
66
+ ```
67
+
68
+ Reinicie o Claude Desktop. Pronto.
69
+
70
+ ### Claude Code
71
+
72
+ ```bash
73
+ claude mcp add brasil-data -- npx -y brasil-data-mcp
74
+ ```
75
+
76
+ ### Cursor
77
+
78
+ Crie ou edite `.cursor/mcp.json` na raiz do projeto:
79
+
80
+ ```json
81
+ {
82
+ "mcpServers": {
83
+ "brasil-data": {
84
+ "command": "npx",
85
+ "args": ["-y", "brasil-data-mcp"]
86
+ }
87
+ }
88
+ }
89
+ ```
90
+
91
+ ---
92
+
93
+ ## 🛠️ Desenvolvimento / Development
94
+
95
+ ```bash
96
+ git clone https://github.com/alanpcf/brasil-data-mcp.git
97
+ cd brasil-data-mcp
98
+ npm install
99
+
100
+ npm run dev # tsx watch — hot reload em desenvolvimento
101
+ npm run lint # tsc --noEmit
102
+ npm run build # tsup → dist/index.js
103
+ npm test # vitest
104
+ ```
105
+
106
+ Pra apontar seu cliente MCP pro build local em vez do pacote do npm:
107
+
108
+ ```json
109
+ {
110
+ "mcpServers": {
111
+ "brasil-data-local": {
112
+ "command": "node",
113
+ "args": ["/caminho/absoluto/para/brasil-data-mcp/dist/index.js"]
114
+ }
115
+ }
116
+ }
117
+ ```
118
+
119
+ ---
120
+
121
+ ## 🗺️ Roadmap
122
+
123
+ - [x] **Fase 1** — Esqueleto + cliente HTTP + `consultar_cnpj`
124
+ - [x] **Fase 2** — `consultar_cep`, `consultar_banco`, `listar_bancos`, `consultar_feriados` + testes Vitest
125
+ - [ ] **Fase 3** — CI (GitHub Actions), CONTRIBUTING.md, cobertura > 80%, publicação no npm
126
+ - [ ] **Fase 4** — FIPE, DDD, ISBN, taxas (SELIC/CDI/IPCA), CVM, MCP prompts pra workflows
127
+
128
+ ---
129
+
130
+ ## 💡 Por que esse projeto existe
131
+
132
+ A maior parte das ferramentas de IA é treinada e demonstrada com dados americanos: ZIP code, EIN, FedEx tracking. Quando um dev brasileiro quer perguntar pra um LLM "me dá o cadastro do CNPJ X", ou cai em scraping, ou monta uma integração HTTP no braço, ou desiste.
133
+
134
+ `brasil-data-mcp` é o caminho mais curto: um único `npx` e o seu Claude (ou Cursor, ou Windsurf) já fala "português de dado público brasileiro". Tudo open source, MIT, sem chave, sem rate limit hostil — porque a [BrasilAPI](https://brasilapi.com.br) já fez o trabalho pesado de unificar e cachear dados oficiais.
135
+
136
+ Se você é dev brasileiro e usa LLM no dia a dia, esse server é pra você.
137
+
138
+ ---
139
+
140
+ ## 🤝 Contribuindo / Contributing
141
+
142
+ Issues e PRs muito bem-vindos. Pra adicionar uma tool nova, siga o padrão de `src/tools/cnpj.ts` (schema Zod + descrição + handler) e registre em `src/index.ts`.
143
+
144
+ Guia detalhado em `CONTRIBUTING.md` _(em breve)_.
145
+
146
+ ---
147
+
148
+ Built with ❤️ in Brazil. Powered by [BrasilAPI](https://brasilapi.com.br).
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+
4
+ /**
5
+ * Entry point do brasil-data-mcp.
6
+ *
7
+ * Boot:
8
+ * 1. Cria um McpServer (high-level API do SDK 1.x).
9
+ * 2. Registra cada tool via registerTool — o SDK deriva o JSON Schema do
10
+ * schema Zod, então atendemos "validação dupla" (Zod runtime + JSON
11
+ * Schema na definição) com uma única declaração por tool.
12
+ * 3. Conecta no StdioServerTransport e fica em loop atendendo requisições.
13
+ *
14
+ * REGRA CRÍTICA: nunca escrever em stdout fora do protocolo. Logs em stderr
15
+ * via console.error. console.log corrompe o canal MCP.
16
+ *
17
+ * REGISTRY: adicionar tool nova = uma chamada de registerTool. O helper
18
+ * wrapHandler aplica o try/catch defensivo padronizado (bug numa tool não
19
+ * derruba o server).
20
+ */
21
+
22
+ declare function createServer(): McpServer;
23
+
24
+ export { createServer };
package/dist/index.js ADDED
@@ -0,0 +1,504 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+
7
+ // src/tools/banco.ts
8
+ import { z } from "zod";
9
+
10
+ // src/clients/brasilapi.ts
11
+ var BASE_URL = "https://brasilapi.com.br/api";
12
+ var DEFAULT_TIMEOUT_MS = 1e4;
13
+ var DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3;
14
+ var MAX_RETRIES = 3;
15
+ var USER_AGENT = "brasil-data-mcp/0.1.0 (+https://github.com/alanpcf/brasil-data-mcp)";
16
+ var BrasilApiError = class extends Error {
17
+ constructor(message, status, path, body) {
18
+ super(message);
19
+ this.status = status;
20
+ this.path = path;
21
+ this.body = body;
22
+ this.name = "BrasilApiError";
23
+ }
24
+ status;
25
+ path;
26
+ body;
27
+ };
28
+ var cache = /* @__PURE__ */ new Map();
29
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
30
+ function deveRetentar(status) {
31
+ if (status === null) return true;
32
+ if (status === 429) return true;
33
+ if (status >= 500 && status < 600) return true;
34
+ return false;
35
+ }
36
+ async function executarRequisicao(path, timeoutMs) {
37
+ const controller = new AbortController();
38
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
39
+ try {
40
+ const response = await fetch(`${BASE_URL}${path}`, {
41
+ method: "GET",
42
+ headers: {
43
+ Accept: "application/json",
44
+ "User-Agent": USER_AGENT
45
+ },
46
+ signal: controller.signal
47
+ });
48
+ let body;
49
+ const text = await response.text();
50
+ try {
51
+ body = text ? JSON.parse(text) : null;
52
+ } catch {
53
+ body = text;
54
+ }
55
+ return { status: response.status, body, ok: response.ok };
56
+ } finally {
57
+ clearTimeout(timer);
58
+ }
59
+ }
60
+ async function getInterno(path, opts) {
61
+ const ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
62
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
63
+ if (ttlMs > 0) {
64
+ const hit = cache.get(path);
65
+ if (hit && hit.expiresAt > Date.now()) {
66
+ return hit.value;
67
+ }
68
+ }
69
+ let ultimoErro;
70
+ for (let tentativa = 0; tentativa <= MAX_RETRIES; tentativa++) {
71
+ try {
72
+ const { status, body, ok } = await executarRequisicao(path, timeoutMs);
73
+ if (ok) {
74
+ if (ttlMs > 0) {
75
+ cache.set(path, { value: body, expiresAt: Date.now() + ttlMs });
76
+ }
77
+ return body;
78
+ }
79
+ if (!deveRetentar(status) || tentativa === MAX_RETRIES) {
80
+ throw new BrasilApiError(
81
+ `BrasilAPI retornou ${status} em ${path}`,
82
+ status,
83
+ path,
84
+ body
85
+ );
86
+ }
87
+ ultimoErro = new BrasilApiError(
88
+ `BrasilAPI ${status} (transiente)`,
89
+ status,
90
+ path,
91
+ body
92
+ );
93
+ } catch (err) {
94
+ if (err instanceof BrasilApiError) throw err;
95
+ ultimoErro = err;
96
+ if (tentativa === MAX_RETRIES) {
97
+ const msg = err instanceof Error ? err.message : String(err);
98
+ throw new BrasilApiError(`Falha de rede em ${path}: ${msg}`, 0, path);
99
+ }
100
+ }
101
+ const delayMs = 200 * Math.pow(2, tentativa);
102
+ await sleep(delayMs);
103
+ }
104
+ throw ultimoErro instanceof Error ? ultimoErro : new Error(`Falha desconhecida em ${path}`);
105
+ }
106
+ var brasilApi = {
107
+ get(path, opts = {}) {
108
+ return getInterno(path, opts);
109
+ },
110
+ clearCache() {
111
+ cache.clear();
112
+ }
113
+ };
114
+
115
+ // src/utils/errors.ts
116
+ function traduzirErroBrasilApi(err, mapa) {
117
+ const ctx = mapa.contextoErro ?? "Erro ao consultar dados";
118
+ if (err instanceof BrasilApiError) {
119
+ if (err.status === 404) return mapa.notFound;
120
+ if (err.status === 400) {
121
+ return `${ctx}: dados inv\xE1lidos enviados ao servi\xE7o.`;
122
+ }
123
+ if (err.status === 429) {
124
+ return `${ctx}: limite de requisi\xE7\xF5es temporariamente atingido. Tente novamente em alguns segundos.`;
125
+ }
126
+ if (err.status >= 500) {
127
+ return `${ctx}: servi\xE7o temporariamente indispon\xEDvel. Tente novamente em alguns instantes.`;
128
+ }
129
+ if (err.status === 0) {
130
+ return `${ctx}: falha de rede ao alcan\xE7ar o servi\xE7o.`;
131
+ }
132
+ return `${ctx}: o servi\xE7o retornou erro inesperado (${err.status}).`;
133
+ }
134
+ const msg = err instanceof Error ? err.message : String(err);
135
+ return `${ctx}: ${msg}`;
136
+ }
137
+
138
+ // src/tools/banco.ts
139
+ var consultarBancoSchema = z.object({
140
+ codigo: z.union([z.string(), z.number()]).describe(
141
+ "C\xF3digo COMPE/Febraban do banco (1 a 4 d\xEDgitos). Aceita string ou n\xFAmero. Ex: 341 (Ita\xFA), 260 (Nubank), 237 (Bradesco)."
142
+ )
143
+ });
144
+ var consultarBancoTool = {
145
+ name: "consultar_banco",
146
+ description: [
147
+ "Consulta os dados de um banco brasileiro pelo c\xF3digo COMPE/Febraban via BrasilAPI (fonte: BACEN).",
148
+ "",
149
+ "Retorna em JSON: nome curto, nome completo, c\xF3digo, ISPB (identificador no SPB).",
150
+ "",
151
+ "Use quando o usu\xE1rio fornecer um c\xF3digo de banco e quiser saber o nome, ou quando precisar do ISPB pra montar um PIX/TED.",
152
+ "",
153
+ "N\xC3O use para: buscar banco por nome (use listar_bancos e filtre), validar conta corrente, ou consultar ag\xEAncia/conta. C\xF3digos comuns: 001=BB, 104=CEF, 237=Bradesco, 341=Ita\xFA, 260=Nubank, 077=Inter."
154
+ ].join(" "),
155
+ inputSchema: consultarBancoSchema
156
+ };
157
+ function normalizarCodigo(codigo) {
158
+ const s = String(codigo).trim();
159
+ if (!/^\d{1,4}$/.test(s)) return "";
160
+ return s;
161
+ }
162
+ async function consultarBancoHandler(input) {
163
+ const codigo = normalizarCodigo(input.codigo);
164
+ if (!codigo) {
165
+ return {
166
+ content: [
167
+ {
168
+ type: "text",
169
+ text: `C\xF3digo de banco inv\xE1lido: '${input.codigo}'. Deve ser num\xE9rico, de 1 a 4 d\xEDgitos.`
170
+ }
171
+ ],
172
+ isError: true
173
+ };
174
+ }
175
+ try {
176
+ const dados = await brasilApi.get(`/banks/v1/${codigo}`);
177
+ return {
178
+ content: [{ type: "text", text: JSON.stringify(dados, null, 2) }]
179
+ };
180
+ } catch (err) {
181
+ return {
182
+ content: [
183
+ {
184
+ type: "text",
185
+ text: traduzirErroBrasilApi(err, {
186
+ notFound: `Banco com c\xF3digo ${codigo} n\xE3o encontrado no cadastro do BACEN.`,
187
+ contextoErro: "Erro ao consultar banco"
188
+ })
189
+ }
190
+ ],
191
+ isError: true
192
+ };
193
+ }
194
+ }
195
+ var listarBancosSchema = z.object({});
196
+ var listarBancosTool = {
197
+ name: "listar_bancos",
198
+ description: [
199
+ "Lista TODOS os bancos brasileiros cadastrados no BACEN via BrasilAPI.",
200
+ "",
201
+ "Retorna em JSON um array com nome, c\xF3digo COMPE/Febraban e ISPB de cada institui\xE7\xE3o.",
202
+ "",
203
+ "Use quando o usu\xE1rio quiser uma lista completa, buscar banco por nome (voc\xEA filtra o resultado), ou descobrir o c\xF3digo de um banco espec\xEDfico cujo nome ele forneceu.",
204
+ "",
205
+ "N\xC3O use quando o usu\xE1rio j\xE1 forneceu o c\xF3digo num\xE9rico \u2014 nesse caso use consultar_banco que \xE9 mais barato. A lista tem ~250 entradas; cite s\xF3 os relevantes na resposta."
206
+ ].join(" "),
207
+ inputSchema: listarBancosSchema
208
+ };
209
+ async function listarBancosHandler(_input) {
210
+ try {
211
+ const dados = await brasilApi.get("/banks/v1");
212
+ return {
213
+ content: [{ type: "text", text: JSON.stringify(dados, null, 2) }]
214
+ };
215
+ } catch (err) {
216
+ return {
217
+ content: [
218
+ {
219
+ type: "text",
220
+ text: traduzirErroBrasilApi(err, {
221
+ // Não há "404" semântico pra lista — se a API errar, todos
222
+ // caem no mesmo balde de "indisponível".
223
+ notFound: "Lista de bancos n\xE3o dispon\xEDvel no momento.",
224
+ contextoErro: "Erro ao listar bancos"
225
+ })
226
+ }
227
+ ],
228
+ isError: true
229
+ };
230
+ }
231
+ }
232
+
233
+ // src/tools/cep.ts
234
+ import { z as z2 } from "zod";
235
+ var consultarCepSchema = z2.object({
236
+ cep: z2.string().describe(
237
+ "CEP brasileiro com ou sem h\xEDfen. Aceita '01310-100' ou '01310100'. Deve ter 8 d\xEDgitos."
238
+ )
239
+ });
240
+ var consultarCepTool = {
241
+ name: "consultar_cep",
242
+ description: [
243
+ "Consulta endere\xE7o completo a partir de um CEP brasileiro via BrasilAPI v2 (agrega ViaCEP, Postmon e outros provedores com fallback autom\xE1tico).",
244
+ "",
245
+ "Retorna em JSON: estado (UF), cidade, bairro, logradouro e, quando dispon\xEDvel, coordenadas geogr\xE1ficas.",
246
+ "",
247
+ "Use quando o usu\xE1rio pedir o endere\xE7o de um CEP, validar um CEP, ou descobrir cidade/UF a partir de um CEP.",
248
+ "",
249
+ "N\xC3O use para: c\xF3digos postais de outros pa\xEDses, descobrir CEP a partir de endere\xE7o (a opera\xE7\xE3o \xE9 s\xF3 CEP \u2192 endere\xE7o, n\xE3o inversa). Aceita CEP com ou sem h\xEDfen."
250
+ ].join(" "),
251
+ inputSchema: consultarCepSchema
252
+ };
253
+ function limparCep(s) {
254
+ return s.replace(/\D/g, "");
255
+ }
256
+ function validarCep(s) {
257
+ if (!/^\d{8}$/.test(s)) return false;
258
+ if (/^(\d)\1{7}$/.test(s)) return false;
259
+ return true;
260
+ }
261
+ async function consultarCepHandler(input) {
262
+ const cepLimpo = limparCep(input.cep);
263
+ if (!validarCep(cepLimpo)) {
264
+ return {
265
+ content: [
266
+ {
267
+ type: "text",
268
+ text: `CEP inv\xE1lido: '${input.cep}'. Deve conter 8 d\xEDgitos (com ou sem h\xEDfen) e n\xE3o pode ser uma sequ\xEAncia repetida.`
269
+ }
270
+ ],
271
+ isError: true
272
+ };
273
+ }
274
+ try {
275
+ const dados = await brasilApi.get(`/cep/v2/${cepLimpo}`);
276
+ return {
277
+ content: [{ type: "text", text: JSON.stringify(dados, null, 2) }]
278
+ };
279
+ } catch (err) {
280
+ return {
281
+ content: [
282
+ {
283
+ type: "text",
284
+ text: traduzirErroBrasilApi(err, {
285
+ notFound: `CEP ${cepLimpo} n\xE3o encontrado. Verifique se est\xE1 correto \u2014 alguns CEPs muito espec\xEDficos s\xF3 existem em provedores pagos.`,
286
+ contextoErro: "Erro ao consultar CEP"
287
+ })
288
+ }
289
+ ],
290
+ isError: true
291
+ };
292
+ }
293
+ }
294
+
295
+ // src/tools/cnpj.ts
296
+ import { z as z3 } from "zod";
297
+ var consultarCnpjSchema = z3.object({
298
+ cnpj: z3.string().describe(
299
+ "CNPJ da empresa, com ou sem m\xE1scara. Aceita '12.345.678/0001-90' ou '12345678000190'. Deve ter 14 d\xEDgitos."
300
+ )
301
+ });
302
+ var consultarCnpjTool = {
303
+ name: "consultar_cnpj",
304
+ // Descrição é PRODUTO: é o que o LLM lê pra decidir quando chamar.
305
+ // Explicita o que retorna, quando usar e quando NÃO usar.
306
+ description: [
307
+ "Consulta dados cadastrais de uma empresa brasileira pelo CNPJ na Receita Federal (via BrasilAPI).",
308
+ "",
309
+ "Retorna em JSON: raz\xE3o social, nome fantasia, situa\xE7\xE3o cadastral (ativa/baixada/etc), data de abertura, ",
310
+ "endere\xE7o completo, CNAE principal e secund\xE1rios, s\xF3cios (QSA), capital social, natureza jur\xEDdica, ",
311
+ "porte (MEI/ME/EPP/Demais), telefones, e-mail, simples nacional/MEI.",
312
+ "",
313
+ "Use quando o usu\xE1rio pedir informa\xE7\xF5es sobre uma empresa identificada por CNPJ.",
314
+ "",
315
+ "N\xC3O use para: CPF (pessoa f\xEDsica), empresas estrangeiras, ou valida\xE7\xE3o local de formato ",
316
+ "(rejeite formato inv\xE1lido sem chamar a tool). Aceita CNPJ com ou sem m\xE1scara."
317
+ ].join(" "),
318
+ inputSchema: consultarCnpjSchema
319
+ };
320
+ function limparCnpj(s) {
321
+ return s.replace(/\D/g, "");
322
+ }
323
+ function validarCnpj(s) {
324
+ if (!/^\d{14}$/.test(s)) return false;
325
+ if (/^(\d)\1{13}$/.test(s)) return false;
326
+ return true;
327
+ }
328
+ async function consultarCnpjHandler(input) {
329
+ const cnpjLimpo = limparCnpj(input.cnpj);
330
+ if (!validarCnpj(cnpjLimpo)) {
331
+ return {
332
+ content: [
333
+ {
334
+ type: "text",
335
+ text: `CNPJ inv\xE1lido: '${input.cnpj}'. Deve conter 14 d\xEDgitos (com ou sem m\xE1scara) e n\xE3o pode ser uma sequ\xEAncia repetida.`
336
+ }
337
+ ],
338
+ isError: true
339
+ };
340
+ }
341
+ try {
342
+ const dados = await brasilApi.get(`/cnpj/v1/${cnpjLimpo}`);
343
+ return {
344
+ content: [
345
+ {
346
+ type: "text",
347
+ // JSON estruturado, não texto livre — o modelo extrai campos com
348
+ // mais confiabilidade quando recebe JSON.
349
+ text: JSON.stringify(dados, null, 2)
350
+ }
351
+ ]
352
+ };
353
+ } catch (err) {
354
+ return {
355
+ content: [
356
+ {
357
+ type: "text",
358
+ text: traduzirErroBrasilApi(err, {
359
+ notFound: `CNPJ ${cnpjLimpo} n\xE3o encontrado na base da Receita Federal. Verifique se est\xE1 correto.`,
360
+ contextoErro: "Erro ao consultar CNPJ"
361
+ })
362
+ }
363
+ ],
364
+ isError: true
365
+ };
366
+ }
367
+ }
368
+
369
+ // src/tools/feriados.ts
370
+ import { z as z4 } from "zod";
371
+ var consultarFeriadosSchema = z4.object({
372
+ ano: z4.number().int().describe(
373
+ "Ano dos feriados, 4 d\xEDgitos. Faixa aceita: 1900 a 2199. Ex: 2026."
374
+ )
375
+ });
376
+ var consultarFeriadosTool = {
377
+ name: "consultar_feriados",
378
+ description: [
379
+ "Lista os feriados NACIONAIS brasileiros de um ano espec\xEDfico via BrasilAPI.",
380
+ "",
381
+ "Retorna em JSON um array com data (YYYY-MM-DD), nome do feriado e tipo (national/optional). Inclui feriados m\xF3veis calculados (Carnaval, P\xE1scoa, Corpus Christi).",
382
+ "",
383
+ "Use quando o usu\xE1rio perguntar quando cai um feriado, listar feriados do ano, planejar emendas/pontes, ou calcular dias \xFAteis.",
384
+ "",
385
+ "N\xC3O use para: feriados estaduais ou municipais (a API s\xF3 cobre nacionais), datas comemorativas sem dia de folga (Dia das M\xE3es etc.), ou anos fora da faixa 1900-2199."
386
+ ].join(" "),
387
+ inputSchema: consultarFeriadosSchema
388
+ };
389
+ function validarAno(ano) {
390
+ return Number.isInteger(ano) && ano >= 1900 && ano <= 2199;
391
+ }
392
+ async function consultarFeriadosHandler(input) {
393
+ if (!validarAno(input.ano)) {
394
+ return {
395
+ content: [
396
+ {
397
+ type: "text",
398
+ text: `Ano inv\xE1lido: ${input.ano}. Deve ser inteiro entre 1900 e 2199.`
399
+ }
400
+ ],
401
+ isError: true
402
+ };
403
+ }
404
+ try {
405
+ const dados = await brasilApi.get(`/feriados/v1/${input.ano}`);
406
+ return {
407
+ content: [{ type: "text", text: JSON.stringify(dados, null, 2) }]
408
+ };
409
+ } catch (err) {
410
+ return {
411
+ content: [
412
+ {
413
+ type: "text",
414
+ text: traduzirErroBrasilApi(err, {
415
+ notFound: `Feriados de ${input.ano} n\xE3o encontrados na base.`,
416
+ contextoErro: "Erro ao consultar feriados"
417
+ })
418
+ }
419
+ ],
420
+ isError: true
421
+ };
422
+ }
423
+ }
424
+
425
+ // src/index.ts
426
+ var VERSION = "0.1.0";
427
+ function wrapHandler(toolName, handler) {
428
+ return async (input) => {
429
+ try {
430
+ return await handler(input);
431
+ } catch (err) {
432
+ const msg = err instanceof Error ? err.message : String(err);
433
+ return {
434
+ content: [
435
+ {
436
+ type: "text",
437
+ text: `Erro interno na tool ${toolName}: ${msg}`
438
+ }
439
+ ],
440
+ isError: true
441
+ };
442
+ }
443
+ };
444
+ }
445
+ function createServer() {
446
+ const server = new McpServer({
447
+ name: "brasil-data-mcp",
448
+ version: VERSION
449
+ });
450
+ server.registerTool(
451
+ consultarCnpjTool.name,
452
+ {
453
+ description: consultarCnpjTool.description,
454
+ inputSchema: consultarCnpjSchema.shape
455
+ },
456
+ wrapHandler(consultarCnpjTool.name, consultarCnpjHandler)
457
+ );
458
+ server.registerTool(
459
+ consultarCepTool.name,
460
+ {
461
+ description: consultarCepTool.description,
462
+ inputSchema: consultarCepSchema.shape
463
+ },
464
+ wrapHandler(consultarCepTool.name, consultarCepHandler)
465
+ );
466
+ server.registerTool(
467
+ consultarBancoTool.name,
468
+ {
469
+ description: consultarBancoTool.description,
470
+ inputSchema: consultarBancoSchema.shape
471
+ },
472
+ wrapHandler(consultarBancoTool.name, consultarBancoHandler)
473
+ );
474
+ server.registerTool(
475
+ listarBancosTool.name,
476
+ {
477
+ description: listarBancosTool.description,
478
+ inputSchema: listarBancosSchema.shape
479
+ },
480
+ wrapHandler(listarBancosTool.name, listarBancosHandler)
481
+ );
482
+ server.registerTool(
483
+ consultarFeriadosTool.name,
484
+ {
485
+ description: consultarFeriadosTool.description,
486
+ inputSchema: consultarFeriadosSchema.shape
487
+ },
488
+ wrapHandler(consultarFeriadosTool.name, consultarFeriadosHandler)
489
+ );
490
+ return server;
491
+ }
492
+ async function main() {
493
+ const server = createServer();
494
+ const transport = new StdioServerTransport();
495
+ await server.connect(transport);
496
+ console.error(`[brasil-data-mcp] v${VERSION} iniciado via stdio`);
497
+ }
498
+ main().catch((err) => {
499
+ console.error("[brasil-data-mcp] falha fatal:", err);
500
+ process.exit(1);
501
+ });
502
+ export {
503
+ createServer
504
+ };
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "brasil-data-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server providing access to Brazilian public data (CNPJ, CEP, banks, holidays) via BrasilAPI. For use with Claude Desktop, Claude Code, Cursor, and other MCP-compatible clients.",
5
+ "keywords": [
6
+ "mcp",
7
+ "model-context-protocol",
8
+ "claude",
9
+ "brasil",
10
+ "brazil",
11
+ "cnpj",
12
+ "cep",
13
+ "brasilapi",
14
+ "ai-tools"
15
+ ],
16
+ "author": "Alan Castriola <alanpcf87@gmail.com>",
17
+ "license": "MIT",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/alanpcf/brasil-data-mcp.git"
21
+ },
22
+ "homepage": "https://github.com/alanpcf/brasil-data-mcp#readme",
23
+ "bugs": {
24
+ "url": "https://github.com/alanpcf/brasil-data-mcp/issues"
25
+ },
26
+ "type": "module",
27
+ "main": "dist/index.js",
28
+ "bin": {
29
+ "brasil-data-mcp": "dist/index.js"
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "README.md",
34
+ "LICENSE"
35
+ ],
36
+ "engines": {
37
+ "node": ">=18.0.0"
38
+ },
39
+ "scripts": {
40
+ "build": "tsup src/index.ts --format esm --dts --clean --shims",
41
+ "dev": "tsx watch src/index.ts",
42
+ "start": "node dist/index.js",
43
+ "test": "vitest run",
44
+ "test:watch": "vitest",
45
+ "test:coverage": "vitest run --coverage",
46
+ "lint": "tsc --noEmit",
47
+ "prepublishOnly": "npm run lint && npm run test && npm run build"
48
+ },
49
+ "dependencies": {
50
+ "@modelcontextprotocol/sdk": "^1.0.0",
51
+ "zod": "^3.23.8"
52
+ },
53
+ "devDependencies": {
54
+ "@types/node": "^22.0.0",
55
+ "@vitest/coverage-v8": "^2.1.9",
56
+ "tsup": "^8.3.0",
57
+ "tsx": "^4.19.0",
58
+ "typescript": "^5.6.0",
59
+ "vitest": "^2.1.0"
60
+ }
61
+ }