ibge-mcp-server 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 +112 -0
- package/dist/env.js +19 -0
- package/dist/index.js +32 -0
- package/dist/tools/agregado-dados.js +56 -0
- package/dist/tools/agregado-metadados.js +42 -0
- package/dist/tools/agregados.js +39 -0
- package/dist/tools/data-hora.js +61 -0
- package/dist/tools/index.js +6 -0
- package/dist/tools/localidades.js +87 -0
- package/dist/tools/pesquisas.js +22 -0
- package/dist/types/agregado.js +1 -0
- package/dist/types/localidade.js +1 -0
- package/dist/version.js +4 -0
- package/package.json +71 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vinicius Cestari
|
|
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,112 @@
|
|
|
1
|
+
# IBGE MCP Server
|
|
2
|
+
|
|
3
|
+
Servidor MCP que expõe as APIs públicas do IBGE (agregados e localidades) como ferramentas para agentes de IA.
|
|
4
|
+
|
|
5
|
+
## Ferramentas
|
|
6
|
+
|
|
7
|
+
| Ferramenta | Descrição |
|
|
8
|
+
| --------------------- | ------------------------------------------------------------------ |
|
|
9
|
+
| `pesquisas` | Lista as pesquisas do IBGE |
|
|
10
|
+
| `agregados` | Lista os agregados de uma pesquisa |
|
|
11
|
+
| `agregado-metadados` | Variáveis, níveis territoriais e períodos de um agregado |
|
|
12
|
+
| `agregado-dados` | Busca os dados de um agregado |
|
|
13
|
+
| `localidades` | Descobre o id de um estado ou cidade |
|
|
14
|
+
| `data-hora` | Data e hora atuais no fuso de Brasília |
|
|
15
|
+
|
|
16
|
+
## Requisitos
|
|
17
|
+
|
|
18
|
+
- Node.js 20+
|
|
19
|
+
|
|
20
|
+
## Instalação
|
|
21
|
+
|
|
22
|
+
Como pacote publicado no npm:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx ibge-mcp-server
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Para desenvolver localmente:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Uso
|
|
35
|
+
|
|
36
|
+
Desenvolvimento:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm run dev
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Produção:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm run build
|
|
46
|
+
npm start
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Inspecionar as ferramentas no MCP Inspector:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm run inspect
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Configuração
|
|
56
|
+
|
|
57
|
+
Crie um arquivo `.env` na raiz do projeto. Todas as variáveis são opcionais e têm valores padrão:
|
|
58
|
+
|
|
59
|
+
| Variável | Padrão |
|
|
60
|
+
| ----------------------- | ------------------------------------------------------- |
|
|
61
|
+
| `TRANSPORT_TYPE` | `stdio` (ou `httpStream`) |
|
|
62
|
+
| `PORT` | `3000` (usado apenas com `httpStream`) |
|
|
63
|
+
| `IBGE_API_AGREGADOS` | `https://servicodados.ibge.gov.br/api/v3/agregados` |
|
|
64
|
+
| `IBGE_API_LOCALIDADES` | `https://servicodados.ibge.gov.br/api/v1/localidades` |
|
|
65
|
+
|
|
66
|
+
## Conectando a um cliente MCP
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"mcpServers": {
|
|
71
|
+
"ibge": {
|
|
72
|
+
"command": "npx",
|
|
73
|
+
"args": ["-y", "ibge-mcp-server"]
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Usando o código local:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"mcpServers": {
|
|
84
|
+
"ibge": {
|
|
85
|
+
"command": "npx",
|
|
86
|
+
"args": ["tsx", "src/index.ts"],
|
|
87
|
+
"cwd": "/caminho/para/ibge-mcp-server"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Testes
|
|
94
|
+
|
|
95
|
+
Os testes são avaliações que rodam um agente real contra o servidor e comparam a resposta com o resultado esperado. Eles usam um `.env` próprio: copie `test/.env.test.example` para `test/.env.test` e preencha as chaves.
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npm test # runner padrão definido em TEST_RUNNER
|
|
99
|
+
npm run test:gemini # usa a API do Gemini
|
|
100
|
+
npm run test:claude-code # usa o Claude Agent SDK
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Outros comandos
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npm run lint
|
|
107
|
+
npm run format
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Licença
|
|
111
|
+
|
|
112
|
+
[MIT](LICENSE)
|
package/dist/env.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
const envSchema = z.object({
|
|
4
|
+
TRANSPORT_TYPE: z.enum(["stdio", "httpStream"]).default("stdio"),
|
|
5
|
+
PORT: z.preprocess((val) => (val === undefined ? undefined : Number(val)), z.number().int().default(3000)),
|
|
6
|
+
IBGE_API_AGREGADOS: z
|
|
7
|
+
.url()
|
|
8
|
+
.default("https://servicodados.ibge.gov.br/api/v3/agregados"),
|
|
9
|
+
IBGE_API_LOCALIDADES: z
|
|
10
|
+
.url()
|
|
11
|
+
.default("https://servicodados.ibge.gov.br/api/v1/localidades"),
|
|
12
|
+
});
|
|
13
|
+
const envParse = envSchema.safeParse(process.env);
|
|
14
|
+
if (!envParse.success) {
|
|
15
|
+
console.error("Invalid environment variables:", envParse.error.issues);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
const env = envParse.data;
|
|
19
|
+
export default env;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
5
|
+
import { createServer } from "node:http";
|
|
6
|
+
import env from "./env.js";
|
|
7
|
+
import * as registerTool from "./tools/index.js";
|
|
8
|
+
import version from "./version.js";
|
|
9
|
+
const server = new McpServer({
|
|
10
|
+
name: "IBGE MCP Server",
|
|
11
|
+
version,
|
|
12
|
+
});
|
|
13
|
+
const tools = Object.values(registerTool);
|
|
14
|
+
for (const tool of tools) {
|
|
15
|
+
tool(server);
|
|
16
|
+
}
|
|
17
|
+
if (env.TRANSPORT_TYPE === "httpStream") {
|
|
18
|
+
const transport = new StreamableHTTPServerTransport({
|
|
19
|
+
sessionIdGenerator: undefined,
|
|
20
|
+
});
|
|
21
|
+
await server.connect(transport);
|
|
22
|
+
const httpServer = createServer((req, res) => {
|
|
23
|
+
transport.handleRequest(req, res);
|
|
24
|
+
});
|
|
25
|
+
httpServer.listen(env.PORT, () => {
|
|
26
|
+
console.log(`IBGE MCP Server listening on http://localhost:${env.PORT}`);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
const transport = new StdioServerTransport();
|
|
31
|
+
await server.connect(transport);
|
|
32
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import env from "../env.js";
|
|
4
|
+
export function registerAgregadoDadosTool(server) {
|
|
5
|
+
server.registerTool("agregado-dados", {
|
|
6
|
+
description: "Busca os dados de um agregado no IBGE",
|
|
7
|
+
annotations: {
|
|
8
|
+
readOnlyHint: true,
|
|
9
|
+
destructiveHint: false,
|
|
10
|
+
idempotentHint: true,
|
|
11
|
+
openWorldHint: true,
|
|
12
|
+
},
|
|
13
|
+
inputSchema: {
|
|
14
|
+
agregadoId: z
|
|
15
|
+
.string()
|
|
16
|
+
.describe("Id do agregado, obtido pela ferramenta 'agregados'. Ex.: '6579'."),
|
|
17
|
+
periodo: z
|
|
18
|
+
.string()
|
|
19
|
+
.describe("Período dos dados. Um ano (ex.: '2018'), vários separados por '|' " +
|
|
20
|
+
"(ex.: '2017|2018'), um intervalo (ex.: '2017-2019') ou os últimos N " +
|
|
21
|
+
"períodos com número negativo (ex.: '-6'). Use os períodos válidos " +
|
|
22
|
+
"informados em 'agregado-metadados'."),
|
|
23
|
+
variavelId: z
|
|
24
|
+
.string()
|
|
25
|
+
.describe("Id da variável, obtido em 'agregado-metadados'. Ex.: '9324'." +
|
|
26
|
+
"Várias variáveis podem ser separadas por '|'."),
|
|
27
|
+
localidades: z
|
|
28
|
+
.string()
|
|
29
|
+
.describe("Localidades no formato 'N<nivel>[<id>]', combinando o código do " +
|
|
30
|
+
"nível territorial (de 'agregado-metadados', ex.: N6 = município, " +
|
|
31
|
+
"N3 = estado, N1 = Brasil) com o id da localidade (da ferramenta " +
|
|
32
|
+
"'localidades'). Ex.: 'N6[3550308]' para o município de São Paulo, " +
|
|
33
|
+
"'N3[35]' para o estado de São Paulo. Vários ids: 'N6[3550308,3304557]'; " +
|
|
34
|
+
"todos do nível: 'N3[all]'."),
|
|
35
|
+
classificacao: z
|
|
36
|
+
.string()
|
|
37
|
+
.optional()
|
|
38
|
+
.describe("Filtro opcional de classificações no formato " +
|
|
39
|
+
"'<classificacaoId>[<categoriaId>,...]', conforme as classificações " +
|
|
40
|
+
"listadas em 'agregado-metadados'. Ex.: '2[4,5]'."),
|
|
41
|
+
},
|
|
42
|
+
}, async ({ agregadoId, periodo, variavelId, localidades, classificacao }) => {
|
|
43
|
+
let url = `${env.IBGE_API_AGREGADOS}/${agregadoId}/periodos/${periodo}/variaveis/${variavelId}?localidades=${encodeURIComponent(localidades)}`;
|
|
44
|
+
if (classificacao) {
|
|
45
|
+
url += `&classificacao=${encodeURIComponent(classificacao)}`;
|
|
46
|
+
}
|
|
47
|
+
const response = await axios.get(url);
|
|
48
|
+
const dados = {
|
|
49
|
+
fonteUrl: url,
|
|
50
|
+
dados: response.data,
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
content: [{ type: "text", text: JSON.stringify(dados, null, 2) }],
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import env from "../env.js";
|
|
4
|
+
export function registerAgregadoMetadadosTool(server) {
|
|
5
|
+
server.registerTool("agregado-metadados", {
|
|
6
|
+
description: "Busca metadados (variáveis, nivelTerritorial, periodicidade, etc.) de um agregado por agregadoId",
|
|
7
|
+
annotations: {
|
|
8
|
+
readOnlyHint: true,
|
|
9
|
+
destructiveHint: false,
|
|
10
|
+
idempotentHint: true,
|
|
11
|
+
openWorldHint: true,
|
|
12
|
+
},
|
|
13
|
+
inputSchema: {
|
|
14
|
+
agregadoId: z
|
|
15
|
+
.string()
|
|
16
|
+
.describe("Id do agregado, obtido pela ferramenta 'agregados'. Ex.: '6579'."),
|
|
17
|
+
},
|
|
18
|
+
}, async ({ agregadoId }) => {
|
|
19
|
+
const response = await axios.get(`${env.IBGE_API_AGREGADOS}/${agregadoId}/metadados`);
|
|
20
|
+
const data = response.data;
|
|
21
|
+
const agregadoMetadados = {
|
|
22
|
+
agregadoId: data.id,
|
|
23
|
+
nome: data.nome,
|
|
24
|
+
pesquisa: data.pesquisa,
|
|
25
|
+
assunto: data.assunto,
|
|
26
|
+
periodicidade: data.periodicidade,
|
|
27
|
+
nivelTerritorial: data.nivelTerritorial,
|
|
28
|
+
variaveis: data.variaveis.map((variavel) => ({
|
|
29
|
+
variavelId: variavel.id,
|
|
30
|
+
nome: variavel.nome,
|
|
31
|
+
unidade: variavel.unidade,
|
|
32
|
+
sumarizacao: variavel.sumarizacao,
|
|
33
|
+
})),
|
|
34
|
+
classificacoes: data.classificacoes,
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
content: [
|
|
38
|
+
{ type: "text", text: JSON.stringify(agregadoMetadados, null, 2) },
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import env from "../env.js";
|
|
4
|
+
export function registerAgregadosTool(server) {
|
|
5
|
+
server.registerTool("agregados", {
|
|
6
|
+
description: "Busca agregados de uma pesquisa por pesquisaId do IBGE",
|
|
7
|
+
annotations: {
|
|
8
|
+
readOnlyHint: true,
|
|
9
|
+
destructiveHint: false,
|
|
10
|
+
idempotentHint: true,
|
|
11
|
+
openWorldHint: true,
|
|
12
|
+
},
|
|
13
|
+
inputSchema: {
|
|
14
|
+
pesquisaId: z
|
|
15
|
+
.string()
|
|
16
|
+
.describe("Id da pesquisa, obtido pela ferramenta 'pesquisas'. Ex.: 'CD'."),
|
|
17
|
+
},
|
|
18
|
+
}, async ({ pesquisaId }) => {
|
|
19
|
+
const response = await axios.get(env.IBGE_API_AGREGADOS);
|
|
20
|
+
const pesquisa = response.data.filter((pesquisa) => pesquisa.id === pesquisaId)[0];
|
|
21
|
+
if (!pesquisa) {
|
|
22
|
+
return {
|
|
23
|
+
content: [
|
|
24
|
+
{
|
|
25
|
+
type: "text",
|
|
26
|
+
text: `Pesquisa com id ${pesquisaId} não encontrada.`,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const agregados = pesquisa.agregados.map((agregado) => ({
|
|
32
|
+
agregadoId: agregado.id,
|
|
33
|
+
nome: agregado.nome,
|
|
34
|
+
}));
|
|
35
|
+
return {
|
|
36
|
+
content: [{ type: "text", text: JSON.stringify(agregados, null, 2) }],
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Os dados do IBGE são publicados em referência ao horário de Brasília. */
|
|
2
|
+
const FUSO_HORARIO = "America/Sao_Paulo";
|
|
3
|
+
const formatter = new Intl.DateTimeFormat("en-CA", {
|
|
4
|
+
timeZone: FUSO_HORARIO,
|
|
5
|
+
year: "numeric",
|
|
6
|
+
month: "2-digit",
|
|
7
|
+
day: "2-digit",
|
|
8
|
+
hour: "2-digit",
|
|
9
|
+
minute: "2-digit",
|
|
10
|
+
second: "2-digit",
|
|
11
|
+
// h23 e não hour12:false, que em alguns locales devolve '24' à meia-noite.
|
|
12
|
+
hourCycle: "h23",
|
|
13
|
+
timeZoneName: "longOffset",
|
|
14
|
+
});
|
|
15
|
+
/**
|
|
16
|
+
* Quebra o instante nos campos do fuso de Brasília. `Date` só sabe formatar em
|
|
17
|
+
* UTC ou no fuso da máquina, então o `Intl` é quem faz a conversão — assim o
|
|
18
|
+
* resultado não muda conforme o servidor onde isto roda.
|
|
19
|
+
*/
|
|
20
|
+
function descreverInstante(agora) {
|
|
21
|
+
const partes = new Map(formatter
|
|
22
|
+
.formatToParts(agora)
|
|
23
|
+
.map((parte) => [parte.type, parte.value]));
|
|
24
|
+
const campo = (nome) => partes.get(nome) ?? "";
|
|
25
|
+
// 'GMT-03:00' -> '-03:00'; no horário de Greenwich o Intl devolve só 'GMT'.
|
|
26
|
+
const offset = campo("timeZoneName").replace("GMT", "") || "+00:00";
|
|
27
|
+
const data = `${campo("year")}-${campo("month")}-${campo("day")}`;
|
|
28
|
+
const hora = `${campo("hour")}:${campo("minute")}:${campo("second")}`;
|
|
29
|
+
return {
|
|
30
|
+
dataHora: `${data}T${hora}${offset}`,
|
|
31
|
+
data,
|
|
32
|
+
hora,
|
|
33
|
+
ano: Number(campo("year")),
|
|
34
|
+
mes: Number(campo("month")),
|
|
35
|
+
dia: Number(campo("day")),
|
|
36
|
+
fusoHorario: FUSO_HORARIO,
|
|
37
|
+
dataHoraUtc: agora.toISOString(),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function registerDataHoraTool(server) {
|
|
41
|
+
server.registerTool("data-hora", {
|
|
42
|
+
description: "Retorna a data e a hora atuais no fuso de Brasília (America/Sao_Paulo). " +
|
|
43
|
+
"Use antes de responder qualquer pergunta relativa ao momento presente — " +
|
|
44
|
+
"'hoje', 'ano passado', 'atualmente', 'último ano disponível' — para saber " +
|
|
45
|
+
"a que ano o usuário se refere, em vez de supor. " +
|
|
46
|
+
"Atenção: o ano atual não é necessariamente o último período publicado de " +
|
|
47
|
+
"uma pesquisa; para isso consulte os períodos do agregado.",
|
|
48
|
+
annotations: {
|
|
49
|
+
readOnlyHint: true,
|
|
50
|
+
destructiveHint: false,
|
|
51
|
+
// O valor muda a cada chamada.
|
|
52
|
+
idempotentHint: false,
|
|
53
|
+
openWorldHint: false,
|
|
54
|
+
},
|
|
55
|
+
}, async () => {
|
|
56
|
+
const resultado = descreverInstante(new Date());
|
|
57
|
+
return {
|
|
58
|
+
content: [{ type: "text", text: JSON.stringify(resultado, null, 2) }],
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { registerPesquisasTool } from "./pesquisas.js";
|
|
2
|
+
export { registerAgregadosTool } from "./agregados.js";
|
|
3
|
+
export { registerAgregadoMetadadosTool } from "./agregado-metadados.js";
|
|
4
|
+
export { registerAgregadoDadosTool } from "./agregado-dados.js";
|
|
5
|
+
export { registerLocalidadesTool } from "./localidades.js";
|
|
6
|
+
export { registerDataHoraTool } from "./data-hora.js";
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import env from "../env.js";
|
|
4
|
+
const normalize = (value) => value.normalize("NFD").replace(/[̀-ͯ]/g, "").trim().toLowerCase();
|
|
5
|
+
const matchEstado = (estado, busca) => {
|
|
6
|
+
const buscaNormalizada = normalize(busca);
|
|
7
|
+
return (normalize(estado.sigla) === buscaNormalizada ||
|
|
8
|
+
normalize(estado.nome) === buscaNormalizada);
|
|
9
|
+
};
|
|
10
|
+
export function registerLocalidadesTool(server) {
|
|
11
|
+
server.registerTool("localidades", {
|
|
12
|
+
description: "Descobre o id de localidade do IBGE de um estado ou cidade. " +
|
|
13
|
+
"Use 'estado' para buscar um estado por nome ou sigla (ex.: 'São Paulo' ou 'SP'). " +
|
|
14
|
+
"Para buscar uma cidade, use 'cidade' como objeto { estado, nome } para evitar ambiguidade, " +
|
|
15
|
+
"pois existem cidades com o mesmo nome em estados diferentes. " +
|
|
16
|
+
"É possível buscar estado e cidade na mesma chamada.",
|
|
17
|
+
annotations: {
|
|
18
|
+
readOnlyHint: true,
|
|
19
|
+
destructiveHint: false,
|
|
20
|
+
idempotentHint: true,
|
|
21
|
+
openWorldHint: true,
|
|
22
|
+
},
|
|
23
|
+
inputSchema: {
|
|
24
|
+
estado: z
|
|
25
|
+
.string()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe("Nome ou sigla do estado, ex.: 'São Paulo' ou 'SP'"),
|
|
28
|
+
cidade: z
|
|
29
|
+
.object({
|
|
30
|
+
estado: z
|
|
31
|
+
.string()
|
|
32
|
+
.describe("Nome ou sigla do estado da cidade, ex.: 'SP'"),
|
|
33
|
+
nome: z.string().describe("Nome da cidade, ex.: 'Campinas'"),
|
|
34
|
+
})
|
|
35
|
+
.optional()
|
|
36
|
+
.describe("Cidade a ser buscada, com o estado para desambiguar o nome"),
|
|
37
|
+
},
|
|
38
|
+
}, async ({ estado, cidade }) => {
|
|
39
|
+
if (!estado && !cidade) {
|
|
40
|
+
return {
|
|
41
|
+
content: [
|
|
42
|
+
{
|
|
43
|
+
type: "text",
|
|
44
|
+
text: "Informe 'estado' ou 'cidade' para buscar uma localidade.",
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const resultado = {};
|
|
50
|
+
if (estado) {
|
|
51
|
+
const response = await axios.get(`${env.IBGE_API_LOCALIDADES}/estados`);
|
|
52
|
+
const encontrado = response.data.find((uf) => matchEstado(uf, estado));
|
|
53
|
+
resultado.estado = encontrado
|
|
54
|
+
? {
|
|
55
|
+
localidadeId: encontrado.id,
|
|
56
|
+
sigla: encontrado.sigla,
|
|
57
|
+
nome: encontrado.nome,
|
|
58
|
+
regiao: encontrado.regiao.nome,
|
|
59
|
+
}
|
|
60
|
+
: `Estado '${estado}' não encontrado.`;
|
|
61
|
+
}
|
|
62
|
+
if (cidade) {
|
|
63
|
+
const response = await axios.get(`${env.IBGE_API_LOCALIDADES}/municipios`);
|
|
64
|
+
const nomeNormalizado = normalize(cidade.nome);
|
|
65
|
+
const encontrados = response.data.filter((municipio) => normalize(municipio.nome) === nomeNormalizado &&
|
|
66
|
+
matchEstado(municipio.microrregiao.mesorregiao.UF, cidade.estado));
|
|
67
|
+
resultado.cidade =
|
|
68
|
+
encontrados.length > 0
|
|
69
|
+
? encontrados.map((municipio) => {
|
|
70
|
+
const uf = municipio.microrregiao.mesorregiao.UF;
|
|
71
|
+
return {
|
|
72
|
+
localidadeId: municipio.id,
|
|
73
|
+
nome: municipio.nome,
|
|
74
|
+
estado: {
|
|
75
|
+
localidadeId: uf.id,
|
|
76
|
+
sigla: uf.sigla,
|
|
77
|
+
nome: uf.nome,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
})
|
|
81
|
+
: `Cidade '${cidade.nome}' não encontrada no estado '${cidade.estado}'.`;
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
content: [{ type: "text", text: JSON.stringify(resultado, null, 2) }],
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import env from "../env.js";
|
|
3
|
+
export function registerPesquisasTool(server) {
|
|
4
|
+
server.registerTool("pesquisas", {
|
|
5
|
+
description: "Busca pesquisas do IBGE",
|
|
6
|
+
annotations: {
|
|
7
|
+
readOnlyHint: true,
|
|
8
|
+
destructiveHint: false,
|
|
9
|
+
idempotentHint: true,
|
|
10
|
+
openWorldHint: true,
|
|
11
|
+
},
|
|
12
|
+
}, async () => {
|
|
13
|
+
const response = await axios.get(env.IBGE_API_AGREGADOS);
|
|
14
|
+
const pesquisas = response.data.map((pesquisa) => ({
|
|
15
|
+
pesquisaId: pesquisa.id,
|
|
16
|
+
nome: pesquisa.nome,
|
|
17
|
+
}));
|
|
18
|
+
return {
|
|
19
|
+
content: [{ type: "text", text: JSON.stringify(pesquisas, null, 2) }],
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/version.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ibge-mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for Brazilian IBGE public APIs",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Vinicius Cestari",
|
|
8
|
+
"homepage": "https://github.com/ViniciusCestarii/ibge-mcp-server#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/ViniciusCestarii/ibge-mcp-server.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/ViniciusCestarii/ibge-mcp-server/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"mcp",
|
|
18
|
+
"mcp-server",
|
|
19
|
+
"modelcontextprotocol",
|
|
20
|
+
"ibge",
|
|
21
|
+
"brasil",
|
|
22
|
+
"brazil"
|
|
23
|
+
],
|
|
24
|
+
"main": "./dist/index.js",
|
|
25
|
+
"bin": {
|
|
26
|
+
"ibge-mcp-server": "./dist/index.js"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=20"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc && tsc-alias",
|
|
41
|
+
"prepublishOnly": "npm run build",
|
|
42
|
+
"start": "node dist/index.js",
|
|
43
|
+
"dev": "tsx src/index.ts",
|
|
44
|
+
"inspect": "npx @modelcontextprotocol/inspector@0.22.0 tsx src/index.ts",
|
|
45
|
+
"lint": "eslint src/",
|
|
46
|
+
"lint:fix": "eslint src/ --fix",
|
|
47
|
+
"format": "prettier --write src/",
|
|
48
|
+
"test": "node --import tsx --test test/**/*.test.ts",
|
|
49
|
+
"test:gemini": "TEST_RUNNER=gemini npm test",
|
|
50
|
+
"test:claude-code": "TEST_RUNNER=claude-code npm test"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.258",
|
|
54
|
+
"@eslint/js": "10.0.1",
|
|
55
|
+
"@types/node": "24.3.1",
|
|
56
|
+
"eslint": "10.1.0",
|
|
57
|
+
"eslint-config-prettier": "10.1.8",
|
|
58
|
+
"eslint-plugin-prettier": "5.5.5",
|
|
59
|
+
"prettier": "3.8.1",
|
|
60
|
+
"tsc-alias": "1.9.4",
|
|
61
|
+
"tsx": "4.20.5",
|
|
62
|
+
"typescript": "5.9.2",
|
|
63
|
+
"typescript-eslint": "8.57.2"
|
|
64
|
+
},
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"@modelcontextprotocol/sdk": "1.30.0",
|
|
67
|
+
"axios": "1.11.0",
|
|
68
|
+
"dotenv": "17.3.1",
|
|
69
|
+
"zod": "4.3.6"
|
|
70
|
+
}
|
|
71
|
+
}
|