next-finance-mcp 0.6.2 → 0.6.4
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/client.d.ts +2 -1
- package/dist/client.js +87 -42
- package/dist/index.js +9 -4
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export interface PaginaMeta {
|
|
|
24
24
|
}
|
|
25
25
|
export interface FiltroContas {
|
|
26
26
|
busca?: string;
|
|
27
|
+
moeda?: string;
|
|
28
|
+
portfolio?: string;
|
|
27
29
|
tipoConta?: string;
|
|
28
30
|
pagina?: number;
|
|
29
31
|
limite?: number;
|
|
@@ -31,7 +33,6 @@ export interface FiltroContas {
|
|
|
31
33
|
export interface ResultadoContas {
|
|
32
34
|
contas: Record<string, unknown>[];
|
|
33
35
|
paginacao: PaginaMeta;
|
|
34
|
-
_fonte?: string;
|
|
35
36
|
}
|
|
36
37
|
export declare function listarContas(filtro?: FiltroContas): Promise<ResultadoContas>;
|
|
37
38
|
export interface FiltroPlanoDeContas {
|
package/dist/client.js
CHANGED
|
@@ -70,14 +70,19 @@ const BASE_HEADERS = {
|
|
|
70
70
|
"Accept-Language": "pt-BR,pt;q=0.9,en;q=0.8",
|
|
71
71
|
};
|
|
72
72
|
/** GET para páginas web (login, seleção de carteira) */
|
|
73
|
-
async function webGet(path) {
|
|
73
|
+
async function webGet(path, checkSession = false) {
|
|
74
74
|
const res = await fetch(`${WEB_BASE}${path}`, {
|
|
75
75
|
headers: { ...BASE_HEADERS, Cookie: getCookieHeader("finance.net.br"),
|
|
76
76
|
Accept: "text/html,application/xhtml+xml,application/json,*/*;q=0.8" },
|
|
77
77
|
redirect: "follow",
|
|
78
78
|
});
|
|
79
79
|
storeCookies(res.headers.getSetCookie?.() ?? [], "finance.net.br");
|
|
80
|
-
|
|
80
|
+
const text = await res.text();
|
|
81
|
+
if (checkSession && (res.status === 401 || res.status === 403 ||
|
|
82
|
+
(res.url?.includes("/Login") && !path.includes("/Login")))) {
|
|
83
|
+
throwSessionExpired();
|
|
84
|
+
}
|
|
85
|
+
return { status: res.status, text, headers: res.headers };
|
|
81
86
|
}
|
|
82
87
|
/** POST form-urlencoded para fluxo de login/seleção de carteira */
|
|
83
88
|
async function webPost(path, body, contentType, followRedirect = true) {
|
|
@@ -92,15 +97,33 @@ async function webPost(path, body, contentType, followRedirect = true) {
|
|
|
92
97
|
storeCookies(res.headers.getSetCookie?.() ?? [], "finance.net.br");
|
|
93
98
|
return { status: res.status, text: await res.text(), location: res.headers.get("location") ?? undefined };
|
|
94
99
|
}
|
|
100
|
+
/** Detecta se o servidor está sinalizando sessão expirada */
|
|
101
|
+
function isSessionExpired(status, text) {
|
|
102
|
+
if (status === 401 || status === 403)
|
|
103
|
+
return true;
|
|
104
|
+
// O servidor pode retornar 200 com redirect HTML para /Login
|
|
105
|
+
if (status === 200 && text.includes("/Login"))
|
|
106
|
+
return true;
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
function throwSessionExpired() {
|
|
110
|
+
_loggedIn = false;
|
|
111
|
+
_selectedWallet = null;
|
|
112
|
+
clearCookies();
|
|
113
|
+
throw new Error("Sessão expirada. Use a ferramenta `login` para autenticar novamente.");
|
|
114
|
+
}
|
|
95
115
|
/** GET para a API REST — usa api.finance.net.br com o mesmo cookie de sessão */
|
|
96
116
|
async function apiGet(path) {
|
|
97
117
|
const res = await fetch(`${API_BASE}${path}`, {
|
|
98
118
|
headers: { ...BASE_HEADERS, Cookie: getCookieHeader("finance.net.br"), Accept: "application/json" },
|
|
99
119
|
});
|
|
100
120
|
storeCookies(res.headers.getSetCookie?.() ?? [], "finance.net.br");
|
|
121
|
+
const text = await res.text();
|
|
122
|
+
if (isSessionExpired(res.status, text))
|
|
123
|
+
throwSessionExpired();
|
|
101
124
|
if (!res.ok)
|
|
102
125
|
throw new Error(`API GET ${path}: ${res.status}`);
|
|
103
|
-
return
|
|
126
|
+
return JSON.parse(text);
|
|
104
127
|
}
|
|
105
128
|
/** POST JSON para a API REST — usa api.finance.net.br com o mesmo cookie de sessão */
|
|
106
129
|
async function apiPost(path, body) {
|
|
@@ -111,9 +134,12 @@ async function apiPost(path, body) {
|
|
|
111
134
|
body: JSON.stringify(body),
|
|
112
135
|
});
|
|
113
136
|
storeCookies(res.headers.getSetCookie?.() ?? [], "finance.net.br");
|
|
137
|
+
const text = await res.text();
|
|
138
|
+
if (isSessionExpired(res.status, text))
|
|
139
|
+
throwSessionExpired();
|
|
114
140
|
if (!res.ok)
|
|
115
141
|
throw new Error(`API POST ${path}: ${res.status}`);
|
|
116
|
-
return
|
|
142
|
+
return JSON.parse(text);
|
|
117
143
|
}
|
|
118
144
|
function extractCsrf(html) {
|
|
119
145
|
return html.match(/name="__RequestVerificationToken"[^>]+value="([^"]+)"/)?.[1] ?? "";
|
|
@@ -252,39 +278,49 @@ export async function selecionarCarteira(nomeCarteira) {
|
|
|
252
278
|
const nomeExibir = String(encontrada["NomeCarteira"] ?? nomeCarteira);
|
|
253
279
|
return { sucesso: ok, mensagem: ok ? `Carteira "${nomeExibir}" selecionada.` : `Falha ao selecionar carteira (${resp.status}).` };
|
|
254
280
|
}
|
|
255
|
-
|
|
256
|
-
const CAMPOS_EXIBIR_CONTA = new Set([
|
|
257
|
-
"Nome", "Descricao", "NomeConta", "NomeTipoConta",
|
|
258
|
-
"Saldo", "Banco", "Agencia", "Numero", "Moeda",
|
|
259
|
-
]);
|
|
260
|
-
/** Slim para exibição — sem IDs. Preserva Id internamente via _id para uso do buscarLancamentos. */
|
|
281
|
+
/** Slim para exibição — captura os 4 níveis hierárquicos, sem IDs. */
|
|
261
282
|
function slimConta(c) {
|
|
262
283
|
if (!c || typeof c !== "object")
|
|
263
284
|
return {};
|
|
264
285
|
const o = c;
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (
|
|
275
|
-
|
|
276
|
-
if (v !== null && typeof v !== "object" && !k.startsWith("Id") && k !== "id")
|
|
277
|
-
slim[k] = v;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
// Extrai NomeTipoConta de objeto aninhado se necessário
|
|
281
|
-
if (!slim["NomeTipoConta"] && o["TipoConta"] && typeof o["TipoConta"] === "object") {
|
|
282
|
-
const tc = o["TipoConta"];
|
|
283
|
-
if (tc["Nome"])
|
|
284
|
-
slim["NomeTipoConta"] = tc["Nome"];
|
|
285
|
-
if (tc["Id"] && !slim["_idTipoConta"])
|
|
286
|
-
slim["_idTipoConta"] = tc["Id"];
|
|
286
|
+
// IDs preservados internamente com prefixo _ (nunca exibidos ao usuário)
|
|
287
|
+
const _id = o["Id"] ?? o["IdConta"] ?? o["id"] ?? o["idConta"];
|
|
288
|
+
const _idTipo = o["IdTipoConta"];
|
|
289
|
+
// Nível 1 — Moeda (ex: "BRL", "USD")
|
|
290
|
+
const moeda = String(o["NomeMoeda"] ?? o["Moeda"] ?? o["SiglaMoeda"] ?? o["CodigoMoeda"] ?? "").trim();
|
|
291
|
+
// Nível 2 — Portfolio (ex: "Tesouraria", "Renda Variável", "Previdência")
|
|
292
|
+
const portfolio = String(o["NomePortfolio"] ?? o["Portfolio"] ?? o["NomeGrupo"] ?? o["Grupo"] ?? "").trim();
|
|
293
|
+
// Nível 3 — Tipo de Conta (ex: "Conta Corrente", "Ações", "CDB")
|
|
294
|
+
let tipoConta = String(o["NomeTipoConta"] ?? "").trim();
|
|
295
|
+
if (!tipoConta && o["TipoConta"] && typeof o["TipoConta"] === "object") {
|
|
296
|
+
tipoConta = String(o["TipoConta"]["Nome"] ?? "").trim();
|
|
287
297
|
}
|
|
298
|
+
// Nível 4 — Nome da Conta
|
|
299
|
+
const nome = String(o["Nome"] ?? o["NomeConta"] ?? o["Descricao"] ?? "").trim();
|
|
300
|
+
const slim = {};
|
|
301
|
+
// Campos internos (nunca retornados ao usuário final, usados pelo MCP)
|
|
302
|
+
if (_id !== undefined)
|
|
303
|
+
slim["_id"] = _id;
|
|
304
|
+
if (_idTipo !== undefined)
|
|
305
|
+
slim["_idTipo"] = _idTipo;
|
|
306
|
+
// Campos exibíveis
|
|
307
|
+
if (moeda)
|
|
308
|
+
slim["moeda"] = moeda;
|
|
309
|
+
if (portfolio)
|
|
310
|
+
slim["portfolio"] = portfolio;
|
|
311
|
+
if (tipoConta)
|
|
312
|
+
slim["tipo_conta"] = tipoConta;
|
|
313
|
+
if (nome)
|
|
314
|
+
slim["nome"] = nome;
|
|
315
|
+
// Campos financeiros extras se disponíveis
|
|
316
|
+
if (o["Saldo"] !== undefined && o["Saldo"] !== null)
|
|
317
|
+
slim["saldo"] = o["Saldo"];
|
|
318
|
+
if (o["Banco"] !== undefined && typeof o["Banco"] === "string")
|
|
319
|
+
slim["banco"] = o["Banco"];
|
|
320
|
+
if (o["Agencia"] !== undefined && typeof o["Agencia"] === "string")
|
|
321
|
+
slim["agencia"] = o["Agencia"];
|
|
322
|
+
if (o["Numero"] !== undefined && typeof o["Numero"] === "string")
|
|
323
|
+
slim["numero"] = o["Numero"];
|
|
288
324
|
return slim;
|
|
289
325
|
}
|
|
290
326
|
/** Tenta endpoints em ordem até encontrar um que responda com dados */
|
|
@@ -319,21 +355,29 @@ async function fetchContas() {
|
|
|
319
355
|
throw new Error("Nenhum endpoint de contas respondeu com dados. Verifique se a carteira está selecionada.");
|
|
320
356
|
}
|
|
321
357
|
export async function listarContas(filtro = {}) {
|
|
322
|
-
const { contas: raw
|
|
358
|
+
const { contas: raw } = await fetchContas();
|
|
323
359
|
// Apenas contas ativas (Ativo === true no cadastro)
|
|
324
360
|
const ativas = raw.filter(c => c["Ativo"] !== false);
|
|
325
361
|
let contas = ativas.map(slimConta);
|
|
326
|
-
//
|
|
362
|
+
// Nível 4 — filtro por nome da conta
|
|
327
363
|
if (filtro.busca?.trim()) {
|
|
328
|
-
const
|
|
329
|
-
contas = contas.filter(c =>
|
|
330
|
-
|
|
364
|
+
const t = filtro.busca.trim().toLowerCase();
|
|
365
|
+
contas = contas.filter(c => String(c["nome"] ?? "").toLowerCase().includes(t));
|
|
366
|
+
}
|
|
367
|
+
// Nível 1 — filtro por moeda (ex: "BRL", "USD")
|
|
368
|
+
if (filtro.moeda?.trim()) {
|
|
369
|
+
const t = filtro.moeda.trim().toLowerCase();
|
|
370
|
+
contas = contas.filter(c => String(c["moeda"] ?? "").toLowerCase().includes(t));
|
|
371
|
+
}
|
|
372
|
+
// Nível 2 — filtro por portfolio (ex: "Renda Variável", "Tesouraria")
|
|
373
|
+
if (filtro.portfolio?.trim()) {
|
|
374
|
+
const t = filtro.portfolio.trim().toLowerCase();
|
|
375
|
+
contas = contas.filter(c => String(c["portfolio"] ?? "").toLowerCase().includes(t));
|
|
331
376
|
}
|
|
332
|
-
//
|
|
377
|
+
// Nível 3 — filtro por tipo de conta (ex: "Conta Corrente", "Ações")
|
|
333
378
|
if (filtro.tipoConta?.trim()) {
|
|
334
|
-
const
|
|
335
|
-
contas = contas.filter(c =>
|
|
336
|
-
c["NomeTipoConta"].toLowerCase().includes(termo));
|
|
379
|
+
const t = filtro.tipoConta.trim().toLowerCase();
|
|
380
|
+
contas = contas.filter(c => String(c["tipo_conta"] ?? "").toLowerCase().includes(t));
|
|
337
381
|
}
|
|
338
382
|
// Paginação client-side
|
|
339
383
|
const pagina = Math.max(1, filtro.pagina ?? 1);
|
|
@@ -502,7 +546,8 @@ export async function buscarLancamentos(opts) {
|
|
|
502
546
|
.find(c => String(c["Nome"] ?? c["NomeConta"] ?? "").toLowerCase().includes(termo));
|
|
503
547
|
if (!encontrada)
|
|
504
548
|
throw new Error(`Conta "${opts.nomeConta}" não encontrada entre as contas ativas. Use listar_contas para ver as contas disponíveis.`);
|
|
505
|
-
|
|
549
|
+
// Tenta todos os nomes possíveis de campo ID na resposta da API
|
|
550
|
+
idContaResolvido = String(encontrada["Id"] ?? encontrada["IdConta"] ?? encontrada["id"] ?? encontrada["idConta"] ?? "");
|
|
506
551
|
}
|
|
507
552
|
let lancamentos;
|
|
508
553
|
// Tenta API REST primeiro; cai no endpoint legado se falhar
|
package/dist/index.js
CHANGED
|
@@ -44,13 +44,16 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
44
44
|
{
|
|
45
45
|
name: "listar_contas",
|
|
46
46
|
description: "Lista as contas ativas da carteira selecionada. " +
|
|
47
|
-
"
|
|
48
|
-
"
|
|
47
|
+
"As contas seguem hierarquia de 4 níveis: Moeda → Portfolio → Tipo de Conta → Conta. " +
|
|
48
|
+
"Portfolios: Liquidez, Previdência, Renda Fixa, Renda Variável, Tesouraria. " +
|
|
49
|
+
"Use os filtros para navegar nessa hierarquia.",
|
|
49
50
|
inputSchema: {
|
|
50
51
|
type: "object",
|
|
51
52
|
properties: {
|
|
52
|
-
busca: { type: "string", description: "Filtra pelo nome da conta (ex: 'Inter', 'Bradesco')" },
|
|
53
|
-
|
|
53
|
+
busca: { type: "string", description: "Filtra pelo nome da conta (ex: 'Inter', 'Bradesco', 'PETR4')" },
|
|
54
|
+
moeda: { type: "string", description: "Filtra pela moeda (ex: 'BRL', 'USD', 'EUR')" },
|
|
55
|
+
portfolio: { type: "string", description: "Filtra pelo portfolio (ex: 'Tesouraria', 'Renda Variável', 'Previdência', 'Liquidez')" },
|
|
56
|
+
tipo_conta: { type: "string", description: "Filtra pelo tipo de conta (ex: 'Conta Corrente', 'Cartão de Crédito', 'Ações', 'CDB', 'Fundo de Previdência')" },
|
|
54
57
|
pagina: { type: "number", description: "Página (começa em 1, padrão: 1)" },
|
|
55
58
|
limite: { type: "number", description: "Contas por página (padrão: 50, máximo: 200)" },
|
|
56
59
|
},
|
|
@@ -185,6 +188,8 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
185
188
|
return walErr;
|
|
186
189
|
return ok(await client.listarContas({
|
|
187
190
|
busca: args["busca"],
|
|
191
|
+
moeda: args["moeda"],
|
|
192
|
+
portfolio: args["portfolio"],
|
|
188
193
|
tipoConta: args["tipo_conta"],
|
|
189
194
|
pagina: args["pagina"],
|
|
190
195
|
limite: args["limite"],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "next-finance-mcp",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
4
4
|
"mcpName": "io.github.paivapiovesan/next-finance",
|
|
5
5
|
"description": "MCP Server para o NEXT Finance (finance.net.br) — login via browser, listagem de carteiras, contas e lançamentos",
|
|
6
6
|
"type": "module",
|