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.
- package/README.md +11 -2
- package/dist/index.js +277 -34
- 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
|
-
|
|
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.
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
596
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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).
|
|
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(
|
|
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;
|