paisa-mcp 0.3.1 → 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/index.js CHANGED
@@ -38033,6 +38033,44 @@ var inputRequired = Object.assign(buildInputRequired, {
38033
38033
  return { method: "roots/list" };
38034
38034
  }
38035
38035
  });
38036
+ function acceptedContent(responses, key, schema) {
38037
+ const view = inputResponse(responses, key);
38038
+ if (view.kind !== "elicit" || view.action !== "accept" || view.content === undefined)
38039
+ return;
38040
+ if (schema === undefined)
38041
+ return view.content;
38042
+ const outcome = schema["~standard"].validate(view.content);
38043
+ if (outcome instanceof Promise)
38044
+ throw new TypeError("acceptedContent(responses, key, schema) requires a synchronously-validating schema");
38045
+ return outcome.issues === undefined ? outcome.value : undefined;
38046
+ }
38047
+ function inputResponse(responses, key) {
38048
+ if (responses === undefined || typeof responses !== "object" || responses === null)
38049
+ return { kind: "missing" };
38050
+ const entry = responses[key];
38051
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry))
38052
+ return { kind: "missing" };
38053
+ const candidate = entry;
38054
+ if (candidate["action"] === "accept" || candidate["action"] === "decline" || candidate["action"] === "cancel") {
38055
+ const content = candidate["content"];
38056
+ return {
38057
+ kind: "elicit",
38058
+ action: candidate["action"],
38059
+ ...content !== null && typeof content === "object" && !Array.isArray(content) && { content }
38060
+ };
38061
+ }
38062
+ if (Array.isArray(candidate["roots"]))
38063
+ return {
38064
+ kind: "roots",
38065
+ roots: candidate["roots"]
38066
+ };
38067
+ if (typeof candidate["role"] === "string" && candidate["content"] !== undefined)
38068
+ return {
38069
+ kind: "sampling",
38070
+ result: candidate
38071
+ };
38072
+ return { kind: "missing" };
38073
+ }
38036
38074
  var REQUEST_STATE_ONLY_LEG_PACING_MS = 250;
38037
38075
  function inputRequiredRoundsExceededMessage(method, maxRounds) {
38038
38076
  return `Multi-round-trip request '${method}' still required input after ${maxRounds} rounds (inputRequired.maxRounds)`;
@@ -53404,7 +53442,24 @@ async function initCrypto(client, pin, userId) {
53404
53442
  // ../../node_modules/.bun/@modelcontextprotocol+server@2.1.0/node_modules/@modelcontextprotocol/server/dist/index.mjs
53405
53443
  var DEFAULT_MAX_REQUEST_BODY_SIZE = 4 * 1024 * 1024;
53406
53444
 
53407
- // ../../node_modules/.bun/@nimit9+signet-server@0.2.3+e803b2305afdb064/node_modules/@nimit9/signet-server/dist/http/status-codes.js
53445
+ // ../../node_modules/.bun/@nimit9+signet-server@0.2.6+e803b2305afdb064/node_modules/@nimit9/signet-server/dist/mcp/interaction.js
53446
+ async function progress(ctx, done, total, message) {
53447
+ const token = ctx.mcp.mcpReq._meta?.progressToken;
53448
+ if (token === undefined)
53449
+ return;
53450
+ try {
53451
+ await ctx.mcp.mcpReq.notify({
53452
+ method: "notifications/progress",
53453
+ params: {
53454
+ progressToken: token,
53455
+ progress: done,
53456
+ ...total !== undefined ? { total } : {},
53457
+ ...message ? { message } : {}
53458
+ }
53459
+ });
53460
+ } catch {}
53461
+ }
53462
+ // ../../node_modules/.bun/@nimit9+signet-server@0.2.6+e803b2305afdb064/node_modules/@nimit9/signet-server/dist/http/status-codes.js
53408
53463
  var STATUS_CODES = {
53409
53464
  400: "BAD_REQUEST",
53410
53465
  401: "UNAUTHORIZED",
@@ -53418,14 +53473,7 @@ var STATUS_CODES = {
53418
53473
  429: "RATE_LIMITED"
53419
53474
  };
53420
53475
 
53421
- // ../../node_modules/.bun/@nimit9+signet-server@0.2.3+e803b2305afdb064/node_modules/@nimit9/signet-server/dist/mcp/server.js
53422
- var RAW = Symbol.for("signet.mcp.rawResult");
53423
- function toolResult(result) {
53424
- return Object.defineProperty({ ...result }, RAW, { value: true, enumerable: false });
53425
- }
53426
- function defineTool(def) {
53427
- return def;
53428
- }
53476
+ // ../../node_modules/.bun/@nimit9+signet-server@0.2.6+e803b2305afdb064/node_modules/@nimit9/signet-server/dist/mcp/errors.js
53429
53477
  function isZodError(err) {
53430
53478
  return err instanceof Error && err.name === "ZodError" && Array.isArray(err.issues);
53431
53479
  }
@@ -53474,10 +53522,10 @@ function toolErrorResult(info) {
53474
53522
  };
53475
53523
  }
53476
53524
  var INTERNAL_ERROR2 = { code: "INTERNAL_ERROR", message: "Internal error" };
53477
- function report(opts, err, tool, c) {
53525
+ function report(opts, err, meta3, c) {
53478
53526
  let pending;
53479
53527
  try {
53480
- pending = opts.onError?.(err, { tool });
53528
+ pending = opts.onError?.(err, meta3);
53481
53529
  } catch {
53482
53530
  return;
53483
53531
  }
@@ -53488,9 +53536,162 @@ function report(opts, err, tool, c) {
53488
53536
  c?.executionCtx.waitUntil(settled);
53489
53537
  } catch {}
53490
53538
  }
53491
- function toResult(value, structured) {
53539
+
53540
+ // ../../node_modules/.bun/@nimit9+signet-server@0.2.6+e803b2305afdb064/node_modules/@nimit9/signet-server/dist/mcp/primitives.js
53541
+ var RAW = Symbol.for("signet.mcp.rawResult");
53542
+ function protocolError(err, deps, meta3) {
53543
+ if (ProtocolError.isInstance(err))
53544
+ return err;
53545
+ const info = deps.options.mapError?.(err) ?? describeToolError(err);
53546
+ if (!info) {
53547
+ report(deps.options, err, meta3, deps.context.c);
53548
+ return new ProtocolError(ProtocolErrorCode.InternalError, "Internal error");
53549
+ }
53550
+ const status = isHttpException(err) ? err.status : isZodError(err) ? 400 : undefined;
53551
+ const code = status !== undefined && status >= 500 ? ProtocolErrorCode.InternalError : ProtocolErrorCode.InvalidParams;
53552
+ return new ProtocolError(code, `${info.code}: ${info.message}`, {
53553
+ success: false,
53554
+ error: info.message,
53555
+ code: info.code,
53556
+ ...info.details ? { details: info.details } : {}
53557
+ });
53558
+ }
53559
+ function checkScopes(scopes, token) {
53560
+ if (!scopes?.length)
53561
+ return;
53562
+ const missing = scopes.filter((s) => !token?.scopes.includes(s));
53563
+ if (!missing.length)
53564
+ return;
53565
+ const message = `This token is missing the scope: ${missing.join(" ")}. Request a new token with it and try again.`;
53566
+ throw new ProtocolError(ProtocolErrorCode.InvalidRequest, `insufficient_scope: ${message}`, {
53567
+ success: false,
53568
+ error: message,
53569
+ code: "insufficient_scope"
53570
+ });
53571
+ }
53572
+ function baseContext(mcp, deps) {
53573
+ const { c, token } = deps.context;
53574
+ return { signal: mcp.mcpReq.signal, mcp, ...c ? { c } : {}, ...token ? { token } : {} };
53575
+ }
53576
+ function toContents(uri, value, mimeType) {
53492
53577
  if (value !== null && typeof value === "object" && RAW in value)
53493
53578
  return value;
53579
+ if (typeof value === "string")
53580
+ return { contents: [{ uri: uri.href, mimeType: mimeType ?? "text/plain", text: value }] };
53581
+ if (value instanceof Uint8Array) {
53582
+ let bin = "";
53583
+ for (const b of value)
53584
+ bin += String.fromCharCode(b);
53585
+ return {
53586
+ contents: [
53587
+ { uri: uri.href, mimeType: mimeType ?? "application/octet-stream", blob: btoa(bin) }
53588
+ ]
53589
+ };
53590
+ }
53591
+ return {
53592
+ contents: [
53593
+ { uri: uri.href, mimeType: mimeType ?? "application/json", text: JSON.stringify(value) }
53594
+ ]
53595
+ };
53596
+ }
53597
+ function registerResources(server, resources, deps) {
53598
+ const register2 = server.registerResource.bind(server);
53599
+ for (const r of resources) {
53600
+ const meta3 = { tool: r.name, kind: "resource" };
53601
+ const ctxOf = (mcp) => ({
53602
+ resource: r.name,
53603
+ ...baseContext(mcp, deps)
53604
+ });
53605
+ const run = async (uri, variables, mcp) => {
53606
+ checkScopes(r.scopes, deps.context.token);
53607
+ let value;
53608
+ try {
53609
+ const read = r.read;
53610
+ value = await read(r.uri !== undefined ? { uri } : { uri, variables }, ctxOf(mcp));
53611
+ } catch (err) {
53612
+ throw protocolError(err, deps, meta3);
53613
+ }
53614
+ if (value === null || value === undefined)
53615
+ throw new ResourceNotFoundError(uri.href);
53616
+ if (isInputRequiredResult(value))
53617
+ return value;
53618
+ return toContents(uri, value, r.mimeType);
53619
+ };
53620
+ const config2 = {
53621
+ ...r.title ? { title: r.title } : {},
53622
+ ...r.description ? { description: r.description } : {},
53623
+ ...r.mimeType ? { mimeType: r.mimeType } : {}
53624
+ };
53625
+ if (r.uri !== undefined) {
53626
+ register2(r.name, r.uri, config2, (uri, mcp) => run(uri, {}, mcp));
53627
+ continue;
53628
+ }
53629
+ const list = r.list;
53630
+ const template = new ResourceTemplate(r.template, {
53631
+ list: list ? async (mcp) => {
53632
+ try {
53633
+ checkScopes(r.scopes, deps.context.token);
53634
+ } catch {
53635
+ return { resources: [] };
53636
+ }
53637
+ try {
53638
+ return { resources: await list(ctxOf(mcp)) };
53639
+ } catch (err) {
53640
+ throw protocolError(err, deps, meta3);
53641
+ }
53642
+ } : undefined
53643
+ });
53644
+ register2(r.name, template, config2, (uri, vars, mcp) => {
53645
+ const variables = {};
53646
+ for (const [k, v] of Object.entries(vars ?? {}))
53647
+ variables[k] = Array.isArray(v) ? v[0] ?? "" : v;
53648
+ return run(uri, variables, mcp);
53649
+ });
53650
+ }
53651
+ }
53652
+ function toPromptResult(value) {
53653
+ if (isInputRequiredResult(value))
53654
+ return value;
53655
+ if (typeof value === "string")
53656
+ return { messages: [{ role: "user", content: { type: "text", text: value } }] };
53657
+ if (Array.isArray(value))
53658
+ return {
53659
+ messages: value.map((m) => typeof m === "string" ? { role: "user", content: { type: "text", text: m } } : typeof m.content === "string" ? { role: m.role, content: { type: "text", text: m.content } } : m)
53660
+ };
53661
+ return value;
53662
+ }
53663
+ function registerPrompts(server, prompts, deps) {
53664
+ const register2 = server.registerPrompt.bind(server);
53665
+ for (const p of prompts) {
53666
+ const meta3 = { tool: p.name, kind: "prompt" };
53667
+ const run = async (args, mcp) => {
53668
+ checkScopes(p.scopes, deps.context.token);
53669
+ try {
53670
+ const render = p.render;
53671
+ return toPromptResult(await render(args, { prompt: p.name, ...baseContext(mcp, deps) }));
53672
+ } catch (err) {
53673
+ throw protocolError(err, deps, meta3);
53674
+ }
53675
+ };
53676
+ register2(p.name, {
53677
+ ...p.title ? { title: p.title } : {},
53678
+ ...p.description ? { description: p.description } : {},
53679
+ ...p.args ? { argsSchema: p.args } : {}
53680
+ }, p.args ? (args, mcp) => run(args, mcp) : (mcp) => run({}, mcp));
53681
+ }
53682
+ }
53683
+
53684
+ // ../../node_modules/.bun/@nimit9+signet-server@0.2.6+e803b2305afdb064/node_modules/@nimit9/signet-server/dist/mcp/server.js
53685
+ var RAW2 = Symbol.for("signet.mcp.rawResult");
53686
+ function toolResult(result) {
53687
+ return Object.defineProperty({ ...result }, RAW2, { value: true, enumerable: false });
53688
+ }
53689
+ function defineTool(def) {
53690
+ return def;
53691
+ }
53692
+ function toResult(value, structured) {
53693
+ if (value !== null && typeof value === "object" && RAW2 in value)
53694
+ return value;
53494
53695
  if (isInputRequiredResult(value))
53495
53696
  return value;
53496
53697
  const text = typeof value === "string" ? value : JSON.stringify(value ?? null) ?? "null";
@@ -53503,7 +53704,11 @@ function toResult(value, structured) {
53503
53704
  }
53504
53705
  function createMcpServer(options) {
53505
53706
  const unlisted = new Set(options.tools.filter((t) => t.listed === false).map((t) => t.name));
53506
- const toolsCapability = { tools: { listChanged: false } };
53707
+ const toolsCapability = {
53708
+ tools: { listChanged: false },
53709
+ ...options.resources?.length ? { resources: { listChanged: false } } : {},
53710
+ ...options.prompts?.length ? { prompts: { listChanged: false } } : {}
53711
+ };
53507
53712
  const server = new McpServer({ name: options.name, version: options.version }, {
53508
53713
  ...unlisted.size ? {} : { capabilities: toolsCapability },
53509
53714
  ...options.instructions ? { instructions: options.instructions } : {}
@@ -53543,7 +53748,7 @@ function createMcpServer(options) {
53543
53748
  } catch {}
53544
53749
  const info = options.mapError?.(err) ?? describeToolError(err);
53545
53750
  if (!info)
53546
- report(options, err, tool.name, c);
53751
+ report(options, err, { tool: tool.name }, c);
53547
53752
  if (custom2)
53548
53753
  return custom2;
53549
53754
  return toolErrorResult(info ?? INTERNAL_ERROR2);
@@ -53559,6 +53764,11 @@ function createMcpServer(options) {
53559
53764
  restore?.();
53560
53765
  restore = undefined;
53561
53766
  }
53767
+ const deps = { options, context: { ...c ? { c } : {}, ...token ? { token } : {} } };
53768
+ if (options.resources?.length)
53769
+ registerResources(server, options.resources, deps);
53770
+ if (options.prompts?.length)
53771
+ registerPrompts(server, options.prompts, deps);
53562
53772
  return server;
53563
53773
  }
53564
53774
  function hideUnlisted(server, unlisted) {
@@ -53580,7 +53790,6 @@ function hideUnlisted(server, unlisted) {
53580
53790
  throw new Error("signet mcp: could not hide unlisted tools; @modelcontextprotocol/server no longer installs tools/list through setRequestHandler");
53581
53791
  };
53582
53792
  }
53583
-
53584
53793
  // src/annotations.ts
53585
53794
  var RO = { readOnlyHint: true, openWorldHint: false };
53586
53795
  var WI = {
@@ -53656,6 +53865,190 @@ function applyAnnotations(tools) {
53656
53865
  return { ...tool, title: meta3.title, annotations: meta3.annotations };
53657
53866
  });
53658
53867
  }
53868
+
53869
+ // src/lib/confirm.ts
53870
+ import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
53871
+ var ANSWER_KEY = "confirm";
53872
+ function clientCanConfirm(mcp, initializeCapabilities) {
53873
+ const envelope = mcp.mcpReq.envelope;
53874
+ const caps = envelope?.[CLIENT_CAPABILITIES_META_KEY] ?? initializeCapabilities();
53875
+ return Boolean(caps?.elicitation);
53876
+ }
53877
+ function planFingerprint(plan) {
53878
+ const canonical = (v) => Array.isArray(v) ? v.map(canonical) : v && typeof v === "object" ? Object.fromEntries(Object.entries(v).filter(([, x]) => x !== undefined).sort(([a], [b]) => a.localeCompare(b)).map(([k, x]) => [k, canonical(x)])) : v;
53879
+ return createHash("sha256").update(JSON.stringify(canonical(plan))).digest("hex");
53880
+ }
53881
+ var STATE_KEY = randomBytes(32);
53882
+ function sign(nonce, fingerprint) {
53883
+ return createHmac("sha256", STATE_KEY).update(`${nonce}.${fingerprint}`).digest("hex");
53884
+ }
53885
+ function mintState(fingerprint) {
53886
+ const nonce = randomBytes(16).toString("hex");
53887
+ return `${nonce}.${sign(nonce, fingerprint)}`;
53888
+ }
53889
+ function verifyState(state, fingerprint) {
53890
+ if (typeof state !== "string")
53891
+ return false;
53892
+ const [nonce, mac3, ...rest] = state.split(".");
53893
+ if (!nonce || !mac3 || rest.length)
53894
+ return false;
53895
+ const expected = Buffer.from(sign(nonce, fingerprint), "hex");
53896
+ const given = Buffer.from(mac3, "hex");
53897
+ return given.length === expected.length && timingSafeEqual(given, expected);
53898
+ }
53899
+ function confirmPlan(mcp, message, plan, initializeCapabilities = () => {
53900
+ return;
53901
+ }) {
53902
+ if (!clientCanConfirm(mcp, initializeCapabilities))
53903
+ return { kind: "unsupported" };
53904
+ const fingerprint = planFingerprint(plan);
53905
+ const responses = mcp.mcpReq.inputResponses;
53906
+ const answered = responses !== undefined && ANSWER_KEY in responses;
53907
+ const echoed = mcp.mcpReq.requestState?.();
53908
+ if (answered && verifyState(echoed, fingerprint)) {
53909
+ const content = acceptedContent(responses, ANSWER_KEY);
53910
+ if (content?.confirm === true)
53911
+ return { kind: "confirmed" };
53912
+ return {
53913
+ kind: "declined",
53914
+ result: { cancelled: true, message: "Cancelled by the user. Nothing was changed." }
53915
+ };
53916
+ }
53917
+ return {
53918
+ kind: "ask",
53919
+ result: inputRequired({
53920
+ inputRequests: {
53921
+ [ANSWER_KEY]: inputRequired.elicit({
53922
+ message,
53923
+ requestedSchema: {
53924
+ type: "object",
53925
+ properties: {
53926
+ confirm: { type: "boolean", title: "Confirm", description: "This can't be undone." }
53927
+ },
53928
+ required: ["confirm"]
53929
+ }
53930
+ })
53931
+ },
53932
+ requestState: mintState(fingerprint)
53933
+ })
53934
+ };
53935
+ }
53936
+ function withConfirmation(tool, describe3, initializeCapabilities) {
53937
+ const run = tool.handler;
53938
+ return {
53939
+ ...tool,
53940
+ handler: (args, ctx) => {
53941
+ const message = describe3(args);
53942
+ if (message === null || !ctx?.mcp)
53943
+ return run(args, ctx);
53944
+ const outcome = confirmPlan(ctx.mcp, message, { tool: tool.name, args }, initializeCapabilities);
53945
+ if (outcome.kind === "confirmed" || outcome.kind === "unsupported")
53946
+ return run(args, ctx);
53947
+ return outcome.result;
53948
+ }
53949
+ };
53950
+ }
53951
+ var safeDate = (v) => typeof v === "string" && /^\d{4}-\d{2}-\d{2}$/.test(v) ? v : v === undefined ? "any" : "a date";
53952
+ var safeAmount = (v) => typeof v === "string" && /^\d+(\.\d{1,2})?$/.test(v) || typeof v === "number" ? String(v) : null;
53953
+ var plural2 = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
53954
+ function describeTransactionFilters(f) {
53955
+ const parts = [];
53956
+ if (f.startDate || f.endDate)
53957
+ parts.push(`dated ${safeDate(f.startDate)} to ${safeDate(f.endDate)}`);
53958
+ if (f.type)
53959
+ parts.push(`${f.type}s only`);
53960
+ if (f.categoryType)
53961
+ parts.push(`category type ${f.categoryType}`);
53962
+ if (f.categoryId)
53963
+ parts.push("in one category");
53964
+ if (f.uncategorized)
53965
+ parts.push("uncategorized");
53966
+ if (f.amountMin !== undefined || f.amountMax !== undefined)
53967
+ parts.push(`amount ₹${f.amountMin ?? 0} to ${f.amountMax === undefined ? "any" : `₹${f.amountMax}`}`);
53968
+ if (f.owner)
53969
+ parts.push("one owner");
53970
+ if (f.search)
53971
+ parts.push("matching a text search");
53972
+ return parts.length ? parts.join(", ") : "with no filters (ALL transactions)";
53973
+ }
53974
+ var describeDeletes = {
53975
+ transactions: (a) => {
53976
+ if (a.id)
53977
+ return "Permanently delete 1 transaction?";
53978
+ if (Array.isArray(a.ids))
53979
+ return `Permanently delete ${plural2(a.ids.length, "transaction")}?`;
53980
+ return `Permanently delete every transaction ${describeTransactionFilters(a.filters ?? {})}?`;
53981
+ },
53982
+ entity: (a) => {
53983
+ const entity = String(a.entity ?? "item").replace(/_/g, " ");
53984
+ const n = Array.isArray(a.ids) ? a.ids.length : 1;
53985
+ const extra = a.entity === "merchant" ? " Their transactions are kept but unlinked." : a.entity === "debt" ? " Its history is lost; settling keeps it." : "";
53986
+ return `Permanently delete ${plural2(n, entity)}?${extra}`;
53987
+ },
53988
+ mergeMerchants: (a) => {
53989
+ const n = Array.isArray(a.duplicateIds) ? a.duplicateIds.length : 0;
53990
+ return `Merge ${plural2(n, "duplicate merchant")} into the one you're keeping? Their transactions move over and the duplicates are deleted.`;
53991
+ },
53992
+ encryptionMode: (a) => `Switch encryption mode to ${a.encryptionMode === "private" ? "private" : "standard"}?`,
53993
+ settleDebt: (a) => {
53994
+ if (a.action !== "settle")
53995
+ return null;
53996
+ const amount = a.settledAmount === undefined ? "0" : safeAmount(a.settledAmount);
53997
+ return amount === null ? "Settle this debt? It will be closed with the remaining balance you gave." : `Settle this debt? It will be closed with a remaining balance of ₹${amount}.`;
53998
+ }
53999
+ };
54000
+ var CONFIRMED_TOOLS = {
54001
+ delete_transactions: describeDeletes.transactions,
54002
+ delete_entity: describeDeletes.entity,
54003
+ merge_merchants: describeDeletes.mergeMerchants,
54004
+ set_encryption_mode: describeDeletes.encryptionMode,
54005
+ manage_debt: describeDeletes.settleDebt
54006
+ };
54007
+ var BULK_CONFIRM_THRESHOLD = 20;
54008
+ var sum = (rows, key) => Array.isArray(rows) ? rows.reduce((n, r) => n + (Number(r?.[key]) || 0), 0) : 0;
54009
+ var fromDryRun = (listKey, countKey) => async (args, dryRun) => {
54010
+ const r = await dryRun({ ...args, dryRun: true });
54011
+ return {
54012
+ n: sum(r?.[listKey], countKey),
54013
+ planHash: typeof r?.planHash === "string" ? r.planHash : undefined
54014
+ };
54015
+ };
54016
+ var BULK_CONFIRMED_TOOLS = {
54017
+ categorize_groups: fromDryRun("applied", "rows"),
54018
+ backfill_persons: fromDryRun("plan", "txnCount"),
54019
+ bulk_categorize_transactions: async (args) => ({
54020
+ n: Array.isArray(args.links) ? args.links.length : 0
54021
+ }),
54022
+ bulk_link_entity: async (args) => ({ n: Array.isArray(args.links) ? args.links.length : 0 })
54023
+ };
54024
+ var PLAN_CHANGED = {
54025
+ planChanged: true,
54026
+ message: "The transactions changed after they were counted, so nothing was written. Run it again to see the new count."
54027
+ };
54028
+ function withBulkConfirmation(tool, count, initializeCapabilities) {
54029
+ const run = tool.handler;
54030
+ const runPlanned = async (args, ctx, hash2) => {
54031
+ const result = await run(hash2 === undefined ? args : { ...args, expectedPlanHash: hash2 }, ctx);
54032
+ return result?.planChanged ? PLAN_CHANGED : result;
54033
+ };
54034
+ return {
54035
+ ...tool,
54036
+ handler: async (args, ctx) => {
54037
+ if (args.dryRun === true || !ctx?.mcp || !clientCanConfirm(ctx.mcp, initializeCapabilities))
54038
+ return run(args, ctx);
54039
+ const { n, planHash: hash2 } = await count(args, async (a) => run(a, ctx));
54040
+ if (n <= BULK_CONFIRM_THRESHOLD)
54041
+ return runPlanned(args, ctx, hash2);
54042
+ const message = `Update ${plural2(n, "transaction")} in one go?`;
54043
+ const outcome = confirmPlan(ctx.mcp, message, { tool: tool.name, args, n, planHash: hash2 }, initializeCapabilities);
54044
+ if (outcome.kind === "confirmed")
54045
+ return runPlanned(args, ctx, hash2);
54046
+ if (outcome.kind === "unsupported")
54047
+ return run(args, ctx);
54048
+ return outcome.result;
54049
+ }
54050
+ };
54051
+ }
53659
54052
  // src/lib/union-tool.ts
53660
54053
  function variant(input2, run) {
53661
54054
  return { input: input2, run };
@@ -53848,15 +54241,15 @@ var PROMPTS = [
53848
54241
  `)
53849
54242
  }
53850
54243
  ];
53851
- function registerPrompts(server) {
54244
+ function registerPrompts2(server2) {
53852
54245
  for (const p of PROMPTS) {
53853
54246
  const cb = (args) => ({
53854
54247
  messages: [{ role: "user", content: { type: "text", text: p.text(args) } }]
53855
54248
  });
53856
54249
  if (p.args) {
53857
- server.registerPrompt(p.name, { title: p.title, description: p.description, argsSchema: p.args }, (args) => cb(args));
54250
+ server2.registerPrompt(p.name, { title: p.title, description: p.description, argsSchema: p.args }, (args) => cb(args));
53858
54251
  } else {
53859
- server.registerPrompt(p.name, { title: p.title, description: p.description }, () => cb({}));
54252
+ server2.registerPrompt(p.name, { title: p.title, description: p.description }, () => cb({}));
53860
54253
  }
53861
54254
  }
53862
54255
  }
@@ -58631,6 +59024,12 @@ function parseAmount4(raw) {
58631
59024
  const num = parseFloat(cleaned);
58632
59025
  return Number.isNaN(num) ? 0 : Math.abs(num);
58633
59026
  }
59027
+ // src/lib/plan-hash.ts
59028
+ import { createHash as createHash2 } from "node:crypto";
59029
+ function planHash(ids) {
59030
+ return createHash2("sha256").update([...ids].sort().join(",")).digest("hex");
59031
+ }
59032
+
58634
59033
  // src/lib/txn-rows.ts
58635
59034
  function toTxnRow(r2) {
58636
59035
  return {
@@ -58799,6 +59198,20 @@ async function backfillPersons(client, crypto3, opts = {}) {
58799
59198
  else
58800
59199
  newGroups.push({ norm, displayName: g2.displayName, upiHandles });
58801
59200
  }
59201
+ const hash2 = planHash([...groups.values()].flatMap((g2) => g2.txnIds));
59202
+ if (!opts.dryRun && opts.expectedPlanHash !== undefined && opts.expectedPlanHash !== hash2) {
59203
+ return {
59204
+ dryRun: false,
59205
+ truncated,
59206
+ candidatesScanned: candidates.length,
59207
+ created: 0,
59208
+ reused: 0,
59209
+ linked: 0,
59210
+ plan,
59211
+ planHash: hash2,
59212
+ planChanged: true
59213
+ };
59214
+ }
58802
59215
  if (!opts.dryRun && newGroups.length > 0) {
58803
59216
  const bodies = await Promise.all(newGroups.map(async ({ displayName, upiHandles }) => {
58804
59217
  const fields = {
@@ -58832,6 +59245,7 @@ async function backfillPersons(client, crypto3, opts = {}) {
58832
59245
  const CHUNK = 25;
58833
59246
  for (let i2 = 0;i2 < links.length; i2 += CHUNK) {
58834
59247
  await client.post("/api/transactions/bulk-link-entity", { links: links.slice(i2, i2 + CHUNK) });
59248
+ await opts.onProgress?.(Math.min(i2 + CHUNK, links.length), links.length, "Linking transactions to people");
58835
59249
  }
58836
59250
  }
58837
59251
  return {
@@ -58841,7 +59255,8 @@ async function backfillPersons(client, crypto3, opts = {}) {
58841
59255
  created,
58842
59256
  reused,
58843
59257
  linked: links.length,
58844
- plan
59258
+ plan,
59259
+ planHash: hash2
58845
59260
  };
58846
59261
  }
58847
59262
 
@@ -58968,6 +59383,10 @@ async function applyGroupAssignments(client, crypto3, assignments, opts = {}) {
58968
59383
  plan.push({ group: g2, categoryId: cat.id, slug: cat.slug, learn: a2.learn !== false });
58969
59384
  }
58970
59385
  }
59386
+ result.planHash = planHash(plan.flatMap((p2) => p2.group.rows.map((r2) => r2.id)));
59387
+ if (!opts.dryRun && opts.expectedPlanHash !== undefined && opts.expectedPlanHash !== result.planHash) {
59388
+ return { ...result, planChanged: true };
59389
+ }
58971
59390
  if (opts.dryRun) {
58972
59391
  result.applied = plan.map((p2) => ({
58973
59392
  pattern: p2.group.pattern,
@@ -59055,7 +59474,11 @@ async function applyGroupAssignments(client, crypto3, assignments, opts = {}) {
59055
59474
  }
59056
59475
  if (result.rulesSaved > 0)
59057
59476
  await saveBag(client, bag, version2);
59477
+ const total = links.length + direct.reduce((n2, d2) => n2 + d2.ids.length, 0);
59478
+ let done = 0;
59058
59479
  await postLinks(client, links);
59480
+ done += links.length;
59481
+ await opts.onProgress?.(done, total, "Categorizing transactions");
59059
59482
  for (const { ids, categoryId } of direct) {
59060
59483
  const categoryType = catIndex.byId.get(categoryId)?.type;
59061
59484
  for (let i2 = 0;i2 < ids.length; i2 += 200) {
@@ -59064,11 +59487,20 @@ async function applyGroupAssignments(client, crypto3, assignments, opts = {}) {
59064
59487
  categoryId,
59065
59488
  ...categoryType ? { categoryType } : {}
59066
59489
  });
59490
+ done += Math.min(200, ids.length - i2);
59491
+ await opts.onProgress?.(done, total, "Categorizing transactions");
59067
59492
  }
59068
59493
  }
59069
59494
  return result;
59070
59495
  }
59071
59496
 
59497
+ // src/lib/progress.ts
59498
+ function progressFor(ctx) {
59499
+ if (!ctx?.mcp)
59500
+ return;
59501
+ return (done, total, message) => progress(ctx, done, total, message);
59502
+ }
59503
+
59072
59504
  // src/tools/transactions.ts
59073
59505
  function dedupeKey(date5, description, amount, type) {
59074
59506
  return `${date5}|${description}|${amount.toFixed(2)}|${type}`;
@@ -59116,6 +59548,7 @@ async function importTransactions(client, crypto3, opts) {
59116
59548
  transactions: rows.slice(i2, i2 + CHUNK)
59117
59549
  });
59118
59550
  imported += res.imported ?? 0;
59551
+ await opts.onProgress?.(Math.min(i2 + CHUNK, rows.length), rows.length, "Importing transactions");
59119
59552
  }
59120
59553
  return { imported, duplicatesSkipped, duplicatesFlagged };
59121
59554
  }
@@ -59296,17 +59729,27 @@ function transactionTools(client, crypto3) {
59296
59729
  endDate: exports_external.string().optional(),
59297
59730
  dryRun: exports_external.boolean().optional()
59298
59731
  }),
59299
- handler: ({ assignments, startDate, endDate, dryRun }) => applyGroupAssignments(client, crypto3, assignments, {
59300
- dryRun,
59301
- startDate,
59302
- endDate
59303
- })
59732
+ handler: (args, ctx) => {
59733
+ const { assignments, startDate, endDate, dryRun } = args;
59734
+ const { expectedPlanHash } = args;
59735
+ return applyGroupAssignments(client, crypto3, assignments, {
59736
+ dryRun,
59737
+ startDate,
59738
+ endDate,
59739
+ expectedPlanHash,
59740
+ onProgress: progressFor(ctx)
59741
+ });
59742
+ }
59304
59743
  }),
59305
59744
  defineTool({
59306
59745
  name: "backfill_persons",
59307
59746
  description: "Create persons from UPI/IMPS counterparties in unlinked transactions (deduped by name) and link those transactions to them. Idempotent. dryRun previews.",
59308
59747
  input: exports_external.object({ dryRun: exports_external.boolean().optional() }),
59309
- handler: ({ dryRun }) => backfillPersons(client, crypto3, { dryRun })
59748
+ handler: (args, ctx) => backfillPersons(client, crypto3, {
59749
+ onProgress: progressFor(ctx),
59750
+ dryRun: args.dryRun,
59751
+ expectedPlanHash: args.expectedPlanHash
59752
+ })
59310
59753
  }),
59311
59754
  defineTool({
59312
59755
  name: "update_transaction",
@@ -60033,7 +60476,7 @@ async function fetchReconciliationContext(client, accountId, start, end, budget
60033
60476
  const seen = new Set;
60034
60477
  return [...left, ...right].filter((r2) => r2.id == null ? true : !seen.has(r2.id) && seen.add(r2.id));
60035
60478
  }
60036
- var sum = (rows) => Math.round(rows.reduce((n2, r2) => n2 + r2.amount, 0));
60479
+ var sum2 = (rows) => Math.round(rows.reduce((n2, r2) => n2 + r2.amount, 0));
60037
60480
  async function runStatementImport(deps, args) {
60038
60481
  const { client, crypto: crypto3 } = deps;
60039
60482
  const accounts = (await client.get("/api/accounts")).data ?? [];
@@ -60075,8 +60518,8 @@ async function runStatementImport(deps, args) {
60075
60518
  conflictCount: result.conflicts.length,
60076
60519
  new: {
60077
60520
  n: fresh.length,
60078
- debits: { n: debits.length, total: sum(debits) },
60079
- credits: { n: credits.length, total: sum(credits) }
60521
+ debits: { n: debits.length, total: sum2(debits) },
60522
+ credits: { n: credits.length, total: sum2(credits) }
60080
60523
  }
60081
60524
  };
60082
60525
  if (!args.confirm) {
@@ -60096,6 +60539,7 @@ async function runStatementImport(deps, args) {
60096
60539
  return { ...summary, dryRun: false, imported: 0 };
60097
60540
  const savings = account.type !== "credit_card";
60098
60541
  const imported = await importTransactions(client, crypto3, {
60542
+ onProgress: deps.onProgress,
60099
60543
  accountId: account.id,
60100
60544
  source: savings ? "bank_statement" : "cc_statement",
60101
60545
  granularity: savings ? 1 : 2,
@@ -60144,7 +60588,13 @@ var statementSource = exports_external.enum([
60144
60588
  ]);
60145
60589
  var granularity = exports_external.number().int().min(1).max(5).describe("1=bank, 2=CC, 3=manual, 4=app export, 5=telegram/AI");
60146
60590
  function statementImportTools(client, crypto3) {
60147
- const pdf = (confirm) => variant(pdfInput, (args) => runStatementImport({ client, crypto: crypto3, readFile: readFileAsArrayBuffer, parsers: realParsers }, confirm ? { ...args, confirm: true } : args));
60591
+ const pdf = (confirm2) => variant(pdfInput, (args, ctx) => runStatementImport({
60592
+ client,
60593
+ crypto: crypto3,
60594
+ readFile: readFileAsArrayBuffer,
60595
+ parsers: realParsers,
60596
+ onProgress: progressFor(ctx)
60597
+ }, confirm2 ? { ...args, confirm: true } : args));
60148
60598
  const csvParse = variant(exports_external.object({ filePath: exports_external.string() }), async ({ filePath }) => {
60149
60599
  if (crypto3)
60150
60600
  return CSV_PRIVATE_MODE_ERROR;
@@ -60205,8 +60655,9 @@ function statementImportTools(client, crypto3) {
60205
60655
  description: exports_external.string(),
60206
60656
  referenceNumber: exports_external.string().optional()
60207
60657
  }))
60208
- }), async ({ accountId, source, granularity: granularity2, transactions: txns }) => {
60658
+ }), async ({ accountId, source, granularity: granularity2, transactions: txns }, ctx) => {
60209
60659
  const data = await importTransactions(client, crypto3, {
60660
+ onProgress: progressFor(ctx),
60210
60661
  accountId,
60211
60662
  source,
60212
60663
  granularity: granularity2,
@@ -60220,7 +60671,7 @@ function statementImportTools(client, crypto3) {
60220
60671
  source: statementSource,
60221
60672
  granularity,
60222
60673
  filePath: exports_external.string()
60223
- }), async ({ accountId, source, granularity: granularity2, filePath }) => {
60674
+ }), async ({ accountId, source, granularity: granularity2, filePath }, ctx) => {
60224
60675
  let txns;
60225
60676
  try {
60226
60677
  const raw = await readFile2(filePath, "utf8");
@@ -60237,6 +60688,7 @@ function statementImportTools(client, crypto3) {
60237
60688
  throw new Error(`Failed to read/parse ${filePath}: ${e2 instanceof Error ? e2.message : String(e2)}`);
60238
60689
  }
60239
60690
  const data = await importTransactions(client, crypto3, {
60691
+ onProgress: progressFor(ctx),
60240
60692
  accountId,
60241
60693
  source,
60242
60694
  granularity: granularity2,
@@ -60363,7 +60815,7 @@ async function runTool(tool, args, mcp) {
60363
60815
  function jsonContents(uri, value) {
60364
60816
  return { contents: [{ uri: uri.href, mimeType: JSON_MIME, text: JSON.stringify(value ?? null) }] };
60365
60817
  }
60366
- function registerResources(server, tools) {
60818
+ function registerResources2(server2, tools) {
60367
60819
  const byName = new Map(tools.map((t2) => [t2.name, t2]));
60368
60820
  const need = (name) => {
60369
60821
  const t2 = byName.get(name);
@@ -60373,13 +60825,13 @@ function registerResources(server, tools) {
60373
60825
  };
60374
60826
  for (const r2 of RESOURCES) {
60375
60827
  const tool = need(r2.tool);
60376
- server.registerResource(r2.name, r2.uri, { title: r2.title, description: r2.description, mimeType: JSON_MIME }, async (uri, mcp) => {
60828
+ server2.registerResource(r2.name, r2.uri, { title: r2.title, description: r2.description, mimeType: JSON_MIME }, async (uri, mcp) => {
60377
60829
  const value = scrubCiphertext(await runTool(tool, r2.args, mcp));
60378
60830
  return jsonContents(uri, r2.shape ? r2.shape(value) : value);
60379
60831
  });
60380
60832
  }
60381
60833
  const getTransaction = need("get_transaction");
60382
- server.registerResource("transaction", new ResourceTemplate(TRANSACTION_TEMPLATE, { list: undefined }), {
60834
+ server2.registerResource("transaction", new ResourceTemplate(TRANSACTION_TEMPLATE, { list: undefined }), {
60383
60835
  title: "Transaction",
60384
60836
  description: "One transaction by UUID (same as get_transaction; decrypted in private mode).",
60385
60837
  mimeType: JSON_MIME
@@ -60392,7 +60844,7 @@ function registerResources(server, tools) {
60392
60844
  // package.json
60393
60845
  var package_default = {
60394
60846
  name: "paisa-mcp",
60395
- version: "0.3.1",
60847
+ version: "0.4.1",
60396
60848
  repository: {
60397
60849
  type: "git",
60398
60850
  url: "git+https://github.com/nimit9/paisa.git",
@@ -60421,7 +60873,7 @@ var package_default = {
60421
60873
  "@modelcontextprotocol/server": "^2.1.0",
60422
60874
  "@nimit9/signet-ai": "^0.1.11",
60423
60875
  "@nimit9/signet-lib": "^0.1.15",
60424
- "@nimit9/signet-server": "^0.2.2",
60876
+ "@nimit9/signet-server": "^0.2.6",
60425
60877
  "@paisa/parsers": "workspace:*",
60426
60878
  "@paisa/reconciliation": "workspace:*",
60427
60879
  "@paisa/types": "workspace:*",
@@ -60448,19 +60900,30 @@ function paisaErrorResult(err) {
60448
60900
  });
60449
60901
  }
60450
60902
  function buildServer(client, crypto3) {
60451
- const tools = applyAnnotations(REGISTRATIONS.flatMap((group) => group.tools(client, crypto3)));
60903
+ let initializeCapabilities = () => {
60904
+ return;
60905
+ };
60906
+ const tools = applyAnnotations(REGISTRATIONS.flatMap((group) => group.tools(client, crypto3)).map((t2) => {
60907
+ const caps = () => initializeCapabilities();
60908
+ const describe3 = CONFIRMED_TOOLS[t2.name];
60909
+ if (describe3)
60910
+ return withConfirmation(t2, describe3, caps);
60911
+ const count = BULK_CONFIRMED_TOOLS[t2.name];
60912
+ return count ? withBulkConfirmation(t2, count, caps) : t2;
60913
+ }));
60452
60914
  for (const t2 of tools)
60453
60915
  if (t2.input)
60454
60916
  withCompactJsonSchema(t2.input);
60455
- const server = createMcpServer({
60917
+ const server2 = createMcpServer({
60456
60918
  name: "paisa",
60457
60919
  version: VERSION2,
60458
60920
  tools,
60459
60921
  errorResult: paisaErrorResult
60460
60922
  });
60461
- registerResources(server, tools);
60462
- registerPrompts(server);
60463
- return server;
60923
+ initializeCapabilities = () => server2.server.getClientCapabilities();
60924
+ registerResources2(server2, tools);
60925
+ registerPrompts2(server2);
60926
+ return server2;
60464
60927
  }
60465
60928
 
60466
60929
  // src/index.ts
@@ -60484,5 +60947,5 @@ main().catch((err) => {
60484
60947
  process.exit(1);
60485
60948
  });
60486
60949
 
60487
- //# debugId=E169545C1C52BE7864756E2164756E21
60950
+ //# debugId=882108748FF621AF64756E2164756E21
60488
60951
  //# sourceMappingURL=index.js.map