bcb-br-mcp 1.3.4 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Índice de séries do Portal de Dados Abertos do BCB (CKAN) — busca real.
3
+ *
4
+ * Desenho fechado pelo decisor na fase bcb (arbitragem 5), e as restrições
5
+ * importam mais que o código:
6
+ *
7
+ * - Fonte é `package_list` do CKAN: UMA requisição (~310 KB) devolve o nome de
8
+ * todos os datasets do portal, e 3.555 deles são nomeados pelo próprio código
9
+ * da série (`{codigo}-{slug}`) — ~24× a cobertura do catálogo curado local.
10
+ * Nada de raspar o localizador JSP do SGS.
11
+ * - O catálogo é servido de cache com validade de 24 h e a renovação é
12
+ * BLOQUEANTE: quem faz a primeira busca depois do vencimento paga o ~0,8 s.
13
+ * Sem tarefa agendada e sem seed manual — se ninguém buscar, a origem não
14
+ * recebe requisição nenhuma. Descartado de propósito o
15
+ * stale-while-revalidate: com tráfego baixo ele entregaria retrato de semanas
16
+ * ao primeiro usuário para poupar 0,8 s de um único usuário por dia.
17
+ * - O cache guarda **só metadados** (código e slug), NUNCA observações. É por
18
+ * isso que ele não é a "base derivada" que a arbitragem 2 evitou: o servidor
19
+ * segue sendo *Produced Work* sob a ODbL, e valores de série continuam
20
+ * buscados ao vivo a cada pergunta.
21
+ * - `Disallow: /api/` + `Crawl-Delay: 10` no robots do portal do catálogo é o
22
+ * motivo de existir cache: ≤1 requisição por dia por instância, não uma por
23
+ * chamada.
24
+ *
25
+ * O cache é módulo-level, ou seja, por processo (stdio) e por isolate
26
+ * (Cloudflare). Isolate reciclado = catálogo buscado de novo; com o tráfego
27
+ * atual isso mantém a ordem de grandeza prometida (unidades de requisição por
28
+ * dia), e é a única forma de cache que não introduz armazenamento persistente.
29
+ */
30
+ // Só primitivos: a curadoria de séries entra por parâmetro (e não por import de
31
+ // `tools.ts`) para a busca ser testável sem estado global e para não haver ciclo
32
+ // entre os módulos de tool.
33
+ import { fetchBcbApi, normalizeString } from "./shared.js";
34
+ export const CKAN_PACKAGE_LIST = "https://dadosabertos.bcb.gov.br/api/3/action/package_list";
35
+ export const CKAN_DATASET_BASE = "https://dadosabertos.bcb.gov.br/dataset";
36
+ /** Validade do catálogo em cache: 24 h (decisão do decisor). */
37
+ export const CATALOGO_TTL_MS = 24 * 60 * 60 * 1000;
38
+ let cache = null;
39
+ /** Renovação em voo: duas buscas simultâneas após o vencimento fazem 1 fetch. */
40
+ let renovacaoEmVoo = null;
41
+ /** Só para os testes: zera o cache do módulo. */
42
+ export function _resetCatalogo() {
43
+ cache = null;
44
+ renovacaoEmVoo = null;
45
+ }
46
+ /** Só para os testes: injeta um snapshot como se tivesse vindo da origem. */
47
+ export function _seedCatalogo(snapshot) {
48
+ cache = snapshot;
49
+ }
50
+ /**
51
+ * Conserta mojibake de ISO-8859-1 servido como UTF-8 ("Câmbio" -> "Câmbio").
52
+ * Os slugs do `package_list` são ASCII, então isto é defesa para os campos de
53
+ * texto do CKAN que já apareceram corrompidos na verificação de abertura.
54
+ */
55
+ export function normalizarMojibake(texto) {
56
+ if (!/[ÃÂ][€-¿]/.test(texto))
57
+ return texto;
58
+ try {
59
+ const bytes = Uint8Array.from(Array.from(texto, c => c.charCodeAt(0) & 0xff));
60
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
61
+ }
62
+ catch {
63
+ return texto;
64
+ }
65
+ }
66
+ /**
67
+ * Nome legível a partir do slug do dataset. O `package_list` devolve slug, não
68
+ * título: `---` é separador de campos e `-` separa palavras, e os acentos foram
69
+ * perdidos na origem — daí o nome reconstruído ser aproximado de propósito. Para
70
+ * as séries do catálogo curado o nome bom (com acento) prevalece; para as
71
+ * demais, quem quiser o nome oficial usa `bcb_serie_metadados`.
72
+ */
73
+ export function nomeDoSlug(slug) {
74
+ const texto = normalizarMojibake(slug)
75
+ .replace(/^\d+-/, "") // o código já é campo próprio
76
+ .split(/-{2,}/) // `---` separa campos; um `-` só separa palavras
77
+ .map(parte => parte.replace(/-/g, " ").replace(/\s+/g, " ").trim())
78
+ .filter(Boolean)
79
+ .join(" - ");
80
+ return texto.charAt(0).toUpperCase() + texto.slice(1);
81
+ }
82
+ /** `1-taxa-de-cambio---livre---dolar-americano-venda---diario` -> {1, slug}. */
83
+ export function parsePackageList(nomes) {
84
+ if (!Array.isArray(nomes))
85
+ return { entradas: [], totalDatasets: 0 };
86
+ const entradas = [];
87
+ const vistos = new Set();
88
+ for (const bruto of nomes) {
89
+ if (typeof bruto !== "string")
90
+ continue;
91
+ const match = /^(\d+)-(.+)$/.exec(bruto);
92
+ if (!match)
93
+ continue;
94
+ const codigo = Number(match[1]);
95
+ if (!Number.isSafeInteger(codigo) || vistos.has(codigo))
96
+ continue;
97
+ vistos.add(codigo);
98
+ entradas.push({ codigo, slug: bruto });
99
+ }
100
+ entradas.sort((a, b) => a.codigo - b.codigo);
101
+ return { entradas, totalDatasets: nomes.length };
102
+ }
103
+ async function renovar(timeoutMs, maxRetries) {
104
+ const resposta = (await fetchBcbApi(CKAN_PACKAGE_LIST, timeoutMs, maxRetries));
105
+ if (!resposta || resposta.success === false || !Array.isArray(resposta.result)) {
106
+ throw new Error("Resposta inesperada do portal de dados abertos (package_list)");
107
+ }
108
+ const { entradas, totalDatasets } = parsePackageList(resposta.result);
109
+ if (entradas.length === 0)
110
+ throw new Error("Índice do portal veio sem nenhuma série identificável por código");
111
+ const agora = Date.now();
112
+ return {
113
+ entradas,
114
+ obtidoEm: new Date(agora).toISOString(),
115
+ totalDatasets,
116
+ expiraEm: agora + CATALOGO_TTL_MS
117
+ };
118
+ }
119
+ /**
120
+ * Devolve o índice do portal, renovando de forma bloqueante quando vencido.
121
+ *
122
+ * Degradação: se a renovação falhar e houver retrato anterior, ele é servido
123
+ * COM aviso e com a data de obtenção visível — a API do BCB cai de verdade
124
+ * (ocorreu durante esta fase), e nesse caso é melhor um índice velho declarado
125
+ * do que busca nenhuma. Sem retrato anterior, devolve `snapshot: null` e quem
126
+ * chamou cai no catálogo curado.
127
+ */
128
+ export async function obterCatalogo(timeoutMs, maxRetries) {
129
+ if (cache && Date.now() < cache.expiraEm)
130
+ return { snapshot: cache };
131
+ if (!renovacaoEmVoo) {
132
+ renovacaoEmVoo = renovar(timeoutMs, maxRetries)
133
+ .then(snapshot => {
134
+ cache = snapshot;
135
+ return snapshot;
136
+ })
137
+ .finally(() => {
138
+ renovacaoEmVoo = null;
139
+ });
140
+ }
141
+ try {
142
+ return { snapshot: await renovacaoEmVoo };
143
+ }
144
+ catch (error) {
145
+ const motivo = error instanceof Error ? error.message : String(error);
146
+ if (cache) {
147
+ return {
148
+ snapshot: cache,
149
+ aviso: `Índice do portal servido de cache VENCIDO (obtido em ${cache.obtidoEm}) — ` +
150
+ `a renovação falhou: ${motivo}`
151
+ };
152
+ }
153
+ return {
154
+ snapshot: null,
155
+ aviso: `Índice do portal indisponível (${motivo}); a busca usou apenas o catálogo curado local.`
156
+ };
157
+ }
158
+ }
159
+ function comoEncontrada(serie) {
160
+ return {
161
+ codigo: serie.codigo,
162
+ nome: serie.nome,
163
+ categoria: serie.categoria,
164
+ periodicidade: serie.periodicidade,
165
+ origem: "curado"
166
+ };
167
+ }
168
+ function comoEncontradaDoIndice(entrada) {
169
+ return {
170
+ codigo: entrada.codigo,
171
+ nome: nomeDoSlug(entrada.slug),
172
+ origem: "indice",
173
+ dataset: `${CKAN_DATASET_BASE}/${entrada.slug}`
174
+ };
175
+ }
176
+ /**
177
+ * Ranqueia os achados. O catálogo curado é a CAMADA DE DESTAQUE: quando uma
178
+ * série está nele, ela vem primeiro e com o nome bom, porque foi revisada à
179
+ * mão; o índice do portal entra depois, ordenado do slug mais curto (mais
180
+ * específico) para o mais longo, com desempate estável pelo código.
181
+ */
182
+ export function buscarSeries(termo, curadoria, entradas, limite) {
183
+ const termoNorm = normalizeString(termo).trim();
184
+ const tokens = termoNorm.split(/\s+/).filter(Boolean);
185
+ // Busca por código: "433" deve achar a série 433, não as que contêm "433".
186
+ if (/^\d+$/.test(termoNorm)) {
187
+ const codigo = Number(termoNorm);
188
+ const curada = curadoria.find(s => s.codigo === codigo);
189
+ if (curada)
190
+ return { total: 1, series: [comoEncontrada(curada)] };
191
+ const doIndice = entradas?.find(e => e.codigo === codigo);
192
+ if (doIndice)
193
+ return { total: 1, series: [comoEncontradaDoIndice(doIndice)] };
194
+ return { total: 0, series: [] };
195
+ }
196
+ const casaTodosTokens = (texto) => tokens.every(t => texto.includes(t));
197
+ const curadas = curadoria.filter(s => casaTodosTokens(normalizeString(s.nome)) || casaTodosTokens(normalizeString(s.categoria)));
198
+ const codigosCurados = new Set(curadas.map(s => s.codigo));
199
+ const doIndice = (entradas ?? [])
200
+ .filter(e => !codigosCurados.has(e.codigo) && casaTodosTokens(normalizeString(e.slug)))
201
+ .sort((a, b) => a.slug.length - b.slug.length || a.codigo - b.codigo);
202
+ const total = curadas.length + doIndice.length;
203
+ const series = [...curadas.map(comoEncontrada), ...doIndice.map(comoEncontradaDoIndice)].slice(0, limite);
204
+ return { total, series };
205
+ }
206
+ //# sourceMappingURL=catalog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.js","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Expectativas de Mercado (Focus) sob contrato consolidado.
3
+ *
4
+ * Desenho aprovado pelo decisor (arbitragem 3 + fronteira concreta da sessão de
5
+ * D3): NÃO espelhar os recursos OData. Três tools em vez de treze:
6
+ *
7
+ * - `bcb_focus_expectativas` — as cinco expectativas de calendário (mensal,
8
+ * trimestral, anual, inflação em 12 e em 24 meses) numa tool só, com
9
+ * `horizonte` como parâmetro. Os dois horizontes rolantes não têm data de
10
+ * referência (o alvo é "os próximos N meses", não um mês do calendário) e têm
11
+ * o campo `suavizada`, que os de calendário não têm: `referencia` é obrigatória
12
+ * nos três de calendário e recusada nos rolantes, com erro que diz o que usar.
13
+ * - `bcb_focus_selic` — separada porque o eixo temporal é a REUNIÃO do Copom, não
14
+ * o calendário. Consolidar aqui esconderia a diferença que importa.
15
+ * - `bcb_focus_referencias` — auxiliar de descoberta. Sem ela o agente adivinha
16
+ * strings de referência ("2026"? "12/2026"? "4/2026"?), que é o modo mais
17
+ * comum de a consulta voltar vazia.
18
+ *
19
+ * Top 5 é SINALIZADOR (`top5: true`), não tool espelhada — e não existe para
20
+ * toda combinação: a tabela de combinações válidas mora em `RECURSOS` e o erro
21
+ * diz qual usar.
22
+ *
23
+ * `ExpectativasMercadoInstituicoes` jamais entra (desativado pelo BCB por
24
+ * confidencialidade).
25
+ */
26
+ import { type ToolDefinition, type ToolResult } from "./shared.js";
27
+ export type Horizonte = "mensal" | "trimestral" | "anual" | "inflacao_12m" | "inflacao_24m";
28
+ interface RecursoFocus {
29
+ /** Recurso OData das expectativas do consenso. */
30
+ consenso: string;
31
+ /** Recurso das expectativas do Top 5, quando existe para este horizonte. */
32
+ top5: string | null;
33
+ /** Nome do campo que carrega o alvo da expectativa. */
34
+ campoReferencia: string | null;
35
+ /** Formato da referência, para descrição e mensagem de erro. */
36
+ formatoReferencia: string | null;
37
+ }
38
+ /**
39
+ * Mapa horizonte -> recurso OData. É o ÚNICO lugar que conhece os nomes dos
40
+ * recursos: se a fonte renomear ou deixar de oferecer Top 5 em algum horizonte, a
41
+ * correção é uma linha aqui.
42
+ *
43
+ * Os nomes foram lidos do documento de serviço do OData contra a origem, não
44
+ * supostos. Repare na irregularidade da fonte, que é real e não erro de digitação:
45
+ * o mensal é `ExpectativaMercadoMensais` (singular) e o Top 5 trimestral é
46
+ * `ExpectativaMercadoTop5Trimestral` (singular nas duas pontas), enquanto todo o
47
+ * resto é plural.
48
+ */
49
+ export declare const RECURSOS: Record<Horizonte, RecursoFocus>;
50
+ /** Recurso de Selic, fora do mapa de horizontes porque o eixo é a reunião do Copom. */
51
+ export declare const RECURSO_SELIC: {
52
+ readonly consenso: "ExpectativasMercadoSelic";
53
+ readonly top5: "ExpectativasMercadoTop5Selic";
54
+ };
55
+ /** Linha normalizada: um contrato para os cinco horizontes e para o Top 5. */
56
+ export interface ExpectativaNormalizada {
57
+ indicador: string | null;
58
+ indicadorDetalhe: string | null;
59
+ /** Data da coleta (campo `Data` da fonte) — o Focus é vintage por construção. */
60
+ coletadoEm: string | null;
61
+ /** Alvo da expectativa: data de referência, reunião do Copom, ou null nos rolantes. */
62
+ referencia: string | null;
63
+ media: number | null;
64
+ mediana: number | null;
65
+ desvioPadrao: number | null;
66
+ minimo: number | null;
67
+ maximo: number | null;
68
+ respondentes: number | null;
69
+ baseCalculo: number | null;
70
+ /** Só nos horizontes rolantes: se a série é a suavizada. */
71
+ suavizada?: boolean | null;
72
+ /** Só no Top 5: tipo de cálculo publicado pela fonte. */
73
+ tipoCalculo?: string | null;
74
+ /** Só no Top 5 da Selic: único recurso da fonte que publica este campo. */
75
+ coeficienteVariacao?: number | null;
76
+ }
77
+ /**
78
+ * Normaliza uma linha do OData. Defensivo de propósito: os recursos não têm o
79
+ * mesmo conjunto de campos, o alvo aparece como `DataReferencia` (calendário) ou
80
+ * `Reuniao` (Selic), e a caixa dos nomes varia entre recursos.
81
+ */
82
+ export declare function normalizarExpectativa(linha: Record<string, unknown>): ExpectativaNormalizada;
83
+ export interface ArgsExpectativas {
84
+ indicador: string;
85
+ horizonte: Horizonte;
86
+ referencia?: string;
87
+ dataInicial?: string;
88
+ dataFinal?: string;
89
+ top5?: boolean;
90
+ suavizada?: boolean;
91
+ limite?: number;
92
+ }
93
+ export declare function handleFocusExpectativas(args: ArgsExpectativas, timeoutMs?: number, maxRetries?: number): Promise<ToolResult>;
94
+ export interface ArgsSelic {
95
+ reuniao?: string;
96
+ dataInicial?: string;
97
+ dataFinal?: string;
98
+ top5?: boolean;
99
+ limite?: number;
100
+ }
101
+ export declare function handleFocusSelic(args: ArgsSelic, timeoutMs?: number, maxRetries?: number): Promise<ToolResult>;
102
+ /** Escopos de descoberta: os cinco horizontes mais a Selic, que é tool própria. */
103
+ export type EscopoReferencias = Horizonte | "selic";
104
+ export interface ArgsReferencias {
105
+ indicador?: string;
106
+ escopo?: EscopoReferencias;
107
+ }
108
+ /**
109
+ * Descobre os textos EXATOS de indicador e de referência, por escopo.
110
+ *
111
+ * O parâmetro chama-se `escopo`, e não `horizonte`, de propósito: os cinco
112
+ * horizontes de `bcb_focus_expectativas` mais a Selic — cujo eixo é a reunião do
113
+ * Copom, não o calendário — não formam um conjunto de horizontes. Esta tool cobre
114
+ * tudo o que o Focus publica, e cada bloco diz em `tool` quem o consome.
115
+ *
116
+ * O desenho consulta os próprios recursos de expectativa — e não o recurso
117
+ * `DatasReferencia`, que o nome sugere e que foi descartado com o spike contra a
118
+ * origem: ele publica `Indicador`, `periodo`, `DataReferencia1` e
119
+ * `DataReferencia2` (não existe `DataReferencia`), cobre 11 indicadores contra os
120
+ * 26 do recurso anual, não separa por escopo e, para o IPCA, para em 12/2026
121
+ * enquanto as expectativas mensais já carregam referência 07/2028. É um calendário
122
+ * de datas de referência, não um índice das referências consultáveis.
123
+ *
124
+ * A quebra POR ESCOPO é o ponto: o conjunto de indicadores varia muito entre eles
125
+ * (9 no mensal contra 26 no anual), e pedir um indicador no horizonte errado —
126
+ * "PIB Total" no mensal, por exemplo — é justamente o modo mais comum de a
127
+ * consulta voltar vazia sem explicação.
128
+ */
129
+ export declare function handleFocusReferencias(args: ArgsReferencias, timeoutMs?: number, maxRetries?: number): Promise<ToolResult>;
130
+ export declare const FOCUS_TOOL_DEFINITIONS: ToolDefinition[];
131
+ /** Retorna null quando a tool não é deste módulo (o dispatcher central segue). */
132
+ export declare function dispatchFocusTool(toolName: string, args: Record<string, unknown>, timeoutMs?: number, maxRetries?: number): Promise<ToolResult> | null;
133
+ export {};
134
+ //# sourceMappingURL=focus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"focus.d.ts","sourceRoot":"","sources":["../src/focus.ts"],"names":[],"mappings":"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"}