opf-br-mcp 0.8.0 → 1.0.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.
Files changed (3) hide show
  1. package/README.md +11 -2
  2. package/dist/index.js +277 -34
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -31,6 +31,8 @@ acesso token-eficiente às regras do Open Finance Brasil.
31
31
  | `mqd` | Confluence público OFB | Motor de Qualidade de Dados (Especificação Técnica, Arquitetura, Documentação da API, Manual de Instalação, Endpoints Validados, FAQ, Troubleshooting) — item por seção |
32
32
  | `webhook-v1-openapi` | GitHub OpenBanking-Brasil/all-services-repo | Spec OpenAPI 1.3.0 da API de Webhook (notificações de mudança de estado: pagamentos, enrollments, pagamentos automáticos) |
33
33
  | `seguranca` | Confluence público OFB | Segurança do Open Finance Brasil (Perfil de Segurança, FAPI, DCR, CIBA, Padrão de Certificados, Assinaturas, Casos de Erro, Redirecionamento App-to-App, Glossário, Versionamento) — item por seção |
34
+ | `requisitos-nao-funcionais` | Confluence público OFB (Manual de APIs) | Requisitos não funcionais de todas as APIs (Desempenho, Disponibilidade, Timeout, Limites de tráfego, Limites operacionais, Indisponibilidade Programada) — item por seção |
35
+ | `limites-por-endpoint` | Confluence público OFB (Manual de APIs) | SLA (p95), timeout, TPM, TPS e limite operacional de cada endpoint de todas as famílias de API — um item por endpoint |
34
36
  | `participantes` | Diretório OFB (data.directory.openbankingbrasil.org.br) | Organizações participantes, marcas (authorisation servers) e famílias de API suportadas com versões — um item por organização |
35
37
  | `portal` | Confluence público OFB (busca ao vivo) | Busca CQL em todo o Portal do Desenvolvedor (espaço OF) — sem cache, `query` obrigatória; fallback quando os domínios específicos não cobrem o assunto |
36
38
 
@@ -39,7 +41,9 @@ acesso token-eficiente às regras do Open Finance Brasil.
39
41
  - `list_domains()` — descoberta: domínios, filtros, versão da spec de origem e estado do cache
40
42
  - `search(domain, query?, filters?, limit?, offset?)` — busca filtrada, retorno compacto
41
43
  - `get_item(domain, id)` — registro completo
42
- - `refresh(domain?)` — força re-extração das fontes
44
+ - `refresh(domain?)` — força re-extração das fontes. Prefira passar `domain`:
45
+ sem ele o server atualiza o que couber em 45s (o timeout padrão do cliente MCP
46
+ é 60s) e devolve o restante em `pendentes`, para o agente retomar um a um
43
47
 
44
48
  Fluxo recomendado para o agente: `list_domains` → `search` → `get_item`.
45
49
 
@@ -67,7 +71,12 @@ bem na janela de contexto de um agente, e despejá-la desperdiça tokens. A solu
67
71
  é **revelação progressiva** — o agente nunca recebe a spec completa de uma vez,
68
72
  apenas o mínimo necessário em cada etapa do funil:
69
73
 
70
- 1. **`list_domains`** — catálogo barato: quais domínios e filtros existem.
74
+ 1. **`list_domains`** — catálogo barato: quais domínios e filtros existem. Os
75
+ filtros idênticos a uma família inteira de domínios (os 9 `*-openapi`, os 12
76
+ de seções do Confluence) saem uma única vez em `filterSets`; cada domínio
77
+ traz `filterSet` e só lista inline o que é próprio dele (em `*-openapi`,
78
+ apenas `path`, cujo exemplo varia por API). Os filtros aceitos por um domínio
79
+ são a união dos dois.
71
80
  2. **`search`** — índice **pesquisável e resumido**. Cada resultado traz só os
72
81
  campos leves (`id`, `type`, `path`, `method`, `summary`/`name`, `required`,
73
82
  `in`); o nó pesado da spec (`detail`) e a lista `refs` são removidos do
package/dist/index.js CHANGED
@@ -18,14 +18,24 @@ function cacheDir() {
18
18
  function cachePath(domainId) {
19
19
  return join(cacheDir(), `${domainId}.json`);
20
20
  }
21
+ function isCacheEntry(value) {
22
+ const entry = value;
23
+ return typeof entry === "object" && entry !== null && typeof entry.extractedAt === "string" && typeof entry.packageVersion === "string" && typeof entry.data === "object" && entry.data !== null && Array.isArray(entry.data.items);
24
+ }
21
25
  function readCache(domainId) {
22
26
  const path = cachePath(domainId);
23
27
  if (!existsSync(path)) return null;
28
+ let parsed;
24
29
  try {
25
- return JSON.parse(readFileSync(path, "utf8"));
30
+ parsed = JSON.parse(readFileSync(path, "utf8"));
26
31
  } catch {
27
32
  return null;
28
33
  }
34
+ if (!isCacheEntry(parsed)) {
35
+ console.error(`[opf-br-mcp] cache de ${domainId} tem forma inesperada; ignorando (${path})`);
36
+ return null;
37
+ }
38
+ return parsed;
29
39
  }
30
40
  function writeCache(domainId, data, packageVersion) {
31
41
  const entry = {
@@ -46,7 +56,7 @@ function isFresh(entry, ttlHours, now = /* @__PURE__ */ new Date()) {
46
56
  }
47
57
 
48
58
  // src/core/version.ts
49
- var PACKAGE_VERSION = "0.8.0";
59
+ var PACKAGE_VERSION = "1.0.0";
50
60
 
51
61
  // src/core/data.ts
52
62
  async function getDomainData(domain, force = false, ctx) {
@@ -66,15 +76,52 @@ async function getDomainData(domain, force = false, ctx) {
66
76
  }
67
77
  }
68
78
 
79
+ // src/core/filter-sets.ts
80
+ var FILTER_SETS = {
81
+ openapi: [
82
+ { name: "method", description: "Verbo HTTP exato (ex.: POST)" },
83
+ { name: "schema", description: "Substring no nome do schema (case-insensitive)" },
84
+ {
85
+ name: "response",
86
+ description: "Substring no nome do components.responses, a chave usada no $ref (ex.: 202Webhook)"
87
+ },
88
+ {
89
+ name: "parameter",
90
+ description: "Substring no nome do components.parameters, a chave usada no $ref (ex.: xFapiInteractionId)"
91
+ },
92
+ {
93
+ name: "header",
94
+ description: "Substring no nome do components.headers, a chave usada no $ref (ex.: X-V)"
95
+ }
96
+ ],
97
+ sections: [
98
+ { name: "page", description: "Substring no t\xEDtulo da p\xE1gina Confluence" },
99
+ { name: "heading", description: "Substring no t\xEDtulo da se\xE7\xE3o (heading)" },
100
+ { name: "contains", description: "Substring no conte\xFAdo da se\xE7\xE3o" }
101
+ ]
102
+ };
103
+
69
104
  // src/core/http.ts
70
- function sleep(ms) {
71
- return new Promise((resolve) => setTimeout(resolve, ms));
105
+ function sleep(ms, signal) {
106
+ if (signal?.aborted) return Promise.resolve();
107
+ return new Promise((resolve) => {
108
+ const done = () => {
109
+ clearTimeout(timer);
110
+ signal?.removeEventListener("abort", done);
111
+ resolve();
112
+ };
113
+ const timer = setTimeout(done, ms);
114
+ signal?.addEventListener("abort", done, { once: true });
115
+ });
72
116
  }
73
117
  async function fetchWithRetry(url, opts) {
74
118
  let lastError;
75
119
  for (let attempt = 0; attempt <= opts.retryDelaysMs.length; attempt++) {
76
120
  if (opts.signal?.aborted) break;
77
- if (attempt > 0) await sleep(opts.retryDelaysMs[attempt - 1]);
121
+ if (attempt > 0) {
122
+ await sleep(opts.retryDelaysMs[attempt - 1], opts.signal);
123
+ if (opts.signal?.aborted) break;
124
+ }
78
125
  const timeout = AbortSignal.timeout(3e4);
79
126
  const signal = opts.signal ? AbortSignal.any([opts.signal, timeout]) : timeout;
80
127
  let response;
@@ -134,8 +181,14 @@ async function fetchConfluencePage(baseUrl, pageId, retryDelaysMs, signal) {
134
181
  const apiUrl = `${baseUrl}/wiki/rest/api/content/${pageId}?expand=body.view`;
135
182
  const response = await fetchWithRetry(apiUrl, { retryDelaysMs, headers: HEADERS, signal });
136
183
  const json = await response.json();
184
+ const html = json?.body?.view?.value;
185
+ if (typeof html !== "string") {
186
+ throw new Error(
187
+ `resposta da p\xE1gina ${pageId} n\xE3o traz body.view.value; n\xE3o \xE9 conte\xFAdo Confluence expandido`
188
+ );
189
+ }
137
190
  return {
138
- html: json.body?.view?.value ?? "",
191
+ html,
139
192
  title: json.title,
140
193
  url: `${baseUrl}/wiki${json._links?.webui ?? ""}`
141
194
  };
@@ -310,8 +363,8 @@ var pcmDomain = {
310
363
  const total = pcmConfig.pages.length;
311
364
  const pages = [];
312
365
  for (const [i, page] of pcmConfig.pages.entries()) {
366
+ if (i > 0) await sleep(pcmConfig.interRequestDelayMs, ctx?.signal);
313
367
  if (ctx?.signal?.aborted) throw new Error("Extra\xE7\xE3o cancelada pelo cliente");
314
- if (i > 0) await sleep(pcmConfig.interRequestDelayMs);
315
368
  ctx?.onProgress?.(i, total, `Extraindo "${page.title}"`);
316
369
  const { html, url } = await fetchConfluencePage(
317
370
  pcmConfig.confluenceBaseUrl,
@@ -380,7 +433,13 @@ function refsOf(node, specName) {
380
433
  return [...out];
381
434
  }
382
435
  function parseOpenApiSpec(yamlText, specName) {
383
- const spec = parse(yamlText);
436
+ const parsed = parse(yamlText);
437
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed) || !("paths" in parsed) && !("components" in parsed)) {
438
+ throw new Error(
439
+ `resposta de ${specName} n\xE3o \xE9 uma spec OpenAPI (sem "paths" nem "components"); in\xEDcio do conte\xFAdo: ${JSON.stringify(yamlText.slice(0, 80))}`
440
+ );
441
+ }
442
+ const spec = parsed;
384
443
  const items = [];
385
444
  for (const [path, pathItem] of Object.entries(spec.paths ?? {})) {
386
445
  for (const method of HTTP_METHODS) {
@@ -435,22 +494,10 @@ function createOpenApiDomain(config) {
435
494
  description: config.description,
436
495
  specVersion: config.specVersion,
437
496
  ttlHours: 72,
497
+ filterSet: "openapi",
438
498
  filters: [
439
499
  { name: "path", description: `Substring no path do endpoint (ex.: ${config.pathExample})` },
440
- { name: "method", description: "Verbo HTTP exato (ex.: POST)" },
441
- { name: "schema", description: "Substring no nome do schema (case-insensitive)" },
442
- {
443
- name: "response",
444
- description: "Substring no nome do components.responses, a chave usada no $ref (ex.: 202Webhook)"
445
- },
446
- {
447
- name: "parameter",
448
- description: "Substring no nome do components.parameters, a chave usada no $ref (ex.: xFapiInteractionId)"
449
- },
450
- {
451
- name: "header",
452
- description: "Substring no nome do components.headers, a chave usada no $ref (ex.: X-V)"
453
- }
500
+ ...FILTER_SETS.openapi
454
501
  ],
455
502
  async extract(ctx) {
456
503
  if (ctx?.signal?.aborted) throw new Error("Extra\xE7\xE3o cancelada pelo cliente");
@@ -592,17 +639,14 @@ function createConfluenceSectionsDomain(config) {
592
639
  description: config.description,
593
640
  specVersion: config.specVersion,
594
641
  ttlHours: 72,
595
- filters: [
596
- { name: "page", description: "Substring no t\xEDtulo da p\xE1gina Confluence" },
597
- { name: "heading", description: "Substring no t\xEDtulo da se\xE7\xE3o (heading)" },
598
- { name: "contains", description: "Substring no conte\xFAdo da se\xE7\xE3o" }
599
- ],
642
+ filterSet: "sections",
643
+ filters: [...FILTER_SETS.sections],
600
644
  async extract(ctx) {
601
645
  const total = config.pages.length;
602
646
  const pages = [];
603
647
  for (const [i, page] of config.pages.entries()) {
648
+ if (i > 0) await sleep(config.interRequestDelayMs, ctx?.signal);
604
649
  if (ctx?.signal?.aborted) throw new Error("Extra\xE7\xE3o cancelada pelo cliente");
605
- if (i > 0) await sleep(config.interRequestDelayMs);
606
650
  ctx?.onProgress?.(i, total, `Extraindo "${page.title}"`);
607
651
  const { html, url } = await fetchConfluencePage(
608
652
  config.confluenceBaseUrl,
@@ -988,6 +1032,175 @@ var segurancaConfig = {
988
1032
  // src/domains/seguranca/index.ts
989
1033
  var segurancaDomain = createConfluenceSectionsDomain(segurancaConfig);
990
1034
 
1035
+ // src/domains/requisitos-nao-funcionais/config.ts
1036
+ var requisitosNaoFuncionaisConfig = {
1037
+ id: "requisitos-nao-funcionais",
1038
+ title: "Requisitos n\xE3o Funcionais \u2014 SLA, disponibilidade e limites (Manual de APIs)",
1039
+ description: "Requisitos n\xE3o funcionais que valem para todas as APIs do Open Finance Brasil, extra\xEDdos das p\xE1ginas Confluence do Manual de APIs: Desempenho (SLA do percentil 95 do tempo de resposta por faixa de frequ\xEAncia do endpoint), Disponibilidade (m\xEDnimos por 24h e por trimestre), Timeout, Limites de tr\xE1fego (TPM por origem e TPS globais), Limites operacionais (teto de consultas por m\xEAs, por endpoint e por recurso, e pagina\xE7\xE3o com pagination-key) e Indisponibilidade Programada. Os n\xFAmeros por endpoint (SLA, timeout, TPM, TPS e limite operacional de cada rota) est\xE3o em limites-por-endpoint, um registro por endpoint. Cada item aqui \xE9 uma se\xE7\xE3o (heading) da p\xE1gina \u2014 as p\xE1ginas Desempenho e Timeout n\xE3o t\xEAm headings e viram um \xFAnico item `intro` cada, ent\xE3o nelas filtre por page ou contains, n\xE3o por heading. search devolve um snippet do conte\xFAdo; use get_item para o texto completo da se\xE7\xE3o.",
1040
+ confluenceBaseUrl: "https://openfinancebrasil.atlassian.net",
1041
+ interRequestDelayMs: 2e3,
1042
+ retryDelaysMs: [2e3, 4e3, 8e3, 16e3],
1043
+ // Fora da lista: a página-mãe "Requisitos não Funcionais" (17956981), que só
1044
+ // nomeia as quatro faixas de frequência sem defini-las (página índice), e a
1045
+ // página "Referência" (17957025), cujas tabelas de limites por endpoint têm
1046
+ // parser próprio no domínio limites-por-endpoint.
1047
+ pages: [
1048
+ { pageId: "17891396", title: "Desempenho" },
1049
+ { pageId: "17891406", title: "Disponibilidade" },
1050
+ { pageId: "17891413", title: "Timeout" },
1051
+ { pageId: "17989722", title: "Limites de tr\xE1fego" },
1052
+ { pageId: "17924220", title: "Limites operacionais" },
1053
+ { pageId: "1808269313", title: "Indisponibilidade Programada" }
1054
+ ]
1055
+ };
1056
+
1057
+ // src/domains/requisitos-nao-funcionais/index.ts
1058
+ var requisitosNaoFuncionaisDomain = createConfluenceSectionsDomain(
1059
+ requisitosNaoFuncionaisConfig
1060
+ );
1061
+
1062
+ // src/domains/limites-por-endpoint/config.ts
1063
+ var limitesPorEndpointConfig = {
1064
+ confluenceBaseUrl: "https://openfinancebrasil.atlassian.net",
1065
+ retryDelaysMs: [2e3, 4e3, 8e3, 16e3],
1066
+ // Página única: a tabela "Referência" do Manual de APIs, com uma tabela por
1067
+ // família de API e uma linha por endpoint.
1068
+ pageId: "17957025",
1069
+ pageTitle: "Refer\xEAncia"
1070
+ };
1071
+
1072
+ // src/domains/limites-por-endpoint/parser.ts
1073
+ import * as cheerio3 from "cheerio";
1074
+ var HTTP_METHODS2 = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
1075
+ function cellText3($, el) {
1076
+ const clone = $(el).clone();
1077
+ clone.find("br").replaceWith("\n");
1078
+ return clone.text().replace(/[​‌‍]/g, "").replace(/ /g, " ").replace(/\s+/g, " ").trim();
1079
+ }
1080
+ function headerKey(raw) {
1081
+ const h = raw.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().trim();
1082
+ if (h.startsWith("endpoint")) return "path";
1083
+ if (h.startsWith("frequencia")) return "frequencia";
1084
+ if (h.startsWith("sla")) return "slaMs";
1085
+ if (h.startsWith("timeout")) return "timeoutS";
1086
+ if (h.startsWith("limite operacional")) return "limiteOperacional";
1087
+ if (h === "tpm") return "tpm";
1088
+ if (h === "tps") return "tps";
1089
+ return null;
1090
+ }
1091
+ function splitEndpoint(cell) {
1092
+ const [first, ...rest] = cell.split(" ");
1093
+ if (HTTP_METHODS2.has(first.toUpperCase()) && rest.length > 0) {
1094
+ return { metodo: first.toUpperCase(), path: rest.join(" ") };
1095
+ }
1096
+ return { metodo: null, path: cell };
1097
+ }
1098
+ function parseEndpointLimits(html) {
1099
+ const $ = cheerio3.load(html);
1100
+ $("style, script").remove();
1101
+ const limits = [];
1102
+ for (const table of $("table").toArray()) {
1103
+ const rows = $(table).find("tr").toArray();
1104
+ const cellsOf = (tr) => $(tr).find("th, td").toArray().map((c) => cellText3($, c));
1105
+ const headerIdx = rows.findIndex(
1106
+ (tr) => cellsOf(tr).some((c) => headerKey(c) === "path")
1107
+ );
1108
+ if (headerIdx === -1) continue;
1109
+ const api = rows.slice(0, headerIdx).map((tr) => cellsOf(tr).filter(Boolean).join(" ")).filter(Boolean).join(" ").trim();
1110
+ const keys = cellsOf(rows[headerIdx]).map(headerKey);
1111
+ for (const tr of rows.slice(headerIdx + 1)) {
1112
+ const cells = cellsOf(tr);
1113
+ const limit = {
1114
+ api,
1115
+ metodo: null,
1116
+ path: "",
1117
+ frequencia: null,
1118
+ slaMs: null,
1119
+ timeoutS: null,
1120
+ tpm: null,
1121
+ tps: null,
1122
+ limiteOperacional: null
1123
+ };
1124
+ cells.forEach((text2, i) => {
1125
+ const key = keys[i];
1126
+ if (!key || !text2) return;
1127
+ if (key === "path") {
1128
+ const { metodo, path } = splitEndpoint(text2);
1129
+ limit.metodo = metodo;
1130
+ limit.path = path;
1131
+ } else {
1132
+ limit[key] = text2;
1133
+ }
1134
+ });
1135
+ if (limit.path) limits.push(limit);
1136
+ }
1137
+ }
1138
+ return limits;
1139
+ }
1140
+
1141
+ // src/domains/limites-por-endpoint/index.ts
1142
+ function slugify3(text2) {
1143
+ return text2.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
1144
+ }
1145
+ function buildItems3(page, limits) {
1146
+ const items = [];
1147
+ const seen = /* @__PURE__ */ new Map();
1148
+ for (const limit of limits) {
1149
+ const base = `${page.pageId}:${slugify3(`${limit.api} ${limit.metodo ?? ""} ${limit.path}`)}`;
1150
+ const count = (seen.get(base) ?? 0) + 1;
1151
+ seen.set(base, count);
1152
+ items.push({ ...limit, id: count > 1 ? `${base}-${count}` : base, page });
1153
+ }
1154
+ return items;
1155
+ }
1156
+ var limitesPorEndpointDomain = {
1157
+ id: "limites-por-endpoint",
1158
+ title: "Limites por endpoint \u2014 SLA, timeout, TPM, TPS e limite operacional",
1159
+ description: 'Tabela de refer\xEAncia do Manual de APIs do Open Finance Brasil com os limites de cada endpoint de todas as fam\xEDlias de API (Dados Abertos, Dados Cadastrais e Transacionais, Servi\xE7os de Inicia\xE7\xE3o, Portabilidade de Cr\xE9dito, Webhook, Admin e Discovery). Um item por endpoint, com a faixa de frequ\xEAncia (alta, m\xE9dia-alta, m\xE9dia ou baixa), o SLA em ms do percentil 95 do tempo de resposta, o timeout em segundos, os limites de tr\xE1fego TPM e TPS e o limite operacional de consultas por m\xEAs. Responde perguntas do tipo "qual o SLA e o timeout do GET /resources". O texto normativo por tr\xE1s desses n\xFAmeros (como o SLA \xE9 medido e apurado, disponibilidade m\xEDnima, regras de limites de tr\xE1fego e operacionais) est\xE1 em requisitos-nao-funcionais. search devolve o registro completo \u2014 em TPM algumas linhas trazem a tabela de faixas QCA\u2192TPM como texto.',
1160
+ ttlHours: 72,
1161
+ filters: [
1162
+ { name: "endpoint", description: "Substring na rota (ex.: /resources)" },
1163
+ { name: "api", description: "Substring no nome da fam\xEDlia de API (ex.: Recursos, [SV] API Pagamentos)" },
1164
+ { name: "method", description: "Verbo HTTP exato (ex.: GET)" },
1165
+ { name: "frequencia", description: "Substring na faixa de frequ\xEAncia (ex.: Alta, Baixa)" }
1166
+ ],
1167
+ async extract(ctx) {
1168
+ ctx?.onProgress?.(0, 1, `Extraindo "${limitesPorEndpointConfig.pageTitle}"`);
1169
+ const { html, url } = await fetchConfluencePage(
1170
+ limitesPorEndpointConfig.confluenceBaseUrl,
1171
+ limitesPorEndpointConfig.pageId,
1172
+ limitesPorEndpointConfig.retryDelaysMs,
1173
+ ctx?.signal
1174
+ );
1175
+ ctx?.onProgress?.(1, 1);
1176
+ const page = {
1177
+ pageId: limitesPorEndpointConfig.pageId,
1178
+ title: limitesPorEndpointConfig.pageTitle,
1179
+ url
1180
+ };
1181
+ return { items: buildItems3(page, parseEndpointLimits(html)) };
1182
+ },
1183
+ search(data, query, filters = {}) {
1184
+ const endpoint = filters.endpoint ? normalize(filters.endpoint) : void 0;
1185
+ const api = filters.api ? normalize(filters.api) : void 0;
1186
+ const metodo = filters.method?.trim().toUpperCase();
1187
+ const frequencia = filters.frequencia ? normalize(filters.frequencia) : void 0;
1188
+ return data.items.filter((item) => {
1189
+ if (endpoint && !normalize(item.path).includes(endpoint)) return false;
1190
+ if (api && !normalize(item.api).includes(api)) return false;
1191
+ if (metodo && item.metodo !== metodo) return false;
1192
+ if (frequencia && !normalize(item.frequencia ?? "").includes(frequencia)) return false;
1193
+ if (query?.trim() && !matchesQuery(`${item.api} ${item.metodo ?? ""} ${item.path}`, query)) {
1194
+ return false;
1195
+ }
1196
+ return true;
1197
+ });
1198
+ },
1199
+ getItem(data, id) {
1200
+ return data.items.find((i) => i.id === id) ?? null;
1201
+ }
1202
+ };
1203
+
991
1204
  // src/domains/participantes/config.ts
992
1205
  var participantesConfig = {
993
1206
  id: "participantes",
@@ -1000,6 +1213,11 @@ var participantesConfig = {
1000
1213
  // src/domains/participantes/parser.ts
1001
1214
  function parseParticipants(jsonText) {
1002
1215
  const orgs = JSON.parse(jsonText);
1216
+ if (!Array.isArray(orgs) || orgs.length === 0) {
1217
+ throw new Error(
1218
+ `resposta do diret\xF3rio n\xE3o \xE9 uma lista de organiza\xE7\xF5es n\xE3o-vazia; in\xEDcio do conte\xFAdo: ${JSON.stringify(jsonText.slice(0, 80))}`
1219
+ );
1220
+ }
1003
1221
  return orgs.map((org) => {
1004
1222
  const servers = org.AuthorisationServers ?? [];
1005
1223
  const families = /* @__PURE__ */ new Set();
@@ -1157,6 +1375,8 @@ var domains = [
1157
1375
  mqdDomain,
1158
1376
  webhookDomain,
1159
1377
  segurancaDomain,
1378
+ requisitosNaoFuncionaisDomain,
1379
+ limitesPorEndpointDomain,
1160
1380
  participantesDomain,
1161
1381
  portalDomain
1162
1382
  ];
@@ -1181,6 +1401,7 @@ function findDomain(id) {
1181
1401
  return domains.find((d) => d.id === id);
1182
1402
  }
1183
1403
  var validIds = () => domains.map((d) => d.id).join(", ");
1404
+ var REFRESH_BUDGET_MS = 45e3;
1184
1405
  var domainIdSchema = z.enum(domains.map((d) => d.id)).describe("Id do dom\xEDnio (ver list_domains)");
1185
1406
  function extractContext(extra) {
1186
1407
  const progressToken = extra._meta?.progressToken;
@@ -1195,7 +1416,7 @@ function extractContext(extra) {
1195
1416
  }
1196
1417
  };
1197
1418
  }
1198
- function createServer() {
1419
+ function createServer(refreshBudgetMs = REFRESH_BUDGET_MS) {
1199
1420
  const server2 = new McpServer(
1200
1421
  { name: "opf-br-mcp", version: PACKAGE_VERSION },
1201
1422
  {
@@ -1206,7 +1427,7 @@ function createServer() {
1206
1427
  "list_domains",
1207
1428
  {
1208
1429
  title: "Listar dom\xEDnios",
1209
- description: "Lista os dom\xEDnios de conhecimento do Open Finance Brasil dispon\xEDveis neste server, com os filtros aceitos por cada um, a vers\xE3o da spec de origem e o estado do cache local. Devolve tamb\xE9m a vers\xE3o deste server em `server.version`. Comece por aqui; depois use search(domain, ...) e get_item(domain, id).",
1430
+ description: "Lista os dom\xEDnios de conhecimento do Open Finance Brasil dispon\xEDveis neste server, com os filtros aceitos por cada um, a vers\xE3o da spec de origem e o estado do cache local. Devolve tamb\xE9m a vers\xE3o deste server em `server.version`. Os filtros aceitos por um dom\xEDnio s\xE3o a uni\xE3o de `filters` (os pr\xF3prios dele, pode vir ausente) com `filterSets[<filterSet>]` (o conjunto comum \xE0 fam\xEDlia, listado uma vez no topo). Comece por aqui; depois use search(domain, ...) e get_item(domain, id).",
1210
1431
  annotations: {
1211
1432
  readOnlyHint: true,
1212
1433
  destructiveHint: false,
@@ -1216,11 +1437,14 @@ function createServer() {
1216
1437
  },
1217
1438
  async () => {
1218
1439
  const out = domains.map((d) => {
1440
+ const shared = d.filterSet ? FILTER_SETS[d.filterSet] : void 0;
1441
+ const own = shared ? d.filters.filter((f) => !shared.some((s) => s.name === f.name && s.description === f.description)) : d.filters;
1219
1442
  const base = {
1220
1443
  id: d.id,
1221
1444
  title: d.title,
1222
1445
  description: d.description,
1223
- filters: d.filters,
1446
+ ...d.filterSet ? { filterSet: d.filterSet } : {},
1447
+ ...own.length > 0 ? { filters: own } : {},
1224
1448
  ...d.specVersion ? { specVersion: d.specVersion } : {}
1225
1449
  };
1226
1450
  if (d.live) {
@@ -1233,7 +1457,14 @@ function createServer() {
1233
1457
  extractedAt: cached?.extractedAt ?? null
1234
1458
  };
1235
1459
  });
1236
- return text({ server: { name: "opf-br-mcp", version: PACKAGE_VERSION }, domains: out });
1460
+ const usedSets = Object.fromEntries(
1461
+ Object.entries(FILTER_SETS).filter(([name]) => domains.some((d) => d.filterSet === name))
1462
+ );
1463
+ return text({
1464
+ server: { name: "opf-br-mcp", version: PACKAGE_VERSION },
1465
+ filterSets: usedSets,
1466
+ domains: out
1467
+ });
1237
1468
  }
1238
1469
  );
1239
1470
  server2.registerTool(
@@ -1349,7 +1580,7 @@ function createServer() {
1349
1580
  "refresh",
1350
1581
  {
1351
1582
  title: "Re-extrair fontes",
1352
- description: "For\xE7a re-extra\xE7\xE3o das fontes p\xFAblicas (ignora o TTL de 72h do cache). Sem `domain`, atualiza todos. Use quando suspeitar de dados desatualizados.",
1583
+ description: "For\xE7a re-extra\xE7\xE3o das fontes p\xFAblicas (ignora o TTL de 72h do cache). Use quando suspeitar de dados desatualizados. Prefira passar `domain`: sem ele o server atualiza o que couber em 45s e devolve o restante em `pendentes`, que voc\xEA deve refazer chamando refresh(domain) para cada id.",
1353
1584
  inputSchema: {
1354
1585
  domain: domainIdSchema.optional().describe("Id do dom\xEDnio; omita para todos")
1355
1586
  },
@@ -1365,7 +1596,13 @@ function createServer() {
1365
1596
  (d) => !d.live && (!domain || d.id === domain)
1366
1597
  );
1367
1598
  const report = {};
1599
+ const pendentes = [];
1600
+ const inicio = Date.now();
1368
1601
  for (const d of targets) {
1602
+ if (!domain && Date.now() - inicio > refreshBudgetMs) {
1603
+ pendentes.push(d.id);
1604
+ continue;
1605
+ }
1369
1606
  try {
1370
1607
  const { data } = await getDomainData(d, true, extractContext(extra));
1371
1608
  report[d.id] = `ok: ${data.items.length} itens`;
@@ -1373,7 +1610,13 @@ function createServer() {
1373
1610
  report[d.id] = `erro: ${err.message}`;
1374
1611
  }
1375
1612
  }
1376
- return text(report);
1613
+ return text({
1614
+ atualizados: report,
1615
+ ...pendentes.length > 0 ? {
1616
+ pendentes,
1617
+ nota: `Parei em ${refreshBudgetMs / 1e3}s para n\xE3o estourar o timeout do cliente. Chame refresh(domain) para cada id em pendentes.`
1618
+ } : {}
1619
+ });
1377
1620
  }
1378
1621
  );
1379
1622
  return server2;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opf-br-mcp",
3
- "version": "0.8.0",
3
+ "version": "1.0.0",
4
4
  "description": "MCP server local com as regras do Open Finance Brasil (PCM additionalInfo, specs OpenAPI de Pagamentos)",
5
5
  "type": "module",
6
6
  "license": "MIT",