next-finance-mcp 0.3.2 → 0.4.1
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 +36 -3
- package/dist/client.js +144 -15
- package/dist/finance-client.d.ts +42 -0
- package/dist/finance-client.js +151 -0
- package/dist/http-index.d.ts +13 -0
- package/dist/http-index.js +235 -0
- package/dist/index.js +27 -6
- package/dist/user-store.d.ts +30 -0
- package/dist/user-store.js +110 -0
- package/package.json +7 -3
package/dist/client.d.ts
CHANGED
|
@@ -13,9 +13,42 @@ export declare function login(email: string, senha: string): Promise<Record<stri
|
|
|
13
13
|
export declare function tryRestoreSession(): Promise<boolean>;
|
|
14
14
|
export declare function listarCarteiras(): Promise<unknown>;
|
|
15
15
|
export declare function selecionarCarteira(idCarteira: string): Promise<Record<string, unknown>>;
|
|
16
|
-
export
|
|
17
|
-
|
|
16
|
+
export interface FiltroContas {
|
|
17
|
+
busca?: string;
|
|
18
|
+
tipos?: number[];
|
|
19
|
+
pagina?: number;
|
|
20
|
+
limite?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface PaginaMeta {
|
|
23
|
+
pagina: number;
|
|
24
|
+
limite: number;
|
|
25
|
+
total: number;
|
|
26
|
+
paginas: number;
|
|
27
|
+
tem_mais: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface ResultadoContas {
|
|
30
|
+
contas: Record<string, unknown>[];
|
|
31
|
+
paginacao: PaginaMeta;
|
|
32
|
+
_debug?: {
|
|
33
|
+
estrutura: string;
|
|
34
|
+
campos_exemplo: string[];
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export declare function listarContas(filtro?: FiltroContas): Promise<ResultadoContas>;
|
|
38
|
+
export interface FiltroLancamentos {
|
|
18
39
|
idConta: string;
|
|
19
40
|
dataInicio?: string;
|
|
20
41
|
dataFim?: string;
|
|
21
|
-
|
|
42
|
+
busca?: string;
|
|
43
|
+
pagina?: number;
|
|
44
|
+
limite?: number;
|
|
45
|
+
}
|
|
46
|
+
export interface ResultadoLancamentos {
|
|
47
|
+
lancamentos: unknown[];
|
|
48
|
+
paginacao: PaginaMeta;
|
|
49
|
+
periodo: {
|
|
50
|
+
inicio: string;
|
|
51
|
+
fim: string;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export declare function buscarLancamentos(opts: FiltroLancamentos): Promise<ResultadoLancamentos>;
|
package/dist/client.js
CHANGED
|
@@ -181,31 +181,160 @@ export async function selecionarCarteira(idCarteira) {
|
|
|
181
181
|
}
|
|
182
182
|
return { sucesso: ok, mensagem: ok ? `Carteira ${idCarteira} selecionada.` : `Falha ao selecionar carteira (${resp.status}).` };
|
|
183
183
|
}
|
|
184
|
-
|
|
185
|
-
|
|
184
|
+
/**
|
|
185
|
+
* Extrai o array de contas de qualquer estrutura de resposta da API.
|
|
186
|
+
* Tenta array direto, depois chaves comuns de envelope, depois primeira array encontrada.
|
|
187
|
+
*/
|
|
188
|
+
function extrairArray(data) {
|
|
189
|
+
if (Array.isArray(data))
|
|
190
|
+
return data;
|
|
191
|
+
if (data && typeof data === "object") {
|
|
192
|
+
const obj = data;
|
|
193
|
+
// Chaves comuns de envelope em APIs C#/EF
|
|
194
|
+
for (const key of ["data", "items", "contas", "resultado", "list", "records", "Data", "Items", "Contas", "Resultado", "Lista", "lista", "value", "Value"]) {
|
|
195
|
+
if (Array.isArray(obj[key]))
|
|
196
|
+
return obj[key];
|
|
197
|
+
}
|
|
198
|
+
// Fallback: primeira propriedade que seja array
|
|
199
|
+
for (const val of Object.values(obj)) {
|
|
200
|
+
if (Array.isArray(val))
|
|
201
|
+
return val;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Retorna apenas campos essenciais de uma conta, descartando metadados pesados.
|
|
208
|
+
* Usa allowlist por padrão de nome (case-insensitive) — valores primitivos apenas.
|
|
209
|
+
*/
|
|
210
|
+
function enxugarConta(conta) {
|
|
211
|
+
if (!conta || typeof conta !== "object")
|
|
212
|
+
return {};
|
|
213
|
+
const obj = conta;
|
|
214
|
+
// Padrões que identificam campos essenciais de conta
|
|
215
|
+
const KEEP = [/id/i, /nome/i, /descri/i, /tipo/i, /ativo/i, /status/i, /situac/i,
|
|
216
|
+
/banco/i, /agenci/i, /saldo/i, /codigo/i, /numero/i, /moeda/i, /cartei/i];
|
|
217
|
+
const slim = {};
|
|
218
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
219
|
+
// só primitivos (sem objetos/arrays aninhados) e com nome relevante
|
|
220
|
+
if (v !== null && typeof v !== "object" && KEEP.some(p => p.test(k))) {
|
|
221
|
+
slim[k] = v;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Fallback: se nada foi selecionado, devolve todos os primitivos (melhor que vazio)
|
|
225
|
+
if (Object.keys(slim).length === 0) {
|
|
226
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
227
|
+
if (v !== null && typeof v !== "object")
|
|
228
|
+
slim[k] = v;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return slim;
|
|
232
|
+
}
|
|
233
|
+
export async function listarContas(filtro = {}) {
|
|
186
234
|
const resp = await webGet("/ContaAjax/ListarTodos");
|
|
187
235
|
if (resp.status !== 200)
|
|
188
236
|
throw new Error(`Erro ${resp.status} ao listar contas.`);
|
|
189
|
-
|
|
237
|
+
const raw = JSON.parse(resp.text);
|
|
238
|
+
// 1. Extrai array independente da estrutura de envelope
|
|
239
|
+
const estrutura = Array.isArray(raw) ? "array_direto"
|
|
240
|
+
: (typeof raw === "object" ? `objeto{${Object.keys(raw).join(",")}}` : typeof raw);
|
|
241
|
+
let contas = extrairArray(raw);
|
|
242
|
+
// 2. Enxuga campos ANTES de filtrar/paginar (resolve o estouro de 1MB)
|
|
243
|
+
let contasSlim = contas.map(enxugarConta);
|
|
244
|
+
// 3. Filtro por busca textual em todos os campos string da versão enxuta
|
|
245
|
+
if (filtro.busca?.trim()) {
|
|
246
|
+
const termo = filtro.busca.trim().toLowerCase();
|
|
247
|
+
contasSlim = contasSlim.filter(c => Object.values(c)
|
|
248
|
+
.filter(v => typeof v === "string")
|
|
249
|
+
.some(v => v.toLowerCase().includes(termo)));
|
|
250
|
+
}
|
|
251
|
+
// 4. Filtro por tipo — tenta todos os campos cujo nome contenha "tipo" (string ou número)
|
|
252
|
+
if (filtro.tipos && filtro.tipos.length > 0) {
|
|
253
|
+
const tiposStr = new Set(filtro.tipos.map(String));
|
|
254
|
+
contasSlim = contasSlim.filter(c => {
|
|
255
|
+
for (const [k, v] of Object.entries(c)) {
|
|
256
|
+
if (/tipo/i.test(k) && v != null && tiposStr.has(String(v)))
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
return false;
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
// 5. Paginação
|
|
263
|
+
const pagina = Math.max(1, filtro.pagina ?? 1);
|
|
264
|
+
const limite = Math.min(200, Math.max(1, filtro.limite ?? 30));
|
|
265
|
+
const total = contasSlim.length;
|
|
266
|
+
const inicio = (pagina - 1) * limite;
|
|
267
|
+
const slice = contasSlim.slice(inicio, inicio + limite);
|
|
268
|
+
// _debug: mostra estrutura e campos da 1ª conta — útil para diagnosticar filtro por tipo
|
|
269
|
+
const primeiraRaw = contas[0] && typeof contas[0] === "object"
|
|
270
|
+
? Object.keys(contas[0])
|
|
271
|
+
: [];
|
|
272
|
+
return {
|
|
273
|
+
contas: slice,
|
|
274
|
+
paginacao: { pagina, limite, total, paginas: Math.ceil(total / limite), tem_mais: inicio + limite < total },
|
|
275
|
+
_debug: { estrutura, campos_exemplo: primeiraRaw.slice(0, 30) },
|
|
276
|
+
};
|
|
190
277
|
}
|
|
191
|
-
// ── Lançamentos ─────────────────────────────────────────────────────────────
|
|
192
|
-
//
|
|
193
|
-
// O endpoint usa jQuery $.ajax padrão → form-urlencoded com chaves aninhadas:
|
|
194
|
-
// filtro[IdConta]=X&filtro[IntervaloDeDatas][DataInicio]=Y&filtro[IntervaloDeDatas][DataFim]=Z
|
|
195
|
-
//
|
|
196
|
-
// IdConta é OBRIGATÓRIO (cada chamada é por conta).
|
|
197
278
|
export async function buscarLancamentos(opts) {
|
|
198
|
-
// DataInicio e DataFim são obrigatórios pela API; usar mês atual como default
|
|
199
279
|
const today = new Date();
|
|
200
|
-
const
|
|
201
|
-
const
|
|
280
|
+
const dataInicio = opts.dataInicio ?? `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-01`;
|
|
281
|
+
const dataFim = opts.dataFim ?? today.toISOString().slice(0, 10);
|
|
202
282
|
const params = new URLSearchParams();
|
|
203
283
|
params.set("filtro[IdConta]", opts.idConta);
|
|
204
|
-
params.set("filtro[IntervaloDeDatas][DataInicio]",
|
|
205
|
-
params.set("filtro[IntervaloDeDatas][DataFim]",
|
|
284
|
+
params.set("filtro[IntervaloDeDatas][DataInicio]", dataInicio);
|
|
285
|
+
params.set("filtro[IntervaloDeDatas][DataFim]", dataFim);
|
|
206
286
|
params.set("filtro[ListarOrdenadoPelaDataDeModificacao]", "false");
|
|
207
287
|
const resp = await webPost("/LancamentoAjax/Filtrar", params.toString(), "application/x-www-form-urlencoded");
|
|
208
288
|
if (resp.status !== 200)
|
|
209
289
|
throw new Error(`Erro ${resp.status}: ${resp.text.slice(0, 200)}`);
|
|
210
|
-
|
|
290
|
+
const raw = JSON.parse(resp.text);
|
|
291
|
+
// Extrai array independente do envelope
|
|
292
|
+
let lancamentos = extrairArray(raw);
|
|
293
|
+
// Enxuga campos: mantém apenas essenciais de lançamento
|
|
294
|
+
const KEEP_LANC = [/id/i, /data/i, /valor/i, /descri/i, /histor/i, /observ/i,
|
|
295
|
+
/categ/i, /plano/i, /conta/i, /tipo/i, /status/i, /situac/i,
|
|
296
|
+
/document/i, /numero/i, /compet/i, /vencim/i];
|
|
297
|
+
let lancsSlim = lancamentos.map(l => {
|
|
298
|
+
if (!l || typeof l !== "object")
|
|
299
|
+
return {};
|
|
300
|
+
const obj = l;
|
|
301
|
+
const slim = {};
|
|
302
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
303
|
+
if (v !== null && typeof v !== "object" && KEEP_LANC.some(p => p.test(k))) {
|
|
304
|
+
slim[k] = v;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// Fallback: todos os primitivos se nada foi capturado
|
|
308
|
+
if (Object.keys(slim).length === 0) {
|
|
309
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
310
|
+
if (v !== null && typeof v !== "object")
|
|
311
|
+
slim[k] = v;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return slim;
|
|
315
|
+
});
|
|
316
|
+
// Filtro por busca textual em todos os campos string
|
|
317
|
+
if (opts.busca?.trim()) {
|
|
318
|
+
const termo = opts.busca.trim().toLowerCase();
|
|
319
|
+
lancsSlim = lancsSlim.filter(l => Object.values(l)
|
|
320
|
+
.filter(v => typeof v === "string")
|
|
321
|
+
.some(v => v.toLowerCase().includes(termo)));
|
|
322
|
+
}
|
|
323
|
+
// Paginação
|
|
324
|
+
const pagina = Math.max(1, opts.pagina ?? 1);
|
|
325
|
+
const limite = Math.min(500, Math.max(1, opts.limite ?? 50));
|
|
326
|
+
const total = lancsSlim.length;
|
|
327
|
+
const inicio = (pagina - 1) * limite;
|
|
328
|
+
const slice = lancsSlim.slice(inicio, inicio + limite);
|
|
329
|
+
return {
|
|
330
|
+
lancamentos: slice,
|
|
331
|
+
paginacao: {
|
|
332
|
+
pagina,
|
|
333
|
+
limite,
|
|
334
|
+
total,
|
|
335
|
+
paginas: Math.ceil(total / limite),
|
|
336
|
+
tem_mais: inicio + limite < total,
|
|
337
|
+
},
|
|
338
|
+
periodo: { inicio: dataInicio, fim: dataFim },
|
|
339
|
+
};
|
|
211
340
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cliente HTTP para o NEXT Finance — instanciável por usuário (multi-tenant).
|
|
3
|
+
*/
|
|
4
|
+
interface Cookie {
|
|
5
|
+
name: string;
|
|
6
|
+
value: string;
|
|
7
|
+
domain: string;
|
|
8
|
+
}
|
|
9
|
+
export declare class NextFinanceClient {
|
|
10
|
+
private cookieStore;
|
|
11
|
+
private _loggedIn;
|
|
12
|
+
private _selectedWallet;
|
|
13
|
+
private storeCookies;
|
|
14
|
+
private getCookieHeader;
|
|
15
|
+
getCookies(): Cookie[];
|
|
16
|
+
setCookies(cookies: Cookie[]): void;
|
|
17
|
+
getSelectedWallet(): string | null;
|
|
18
|
+
setSelectedWallet(id: string | null): void;
|
|
19
|
+
isAuthenticated(): boolean;
|
|
20
|
+
logout(): void;
|
|
21
|
+
private webGet;
|
|
22
|
+
private webPost;
|
|
23
|
+
private extractCsrf;
|
|
24
|
+
login(email: string, senha: string): Promise<{
|
|
25
|
+
sucesso: boolean;
|
|
26
|
+
mensagem: string;
|
|
27
|
+
}>;
|
|
28
|
+
/** Verifica se a sessão ainda funciona fazendo uma chamada leve. */
|
|
29
|
+
checkSession(): Promise<boolean>;
|
|
30
|
+
listarCarteiras(): Promise<unknown>;
|
|
31
|
+
selecionarCarteira(idCarteira: string): Promise<{
|
|
32
|
+
sucesso: boolean;
|
|
33
|
+
mensagem: string;
|
|
34
|
+
}>;
|
|
35
|
+
listarContas(): Promise<unknown>;
|
|
36
|
+
buscarLancamentos(opts: {
|
|
37
|
+
idConta: string;
|
|
38
|
+
dataInicio?: string;
|
|
39
|
+
dataFim?: string;
|
|
40
|
+
}): Promise<unknown>;
|
|
41
|
+
}
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cliente HTTP para o NEXT Finance — instanciável por usuário (multi-tenant).
|
|
3
|
+
*/
|
|
4
|
+
const WEB_BASE = "https://finance.net.br";
|
|
5
|
+
const BASE_HEADERS = {
|
|
6
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
|
7
|
+
"Accept-Language": "pt-BR,pt;q=0.9,en;q=0.8",
|
|
8
|
+
};
|
|
9
|
+
export class NextFinanceClient {
|
|
10
|
+
cookieStore = [];
|
|
11
|
+
_loggedIn = false;
|
|
12
|
+
_selectedWallet = null;
|
|
13
|
+
// ── Cookie store ────────────────────────────────────────────────────────────
|
|
14
|
+
storeCookies(setCookieHeaders, domain) {
|
|
15
|
+
for (const header of setCookieHeaders) {
|
|
16
|
+
const pair = header.split(";")[0].trim();
|
|
17
|
+
const eqIdx = pair.indexOf("=");
|
|
18
|
+
if (eqIdx < 0)
|
|
19
|
+
continue;
|
|
20
|
+
const name = pair.slice(0, eqIdx).trim();
|
|
21
|
+
const value = pair.slice(eqIdx + 1).trim();
|
|
22
|
+
const idx = this.cookieStore.findIndex(c => c.name === name && c.domain === domain);
|
|
23
|
+
if (idx >= 0)
|
|
24
|
+
this.cookieStore[idx].value = value;
|
|
25
|
+
else
|
|
26
|
+
this.cookieStore.push({ name, value, domain });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
getCookieHeader(domain) {
|
|
30
|
+
return this.cookieStore
|
|
31
|
+
.filter(c => domain.includes(c.domain) || c.domain.includes(domain))
|
|
32
|
+
.map(c => `${c.name}=${c.value}`)
|
|
33
|
+
.join("; ");
|
|
34
|
+
}
|
|
35
|
+
getCookies() { return [...this.cookieStore]; }
|
|
36
|
+
setCookies(cookies) { this.cookieStore = [...cookies]; }
|
|
37
|
+
getSelectedWallet() { return this._selectedWallet; }
|
|
38
|
+
setSelectedWallet(id) { this._selectedWallet = id; }
|
|
39
|
+
// ── Estado ──────────────────────────────────────────────────────────────────
|
|
40
|
+
isAuthenticated() { return this._loggedIn; }
|
|
41
|
+
logout() {
|
|
42
|
+
this.cookieStore = [];
|
|
43
|
+
this._loggedIn = false;
|
|
44
|
+
this._selectedWallet = null;
|
|
45
|
+
}
|
|
46
|
+
// ── HTTP helpers ────────────────────────────────────────────────────────────
|
|
47
|
+
async webGet(path) {
|
|
48
|
+
const res = await fetch(`${WEB_BASE}${path}`, {
|
|
49
|
+
headers: {
|
|
50
|
+
...BASE_HEADERS,
|
|
51
|
+
Cookie: this.getCookieHeader("finance.net.br"),
|
|
52
|
+
Accept: "text/html,application/xhtml+xml,application/json,*/*;q=0.8",
|
|
53
|
+
},
|
|
54
|
+
redirect: "follow",
|
|
55
|
+
});
|
|
56
|
+
this.storeCookies(res.headers.getSetCookie?.() ?? [], "finance.net.br");
|
|
57
|
+
return { status: res.status, text: await res.text() };
|
|
58
|
+
}
|
|
59
|
+
async webPost(path, body, contentType, followRedirect = true) {
|
|
60
|
+
const res = await fetch(`${WEB_BASE}${path}`, {
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: {
|
|
63
|
+
...BASE_HEADERS,
|
|
64
|
+
Cookie: this.getCookieHeader("finance.net.br"),
|
|
65
|
+
"Content-Type": contentType,
|
|
66
|
+
Accept: "application/json, text/html, */*",
|
|
67
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
68
|
+
},
|
|
69
|
+
body,
|
|
70
|
+
redirect: followRedirect ? "follow" : "manual",
|
|
71
|
+
});
|
|
72
|
+
this.storeCookies(res.headers.getSetCookie?.() ?? [], "finance.net.br");
|
|
73
|
+
return { status: res.status, text: await res.text(), location: res.headers.get("location") ?? undefined };
|
|
74
|
+
}
|
|
75
|
+
extractCsrf(html) {
|
|
76
|
+
return html.match(/name="__RequestVerificationToken"[^>]+value="([^"]+)"/)?.[1] ?? "";
|
|
77
|
+
}
|
|
78
|
+
// ── Auth ────────────────────────────────────────────────────────────────────
|
|
79
|
+
async login(email, senha) {
|
|
80
|
+
this.logout();
|
|
81
|
+
const loginPage = await this.webGet("/Login");
|
|
82
|
+
const csrf = this.extractCsrf(loginPage.text);
|
|
83
|
+
if (!csrf)
|
|
84
|
+
return { sucesso: false, mensagem: "Não foi possível obter o token CSRF da página de login." };
|
|
85
|
+
const formBody = new URLSearchParams({ Email: email, Senha: senha, __RequestVerificationToken: csrf }).toString();
|
|
86
|
+
const resp = await this.webPost("/Login", formBody, "application/x-www-form-urlencoded", false);
|
|
87
|
+
const isSuccess = resp.status === 302 && (resp.location?.includes("/Carteira") ||
|
|
88
|
+
resp.location?.includes("/carteira") ||
|
|
89
|
+
resp.location === "/");
|
|
90
|
+
if (!isSuccess) {
|
|
91
|
+
const errMsg = resp.text.match(/mensagemErroVal\"[^>]+value=\"([^\"]+)\"/)?.[1]?.trim() ??
|
|
92
|
+
resp.text.match(/texto-mensagem-erro[^>]*>([^<]+)/)?.[1]?.trim() ??
|
|
93
|
+
`Credenciais inválidas (status ${resp.status}).`;
|
|
94
|
+
return { sucesso: false, mensagem: errMsg };
|
|
95
|
+
}
|
|
96
|
+
this._loggedIn = true;
|
|
97
|
+
return { sucesso: true, mensagem: "Login realizado com sucesso." };
|
|
98
|
+
}
|
|
99
|
+
/** Verifica se a sessão ainda funciona fazendo uma chamada leve. */
|
|
100
|
+
async checkSession() {
|
|
101
|
+
try {
|
|
102
|
+
const resp = await this.webGet("/CarteiraAjax/Listar");
|
|
103
|
+
if (resp.status === 200) {
|
|
104
|
+
this._loggedIn = true;
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch { /* ignora */ }
|
|
109
|
+
this.logout();
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
// ── Carteiras ───────────────────────────────────────────────────────────────
|
|
113
|
+
async listarCarteiras() {
|
|
114
|
+
const resp = await this.webGet("/CarteiraAjax/Listar");
|
|
115
|
+
if (resp.status !== 200)
|
|
116
|
+
throw new Error(`Erro ${resp.status} ao listar carteiras.`);
|
|
117
|
+
return JSON.parse(resp.text);
|
|
118
|
+
}
|
|
119
|
+
async selecionarCarteira(idCarteira) {
|
|
120
|
+
const cartPage = await this.webGet("/Carteira");
|
|
121
|
+
const csrf = this.extractCsrf(cartPage.text);
|
|
122
|
+
const formBody = new URLSearchParams({ __RequestVerificationToken: csrf, idCarteira }).toString();
|
|
123
|
+
const resp = await this.webPost("/Carteira", formBody, "application/x-www-form-urlencoded", false);
|
|
124
|
+
const ok = resp.status === 302;
|
|
125
|
+
if (ok)
|
|
126
|
+
this._selectedWallet = idCarteira;
|
|
127
|
+
return { sucesso: ok, mensagem: ok ? `Carteira ${idCarteira} selecionada.` : `Falha ao selecionar (${resp.status}).` };
|
|
128
|
+
}
|
|
129
|
+
// ── Contas ──────────────────────────────────────────────────────────────────
|
|
130
|
+
async listarContas() {
|
|
131
|
+
const resp = await this.webGet("/ContaAjax/ListarTodos");
|
|
132
|
+
if (resp.status !== 200)
|
|
133
|
+
throw new Error(`Erro ${resp.status} ao listar contas.`);
|
|
134
|
+
return JSON.parse(resp.text);
|
|
135
|
+
}
|
|
136
|
+
// ── Lançamentos ─────────────────────────────────────────────────────────────
|
|
137
|
+
async buscarLancamentos(opts) {
|
|
138
|
+
const today = new Date();
|
|
139
|
+
const defaultInicio = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-01`;
|
|
140
|
+
const defaultFim = today.toISOString().slice(0, 10);
|
|
141
|
+
const params = new URLSearchParams();
|
|
142
|
+
params.set("filtro[IdConta]", opts.idConta);
|
|
143
|
+
params.set("filtro[IntervaloDeDatas][DataInicio]", opts.dataInicio ?? defaultInicio);
|
|
144
|
+
params.set("filtro[IntervaloDeDatas][DataFim]", opts.dataFim ?? defaultFim);
|
|
145
|
+
params.set("filtro[ListarOrdenadoPelaDataDeModificacao]", "false");
|
|
146
|
+
const resp = await this.webPost("/LancamentoAjax/Filtrar", params.toString(), "application/x-www-form-urlencoded");
|
|
147
|
+
if (resp.status !== 200)
|
|
148
|
+
throw new Error(`Erro ${resp.status}: ${resp.text.slice(0, 200)}`);
|
|
149
|
+
return JSON.parse(resp.text);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Servidor MCP remoto para o NEXT Finance — modo HTTP (para Claude Mobile e outros clientes remotos).
|
|
4
|
+
*
|
|
5
|
+
* Variáveis de ambiente obrigatórias:
|
|
6
|
+
* NEXT_FINANCE_EMAIL — e-mail da sua conta no NEXT Finance
|
|
7
|
+
* NEXT_FINANCE_PASSWORD — senha da sua conta no NEXT Finance
|
|
8
|
+
* API_KEY — chave secreta para autenticar o cliente MCP (você escolhe)
|
|
9
|
+
*
|
|
10
|
+
* Opcional:
|
|
11
|
+
* PORT — porta do servidor (padrão: 3000)
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Servidor MCP remoto para o NEXT Finance — modo HTTP (para Claude Mobile e outros clientes remotos).
|
|
4
|
+
*
|
|
5
|
+
* Variáveis de ambiente obrigatórias:
|
|
6
|
+
* NEXT_FINANCE_EMAIL — e-mail da sua conta no NEXT Finance
|
|
7
|
+
* NEXT_FINANCE_PASSWORD — senha da sua conta no NEXT Finance
|
|
8
|
+
* API_KEY — chave secreta para autenticar o cliente MCP (você escolhe)
|
|
9
|
+
*
|
|
10
|
+
* Opcional:
|
|
11
|
+
* PORT — porta do servidor (padrão: 3000)
|
|
12
|
+
*/
|
|
13
|
+
import express from "express";
|
|
14
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
15
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
16
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
17
|
+
import { randomUUID } from "crypto";
|
|
18
|
+
import * as client from "./client.js";
|
|
19
|
+
// ── Configuração ─────────────────────────────────────────────────────────────
|
|
20
|
+
const PORT = parseInt(process.env.PORT ?? "3000", 10);
|
|
21
|
+
const API_KEY = process.env.API_KEY ?? "";
|
|
22
|
+
const NF_EMAIL = process.env.NEXT_FINANCE_EMAIL ?? "";
|
|
23
|
+
const NF_PASS = process.env.NEXT_FINANCE_PASSWORD ?? "";
|
|
24
|
+
if (!API_KEY) {
|
|
25
|
+
console.error("❌ Variável de ambiente API_KEY não definida.");
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
if (!NF_EMAIL) {
|
|
29
|
+
console.error("❌ Variável de ambiente NEXT_FINANCE_EMAIL não definida.");
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
if (!NF_PASS) {
|
|
33
|
+
console.error("❌ Variável de ambiente NEXT_FINANCE_PASSWORD não definida.");
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
// ── Auto-login ───────────────────────────────────────────────────────────────
|
|
37
|
+
let loginAttempted = false;
|
|
38
|
+
async function ensureLoggedIn() {
|
|
39
|
+
if (client.isAuthenticated())
|
|
40
|
+
return null;
|
|
41
|
+
if (loginAttempted)
|
|
42
|
+
return "Aguardando login. Tente novamente em alguns segundos.";
|
|
43
|
+
loginAttempted = true;
|
|
44
|
+
console.log("🔐 Fazendo login no NEXT Finance...");
|
|
45
|
+
const result = await client.login(NF_EMAIL, NF_PASS);
|
|
46
|
+
loginAttempted = false;
|
|
47
|
+
if (!result.sucesso) {
|
|
48
|
+
return `Falha no login: ${result.mensagem}`;
|
|
49
|
+
}
|
|
50
|
+
console.log("✅ Login realizado com sucesso.");
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
// Re-login automático a cada 2 dias
|
|
54
|
+
setInterval(() => {
|
|
55
|
+
client.logout();
|
|
56
|
+
ensureLoggedIn().catch(console.error);
|
|
57
|
+
}, 2 * 24 * 60 * 60 * 1000);
|
|
58
|
+
// ── Cria servidor MCP ────────────────────────────────────────────────────────
|
|
59
|
+
function createMcpServer() {
|
|
60
|
+
const server = new Server({ name: "next-finance-mcp", version: "0.3.2" }, { capabilities: { tools: {} } });
|
|
61
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
62
|
+
tools: [
|
|
63
|
+
{
|
|
64
|
+
name: "login",
|
|
65
|
+
description: "Verifica e restabelece a sessão no NEXT Finance (usa credenciais configuradas no servidor).",
|
|
66
|
+
inputSchema: { type: "object", properties: {} },
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: "logout",
|
|
70
|
+
description: "Encerra a sessão atual.",
|
|
71
|
+
inputSchema: { type: "object", properties: {} },
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "listar_carteiras",
|
|
75
|
+
description: "Lista todas as carteiras disponíveis para o usuário logado.",
|
|
76
|
+
inputSchema: { type: "object", properties: {} },
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "selecionar_carteira",
|
|
80
|
+
description: "Seleciona uma carteira pelo ID (necessário antes de listar contas e lançamentos).",
|
|
81
|
+
inputSchema: {
|
|
82
|
+
type: "object",
|
|
83
|
+
properties: {
|
|
84
|
+
id_carteira: { type: "string", description: "ID da carteira" },
|
|
85
|
+
},
|
|
86
|
+
required: ["id_carteira"],
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "listar_contas",
|
|
91
|
+
description: "Lista as contas da carteira selecionada.",
|
|
92
|
+
inputSchema: { type: "object", properties: {} },
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: "buscar_lancamentos",
|
|
96
|
+
description: "Busca lançamentos/transações de uma conta. Se não informar datas, retorna o mês atual.",
|
|
97
|
+
inputSchema: {
|
|
98
|
+
type: "object",
|
|
99
|
+
properties: {
|
|
100
|
+
id_conta: { type: "string", description: "ID da conta (use listar_contas para descobrir)" },
|
|
101
|
+
data_inicio: { type: "string", description: "Data início YYYY-MM-DD (padrão: 1º do mês atual)" },
|
|
102
|
+
data_fim: { type: "string", description: "Data fim YYYY-MM-DD (padrão: hoje)" },
|
|
103
|
+
},
|
|
104
|
+
required: ["id_conta"],
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
}));
|
|
109
|
+
function ok(data) {
|
|
110
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
111
|
+
}
|
|
112
|
+
function requireWallet() {
|
|
113
|
+
if (!client.getSelectedWallet())
|
|
114
|
+
return ok({ erro: "Nenhuma carteira selecionada. Use `selecionar_carteira` primeiro." });
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
118
|
+
const { name, arguments: args = {} } = req.params;
|
|
119
|
+
try {
|
|
120
|
+
// Garante login antes de qualquer operação (exceto login/logout)
|
|
121
|
+
if (name !== "login" && name !== "logout") {
|
|
122
|
+
const loginErr = await ensureLoggedIn();
|
|
123
|
+
if (loginErr)
|
|
124
|
+
return ok({ erro: loginErr });
|
|
125
|
+
}
|
|
126
|
+
switch (name) {
|
|
127
|
+
case "login": {
|
|
128
|
+
const err = await ensureLoggedIn();
|
|
129
|
+
if (err) {
|
|
130
|
+
// Forçar novo login
|
|
131
|
+
client.logout();
|
|
132
|
+
const result = await client.login(NF_EMAIL, NF_PASS);
|
|
133
|
+
return ok(result);
|
|
134
|
+
}
|
|
135
|
+
return ok({ sucesso: true, mensagem: "Sessão ativa. Pronto para usar!" });
|
|
136
|
+
}
|
|
137
|
+
case "logout": {
|
|
138
|
+
client.logout();
|
|
139
|
+
return ok({ mensagem: "Sessão encerrada." });
|
|
140
|
+
}
|
|
141
|
+
case "listar_carteiras":
|
|
142
|
+
return ok(await client.listarCarteiras());
|
|
143
|
+
case "selecionar_carteira":
|
|
144
|
+
return ok(await client.selecionarCarteira(args["id_carteira"]));
|
|
145
|
+
case "listar_contas": {
|
|
146
|
+
const walErr = requireWallet();
|
|
147
|
+
if (walErr)
|
|
148
|
+
return walErr;
|
|
149
|
+
return ok(await client.listarContas());
|
|
150
|
+
}
|
|
151
|
+
case "buscar_lancamentos": {
|
|
152
|
+
const walErr = requireWallet();
|
|
153
|
+
if (walErr)
|
|
154
|
+
return walErr;
|
|
155
|
+
return ok(await client.buscarLancamentos({
|
|
156
|
+
idConta: args["id_conta"],
|
|
157
|
+
dataInicio: args["data_inicio"],
|
|
158
|
+
dataFim: args["data_fim"],
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
default:
|
|
162
|
+
return ok({ erro: `Ferramenta desconhecida: ${name}` });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
return ok({ erro: err instanceof Error ? err.message : String(err) });
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
return server;
|
|
170
|
+
}
|
|
171
|
+
// ── Express app ──────────────────────────────────────────────────────────────
|
|
172
|
+
const app = express();
|
|
173
|
+
app.use(express.json());
|
|
174
|
+
// Middleware de autenticação via Bearer token
|
|
175
|
+
app.use((req, res, next) => {
|
|
176
|
+
// Health check não exige auth
|
|
177
|
+
if (req.path === "/health" && req.method === "GET") {
|
|
178
|
+
next();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const auth = req.headers.authorization ?? "";
|
|
182
|
+
const token = auth.startsWith("Bearer ") ? auth.slice(7) : "";
|
|
183
|
+
if (token !== API_KEY) {
|
|
184
|
+
res.status(401).json({ error: "Unauthorized — API_KEY inválida." });
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
next();
|
|
188
|
+
});
|
|
189
|
+
// Health check
|
|
190
|
+
app.get("/health", (_req, res) => {
|
|
191
|
+
res.json({ status: "ok", authenticated: client.isAuthenticated() });
|
|
192
|
+
});
|
|
193
|
+
// Sessões MCP ativas
|
|
194
|
+
const sessions = new Map();
|
|
195
|
+
// Endpoint MCP principal (POST inicia sessão, GET/DELETE gerencia)
|
|
196
|
+
app.all("/mcp", async (req, res) => {
|
|
197
|
+
try {
|
|
198
|
+
// Reutilizar sessão existente se o header Mcp-Session-Id for enviado
|
|
199
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
200
|
+
let transport = sessionId ? sessions.get(sessionId) : undefined;
|
|
201
|
+
if (!transport) {
|
|
202
|
+
// Nova sessão
|
|
203
|
+
const newId = randomUUID();
|
|
204
|
+
transport = new StreamableHTTPServerTransport({
|
|
205
|
+
sessionIdGenerator: () => newId,
|
|
206
|
+
onsessioninitialized: (id) => {
|
|
207
|
+
sessions.set(id, transport);
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
transport.onclose = () => {
|
|
211
|
+
sessions.delete(newId);
|
|
212
|
+
};
|
|
213
|
+
const server = createMcpServer();
|
|
214
|
+
await server.connect(transport);
|
|
215
|
+
}
|
|
216
|
+
await transport.handleRequest(req, res, req.body);
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
console.error("Erro no handler MCP:", err);
|
|
220
|
+
if (!res.headersSent) {
|
|
221
|
+
res.status(500).json({ error: "Erro interno do servidor." });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
// ── Inicialização ─────────────────────────────────────────────────────────────
|
|
226
|
+
async function main() {
|
|
227
|
+
// Login inicial ao subir o servidor
|
|
228
|
+
await ensureLoggedIn();
|
|
229
|
+
app.listen(PORT, () => {
|
|
230
|
+
console.log(`🚀 NEXT Finance MCP Server rodando na porta ${PORT}`);
|
|
231
|
+
console.log(`📡 Endpoint MCP: http://localhost:${PORT}/mcp`);
|
|
232
|
+
console.log(`❤️ Health check: http://localhost:${PORT}/health`);
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
main().catch((err) => { console.error("Erro fatal:", err); process.exit(1); });
|
package/dist/index.js
CHANGED
|
@@ -43,20 +43,33 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
name: "listar_contas",
|
|
46
|
-
description: "Lista as contas da carteira selecionada."
|
|
47
|
-
|
|
46
|
+
description: "Lista as contas da carteira selecionada com suporte a filtro e paginação. " +
|
|
47
|
+
"Use 'busca' para filtrar por nome. Use 'pagina' e 'limite' para navegar em carteiras grandes.",
|
|
48
|
+
inputSchema: {
|
|
49
|
+
type: "object",
|
|
50
|
+
properties: {
|
|
51
|
+
busca: { type: "string", description: "Filtra contas cujo nome contenha este texto (ex: 'Inter', 'Bradesco')" },
|
|
52
|
+
tipos: { type: "array", items: { type: "number" }, description: "Filtra por tipo(s) de conta (ex: [1, 2])" },
|
|
53
|
+
pagina: { type: "number", description: "Página a retornar (começa em 1, padrão: 1)" },
|
|
54
|
+
limite: { type: "number", description: "Contas por página (padrão: 30, máximo: 200)" },
|
|
55
|
+
},
|
|
56
|
+
},
|
|
48
57
|
},
|
|
49
58
|
{
|
|
50
59
|
name: "buscar_lancamentos",
|
|
51
|
-
description: "Busca lançamentos/transações de uma conta. " +
|
|
52
|
-
"Use listar_contas para obter os IDs de conta
|
|
53
|
-
"Se não informar datas, retorna o mês atual."
|
|
60
|
+
description: "Busca lançamentos/transações de uma conta com filtro e paginação. " +
|
|
61
|
+
"Use listar_contas para obter os IDs de conta. " +
|
|
62
|
+
"Se não informar datas, retorna o mês atual. " +
|
|
63
|
+
"Em períodos longos ou contas movimentadas, use 'pagina' para navegar.",
|
|
54
64
|
inputSchema: {
|
|
55
65
|
type: "object",
|
|
56
66
|
properties: {
|
|
57
67
|
id_conta: { type: "string", description: "ID da conta (use listar_contas para descobrir)" },
|
|
58
68
|
data_inicio: { type: "string", description: "Data início YYYY-MM-DD (padrão: 1º do mês atual)" },
|
|
59
69
|
data_fim: { type: "string", description: "Data fim YYYY-MM-DD (padrão: hoje)" },
|
|
70
|
+
busca: { type: "string", description: "Filtra lançamentos cuja descrição/histórico contenha este texto" },
|
|
71
|
+
pagina: { type: "number", description: "Página a retornar (começa em 1, padrão: 1)" },
|
|
72
|
+
limite: { type: "number", description: "Lançamentos por página (padrão: 50, máximo: 500)" },
|
|
60
73
|
},
|
|
61
74
|
required: ["id_conta"],
|
|
62
75
|
},
|
|
@@ -117,7 +130,12 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
117
130
|
const walErr = requireWallet();
|
|
118
131
|
if (walErr)
|
|
119
132
|
return walErr;
|
|
120
|
-
return ok(await client.listarContas(
|
|
133
|
+
return ok(await client.listarContas({
|
|
134
|
+
busca: args["busca"],
|
|
135
|
+
tipos: args["tipos"],
|
|
136
|
+
pagina: args["pagina"],
|
|
137
|
+
limite: args["limite"],
|
|
138
|
+
}));
|
|
121
139
|
}
|
|
122
140
|
case "buscar_lancamentos": {
|
|
123
141
|
const authErr = requireAuth();
|
|
@@ -130,6 +148,9 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
130
148
|
idConta: args["id_conta"],
|
|
131
149
|
dataInicio: args["data_inicio"],
|
|
132
150
|
dataFim: args["data_fim"],
|
|
151
|
+
busca: args["busca"],
|
|
152
|
+
pagina: args["pagina"],
|
|
153
|
+
limite: args["limite"],
|
|
133
154
|
}));
|
|
134
155
|
}
|
|
135
156
|
default:
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Armazenamento persistente de usuários com credenciais criptografadas (AES-256-GCM).
|
|
3
|
+
* Cada usuário tem uma API key única gerada no registro.
|
|
4
|
+
*/
|
|
5
|
+
export interface UserRecord {
|
|
6
|
+
apiKey: string;
|
|
7
|
+
email: string;
|
|
8
|
+
encEmail: {
|
|
9
|
+
iv: string;
|
|
10
|
+
tag: string;
|
|
11
|
+
data: string;
|
|
12
|
+
};
|
|
13
|
+
encPassword: {
|
|
14
|
+
iv: string;
|
|
15
|
+
tag: string;
|
|
16
|
+
data: string;
|
|
17
|
+
};
|
|
18
|
+
createdAt: string;
|
|
19
|
+
}
|
|
20
|
+
/** Registra um novo usuário. Retorna a API key gerada. */
|
|
21
|
+
export declare function registerUser(email: string, password: string): string;
|
|
22
|
+
/** Retorna credenciais decifradas dado a API key. */
|
|
23
|
+
export declare function getCredentials(apiKey: string): {
|
|
24
|
+
email: string;
|
|
25
|
+
password: string;
|
|
26
|
+
} | null;
|
|
27
|
+
/** Remove um usuário. */
|
|
28
|
+
export declare function removeUser(apiKey: string): boolean;
|
|
29
|
+
/** Conta total de usuários registrados. */
|
|
30
|
+
export declare function userCount(): number;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Armazenamento persistente de usuários com credenciais criptografadas (AES-256-GCM).
|
|
3
|
+
* Cada usuário tem uma API key única gerada no registro.
|
|
4
|
+
*/
|
|
5
|
+
import * as crypto from "crypto";
|
|
6
|
+
import * as fs from "fs";
|
|
7
|
+
import * as path from "path";
|
|
8
|
+
const STORE_FILE = process.env.STORE_FILE ?? path.join(process.cwd(), "users.json");
|
|
9
|
+
const ENCRYPTION_KEY_RAW = process.env.ENCRYPTION_KEY ?? "";
|
|
10
|
+
if (!ENCRYPTION_KEY_RAW) {
|
|
11
|
+
console.error("❌ Variável de ambiente ENCRYPTION_KEY não definida.");
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
// Deriva uma chave de 32 bytes do valor fornecido
|
|
15
|
+
const ENC_KEY = crypto.createHash("sha256").update(ENCRYPTION_KEY_RAW).digest();
|
|
16
|
+
// ── Criptografia ─────────────────────────────────────────────────────────────
|
|
17
|
+
function encrypt(text) {
|
|
18
|
+
const iv = crypto.randomBytes(12);
|
|
19
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", ENC_KEY, iv);
|
|
20
|
+
const encrypted = Buffer.concat([cipher.update(text, "utf8"), cipher.final()]);
|
|
21
|
+
return {
|
|
22
|
+
iv: iv.toString("hex"),
|
|
23
|
+
tag: cipher.getAuthTag().toString("hex"),
|
|
24
|
+
data: encrypted.toString("hex"),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function decrypt(enc) {
|
|
28
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", ENC_KEY, Buffer.from(enc.iv, "hex"));
|
|
29
|
+
decipher.setAuthTag(Buffer.from(enc.tag, "hex"));
|
|
30
|
+
return Buffer.concat([
|
|
31
|
+
decipher.update(Buffer.from(enc.data, "hex")),
|
|
32
|
+
decipher.final(),
|
|
33
|
+
]).toString("utf8");
|
|
34
|
+
}
|
|
35
|
+
// ── Persistência ──────────────────────────────────────────────────────────────
|
|
36
|
+
let store = new Map(); // apiKey → UserRecord
|
|
37
|
+
function loadStore() {
|
|
38
|
+
try {
|
|
39
|
+
if (!fs.existsSync(STORE_FILE))
|
|
40
|
+
return;
|
|
41
|
+
const records = JSON.parse(fs.readFileSync(STORE_FILE, "utf-8"));
|
|
42
|
+
store = new Map(records.map(r => [r.apiKey, r]));
|
|
43
|
+
console.log(`📂 ${store.size} usuário(s) carregado(s) do arquivo.`);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
console.error("Erro ao carregar store:", err);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function saveStore() {
|
|
50
|
+
try {
|
|
51
|
+
fs.writeFileSync(STORE_FILE, JSON.stringify([...store.values()], null, 2), "utf-8");
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
console.error("Erro ao salvar store:", err);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Carrega ao importar
|
|
58
|
+
loadStore();
|
|
59
|
+
// ── API pública ───────────────────────────────────────────────────────────────
|
|
60
|
+
/** Registra um novo usuário. Retorna a API key gerada. */
|
|
61
|
+
export function registerUser(email, password) {
|
|
62
|
+
// Verifica se e-mail já existe
|
|
63
|
+
for (const r of store.values()) {
|
|
64
|
+
try {
|
|
65
|
+
if (decrypt(r.encEmail) === email) {
|
|
66
|
+
// Atualiza a senha e retorna a chave existente
|
|
67
|
+
r.encPassword = encrypt(password);
|
|
68
|
+
saveStore();
|
|
69
|
+
return r.apiKey;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch { /* ignora registros corrompidos */ }
|
|
73
|
+
}
|
|
74
|
+
const apiKey = crypto.randomBytes(32).toString("hex");
|
|
75
|
+
const record = {
|
|
76
|
+
apiKey,
|
|
77
|
+
email: email.split("@")[0] + "@***", // ofuscado no arquivo
|
|
78
|
+
encEmail: encrypt(email),
|
|
79
|
+
encPassword: encrypt(password),
|
|
80
|
+
createdAt: new Date().toISOString(),
|
|
81
|
+
};
|
|
82
|
+
store.set(apiKey, record);
|
|
83
|
+
saveStore();
|
|
84
|
+
return apiKey;
|
|
85
|
+
}
|
|
86
|
+
/** Retorna credenciais decifradas dado a API key. */
|
|
87
|
+
export function getCredentials(apiKey) {
|
|
88
|
+
const record = store.get(apiKey);
|
|
89
|
+
if (!record)
|
|
90
|
+
return null;
|
|
91
|
+
try {
|
|
92
|
+
return {
|
|
93
|
+
email: decrypt(record.encEmail),
|
|
94
|
+
password: decrypt(record.encPassword),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Remove um usuário. */
|
|
102
|
+
export function removeUser(apiKey) {
|
|
103
|
+
if (!store.has(apiKey))
|
|
104
|
+
return false;
|
|
105
|
+
store.delete(apiKey);
|
|
106
|
+
saveStore();
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
/** Conta total de usuários registrados. */
|
|
110
|
+
export function userCount() { return store.size; }
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "next-finance-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
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",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
8
|
"bin": {
|
|
9
|
-
"next-finance-mcp": "dist/index.js"
|
|
9
|
+
"next-finance-mcp": "dist/index.js",
|
|
10
|
+
"next-finance-mcp-server": "dist/http-index.js"
|
|
10
11
|
},
|
|
11
12
|
"files": [
|
|
12
13
|
"dist"
|
|
@@ -15,6 +16,7 @@
|
|
|
15
16
|
"build": "tsc",
|
|
16
17
|
"dev": "tsx src/index.ts",
|
|
17
18
|
"start": "node dist/index.js",
|
|
19
|
+
"start:server": "node dist/http-index.js",
|
|
18
20
|
"prepublishOnly": "npm run build"
|
|
19
21
|
},
|
|
20
22
|
"keywords": [
|
|
@@ -36,9 +38,11 @@
|
|
|
36
38
|
"node": ">=18"
|
|
37
39
|
},
|
|
38
40
|
"dependencies": {
|
|
39
|
-
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
42
|
+
"express": "^5.2.1"
|
|
40
43
|
},
|
|
41
44
|
"devDependencies": {
|
|
45
|
+
"@types/express": "^5.0.6",
|
|
42
46
|
"@types/node": "^22.0.0",
|
|
43
47
|
"tsx": "^4.0.0",
|
|
44
48
|
"typescript": "^5.0.0"
|