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 +7 -0
- package/dist/client.js +117 -30
- package/dist/index.js +30 -0
- package/package.json +1 -1
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
|
|
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(`${
|
|
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
|
|
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(`${
|
|
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
|
|
199
|
-
let contas =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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.
|
|
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",
|