next-finance-mcp 0.3.1 → 0.4.0
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 +32 -3
- package/dist/client.js +82 -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 +8 -3
package/dist/client.d.ts
CHANGED
|
@@ -13,9 +13,38 @@ 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: unknown[];
|
|
31
|
+
paginacao: PaginaMeta;
|
|
32
|
+
}
|
|
33
|
+
export declare function listarContas(filtro?: FiltroContas): Promise<ResultadoContas>;
|
|
34
|
+
export interface FiltroLancamentos {
|
|
18
35
|
idConta: string;
|
|
19
36
|
dataInicio?: string;
|
|
20
37
|
dataFim?: string;
|
|
21
|
-
|
|
38
|
+
busca?: string;
|
|
39
|
+
pagina?: number;
|
|
40
|
+
limite?: number;
|
|
41
|
+
}
|
|
42
|
+
export interface ResultadoLancamentos {
|
|
43
|
+
lancamentos: unknown[];
|
|
44
|
+
paginacao: PaginaMeta;
|
|
45
|
+
periodo: {
|
|
46
|
+
inicio: string;
|
|
47
|
+
fim: string;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export declare function buscarLancamentos(opts: FiltroLancamentos): Promise<ResultadoLancamentos>;
|
package/dist/client.js
CHANGED
|
@@ -181,31 +181,98 @@ 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
|
-
export async function listarContas() {
|
|
184
|
+
export async function listarContas(filtro = {}) {
|
|
186
185
|
const resp = await webGet("/ContaAjax/ListarTodos");
|
|
187
186
|
if (resp.status !== 200)
|
|
188
187
|
throw new Error(`Erro ${resp.status} ao listar contas.`);
|
|
189
|
-
|
|
188
|
+
let contas = JSON.parse(resp.text);
|
|
189
|
+
// Garante array (API pode retornar objeto em algumas versões)
|
|
190
|
+
if (!Array.isArray(contas))
|
|
191
|
+
contas = Object.values(contas);
|
|
192
|
+
// Filtro por busca textual (nome, descrição — busca em qualquer campo string)
|
|
193
|
+
if (filtro.busca?.trim()) {
|
|
194
|
+
const termo = filtro.busca.trim().toLowerCase();
|
|
195
|
+
contas = contas.filter(c => {
|
|
196
|
+
const texto = Object.values(c)
|
|
197
|
+
.filter(v => typeof v === "string")
|
|
198
|
+
.join(" ")
|
|
199
|
+
.toLowerCase();
|
|
200
|
+
return texto.includes(termo);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
// Filtro por tipo(s)
|
|
204
|
+
if (filtro.tipos && filtro.tipos.length > 0) {
|
|
205
|
+
const tipos = new Set(filtro.tipos);
|
|
206
|
+
contas = contas.filter(c => {
|
|
207
|
+
const obj = c;
|
|
208
|
+
// tenta campos comuns: Tipo, IdTipo, TipoConta
|
|
209
|
+
const tipo = obj["Tipo"] ?? obj["IdTipo"] ?? obj["TipoConta"] ?? obj["tipo"];
|
|
210
|
+
return tipo != null && tipos.has(Number(tipo));
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
// Paginação
|
|
214
|
+
const pagina = Math.max(1, filtro.pagina ?? 1);
|
|
215
|
+
const limite = Math.min(200, Math.max(1, filtro.limite ?? 30));
|
|
216
|
+
const total = contas.length;
|
|
217
|
+
const inicio = (pagina - 1) * limite;
|
|
218
|
+
const slice = contas.slice(inicio, inicio + limite);
|
|
219
|
+
return {
|
|
220
|
+
contas: slice,
|
|
221
|
+
paginacao: {
|
|
222
|
+
pagina,
|
|
223
|
+
limite,
|
|
224
|
+
total,
|
|
225
|
+
paginas: Math.ceil(total / limite),
|
|
226
|
+
tem_mais: inicio + limite < total,
|
|
227
|
+
},
|
|
228
|
+
};
|
|
190
229
|
}
|
|
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
230
|
export async function buscarLancamentos(opts) {
|
|
198
|
-
// DataInicio e DataFim são obrigatórios pela API; usar mês atual como default
|
|
199
231
|
const today = new Date();
|
|
200
|
-
const
|
|
201
|
-
const
|
|
232
|
+
const dataInicio = opts.dataInicio ?? `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-01`;
|
|
233
|
+
const dataFim = opts.dataFim ?? today.toISOString().slice(0, 10);
|
|
202
234
|
const params = new URLSearchParams();
|
|
203
235
|
params.set("filtro[IdConta]", opts.idConta);
|
|
204
|
-
params.set("filtro[IntervaloDeDatas][DataInicio]",
|
|
205
|
-
params.set("filtro[IntervaloDeDatas][DataFim]",
|
|
236
|
+
params.set("filtro[IntervaloDeDatas][DataInicio]", dataInicio);
|
|
237
|
+
params.set("filtro[IntervaloDeDatas][DataFim]", dataFim);
|
|
206
238
|
params.set("filtro[ListarOrdenadoPelaDataDeModificacao]", "false");
|
|
207
239
|
const resp = await webPost("/LancamentoAjax/Filtrar", params.toString(), "application/x-www-form-urlencoded");
|
|
208
240
|
if (resp.status !== 200)
|
|
209
241
|
throw new Error(`Erro ${resp.status}: ${resp.text.slice(0, 200)}`);
|
|
210
|
-
|
|
242
|
+
let lancamentos = JSON.parse(resp.text);
|
|
243
|
+
if (!Array.isArray(lancamentos))
|
|
244
|
+
lancamentos = Object.values(lancamentos);
|
|
245
|
+
// Filtro por busca textual na descrição/histórico
|
|
246
|
+
if (opts.busca?.trim()) {
|
|
247
|
+
const termo = opts.busca.trim().toLowerCase();
|
|
248
|
+
lancamentos = lancamentos.filter(l => {
|
|
249
|
+
const obj = l;
|
|
250
|
+
// campos típicos de descrição em ERPs
|
|
251
|
+
const texto = [
|
|
252
|
+
obj["Descricao"], obj["Historico"], obj["Observacao"],
|
|
253
|
+
obj["descricao"], obj["historico"], obj["observacao"],
|
|
254
|
+
]
|
|
255
|
+
.filter(v => typeof v === "string")
|
|
256
|
+
.join(" ")
|
|
257
|
+
.toLowerCase();
|
|
258
|
+
return texto.includes(termo);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
// Paginação
|
|
262
|
+
const pagina = Math.max(1, opts.pagina ?? 1);
|
|
263
|
+
const limite = Math.min(500, Math.max(1, opts.limite ?? 50));
|
|
264
|
+
const total = lancamentos.length;
|
|
265
|
+
const inicio = (pagina - 1) * limite;
|
|
266
|
+
const slice = lancamentos.slice(inicio, inicio + limite);
|
|
267
|
+
return {
|
|
268
|
+
lancamentos: slice,
|
|
269
|
+
paginacao: {
|
|
270
|
+
pagina,
|
|
271
|
+
limite,
|
|
272
|
+
total,
|
|
273
|
+
paginas: Math.ceil(total / limite),
|
|
274
|
+
tem_mais: inicio + limite < total,
|
|
275
|
+
},
|
|
276
|
+
periodo: { inicio: dataInicio, fim: dataFim },
|
|
277
|
+
};
|
|
211
278
|
}
|
|
@@ -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,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "next-finance-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"mcpName": "io.github.paivapiovesan/next-finance",
|
|
4
5
|
"description": "MCP Server para o NEXT Finance (finance.net.br) — login via browser, listagem de carteiras, contas e lançamentos",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"main": "dist/index.js",
|
|
7
8
|
"bin": {
|
|
8
|
-
"next-finance-mcp": "dist/index.js"
|
|
9
|
+
"next-finance-mcp": "dist/index.js",
|
|
10
|
+
"next-finance-mcp-server": "dist/http-index.js"
|
|
9
11
|
},
|
|
10
12
|
"files": [
|
|
11
13
|
"dist"
|
|
@@ -14,6 +16,7 @@
|
|
|
14
16
|
"build": "tsc",
|
|
15
17
|
"dev": "tsx src/index.ts",
|
|
16
18
|
"start": "node dist/index.js",
|
|
19
|
+
"start:server": "node dist/http-index.js",
|
|
17
20
|
"prepublishOnly": "npm run build"
|
|
18
21
|
},
|
|
19
22
|
"keywords": [
|
|
@@ -35,9 +38,11 @@
|
|
|
35
38
|
"node": ">=18"
|
|
36
39
|
},
|
|
37
40
|
"dependencies": {
|
|
38
|
-
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
42
|
+
"express": "^5.2.1"
|
|
39
43
|
},
|
|
40
44
|
"devDependencies": {
|
|
45
|
+
"@types/express": "^5.0.6",
|
|
41
46
|
"@types/node": "^22.0.0",
|
|
42
47
|
"tsx": "^4.0.0",
|
|
43
48
|
"typescript": "^5.0.0"
|