opf-br-mcp 0.8.0 → 0.9.3
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 +9 -2
- package/dist/index.js +106 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,7 +39,9 @@ acesso token-eficiente às regras do Open Finance Brasil.
|
|
|
39
39
|
- `list_domains()` — descoberta: domínios, filtros, versão da spec de origem e estado do cache
|
|
40
40
|
- `search(domain, query?, filters?, limit?, offset?)` — busca filtrada, retorno compacto
|
|
41
41
|
- `get_item(domain, id)` — registro completo
|
|
42
|
-
- `refresh(domain?)` — força re-extração das fontes
|
|
42
|
+
- `refresh(domain?)` — força re-extração das fontes. Prefira passar `domain`:
|
|
43
|
+
sem ele o server atualiza o que couber em 45s (o timeout padrão do cliente MCP
|
|
44
|
+
é 60s) e devolve o restante em `pendentes`, para o agente retomar um a um
|
|
43
45
|
|
|
44
46
|
Fluxo recomendado para o agente: `list_domains` → `search` → `get_item`.
|
|
45
47
|
|
|
@@ -67,7 +69,12 @@ bem na janela de contexto de um agente, e despejá-la desperdiça tokens. A solu
|
|
|
67
69
|
é **revelação progressiva** — o agente nunca recebe a spec completa de uma vez,
|
|
68
70
|
apenas o mínimo necessário em cada etapa do funil:
|
|
69
71
|
|
|
70
|
-
1. **`list_domains`** — catálogo barato: quais domínios e filtros existem.
|
|
72
|
+
1. **`list_domains`** — catálogo barato: quais domínios e filtros existem. Os
|
|
73
|
+
filtros idênticos a uma família inteira de domínios (os 8 `*-openapi`, os 11
|
|
74
|
+
de seções do Confluence) saem uma única vez em `filterSets`; cada domínio
|
|
75
|
+
traz `filterSet` e só lista inline o que é próprio dele (em `*-openapi`,
|
|
76
|
+
apenas `path`, cujo exemplo varia por API). Os filtros aceitos por um domínio
|
|
77
|
+
são a união dos dois.
|
|
71
78
|
2. **`search`** — índice **pesquisável e resumido**. Cada resultado traz só os
|
|
72
79
|
campos leves (`id`, `type`, `path`, `method`, `summary`/`name`, `required`,
|
|
73
80
|
`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 = "0.9.3";
|
|
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,
|
|
@@ -1000,6 +1044,11 @@ var participantesConfig = {
|
|
|
1000
1044
|
// src/domains/participantes/parser.ts
|
|
1001
1045
|
function parseParticipants(jsonText) {
|
|
1002
1046
|
const orgs = JSON.parse(jsonText);
|
|
1047
|
+
if (!Array.isArray(orgs) || orgs.length === 0) {
|
|
1048
|
+
throw new Error(
|
|
1049
|
+
`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))}`
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1003
1052
|
return orgs.map((org) => {
|
|
1004
1053
|
const servers = org.AuthorisationServers ?? [];
|
|
1005
1054
|
const families = /* @__PURE__ */ new Set();
|
|
@@ -1181,6 +1230,7 @@ function findDomain(id) {
|
|
|
1181
1230
|
return domains.find((d) => d.id === id);
|
|
1182
1231
|
}
|
|
1183
1232
|
var validIds = () => domains.map((d) => d.id).join(", ");
|
|
1233
|
+
var REFRESH_BUDGET_MS = 45e3;
|
|
1184
1234
|
var domainIdSchema = z.enum(domains.map((d) => d.id)).describe("Id do dom\xEDnio (ver list_domains)");
|
|
1185
1235
|
function extractContext(extra) {
|
|
1186
1236
|
const progressToken = extra._meta?.progressToken;
|
|
@@ -1195,7 +1245,7 @@ function extractContext(extra) {
|
|
|
1195
1245
|
}
|
|
1196
1246
|
};
|
|
1197
1247
|
}
|
|
1198
|
-
function createServer() {
|
|
1248
|
+
function createServer(refreshBudgetMs = REFRESH_BUDGET_MS) {
|
|
1199
1249
|
const server2 = new McpServer(
|
|
1200
1250
|
{ name: "opf-br-mcp", version: PACKAGE_VERSION },
|
|
1201
1251
|
{
|
|
@@ -1206,7 +1256,7 @@ function createServer() {
|
|
|
1206
1256
|
"list_domains",
|
|
1207
1257
|
{
|
|
1208
1258
|
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).",
|
|
1259
|
+
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
1260
|
annotations: {
|
|
1211
1261
|
readOnlyHint: true,
|
|
1212
1262
|
destructiveHint: false,
|
|
@@ -1216,11 +1266,14 @@ function createServer() {
|
|
|
1216
1266
|
},
|
|
1217
1267
|
async () => {
|
|
1218
1268
|
const out = domains.map((d) => {
|
|
1269
|
+
const shared = d.filterSet ? FILTER_SETS[d.filterSet] : void 0;
|
|
1270
|
+
const own = shared ? d.filters.filter((f) => !shared.some((s) => s.name === f.name && s.description === f.description)) : d.filters;
|
|
1219
1271
|
const base = {
|
|
1220
1272
|
id: d.id,
|
|
1221
1273
|
title: d.title,
|
|
1222
1274
|
description: d.description,
|
|
1223
|
-
|
|
1275
|
+
...d.filterSet ? { filterSet: d.filterSet } : {},
|
|
1276
|
+
...own.length > 0 ? { filters: own } : {},
|
|
1224
1277
|
...d.specVersion ? { specVersion: d.specVersion } : {}
|
|
1225
1278
|
};
|
|
1226
1279
|
if (d.live) {
|
|
@@ -1233,7 +1286,14 @@ function createServer() {
|
|
|
1233
1286
|
extractedAt: cached?.extractedAt ?? null
|
|
1234
1287
|
};
|
|
1235
1288
|
});
|
|
1236
|
-
|
|
1289
|
+
const usedSets = Object.fromEntries(
|
|
1290
|
+
Object.entries(FILTER_SETS).filter(([name]) => domains.some((d) => d.filterSet === name))
|
|
1291
|
+
);
|
|
1292
|
+
return text({
|
|
1293
|
+
server: { name: "opf-br-mcp", version: PACKAGE_VERSION },
|
|
1294
|
+
filterSets: usedSets,
|
|
1295
|
+
domains: out
|
|
1296
|
+
});
|
|
1237
1297
|
}
|
|
1238
1298
|
);
|
|
1239
1299
|
server2.registerTool(
|
|
@@ -1349,7 +1409,7 @@ function createServer() {
|
|
|
1349
1409
|
"refresh",
|
|
1350
1410
|
{
|
|
1351
1411
|
title: "Re-extrair fontes",
|
|
1352
|
-
description: "For\xE7a re-extra\xE7\xE3o das fontes p\xFAblicas (ignora o TTL de 72h do cache).
|
|
1412
|
+
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
1413
|
inputSchema: {
|
|
1354
1414
|
domain: domainIdSchema.optional().describe("Id do dom\xEDnio; omita para todos")
|
|
1355
1415
|
},
|
|
@@ -1365,7 +1425,13 @@ function createServer() {
|
|
|
1365
1425
|
(d) => !d.live && (!domain || d.id === domain)
|
|
1366
1426
|
);
|
|
1367
1427
|
const report = {};
|
|
1428
|
+
const pendentes = [];
|
|
1429
|
+
const inicio = Date.now();
|
|
1368
1430
|
for (const d of targets) {
|
|
1431
|
+
if (!domain && Date.now() - inicio > refreshBudgetMs) {
|
|
1432
|
+
pendentes.push(d.id);
|
|
1433
|
+
continue;
|
|
1434
|
+
}
|
|
1369
1435
|
try {
|
|
1370
1436
|
const { data } = await getDomainData(d, true, extractContext(extra));
|
|
1371
1437
|
report[d.id] = `ok: ${data.items.length} itens`;
|
|
@@ -1373,7 +1439,13 @@ function createServer() {
|
|
|
1373
1439
|
report[d.id] = `erro: ${err.message}`;
|
|
1374
1440
|
}
|
|
1375
1441
|
}
|
|
1376
|
-
return text(
|
|
1442
|
+
return text({
|
|
1443
|
+
atualizados: report,
|
|
1444
|
+
...pendentes.length > 0 ? {
|
|
1445
|
+
pendentes,
|
|
1446
|
+
nota: `Parei em ${refreshBudgetMs / 1e3}s para n\xE3o estourar o timeout do cliente. Chame refresh(domain) para cada id em pendentes.`
|
|
1447
|
+
} : {}
|
|
1448
|
+
});
|
|
1377
1449
|
}
|
|
1378
1450
|
);
|
|
1379
1451
|
return server2;
|