next-finance-mcp 0.9.21 → 0.9.23
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 +8 -0
- package/dist/client.js +97 -72
- package/dist/context.d.ts +44 -0
- package/dist/context.js +46 -0
- package/dist/http-index.d.ts +6 -1
- package/dist/http-index.js +22 -0
- package/dist/index.js +4 -1
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* - O walletId é salvo na sessão para re-seleção automática ao restaurar.
|
|
10
10
|
* - apiGet/apiPost detectam 500 "Erro desconhecido" e fazem auto-refresh da carteira.
|
|
11
11
|
*/
|
|
12
|
+
/** Persistência opcional: só faz sentido no modo stdio (1 usuário por máquina).
|
|
13
|
+
* No modo HTTP multi-tenant, o caller controla o lifecycle do contexto. */
|
|
12
14
|
export declare function saveSession(): void;
|
|
13
15
|
export declare function loadSession(): boolean;
|
|
14
16
|
export declare function deleteSession(): void;
|
|
@@ -121,7 +123,10 @@ export declare function buscarComprasProduto(opts?: FiltroComprasProduto): Promi
|
|
|
121
123
|
fornecedor: string;
|
|
122
124
|
moeda: string;
|
|
123
125
|
valor: number;
|
|
126
|
+
quantidade: number;
|
|
127
|
+
valor_total: number;
|
|
124
128
|
data: string;
|
|
129
|
+
observacao: string;
|
|
125
130
|
}[];
|
|
126
131
|
paginacao: {
|
|
127
132
|
pagina: number;
|
|
@@ -200,6 +205,9 @@ export interface ItemDespesaSlim {
|
|
|
200
205
|
valor: number;
|
|
201
206
|
desconto: number;
|
|
202
207
|
valor_total: number;
|
|
208
|
+
valor_bruto?: number;
|
|
209
|
+
valor_total_bruto?: number;
|
|
210
|
+
desconto_aplicado?: number;
|
|
203
211
|
}
|
|
204
212
|
export interface DespesaSlim {
|
|
205
213
|
data: string;
|
package/dist/client.js
CHANGED
|
@@ -13,6 +13,7 @@ import * as fs from "fs";
|
|
|
13
13
|
import * as path from "path";
|
|
14
14
|
import * as os from "os";
|
|
15
15
|
import { request as undiciRequest, Agent, interceptors } from "undici";
|
|
16
|
+
import { getCtx } from "./context.js";
|
|
16
17
|
// Agentes pré-configurados para controle de redirects (undici v7 usa interceptors)
|
|
17
18
|
const agentNoRedirect = new Agent().compose(interceptors.redirect({ maxRedirections: 0 }));
|
|
18
19
|
const agentFollow = new Agent().compose(interceptors.redirect({ maxRedirections: 5 }));
|
|
@@ -20,13 +21,16 @@ const WEB_BASE = "https://finance.net.br"; // login e seleção de carteira (MVC
|
|
|
20
21
|
const API_BASE = "https://api.finance.net.br"; // API REST (mesmo cookie de sessão)
|
|
21
22
|
const SESSION_DIR = path.join(os.homedir(), ".next-finance-mcp");
|
|
22
23
|
const SESSION_FILE = path.join(SESSION_DIR, "session.json");
|
|
24
|
+
/** Persistência opcional: só faz sentido no modo stdio (1 usuário por máquina).
|
|
25
|
+
* No modo HTTP multi-tenant, o caller controla o lifecycle do contexto. */
|
|
23
26
|
export function saveSession() {
|
|
24
27
|
try {
|
|
28
|
+
const ctx = getCtx();
|
|
25
29
|
fs.mkdirSync(SESSION_DIR, { recursive: true });
|
|
26
30
|
const data = {
|
|
27
|
-
cookies: [...
|
|
31
|
+
cookies: [...ctx.cookies],
|
|
28
32
|
savedAt: new Date().toISOString(),
|
|
29
|
-
selectedWalletId:
|
|
33
|
+
selectedWalletId: ctx.selectedWallet ?? undefined,
|
|
30
34
|
};
|
|
31
35
|
fs.writeFileSync(SESSION_FILE, JSON.stringify(data, null, 2), "utf-8");
|
|
32
36
|
}
|
|
@@ -34,16 +38,17 @@ export function saveSession() {
|
|
|
34
38
|
}
|
|
35
39
|
export function loadSession() {
|
|
36
40
|
try {
|
|
41
|
+
const ctx = getCtx();
|
|
37
42
|
const raw = fs.readFileSync(SESSION_FILE, "utf-8");
|
|
38
43
|
const data = JSON.parse(raw);
|
|
39
44
|
// Aceita sessões salvas há menos de 3 dias
|
|
40
45
|
const age = Date.now() - new Date(data.savedAt).getTime();
|
|
41
46
|
if (age > 3 * 24 * 60 * 60 * 1000)
|
|
42
47
|
return false;
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
ctx.cookies.length = 0;
|
|
49
|
+
ctx.cookies.push(...data.cookies);
|
|
45
50
|
if (data.selectedWalletId)
|
|
46
|
-
|
|
51
|
+
ctx.selectedWallet = data.selectedWalletId;
|
|
47
52
|
return true;
|
|
48
53
|
}
|
|
49
54
|
catch {
|
|
@@ -56,8 +61,9 @@ export function deleteSession() {
|
|
|
56
61
|
}
|
|
57
62
|
catch { /* ignora */ }
|
|
58
63
|
}
|
|
59
|
-
|
|
64
|
+
// ── Cookie store (por contexto) ─────────────────────────────────────────────
|
|
60
65
|
function storeCookies(setCookieHeaders, domain) {
|
|
66
|
+
const cookies = getCtx().cookies;
|
|
61
67
|
for (const header of setCookieHeaders) {
|
|
62
68
|
const pair = header.split(";")[0].trim();
|
|
63
69
|
const eqIdx = pair.indexOf("=");
|
|
@@ -65,21 +71,21 @@ function storeCookies(setCookieHeaders, domain) {
|
|
|
65
71
|
continue;
|
|
66
72
|
const name = pair.slice(0, eqIdx).trim();
|
|
67
73
|
const value = pair.slice(eqIdx + 1).trim();
|
|
68
|
-
const existing =
|
|
74
|
+
const existing = cookies.findIndex(c => c.name === name && c.domain === domain);
|
|
69
75
|
if (existing >= 0)
|
|
70
|
-
|
|
76
|
+
cookies[existing].value = value;
|
|
71
77
|
else
|
|
72
|
-
|
|
78
|
+
cookies.push({ name, value, domain });
|
|
73
79
|
}
|
|
74
80
|
}
|
|
75
81
|
function getCookieHeader(domain) {
|
|
76
|
-
return
|
|
82
|
+
return getCtx().cookies
|
|
77
83
|
.filter(c => domain.includes(c.domain) || c.domain.includes(domain))
|
|
78
84
|
.map(c => `${c.name}=${c.value}`)
|
|
79
85
|
.join("; ");
|
|
80
86
|
}
|
|
81
87
|
function clearCookies() {
|
|
82
|
-
|
|
88
|
+
getCtx().cookies.length = 0;
|
|
83
89
|
}
|
|
84
90
|
// ── Helpers de request ─────────────────────────────────────────────────────
|
|
85
91
|
const BASE_HEADERS = {
|
|
@@ -153,14 +159,15 @@ function isCarteiraTokenExpired(status, text) {
|
|
|
153
159
|
}
|
|
154
160
|
/** Re-seleciona a carteira para renovar o token (c=) expirado. */
|
|
155
161
|
async function refreshCarteiraToken() {
|
|
156
|
-
|
|
162
|
+
const idCarteira = getCtx().selectedWallet;
|
|
163
|
+
if (!idCarteira)
|
|
157
164
|
return false;
|
|
158
165
|
try {
|
|
159
166
|
const cartPage = await webGet("/Carteira");
|
|
160
167
|
const csrf = extractCsrf(cartPage.text);
|
|
161
168
|
if (!csrf)
|
|
162
169
|
return false;
|
|
163
|
-
const formBody = new URLSearchParams({ __RequestVerificationToken: csrf, idCarteira
|
|
170
|
+
const formBody = new URLSearchParams({ __RequestVerificationToken: csrf, idCarteira }).toString();
|
|
164
171
|
const resp = await webPost("/Carteira", formBody, "application/x-www-form-urlencoded", false);
|
|
165
172
|
if (resp.status === 302 || resp.status === 200) {
|
|
166
173
|
saveSession();
|
|
@@ -171,8 +178,8 @@ async function refreshCarteiraToken() {
|
|
|
171
178
|
return false;
|
|
172
179
|
}
|
|
173
180
|
function throwSessionExpired() {
|
|
174
|
-
|
|
175
|
-
|
|
181
|
+
getCtx().loggedIn = false;
|
|
182
|
+
getCtx().selectedWallet = null;
|
|
176
183
|
clearCookies();
|
|
177
184
|
throw new Error("Sessão expirada. Use a ferramenta `login` para autenticar novamente.");
|
|
178
185
|
}
|
|
@@ -265,21 +272,19 @@ async function apiPost(path, body, retry = true) {
|
|
|
265
272
|
function extractCsrf(html) {
|
|
266
273
|
return html.match(/name="__RequestVerificationToken"[^>]+value="([^"]+)"/)?.[1] ?? "";
|
|
267
274
|
}
|
|
268
|
-
// ── Estado da sessão
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
export function isAuthenticated() { return _loggedIn; }
|
|
272
|
-
export function getSelectedWallet() { return _selectedWallet; }
|
|
275
|
+
// ── Estado da sessão (delegado ao ClientContext) ────────────────────────────
|
|
276
|
+
export function isAuthenticated() { return getCtx().loggedIn; }
|
|
277
|
+
export function getSelectedWallet() { return getCtx().selectedWallet; }
|
|
273
278
|
export function logout() {
|
|
274
279
|
clearCookies();
|
|
275
|
-
|
|
276
|
-
|
|
280
|
+
getCtx().loggedIn = false;
|
|
281
|
+
getCtx().selectedWallet = null;
|
|
277
282
|
}
|
|
278
283
|
// ── Auth ────────────────────────────────────────────────────────────────────
|
|
279
284
|
export async function login(email, senha) {
|
|
280
285
|
clearCookies();
|
|
281
|
-
|
|
282
|
-
|
|
286
|
+
getCtx().loggedIn = false;
|
|
287
|
+
getCtx().selectedWallet = null;
|
|
283
288
|
// 1. Pegar página de login para obter CSRF token e cookie
|
|
284
289
|
const loginPage = await webGet("/Login");
|
|
285
290
|
const csrf = extractCsrf(loginPage.text);
|
|
@@ -299,7 +304,7 @@ export async function login(email, senha) {
|
|
|
299
304
|
?? `Credenciais inválidas (status ${loginResp.status}, location: ${loginResp.location ?? "none"}).`;
|
|
300
305
|
return { sucesso: false, mensagem: errMsg };
|
|
301
306
|
}
|
|
302
|
-
|
|
307
|
+
getCtx().loggedIn = true;
|
|
303
308
|
saveSession();
|
|
304
309
|
return { sucesso: true, mensagem: "Login realizado com sucesso." };
|
|
305
310
|
}
|
|
@@ -311,9 +316,9 @@ export async function tryRestoreSession() {
|
|
|
311
316
|
// Valida se a sessão web ainda funciona
|
|
312
317
|
const resp = await webGet("/CarteiraAjax/Listar");
|
|
313
318
|
if (resp.status === 200) {
|
|
314
|
-
|
|
319
|
+
getCtx().loggedIn = true;
|
|
315
320
|
// Re-selecionar carteira para renovar o token de carteira (c=) expirado em 5 min
|
|
316
|
-
if (
|
|
321
|
+
if (getCtx().selectedWallet) {
|
|
317
322
|
await refreshCarteiraToken();
|
|
318
323
|
}
|
|
319
324
|
return true;
|
|
@@ -397,7 +402,7 @@ export async function selecionarCarteira(nomeCarteira) {
|
|
|
397
402
|
const resp = await webPost("/Carteira", formBody, "application/x-www-form-urlencoded", false);
|
|
398
403
|
const ok = resp.status === 302;
|
|
399
404
|
if (ok) {
|
|
400
|
-
|
|
405
|
+
getCtx().selectedWallet = idCarteira;
|
|
401
406
|
invalidarCacheContas();
|
|
402
407
|
saveSession();
|
|
403
408
|
}
|
|
@@ -471,13 +476,13 @@ function isContaAtiva(c) {
|
|
|
471
476
|
}
|
|
472
477
|
// Cache em memória da lista de contas (TTL 60s) para evitar carregar 1.2MB em
|
|
473
478
|
// cada chamada de buscarLancamentos quando se itera por várias contas.
|
|
474
|
-
let _contasCache = null;
|
|
475
479
|
const CONTAS_CACHE_TTL_MS = 60 * 1000;
|
|
476
|
-
export function invalidarCacheContas() {
|
|
480
|
+
export function invalidarCacheContas() { getCtx().contasCache = null; }
|
|
477
481
|
/** Tenta endpoints em ordem até encontrar um que responda com dados */
|
|
478
482
|
async function fetchContas() {
|
|
479
|
-
|
|
480
|
-
|
|
483
|
+
const cache = getCtx().contasCache;
|
|
484
|
+
if (cache && cache.expira > Date.now()) {
|
|
485
|
+
return { contas: cache.contas, fonte: cache.fonte };
|
|
481
486
|
}
|
|
482
487
|
// Todos os endpoints REST agora apontam para api.finance.net.br via apiGet()
|
|
483
488
|
const tentativas = [
|
|
@@ -502,7 +507,7 @@ async function fetchContas() {
|
|
|
502
507
|
? raw
|
|
503
508
|
: (Object.values(raw).find(v => Array.isArray(v)) ?? []);
|
|
504
509
|
if (arr.length > 0) {
|
|
505
|
-
|
|
510
|
+
getCtx().contasCache = { contas: arr, fonte: path, expira: Date.now() + CONTAS_CACHE_TTL_MS };
|
|
506
511
|
return { contas: arr, fonte: path };
|
|
507
512
|
}
|
|
508
513
|
}
|
|
@@ -851,15 +856,22 @@ export async function buscarComprasProduto(opts = {}) {
|
|
|
851
856
|
const total = items.length;
|
|
852
857
|
const offset = (pagina - 1) * limite;
|
|
853
858
|
const paginados = items.slice(offset, offset + limite);
|
|
854
|
-
// Slim: apenas campos legíveis — sem IDs
|
|
859
|
+
// Slim: apenas campos legíveis — sem IDs.
|
|
860
|
+
// Nota: o endpoint /api/ProdutoBusiness/FiltrarCompra retorna o produto cadastrado
|
|
861
|
+
// (agregação por produto). Não traz a Despesa pai, então NÃO há como ratear desconto
|
|
862
|
+
// a nível de documento aqui. Use ultimo_preco_produto para obter o preço REAL pago
|
|
863
|
+
// (que cruza com Despesa/FiltrarDespesaServProd e aplica desconto proporcional).
|
|
855
864
|
const compras = paginados.map(item => {
|
|
856
865
|
const prod = item["ProdutoBusiness"];
|
|
857
866
|
return {
|
|
858
|
-
produto: String(prod?.["Nome"] ?? ""),
|
|
867
|
+
produto: String(prod?.["Descricao"] ?? prod?.["Nome"] ?? item["Codigo"] ?? ""),
|
|
859
868
|
fornecedor: String(item["PessoaNome"] ?? ""),
|
|
860
869
|
moeda: String(item["MoedaNome"] ?? "BRL"),
|
|
861
|
-
valor: Number(item["Valor"] ?? 0),
|
|
870
|
+
valor: Number(item["Valor"] ?? 0), // preço bruto unitário (sem ratear desconto)
|
|
871
|
+
quantidade: Number(item["Quantidade"] ?? 0),
|
|
872
|
+
valor_total: Number(item["ValorTotal"] ?? 0),
|
|
862
873
|
data: String(item["DataAtualizacao"] ?? "").slice(0, 10),
|
|
874
|
+
observacao: "Preço bruto sem desconto da nota. Para preço real pago, use 'ultimo_preco_produto'.",
|
|
863
875
|
};
|
|
864
876
|
});
|
|
865
877
|
return {
|
|
@@ -1088,15 +1100,43 @@ async function resolverIdFornecedor(nome) {
|
|
|
1088
1100
|
const primeiro = arr[0];
|
|
1089
1101
|
return String(primeiro["Id"] ?? "");
|
|
1090
1102
|
}
|
|
1091
|
-
|
|
1103
|
+
/** Calcula valores REAIS (com desconto da nota distribuído) para um item de DespesaServProd.
|
|
1104
|
+
* Pode receber a Despesa pai inline (do FiltrarDespesaServProd) ou separadamente. */
|
|
1105
|
+
function aplicarDescontoProporcional(item, despesa) {
|
|
1106
|
+
const qtd = Number(item["Quantidade"] ?? 0);
|
|
1107
|
+
const valorBruto = Number(item["Valor"] ?? 0);
|
|
1108
|
+
const totalBruto = Number(item["ValorTotal"] ?? (qtd > 0 ? qtd * valorBruto : valorBruto));
|
|
1109
|
+
if (!despesa)
|
|
1110
|
+
return { precoUnitario: valorBruto, valorTotal: totalBruto };
|
|
1111
|
+
const despBruto = Number(despesa["Valor"] ?? 0);
|
|
1112
|
+
const despLiquido = Number(despesa["ValorLiquido"] ?? despBruto);
|
|
1113
|
+
if (despBruto <= 0 || Math.abs(despBruto - despLiquido) < 0.01) {
|
|
1114
|
+
return { precoUnitario: valorBruto, valorTotal: totalBruto };
|
|
1115
|
+
}
|
|
1116
|
+
const fator = despLiquido / despBruto;
|
|
1117
|
+
const totalReal = Math.round(totalBruto * fator * 100) / 100;
|
|
1118
|
+
const precoReal = qtd > 0 ? Math.round((totalReal / qtd) * 100) / 100 : valorBruto * fator;
|
|
1119
|
+
return {
|
|
1120
|
+
precoUnitario: precoReal,
|
|
1121
|
+
valorTotal: totalReal,
|
|
1122
|
+
descontoAplicado: Math.round((totalBruto - totalReal) * 100) / 100,
|
|
1123
|
+
precoUnitarioBruto: valorBruto,
|
|
1124
|
+
valorTotalBruto: totalBruto,
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
function slimItemDespesa(i, despesa) {
|
|
1128
|
+
const calc = aplicarDescontoProporcional(i, despesa);
|
|
1092
1129
|
return {
|
|
1093
1130
|
descricao: String(i["DescricaoServProd"] ?? "").trim(),
|
|
1094
1131
|
codigo: i["Codigo"] ? String(i["Codigo"]).trim() : undefined,
|
|
1095
1132
|
quantidade: Number(i["Quantidade"] ?? 0),
|
|
1096
1133
|
unidade: i["UnidadeMedida"] ? String(i["UnidadeMedida"]).trim() : undefined,
|
|
1097
|
-
valor:
|
|
1134
|
+
valor: calc.precoUnitario,
|
|
1098
1135
|
desconto: Number(i["Desconto"] ?? 0),
|
|
1099
|
-
valor_total:
|
|
1136
|
+
valor_total: calc.valorTotal,
|
|
1137
|
+
valor_bruto: calc.precoUnitarioBruto,
|
|
1138
|
+
valor_total_bruto: calc.valorTotalBruto,
|
|
1139
|
+
desconto_aplicado: calc.descontoAplicado,
|
|
1100
1140
|
};
|
|
1101
1141
|
}
|
|
1102
1142
|
function slimDespesa(d, comItens) {
|
|
@@ -1110,7 +1150,8 @@ function slimDespesa(d, comItens) {
|
|
|
1110
1150
|
valor_liquido: Number(d["ValorLiquido"] ?? 0),
|
|
1111
1151
|
};
|
|
1112
1152
|
if (comItens && itensRaw.length > 0) {
|
|
1113
|
-
|
|
1153
|
+
// Passa a despesa pai (d) pra distribuir descontos proporcionalmente
|
|
1154
|
+
result.itens = itensRaw.map(i => slimItemDespesa(i, d));
|
|
1114
1155
|
}
|
|
1115
1156
|
return result;
|
|
1116
1157
|
}
|
|
@@ -1179,19 +1220,23 @@ export async function buscarItensDespesa(opts = {}) {
|
|
|
1179
1220
|
const total = itens.length;
|
|
1180
1221
|
const inicio = (pagina - 1) * limite;
|
|
1181
1222
|
const resultado = itens.slice(inicio, inicio + limite).map(i => {
|
|
1182
|
-
const despesa = i["Despesa"] ??
|
|
1223
|
+
const despesa = i["Despesa"] ?? null;
|
|
1224
|
+
const calc = aplicarDescontoProporcional(i, despesa);
|
|
1183
1225
|
return {
|
|
1184
1226
|
descricao: String(i["DescricaoServProd"] ?? "").trim(),
|
|
1185
1227
|
codigo: i["Codigo"] ? String(i["Codigo"]).trim() : undefined,
|
|
1186
1228
|
quantidade: Number(i["Quantidade"] ?? 0),
|
|
1187
1229
|
unidade: i["UnidadeMedida"] ? String(i["UnidadeMedida"]).trim() : undefined,
|
|
1188
|
-
valor:
|
|
1230
|
+
valor: calc.precoUnitario, // REAL pago
|
|
1189
1231
|
desconto: Number(i["Desconto"] ?? 0),
|
|
1190
|
-
valor_total:
|
|
1232
|
+
valor_total: calc.valorTotal, // REAL pago
|
|
1233
|
+
valor_bruto: calc.precoUnitarioBruto,
|
|
1234
|
+
valor_total_bruto: calc.valorTotalBruto,
|
|
1235
|
+
desconto_aplicado: calc.descontoAplicado,
|
|
1191
1236
|
// Dados do documento pai — sem IDs
|
|
1192
|
-
data: String(despesa["Data"] ?? "").slice(0, 10),
|
|
1193
|
-
numero: String(despesa["Numero"] ?? "").trim(),
|
|
1194
|
-
fornecedor: String(despesa["PessoaNome"] ?? "").trim(),
|
|
1237
|
+
data: String(despesa?.["Data"] ?? "").slice(0, 10),
|
|
1238
|
+
numero: String(despesa?.["Numero"] ?? "").trim(),
|
|
1239
|
+
fornecedor: String(despesa?.["PessoaNome"] ?? "").trim(),
|
|
1195
1240
|
};
|
|
1196
1241
|
});
|
|
1197
1242
|
return {
|
|
@@ -1541,36 +1586,16 @@ export async function ultimoPrecoProduto(opts) {
|
|
|
1541
1586
|
const dataIso = String(it["DataAtualizacao"] ?? "");
|
|
1542
1587
|
const despesa = it["Despesa"] ?? null;
|
|
1543
1588
|
const data = dataIso || String(despesa?.["Data"] ?? "");
|
|
1544
|
-
const
|
|
1545
|
-
const valorBruto = Number(it["Valor"] ?? 0);
|
|
1546
|
-
const totalBruto = Number(it["ValorTotal"] ?? (qtd > 0 ? qtd * valorBruto : valorBruto));
|
|
1547
|
-
// Distribui descontos a nível de despesa proporcionalmente ao valor do item.
|
|
1548
|
-
// Despesa.Valor = total bruto; Despesa.ValorLiquido = líquido pago.
|
|
1549
|
-
// Fator = ValorLiquido / Valor (entre 0 e 1).
|
|
1550
|
-
let precoUnitarioReal = valorBruto;
|
|
1551
|
-
let totalReal = totalBruto;
|
|
1552
|
-
let descontoAplicado;
|
|
1553
|
-
let precoBrutoOut;
|
|
1554
|
-
if (despesa) {
|
|
1555
|
-
const despBruto = Number(despesa["Valor"] ?? 0);
|
|
1556
|
-
const despLiquido = Number(despesa["ValorLiquido"] ?? despBruto);
|
|
1557
|
-
if (despBruto > 0 && Math.abs(despBruto - despLiquido) > 0.01) {
|
|
1558
|
-
const fator = despLiquido / despBruto;
|
|
1559
|
-
totalReal = Math.round(totalBruto * fator * 100) / 100;
|
|
1560
|
-
precoUnitarioReal = qtd > 0 ? Math.round((totalReal / qtd) * 100) / 100 : valorBruto * fator;
|
|
1561
|
-
descontoAplicado = Math.round((totalBruto - totalReal) * 100) / 100;
|
|
1562
|
-
precoBrutoOut = valorBruto;
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1589
|
+
const calc = aplicarDescontoProporcional(it, despesa);
|
|
1565
1590
|
return {
|
|
1566
1591
|
data: data.slice(0, 10),
|
|
1567
1592
|
produto: nomeProd.trim(),
|
|
1568
1593
|
fornecedor: String(it["PessoaNome"] ?? despesa?.["PessoaNome"] ?? "").trim(),
|
|
1569
|
-
preco_unitario:
|
|
1570
|
-
quantidade:
|
|
1571
|
-
valor_total:
|
|
1572
|
-
preco_unitario_bruto:
|
|
1573
|
-
desconto_aplicado: descontoAplicado,
|
|
1594
|
+
preco_unitario: calc.precoUnitario,
|
|
1595
|
+
quantidade: Number(it["Quantidade"] ?? 0),
|
|
1596
|
+
valor_total: calc.valorTotal,
|
|
1597
|
+
preco_unitario_bruto: calc.precoUnitarioBruto,
|
|
1598
|
+
desconto_aplicado: calc.descontoAplicado,
|
|
1574
1599
|
moeda: it["MoedaNome"] ? String(it["MoedaNome"]) : undefined,
|
|
1575
1600
|
};
|
|
1576
1601
|
})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-context state holder usando AsyncLocalStorage.
|
|
3
|
+
*
|
|
4
|
+
* Objetivo: permitir multi-tenancy quando o MCP rodar como serviço HTTPS
|
|
5
|
+
* (cada requisição com sua própria sessão NEXT), mantendo o modo stdio
|
|
6
|
+
* (uma única sessão global lida de ~/.next-finance-mcp/session.json)
|
|
7
|
+
* funcionando exatamente igual ao atual.
|
|
8
|
+
*
|
|
9
|
+
* USO:
|
|
10
|
+
* - stdio: `enterDefaultCtx()` é chamado UMA vez no bootstrap; daí em
|
|
11
|
+
* diante todas as chamadas usam o contexto default.
|
|
12
|
+
* - http : `runInCtx(ctx, async () => { ... })` envolve cada handler
|
|
13
|
+
* da request com seu próprio contexto isolado.
|
|
14
|
+
*/
|
|
15
|
+
export interface Cookie {
|
|
16
|
+
name: string;
|
|
17
|
+
value: string;
|
|
18
|
+
domain: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ClientContext {
|
|
21
|
+
/** Cookies acumulados para o domínio finance.net.br (inclui cookieAutenticacao). */
|
|
22
|
+
cookies: Cookie[];
|
|
23
|
+
/** Estado de auth: true após login (web) bem-sucedido ou tryRestoreSession. */
|
|
24
|
+
loggedIn: boolean;
|
|
25
|
+
/** IdCarteira selecionada (ex: "Pa"). Usado para auto-refresh do token c=. */
|
|
26
|
+
selectedWallet: string | null;
|
|
27
|
+
/** Cache de fetchContas com TTL — invalidado em selecionar_carteira. */
|
|
28
|
+
contasCache: {
|
|
29
|
+
contas: unknown[];
|
|
30
|
+
fonte: string;
|
|
31
|
+
expira: number;
|
|
32
|
+
} | null;
|
|
33
|
+
/** Identificador opcional do tenant (para logs/telemetria no modo HTTP). */
|
|
34
|
+
tenantId?: string;
|
|
35
|
+
}
|
|
36
|
+
export declare function createCtx(init?: Partial<ClientContext>): ClientContext;
|
|
37
|
+
/** Lê o contexto da chamada atual. Falha se ninguém entrou em contexto antes. */
|
|
38
|
+
export declare function getCtx(): ClientContext;
|
|
39
|
+
/** Retorna o contexto atual sem lançar — útil para checagens defensivas. */
|
|
40
|
+
export declare function peekCtx(): ClientContext | undefined;
|
|
41
|
+
/** Executa `fn` com `ctx` ligado ao contexto async. Usado por handlers HTTP. */
|
|
42
|
+
export declare function runInCtx<T>(ctx: ClientContext, fn: () => Promise<T>): Promise<T>;
|
|
43
|
+
/** Modo stdio: define o contexto default UMA vez no startup. */
|
|
44
|
+
export declare function enterDefaultCtx(ctx?: ClientContext): void;
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-context state holder usando AsyncLocalStorage.
|
|
3
|
+
*
|
|
4
|
+
* Objetivo: permitir multi-tenancy quando o MCP rodar como serviço HTTPS
|
|
5
|
+
* (cada requisição com sua própria sessão NEXT), mantendo o modo stdio
|
|
6
|
+
* (uma única sessão global lida de ~/.next-finance-mcp/session.json)
|
|
7
|
+
* funcionando exatamente igual ao atual.
|
|
8
|
+
*
|
|
9
|
+
* USO:
|
|
10
|
+
* - stdio: `enterDefaultCtx()` é chamado UMA vez no bootstrap; daí em
|
|
11
|
+
* diante todas as chamadas usam o contexto default.
|
|
12
|
+
* - http : `runInCtx(ctx, async () => { ... })` envolve cada handler
|
|
13
|
+
* da request com seu próprio contexto isolado.
|
|
14
|
+
*/
|
|
15
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
16
|
+
export function createCtx(init) {
|
|
17
|
+
return {
|
|
18
|
+
cookies: init?.cookies ?? [],
|
|
19
|
+
loggedIn: init?.loggedIn ?? false,
|
|
20
|
+
selectedWallet: init?.selectedWallet ?? null,
|
|
21
|
+
contasCache: init?.contasCache ?? null,
|
|
22
|
+
tenantId: init?.tenantId,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const als = new AsyncLocalStorage();
|
|
26
|
+
/** Lê o contexto da chamada atual. Falha se ninguém entrou em contexto antes. */
|
|
27
|
+
export function getCtx() {
|
|
28
|
+
const ctx = als.getStore();
|
|
29
|
+
if (!ctx) {
|
|
30
|
+
throw new Error("ClientContext não inicializado. Chame enterDefaultCtx() (stdio) ou " +
|
|
31
|
+
"runInCtx(ctx, fn) (http) antes de invocar funções do client.");
|
|
32
|
+
}
|
|
33
|
+
return ctx;
|
|
34
|
+
}
|
|
35
|
+
/** Retorna o contexto atual sem lançar — útil para checagens defensivas. */
|
|
36
|
+
export function peekCtx() {
|
|
37
|
+
return als.getStore();
|
|
38
|
+
}
|
|
39
|
+
/** Executa `fn` com `ctx` ligado ao contexto async. Usado por handlers HTTP. */
|
|
40
|
+
export function runInCtx(ctx, fn) {
|
|
41
|
+
return als.run(ctx, fn);
|
|
42
|
+
}
|
|
43
|
+
/** Modo stdio: define o contexto default UMA vez no startup. */
|
|
44
|
+
export function enterDefaultCtx(ctx = createCtx()) {
|
|
45
|
+
als.enterWith(ctx);
|
|
46
|
+
}
|
package/dist/http-index.d.ts
CHANGED
|
@@ -10,4 +10,9 @@
|
|
|
10
10
|
* Opcional:
|
|
11
11
|
* PORT — porta do servidor (padrão: 3000)
|
|
12
12
|
*/
|
|
13
|
-
|
|
13
|
+
import { type ClientContext } from "./context.js";
|
|
14
|
+
/** Helper pronto para o futuro fluxo multi-tenant.
|
|
15
|
+
* Recebe Authorization: Basic <jwt> e cria um ClientContext isolado.
|
|
16
|
+
* Use runInCtx(buildCtxFromAuthHeader(req.headers.authorization), () => ...). */
|
|
17
|
+
export declare function buildCtxFromAuthHeader(authorization: string | undefined, tenantId?: string): ClientContext;
|
|
18
|
+
export declare function runMcpHandler<T>(authorization: string | undefined, fn: () => Promise<T>): Promise<T>;
|
package/dist/http-index.js
CHANGED
|
@@ -16,6 +16,28 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
|
|
|
16
16
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
17
17
|
import { randomUUID } from "crypto";
|
|
18
18
|
import * as client from "./client.js";
|
|
19
|
+
import { enterDefaultCtx, createCtx, runInCtx } from "./context.js";
|
|
20
|
+
// Modo HTTP single-tenant atual (env NEXT_FINANCE_EMAIL/PASSWORD) — ativa contexto único.
|
|
21
|
+
// TODO multi-tenant: substituir essa linha por wrap runInCtx() por requisição.
|
|
22
|
+
enterDefaultCtx();
|
|
23
|
+
/** Helper pronto para o futuro fluxo multi-tenant.
|
|
24
|
+
* Recebe Authorization: Basic <jwt> e cria um ClientContext isolado.
|
|
25
|
+
* Use runInCtx(buildCtxFromAuthHeader(req.headers.authorization), () => ...). */
|
|
26
|
+
export function buildCtxFromAuthHeader(authorization, tenantId) {
|
|
27
|
+
const ctx = createCtx({ tenantId });
|
|
28
|
+
if (authorization?.startsWith("Basic ")) {
|
|
29
|
+
const jwt = authorization.slice("Basic ".length);
|
|
30
|
+
// O JWT enviado vai pra Authorization Basic em apiGet/apiPost via getAuthHeader().
|
|
31
|
+
// Aqui só guardamos no contexto para que getCookieHeader retorne string compatível
|
|
32
|
+
// (mesmo que algumas chamadas web ainda usem cookies).
|
|
33
|
+
ctx.cookies.push({ name: "cookieAutenticacao", value: `tuf=${jwt}`, domain: "finance.net.br" });
|
|
34
|
+
ctx.loggedIn = true;
|
|
35
|
+
}
|
|
36
|
+
return ctx;
|
|
37
|
+
}
|
|
38
|
+
export async function runMcpHandler(authorization, fn) {
|
|
39
|
+
return runInCtx(buildCtxFromAuthHeader(authorization), fn);
|
|
40
|
+
}
|
|
19
41
|
// ── Configuração ─────────────────────────────────────────────────────────────
|
|
20
42
|
const PORT = parseInt(process.env.PORT ?? "3000", 10);
|
|
21
43
|
const API_KEY = process.env.API_KEY ?? "";
|
package/dist/index.js
CHANGED
|
@@ -7,8 +7,11 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
7
7
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
8
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
9
9
|
import * as client from "./client.js";
|
|
10
|
+
import { enterDefaultCtx } from "./context.js";
|
|
10
11
|
import { openLoginUI } from "./login-server.js";
|
|
11
|
-
|
|
12
|
+
// Modo stdio: ativa um contexto único global no startup. Multi-tenancy só no modo HTTP.
|
|
13
|
+
enterDefaultCtx();
|
|
14
|
+
const server = new Server({ name: "next-finance-mcp", version: "0.9.23" }, { capabilities: { tools: {} } });
|
|
12
15
|
// ── Ferramentas ─────────────────────────────────────────────────────────────
|
|
13
16
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
14
17
|
tools: [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "next-finance-mcp",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.23",
|
|
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",
|