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 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
- return { status: res.status, text: await res.text(), headers: res.headers };
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 res.json();
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 res.json();
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
- // Campos a exibir de conta sem IDs internos, sem Ativo (todas exibidas são ativas)
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
- const slim = {};
266
- // Preserva _id internamente (prefixo _ sinaliza "não exibir ao usuário")
267
- if (o["Id"] !== undefined)
268
- slim["_id"] = o["Id"];
269
- for (const [k, v] of Object.entries(o)) {
270
- if (CAMPOS_EXIBIR_CONTA.has(k) && v !== null && typeof v !== "object")
271
- slim[k] = v;
272
- }
273
- // Fallback: primitivos sem IDs se lista ficou vazia
274
- if (Object.keys(slim).filter(k => !k.startsWith("_")).length === 0) {
275
- for (const [k, v] of Object.entries(o)) {
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, fonte } = await fetchContas();
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
- // Filtro por nome (busca em Nome, Descricao, NomeConta)
362
+ // Nível 4 filtro por nome da conta
327
363
  if (filtro.busca?.trim()) {
328
- const termo = filtro.busca.trim().toLowerCase();
329
- contas = contas.filter(c => [c["Nome"], c["Descricao"], c["NomeConta"]]
330
- .some(v => typeof v === "string" && v.toLowerCase().includes(termo)));
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
- // Filtro por nome do tipo de conta (ex: "Cartão de Crédito", "Conta Corrente")
377
+ // Nível 3 filtro por tipo de conta (ex: "Conta Corrente", "Ações")
333
378
  if (filtro.tipoConta?.trim()) {
334
- const termo = filtro.tipoConta.trim().toLowerCase();
335
- contas = contas.filter(c => typeof c["NomeTipoConta"] === "string" &&
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
- idContaResolvido = String(encontrada["Id"] ?? encontrada["IdConta"] ?? "");
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
- "Use 'busca' para filtrar por nome (ex: 'Inter', 'Nubank'). " +
48
- "Use 'tipo_conta' para filtrar por tipo (ex: 'Cartão de Crédito', 'Conta Corrente').",
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
- tipo_conta: { type: "string", description: "Filtra pelo tipo de conta (ex: 'Cartão de Crédito', 'Conta Corrente', 'Poupança', 'Investimento')" },
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.2",
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",