bcb-br-mcp 1.16.2 → 1.18.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/dist/tools.js CHANGED
@@ -14,7 +14,7 @@ import { criarDeepResearchTools } from "./deep-research.js";
14
14
  import { FOCUS_TOOL_DEFINITIONS, dispatchFocusTool } from "./focus.js";
15
15
  import { CAMBIO_TOOL_DEFINITIONS, dispatchCambioTool } from "./cambio.js";
16
16
  import { DERIVACAO_CORRELACAO, DERIVACAO_DEFLACAO, DERIVACAO_ENCADEAMENTO, DERIVACAO_ESTATISTICA, arredondarDerivado, variacaoAcumulada, correlacaoEntreSeries, emVariacoes, estatisticasDaSerie } from "./stats.js";
17
- import { ROTULO_PERIODICIDADE, TETO_ULTIMOS, alinharSeries, buscarSerieSgs, buscarUltimosSgs, chaveMes, construirDeflator, deflacionar, harmonizar, hojeSgs, urlSerie } from "./series.js";
17
+ import { BCB_SGS_BASE, ROTULO_PERIODICIDADE, TETO_ULTIMOS, alinharSeries, buscarSerieSgs, buscarUltimosSgs, chaveMes, construirDeflator, deflacionar, formatarDataSgs, harmonizar, hojeSgs, parseDataSgs, urlSerie } from "./series.js";
18
18
  import { calculateVariation, erroDeExcecao, erroResult, fetchBcbApi, formatDateForApi, mensagemDeErro, normalizeString, sealDeep, upstreamBcb } from "./shared.js";
19
19
  import { withCall } from "@sbissoli/mcp-upstream/als";
20
20
  import { classifyThrown } from "./call-shape.js";
@@ -295,6 +295,99 @@ export function mensagemRecusaAcumulado(codigo, nome) {
295
295
  `é o próprio valor publicado nessa data — use bcb_serie_valores para lê-lo. Para acumular a inflação ` +
296
296
  `num período arbitrário use bcb_variacao sobre a série de variação mensal (IPCA 433).`);
297
297
  }
298
+ export const NATUREZAS_SERIE = [
299
+ "nivel",
300
+ "variacao_no_periodo",
301
+ "acumulado_no_ano",
302
+ "acumulado_12_meses"
303
+ ];
304
+ /**
305
+ * Emissão do campo `natureza` no bloco `serie` — DESLIGADA no tempo 1.
306
+ *
307
+ * Mesmo rollout em dois tempos do contrato de proveniência: nesta versão o
308
+ * `outputSchema` passa a DECLARAR o campo (opcional), e nenhuma resposta muda;
309
+ * um conector que guardou o schema antigo, fechado por `additionalProperties:
310
+ * false`, recusaria a resposta inteira se o campo saísse já. A sessão de ~7 dias
311
+ * depois liga a constante (um patch, sem mexer em schema).
312
+ */
313
+ export const EMITIR_NATUREZA = false;
314
+ /**
315
+ * Unidades do portal que dizem, por si, que o valor é NÍVEL: montante em moeda,
316
+ * número-índice, preço (câmbio), razão ou participação ("Percentual", "Pontos
317
+ * percentuais") e taxa vigente expressa ao ano ("Percentual ao ano" — a meta
318
+ * Selic, o juro médio cobrado). De fora, de propósito: "Percentual ao dia" e
319
+ * "Percentual ao mês", que no SGS tanto podem ser a taxa cobrada (nível) quanto o
320
+ * rendimento do período (variação) — 4390 e a poupança são as segundas, e são
321
+ * reconhecidas por `metodoVariacaoDaSerie`; o resto fica sem rótulo.
322
+ */
323
+ const UNIDADES_DE_NIVEL = new Set([
324
+ "Milhões de reais",
325
+ "Milhões de dólares americanos",
326
+ "Unidades monetárias correntes",
327
+ "Milhares de unidades monetárias correntes",
328
+ "Índice",
329
+ "Taxa unidade monetária corrente/dólar americano",
330
+ "Percentual",
331
+ "Pontos percentuais",
332
+ "Percentual ao ano"
333
+ ]);
334
+ /**
335
+ * Nomes que dizem nível nas séries SEM unidade (fonteNome `medido`): o montante
336
+ * em moeda escrito no nome (PIB em R$ milhões ou US$ milhões) e a taxa de câmbio,
337
+ * que é preço. Nenhum outro nome do catálogo é afirmativo o bastante.
338
+ */
339
+ const NOME_DE_NIVEL = /Valores correntes \(R\$ milhões\)|em US\$ \(milhões\)|^Taxa de câmbio - /;
340
+ /**
341
+ * Natureza de uma série pelo código, ou `null` = "tipo não identificado".
342
+ *
343
+ * Deriva SÓ do que o servidor já sabe, nesta ordem:
344
+ * 1. as listas de acumulados (`ACUMULADOS_NO_ANO`, `ACUMULADOS_EM_12_MESES`),
345
+ * as mesmas que a descrição das tools cita;
346
+ * 2. a decisão de encadeamento (`metodoVariacaoDaSerie` = `encadeamento`: unidade
347
+ * "Variação percentual mensal", nome em "Variação mensal" e
348
+ * `TAXAS_POR_PERIODO`) — o que a conta já trata como variação do período;
349
+ * 3. para o resto do CATÁLOGO, o que ele MEDE: `nivel` só quando a unidade do
350
+ * portal (`UNIDADES_DE_NIVEL`) ou o nome (`NOME_DE_NIVEL`) o afirma, e nunca
351
+ * para nome que diga "acumulada no mês" fora das listas acima (4189: o
352
+ * rendimento do mês anualizado não é nível nem cabe no vocabulário).
353
+ * Sem afirmação, `null`.
354
+ *
355
+ * Fora do catálogo é SEMPRE `null` (decisão do decisor, 08/10/2026): a conta da
356
+ * `bcb_variacao` supõe nível para poder calcular, mas o rótulo publicado não
357
+ * repete o palpite — afirmar "nível" sobre uma série que pode ser variação é o
358
+ * defeito que publicou +23,81% para o IPCA de 2024.
359
+ *
360
+ * As RAZÕES 29037/29038 (endividamento sobre renda acumulada em 12 meses) são
361
+ * `nivel`: só o denominador é acumulado, o valor é a razão naquela data.
362
+ */
363
+ export function naturezaDaSerie(codigo) {
364
+ const info = SERIES_POPULARES.find(s => s.codigo === codigo);
365
+ if (!info)
366
+ return null;
367
+ if (ACUMULADOS_NO_ANO.includes(codigo))
368
+ return "acumulado_no_ano";
369
+ if (ACUMULADOS_EM_12_MESES.includes(codigo))
370
+ return "acumulado_12_meses";
371
+ if (metodoVariacaoDaSerie(codigo) === "encadeamento")
372
+ return "variacao_no_periodo";
373
+ if (/acumulad[ao] no mês/i.test(info.nome))
374
+ return null;
375
+ if (info.unidade !== undefined && UNIDADES_DE_NIVEL.has(info.unidade))
376
+ return "nivel";
377
+ if (NOME_DE_NIVEL.test(info.nome))
378
+ return "nivel";
379
+ return null;
380
+ }
381
+ /** Fragmento do `outputSchema` do campo — declarado opcional, aceito desde o tempo 1. */
382
+ const NATUREZA_SCHEMA = {
383
+ type: ["string", "null"],
384
+ enum: [...NATUREZAS_SERIE, null],
385
+ description: "O que o valor da série é em cada data: 'nivel' (o valor da grandeza no período — saldo, fluxo, preço, " +
386
+ "taxa vigente, razão ou índice), 'variacao_no_periodo' (a variação ou rendimento do próprio período, como " +
387
+ "o IPCA mensal), 'acumulado_no_ano' ou 'acumulado_12_meses' (o acumulado até aquela data: não some nem " +
388
+ "subtraia esses valores). null = tipo não identificado: a série está fora do catálogo curado ou o catálogo " +
389
+ "não diz o que ela mede."
390
+ };
298
391
  // ==================== TOOL HANDLERS ====================
299
392
  /**
300
393
  * Bloco `serie` das respostas do SGS.
@@ -314,7 +407,8 @@ function refSerie(codigo, periodicidade) {
314
407
  nome: info?.nome || `Série ${codigo}`,
315
408
  categoria: info?.categoria || "Desconhecida",
316
409
  periodicidade: info?.periodicidade || (periodicidade ? ROTULO_PERIODICIDADE[periodicidade] : "Desconhecida"),
317
- ...(inferida ? { periodicidadeInferida: true } : {})
410
+ ...(inferida ? { periodicidadeInferida: true } : {}),
411
+ ...(EMITIR_NATUREZA ? { natureza: naturezaDaSerie(codigo) } : {})
318
412
  };
319
413
  }
320
414
  /**
@@ -324,13 +418,28 @@ function refSerie(codigo, periodicidade) {
324
418
  * O SGS não publica versão do dado e **não tem endpoint de metadados**
325
419
  * (`bcb/docs/04`), então a competência sai do que já veio na resposta — zero
326
420
  * requisição a mais. As datas saem no formato da própria fonte.
421
+ *
422
+ * Primeira e última são o MÍNIMO e o MÁXIMO pela data lida, não as pontas da
423
+ * lista: até 08/10/2026 a função pegava `[0]` e `[length − 1]` e só acertava
424
+ * porque toda leitura passava antes por `ordenarPorData`. Ordenar por texto
425
+ * dd/MM/yyyy ordena pelo dia — o defeito que o ibge tinha com rótulos por
426
+ * extenso. Data que não se lê não entra no intervalo.
327
427
  */
328
- function vintageDeObservacoes(observacoes) {
329
- if (observacoes.length === 0)
428
+ export function vintageDeObservacoes(observacoes) {
429
+ let min = null;
430
+ let max = null;
431
+ for (const { data } of observacoes) {
432
+ const t = parseDataSgs(data);
433
+ if (t === null)
434
+ continue;
435
+ if (min === null || t < min)
436
+ min = t;
437
+ if (max === null || t > max)
438
+ max = t;
439
+ }
440
+ if (min === null || max === null)
330
441
  return null;
331
- const primeira = observacoes[0].data;
332
- const ultima = observacoes[observacoes.length - 1].data;
333
- return primeira === ultima ? primeira : `${primeira}–${ultima}`;
442
+ return min === max ? formatarDataSgs(min) : `${formatarDataSgs(min)}–${formatarDataSgs(max)}`;
334
443
  }
335
444
  /**
336
445
  * Bloco de proveniência do catálogo curado do servidor.
@@ -366,7 +475,15 @@ function provSerieSgs(codigo, observacoes, janela = {}, derivado) {
366
475
  *
367
476
  * O `source_url` não pode ser o de uma série só — escolher uma entre cinco
368
477
  * mentiria por omissão sobre as outras quatro. Vai o endpoint-base, e cada série
369
- * entra em `field_sources` com a própria URL.
478
+ * entra em `field_sources` com a própria URL e a própria competência.
479
+ *
480
+ * A competência sai das observações de cada série (as ORIGINAIS da fonte, antes
481
+ * de qualquer harmonização); a do topo cobre todas — da data mais antiga à mais
482
+ * nova entre as séries. Até 08/10/2026 nenhum chamador passava a competência, e
483
+ * as quatro tools que calculam sobre um período (`bcb_indicadores_atuais`,
484
+ * `bcb_comparar`, `bcb_correlacao`, `bcb_deflacionar`) saíam com
485
+ * `data_vintage: null` no topo e em toda sub-fonte. Série que falhou não tem
486
+ * observação e sai com `null` — "não há dado", que é o que aconteceu.
370
487
  */
371
488
  function provMultiSerieSgs(series, detalhe, derivado) {
372
489
  return provenienciaBcb({
@@ -377,13 +494,17 @@ function provMultiSerieSgs(series, detalhe, derivado) {
377
494
  name: detalhe,
378
495
  version: null
379
496
  },
380
- dataVintage: series.map(s => s.vintage).find(v => v != null) ?? null,
497
+ dataVintage: vintageDeObservacoes(series.flatMap(s => s.observacoes ?? [])),
381
498
  detalheCitacao: `${detalhe} (séries ${series.map(s => s.codigo).join(", ")})`,
382
499
  fontesPorCampo: series.map(s => ({
383
500
  fields: [s.campo],
384
501
  source_url: urlSerie(s.codigo, s.inicio, s.fim),
385
502
  dataset_id: `bcdata.sgs.${s.codigo}`,
386
- data_vintage: s.vintage ?? null
503
+ data_vintage: vintageDeObservacoes(s.observacoes ?? []),
504
+ // Todo acesso DESTA série — janela fatiada, sonda `ultimos/20` e
505
+ // repetições —, não só a URL canônica: o instante de cada sub-fonte é o
506
+ // da série dela, e o topo é o mais antigo entre elas.
507
+ filtro: (url) => url.startsWith(`${BCB_SGS_BASE}.${s.codigo}/`)
387
508
  })),
388
509
  ...(derivado ? { derivado } : {})
389
510
  });
@@ -656,7 +777,10 @@ export async function handleIndicadoresAtuais(_args, timeoutMs, maxRetries) {
656
777
  return { indicador: ind.nome, codigo: ind.codigo, erro: err instanceof Error ? err.message : "Erro desconhecido" };
657
778
  }
658
779
  }));
659
- return resultadoComProveniencia({ consultadoEm: new Date().toISOString(), indicadores: resultados }, provMultiSerieSgs(indicadores.map(i => ({ codigo: i.codigo, campo: i.nome })), "painel de indicadores atuais"));
780
+ return resultadoComProveniencia({ consultadoEm: new Date().toISOString(), indicadores: resultados }, provMultiSerieSgs(indicadores.map((i, k) => {
781
+ const r = resultados[k];
782
+ return { codigo: i.codigo, campo: i.nome, observacoes: "data" in r && r.data ? [{ data: r.data }] : [] };
783
+ }), "painel de indicadores atuais"));
660
784
  }
661
785
  catch (error) {
662
786
  return erroDeExcecao(`Erro ao consultar indicadores`, error);
@@ -726,6 +850,8 @@ export async function handleVariacao(args, timeoutMs, maxRetries) {
726
850
  export async function handleComparar(args, timeoutMs, maxRetries) {
727
851
  try {
728
852
  const periodicidades = new Map();
853
+ // Observações ORIGINAIS de cada série, para a competência da proveniência.
854
+ const originais = new Map();
729
855
  let harmonizacao;
730
856
  const resultados = await Promise.all(args.codigos.map(async (codigo) => {
731
857
  const serieInfo = SERIES_POPULARES.find(s => s.codigo === codigo);
@@ -748,6 +874,7 @@ export async function handleComparar(args, timeoutMs, maxRetries) {
748
874
  if (data.length === 0) {
749
875
  return { codigo, nome: serieInfo?.nome || `Série ${codigo}`, erro: "Sem dados no período" };
750
876
  }
877
+ originais.set(codigo, data);
751
878
  const ref = refSerie(codigo, resultado.periodicidade);
752
879
  // O aviso de periodicidade decide pela periodicidade MEDIDA, não pelo
753
880
  // rótulo do catálogo — mesma regra do `bcb_correlacao`. A série 11 está
@@ -842,7 +969,8 @@ export async function handleComparar(args, timeoutMs, maxRetries) {
842
969
  codigo,
843
970
  campo: `ranking[codigo=${codigo}]`,
844
971
  inicio: formatDateForApi(args.dataInicial),
845
- fim: formatDateForApi(args.dataFinal)
972
+ fim: formatDateForApi(args.dataFinal),
973
+ observacoes: originais.get(codigo)
846
974
  })), "comparação entre séries", { nota: haEncadeada ? NOTA_DERIVACAO_ENCADEAMENTO : NOTA_DERIVACAO_ESTATISTICA }));
847
975
  }
848
976
  catch (error) {
@@ -989,7 +1117,8 @@ export async function handleCorrelacao(args, timeoutMs, maxRetries) {
989
1117
  codigo: s.codigo,
990
1118
  campo: `series[codigo=${s.codigo}]`,
991
1119
  inicio: formatDateForApi(args.dataInicial),
992
- fim: formatDateForApi(args.dataFinal)
1120
+ fim: formatDateForApi(args.dataFinal),
1121
+ observacoes: s.observacoes
993
1122
  })), "correlação entre séries", { nota: NOTA_DERIVACAO_ESTATISTICA }));
994
1123
  }
995
1124
  catch (error) {
@@ -1086,8 +1215,8 @@ export async function handleDeflacionar(args, timeoutMs, maxRetries) {
1086
1215
  ...(avisos.length > 0 ? { avisos } : {}),
1087
1216
  ...blocoRede(serie)
1088
1217
  }, provMultiSerieSgs([
1089
- { codigo: args.codigo, campo: "dados[].valorNominal", inicio, fim },
1090
- { codigo: deflatorInfo.codigo, campo: "dados[].fator", inicio }
1218
+ { codigo: args.codigo, campo: "dados[].valorNominal", inicio, fim, observacoes: serie.observacoes },
1219
+ { codigo: deflatorInfo.codigo, campo: "dados[].fator", inicio, observacoes: indice.observacoes }
1091
1220
  ], `deflação por ${chaveIndice.toUpperCase()}`, { nota: NOTA_DERIVACAO_DEFLACAO }));
1092
1221
  }
1093
1222
  catch (error) {
@@ -1145,14 +1274,19 @@ const SERIE_REF_CONSULTADA_SCHEMA = {
1145
1274
  type: "boolean",
1146
1275
  description: "Presente e true quando a periodicidade foi inferida do espaçamento das observações, e não lida " +
1147
1276
  "do catálogo — a API do SGS não publica metadados de série."
1148
- }
1277
+ },
1278
+ natureza: NATUREZA_SCHEMA
1149
1279
  }
1150
1280
  };
1151
1281
  // Shared fragment: a single observation (date + numeric value).
1152
1282
  const OBSERVACAO_SCHEMA = {
1153
1283
  type: "object",
1154
1284
  properties: {
1155
- data: { type: "string", description: "Data da observação (dd/MM/yyyy)" },
1285
+ data: {
1286
+ type: "string",
1287
+ description: "Data de referência da observação (dd/MM/yyyy), na convenção do SGS: em série mensal, trimestral ou " +
1288
+ "anual é o PRIMEIRO dia do período (01/03/2026 = março de 2026); em série diária, o próprio dia"
1289
+ },
1156
1290
  valor: { type: "number", description: "Valor numérico da observação" }
1157
1291
  },
1158
1292
  required: ["data", "valor"]
@@ -1271,6 +1405,25 @@ const BEHAVIOR_NOTE = "Comportamento: consome a API pública SGS do Banco Centra
1271
1405
  */
1272
1406
  const TOTAL_CURADAS = SERIES_POPULARES.length;
1273
1407
  const CURADAS_DO_PORTAL = SERIES_POPULARES.filter(s => s.fonteNome === "portal").length;
1408
+ /**
1409
+ * O que cada número do SGS é — período, acumulação e revisão. Nada disso está no
1410
+ * dado: o SGS devolve só `{data, valor}`, a acumulação só aparece no NOME da série
1411
+ * e a fonte não guarda a versão originalmente divulgada (achado de leitor no
1412
+ * dev.to, 08/10/2026: o mesmo par de armadilhas dos dados XBRL da SEC). Os códigos
1413
+ * citados são os acumulados do catálogo curado; o teste confere que cada um está
1414
+ * lá com "acumulad" no nome, para a frase não envelhecer calada.
1415
+ */
1416
+ export const ACUMULADOS_NO_ANO = [4381, 4386];
1417
+ export const ACUMULADOS_EM_12_MESES = [13522, 4382, 5793];
1418
+ const NOTA_O_QUE_E_CADA_NUMERO = "O que cada número é: `data` é a data de referência na convenção do SGS — em série mensal, trimestral ou " +
1419
+ "anual, o PRIMEIRO dia do período (01/03/2026 = março de 2026). Algumas séries já chegam ACUMULADAS da " +
1420
+ "origem e só o nome diz isso: acumulado no ano (PIB " + ACUMULADOS_NO_ANO.join(", ") + ") e acumulado em " +
1421
+ "12 meses (" + ACUMULADOS_EM_12_MESES.join(", ") + ") — o valor numa data é o acumulado até aquele " +
1422
+ "período, então não some esses valores nem subtraia um do outro; para o valor de um mês, use a série mensal. " +
1423
+ "Revisões: o valor é o que o SGS publica no instante da extração (`retrieved_at` na proveniência). O BCB " +
1424
+ "revisa séries como PIB, IBC-Br, balanço de pagamentos e crédito, e o SGS não guarda a versão originalmente " +
1425
+ "divulgada — um ponto passado pode mudar entre duas consultas, e este servidor não tem como devolver o " +
1426
+ "número da primeira divulgação. ";
1274
1427
  export const TOOL_DESCRIPTIONS = {
1275
1428
  bcb_serie_valores: "Consulta o histórico de valores de UMA série temporal do BCB pelo código SGS, opcionalmente " +
1276
1429
  "limitado por um intervalo de datas (dataInicial/dataFinal). " +
@@ -1287,6 +1440,7 @@ export const TOOL_DESCRIPTIONS = {
1287
1440
  "se o período pedido estava aberto numa série diária, `janelaAplicada` diz qual janela foi usada e por quê. " +
1288
1441
  "Harmonização: `frequencia` (mensal|trimestral|anual) reamostra a série antes de responder, com a " +
1289
1442
  "convenção escolhida em `agregacao`; a resposta traz `harmonizacao` com `derived: true` e a nota do cálculo. " +
1443
+ NOTA_O_QUE_E_CADA_NUMERO +
1290
1444
  BEHAVIOR_NOTE,
1291
1445
  bcb_serie_ultimos: "Obtém as últimas N observações de UMA série temporal do BCB (mais recentes primeiro a partir " +
1292
1446
  "do fim da série). " +
@@ -1297,6 +1451,7 @@ export const TOOL_DESCRIPTIONS = {
1297
1451
  "`totalRegistros` = 0 com `observacao`. " +
1298
1452
  "Acima de 20: o endpoint nativo do BCB rejeita N > 20 em qualquer periodicidade, então o servidor " +
1299
1453
  "descobre a periodicidade da série e busca por janela de datas, devolvendo os N últimos pontos. " +
1454
+ NOTA_O_QUE_E_CADA_NUMERO +
1300
1455
  BEHAVIOR_NOTE,
1301
1456
  bcb_serie_metadados: "Obtém a descrição de UMA série do BCB (nome, periodicidade, categoria, fonte e último valor), sem " +
1302
1457
  "trazer a série histórica. " +
@@ -1953,6 +2108,7 @@ const RAW_TOOL_DEFINITIONS = [
1953
2108
  categoria: { type: "string" },
1954
2109
  periodicidade: { type: "string" },
1955
2110
  periodicidadeInferida: { type: "boolean" },
2111
+ natureza: NATUREZA_SCHEMA,
1956
2112
  totalRegistros: { type: "number" }
1957
2113
  },
1958
2114
  required: ["codigo", "nome"]
@@ -2060,6 +2216,7 @@ const RAW_TOOL_DEFINITIONS = [
2060
2216
  categoria: { type: "string" },
2061
2217
  periodicidade: { type: "string" },
2062
2218
  periodicidadeInferida: { type: "boolean" },
2219
+ natureza: NATUREZA_SCHEMA,
2063
2220
  totalRegistros: { type: "number" }
2064
2221
  },
2065
2222
  required: ["codigo", "nome"]