paisa-mcp 0.4.0 → 0.4.2

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,17 +38033,6 @@ 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
38036
  function inputResponse(responses, key) {
38048
38037
  if (responses === undefined || typeof responses !== "object" || responses === null)
38049
38038
  return { kind: "missing" };
@@ -53439,10 +53428,170 @@ async function initCrypto(client, pin, userId) {
53439
53428
  return { key };
53440
53429
  }
53441
53430
 
53431
+ // src/server.ts
53432
+ import { randomBytes } from "node:crypto";
53433
+
53442
53434
  // ../../node_modules/.bun/@modelcontextprotocol+server@2.1.0/node_modules/@modelcontextprotocol/server/dist/index.mjs
53443
53435
  var DEFAULT_MAX_REQUEST_BODY_SIZE = 4 * 1024 * 1024;
53436
+ var PREFIX = "v1.";
53437
+ function bytesToBase64Url(bytes) {
53438
+ let bin = "";
53439
+ for (const b of bytes)
53440
+ bin += String.fromCodePoint(b);
53441
+ return btoa(bin).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
53442
+ }
53443
+ function constantTimeTagEqual(a, b) {
53444
+ if (a.length !== b.length)
53445
+ return false;
53446
+ let r = 0;
53447
+ for (let i = 0;i < a.length; i++)
53448
+ r |= a.codePointAt(i) ^ b.codePointAt(i);
53449
+ return r === 0;
53450
+ }
53451
+ function base64UrlToBytes(s) {
53452
+ const b64 = s.replaceAll("-", "+").replaceAll("_", "/");
53453
+ const bin = atob(b64);
53454
+ const bytes = new Uint8Array(bin.length);
53455
+ for (let i = 0;i < bin.length; i++)
53456
+ bytes[i] = bin.codePointAt(i);
53457
+ return bytes;
53458
+ }
53459
+ function createRequestStateCodec(options) {
53460
+ const subtle = globalThis.crypto?.subtle;
53461
+ if (subtle === undefined)
53462
+ throw new TypeError("createRequestStateCodec requires the Web Crypto API (globalThis.crypto.subtle); see https://ts.sdk.modelcontextprotocol.io/v2/troubleshooting for the Node.js polyfill instructions");
53463
+ const keyBytes = typeof options.key === "string" ? new TextEncoder().encode(options.key) : Uint8Array.from(options.key);
53464
+ if (keyBytes.byteLength < 32)
53465
+ throw new RangeError(`createRequestStateCodec: key must be at least 32 bytes (got ${keyBytes.byteLength})`);
53466
+ const ttlSeconds = options.ttlSeconds ?? 600;
53467
+ if (!Number.isFinite(ttlSeconds))
53468
+ throw new RangeError("createRequestStateCodec: ttlSeconds must be a finite number");
53469
+ const bind2 = options.bind;
53470
+ let cryptoKey;
53471
+ const importedKey = () => cryptoKey ??= subtle.importKey("raw", keyBytes, {
53472
+ name: "HMAC",
53473
+ hash: "SHA-256"
53474
+ }, false, ["sign", "verify"]);
53475
+ const utf8 = new TextEncoder;
53476
+ const BIND_LABEL = "mcp.requestState.bind:";
53477
+ const bindTag = async (value) => {
53478
+ return bytesToBase64Url(new Uint8Array(await subtle.sign("HMAC", await importedKey(), utf8.encode(BIND_LABEL + value))).slice(0, 16));
53479
+ };
53480
+ return {
53481
+ async mint(payload, ctx) {
53482
+ const envelope = {
53483
+ p: payload,
53484
+ exp: Math.floor(Date.now() / 1000) + ttlSeconds
53485
+ };
53486
+ if (bind2 !== undefined) {
53487
+ if (ctx === undefined)
53488
+ throw new TypeError("createRequestStateCodec: mint() requires ctx when a bind callback is configured");
53489
+ envelope.b = await bindTag(bind2(ctx));
53490
+ }
53491
+ const body = bytesToBase64Url(utf8.encode(JSON.stringify(envelope)));
53492
+ return `${PREFIX}${body}.${bytesToBase64Url(new Uint8Array(await subtle.sign("HMAC", await importedKey(), utf8.encode(PREFIX + body))))}`;
53493
+ },
53494
+ async verify(state, ctx) {
53495
+ const dot = state.lastIndexOf(".");
53496
+ if (!state.startsWith(PREFIX) || dot <= 3)
53497
+ throw new Error("malformed");
53498
+ const body = state.slice(3, dot);
53499
+ let macBytes;
53500
+ try {
53501
+ macBytes = base64UrlToBytes(state.slice(dot + 1));
53502
+ } catch {
53503
+ throw new Error("malformed");
53504
+ }
53505
+ if (!await subtle.verify("HMAC", await importedKey(), macBytes, utf8.encode(PREFIX + body)))
53506
+ throw new Error("mac");
53507
+ let envelope;
53508
+ try {
53509
+ envelope = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(base64UrlToBytes(body)));
53510
+ } catch {
53511
+ throw new Error("malformed");
53512
+ }
53513
+ if (typeof envelope.exp !== "number" || envelope.exp < Math.floor(Date.now() / 1000))
53514
+ throw new Error("expired");
53515
+ if (bind2 !== undefined) {
53516
+ const expected = await bindTag(bind2(ctx));
53517
+ if (envelope.b === undefined || !constantTimeTagEqual(envelope.b, expected))
53518
+ throw new Error("bind");
53519
+ } else if (envelope.b !== undefined)
53520
+ throw new Error("bind");
53521
+ return envelope.p;
53522
+ }
53523
+ };
53524
+ }
53525
+
53526
+ // ../../node_modules/.bun/@nimit9+signet-server@0.2.7+e803b2305afdb064/node_modules/@nimit9/signet-server/dist/mcp/interaction.js
53527
+ var CONFIRM_KEY = "signet_confirm";
53528
+ function canonicalize(value) {
53529
+ if (Array.isArray(value))
53530
+ return value.map(canonicalize);
53531
+ if (value !== null && typeof value === "object") {
53532
+ const obj = value;
53533
+ return Object.fromEntries(Object.keys(obj).sort().map((k) => [k, canonicalize(obj[k])]));
53534
+ }
53535
+ return value;
53536
+ }
53537
+ async function fingerprint(args) {
53538
+ const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify(canonicalize(args ?? null))));
53539
+ return [...new Uint8Array(bytes)].map((b) => b.toString(16).padStart(2, "0")).join("");
53540
+ }
53541
+ async function confirm(ctx, { message, detail, args, key }) {
53542
+ const id = key ?? `${CONFIRM_KEY}:${ctx.tool}`;
53543
+ const view = inputResponse(ctx.mcp.mcpReq.inputResponses, id);
53544
+ if (view.kind === "missing") {
53545
+ if (!ctx.confirmSupport.getClientCapabilities()?.elicitation)
53546
+ return "unsupported";
53547
+ const payload = { tool: ctx.tool, id, fp: await fingerprint(args) };
53548
+ const requestState = await ctx.confirmSupport.mint?.(payload);
53549
+ return inputRequired({
53550
+ inputRequests: {
53551
+ [id]: inputRequired.elicit({
53552
+ message: detail ? `${message}
53444
53553
 
53445
- // ../../node_modules/.bun/@nimit9+signet-server@0.2.3+e803b2305afdb064/node_modules/@nimit9/signet-server/dist/http/status-codes.js
53554
+ ${detail}` : message,
53555
+ requestedSchema: {
53556
+ type: "object",
53557
+ properties: { confirm: { type: "boolean", title: "Confirm" } },
53558
+ required: ["confirm"]
53559
+ }
53560
+ })
53561
+ },
53562
+ ...requestState ? { requestState } : {}
53563
+ });
53564
+ }
53565
+ if (view.kind !== "elicit" || view.action !== "accept" || view.content?.confirm !== true) {
53566
+ return "declined";
53567
+ }
53568
+ if (ctx.confirmSupport.mint) {
53569
+ const state = ctx.mcp.mcpReq.requestState();
53570
+ if (!state || state.tool !== ctx.tool || state.id !== id || state.fp !== await fingerprint(args))
53571
+ return "declined";
53572
+ }
53573
+ return "confirmed";
53574
+ }
53575
+ function isConfirmPending(value) {
53576
+ return isInputRequiredResult(value);
53577
+ }
53578
+ async function progress(ctx, done, total, message) {
53579
+ const token = ctx.mcp.mcpReq._meta?.progressToken;
53580
+ if (token === undefined)
53581
+ return;
53582
+ try {
53583
+ await ctx.mcp.mcpReq.notify({
53584
+ method: "notifications/progress",
53585
+ params: {
53586
+ progressToken: token,
53587
+ progress: done,
53588
+ ...total !== undefined ? { total } : {},
53589
+ ...message ? { message } : {}
53590
+ }
53591
+ });
53592
+ } catch {}
53593
+ }
53594
+ // ../../node_modules/.bun/@nimit9+signet-server@0.2.7+e803b2305afdb064/node_modules/@nimit9/signet-server/dist/http/status-codes.js
53446
53595
  var STATUS_CODES = {
53447
53596
  400: "BAD_REQUEST",
53448
53597
  401: "UNAUTHORIZED",
@@ -53456,14 +53605,7 @@ var STATUS_CODES = {
53456
53605
  429: "RATE_LIMITED"
53457
53606
  };
53458
53607
 
53459
- // ../../node_modules/.bun/@nimit9+signet-server@0.2.3+e803b2305afdb064/node_modules/@nimit9/signet-server/dist/mcp/server.js
53460
- var RAW = Symbol.for("signet.mcp.rawResult");
53461
- function toolResult(result) {
53462
- return Object.defineProperty({ ...result }, RAW, { value: true, enumerable: false });
53463
- }
53464
- function defineTool(def) {
53465
- return def;
53466
- }
53608
+ // ../../node_modules/.bun/@nimit9+signet-server@0.2.7+e803b2305afdb064/node_modules/@nimit9/signet-server/dist/mcp/errors.js
53467
53609
  function isZodError(err) {
53468
53610
  return err instanceof Error && err.name === "ZodError" && Array.isArray(err.issues);
53469
53611
  }
@@ -53512,10 +53654,10 @@ function toolErrorResult(info) {
53512
53654
  };
53513
53655
  }
53514
53656
  var INTERNAL_ERROR2 = { code: "INTERNAL_ERROR", message: "Internal error" };
53515
- function report(opts, err, tool, c) {
53657
+ function report(opts, err, meta3, c) {
53516
53658
  let pending;
53517
53659
  try {
53518
- pending = opts.onError?.(err, { tool });
53660
+ pending = opts.onError?.(err, meta3);
53519
53661
  } catch {
53520
53662
  return;
53521
53663
  }
@@ -53526,9 +53668,168 @@ function report(opts, err, tool, c) {
53526
53668
  c?.executionCtx.waitUntil(settled);
53527
53669
  } catch {}
53528
53670
  }
53529
- function toResult(value, structured) {
53671
+
53672
+ // ../../node_modules/.bun/@nimit9+signet-server@0.2.7+e803b2305afdb064/node_modules/@nimit9/signet-server/dist/mcp/primitives.js
53673
+ var RAW = Symbol.for("signet.mcp.rawResult");
53674
+ function protocolError(err, deps, meta3) {
53675
+ if (ProtocolError.isInstance(err))
53676
+ return err;
53677
+ const info = deps.options.mapError?.(err) ?? describeToolError(err);
53678
+ if (!info) {
53679
+ report(deps.options, err, meta3, deps.context.c);
53680
+ return new ProtocolError(ProtocolErrorCode.InternalError, "Internal error");
53681
+ }
53682
+ const status = isHttpException(err) ? err.status : isZodError(err) ? 400 : undefined;
53683
+ const code = status !== undefined && status >= 500 ? ProtocolErrorCode.InternalError : ProtocolErrorCode.InvalidParams;
53684
+ return new ProtocolError(code, `${info.code}: ${info.message}`, {
53685
+ success: false,
53686
+ error: info.message,
53687
+ code: info.code,
53688
+ ...info.details ? { details: info.details } : {}
53689
+ });
53690
+ }
53691
+ function checkScopes(scopes, token) {
53692
+ if (!scopes?.length)
53693
+ return;
53694
+ const missing = scopes.filter((s) => !token?.scopes.includes(s));
53695
+ if (!missing.length)
53696
+ return;
53697
+ const message = `This token is missing the scope: ${missing.join(" ")}. Request a new token with it and try again.`;
53698
+ throw new ProtocolError(ProtocolErrorCode.InvalidRequest, `insufficient_scope: ${message}`, {
53699
+ success: false,
53700
+ error: message,
53701
+ code: "insufficient_scope"
53702
+ });
53703
+ }
53704
+ function baseContext(mcp, deps) {
53705
+ const { c, token, confirmSupport } = deps.context;
53706
+ return {
53707
+ signal: mcp.mcpReq.signal,
53708
+ mcp,
53709
+ confirmSupport,
53710
+ ...c ? { c } : {},
53711
+ ...token ? { token } : {}
53712
+ };
53713
+ }
53714
+ function toContents(uri, value, mimeType) {
53530
53715
  if (value !== null && typeof value === "object" && RAW in value)
53531
53716
  return value;
53717
+ if (typeof value === "string")
53718
+ return { contents: [{ uri: uri.href, mimeType: mimeType ?? "text/plain", text: value }] };
53719
+ if (value instanceof Uint8Array) {
53720
+ let bin = "";
53721
+ for (const b of value)
53722
+ bin += String.fromCharCode(b);
53723
+ return {
53724
+ contents: [
53725
+ { uri: uri.href, mimeType: mimeType ?? "application/octet-stream", blob: btoa(bin) }
53726
+ ]
53727
+ };
53728
+ }
53729
+ return {
53730
+ contents: [
53731
+ { uri: uri.href, mimeType: mimeType ?? "application/json", text: JSON.stringify(value) }
53732
+ ]
53733
+ };
53734
+ }
53735
+ function registerResources(server, resources, deps) {
53736
+ const register2 = server.registerResource.bind(server);
53737
+ for (const r of resources) {
53738
+ const meta3 = { tool: r.name, kind: "resource" };
53739
+ const ctxOf = (mcp) => ({
53740
+ resource: r.name,
53741
+ ...baseContext(mcp, deps)
53742
+ });
53743
+ const run = async (uri, variables, mcp) => {
53744
+ checkScopes(r.scopes, deps.context.token);
53745
+ let value;
53746
+ try {
53747
+ const read = r.read;
53748
+ value = await read(r.uri !== undefined ? { uri } : { uri, variables }, ctxOf(mcp));
53749
+ } catch (err) {
53750
+ throw protocolError(err, deps, meta3);
53751
+ }
53752
+ if (value === null || value === undefined)
53753
+ throw new ResourceNotFoundError(uri.href);
53754
+ if (isInputRequiredResult(value))
53755
+ return value;
53756
+ return toContents(uri, value, r.mimeType);
53757
+ };
53758
+ const config2 = {
53759
+ ...r.title ? { title: r.title } : {},
53760
+ ...r.description ? { description: r.description } : {},
53761
+ ...r.mimeType ? { mimeType: r.mimeType } : {}
53762
+ };
53763
+ if (r.uri !== undefined) {
53764
+ register2(r.name, r.uri, config2, (uri, mcp) => run(uri, {}, mcp));
53765
+ continue;
53766
+ }
53767
+ const list = r.list;
53768
+ const template = new ResourceTemplate(r.template, {
53769
+ list: list ? async (mcp) => {
53770
+ try {
53771
+ checkScopes(r.scopes, deps.context.token);
53772
+ } catch {
53773
+ return { resources: [] };
53774
+ }
53775
+ try {
53776
+ return { resources: await list(ctxOf(mcp)) };
53777
+ } catch (err) {
53778
+ throw protocolError(err, deps, meta3);
53779
+ }
53780
+ } : undefined
53781
+ });
53782
+ register2(r.name, template, config2, (uri, vars, mcp) => {
53783
+ const variables = {};
53784
+ for (const [k, v] of Object.entries(vars ?? {}))
53785
+ variables[k] = Array.isArray(v) ? v[0] ?? "" : v;
53786
+ return run(uri, variables, mcp);
53787
+ });
53788
+ }
53789
+ }
53790
+ function toPromptResult(value) {
53791
+ if (isInputRequiredResult(value))
53792
+ return value;
53793
+ if (typeof value === "string")
53794
+ return { messages: [{ role: "user", content: { type: "text", text: value } }] };
53795
+ if (Array.isArray(value))
53796
+ return {
53797
+ 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)
53798
+ };
53799
+ return value;
53800
+ }
53801
+ function registerPrompts(server, prompts, deps) {
53802
+ const register2 = server.registerPrompt.bind(server);
53803
+ for (const p of prompts) {
53804
+ const meta3 = { tool: p.name, kind: "prompt" };
53805
+ const run = async (args, mcp) => {
53806
+ checkScopes(p.scopes, deps.context.token);
53807
+ try {
53808
+ const render = p.render;
53809
+ return toPromptResult(await render(args, { prompt: p.name, ...baseContext(mcp, deps) }));
53810
+ } catch (err) {
53811
+ throw protocolError(err, deps, meta3);
53812
+ }
53813
+ };
53814
+ register2(p.name, {
53815
+ ...p.title ? { title: p.title } : {},
53816
+ ...p.description ? { description: p.description } : {},
53817
+ ...p.args ? { argsSchema: p.args } : {}
53818
+ }, p.args ? (args, mcp) => run(args, mcp) : (mcp) => run({}, mcp));
53819
+ }
53820
+ }
53821
+
53822
+ // ../../node_modules/.bun/@nimit9+signet-server@0.2.7+e803b2305afdb064/node_modules/@nimit9/signet-server/dist/mcp/server.js
53823
+ var RAW2 = Symbol.for("signet.mcp.rawResult");
53824
+ function toolResult(result) {
53825
+ return Object.defineProperty({ ...result }, RAW2, { value: true, enumerable: false });
53826
+ }
53827
+ function defineTool(def) {
53828
+ return def;
53829
+ }
53830
+ function toResult(value, structured) {
53831
+ if (value !== null && typeof value === "object" && RAW2 in value)
53832
+ return value;
53532
53833
  if (isInputRequiredResult(value))
53533
53834
  return value;
53534
53835
  const text = typeof value === "string" ? value : JSON.stringify(value ?? null) ?? "null";
@@ -53541,10 +53842,19 @@ function toResult(value, structured) {
53541
53842
  }
53542
53843
  function createMcpServer(options) {
53543
53844
  const unlisted = new Set(options.tools.filter((t) => t.listed === false).map((t) => t.name));
53544
- const toolsCapability = { tools: { listChanged: false } };
53845
+ const toolsCapability = {
53846
+ tools: { listChanged: false },
53847
+ ...options.resources?.length ? { resources: { listChanged: false } } : {},
53848
+ ...options.prompts?.length ? { prompts: { listChanged: false } } : {}
53849
+ };
53850
+ const confirmCodec = options.confirm ? createRequestStateCodec({
53851
+ key: options.confirm.key,
53852
+ ...options.confirm.ttlSeconds !== undefined ? { ttlSeconds: options.confirm.ttlSeconds } : {}
53853
+ }) : undefined;
53545
53854
  const server = new McpServer({ name: options.name, version: options.version }, {
53546
53855
  ...unlisted.size ? {} : { capabilities: toolsCapability },
53547
- ...options.instructions ? { instructions: options.instructions } : {}
53856
+ ...options.instructions ? { instructions: options.instructions } : {},
53857
+ ...confirmCodec ? { requestState: { verify: confirmCodec.verify } } : {}
53548
53858
  });
53549
53859
  let restore;
53550
53860
  if (unlisted.size) {
@@ -53559,6 +53869,10 @@ function createMcpServer(options) {
53559
53869
  tool: tool.name,
53560
53870
  signal: mcp.mcpReq.signal,
53561
53871
  mcp,
53872
+ confirmSupport: {
53873
+ getClientCapabilities: () => server.server.getClientCapabilities(),
53874
+ ...confirmCodec ? { mint: (payload) => confirmCodec.mint(payload, mcp) } : {}
53875
+ },
53562
53876
  ...c ? { c } : {},
53563
53877
  ...token ? { token } : {}
53564
53878
  };
@@ -53581,7 +53895,7 @@ function createMcpServer(options) {
53581
53895
  } catch {}
53582
53896
  const info = options.mapError?.(err) ?? describeToolError(err);
53583
53897
  if (!info)
53584
- report(options, err, tool.name, c);
53898
+ report(options, err, { tool: tool.name }, c);
53585
53899
  if (custom2)
53586
53900
  return custom2;
53587
53901
  return toolErrorResult(info ?? INTERNAL_ERROR2);
@@ -53597,6 +53911,18 @@ function createMcpServer(options) {
53597
53911
  restore?.();
53598
53912
  restore = undefined;
53599
53913
  }
53914
+ const confirmSupport = {
53915
+ getClientCapabilities: () => server.server.getClientCapabilities(),
53916
+ ...confirmCodec ? { mint: (payload) => confirmCodec.mint(payload) } : {}
53917
+ };
53918
+ const deps = {
53919
+ options,
53920
+ context: { confirmSupport, ...c ? { c } : {}, ...token ? { token } : {} }
53921
+ };
53922
+ if (options.resources?.length)
53923
+ registerResources(server, options.resources, deps);
53924
+ if (options.prompts?.length)
53925
+ registerPrompts(server, options.prompts, deps);
53600
53926
  return server;
53601
53927
  }
53602
53928
  function hideUnlisted(server, unlisted) {
@@ -53618,7 +53944,6 @@ function hideUnlisted(server, unlisted) {
53618
53944
  throw new Error("signet mcp: could not hide unlisted tools; @modelcontextprotocol/server no longer installs tools/list through setRequestHandler");
53619
53945
  };
53620
53946
  }
53621
-
53622
53947
  // src/annotations.ts
53623
53948
  var RO = { readOnlyHint: true, openWorldHint: false };
53624
53949
  var WI = {
@@ -53696,84 +54021,34 @@ function applyAnnotations(tools) {
53696
54021
  }
53697
54022
 
53698
54023
  // src/lib/confirm.ts
53699
- import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
53700
- var ANSWER_KEY = "confirm";
53701
- function clientCanConfirm(mcp, initializeCapabilities) {
53702
- const envelope = mcp.mcpReq.envelope;
53703
- const caps = envelope?.[CLIENT_CAPABILITIES_META_KEY] ?? initializeCapabilities();
53704
- return Boolean(caps?.elicitation);
53705
- }
53706
- function planFingerprint(plan) {
53707
- 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;
53708
- return createHash("sha256").update(JSON.stringify(canonical(plan))).digest("hex");
53709
- }
53710
- var STATE_KEY = randomBytes(32);
53711
- function sign(nonce, fingerprint) {
53712
- return createHmac("sha256", STATE_KEY).update(`${nonce}.${fingerprint}`).digest("hex");
53713
- }
53714
- function mintState(fingerprint) {
53715
- const nonce = randomBytes(16).toString("hex");
53716
- return `${nonce}.${sign(nonce, fingerprint)}`;
53717
- }
53718
- function verifyState(state, fingerprint) {
53719
- if (typeof state !== "string")
53720
- return false;
53721
- const [nonce, mac3, ...rest] = state.split(".");
53722
- if (!nonce || !mac3 || rest.length)
53723
- return false;
53724
- const expected = Buffer.from(sign(nonce, fingerprint), "hex");
53725
- const given = Buffer.from(mac3, "hex");
53726
- return given.length === expected.length && timingSafeEqual(given, expected);
53727
- }
53728
- function confirmPlan(mcp, message, plan, initializeCapabilities = () => {
53729
- return;
53730
- }) {
53731
- if (!clientCanConfirm(mcp, initializeCapabilities))
53732
- return { kind: "unsupported" };
53733
- const fingerprint = planFingerprint(plan);
53734
- const responses = mcp.mcpReq.inputResponses;
53735
- const answered = responses !== undefined && ANSWER_KEY in responses;
53736
- const echoed = mcp.mcpReq.requestState?.();
53737
- if (answered && verifyState(echoed, fingerprint)) {
53738
- const content = acceptedContent(responses, ANSWER_KEY);
53739
- if (content?.confirm === true)
53740
- return { kind: "confirmed" };
53741
- return {
53742
- kind: "declined",
53743
- result: { cancelled: true, message: "Cancelled by the user. Nothing was changed." }
53744
- };
53745
- }
53746
- return {
53747
- kind: "ask",
53748
- result: inputRequired({
53749
- inputRequests: {
53750
- [ANSWER_KEY]: inputRequired.elicit({
53751
- message,
53752
- requestedSchema: {
53753
- type: "object",
53754
- properties: {
53755
- confirm: { type: "boolean", title: "Confirm", description: "This can't be undone." }
53756
- },
53757
- required: ["confirm"]
53758
- }
53759
- })
53760
- },
53761
- requestState: mintState(fingerprint)
53762
- })
53763
- };
54024
+ var CANCELLED = { cancelled: true, message: "Cancelled by the user. Nothing was changed." };
54025
+ var PLAN_CHANGED = {
54026
+ planChanged: true,
54027
+ message: "The request changed after you confirmed, so nothing was changed. Run it again to see the new plan."
54028
+ };
54029
+ function acceptedDifferentPlan(ctx) {
54030
+ const view = inputResponse(ctx.mcp.mcpReq.inputResponses, `signet_confirm:${ctx.tool}`);
54031
+ return view.kind === "elicit" && view.action === "accept" && view.content?.confirm === true;
54032
+ }
54033
+ async function askThenRun(ctx, message, plan, run, fallback = run) {
54034
+ const answer = await confirm(ctx, { message, args: plan });
54035
+ if (isConfirmPending(answer))
54036
+ return answer;
54037
+ if (answer === "confirmed")
54038
+ return run();
54039
+ if (answer === "unsupported")
54040
+ return fallback();
54041
+ return acceptedDifferentPlan(ctx) ? PLAN_CHANGED : CANCELLED;
53764
54042
  }
53765
- function withConfirmation(tool, describe3, initializeCapabilities) {
54043
+ function withConfirmation(tool, describe3) {
53766
54044
  const run = tool.handler;
53767
54045
  return {
53768
54046
  ...tool,
53769
- handler: (args, ctx) => {
54047
+ handler: async (args, ctx) => {
53770
54048
  const message = describe3(args);
53771
54049
  if (message === null || !ctx?.mcp)
53772
54050
  return run(args, ctx);
53773
- const outcome = confirmPlan(ctx.mcp, message, { tool: tool.name, args }, initializeCapabilities);
53774
- if (outcome.kind === "confirmed" || outcome.kind === "unsupported")
53775
- return run(args, ctx);
53776
- return outcome.result;
54051
+ return askThenRun(ctx, message, { tool: tool.name, args }, () => run(args, ctx));
53777
54052
  }
53778
54053
  };
53779
54054
  }
@@ -53833,6 +54108,42 @@ var CONFIRMED_TOOLS = {
53833
54108
  set_encryption_mode: describeDeletes.encryptionMode,
53834
54109
  manage_debt: describeDeletes.settleDebt
53835
54110
  };
54111
+ var BULK_CONFIRM_THRESHOLD = 20;
54112
+ var sum = (rows, key) => Array.isArray(rows) ? rows.reduce((n, r) => n + (Number(r?.[key]) || 0), 0) : 0;
54113
+ var fromDryRun = (listKey, countKey) => async (args, dryRun) => {
54114
+ const r = await dryRun({ ...args, dryRun: true });
54115
+ return {
54116
+ n: sum(r?.[listKey], countKey),
54117
+ planHash: typeof r?.planHash === "string" ? r.planHash : undefined
54118
+ };
54119
+ };
54120
+ var BULK_CONFIRMED_TOOLS = {
54121
+ categorize_groups: fromDryRun("applied", "rows"),
54122
+ backfill_persons: fromDryRun("plan", "txnCount"),
54123
+ bulk_categorize_transactions: async (args) => ({
54124
+ n: Array.isArray(args.links) ? args.links.length : 0
54125
+ }),
54126
+ bulk_link_entity: async (args) => ({ n: Array.isArray(args.links) ? args.links.length : 0 })
54127
+ };
54128
+ function withBulkConfirmation(tool, count) {
54129
+ const run = tool.handler;
54130
+ const runPlanned = async (args, ctx, hash2) => {
54131
+ const result = await run(hash2 === undefined ? args : { ...args, expectedPlanHash: hash2 }, ctx);
54132
+ return result?.planChanged ? PLAN_CHANGED : result;
54133
+ };
54134
+ return {
54135
+ ...tool,
54136
+ handler: async (args, ctx) => {
54137
+ const canAsk = Boolean(ctx?.confirmSupport?.getClientCapabilities()?.elicitation);
54138
+ if (args.dryRun === true || !ctx?.mcp || !canAsk)
54139
+ return run(args, ctx);
54140
+ const { n, planHash: hash2 } = await count(args, async (a) => run(a, ctx));
54141
+ if (n <= BULK_CONFIRM_THRESHOLD)
54142
+ return runPlanned(args, ctx, hash2);
54143
+ return askThenRun(ctx, `Update ${plural2(n, "transaction")} in one go?`, { tool: tool.name, args, n, planHash: hash2 }, () => runPlanned(args, ctx, hash2), () => run(args, ctx));
54144
+ }
54145
+ };
54146
+ }
53836
54147
  // src/lib/union-tool.ts
53837
54148
  function variant(input2, run) {
53838
54149
  return { input: input2, run };
@@ -54025,15 +54336,15 @@ var PROMPTS = [
54025
54336
  `)
54026
54337
  }
54027
54338
  ];
54028
- function registerPrompts(server) {
54339
+ function registerPrompts2(server2) {
54029
54340
  for (const p of PROMPTS) {
54030
54341
  const cb = (args) => ({
54031
54342
  messages: [{ role: "user", content: { type: "text", text: p.text(args) } }]
54032
54343
  });
54033
54344
  if (p.args) {
54034
- server.registerPrompt(p.name, { title: p.title, description: p.description, argsSchema: p.args }, (args) => cb(args));
54345
+ server2.registerPrompt(p.name, { title: p.title, description: p.description, argsSchema: p.args }, (args) => cb(args));
54035
54346
  } else {
54036
- server.registerPrompt(p.name, { title: p.title, description: p.description }, () => cb({}));
54347
+ server2.registerPrompt(p.name, { title: p.title, description: p.description }, () => cb({}));
54037
54348
  }
54038
54349
  }
54039
54350
  }
@@ -54502,7 +54813,7 @@ function goalTools(client) {
54502
54813
  return [
54503
54814
  defineTool({
54504
54815
  name: "upsert_savings",
54505
- description: "Create (omit id) or update (pass id; all fields optional) a savings goal, EMI or sinking fund. Amounts are ₹ decimal strings, dates YYYY-MM-DD. Required on create — " + "goal: goalType, name, targetAmount. emi: name, totalAmount, emiAmount (monthly), startDate, endDate (last payment), totalMonths, owner. " + "sinking_fund: name, targetAmount, targetDate (due), monthlyContribution.",
54816
+ description: "Create (omit id) or update (pass id; all fields optional) a savings goal, EMI or sinking fund. Amounts are ₹ decimal strings, dates YYYY-MM-DD. Required on create — " + "goal: goalType, name, targetAmount. emi: name, totalAmount, emiAmount (monthly), startDate, endDate (last payment), totalMonths, owner. " + "sinking_fund: name, targetAmount, targetDate (due), monthlyContribution. " + "status — goal: active|paused|completed|cancelled; emi: active|completed|foreclosed; sinking funds have none.",
54506
54817
  input: exports_external.object({
54507
54818
  type: exports_external.enum(["goal", "emi", "sinking_fund"]),
54508
54819
  id: exports_external.string().optional(),
@@ -54523,7 +54834,8 @@ function goalTools(client) {
54523
54834
  startDate: exports_external.string().optional(),
54524
54835
  endDate: exports_external.string().optional(),
54525
54836
  totalMonths: exports_external.number().optional(),
54526
- paidMonths: exports_external.number().optional()
54837
+ paidMonths: exports_external.number().optional(),
54838
+ status: exports_external.enum(["active", "paused", "completed", "cancelled", "foreclosed"]).optional().describe("goal: active|paused|completed|cancelled; emi: active|completed|foreclosed")
54527
54839
  }),
54528
54840
  handler: async ({ type, ...fields }) => {
54529
54841
  const require2 = (keys) => {
@@ -54532,6 +54844,14 @@ function goalTools(client) {
54532
54844
  throw new Error(`upsert_savings type='${type}' requires: ${missing.join(", ")}`);
54533
54845
  }
54534
54846
  };
54847
+ const allowedStatus = {
54848
+ goal: ["active", "paused", "completed", "cancelled"],
54849
+ emi: ["active", "completed", "foreclosed"],
54850
+ sinking_fund: []
54851
+ };
54852
+ if (fields.status !== undefined && !allowedStatus[type].includes(fields.status)) {
54853
+ throw new Error(type === "sinking_fund" ? "upsert_savings type='sinking_fund' has no status" : `upsert_savings type='${type}' status must be one of: ${allowedStatus[type].join(", ")}`);
54854
+ }
54535
54855
  if (type === "goal") {
54536
54856
  const {
54537
54857
  id: id2,
@@ -54544,7 +54864,8 @@ function goalTools(client) {
54544
54864
  owner,
54545
54865
  icon,
54546
54866
  color,
54547
- notes
54867
+ notes,
54868
+ status
54548
54869
  } = fields;
54549
54870
  const body2 = {
54550
54871
  type: goalType,
@@ -54556,7 +54877,8 @@ function goalTools(client) {
54556
54877
  owner,
54557
54878
  icon,
54558
54879
  color,
54559
- notes
54880
+ notes,
54881
+ status
54560
54882
  };
54561
54883
  if (!id2)
54562
54884
  require2(["name", "targetAmount", "goalType"]);
@@ -54574,7 +54896,8 @@ function goalTools(client) {
54574
54896
  totalMonths,
54575
54897
  paidMonths,
54576
54898
  interestRate,
54577
- outstandingPrincipal
54899
+ outstandingPrincipal,
54900
+ status
54578
54901
  } = fields;
54579
54902
  const { owner } = fields;
54580
54903
  if (!id2) {
@@ -54598,7 +54921,8 @@ function goalTools(client) {
54598
54921
  paidMonths,
54599
54922
  interestRate,
54600
54923
  outstandingPrincipal,
54601
- owner
54924
+ owner,
54925
+ status
54602
54926
  };
54603
54927
  const data2 = id2 ? await client.patch(`/api/goals/emis/${id2}`, body2) : await client.post("/api/goals/emis", body2);
54604
54928
  return data2;
@@ -58808,6 +59132,12 @@ function parseAmount4(raw) {
58808
59132
  const num = parseFloat(cleaned);
58809
59133
  return Number.isNaN(num) ? 0 : Math.abs(num);
58810
59134
  }
59135
+ // src/lib/plan-hash.ts
59136
+ import { createHash } from "node:crypto";
59137
+ function planHash(ids) {
59138
+ return createHash("sha256").update([...ids].sort().join(",")).digest("hex");
59139
+ }
59140
+
58811
59141
  // src/lib/txn-rows.ts
58812
59142
  function toTxnRow(r2) {
58813
59143
  return {
@@ -58883,6 +59213,9 @@ function toLeanRow(r2, personNames) {
58883
59213
  function normalizePersonName(name) {
58884
59214
  return name.toLowerCase().trim().replace(/\s+/g, " ");
58885
59215
  }
59216
+ function normalizeUpiHandle(handle) {
59217
+ return handle.toLowerCase().trim().split("@")[0];
59218
+ }
58886
59219
  async function loadDecryptedPersons(client, crypto3) {
58887
59220
  const res = await client.get("/api/persons");
58888
59221
  const persons = [];
@@ -58894,7 +59227,7 @@ async function loadDecryptedPersons(client, crypto3) {
58894
59227
  } catch {}
58895
59228
  }
58896
59229
  if (name)
58897
- persons.push({ id: p2.id, name });
59230
+ persons.push({ id: p2.id, name, upiHandles: p2.upiHandles ?? [] });
58898
59231
  }
58899
59232
  return persons;
58900
59233
  }
@@ -58934,10 +59267,15 @@ function isPersonLikeNarration(description) {
58934
59267
  async function backfillPersons(client, crypto3, opts = {}) {
58935
59268
  const existing = await loadDecryptedPersons(client, crypto3);
58936
59269
  const personIdByNorm = new Map;
58937
- for (const p2 of existing)
59270
+ const personIdByHandle = new Map;
59271
+ for (const p2 of existing) {
58938
59272
  personIdByNorm.set(normalizePersonName(p2.name), p2.id);
59273
+ for (const h2 of p2.upiHandles)
59274
+ personIdByHandle.set(normalizeUpiHandle(h2), p2.id);
59275
+ }
58939
59276
  const { rows, truncated } = await fetchAllRowsPaged(client, crypto3, { unresolved: "true" });
58940
59277
  const candidates = rows.filter((r2) => !r2.merchantId && !r2.personId && !r2.isTransfer && !r2.isIgnored);
59278
+ const directLinks = [];
58941
59279
  const groups = new Map;
58942
59280
  for (const t2 of candidates) {
58943
59281
  const { name, upiId, paymentMethod } = parseNarration(t2.description);
@@ -58945,6 +59283,11 @@ async function backfillPersons(client, crypto3, opts = {}) {
58945
59283
  continue;
58946
59284
  if (classifyEntity({ paymentMethod, extractedName: name }) !== "person")
58947
59285
  continue;
59286
+ const handlePersonId = upiId ? personIdByHandle.get(normalizeUpiHandle(upiId)) : undefined;
59287
+ if (handlePersonId) {
59288
+ directLinks.push({ transactionId: t2.id, personId: handlePersonId, upiHandle: upiId });
59289
+ continue;
59290
+ }
58948
59291
  const norm = normalizePersonName(name);
58949
59292
  if (!norm)
58950
59293
  continue;
@@ -58960,7 +59303,7 @@ async function backfillPersons(client, crypto3, opts = {}) {
58960
59303
  }
58961
59304
  const plan = [];
58962
59305
  const newGroups = [];
58963
- let reused = 0;
59306
+ const reusedPersonIds = new Set;
58964
59307
  for (const [norm, g2] of groups) {
58965
59308
  const upiHandles = [...g2.upiHandles];
58966
59309
  const isReuse = personIdByNorm.has(norm);
@@ -58972,10 +59315,47 @@ async function backfillPersons(client, crypto3, opts = {}) {
58972
59315
  action: isReuse ? "reuse" : "create"
58973
59316
  });
58974
59317
  if (isReuse)
58975
- reused++;
59318
+ reusedPersonIds.add(personIdByNorm.get(norm));
58976
59319
  else
58977
59320
  newGroups.push({ norm, displayName: g2.displayName, upiHandles });
58978
59321
  }
59322
+ const directByPerson = new Map;
59323
+ for (const d2 of directLinks) {
59324
+ const e2 = directByPerson.get(d2.personId) ?? { txnIds: [], handles: new Set };
59325
+ e2.txnIds.push(d2.transactionId);
59326
+ if (d2.upiHandle)
59327
+ e2.handles.add(d2.upiHandle);
59328
+ directByPerson.set(d2.personId, e2);
59329
+ }
59330
+ for (const [personId, e2] of directByPerson) {
59331
+ const person = existing.find((p2) => p2.id === personId);
59332
+ plan.push({
59333
+ name: person?.name ?? personId,
59334
+ normalized: person ? normalizePersonName(person.name) : personId,
59335
+ txnCount: e2.txnIds.length,
59336
+ upiHandles: [...e2.handles],
59337
+ action: "reuse"
59338
+ });
59339
+ reusedPersonIds.add(personId);
59340
+ }
59341
+ const reused = reusedPersonIds.size;
59342
+ const hash2 = planHash([
59343
+ ...[...groups.values()].flatMap((g2) => g2.txnIds),
59344
+ ...directLinks.map((d2) => d2.transactionId)
59345
+ ]);
59346
+ if (!opts.dryRun && opts.expectedPlanHash !== undefined && opts.expectedPlanHash !== hash2) {
59347
+ return {
59348
+ dryRun: false,
59349
+ truncated,
59350
+ candidatesScanned: candidates.length,
59351
+ created: 0,
59352
+ reused: 0,
59353
+ linked: 0,
59354
+ plan,
59355
+ planHash: hash2,
59356
+ planChanged: true
59357
+ };
59358
+ }
58979
59359
  if (!opts.dryRun && newGroups.length > 0) {
58980
59360
  const bodies = await Promise.all(newGroups.map(async ({ displayName, upiHandles }) => {
58981
59361
  const fields = {
@@ -59004,11 +59384,13 @@ async function backfillPersons(client, crypto3, opts = {}) {
59004
59384
  for (const txnId of g2.txnIds)
59005
59385
  links.push({ transactionId: txnId, personId });
59006
59386
  }
59387
+ links.push(...directLinks);
59007
59388
  }
59008
59389
  if (!opts.dryRun && links.length > 0) {
59009
59390
  const CHUNK = 25;
59010
59391
  for (let i2 = 0;i2 < links.length; i2 += CHUNK) {
59011
59392
  await client.post("/api/transactions/bulk-link-entity", { links: links.slice(i2, i2 + CHUNK) });
59393
+ await opts.onProgress?.(Math.min(i2 + CHUNK, links.length), links.length, "Linking transactions to people");
59012
59394
  }
59013
59395
  }
59014
59396
  return {
@@ -59018,7 +59400,8 @@ async function backfillPersons(client, crypto3, opts = {}) {
59018
59400
  created,
59019
59401
  reused,
59020
59402
  linked: links.length,
59021
- plan
59403
+ plan,
59404
+ planHash: hash2
59022
59405
  };
59023
59406
  }
59024
59407
 
@@ -59142,9 +59525,19 @@ async function applyGroupAssignments(client, crypto3, assignments, opts = {}) {
59142
59525
  }
59143
59526
  for (const g2 of hits) {
59144
59527
  claimed.add(g2);
59145
- plan.push({ group: g2, categoryId: cat.id, slug: cat.slug, learn: a2.learn !== false });
59528
+ plan.push({
59529
+ group: g2,
59530
+ categoryId: cat.id,
59531
+ slug: cat.slug,
59532
+ learn: a2.learn !== false,
59533
+ confirmMerchant: !!a2.confirmMerchant
59534
+ });
59146
59535
  }
59147
59536
  }
59537
+ result.planHash = planHash(plan.flatMap((p2) => p2.group.rows.map((r2) => r2.id)));
59538
+ if (!opts.dryRun && opts.expectedPlanHash !== undefined && opts.expectedPlanHash !== result.planHash) {
59539
+ return { ...result, planChanged: true };
59540
+ }
59148
59541
  if (opts.dryRun) {
59149
59542
  result.applied = plan.map((p2) => ({
59150
59543
  pattern: p2.group.pattern,
@@ -59172,10 +59565,10 @@ async function applyGroupAssignments(client, crypto3, assignments, opts = {}) {
59172
59565
  const learnPlans = [];
59173
59566
  const directPlans = [];
59174
59567
  for (const p2 of plan) {
59175
- const { group, learn } = p2;
59568
+ const { group, learn, confirmMerchant } = p2;
59176
59569
  if (!learn || group.pattern === UNPARSED) {
59177
59570
  directPlans.push({ p: p2, reason: learn ? UNPARSED : undefined });
59178
- } else if (crypto3 && group.rows.some((r2) => isPersonLikeNarration(r2.description))) {
59571
+ } else if (crypto3 && !confirmMerchant && group.rows.some((r2) => isPersonLikeNarration(r2.description))) {
59179
59572
  directPlans.push({ p: p2, reason: `${group.pattern} (private mode: person-like)` });
59180
59573
  } else if (taught.has(group.pattern) && taught.get(group.pattern) !== p2.categoryId) {
59181
59574
  directPlans.push({ p: p2, reason: `${group.pattern} (conflicting category for same pattern)` });
@@ -59232,7 +59625,11 @@ async function applyGroupAssignments(client, crypto3, assignments, opts = {}) {
59232
59625
  }
59233
59626
  if (result.rulesSaved > 0)
59234
59627
  await saveBag(client, bag, version2);
59628
+ const total = links.length + direct.reduce((n2, d2) => n2 + d2.ids.length, 0);
59629
+ let done = 0;
59235
59630
  await postLinks(client, links);
59631
+ done += links.length;
59632
+ await opts.onProgress?.(done, total, "Categorizing transactions");
59236
59633
  for (const { ids, categoryId } of direct) {
59237
59634
  const categoryType = catIndex.byId.get(categoryId)?.type;
59238
59635
  for (let i2 = 0;i2 < ids.length; i2 += 200) {
@@ -59241,11 +59638,20 @@ async function applyGroupAssignments(client, crypto3, assignments, opts = {}) {
59241
59638
  categoryId,
59242
59639
  ...categoryType ? { categoryType } : {}
59243
59640
  });
59641
+ done += Math.min(200, ids.length - i2);
59642
+ await opts.onProgress?.(done, total, "Categorizing transactions");
59244
59643
  }
59245
59644
  }
59246
59645
  return result;
59247
59646
  }
59248
59647
 
59648
+ // src/lib/progress.ts
59649
+ function progressFor(ctx) {
59650
+ if (!ctx?.mcp)
59651
+ return;
59652
+ return (done, total, message) => progress(ctx, done, total, message);
59653
+ }
59654
+
59249
59655
  // src/tools/transactions.ts
59250
59656
  function dedupeKey(date5, description, amount, type) {
59251
59657
  return `${date5}|${description}|${amount.toFixed(2)}|${type}`;
@@ -59293,6 +59699,7 @@ async function importTransactions(client, crypto3, opts) {
59293
59699
  transactions: rows.slice(i2, i2 + CHUNK)
59294
59700
  });
59295
59701
  imported += res.imported ?? 0;
59702
+ await opts.onProgress?.(Math.min(i2 + CHUNK, rows.length), rows.length, "Importing transactions");
59296
59703
  }
59297
59704
  return { imported, duplicatesSkipped, duplicatesFlagged };
59298
59705
  }
@@ -59467,23 +59874,34 @@ function transactionTools(client, crypto3) {
59467
59874
  pattern: exports_external.string().describe("Exactly as returned"),
59468
59875
  type: exports_external.enum(["debit", "credit"]).optional(),
59469
59876
  categorySlug: exports_external.string(),
59470
- learn: exports_external.boolean().optional().describe("false = no rule (short/generic patterns)")
59877
+ learn: exports_external.boolean().optional().describe("false = no rule (short/generic patterns)"),
59878
+ confirmMerchant: exports_external.boolean().optional().describe("true = you've confirmed this pattern is a real business (brand lookup, web search, etc), not a person — skips the private-mode person-like block that would otherwise refuse to save a merchant rule")
59471
59879
  })).min(1).max(100),
59472
59880
  startDate: exports_external.string().optional().describe("Same range as the listing"),
59473
59881
  endDate: exports_external.string().optional(),
59474
59882
  dryRun: exports_external.boolean().optional()
59475
59883
  }),
59476
- handler: ({ assignments, startDate, endDate, dryRun }) => applyGroupAssignments(client, crypto3, assignments, {
59477
- dryRun,
59478
- startDate,
59479
- endDate
59480
- })
59884
+ handler: (args, ctx) => {
59885
+ const { assignments, startDate, endDate, dryRun } = args;
59886
+ const { expectedPlanHash } = args;
59887
+ return applyGroupAssignments(client, crypto3, assignments, {
59888
+ dryRun,
59889
+ startDate,
59890
+ endDate,
59891
+ expectedPlanHash,
59892
+ onProgress: progressFor(ctx)
59893
+ });
59894
+ }
59481
59895
  }),
59482
59896
  defineTool({
59483
59897
  name: "backfill_persons",
59484
59898
  description: "Create persons from UPI/IMPS counterparties in unlinked transactions (deduped by name) and link those transactions to them. Idempotent. dryRun previews.",
59485
59899
  input: exports_external.object({ dryRun: exports_external.boolean().optional() }),
59486
- handler: ({ dryRun }) => backfillPersons(client, crypto3, { dryRun })
59900
+ handler: (args, ctx) => backfillPersons(client, crypto3, {
59901
+ onProgress: progressFor(ctx),
59902
+ dryRun: args.dryRun,
59903
+ expectedPlanHash: args.expectedPlanHash
59904
+ })
59487
59905
  }),
59488
59906
  defineTool({
59489
59907
  name: "update_transaction",
@@ -60210,7 +60628,7 @@ async function fetchReconciliationContext(client, accountId, start, end, budget
60210
60628
  const seen = new Set;
60211
60629
  return [...left, ...right].filter((r2) => r2.id == null ? true : !seen.has(r2.id) && seen.add(r2.id));
60212
60630
  }
60213
- var sum = (rows) => Math.round(rows.reduce((n2, r2) => n2 + r2.amount, 0));
60631
+ var sum2 = (rows) => Math.round(rows.reduce((n2, r2) => n2 + r2.amount, 0));
60214
60632
  async function runStatementImport(deps, args) {
60215
60633
  const { client, crypto: crypto3 } = deps;
60216
60634
  const accounts = (await client.get("/api/accounts")).data ?? [];
@@ -60252,8 +60670,8 @@ async function runStatementImport(deps, args) {
60252
60670
  conflictCount: result.conflicts.length,
60253
60671
  new: {
60254
60672
  n: fresh.length,
60255
- debits: { n: debits.length, total: sum(debits) },
60256
- credits: { n: credits.length, total: sum(credits) }
60673
+ debits: { n: debits.length, total: sum2(debits) },
60674
+ credits: { n: credits.length, total: sum2(credits) }
60257
60675
  }
60258
60676
  };
60259
60677
  if (!args.confirm) {
@@ -60273,6 +60691,7 @@ async function runStatementImport(deps, args) {
60273
60691
  return { ...summary, dryRun: false, imported: 0 };
60274
60692
  const savings = account.type !== "credit_card";
60275
60693
  const imported = await importTransactions(client, crypto3, {
60694
+ onProgress: deps.onProgress,
60276
60695
  accountId: account.id,
60277
60696
  source: savings ? "bank_statement" : "cc_statement",
60278
60697
  granularity: savings ? 1 : 2,
@@ -60321,7 +60740,13 @@ var statementSource = exports_external.enum([
60321
60740
  ]);
60322
60741
  var granularity = exports_external.number().int().min(1).max(5).describe("1=bank, 2=CC, 3=manual, 4=app export, 5=telegram/AI");
60323
60742
  function statementImportTools(client, crypto3) {
60324
- const pdf = (confirm) => variant(pdfInput, (args) => runStatementImport({ client, crypto: crypto3, readFile: readFileAsArrayBuffer, parsers: realParsers }, confirm ? { ...args, confirm: true } : args));
60743
+ const pdf = (confirm2) => variant(pdfInput, (args, ctx) => runStatementImport({
60744
+ client,
60745
+ crypto: crypto3,
60746
+ readFile: readFileAsArrayBuffer,
60747
+ parsers: realParsers,
60748
+ onProgress: progressFor(ctx)
60749
+ }, confirm2 ? { ...args, confirm: true } : args));
60325
60750
  const csvParse = variant(exports_external.object({ filePath: exports_external.string() }), async ({ filePath }) => {
60326
60751
  if (crypto3)
60327
60752
  return CSV_PRIVATE_MODE_ERROR;
@@ -60382,8 +60807,9 @@ function statementImportTools(client, crypto3) {
60382
60807
  description: exports_external.string(),
60383
60808
  referenceNumber: exports_external.string().optional()
60384
60809
  }))
60385
- }), async ({ accountId, source, granularity: granularity2, transactions: txns }) => {
60810
+ }), async ({ accountId, source, granularity: granularity2, transactions: txns }, ctx) => {
60386
60811
  const data = await importTransactions(client, crypto3, {
60812
+ onProgress: progressFor(ctx),
60387
60813
  accountId,
60388
60814
  source,
60389
60815
  granularity: granularity2,
@@ -60397,7 +60823,7 @@ function statementImportTools(client, crypto3) {
60397
60823
  source: statementSource,
60398
60824
  granularity,
60399
60825
  filePath: exports_external.string()
60400
- }), async ({ accountId, source, granularity: granularity2, filePath }) => {
60826
+ }), async ({ accountId, source, granularity: granularity2, filePath }, ctx) => {
60401
60827
  let txns;
60402
60828
  try {
60403
60829
  const raw = await readFile2(filePath, "utf8");
@@ -60414,6 +60840,7 @@ function statementImportTools(client, crypto3) {
60414
60840
  throw new Error(`Failed to read/parse ${filePath}: ${e2 instanceof Error ? e2.message : String(e2)}`);
60415
60841
  }
60416
60842
  const data = await importTransactions(client, crypto3, {
60843
+ onProgress: progressFor(ctx),
60417
60844
  accountId,
60418
60845
  source,
60419
60846
  granularity: granularity2,
@@ -60534,13 +60961,20 @@ function scrubCiphertext(value) {
60534
60961
  }
60535
60962
  async function runTool(tool, args, mcp) {
60536
60963
  const input2 = tool.input ? await tool.input.parseAsync(args) : args;
60537
- const ctx = { tool: tool.name, signal: mcp.mcpReq.signal, mcp };
60964
+ const ctx = {
60965
+ tool: tool.name,
60966
+ signal: mcp.mcpReq.signal,
60967
+ mcp,
60968
+ confirmSupport: { getClientCapabilities: () => {
60969
+ return;
60970
+ } }
60971
+ };
60538
60972
  return tool.handler(input2, ctx);
60539
60973
  }
60540
60974
  function jsonContents(uri, value) {
60541
60975
  return { contents: [{ uri: uri.href, mimeType: JSON_MIME, text: JSON.stringify(value ?? null) }] };
60542
60976
  }
60543
- function registerResources(server, tools) {
60977
+ function registerResources2(server2, tools) {
60544
60978
  const byName = new Map(tools.map((t2) => [t2.name, t2]));
60545
60979
  const need = (name) => {
60546
60980
  const t2 = byName.get(name);
@@ -60550,13 +60984,13 @@ function registerResources(server, tools) {
60550
60984
  };
60551
60985
  for (const r2 of RESOURCES) {
60552
60986
  const tool = need(r2.tool);
60553
- server.registerResource(r2.name, r2.uri, { title: r2.title, description: r2.description, mimeType: JSON_MIME }, async (uri, mcp) => {
60987
+ server2.registerResource(r2.name, r2.uri, { title: r2.title, description: r2.description, mimeType: JSON_MIME }, async (uri, mcp) => {
60554
60988
  const value = scrubCiphertext(await runTool(tool, r2.args, mcp));
60555
60989
  return jsonContents(uri, r2.shape ? r2.shape(value) : value);
60556
60990
  });
60557
60991
  }
60558
60992
  const getTransaction = need("get_transaction");
60559
- server.registerResource("transaction", new ResourceTemplate(TRANSACTION_TEMPLATE, { list: undefined }), {
60993
+ server2.registerResource("transaction", new ResourceTemplate(TRANSACTION_TEMPLATE, { list: undefined }), {
60560
60994
  title: "Transaction",
60561
60995
  description: "One transaction by UUID (same as get_transaction; decrypted in private mode).",
60562
60996
  mimeType: JSON_MIME
@@ -60569,7 +61003,7 @@ function registerResources(server, tools) {
60569
61003
  // package.json
60570
61004
  var package_default = {
60571
61005
  name: "paisa-mcp",
60572
- version: "0.4.0",
61006
+ version: "0.4.2",
60573
61007
  repository: {
60574
61008
  type: "git",
60575
61009
  url: "git+https://github.com/nimit9/paisa.git",
@@ -60598,7 +61032,7 @@ var package_default = {
60598
61032
  "@modelcontextprotocol/server": "^2.1.0",
60599
61033
  "@nimit9/signet-ai": "^0.1.11",
60600
61034
  "@nimit9/signet-lib": "^0.1.15",
60601
- "@nimit9/signet-server": "^0.2.2",
61035
+ "@nimit9/signet-server": "^0.2.7",
60602
61036
  "@paisa/parsers": "workspace:*",
60603
61037
  "@paisa/reconciliation": "workspace:*",
60604
61038
  "@paisa/types": "workspace:*",
@@ -60624,27 +61058,28 @@ function paisaErrorResult(err) {
60624
61058
  content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }]
60625
61059
  });
60626
61060
  }
61061
+ var CONFIRM_KEY2 = randomBytes(32);
60627
61062
  function buildServer(client, crypto3) {
60628
- let initializeCapabilities = () => {
60629
- return;
60630
- };
60631
61063
  const tools = applyAnnotations(REGISTRATIONS.flatMap((group) => group.tools(client, crypto3)).map((t2) => {
60632
61064
  const describe3 = CONFIRMED_TOOLS[t2.name];
60633
- return describe3 ? withConfirmation(t2, describe3, () => initializeCapabilities()) : t2;
61065
+ if (describe3)
61066
+ return withConfirmation(t2, describe3);
61067
+ const count = BULK_CONFIRMED_TOOLS[t2.name];
61068
+ return count ? withBulkConfirmation(t2, count) : t2;
60634
61069
  }));
60635
61070
  for (const t2 of tools)
60636
61071
  if (t2.input)
60637
61072
  withCompactJsonSchema(t2.input);
60638
- const server = createMcpServer({
61073
+ const server2 = createMcpServer({
60639
61074
  name: "paisa",
60640
61075
  version: VERSION2,
60641
61076
  tools,
60642
- errorResult: paisaErrorResult
61077
+ errorResult: paisaErrorResult,
61078
+ confirm: { key: CONFIRM_KEY2 }
60643
61079
  });
60644
- initializeCapabilities = () => server.server.getClientCapabilities();
60645
- registerResources(server, tools);
60646
- registerPrompts(server);
60647
- return server;
61080
+ registerResources2(server2, tools);
61081
+ registerPrompts2(server2);
61082
+ return server2;
60648
61083
  }
60649
61084
 
60650
61085
  // src/index.ts
@@ -60668,5 +61103,5 @@ main().catch((err) => {
60668
61103
  process.exit(1);
60669
61104
  });
60670
61105
 
60671
- //# debugId=F56B7BEACF2C363564756E2164756E21
61106
+ //# debugId=24F552E300AC132164756E2164756E21
60672
61107
  //# sourceMappingURL=index.js.map