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 +1 -1
- package/index.mjs +181 -5
- package/local_engine.mjs +514 -86
- package/mcp-config.json +12 -1
- package/package.json +4 -1
- package/server.json +12 -12
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.
|
|
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.
|
|
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
|
|
878
|
-
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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
|
|
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 (
|
|
628
|
-
return { ok:
|
|
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
|
|
712
|
+
const prefixSegments = [];
|
|
713
|
+
let trailingSegment = "";
|
|
632
714
|
while (cursor < rawNotes.length) {
|
|
633
|
-
const startIdx = rawNotes.indexOf(
|
|
715
|
+
const startIdx = rawNotes.indexOf(activeStart, cursor);
|
|
634
716
|
if (startIdx === -1) {
|
|
635
|
-
if (rawNotes.indexOf(
|
|
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
|
-
|
|
639
|
-
if (tail) keptSegments.push(tail);
|
|
720
|
+
trailingSegment = rawNotes.slice(cursor);
|
|
640
721
|
break;
|
|
641
722
|
}
|
|
642
|
-
if (rawNotes.slice(cursor, startIdx).includes(
|
|
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 +
|
|
646
|
-
const endIdx = rawNotes.indexOf(
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
1651
|
-
|
|
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
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
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
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
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
|
|
1682
|
-
|
|
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
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
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
|
|
1745
|
-
|
|
1746
|
-
? args.
|
|
1747
|
-
: "
|
|
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 (
|
|
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
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
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
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
(
|
|
1887
|
-
cand
|
|
1888
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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-
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.revclamp/revclamp-mcp",
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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.
|
|
10
|
+
"version": "1.20.9",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
|
-
"
|
|
14
|
-
"
|
|
13
|
+
"registryType": "npm",
|
|
14
|
+
"registryBaseUrl": "https://registry.npmjs.org",
|
|
15
15
|
"identifier": "revclamp-mcp",
|
|
16
|
-
"version": "1.20.
|
|
16
|
+
"version": "1.20.9",
|
|
17
17
|
"transport": {
|
|
18
18
|
"type": "stdio"
|
|
19
19
|
},
|
|
20
|
-
"
|
|
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
|
-
"
|
|
25
|
-
"
|
|
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
|
-
"
|
|
31
|
-
"
|
|
30
|
+
"isRequired": false,
|
|
31
|
+
"isSecret": false,
|
|
32
32
|
"default": "https://api.revclamp.com/api/mcp"
|
|
33
33
|
}
|
|
34
34
|
]
|