revclamp-mcp 1.20.8 → 1.20.9

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/README.md CHANGED
@@ -59,7 +59,7 @@ curl -sS -X POST https://api.revclamp.com/api/mcp \
59
59
 
60
60
  ```bash
61
61
  # Run a <60s local CLI smoke check (verifies all 9 tool schemas & executes a local_engine.mjs sample audit with zero API key)
62
- npx -y github:lhphong09t2/revclamp-mcp#v1.20.8 --self-test
62
+ npx -y github:lhphong09t2/revclamp-mcp#v1.20.9 --self-test
63
63
 
64
64
  # List all 9 MCP tools and JSON schemas (runs locally in-process)
65
65
  node index.mjs --list-tools
package/index.mjs CHANGED
@@ -33,7 +33,7 @@ export {
33
33
  executeLocalMcpJsonRpc,
34
34
  };
35
35
 
36
- export const PACKAGE_VERSION = "1.20.8";
36
+ export const PACKAGE_VERSION = "1.20.9";
37
37
  export const DEFAULT_MCP_ENDPOINT = "https://api.revclamp.com/api/mcp";
38
38
  export const ALLOWED_MCP_ORIGINS = Object.freeze(
39
39
  new Set(["https://api.revclamp.com", "https://revclamp.com"])
@@ -874,18 +874,191 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
874
874
  effectiveOptions.apiUrl ||
875
875
  DEFAULT_MCP_ENDPOINT
876
876
  );
877
- const smokeAudit = executeLocalMcpTool("revclamp_audit_revenue_leakage", { demo: true });
878
- const toolsCount = Array.isArray(LOCAL_MCP_TOOLS) ? LOCAL_MCP_TOOLS.length : 0;
877
+ const expectedToolNames = [
878
+ "revclamp_audit_revenue_leakage",
879
+ "revclamp_evaluate_ghost_usage",
880
+ "revclamp_stage_stripe_dispute_ce3",
881
+ "revclamp_validate_eu_vat_vies",
882
+ "revclamp_reconcile_b2b_net_terms",
883
+ "revclamp_verify_webhook_signature",
884
+ "revclamp_calculate_roi_and_plan_fit",
885
+ "revclamp_simulate_webhook_replay",
886
+ "revclamp_lint_dispute_evidence",
887
+ ];
888
+ const selfTestFixtures = {
889
+ revclamp_audit_revenue_leakage: {
890
+ args: { demo: true },
891
+ verify: (r) => Boolean(r && r.ok === true && r.ghostEventsDetected > 0),
892
+ },
893
+ revclamp_evaluate_ghost_usage: {
894
+ args: {
895
+ customerId: "cus_selftest_01",
896
+ subscriptions: {
897
+ has_more: false,
898
+ data: [
899
+ {
900
+ customer: "cus_selftest_01",
901
+ status: "canceled",
902
+ current_period_end: 1750000000,
903
+ },
904
+ ],
905
+ },
906
+ usageEvent: {
907
+ customer_id: "cus_selftest_01",
908
+ occurred_at_sec: 1760000000,
909
+ },
910
+ },
911
+ verify: (r) =>
912
+ Boolean(
913
+ r &&
914
+ r.ok === true &&
915
+ r.isGhostUsage === true &&
916
+ r.decision === "DISPATCH_SIGNED_REVOKE_WEBHOOK"
917
+ ),
918
+ },
919
+ revclamp_stage_stripe_dispute_ce3: {
920
+ args: {
921
+ disputeId: "du_selftest_01",
922
+ customerId: "cus_selftest_01",
923
+ demo: true,
924
+ },
925
+ verify: (r) =>
926
+ Boolean(
927
+ r &&
928
+ r.ok === true &&
929
+ r.submit === false &&
930
+ typeof r.evidence?.access_activity_log === "string" &&
931
+ r.utf16CodeUnits <= 18000
932
+ ),
933
+ },
934
+ revclamp_validate_eu_vat_vies: {
935
+ args: {
936
+ sellerCountry: "DE",
937
+ buyerVatId: "XI123456789",
938
+ viesMode: "FORMAT_ONLY",
939
+ },
940
+ verify: (r) =>
941
+ Boolean(
942
+ r &&
943
+ r.ok === true &&
944
+ r.formatValid === true &&
945
+ r.crossBorderSupply === true &&
946
+ r.memberState === "XI"
947
+ ),
948
+ },
949
+ revclamp_reconcile_b2b_net_terms: {
950
+ args: {
951
+ orderId: "ord_selftest_01",
952
+ totalOrderCents: 100000,
953
+ paidAmountCents: 30000,
954
+ requiredDepositPercent: 30,
955
+ paymentTermsDays: 30,
956
+ daysElapsedSinceInvoice: 5,
957
+ },
958
+ verify: (r) =>
959
+ Boolean(
960
+ r &&
961
+ r.ok === true &&
962
+ r.fulfillmentAllowed === true &&
963
+ r.doubleEntryBalanced === true &&
964
+ r.remainingBalanceCents === 70000
965
+ ),
966
+ },
967
+ revclamp_verify_webhook_signature: {
968
+ args: {
969
+ provider: "stripe",
970
+ secret: "whsec_test_selftest_secret_2026",
971
+ rawPayload: '{"id":"evt_selftest_01","type":"customer.subscription.deleted"}',
972
+ demo: true,
973
+ },
974
+ verify: (r) =>
975
+ Boolean(
976
+ r &&
977
+ r.ok === true &&
978
+ typeof r.verification?.generatedHeader === "string" &&
979
+ r.verification.generatedHeader.includes("v1=")
980
+ ),
981
+ },
982
+ revclamp_calculate_roi_and_plan_fit: {
983
+ args: {
984
+ monthlyWonDisputeUsd: 10000,
985
+ monthlyGhostBleedUsd: 600,
986
+ contingencyTakeRatePercent: 25,
987
+ monthlyUsageEvents: 250000,
988
+ },
989
+ verify: (r) =>
990
+ Boolean(
991
+ r &&
992
+ r.ok === true &&
993
+ r.zeroTakeRateGuaranteed === true &&
994
+ r.annualNetMarginSavedCents > 0 &&
995
+ r.recommendedTier === "growth_59"
996
+ ),
997
+ },
998
+ revclamp_simulate_webhook_replay: {
999
+ args: {
1000
+ scenarioPreset: "out_of_order_stale_update_after_cancel",
1001
+ demo: true,
1002
+ },
1003
+ verify: (r) =>
1004
+ Boolean(
1005
+ r &&
1006
+ r.ok === true &&
1007
+ r.preventedStateCorruption === true &&
1008
+ r.finalStatus === "canceled"
1009
+ ),
1010
+ },
1011
+ revclamp_lint_dispute_evidence: {
1012
+ args: {
1013
+ preset: "valid_ce3_non_destructive",
1014
+ demo: true,
1015
+ },
1016
+ verify: (r) =>
1017
+ Boolean(
1018
+ r &&
1019
+ r.ok === true &&
1020
+ r.within18kCeiling === true &&
1021
+ r.withinCeiling === true &&
1022
+ r.overallVerdict === "PASS_CE3_READY"
1023
+ ),
1024
+ },
1025
+ };
1026
+
1027
+ const toolsList = Array.isArray(LOCAL_MCP_TOOLS) ? LOCAL_MCP_TOOLS : [];
1028
+ const toolsCount = toolsList.length;
1029
+ const exactCatalogMatch =
1030
+ toolsCount === expectedToolNames.length &&
1031
+ expectedToolNames.every((tName, idx) => toolsList[idx]?.name === tName);
1032
+
1033
+ const toolResults = [];
1034
+ let passedCount = 0;
1035
+ for (const toolDef of toolsList) {
1036
+ const tName = String(toolDef?.name || "");
1037
+ const spec = selfTestFixtures[tName];
1038
+ let itemOk = false;
1039
+ if (spec) {
1040
+ try {
1041
+ const out = executeLocalMcpTool(tName, spec.args);
1042
+ itemOk = Boolean(spec.verify(out));
1043
+ } catch {
1044
+ itemOk = false;
1045
+ }
1046
+ }
1047
+ if (itemOk) passedCount += 1;
1048
+ toolResults.push({ tool: tName, ok: itemOk });
1049
+ }
1050
+
879
1051
  const localEngineVerified =
880
- Boolean(smokeAudit && smokeAudit.ok === true && smokeAudit.ghostEventsDetected > 0) &&
881
- toolsCount === 9;
1052
+ Boolean(exactCatalogMatch) && toolsCount === 9 && passedCount === 9;
882
1053
  console.log(
883
1054
  JSON.stringify({
884
1055
  ok: localEngineVerified,
885
1056
  version: PACKAGE_VERSION,
886
1057
  toolsCount,
1058
+ passedCount,
887
1059
  localEngineVerified,
888
1060
  endpoint: validated,
1061
+ toolResults,
889
1062
  })
890
1063
  );
891
1064
  return localEngineVerified ? 0 : 1;
@@ -1073,6 +1246,9 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
1073
1246
  "valid_ce3_non_destructive",
1074
1247
  "utf16_overflow_19k",
1075
1248
  "missing_device_and_overwrite_risk",
1249
+ "ce3_24k_truncation_trap",
1250
+ "ce3_clean_compacted_14k",
1251
+ "ce3_missing_device_binding_19k",
1076
1252
  ]);
1077
1253
  const preset = positional[0] || "valid_ce3_non_destructive";
1078
1254
  if (!allowedPresets.has(preset)) {
package/local_engine.mjs CHANGED
@@ -17,6 +17,7 @@
17
17
  */
18
18
 
19
19
  import crypto from "node:crypto";
20
+ import { isDeepStrictEqual } from "node:util";
20
21
 
21
22
  export const MCP_PROTOCOL_VERSION = "2024-11-05";
22
23
  export const SUPPORTED_MCP_PROTOCOL_VERSIONS = Object.freeze([
@@ -30,6 +31,7 @@ const LOCAL_LIVE_SECRET_GUARD_RE =
30
31
 
31
32
  const LOCAL_MCP_TOOL_ARG_ALIAS_GROUPS = Object.freeze({
32
33
  revclamp_verify_webhook_signature: Object.freeze([
34
+ Object.freeze({ canonical: "provider", aliases: Object.freeze(["webhookProvider"]) }),
33
35
  Object.freeze({ canonical: "rawPayload", aliases: Object.freeze(["payload", "body"]) }),
34
36
  Object.freeze({
35
37
  canonical: "signatureHeader",
@@ -47,6 +49,10 @@ const LOCAL_MCP_TOOL_ARG_ALIAS_GROUPS = Object.freeze({
47
49
  ]),
48
50
  revclamp_simulate_webhook_replay: Object.freeze([
49
51
  Object.freeze({ canonical: "scenarioPreset", aliases: Object.freeze(["preset"]) }),
52
+ Object.freeze({
53
+ canonical: "events",
54
+ aliases: Object.freeze(["webhookEvents", "eventSequence"]),
55
+ }),
50
56
  Object.freeze({
51
57
  canonical: "idempotencyWindowSec",
52
58
  aliases: Object.freeze(["windowSec", "dedupWindowSec"]),
@@ -79,7 +85,8 @@ export function normalizeMcpToolArgs(toolName, rawArgs) {
79
85
  const firstVal = rawArgs[firstKey];
80
86
  for (let i = 1; i < presentKeys.length; i += 1) {
81
87
  const otherKey = presentKeys[i];
82
- if (!Object.is(firstVal, rawArgs[otherKey])) {
88
+ const otherVal = rawArgs[otherKey];
89
+ if (!Object.is(firstVal, otherVal) && !isDeepStrictEqual(firstVal, otherVal)) {
83
90
  return {
84
91
  ok: false,
85
92
  error: "CONFLICTING_PARAMETER_ALIASES",
@@ -205,6 +212,10 @@ const EU_VIES_FORMAT_RULES = Object.freeze({
205
212
 
206
213
  const REVCLAMP_START_MARKER = "--- BEGIN REVCLAMP CE3 EVIDENCE ---";
207
214
  const REVCLAMP_END_MARKER = "--- END REVCLAMP CE3 EVIDENCE ---";
215
+ const REVCLAMP_BRACKET_START_MARKER = "[RevClamp Verified Product Usage Log]";
216
+ const REVCLAMP_BRACKET_END_MARKER = "[/RevClamp Verified Product Usage Log]";
217
+ const PRODUCTION_EMPTY_BLOCK_WRAPPER_CHARS =
218
+ REVCLAMP_BRACKET_START_MARKER.length + 1 + REVCLAMP_BRACKET_END_MARKER.length;
208
219
  const MAX_ACCESS_ACTIVITY_LOG_CHARS = 18_000;
209
220
  const CE3_MIN_WINDOW_SEC = 120 * 86_400;
210
221
  const CE3_MAX_WINDOW_SEC = 365 * 86_400;
@@ -400,6 +411,11 @@ export const LOCAL_MCP_TOOLS = Object.freeze([
400
411
  properties: {
401
412
  provider: { type: "string", enum: ["stripe", "shopify", "revclamp"] },
402
413
  secret: { type: "string" },
414
+ secrets: {
415
+ type: "array",
416
+ maxItems: 5,
417
+ items: { type: "string" },
418
+ },
403
419
  rawPayload: { type: "string" },
404
420
  signatureHeader: { type: "string" },
405
421
  timestampSec: { type: "integer", minimum: 1, maximum: 9999999999 },
@@ -482,7 +498,7 @@ export const LOCAL_MCP_TOOLS = Object.freeze([
482
498
  {
483
499
  name: "revclamp_lint_dispute_evidence",
484
500
  description:
485
- "Lints Stripe dispute evidence[access_activity_log] payloads against the 18,000 UTF-16 code unit ceiling, non-destructive RevClamp CE 3.0 block markers, merchant note preservation, and Visa CE 3.0 two-identifier overlap.",
501
+ "Lints Stripe dispute evidence[access_activity_log] payloads against the 18,000 UTF-16 code unit ceiling, non-destructive RevClamp CE 3.0 block markers, merchant note preservation, 17,922/17,920-char fixed wrapper capacity threshold (SKIP_POST_PRESERVE_DRAFT), and Visa CE 3.0 two-identifier overlap.",
486
502
  inputSchema: {
487
503
  type: "object",
488
504
  additionalProperties: false,
@@ -493,15 +509,64 @@ export const LOCAL_MCP_TOOLS = Object.freeze([
493
509
  "valid_ce3_non_destructive",
494
510
  "utf16_overflow_19k",
495
511
  "missing_device_and_overwrite_risk",
512
+ "custom",
513
+ "ce3_24k_truncation_trap",
514
+ "ce3_clean_compacted_14k",
515
+ "ce3_missing_device_binding_19k",
496
516
  ],
497
517
  default: "valid_ce3_non_destructive",
518
+ description:
519
+ "Scenario preset to evaluate when custom evidenceLogText / accessActivityLog is omitted (default: 'valid_ce3_non_destructive').",
520
+ },
521
+ existingMerchantNotes: {
522
+ type: "string",
523
+ maxLength: 30000,
524
+ description:
525
+ "Existing human AE/support notes inside evidence[access_activity_log] that must be preserved.",
526
+ },
527
+ evidenceLogText: {
528
+ type: "string",
529
+ maxLength: 30000,
530
+ description:
531
+ "Merged or candidate Stripe dispute evidence[access_activity_log] payload evaluated against the 18,000 UTF-16 code unit ceiling and non-destructive delimiter markers.",
532
+ },
533
+ accessActivityLog: {
534
+ type: "string",
535
+ maxLength: 30000,
536
+ description:
537
+ "Alias for evidenceLogText representing Stripe dispute evidence[access_activity_log] payload.",
538
+ },
539
+ priorMatchedCount: {
540
+ type: "integer",
541
+ minimum: 0,
542
+ maximum: 100,
543
+ description:
544
+ "Number of prior undisputed Stripe charges (120d-365d old) with matching customer_id and IP/device fingerprint (Visa CE 3.0 requires >= 2).",
545
+ },
546
+ hasIpMatch: {
547
+ type: "boolean",
548
+ description:
549
+ "Whether the disputed charge IPv4/IPv6 address matches the historical undisputed charges.",
550
+ },
551
+ hasDeviceMatch: {
552
+ type: "boolean",
553
+ description:
554
+ "Whether the disputed charge device_id fingerprint matches the historical undisputed charges.",
555
+ },
556
+ disputedTransaction: {
557
+ type: "object",
558
+ description:
559
+ "Optional disputed Stripe charge/transaction metadata evaluated against priorTransactions for Visa CE 3.0 qualification.",
560
+ },
561
+ priorTransactions: {
562
+ type: "array",
563
+ description:
564
+ "Optional historical undisputed Stripe charges (120d-365d prior) evaluated for Visa CE 3.0 two-identifier overlap.",
498
565
  },
499
- accessActivityLog: { type: "string" },
500
- disputedTransaction: { type: "object" },
501
- priorTransactions: { type: "array" },
502
566
  demo: {
503
567
  type: "boolean",
504
- description: "Set true to lint a sample preset fixture when accessActivityLog is omitted.",
568
+ description:
569
+ "Set true to lint a sample preset fixture when evidenceLogText / accessActivityLog is omitted.",
505
570
  default: false,
506
571
  },
507
572
  },
@@ -557,6 +622,12 @@ export function validateArgsAgainstSchema(schema, args) {
557
622
  if (typeof val !== "string") {
558
623
  return { ok: false, message: `Argument '${key}' must be a string.` };
559
624
  }
625
+ if (propSchema.maxLength !== undefined && val.length > propSchema.maxLength) {
626
+ return {
627
+ ok: false,
628
+ message: `Argument '${key}' exceeds maximum length of ${propSchema.maxLength}.`,
629
+ };
630
+ }
560
631
  } else if (propSchema.type === "boolean") {
561
632
  if (typeof val !== "boolean") {
562
633
  return { ok: false, message: `Argument '${key}' must be a boolean.` };
@@ -590,7 +661,7 @@ function normalizeCountryCode(raw) {
590
661
  function normalizeVatId(rawVat) {
591
662
  const compact = String(rawVat || "")
592
663
  .toUpperCase()
593
- .replace(/[\s.-]+/g, "");
664
+ .replace(/[\s.\-/:]+/g, "");
594
665
  if (compact.length < 4) {
595
666
  return { validFormat: false, memberState: "", vatNumber: "", fullVatId: compact };
596
667
  }
@@ -622,40 +693,66 @@ function sanitizeLogToken(raw, maxLen = 96) {
622
693
 
623
694
  function stripExistingRevClampBlock(rawNotes) {
624
695
  if (typeof rawNotes !== "string" || !rawNotes) {
625
- return { ok: true, hadBlock: false, text: "" };
696
+ return { ok: true, hadBlock: false, prefix: "", suffix: "", text: "" };
697
+ }
698
+ const hasCanonical =
699
+ rawNotes.includes(REVCLAMP_START_MARKER) || rawNotes.includes(REVCLAMP_END_MARKER);
700
+ const hasBracket =
701
+ rawNotes.includes(REVCLAMP_BRACKET_START_MARKER) ||
702
+ rawNotes.includes(REVCLAMP_BRACKET_END_MARKER);
703
+ if (!hasCanonical && !hasBracket) {
704
+ return { ok: true, hadBlock: false, prefix: rawNotes, suffix: "", text: rawNotes.trim() };
626
705
  }
627
- if (!rawNotes.includes(REVCLAMP_START_MARKER) && !rawNotes.includes(REVCLAMP_END_MARKER)) {
628
- return { ok: true, hadBlock: false, text: rawNotes.trim() };
706
+ if (hasCanonical && hasBracket) {
707
+ return { ok: false, hadBlock: true, prefix: rawNotes, suffix: "", text: rawNotes.trim() };
629
708
  }
709
+ const activeStart = hasCanonical ? REVCLAMP_START_MARKER : REVCLAMP_BRACKET_START_MARKER;
710
+ const activeEnd = hasCanonical ? REVCLAMP_END_MARKER : REVCLAMP_BRACKET_END_MARKER;
630
711
  let cursor = 0;
631
- const keptSegments = [];
712
+ const prefixSegments = [];
713
+ let trailingSegment = "";
632
714
  while (cursor < rawNotes.length) {
633
- const startIdx = rawNotes.indexOf(REVCLAMP_START_MARKER, cursor);
715
+ const startIdx = rawNotes.indexOf(activeStart, cursor);
634
716
  if (startIdx === -1) {
635
- if (rawNotes.indexOf(REVCLAMP_END_MARKER, cursor) !== -1) {
636
- return { ok: false, hadBlock: true, text: rawNotes.trim() };
717
+ if (rawNotes.indexOf(activeEnd, cursor) !== -1) {
718
+ return { ok: false, hadBlock: true, prefix: rawNotes, suffix: "", text: rawNotes.trim() };
637
719
  }
638
- const tail = rawNotes.slice(cursor).trim();
639
- if (tail) keptSegments.push(tail);
720
+ trailingSegment = rawNotes.slice(cursor);
640
721
  break;
641
722
  }
642
- if (rawNotes.slice(cursor, startIdx).includes(REVCLAMP_END_MARKER)) {
643
- return { ok: false, hadBlock: true, text: rawNotes.trim() };
723
+ if (rawNotes.slice(cursor, startIdx).includes(activeEnd)) {
724
+ return { ok: false, hadBlock: true, prefix: rawNotes, suffix: "", text: rawNotes.trim() };
644
725
  }
645
- const contentStart = startIdx + REVCLAMP_START_MARKER.length;
646
- const endIdx = rawNotes.indexOf(REVCLAMP_END_MARKER, contentStart);
726
+ const contentStart = startIdx + activeStart.length;
727
+ const endIdx = rawNotes.indexOf(activeEnd, contentStart);
647
728
  if (endIdx === -1) {
648
- return { ok: false, hadBlock: true, text: rawNotes.trim() };
729
+ return { ok: false, hadBlock: true, prefix: rawNotes, suffix: "", text: rawNotes.trim() };
649
730
  }
650
- const nextStartIdx = rawNotes.indexOf(REVCLAMP_START_MARKER, contentStart);
731
+ const nextStartIdx = rawNotes.indexOf(activeStart, contentStart);
651
732
  if (nextStartIdx !== -1 && nextStartIdx < endIdx) {
652
- return { ok: false, hadBlock: true, text: rawNotes.trim() };
733
+ return { ok: false, hadBlock: true, prefix: rawNotes, suffix: "", text: rawNotes.trim() };
734
+ }
735
+ const beforeSegment = rawNotes.slice(cursor, startIdx).replace(/\n{1,2}$/, "");
736
+ if (beforeSegment.length > 0) {
737
+ prefixSegments.push(beforeSegment);
738
+ }
739
+ cursor = endIdx + activeEnd.length;
740
+ if (rawNotes.slice(cursor, cursor + 2) === "\n\n") {
741
+ cursor += 2;
742
+ } else if (rawNotes[cursor] === "\n") {
743
+ cursor += 1;
653
744
  }
654
- const before = rawNotes.slice(cursor, startIdx).trim();
655
- if (before) keptSegments.push(before);
656
- cursor = endIdx + REVCLAMP_END_MARKER.length;
657
745
  }
658
- return { ok: true, hadBlock: true, text: keptSegments.join("\n\n").trim() };
746
+ const prefix = prefixSegments.join("\n");
747
+ const suffix = trailingSegment;
748
+ const keptTrimmed = [prefix.trim(), suffix.trim()].filter(Boolean);
749
+ return {
750
+ ok: true,
751
+ hadBlock: true,
752
+ prefix,
753
+ suffix,
754
+ text: keptTrimmed.join("\n\n").trim(),
755
+ };
659
756
  }
660
757
 
661
758
  function isValidUnixSec(val) {
@@ -1071,7 +1168,13 @@ function extractNormalizedCe3Identifiers(tx) {
1071
1168
  tx.metadata?.email ||
1072
1169
  "";
1073
1170
  const email = sanitizeLogToken(String(rawEmail || "").toLowerCase(), 96);
1074
- const rawIp = tx.ip_address || tx.ip || tx.client_ip || tx.metadata?.ip_address || "";
1171
+ const rawIp =
1172
+ tx.ip_address ||
1173
+ tx.ip ||
1174
+ tx.client_ip ||
1175
+ tx.metadata?.ip_address ||
1176
+ (typeof tx.ip_subnet === "string" ? tx.ip_subnet.replace(/\/\d{1,3}$/, "") : "") ||
1177
+ "";
1075
1178
  const ipSubnet = normalizeIpForCe3Correlation(typeof rawIp === "string" ? rawIp : "");
1076
1179
  const rawDevice =
1077
1180
  tx.device_id ||
@@ -1088,6 +1191,27 @@ function extractNormalizedCe3Identifiers(tx) {
1088
1191
  .filter(Boolean)
1089
1192
  .join("|")
1090
1193
  .toLowerCase();
1194
+ } else {
1195
+ const addr =
1196
+ tx.shipping?.address && typeof tx.shipping.address === "object"
1197
+ ? tx.shipping.address
1198
+ : tx.shipping_address && typeof tx.shipping_address === "object"
1199
+ ? tx.shipping_address
1200
+ : null;
1201
+ if (addr) {
1202
+ shippingAddr = [
1203
+ addr.line1,
1204
+ addr.line2,
1205
+ addr.city,
1206
+ addr.state,
1207
+ addr.postal_code,
1208
+ addr.country,
1209
+ ]
1210
+ .map((v) => sanitizeLogToken(String(v || "").trim(), 96))
1211
+ .filter(Boolean)
1212
+ .join("|")
1213
+ .toLowerCase();
1214
+ }
1091
1215
  }
1092
1216
  return {
1093
1217
  user_id: userId || null,
@@ -1183,11 +1307,16 @@ function evaluateLocalVisaCe3Qualification(disputeReason, disputedTransaction, p
1183
1307
  }
1184
1308
 
1185
1309
  const windowQualified = allWindowQualified
1186
- .sort((a, b) =>
1187
- b.occurredAtSec !== a.occurredAtSec
1310
+ .sort((a, b) => {
1311
+ const aHasNetOrDev =
1312
+ a.matchedFields.includes("ip_subnet") || a.matchedFields.includes("device_id") ? 1 : 0;
1313
+ const bHasNetOrDev =
1314
+ b.matchedFields.includes("ip_subnet") || b.matchedFields.includes("device_id") ? 1 : 0;
1315
+ if (bHasNetOrDev !== aHasNetOrDev) return bHasNetOrDev - aHasNetOrDev;
1316
+ return b.occurredAtSec !== a.occurredAtSec
1188
1317
  ? b.occurredAtSec - a.occurredAtSec
1189
- : a.id.localeCompare(b.id)
1190
- )
1318
+ : a.id.localeCompare(b.id);
1319
+ })
1191
1320
  .slice(0, 100);
1192
1321
 
1193
1322
  let bestGroup = null;
@@ -1219,8 +1348,10 @@ function evaluateLocalVisaCe3Qualification(disputeReason, disputedTransaction, p
1219
1348
  const samePaymentMethodVerified = Boolean(
1220
1349
  qualified && bestGroup.every((p) => p.samePaymentMethodVerified)
1221
1350
  );
1351
+ const matchedIds = qualified ? bestGroup.map((q) => q.id) : [];
1222
1352
  return {
1223
1353
  qualified,
1354
+ eligible: qualified,
1224
1355
  samePaymentMethodVerified,
1225
1356
  status: !qualified
1226
1357
  ? "INSUFFICIENT_HISTORICAL_OVERLAP"
@@ -1232,7 +1363,8 @@ function evaluateLocalVisaCe3Qualification(disputeReason, disputedTransaction, p
1232
1363
  : sawPaymentCredentialMismatch
1233
1364
  ? "PAYMENT_CREDENTIAL_MISMATCH_OR_MISSING_ON_PRIOR_CHARGES"
1234
1365
  : "REQUIRES_2_UNDISPUTED_CHARGES_120D_TO_365D_WITH_CONSISTENT_TWO_FIELD_OVERLAP",
1235
- matchedTransactionIds: qualified ? bestGroup.map((q) => q.id) : [],
1366
+ matchedTransactionIds: matchedIds,
1367
+ matchedPriorTransactionIds: matchedIds,
1236
1368
  sharedIdentifiers: qualified ? bestSharedFields : [],
1237
1369
  ...(qualified && !samePaymentMethodVerified
1238
1370
  ? { limitations: ["SAME_PAYMENT_METHOD_NOT_PROVIDED_IN_LOCAL_PAYLOAD"] }
@@ -1645,77 +1777,312 @@ export function executeLocalMcpTool(toolName, rawArgs = {}) {
1645
1777
  }
1646
1778
 
1647
1779
  if (toolName === "revclamp_lint_dispute_evidence") {
1648
- const hasCallerLog =
1780
+ const lintToolSpec = LOCAL_MCP_TOOLS.find((t) => t.name === "revclamp_lint_dispute_evidence");
1781
+ if (lintToolSpec) {
1782
+ const schemaCheck = validateArgsAgainstSchema(lintToolSpec.inputSchema, args);
1783
+ if (!schemaCheck.ok) {
1784
+ const err = new Error(`Invalid params: ${schemaCheck.message}`);
1785
+ err.code = -32602;
1786
+ err.reason = "INVALID_DISPUTE_EVIDENCE_LINT_PARAMETERS";
1787
+ throw err;
1788
+ }
1789
+ }
1790
+ const hasEvidenceLogText =
1791
+ typeof args.evidenceLogText === "string" && args.evidenceLogText.trim().length > 0;
1792
+ const hasAccessActivityLog =
1649
1793
  typeof args.accessActivityLog === "string" && args.accessActivityLog.trim().length > 0;
1650
- const hasExplicitPreset = typeof args.preset === "string" && args.preset.trim().length > 0;
1651
- const isDemo = args.demo === true || (!hasCallerLog && hasExplicitPreset);
1794
+ if (
1795
+ hasEvidenceLogText &&
1796
+ hasAccessActivityLog &&
1797
+ args.evidenceLogText.trim() !== args.accessActivityLog.trim()
1798
+ ) {
1799
+ const err = new Error(
1800
+ "CONFLICTING_EVIDENCE_LOG_ALIASES: evidenceLogText and accessActivityLog must match when both are provided"
1801
+ );
1802
+ err.code = -32602;
1803
+ err.reason = "CONFLICTING_EVIDENCE_LOG_ALIASES";
1804
+ throw err;
1805
+ }
1806
+
1807
+ const hasExplicitLogString =
1808
+ typeof args.evidenceLogText === "string" || typeof args.accessActivityLog === "string";
1809
+ const resolvedLogInput = hasEvidenceLogText
1810
+ ? args.evidenceLogText
1811
+ : hasAccessActivityLog
1812
+ ? args.accessActivityLog
1813
+ : typeof args.evidenceLogText === "string"
1814
+ ? args.evidenceLogText
1815
+ : typeof args.accessActivityLog === "string"
1816
+ ? args.accessActivityLog
1817
+ : undefined;
1818
+ const rawCallerLog =
1819
+ typeof resolvedLogInput === "string"
1820
+ ? resolvedLogInput
1821
+ : typeof args.existingMerchantNotes === "string" &&
1822
+ args.existingMerchantNotes.trim().length > 0
1823
+ ? args.existingMerchantNotes
1824
+ : "";
1825
+ const hasCallerLog = rawCallerLog.length > 0 || hasExplicitLogString;
1826
+ const hasExplicitPreset =
1827
+ typeof args.preset === "string" &&
1828
+ args.preset.trim().length > 0 &&
1829
+ args.preset.trim() !== "custom";
1830
+ const hasSummaryArgs =
1831
+ args.priorMatchedCount !== undefined ||
1832
+ args.hasIpMatch !== undefined ||
1833
+ args.hasDeviceMatch !== undefined;
1834
+ const hasTransactionArgs =
1835
+ (args.disputedTransaction !== undefined && args.disputedTransaction !== null) ||
1836
+ (args.priorTransactions !== undefined && args.priorTransactions !== null);
1837
+ const hasCustomInputFields =
1838
+ args.evidenceLogText !== undefined ||
1839
+ args.accessActivityLog !== undefined ||
1840
+ args.existingMerchantNotes !== undefined ||
1841
+ args.priorMatchedCount !== undefined ||
1842
+ args.hasIpMatch !== undefined ||
1843
+ args.hasDeviceMatch !== undefined ||
1844
+ args.disputedTransaction !== undefined ||
1845
+ args.priorTransactions !== undefined;
1846
+ const preset =
1847
+ typeof args.preset === "string" && args.preset.trim()
1848
+ ? args.preset.trim()
1849
+ : hasCustomInputFields
1850
+ ? "custom"
1851
+ : "valid_ce3_non_destructive";
1852
+ const isDemo =
1853
+ args.demo === true ||
1854
+ (!hasCallerLog &&
1855
+ (preset !== "custom" || hasExplicitPreset || hasSummaryArgs || hasTransactionArgs));
1652
1856
  if (!hasCallerLog && !isDemo) {
1653
1857
  const err = new Error(
1654
- "MISSING_ACCESS_ACTIVITY_LOG: Provide accessActivityLog to lint a real dispute payload, or specify preset / demo: true to lint a sample fixture."
1858
+ "MISSING_ACCESS_ACTIVITY_LOG: Provide accessActivityLog / evidenceLogText to lint a real dispute payload, or specify preset / demo: true to lint a sample fixture."
1655
1859
  );
1656
1860
  err.code = -32602;
1861
+ err.reason = "MISSING_ACCESS_ACTIVITY_LOG";
1657
1862
  throw err;
1658
1863
  }
1659
- const preset = String(args.preset || "valid_ce3_non_destructive");
1660
- let logText = hasCallerLog ? args.accessActivityLog : "";
1661
-
1662
- if (!logText) {
1663
- if (preset === "utf16_overflow_19k") {
1664
- logText =
1665
- "MERCHANT_NOTE\n--- BEGIN REVCLAMP CE3 EVIDENCE ---\n" +
1666
- "A".repeat(19_000) +
1667
- "\n--- END REVCLAMP CE3 EVIDENCE ---";
1668
- } else if (preset === "missing_device_and_overwrite_risk") {
1669
- logText = "2026-10-01T10:00:00Z | action=api.call | user=cus_123";
1670
- } else {
1671
- logText =
1672
- "MERCHANT_SUPPORT_NOTE: Confirmed seat provisioning.\n--- BEGIN REVCLAMP CE3 EVIDENCE ---\n2026-04-10T12:00:00Z | event=api.inference | actor=cus_123 | ip=203.0.113.42 | device=dev_fp_99a | model=gpt-4o | tokens=2048\n--- END REVCLAMP CE3 EVIDENCE ---";
1864
+
1865
+ const ce3Qualification = hasTransactionArgs
1866
+ ? evaluateLocalVisaCe3Qualification(
1867
+ "fraudulent",
1868
+ args.disputedTransaction,
1869
+ args.priorTransactions
1870
+ )
1871
+ : null;
1872
+
1873
+ if (ce3Qualification && hasSummaryArgs) {
1874
+ const derived = {
1875
+ priorMatchedCount: (ce3Qualification.matchedTransactionIds || []).length,
1876
+ hasIpMatch: (ce3Qualification.sharedIdentifiers || []).includes("ip_subnet"),
1877
+ hasDeviceMatch: (ce3Qualification.sharedIdentifiers || []).includes("device_id"),
1878
+ };
1879
+ if (
1880
+ (args.priorMatchedCount !== undefined &&
1881
+ Number(args.priorMatchedCount) !== derived.priorMatchedCount) ||
1882
+ (args.hasIpMatch !== undefined && Boolean(args.hasIpMatch) !== derived.hasIpMatch) ||
1883
+ (args.hasDeviceMatch !== undefined &&
1884
+ Boolean(args.hasDeviceMatch) !== derived.hasDeviceMatch)
1885
+ ) {
1886
+ const err = new Error(
1887
+ "CONFLICTING_CE3_SUMMARY_AND_TRANSACTIONS: Explicit CE 3.0 summary parameters (priorMatchedCount, hasIpMatch, hasDeviceMatch) conflict with transaction-derived qualification."
1888
+ );
1889
+ err.code = -32602;
1890
+ err.reason = "CONFLICTING_CE3_SUMMARY_AND_TRANSACTIONS";
1891
+ throw err;
1673
1892
  }
1674
1893
  }
1675
1894
 
1676
- const hasNetworkOrDevice = /\b(?:ip|device)=/i.test(logText);
1677
- const hasDeviceAndIp = /\bip=/i.test(logText) && /\bdevice=/i.test(logText);
1678
- const hasMarkers =
1679
- logText.includes(REVCLAMP_START_MARKER) && logText.includes(REVCLAMP_END_MARKER);
1895
+ const ce3SummarySource = hasTransactionArgs
1896
+ ? "transactions"
1897
+ : hasSummaryArgs
1898
+ ? "explicit"
1899
+ : preset !== "custom"
1900
+ ? "preset"
1901
+ : "log_inferred";
1902
+ const normalizedPresetKey =
1903
+ preset === "ce3_24k_truncation_trap"
1904
+ ? "utf16_overflow_19k"
1905
+ : preset === "ce3_missing_device_binding_19k"
1906
+ ? "missing_device_and_overwrite_risk"
1907
+ : preset === "ce3_clean_compacted_14k"
1908
+ ? "valid_ce3_non_destructive"
1909
+ : preset;
1910
+ const localPresetFixtures = {
1911
+ valid_ce3_non_destructive: {
1912
+ existingMerchantNotes: "Customer signed annual MSA #2025-884; ticket #9102 resolved.",
1913
+ evidenceLogText: [
1914
+ "Customer signed annual MSA #2025-884; ticket #9102 resolved.",
1915
+ "--- BEGIN REVCLAMP CE3 EVIDENCE ---",
1916
+ "Prior Charge 1: py_1Ox99a (2026-03-11, IP 198.51.100.42, Device dev_88a)",
1917
+ "Prior Charge 2: py_1P211b (2026-05-19, IP 198.51.100.42, Device dev_88a)",
1918
+ "90-Day Verified Usage Events: 412 API calls authenticated via workspace key.",
1919
+ "--- END REVCLAMP CE3 EVIDENCE ---",
1920
+ ].join("\n"),
1921
+ },
1922
+ utf16_overflow_19k: {
1923
+ existingMerchantNotes: "Finance note: enterprise renewal disputed.",
1924
+ evidenceLogText: `Finance note: enterprise renewal disputed.\n--- BEGIN REVCLAMP CE3 EVIDENCE ---\n${"E".repeat(19000)}\n--- END REVCLAMP CE3 EVIDENCE ---`,
1925
+ },
1926
+ missing_device_and_overwrite_risk: {
1927
+ existingMerchantNotes: "VIP customer escalation note from account executive.",
1928
+ evidenceLogText: "Raw replacement log that overwrote AE notes without markers.",
1929
+ },
1930
+ };
1931
+ const basePreset =
1932
+ normalizedPresetKey !== "custom" ? localPresetFixtures[normalizedPresetKey] : null;
1933
+ const existingMerchantNotes =
1934
+ typeof args.existingMerchantNotes === "string"
1935
+ ? args.existingMerchantNotes
1936
+ : basePreset?.existingMerchantNotes ?? "";
1937
+ const logText =
1938
+ typeof resolvedLogInput === "string"
1939
+ ? resolvedLogInput
1940
+ : basePreset
1941
+ ? basePreset.evidenceLogText ?? ""
1942
+ : existingMerchantNotes ||
1943
+ localPresetFixtures.valid_ce3_non_destructive.evidenceLogText;
1944
+
1945
+ const inferredHasIp = /\bip(?:=|\s+\d)/i.test(logText);
1946
+ const inferredHasDevice = /\bdevice(?:=|\s+[a-z0-9_])/i.test(logText);
1947
+ const inferredPriorCount = inferredHasIp || inferredHasDevice ? 2 : 0;
1948
+ const isMissingDevicePreset = normalizedPresetKey === "missing_device_and_overwrite_risk";
1949
+ const defaultPriorMatchedCount = ce3Qualification
1950
+ ? (ce3Qualification.matchedTransactionIds || []).length
1951
+ : isMissingDevicePreset
1952
+ ? 1
1953
+ : normalizedPresetKey !== "custom"
1954
+ ? 2
1955
+ : inferredPriorCount;
1956
+ const defaultHasIpMatch = ce3Qualification
1957
+ ? (ce3Qualification.sharedIdentifiers || []).includes("ip_subnet")
1958
+ : isMissingDevicePreset
1959
+ ? false
1960
+ : normalizedPresetKey !== "custom"
1961
+ ? true
1962
+ : inferredHasIp;
1963
+ const defaultHasDeviceMatch = ce3Qualification
1964
+ ? (ce3Qualification.sharedIdentifiers || []).includes("device_id")
1965
+ : isMissingDevicePreset
1966
+ ? false
1967
+ : normalizedPresetKey !== "custom"
1968
+ ? true
1969
+ : inferredHasDevice;
1970
+
1971
+ const priorMatchedCount =
1972
+ args.priorMatchedCount !== undefined
1973
+ ? Number(args.priorMatchedCount)
1974
+ : defaultPriorMatchedCount;
1975
+ const hasIpMatch =
1976
+ args.hasIpMatch !== undefined ? Boolean(args.hasIpMatch) : defaultHasIpMatch;
1977
+ const hasDeviceMatch =
1978
+ args.hasDeviceMatch !== undefined ? Boolean(args.hasDeviceMatch) : defaultHasDeviceMatch;
1979
+ const ce3RubricPassed =
1980
+ priorMatchedCount >= 2 &&
1981
+ Boolean(hasIpMatch || hasDeviceMatch) &&
1982
+ (!ce3Qualification || ce3Qualification.qualified);
1983
+
1984
+ const hasNetworkOrDevice = Boolean(hasIpMatch || hasDeviceMatch);
1985
+ const hasDeviceAndIp = Boolean(hasIpMatch && hasDeviceMatch);
1680
1986
  const strippedNotes = stripExistingRevClampBlock(logText);
1681
- const preservesMerchantNotes =
1682
- strippedNotes.ok && hasMarkers && strippedNotes.text.length > 0;
1987
+ const nonDestructiveMarkersPresent = Boolean(strippedNotes.ok && strippedNotes.hadBlock);
1988
+ const malformedExistingMarkers = Boolean(!strippedNotes.ok);
1989
+ const warnings = malformedExistingMarkers ? ["MALFORMED_EXISTING_MARKERS"] : [];
1990
+ const trimmedExistingNotes = existingMerchantNotes.trim();
1991
+ const outsideMarkerText = `${strippedNotes.prefix || ""}\n${strippedNotes.suffix || ""}`;
1992
+ const preservesMerchantNotes = Boolean(
1993
+ nonDestructiveMarkersPresent &&
1994
+ strippedNotes.text.length > 0 &&
1995
+ (trimmedExistingNotes === "" || outsideMarkerText.includes(trimmedExistingNotes))
1996
+ );
1683
1997
 
1684
- const ce3Qualification =
1685
- args.disputedTransaction || args.priorTransactions
1686
- ? evaluateLocalVisaCe3Qualification(
1687
- "fraudulent",
1688
- args.disputedTransaction,
1689
- args.priorTransactions
1690
- )
1691
- : null;
1998
+ const extractedNotesLen =
1999
+ (strippedNotes.prefix?.length || 0) + (strippedNotes.suffix?.length || 0);
2000
+ const fallbackNotes = existingMerchantNotes;
2001
+ const capacityPrefix =
2002
+ extractedNotesLen > 0 ? strippedNotes.prefix || "" : fallbackNotes;
2003
+ const capacitySuffix = extractedNotesLen > 0 ? strippedNotes.suffix || "" : "";
2004
+ const preSeparatorUnits = capacityPrefix.length > 0 ? 2 : 0;
2005
+ const postSeparatorUnits = capacitySuffix.length > 0 ? 2 : 0;
2006
+ const merchantNotesUtf16CodeUnits = capacityPrefix.length + capacitySuffix.length;
2007
+ const fixedWrapperUtf16CodeUnits =
2008
+ capacityPrefix.length +
2009
+ preSeparatorUnits +
2010
+ PRODUCTION_EMPTY_BLOCK_WRAPPER_CHARS +
2011
+ postSeparatorUnits +
2012
+ capacitySuffix.length;
2013
+ const merchantLogAtCapacity = Boolean(
2014
+ fixedWrapperUtf16CodeUnits >= MAX_ACCESS_ACTIVITY_LOG_CHARS
2015
+ );
2016
+ const stagingActionWhenAtCapacity = merchantLogAtCapacity
2017
+ ? "SKIP_POST_PRESERVE_DRAFT"
2018
+ : null;
2019
+ if (merchantLogAtCapacity) {
2020
+ warnings.push("MERCHANT_LOG_AT_CAPACITY");
2021
+ }
1692
2022
 
1693
2023
  const utf16CodeUnits = logText.length;
2024
+ const remainingUtf16Units = Math.max(0, MAX_ACCESS_ACTIVITY_LOG_CHARS - utf16CodeUnits);
2025
+ const remainingCodeUnitsTo20k = Math.max(0, 20_000 - utf16CodeUnits);
2026
+ const utf8Bytes = Buffer.byteLength(logText, "utf8");
1694
2027
  const within18kCeiling = utf16CodeUnits <= MAX_ACCESS_ACTIVITY_LOG_CHARS;
2028
+ const within20kHardCeiling = utf16CodeUnits <= 20_000;
2029
+ const exceeds20kHardCeiling = utf16CodeUnits > 20_000;
1695
2030
  let overallVerdict = "PASS_CE3_READY";
1696
2031
  if (!within18kCeiling) {
1697
2032
  overallVerdict = "FAIL_UTF16_OVERFLOW";
1698
2033
  } else if (
1699
2034
  !hasNetworkOrDevice ||
2035
+ !ce3RubricPassed ||
1700
2036
  !preservesMerchantNotes ||
2037
+ malformedExistingMarkers ||
2038
+ merchantLogAtCapacity ||
1701
2039
  (ce3Qualification && !ce3Qualification.qualified)
1702
2040
  ) {
1703
2041
  overallVerdict = "WARN_INCOMPLETE_CE3_OR_OVERWRITE_RISK";
1704
2042
  }
2043
+ const verdictClass =
2044
+ overallVerdict === "PASS_CE3_READY"
2045
+ ? "PASS"
2046
+ : overallVerdict.startsWith("FAIL")
2047
+ ? "FAIL"
2048
+ : "WARN";
1705
2049
 
1706
2050
  return {
1707
2051
  ok: true,
1708
2052
  executionMode: "local_open_source_engine",
1709
2053
  ...(!hasCallerLog ? { sample: true } : {}),
1710
2054
  preset,
2055
+ ce3SummarySource,
1711
2056
  utf16CodeUnits,
1712
2057
  maxAllowedUtf16CodeUnits: MAX_ACCESS_ACTIVITY_LOG_CHARS,
2058
+ maxUtf16Ceiling: MAX_ACCESS_ACTIVITY_LOG_CHARS,
2059
+ remainingUtf16Units,
2060
+ remainingCodeUnitsTo18k: remainingUtf16Units,
2061
+ remainingCodeUnitsTo20k,
1713
2062
  within18kCeiling,
2063
+ withinCeiling: within18kCeiling,
2064
+ within18kSafeBudget: within18kCeiling,
2065
+ within20kHardCeiling,
2066
+ exceeds20kHardCeiling,
2067
+ utf8Bytes,
1714
2068
  hasNetworkOrDevice,
1715
2069
  hasDeviceAndIp,
2070
+ nonDestructiveMarkersPresent,
2071
+ malformedExistingMarkers,
2072
+ warnings,
1716
2073
  preservesMerchantNotes,
2074
+ merchantNotesPreserved: preservesMerchantNotes,
2075
+ merchantNotesUtf16CodeUnits,
2076
+ fixedWrapperUtf16CodeUnits,
2077
+ merchantLogAtCapacity,
2078
+ stagingActionWhenAtCapacity,
2079
+ priorMatchedCount,
2080
+ hasIpMatch,
2081
+ hasDeviceMatch,
2082
+ ce3RubricPassed,
1717
2083
  ...(ce3Qualification ? { ce3Qualification } : {}),
1718
2084
  overallVerdict,
2085
+ verdictClass,
1719
2086
  };
1720
2087
  }
1721
2088
 
@@ -1741,15 +2108,46 @@ export function executeLocalMcpTool(toolName, rawArgs = {}) {
1741
2108
  revclamp:
1742
2109
  '{"event":"revclamp.entitlement.revoke","tenantId":"tnt_sandbox_preview","accountId":"acct_sandbox_preview","customerId":"cus_basil_ai_team_01","leakReason":"UNPAID_GHOST_USAGE_CONFIRMED"}',
1743
2110
  };
1744
- const secret =
1745
- typeof args.secret === "string" && args.secret.trim().length > 0
1746
- ? args.secret.trim()
1747
- : "whsec_test_revclamp_sandbox_secret_2026";
2111
+ const rawSecretsList =
2112
+ Array.isArray(args.secrets) && args.secrets.length > 0
2113
+ ? args.secrets
2114
+ : typeof args.secret === "string" && args.secret.trim().length > 0
2115
+ ? args.secret.split(",")
2116
+ : ["whsec_test_revclamp_sandbox_secret_2026"];
2117
+ if (rawSecretsList.length > 5) {
2118
+ const err = new Error(
2119
+ "PAYLOAD_OR_SECRET_TOO_LONG: at most 5 rotation secrets may be supplied."
2120
+ );
2121
+ err.code = -32602;
2122
+ throw err;
2123
+ }
2124
+ const candidateSecrets = [];
2125
+ for (const item of rawSecretsList) {
2126
+ if (typeof item !== "string") {
2127
+ const err = new Error("INVALID_SECRET: each secret must be a string.");
2128
+ err.code = -32602;
2129
+ throw err;
2130
+ }
2131
+ const trimmed = item.trim();
2132
+ if (!trimmed) continue;
2133
+ if (trimmed.length > 256) {
2134
+ const err = new Error(
2135
+ "PAYLOAD_OR_SECRET_TOO_LONG: secret must be <= 256 chars and rawPayload must be <= 16384 chars."
2136
+ );
2137
+ err.code = -32602;
2138
+ throw err;
2139
+ }
2140
+ candidateSecrets.push(trimmed);
2141
+ }
2142
+ if (candidateSecrets.length === 0) {
2143
+ candidateSecrets.push("whsec_test_revclamp_sandbox_secret_2026");
2144
+ }
2145
+ const secret = candidateSecrets[0];
1748
2146
  const rawPayload =
1749
2147
  typeof args.rawPayload === "string" && args.rawPayload.length > 0
1750
2148
  ? args.rawPayload
1751
2149
  : defaultPayloadByProvider[provider];
1752
- if (secret.length > 256 || rawPayload.length > 16_384) {
2150
+ if (rawPayload.length > 16_384) {
1753
2151
  const err = new Error(
1754
2152
  "PAYLOAD_OR_SECRET_TOO_LONG: secret must be <= 256 chars and rawPayload must be <= 16384 chars."
1755
2153
  );
@@ -1808,13 +2206,28 @@ export function executeLocalMcpTool(toolName, rawArgs = {}) {
1808
2206
  mode: "generate_only",
1809
2207
  signatureValid: null,
1810
2208
  generatedHeader: expectedB64,
2209
+ matchedSecretIndex: null,
2210
+ secretsEvaluatedCount: candidateSecrets.length,
1811
2211
  },
1812
2212
  };
1813
2213
  }
1814
2214
  const provided = args.signatureHeader.trim();
1815
- const valid =
1816
- Buffer.byteLength(provided) === Buffer.byteLength(expectedB64) &&
1817
- crypto.timingSafeEqual(Buffer.from(provided), Buffer.from(expectedB64));
2215
+ let valid = false;
2216
+ let matchedSecretIndex = null;
2217
+ for (let sIdx = 0; sIdx < candidateSecrets.length; sIdx += 1) {
2218
+ const candB64 = crypto
2219
+ .createHmac("sha256", candidateSecrets[sIdx])
2220
+ .update(rawPayload, "utf8")
2221
+ .digest("base64");
2222
+ if (
2223
+ Buffer.byteLength(provided) === Buffer.byteLength(candB64) &&
2224
+ crypto.timingSafeEqual(Buffer.from(provided), Buffer.from(candB64))
2225
+ ) {
2226
+ valid = true;
2227
+ matchedSecretIndex = sIdx;
2228
+ break;
2229
+ }
2230
+ }
1818
2231
  return {
1819
2232
  ok: true,
1820
2233
  executionMode: "local_open_source_engine",
@@ -1823,6 +2236,8 @@ export function executeLocalMcpTool(toolName, rawArgs = {}) {
1823
2236
  mode: "verify",
1824
2237
  signatureValid: valid,
1825
2238
  generatedHeader: expectedB64,
2239
+ matchedSecretIndex,
2240
+ secretsEvaluatedCount: candidateSecrets.length,
1826
2241
  },
1827
2242
  };
1828
2243
  }
@@ -1844,6 +2259,8 @@ export function executeLocalMcpTool(toolName, rawArgs = {}) {
1844
2259
  generatedHeader,
1845
2260
  timestampSec: signingTimestampSec,
1846
2261
  toleranceSec,
2262
+ matchedSecretIndex: null,
2263
+ secretsEvaluatedCount: candidateSecrets.length,
1847
2264
  },
1848
2265
  };
1849
2266
  }
@@ -1876,17 +2293,25 @@ export function executeLocalMcpTool(toolName, rawArgs = {}) {
1876
2293
  const timestampSkewSeconds = Math.abs(wallNowSec - effectiveTs);
1877
2294
  const withinWindow = timestampSkewSeconds <= toleranceSec;
1878
2295
  let signatureValid = false;
2296
+ let matchedSecretIndex = null;
1879
2297
  if (parsedTs !== null && v1Candidates.length > 0 && withinWindow) {
1880
- const expectedForTs = crypto
1881
- .createHmac("sha256", secret)
1882
- .update(`${parsedTs}.${rawPayload}`, "utf8")
1883
- .digest("hex");
1884
- const expectedBuf = Buffer.from(expectedForTs, "utf8");
1885
- signatureValid = v1Candidates.some(
1886
- (cand) =>
1887
- cand.length === expectedForTs.length &&
1888
- crypto.timingSafeEqual(Buffer.from(cand, "utf8"), expectedBuf)
1889
- );
2298
+ for (let sIdx = 0; sIdx < candidateSecrets.length; sIdx += 1) {
2299
+ const expectedForTs = crypto
2300
+ .createHmac("sha256", candidateSecrets[sIdx])
2301
+ .update(`${parsedTs}.${rawPayload}`, "utf8")
2302
+ .digest("hex");
2303
+ const expectedBuf = Buffer.from(expectedForTs, "utf8");
2304
+ const matched = v1Candidates.some(
2305
+ (cand) =>
2306
+ cand.length === expectedForTs.length &&
2307
+ crypto.timingSafeEqual(Buffer.from(cand, "utf8"), expectedBuf)
2308
+ );
2309
+ if (matched) {
2310
+ signatureValid = true;
2311
+ matchedSecretIndex = sIdx;
2312
+ break;
2313
+ }
2314
+ }
1890
2315
  }
1891
2316
 
1892
2317
  return {
@@ -1901,6 +2326,8 @@ export function executeLocalMcpTool(toolName, rawArgs = {}) {
1901
2326
  timestampSkewSeconds,
1902
2327
  withinReplayWindow: withinWindow,
1903
2328
  toleranceSec,
2329
+ matchedSecretIndex,
2330
+ secretsEvaluatedCount: candidateSecrets.length,
1904
2331
  },
1905
2332
  };
1906
2333
  }
@@ -2723,7 +3150,7 @@ function toSafeLocalRpcId(rawId) {
2723
3150
  return null;
2724
3151
  }
2725
3152
 
2726
- export function executeLocalMcpJsonRpc(rpcRequest, packageVersion = "1.20.8") {
3153
+ export function executeLocalMcpJsonRpc(rpcRequest, packageVersion = "1.20.9") {
2727
3154
  if (!rpcRequest || typeof rpcRequest !== "object" || Array.isArray(rpcRequest)) {
2728
3155
  return {
2729
3156
  jsonrpc: "2.0",
@@ -3102,12 +3529,13 @@ export function executeLocalMcpJsonRpc(rpcRequest, packageVersion = "1.20.8") {
3102
3529
  error: {
3103
3530
  code: errCode,
3104
3531
  message: err instanceof Error ? err.message : "Tool execution failed",
3105
- ...(errCode === -32602 && exampleCallMap[toolName]
3532
+ ...(errCode === -32602 && (err?.reason || exampleCallMap[toolName])
3106
3533
  ? {
3107
3534
  data: {
3108
3535
  tool: toolName,
3536
+ error: err?.reason || "INVALID_TOOL_PARAMS",
3109
3537
  reason: err?.reason || "INVALID_TOOL_PARAMS",
3110
- exampleCall: exampleCallMap[toolName],
3538
+ ...(exampleCallMap[toolName] ? { exampleCall: exampleCallMap[toolName] } : {}),
3111
3539
  },
3112
3540
  }
3113
3541
  : {}),
package/mcp-config.json CHANGED
@@ -26,6 +26,17 @@
26
26
  },
27
27
  "manifest": {
28
28
  "name": "revclamp-mcp",
29
- "version": "1.20.8"
29
+ "version": "1.20.9",
30
+ "tools": [
31
+ "revclamp_audit_revenue_leakage",
32
+ "revclamp_evaluate_ghost_usage",
33
+ "revclamp_stage_stripe_dispute_ce3",
34
+ "revclamp_validate_eu_vat_vies",
35
+ "revclamp_reconcile_b2b_net_terms",
36
+ "revclamp_verify_webhook_signature",
37
+ "revclamp_calculate_roi_and_plan_fit",
38
+ "revclamp_simulate_webhook_replay",
39
+ "revclamp_lint_dispute_evidence"
40
+ ]
30
41
  }
31
42
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "revclamp-mcp",
3
3
  "mcpName": "io.github.revclamp/revclamp-mcp",
4
- "version": "1.20.8",
4
+ "version": "1.20.9",
5
5
  "description": "Model Context Protocol (MCP) server and CLI for Stripe 2025-03-31.basil entitlement checks, Visa CE 3.0 dispute staging (submit: false), EU/XI VIES VAT validation, Shopify Plus B2B Net-15/30/60 reconciliation, webhook HMAC-SHA256 verification, 0% take-rate ROI calculation, 72-hour webhook replay & out-of-order state-machine simulation, and Stripe dispute evidence 18,000 UTF-16 code unit & CE 3.0 completeness linting.",
6
6
  "type": "module",
7
7
  "main": "./index.mjs",
@@ -44,5 +44,8 @@
44
44
  "license": "MIT",
45
45
  "engines": {
46
46
  "node": ">=20.0.0"
47
+ },
48
+ "revclamp": {
49
+ "engineSha256": "775f5ff489a939d0f262ef972da38598e11fef0ce9e1371e6980e3946209fbc5"
47
50
  }
48
51
  }
package/server.json CHANGED
@@ -1,34 +1,34 @@
1
1
  {
2
- "$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json",
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.revclamp/revclamp-mcp",
4
- "description": "Zero-take-rate Stripe 2025-03-31.basil, Visa CE 3.0 & EU/UK VIES VAT guardrails MCP server.",
5
- "status": "active",
4
+ "title": "RevClamp MCP Server",
5
+ "description": "Zero-take-rate Stripe 2025-03-31.basil, Visa CE 3.0 & EU/XI VIES VAT guardrails MCP server.",
6
6
  "repository": {
7
7
  "url": "https://github.com/lhphong09t2/revclamp-mcp",
8
8
  "source": "github"
9
9
  },
10
- "version": "1.20.8",
10
+ "version": "1.20.9",
11
11
  "packages": [
12
12
  {
13
- "registry_type": "npm",
14
- "registry_base_url": "https://registry.npmjs.org",
13
+ "registryType": "npm",
14
+ "registryBaseUrl": "https://registry.npmjs.org",
15
15
  "identifier": "revclamp-mcp",
16
- "version": "1.20.8",
16
+ "version": "1.20.9",
17
17
  "transport": {
18
18
  "type": "stdio"
19
19
  },
20
- "environment_variables": [
20
+ "environmentVariables": [
21
21
  {
22
22
  "name": "REVCLAMP_API_KEY",
23
23
  "description": "Optional RevClamp workspace key (rg_live_...). Omit to run all 9 tools 100% locally in-process via local_engine.mjs.",
24
- "is_required": false,
25
- "is_secret": true
24
+ "isRequired": false,
25
+ "isSecret": true
26
26
  },
27
27
  {
28
28
  "name": "REVCLAMP_MCP_URL",
29
29
  "description": "Optional Streamable HTTP MCP endpoint override (defaults to https://api.revclamp.com/api/mcp).",
30
- "is_required": false,
31
- "is_secret": false,
30
+ "isRequired": false,
31
+ "isSecret": false,
32
32
  "default": "https://api.revclamp.com/api/mcp"
33
33
  }
34
34
  ]