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 +21 -0
- package/README.md +148 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +504 -0
- package/package.json +61 -0
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
|
+
[](https://www.npmjs.com/package/brasil-data-mcp)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
[](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).
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|