paisa-mcp 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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)`;
@@ -53656,6 +53694,145 @@ function applyAnnotations(tools) {
53656
53694
  return { ...tool, title: meta3.title, annotations: meta3.annotations };
53657
53695
  });
53658
53696
  }
53697
+
53698
+ // 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
+ };
53764
+ }
53765
+ function withConfirmation(tool, describe3, initializeCapabilities) {
53766
+ const run = tool.handler;
53767
+ return {
53768
+ ...tool,
53769
+ handler: (args, ctx) => {
53770
+ const message = describe3(args);
53771
+ if (message === null || !ctx?.mcp)
53772
+ 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;
53777
+ }
53778
+ };
53779
+ }
53780
+ var safeDate = (v) => typeof v === "string" && /^\d{4}-\d{2}-\d{2}$/.test(v) ? v : v === undefined ? "any" : "a date";
53781
+ var safeAmount = (v) => typeof v === "string" && /^\d+(\.\d{1,2})?$/.test(v) || typeof v === "number" ? String(v) : null;
53782
+ var plural2 = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
53783
+ function describeTransactionFilters(f) {
53784
+ const parts = [];
53785
+ if (f.startDate || f.endDate)
53786
+ parts.push(`dated ${safeDate(f.startDate)} to ${safeDate(f.endDate)}`);
53787
+ if (f.type)
53788
+ parts.push(`${f.type}s only`);
53789
+ if (f.categoryType)
53790
+ parts.push(`category type ${f.categoryType}`);
53791
+ if (f.categoryId)
53792
+ parts.push("in one category");
53793
+ if (f.uncategorized)
53794
+ parts.push("uncategorized");
53795
+ if (f.amountMin !== undefined || f.amountMax !== undefined)
53796
+ parts.push(`amount ₹${f.amountMin ?? 0} to ${f.amountMax === undefined ? "any" : `₹${f.amountMax}`}`);
53797
+ if (f.owner)
53798
+ parts.push("one owner");
53799
+ if (f.search)
53800
+ parts.push("matching a text search");
53801
+ return parts.length ? parts.join(", ") : "with no filters (ALL transactions)";
53802
+ }
53803
+ var describeDeletes = {
53804
+ transactions: (a) => {
53805
+ if (a.id)
53806
+ return "Permanently delete 1 transaction?";
53807
+ if (Array.isArray(a.ids))
53808
+ return `Permanently delete ${plural2(a.ids.length, "transaction")}?`;
53809
+ return `Permanently delete every transaction ${describeTransactionFilters(a.filters ?? {})}?`;
53810
+ },
53811
+ entity: (a) => {
53812
+ const entity = String(a.entity ?? "item").replace(/_/g, " ");
53813
+ const n = Array.isArray(a.ids) ? a.ids.length : 1;
53814
+ const extra = a.entity === "merchant" ? " Their transactions are kept but unlinked." : a.entity === "debt" ? " Its history is lost; settling keeps it." : "";
53815
+ return `Permanently delete ${plural2(n, entity)}?${extra}`;
53816
+ },
53817
+ mergeMerchants: (a) => {
53818
+ const n = Array.isArray(a.duplicateIds) ? a.duplicateIds.length : 0;
53819
+ return `Merge ${plural2(n, "duplicate merchant")} into the one you're keeping? Their transactions move over and the duplicates are deleted.`;
53820
+ },
53821
+ encryptionMode: (a) => `Switch encryption mode to ${a.encryptionMode === "private" ? "private" : "standard"}?`,
53822
+ settleDebt: (a) => {
53823
+ if (a.action !== "settle")
53824
+ return null;
53825
+ const amount = a.settledAmount === undefined ? "0" : safeAmount(a.settledAmount);
53826
+ 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}.`;
53827
+ }
53828
+ };
53829
+ var CONFIRMED_TOOLS = {
53830
+ delete_transactions: describeDeletes.transactions,
53831
+ delete_entity: describeDeletes.entity,
53832
+ merge_merchants: describeDeletes.mergeMerchants,
53833
+ set_encryption_mode: describeDeletes.encryptionMode,
53834
+ manage_debt: describeDeletes.settleDebt
53835
+ };
53659
53836
  // src/lib/union-tool.ts
53660
53837
  function variant(input2, run) {
53661
53838
  return { input: input2, run };
@@ -60392,7 +60569,7 @@ function registerResources(server, tools) {
60392
60569
  // package.json
60393
60570
  var package_default = {
60394
60571
  name: "paisa-mcp",
60395
- version: "0.3.1",
60572
+ version: "0.4.0",
60396
60573
  repository: {
60397
60574
  type: "git",
60398
60575
  url: "git+https://github.com/nimit9/paisa.git",
@@ -60448,7 +60625,13 @@ function paisaErrorResult(err) {
60448
60625
  });
60449
60626
  }
60450
60627
  function buildServer(client, crypto3) {
60451
- const tools = applyAnnotations(REGISTRATIONS.flatMap((group) => group.tools(client, crypto3)));
60628
+ let initializeCapabilities = () => {
60629
+ return;
60630
+ };
60631
+ const tools = applyAnnotations(REGISTRATIONS.flatMap((group) => group.tools(client, crypto3)).map((t2) => {
60632
+ const describe3 = CONFIRMED_TOOLS[t2.name];
60633
+ return describe3 ? withConfirmation(t2, describe3, () => initializeCapabilities()) : t2;
60634
+ }));
60452
60635
  for (const t2 of tools)
60453
60636
  if (t2.input)
60454
60637
  withCompactJsonSchema(t2.input);
@@ -60458,6 +60641,7 @@ function buildServer(client, crypto3) {
60458
60641
  tools,
60459
60642
  errorResult: paisaErrorResult
60460
60643
  });
60644
+ initializeCapabilities = () => server.server.getClientCapabilities();
60461
60645
  registerResources(server, tools);
60462
60646
  registerPrompts(server);
60463
60647
  return server;
@@ -60484,5 +60668,5 @@ main().catch((err) => {
60484
60668
  process.exit(1);
60485
60669
  });
60486
60670
 
60487
- //# debugId=E169545C1C52BE7864756E2164756E21
60671
+ //# debugId=F56B7BEACF2C363564756E2164756E21
60488
60672
  //# sourceMappingURL=index.js.map