next-finance-mcp 0.5.0 → 0.5.2

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
@@ -29,6 +29,7 @@ export interface FiltroContas {
29
29
  export interface ResultadoContas {
30
30
  contas: Record<string, unknown>[];
31
31
  paginacao: PaginaMeta;
32
+ _fonte?: string;
32
33
  }
33
34
  export declare function listarContas(filtro?: FiltroContas): Promise<ResultadoContas>;
34
35
  export interface FiltroPlanoDeContas {
@@ -40,6 +41,12 @@ export declare function listarPlanoDeContas(filtro?: FiltroPlanoDeContas): Promi
40
41
  planos: Record<string, unknown>[];
41
42
  paginacao: PaginaMeta;
42
43
  }>;
44
+ export declare function listarCentrosDeCusto(busca?: string): Promise<{
45
+ centros: Record<string, unknown>[];
46
+ }>;
47
+ export declare function listarTiposConta(): Promise<{
48
+ tipos: Record<string, unknown>[];
49
+ }>;
43
50
  export interface FiltroLancamentos {
44
51
  idConta?: string;
45
52
  dataInicio?: string;
package/dist/client.js CHANGED
@@ -5,7 +5,8 @@
5
5
  import * as fs from "fs";
6
6
  import * as path from "path";
7
7
  import * as os from "os";
8
- const WEB_BASE = "https://finance.net.br";
8
+ const WEB_BASE = "https://finance.net.br"; // login e seleção de carteira (MVC)
9
+ const API_BASE = "https://api.finance.net.br"; // API REST (mesmo cookie de sessão)
9
10
  const SESSION_DIR = path.join(os.homedir(), ".next-finance-mcp");
10
11
  const SESSION_FILE = path.join(SESSION_DIR, "session.json");
11
12
  export function saveSession() {
@@ -91,9 +92,9 @@ async function webPost(path, body, contentType, followRedirect = true) {
91
92
  storeCookies(res.headers.getSetCookie?.() ?? [], "finance.net.br");
92
93
  return { status: res.status, text: await res.text(), location: res.headers.get("location") ?? undefined };
93
94
  }
94
- /** GET para a API REST (/api/*) mesmo cookie de sessão */
95
+ /** GET para a API REST — usa api.finance.net.br com o mesmo cookie de sessão */
95
96
  async function apiGet(path) {
96
- const res = await fetch(`${WEB_BASE}${path}`, {
97
+ const res = await fetch(`${API_BASE}${path}`, {
97
98
  headers: { ...BASE_HEADERS, Cookie: getCookieHeader("finance.net.br"), Accept: "application/json" },
98
99
  });
99
100
  storeCookies(res.headers.getSetCookie?.() ?? [], "finance.net.br");
@@ -101,9 +102,9 @@ async function apiGet(path) {
101
102
  throw new Error(`API GET ${path}: ${res.status}`);
102
103
  return res.json();
103
104
  }
104
- /** POST JSON para a API REST (/api/*) mesmo cookie de sessão */
105
+ /** POST JSON para a API REST — usa api.finance.net.br com o mesmo cookie de sessão */
105
106
  async function apiPost(path, body) {
106
- const res = await fetch(`${WEB_BASE}${path}`, {
107
+ const res = await fetch(`${API_BASE}${path}`, {
107
108
  method: "POST",
108
109
  headers: { ...BASE_HEADERS, Cookie: getCookieHeader("finance.net.br"),
109
110
  "Content-Type": "application/json", Accept: "application/json" },
@@ -194,27 +195,73 @@ export async function selecionarCarteira(idCarteira) {
194
195
  }
195
196
  return { sucesso: ok, mensagem: ok ? `Carteira ${idCarteira} selecionada.` : `Falha ao selecionar carteira (${resp.status}).` };
196
197
  }
198
+ /** Campos essenciais de conta — descarta metadados pesados */
199
+ function slimConta(c) {
200
+ if (!c || typeof c !== "object")
201
+ return {};
202
+ const o = c;
203
+ const KEEP = new Set(["Id", "Nome", "Descricao", "NomeConta", "IdTipoConta", "NomeTipoConta",
204
+ "Tipo", "Ativo", "Status", "Saldo", "Banco", "Agencia", "Numero", "Moeda"]);
205
+ const slim = {};
206
+ for (const [k, v] of Object.entries(o)) {
207
+ if (KEEP.has(k) && v !== null && typeof v !== "object")
208
+ slim[k] = v;
209
+ }
210
+ // Fallback: todos os primitivos se lista ficou vazia
211
+ if (Object.keys(slim).length === 0) {
212
+ for (const [k, v] of Object.entries(o)) {
213
+ if (v !== null && typeof v !== "object")
214
+ slim[k] = v;
215
+ }
216
+ }
217
+ return slim;
218
+ }
219
+ /** Tenta endpoints em ordem até encontrar um que responda com dados */
220
+ async function fetchContas() {
221
+ // Todos os endpoints REST agora apontam para api.finance.net.br via apiGet()
222
+ const tentativas = [
223
+ { path: "/api/Conta/ListarContasDeFormaLeve", tipo: "json" },
224
+ { path: "/api/Conta", tipo: "json" },
225
+ { path: "/api/Conta/TodasAsContas", tipo: "json" },
226
+ { path: "/ContaAjax/ListarTodos", tipo: "legacy" }, // fallback MVC legado
227
+ ];
228
+ for (const { path, tipo } of tentativas) {
229
+ try {
230
+ let raw;
231
+ if (tipo === "json") {
232
+ raw = await apiGet(path);
233
+ }
234
+ else {
235
+ const r = await webGet(path);
236
+ if (r.status !== 200)
237
+ continue;
238
+ raw = JSON.parse(r.text);
239
+ }
240
+ const arr = Array.isArray(raw)
241
+ ? raw
242
+ : (Object.values(raw).find(v => Array.isArray(v)) ?? []);
243
+ if (arr.length > 0)
244
+ return { contas: arr, fonte: path };
245
+ }
246
+ catch { /* tenta o próximo */ }
247
+ }
248
+ throw new Error("Nenhum endpoint de contas respondeu com dados. Verifique se a carteira está selecionada.");
249
+ }
197
250
  export async function listarContas(filtro = {}) {
198
- const raw = await apiGet("/api/Conta/ListarContasDeFormaLeve");
199
- let contas = Array.isArray(raw) ? raw : Object.values(raw).find(v => Array.isArray(v)) ?? [];
200
- // Filtro por nome
251
+ const { contas: raw, fonte } = await fetchContas();
252
+ let contas = raw.map(slimConta);
253
+ // Filtro por nome (busca em Nome, Descricao, NomeConta)
201
254
  if (filtro.busca?.trim()) {
202
255
  const termo = filtro.busca.trim().toLowerCase();
203
- contas = contas.filter(c => {
204
- const obj = c;
205
- return [obj["Nome"], obj["Descricao"], obj["NomeConta"]]
206
- .some(v => typeof v === "string" && v.toLowerCase().includes(termo));
207
- });
256
+ contas = contas.filter(c => [c["Nome"], c["Descricao"], c["NomeConta"]]
257
+ .some(v => typeof v === "string" && v.toLowerCase().includes(termo)));
208
258
  }
209
- // Filtro por IdTipoConta (campo correto conforme estrutura da API)
259
+ // Filtro por IdTipoConta
210
260
  if (filtro.idTipoConta && filtro.idTipoConta.length > 0) {
211
261
  const tipos = new Set(filtro.idTipoConta.map(String));
212
- contas = contas.filter(c => {
213
- const obj = c;
214
- return tipos.has(String(obj["IdTipoConta"]));
215
- });
262
+ contas = contas.filter(c => tipos.has(String(c["IdTipoConta"])));
216
263
  }
217
- // Paginação client-side (o endpoint leve retorna tudo de uma vez)
264
+ // Paginação client-side
218
265
  const pagina = Math.max(1, filtro.pagina ?? 1);
219
266
  const limite = Math.min(200, Math.max(1, filtro.limite ?? 50));
220
267
  const total = contas.length;
@@ -222,6 +269,7 @@ export async function listarContas(filtro = {}) {
222
269
  return {
223
270
  contas: contas.slice(inicio, inicio + limite),
224
271
  paginacao: { pagina, limite, total, paginas: Math.ceil(total / limite), tem_mais: inicio + limite < total },
272
+ _fonte: fonte,
225
273
  };
226
274
  }
227
275
  export async function listarPlanoDeContas(filtro = {}) {
@@ -244,6 +292,25 @@ export async function listarPlanoDeContas(filtro = {}) {
244
292
  paginacao: { pagina, limite, total, paginas: Math.ceil(total / limite), tem_mais: inicio + limite < total },
245
293
  };
246
294
  }
295
+ // ── Centro de Custos ─────────────────────────────────────────────────────────
296
+ export async function listarCentrosDeCusto(busca) {
297
+ const raw = await apiGet("/api/CentroDeCustos");
298
+ let centros = Array.isArray(raw) ? raw : Object.values(raw).find(v => Array.isArray(v)) ?? [];
299
+ if (busca?.trim()) {
300
+ const t = busca.trim().toLowerCase();
301
+ centros = centros.filter(c => {
302
+ const o = c;
303
+ return [o["Nome"], o["Descricao"]].some(v => typeof v === "string" && v.toLowerCase().includes(t));
304
+ });
305
+ }
306
+ return { centros: centros };
307
+ }
308
+ // ── Tipos de Conta ────────────────────────────────────────────────────────────
309
+ export async function listarTiposConta() {
310
+ const raw = await apiGet("/api/TipoConta");
311
+ const tipos = Array.isArray(raw) ? raw : Object.values(raw).find(v => Array.isArray(v)) ?? [];
312
+ return { tipos: tipos };
313
+ }
247
314
  function slimLancamento(l) {
248
315
  return {
249
316
  data: String(l["Data"] ?? "").slice(0, 10),
@@ -266,17 +333,37 @@ export async function buscarLancamentos(opts) {
266
333
  const dataFim = opts.dataFim ?? today.toISOString().slice(0, 10);
267
334
  const pagina = Math.max(1, opts.pagina ?? 1);
268
335
  const limite = Math.min(200, Math.max(1, opts.limite ?? 50));
269
- // Chama a API REST com paginação server-side nativa
270
- const body = {
271
- IntervaloDeDatas: { DataInicio: dataInicio, DataFim: dataFim },
272
- Pagina: pagina - 1, // API é 0-indexed
273
- RegistrosPorPagina: limite,
274
- ListarOrdenadoPelaDataDeModificacao: false,
275
- };
276
- if (opts.idConta)
277
- body["IdConta"] = opts.idConta;
278
- const resp = await apiPost("/api/Lancamento/Filtrar", body);
279
- let lancamentos = resp["Lancamentos"] ?? [];
336
+ let lancamentos;
337
+ // Tenta API REST primeiro; cai no endpoint legado se falhar
338
+ try {
339
+ const body = {
340
+ IntervaloDeDatas: { DataInicio: dataInicio, DataFim: dataFim },
341
+ Pagina: pagina - 1, // API é 0-indexed
342
+ RegistrosPorPagina: limite,
343
+ ListarOrdenadoPelaDataDeModificacao: false,
344
+ };
345
+ if (opts.idConta)
346
+ body["IdConta"] = opts.idConta;
347
+ const resp = await apiPost("/api/Lancamento/Filtrar", body);
348
+ lancamentos = resp["Lancamentos"] ?? [];
349
+ }
350
+ catch {
351
+ // Fallback: endpoint legado (form-urlencoded) — requer IdConta
352
+ if (!opts.idConta)
353
+ throw new Error("A API REST de lançamentos não está disponível e o endpoint legado requer id_conta. " +
354
+ "Informe um id_conta e tente novamente.");
355
+ const params = new URLSearchParams();
356
+ params.set("filtro[IdConta]", opts.idConta);
357
+ params.set("filtro[IntervaloDeDatas][DataInicio]", dataInicio);
358
+ params.set("filtro[IntervaloDeDatas][DataFim]", dataFim);
359
+ params.set("filtro[ListarOrdenadoPelaDataDeModificacao]", "false");
360
+ const r = await webPost("/LancamentoAjax/Filtrar", params.toString(), "application/x-www-form-urlencoded");
361
+ if (r.status !== 200)
362
+ throw new Error(`Erro ${r.status} ao buscar lançamentos.`);
363
+ const raw = JSON.parse(r.text);
364
+ lancamentos = Array.isArray(raw) ? raw
365
+ : (Object.values(raw).find(v => Array.isArray(v)) ?? []);
366
+ }
280
367
  // Filtros client-side (a API não filtra por plano/centro/tipo)
281
368
  const excluirTransf = opts.excluirTransferencias !== false; // default: true
282
369
  if (excluirTransf)
package/dist/index.js CHANGED
@@ -56,6 +56,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
56
56
  },
57
57
  },
58
58
  },
59
+ {
60
+ name: "listar_tipos_conta",
61
+ description: "Lista os tipos de conta disponíveis (corrente, poupança, cartão, investimento etc.) com seus IDs. Use para descobrir os IDs antes de filtrar listar_contas por tipo.",
62
+ inputSchema: { type: "object", properties: {} },
63
+ },
64
+ {
65
+ name: "listar_centros_de_custo",
66
+ description: "Lista os centros de custo da carteira. Use para descobrir nomes exatos antes de filtrar lançamentos por centro de custo.",
67
+ inputSchema: {
68
+ type: "object",
69
+ properties: {
70
+ busca: { type: "string", description: "Filtra pelo nome do centro de custo" },
71
+ },
72
+ },
73
+ },
59
74
  {
60
75
  name: "listar_plano_de_contas",
61
76
  description: "Lista o plano de contas da carteira (categorias de receita e despesa). " +
@@ -157,6 +172,21 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
157
172
  limite: args["limite"],
158
173
  }));
159
174
  }
175
+ case "listar_tipos_conta": {
176
+ const authErr = requireAuth();
177
+ if (authErr)
178
+ return authErr;
179
+ return ok(await client.listarTiposConta());
180
+ }
181
+ case "listar_centros_de_custo": {
182
+ const authErr = requireAuth();
183
+ if (authErr)
184
+ return authErr;
185
+ const walErr = requireWallet();
186
+ if (walErr)
187
+ return walErr;
188
+ return ok(await client.listarCentrosDeCusto(args["busca"]));
189
+ }
160
190
  case "listar_plano_de_contas": {
161
191
  const authErr = requireAuth();
162
192
  if (authErr)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-finance-mcp",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
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",