next-finance-mcp 0.9.13 → 0.9.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.d.ts CHANGED
@@ -113,8 +113,15 @@ export declare function atualizarOpenFinance(nomeConta: string): Promise<{
113
113
  sucesso: boolean;
114
114
  mensagem: string;
115
115
  }>;
116
- /** Solicita o extrato OF (faz pull do banco para o período) — POST JSON web. */
117
- export declare function obterExtratoOpenFinance(nomeConta: string, dataInicio: string, dataFim: string): Promise<{
116
+ /** Abre janela local com QR do Pluggy para conta com MFA. */
117
+ export declare function iniciarOpenFinanceQR(nomeConta: string): Promise<{
118
+ conta: string;
119
+ status: string;
120
+ mensagem: string;
121
+ }>;
122
+ /** Solicita o extrato OF (faz pull do banco para o período) — POST JSON web.
123
+ * Período default: últimos 30 dias. */
124
+ export declare function obterExtratoOpenFinance(nomeConta: string, dataInicioOpt?: string, dataFimOpt?: string): Promise<{
118
125
  conta: string;
119
126
  sucesso: boolean;
120
127
  mensagem: string;
package/dist/client.js CHANGED
@@ -646,6 +646,18 @@ export async function listarTiposConta() {
646
646
  const tipos = arr.map(t => ({ nome: String(t["Nome"] ?? t["Descricao"] ?? "") })).filter(t => t.nome);
647
647
  return { tipos };
648
648
  }
649
+ // ── Helpers de datas padrão ──────────────────────────────────────────────────
650
+ // Default consistente em toda a API: últimos 30 dias terminando hoje.
651
+ function isoDate(d) { return d.toISOString().slice(0, 10); }
652
+ function defaultsUltimos30Dias(opts) {
653
+ const hoje = new Date();
654
+ const dataFim = opts.dataFim ?? isoDate(hoje);
655
+ // 30 dias atrás = hoje - 30 (inclusive)
656
+ const trintaDiasAtras = new Date(hoje);
657
+ trintaDiasAtras.setDate(trintaDiasAtras.getDate() - 30);
658
+ const dataInicio = opts.dataInicio ?? isoDate(trintaDiasAtras);
659
+ return { dataInicio, dataFim };
660
+ }
649
661
  /** Resolve um nome de conta → IdConta interno, exigindo match único (não expõe ID). */
650
662
  async function resolverIdConta(nome) {
651
663
  const { contas } = await fetchContas();
@@ -709,9 +721,49 @@ export async function atualizarOpenFinance(nomeConta) {
709
721
  }
710
722
  return { conta: nome, sucesso: true, mensagem: r.text?.trim() || "Sincronização iniciada." };
711
723
  }
712
- /** Solicita o extrato OF (faz pull do banco para o período) — POST JSON web. */
713
- export async function obterExtratoOpenFinance(nomeConta, dataInicio, dataFim) {
724
+ /** Pega o accessToken do Pluggy para iniciar o widget Connect. */
725
+ async function obterPluggyAccessToken(itemId) {
726
+ const r = await webGet(`/ConciliacaoAjax/ObterOpenFinanceAccessToken?itemId=${encodeURIComponent(itemId)}`);
727
+ if (r.status !== 200)
728
+ throw new Error(`Falha ao obter access token Pluggy (HTTP ${r.status}).`);
729
+ return r.text.trim().replace(/^"|"$/g, "");
730
+ }
731
+ /** Abre janela local com QR do Pluggy para conta com MFA. */
732
+ export async function iniciarOpenFinanceQR(nomeConta) {
733
+ const { contas } = await fetchContas();
734
+ const ativas = contas.filter(isContaAtiva);
735
+ const termo = nomeConta.trim().toLowerCase();
736
+ const matches = ativas.filter(c => String(c["Nome"] ?? c["NomeConta"] ?? "").toLowerCase().includes(termo));
737
+ if (matches.length === 0)
738
+ throw new Error(`Conta "${nomeConta}" não encontrada.`);
739
+ const exata = matches.find(c => String(c["Nome"] ?? c["NomeConta"] ?? "").toLowerCase() === termo);
740
+ const conta = exata ?? (matches.length === 1 ? matches[0] : null);
741
+ if (!conta)
742
+ throw new Error(`"${nomeConta}" corresponde a ${matches.length} contas. Seja mais específico.`);
743
+ const itemId = String(conta["OpenFinanceId"] ?? "");
744
+ if (!itemId)
745
+ throw new Error(`Conta "${nomeConta}" não tem vínculo Open Finance.`);
746
+ const nomeFmt = String(conta["Nome"] ?? "");
747
+ const banco = String(conta["NomeBanco"] ?? "Banco");
748
+ const accessToken = await obterPluggyAccessToken(itemId);
749
+ const { abrirQrPluggy } = await import("./qr-server.js");
750
+ const result = await abrirQrPluggy(accessToken, itemId, banco, nomeFmt);
751
+ if (result.status === "success") {
752
+ return { conta: nomeFmt, status: "success", mensagem: "Autenticação Open Finance concluída. Use 'obter_extrato_open_finance' para puxar o extrato." };
753
+ }
754
+ if (result.status === "timeout") {
755
+ return { conta: nomeFmt, status: "timeout", mensagem: "Tempo esgotado (5 min). Tente novamente." };
756
+ }
757
+ if (result.status === "error") {
758
+ return { conta: nomeFmt, status: "error", mensagem: `Erro no widget Pluggy: ${result.error ?? "desconhecido"}` };
759
+ }
760
+ return { conta: nomeFmt, status: "closed", mensagem: "Janela fechada antes de concluir a autenticação." };
761
+ }
762
+ /** Solicita o extrato OF (faz pull do banco para o período) — POST JSON web.
763
+ * Período default: últimos 30 dias. */
764
+ export async function obterExtratoOpenFinance(nomeConta, dataInicioOpt, dataFimOpt) {
714
765
  const { id, nome } = await resolverIdConta(nomeConta);
766
+ const { dataInicio, dataFim } = defaultsUltimos30Dias({ dataInicio: dataInicioOpt, dataFim: dataFimOpt });
715
767
  // Endpoint web real (verificado): POST application/json com DataInicio/DataFim em ISO datetime
716
768
  const body = JSON.stringify({
717
769
  DataInicio: `${dataInicio}T00:00:00`,
@@ -819,9 +871,7 @@ function slimDespesa(d, comItens) {
819
871
  return result;
820
872
  }
821
873
  export async function buscarDespesas(opts = {}) {
822
- const today = new Date();
823
- const dataInicio = opts.dataInicio ?? `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-01`;
824
- const dataFim = opts.dataFim ?? today.toISOString().slice(0, 10);
874
+ const { dataInicio, dataFim } = defaultsUltimos30Dias(opts);
825
875
  // API exige formato ISO completo com hora
826
876
  const raw = await apiPost("/api/Despesa/Filtrar", {
827
877
  DataInicio: dataInicio + "T00:00:00.000Z",
@@ -855,9 +905,7 @@ export async function buscarDespesas(opts = {}) {
855
905
  };
856
906
  }
857
907
  export async function buscarItensDespesa(opts = {}) {
858
- const today = new Date();
859
- const dataInicio = opts.dataInicio ?? `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-01`;
860
- const dataFim = opts.dataFim ?? today.toISOString().slice(0, 10);
908
+ const { dataInicio, dataFim } = defaultsUltimos30Dias(opts);
861
909
  // Resolve nome do fornecedor → ID interno (nunca exposto)
862
910
  let pessoaId;
863
911
  if (opts.nomeFornecedor?.trim()) {
@@ -982,9 +1030,7 @@ const TIPOS_CONTA_MOVIMENTO = new Set([
982
1030
  "contas em cobrança", "contas em cobranca",
983
1031
  ]);
984
1032
  export async function buscarLancamentos(opts) {
985
- const today = new Date();
986
- const dataInicio = opts.dataInicio ?? `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-01`;
987
- const dataFim = opts.dataFim ?? today.toISOString().slice(0, 10);
1033
+ const { dataInicio, dataFim } = defaultsUltimos30Dias(opts);
988
1034
  const pagina = Math.max(1, opts.pagina ?? 1);
989
1035
  const limite = Math.min(200, Math.max(1, opts.limite ?? 50));
990
1036
  // Resolve nome da conta → ID interno (nunca exposto ao usuário)
package/dist/index.js CHANGED
@@ -8,7 +8,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
10
  import { openLoginUI } from "./login-server.js";
11
- const server = new Server({ name: "next-finance-mcp", version: "0.9.13" }, { capabilities: { tools: {} } });
11
+ const server = new Server({ name: "next-finance-mcp", version: "0.9.15" }, { capabilities: { tools: {} } });
12
12
  // ── Ferramentas ─────────────────────────────────────────────────────────────
13
13
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
14
14
  tools: [
@@ -100,7 +100,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
100
100
  type: "object",
101
101
  properties: {
102
102
  nome_conta: { type: "string", description: "Nome da conta (opcional — omitir busca em todas as contas, ex: 'Nubank', 'Inter')" },
103
- data_inicio: { type: "string", description: "Data início YYYY-MM-DD (padrão: do mês atual)" },
103
+ data_inicio: { type: "string", description: "Data início YYYY-MM-DD (padrão: 30 dias atrás)" },
104
104
  data_fim: { type: "string", description: "Data fim YYYY-MM-DD (padrão: hoje)" },
105
105
  plano_de_contas: { type: "string", description: "Filtra por categoria/plano de contas (ex: 'Restaurante')" },
106
106
  centro_de_custo: { type: "string", description: "Filtra por centro de custo" },
@@ -157,20 +157,31 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
157
157
  required: ["nome_conta"],
158
158
  },
159
159
  },
160
+ {
161
+ name: "iniciar_open_finance_qr",
162
+ description: "Abre uma janela local no navegador com o widget Pluggy Connect para autenticar uma conta Open Finance que exige MFA (ex: Inter, que pede leitura de QR Code no Super App). Após o usuário concluir a autenticação, a janela fecha sozinha e a conta fica pronta para 'obter_extrato_open_finance'.",
163
+ inputSchema: {
164
+ type: "object",
165
+ properties: {
166
+ nome_conta: { type: "string", description: "Nome da conta (ex: 'Inter CC 651549')" },
167
+ },
168
+ required: ["nome_conta"],
169
+ },
170
+ },
160
171
  {
161
172
  name: "obter_extrato_open_finance",
162
- description: "Solicita ao banco (via Open Finance) o extrato completo de uma conta no período. " +
163
- "Diferente de 'atualizar_open_finance' (que só pega o mais recente), este endpoint " +
164
- "permite especificar um intervalo de datas. Após sucesso, use 'buscar_lancamentos' " +
165
- "para ver os lançamentos importados.",
173
+ description: "Solicita ao banco (via Open Finance) o extrato de uma conta no período. " +
174
+ "Diferente de 'atualizar_open_finance' (que só pega o mais recente), permite " +
175
+ "especificar um intervalo de datas. Default: últimos 30 dias. Após sucesso, " +
176
+ "a conciliação fica pendente no NEXT — finalize no app para os lançamentos aparecerem em 'buscar_lancamentos'.",
166
177
  inputSchema: {
167
178
  type: "object",
168
179
  properties: {
169
180
  nome_conta: { type: "string", description: "Nome da conta (ex: 'Inter CC 651549')" },
170
- data_inicio: { type: "string", description: "Data início YYYY-MM-DD" },
171
- data_fim: { type: "string", description: "Data fim YYYY-MM-DD" },
181
+ data_inicio: { type: "string", description: "Data início YYYY-MM-DD (padrão: 30 dias atrás)" },
182
+ data_fim: { type: "string", description: "Data fim YYYY-MM-DD (padrão: hoje)" },
172
183
  },
173
- required: ["nome_conta", "data_inicio", "data_fim"],
184
+ required: ["nome_conta"],
174
185
  },
175
186
  },
176
187
  {
@@ -182,7 +193,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
182
193
  inputSchema: {
183
194
  type: "object",
184
195
  properties: {
185
- data_inicio: { type: "string", description: "Data início YYYY-MM-DD (padrão: do mês atual)" },
196
+ data_inicio: { type: "string", description: "Data início YYYY-MM-DD (padrão: 30 dias atrás)" },
186
197
  data_fim: { type: "string", description: "Data fim YYYY-MM-DD (padrão: hoje)" },
187
198
  nome_fornecedor: { type: "string", description: "Filtra por nome do fornecedor (ex: 'Supermercados BH', 'Epa')" },
188
199
  busca: { type: "string", description: "Filtra por número do documento ou tipo" },
@@ -377,6 +388,15 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
377
388
  return walErr;
378
389
  return ok(await client.atualizarOpenFinance(args["nome_conta"]));
379
390
  }
391
+ case "iniciar_open_finance_qr": {
392
+ const authErr = requireAuth();
393
+ if (authErr)
394
+ return authErr;
395
+ const walErr = requireWallet();
396
+ if (walErr)
397
+ return walErr;
398
+ return ok(await client.iniciarOpenFinanceQR(args["nome_conta"]));
399
+ }
380
400
  case "obter_extrato_open_finance": {
381
401
  const authErr = requireAuth();
382
402
  if (authErr)
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Servidor HTTP local para exibir o QR Code do Pluggy Connect.
3
+ * Usado quando uma conta Open Finance exige MFA (ex: Inter).
4
+ *
5
+ * Fluxo:
6
+ * 1. MCP busca o accessToken do Pluggy via ConciliacaoAjax/ObterOpenFinanceAccessToken
7
+ * 2. Inicia servidor HTTP local com página que carrega o widget Pluggy Connect
8
+ * 3. Abre o browser na URL local
9
+ * 4. Widget Pluggy renderiza o QR; usuário escaneia no app do banco
10
+ * 5. Após sucesso, a página POSTa /done de volta para o servidor → MCP encerra e retorna
11
+ */
12
+ export interface QrSessionResult {
13
+ status: "success" | "error" | "closed" | "timeout";
14
+ itemId?: string;
15
+ error?: string;
16
+ }
17
+ /** Abre o widget Pluggy no browser local e espera o usuário concluir o MFA. */
18
+ export declare function abrirQrPluggy(accessToken: string, itemId: string, banco: string, conta: string, timeoutMs?: number): Promise<QrSessionResult>;
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Servidor HTTP local para exibir o QR Code do Pluggy Connect.
3
+ * Usado quando uma conta Open Finance exige MFA (ex: Inter).
4
+ *
5
+ * Fluxo:
6
+ * 1. MCP busca o accessToken do Pluggy via ConciliacaoAjax/ObterOpenFinanceAccessToken
7
+ * 2. Inicia servidor HTTP local com página que carrega o widget Pluggy Connect
8
+ * 3. Abre o browser na URL local
9
+ * 4. Widget Pluggy renderiza o QR; usuário escaneia no app do banco
10
+ * 5. Após sucesso, a página POSTa /done de volta para o servidor → MCP encerra e retorna
11
+ */
12
+ import * as http from "http";
13
+ import * as os from "os";
14
+ import { exec } from "child_process";
15
+ function openBrowser(url) {
16
+ const platform = os.platform();
17
+ const cmd = platform === "darwin" ? `open "${url}"` :
18
+ platform === "win32" ? `start "" "${url}"` :
19
+ `xdg-open "${url}"`;
20
+ exec(cmd, (err) => {
21
+ if (err)
22
+ console.error("Não foi possível abrir o browser:", err.message);
23
+ });
24
+ }
25
+ function buildHtml(accessToken, itemId, banco, conta) {
26
+ return `<!DOCTYPE html>
27
+ <html lang="pt-br">
28
+ <head>
29
+ <meta charset="utf-8">
30
+ <meta name="viewport" content="width=device-width, initial-scale=1">
31
+ <title>Open Finance — ${conta}</title>
32
+ <style>
33
+ * { box-sizing: border-box; margin: 0; padding: 0; }
34
+ body {
35
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
36
+ background: #f0f2f5; min-height: 100vh;
37
+ display: flex; align-items: center; justify-content: center;
38
+ }
39
+ .card {
40
+ background: #fff; border-radius: 12px;
41
+ box-shadow: 0 4px 24px rgba(0,0,0,.10);
42
+ padding: 32px; width: 480px; text-align: center;
43
+ }
44
+ h1 { color: #1a73e8; font-size: 20px; margin-bottom: 8px; }
45
+ .sub { color: #666; font-size: 13px; margin-bottom: 24px; }
46
+ .status {
47
+ padding: 16px; border-radius: 8px; margin-top: 20px;
48
+ font-size: 14px; line-height: 1.5;
49
+ }
50
+ .status.info { background: #e8f0fe; color: #1a73e8; }
51
+ .status.success { background: #e6f4ea; color: #137333; }
52
+ .status.error { background: #fce8e6; color: #c5221f; }
53
+ .btn {
54
+ margin-top: 16px; padding: 10px 20px; background: #1a73e8; color: #fff;
55
+ border: none; border-radius: 6px; font-size: 14px; cursor: pointer;
56
+ }
57
+ </style>
58
+ </head>
59
+ <body>
60
+ <div class="card">
61
+ <h1>Conectar via Open Finance</h1>
62
+ <p class="sub"><strong>${banco}</strong> — ${conta}</p>
63
+ <div id="status" class="status info">Carregando o widget do Pluggy…</div>
64
+ </div>
65
+
66
+ <script src="https://cdn.pluggy.ai/pluggy-connect/v2.10.0/pluggy-connect.js"></script>
67
+ <script>
68
+ const ACCESS_TOKEN = ${JSON.stringify(accessToken)};
69
+ const ITEM_ID = ${JSON.stringify(itemId)};
70
+ const statusEl = document.getElementById("status");
71
+ function setStatus(text, kind) { statusEl.textContent = text; statusEl.className = "status " + kind; }
72
+ function notifyServer(payload) {
73
+ fetch("/done", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify(payload) }).catch(() => {});
74
+ }
75
+ try {
76
+ const pc = new PluggyConnect({
77
+ connectToken: ACCESS_TOKEN,
78
+ updateItem: ITEM_ID,
79
+ includeSandbox: false,
80
+ onSuccess: (data) => {
81
+ setStatus("✅ Autenticação concluída! Você já pode fechar esta janela.", "success");
82
+ notifyServer({ status: "success", itemId: data.item?.id ?? ITEM_ID });
83
+ },
84
+ onError: (err) => {
85
+ setStatus("❌ Erro: " + (err.message || JSON.stringify(err)), "error");
86
+ notifyServer({ status: "error", error: err.message || String(err) });
87
+ },
88
+ onClose: () => {
89
+ setStatus("Widget fechado. Se concluiu a autenticação, pode fechar esta janela.", "info");
90
+ notifyServer({ status: "closed" });
91
+ },
92
+ });
93
+ pc.init();
94
+ setStatus("Aponte a câmera do app do banco para o QR Code que vai aparecer.", "info");
95
+ } catch (e) {
96
+ setStatus("Erro ao iniciar widget Pluggy: " + e.message, "error");
97
+ notifyServer({ status: "error", error: e.message });
98
+ }
99
+ </script>
100
+ </body>
101
+ </html>`;
102
+ }
103
+ /** Abre o widget Pluggy no browser local e espera o usuário concluir o MFA. */
104
+ export function abrirQrPluggy(accessToken, itemId, banco, conta, timeoutMs = 5 * 60 * 1000) {
105
+ return new Promise((resolve) => {
106
+ let resolved = false;
107
+ const resolveOnce = (r) => {
108
+ if (resolved)
109
+ return;
110
+ resolved = true;
111
+ try {
112
+ server.close();
113
+ }
114
+ catch { }
115
+ clearTimeout(timer);
116
+ resolve(r);
117
+ };
118
+ const server = http.createServer((req, res) => {
119
+ if (req.method === "GET" && (req.url === "/" || req.url === "/index.html")) {
120
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
121
+ res.end(buildHtml(accessToken, itemId, banco, conta));
122
+ return;
123
+ }
124
+ if (req.method === "POST" && req.url === "/done") {
125
+ let body = "";
126
+ req.on("data", chunk => body += chunk);
127
+ req.on("end", () => {
128
+ res.writeHead(200, { "Content-Type": "application/json" });
129
+ res.end(`{"ok":true}`);
130
+ try {
131
+ const data = JSON.parse(body);
132
+ if (data.status === "success")
133
+ resolveOnce({ status: "success", itemId: data.itemId });
134
+ else if (data.status === "error")
135
+ resolveOnce({ status: "error", error: data.error });
136
+ else if (data.status === "closed") {
137
+ // Closed sem success: mantemos servidor por mais alguns segundos caso o user reabra
138
+ // Mas pra simplificar, vamos fechar mesmo
139
+ setTimeout(() => resolveOnce({ status: "closed" }), 1000);
140
+ }
141
+ }
142
+ catch { /* ignora payload inválido */ }
143
+ });
144
+ return;
145
+ }
146
+ res.writeHead(404);
147
+ res.end("Not found");
148
+ });
149
+ server.listen(0, "127.0.0.1", () => {
150
+ const addr = server.address();
151
+ if (!addr || typeof addr === "string") {
152
+ resolveOnce({ status: "error", error: "Falha ao iniciar servidor local" });
153
+ return;
154
+ }
155
+ const url = `http://127.0.0.1:${addr.port}/`;
156
+ openBrowser(url);
157
+ });
158
+ const timer = setTimeout(() => resolveOnce({ status: "timeout" }), timeoutMs);
159
+ });
160
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-finance-mcp",
3
- "version": "0.9.13",
3
+ "version": "0.9.15",
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",