revclamp-mcp 1.20.9 → 1.20.11
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 +10 -4
- package/index.mjs +546 -4
- package/local_engine.mjs +368 -53
- package/mcp-config.json +1 -1
- package/package.json +3 -3
- package/server.json +3 -3
package/README.md
CHANGED
|
@@ -5,18 +5,18 @@
|
|
|
5
5
|
**`revclamp-mcp`** is a standalone, zero-dependency **Open-Source Model Context Protocol (MCP) Server & CLI** (`index.mjs` + `local_engine.mjs`) implementing **9 deterministic B2B FinOps, Stripe `2025-03-31.basil`, Visa CE 3.0, EU/XI VIES VAT, Shopify Plus B2B Net-15/30/60, and Webhook Security tools** in pure Node.js.
|
|
6
6
|
|
|
7
7
|
- **100% Local In-Process Execution (Default — Zero Network / Zero API Key Required):** All 9 MCP tools (`initialize`, `tools/list`, and `tools/call`) execute locally in-process inside [`local_engine.mjs`](./local_engine.mjs) using pure Node.js (`node:crypto` HMAC-SHA256 verification, integer-cent double-entry accounting, 72-hour webhook replay state machines, and `18,000` UTF-16 code unit CE 3.0 linting). Zero network calls are ever made unless `REVCLAMP_API_KEY` or `REVCLAMP_MCP_URL` is explicitly configured.
|
|
8
|
-
- **Optional Hosted Cloud Workspace Sync (`REVCLAMP_API_KEY`):** When `REVCLAMP_API_KEY` (`rg_live_...`) is configured, `revclamp-mcp` connects to [**RevClamp Enterprise**](https://revclamp.com) (`https://api.revclamp.com/api/mcp`) to unlock the **90-day
|
|
8
|
+
- **Optional Hosted Cloud Workspace Sync (`REVCLAMP_API_KEY`):** When `REVCLAMP_API_KEY` (`rg_live_...`) is configured, `revclamp-mcp` connects to [**RevClamp Enterprise**](https://revclamp.com) (`https://api.revclamp.com/api/mcp`) to unlock the **90-day durable transactional usage & audit ledger**, **24/7 autonomous Stripe dispute & entitlement webhook outbox**, and **real-time EU Commission VIES SOAP gateway consultation numbers**.
|
|
9
9
|
|
|
10
10
|
## Local Open-Core (`local_engine.mjs`) vs. Hosted Cloud (`REVCLAMP_API_KEY`)
|
|
11
11
|
|
|
12
12
|
| Capability | Local Open-Core Default (Zero API Key) | Hosted Cloud (`REVCLAMP_API_KEY`) |
|
|
13
13
|
| :--- | :--- | :--- |
|
|
14
14
|
| **Execution & Network** | 100% in-process (`local_engine.mjs`), zero network calls | HTTPS JSON-RPC to `https://api.revclamp.com/api/mcp` (`redirect: "error"`) |
|
|
15
|
-
| **Usage & Audit Storage** | Stateless evaluation of caller-supplied JSON snapshots | **Persistent 90-day
|
|
15
|
+
| **Usage & Audit Storage** | Stateless evaluation of caller-supplied JSON snapshots | **Persistent 90-day durable transactional ledger** + Stripe historical backfill |
|
|
16
16
|
| **Dispute & Entitlement Action** | Returns non-destructive `submit: false` draft & `revoke_entitlement` decision | **24/7 autonomous Stripe webhook outbox** (`disputes.update` & signed revoke webhooks) |
|
|
17
17
|
| **EU & `XI` B2B VAT (`VIES`)** | Deterministic syntax & `GR -> EL` normalization (`FORMAT_ONLY` default, `valid: null`) | **Live EU Commission VIES SOAP consultation IDs** + circuit-breaker retry queue (`viesMode: "LIVE"` + `REVCLAMP_API_KEY`) |
|
|
18
18
|
| **Monthly Event Tiers** | Unlimited local in-process evaluations (0 network cost) | **Free Sandbox** (`5,000` events/mo) · **Starter $19/mo** (`100k`) · **Growth $59/mo** (`1M`) · **Scale $199/mo** (`5M`) |
|
|
19
|
-
| **Data Privacy
|
|
19
|
+
| **Data Minimization & Privacy** | Nothing ever leaves your machine | Only explicit `tools/call` arguments are sent over TLS after local live-secret scrubbing (blocks `sk_live_`, non-test `whsec_`, and `shpat_` locally before any network request) |
|
|
20
20
|
|
|
21
21
|
## Open-Source MCP Tools Implemented in `local_engine.mjs` (9)
|
|
22
22
|
|
|
@@ -59,7 +59,13 @@ 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.11 --self-test
|
|
63
|
+
|
|
64
|
+
# Provision a free Cloud Workspace (or reuse REVCLAMP_API_KEY) & send a 6-digit login OTP
|
|
65
|
+
npx -y revclamp-mcp@1.20.11 --init-workspace you@company.com
|
|
66
|
+
|
|
67
|
+
# Verify the 6-digit OTP using REVCLAMP_API_KEY from environment
|
|
68
|
+
REVCLAMP_API_KEY="rg_live_..." npx -y revclamp-mcp@1.20.11 --verify-otp you@company.com 123456
|
|
63
69
|
|
|
64
70
|
# List all 9 MCP tools and JSON schemas (runs locally in-process)
|
|
65
71
|
node index.mjs --list-tools
|
package/index.mjs
CHANGED
|
@@ -33,10 +33,16 @@ export {
|
|
|
33
33
|
executeLocalMcpJsonRpc,
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
-
export const PACKAGE_VERSION = "1.20.
|
|
36
|
+
export const PACKAGE_VERSION = "1.20.11";
|
|
37
37
|
export const DEFAULT_MCP_ENDPOINT = "https://api.revclamp.com/api/mcp";
|
|
38
38
|
export const ALLOWED_MCP_ORIGINS = Object.freeze(
|
|
39
|
-
new Set([
|
|
39
|
+
new Set([
|
|
40
|
+
"https://api.revclamp.com",
|
|
41
|
+
"https://revclamp.com",
|
|
42
|
+
"https://app.revclamp.com",
|
|
43
|
+
"http://127.0.0.1",
|
|
44
|
+
"http://localhost",
|
|
45
|
+
])
|
|
40
46
|
);
|
|
41
47
|
export const MAX_PAYLOAD_BYTES = 32_768;
|
|
42
48
|
export const MAX_STDIO_LINE_BYTES = MAX_PAYLOAD_BYTES;
|
|
@@ -371,13 +377,19 @@ export async function forwardMcpJsonRpc(
|
|
|
371
377
|
rpcRequest?.params?.name === "revclamp_validate_eu_vat_vies" &&
|
|
372
378
|
(rawViesMode === "FORMAT_ONLY" || rpcRequest?.params?.arguments?.demo === true);
|
|
373
379
|
|
|
380
|
+
const cleanKey = String(apiKey || "").trim();
|
|
381
|
+
|
|
374
382
|
if (
|
|
375
383
|
String(mode || "").toLowerCase() === "local" ||
|
|
376
384
|
!hasExplicitRemote ||
|
|
377
385
|
isLocalOnlyViesCall
|
|
378
386
|
) {
|
|
379
387
|
if (isNotification) return null;
|
|
380
|
-
return executeLocalMcpJsonRpc(
|
|
388
|
+
return executeLocalMcpJsonRpc(
|
|
389
|
+
{ ...rpcRequest, id: safeId },
|
|
390
|
+
PACKAGE_VERSION,
|
|
391
|
+
{ suppressCloudUpgradeHint: Boolean(cleanKey) }
|
|
392
|
+
);
|
|
381
393
|
}
|
|
382
394
|
|
|
383
395
|
const boundedTimeoutMs = Math.min(10_000, Math.max(100, Number(timeoutMs) || REQUEST_TIMEOUT_MS));
|
|
@@ -385,7 +397,6 @@ export async function forwardMcpJsonRpc(
|
|
|
385
397
|
"content-type": "application/json",
|
|
386
398
|
accept: "application/json",
|
|
387
399
|
};
|
|
388
|
-
const cleanKey = String(apiKey || "").trim();
|
|
389
400
|
if (cleanKey) {
|
|
390
401
|
headers.authorization = `Bearer ${cleanKey}`;
|
|
391
402
|
}
|
|
@@ -788,6 +799,9 @@ function printHelp() {
|
|
|
788
799
|
" revclamp-mcp --sim-replay [presetKey] [windowSec] Simulate webhook replay & out-of-order state machine",
|
|
789
800
|
" revclamp-mcp --lint-evidence [preset] Lint Stripe dispute evidence UTF-16 & CE 3.0 completeness",
|
|
790
801
|
" revclamp-mcp --verify-webhook [provider] [rawPayload] [signatureHeader] Verify webhook HMAC via REVCLAMP_WEBHOOK_SECRET env var",
|
|
802
|
+
" revclamp-mcp --init-workspace <email> Provision a free Cloud Workspace (or reuse REVCLAMP_API_KEY) & send 6-digit OTP",
|
|
803
|
+
" revclamp-mcp --resend-otp <email> Resend 6-digit OTP using existing REVCLAMP_API_KEY env var",
|
|
804
|
+
" revclamp-mcp --verify-otp <email> <code> Verify 6-digit OTP using REVCLAMP_API_KEY env var & print MCP config snippet",
|
|
791
805
|
" revclamp-mcp --call <tool_name> '<json_args>' Invoke any RevClamp MCP tool directly",
|
|
792
806
|
"",
|
|
793
807
|
"Environment Variables:",
|
|
@@ -810,17 +824,468 @@ const KNOWN_CLI_FLAGS = new Set([
|
|
|
810
824
|
"--sim-replay",
|
|
811
825
|
"--lint-evidence",
|
|
812
826
|
"--verify-webhook",
|
|
827
|
+
"--init-workspace",
|
|
828
|
+
"--resend-otp",
|
|
829
|
+
"--verify-otp",
|
|
813
830
|
"--call",
|
|
814
831
|
"--stdio",
|
|
815
832
|
"--url",
|
|
816
833
|
"--endpoint",
|
|
817
834
|
]);
|
|
818
835
|
|
|
836
|
+
function resolveValidatedOnboardingOrigin(rawBaseUrl = "https://revclamp.com") {
|
|
837
|
+
const trimmed = String(rawBaseUrl || "https://revclamp.com").trim();
|
|
838
|
+
let parsed;
|
|
839
|
+
try {
|
|
840
|
+
parsed = new URL(trimmed);
|
|
841
|
+
} catch {
|
|
842
|
+
return { ok: false, errorCode: "INVALID_MCP_ENDPOINT_URL" };
|
|
843
|
+
}
|
|
844
|
+
if (parsed.username || parsed.password) {
|
|
845
|
+
return { ok: false, errorCode: "UNSAFE_MCP_ENDPOINT_CREDENTIALS" };
|
|
846
|
+
}
|
|
847
|
+
const baseOriginWithoutPort = `${parsed.protocol}//${parsed.hostname}`;
|
|
848
|
+
const isLoopbackHttp =
|
|
849
|
+
parsed.protocol === "http:" &&
|
|
850
|
+
(parsed.hostname === "127.0.0.1" || parsed.hostname === "localhost") &&
|
|
851
|
+
ALLOWED_MCP_ORIGINS.has(baseOriginWithoutPort);
|
|
852
|
+
const isAllowedHttps =
|
|
853
|
+
parsed.protocol === "https:" && ALLOWED_MCP_ORIGINS.has(parsed.origin);
|
|
854
|
+
if (!isAllowedHttps && !isLoopbackHttp) {
|
|
855
|
+
return {
|
|
856
|
+
ok: false,
|
|
857
|
+
errorCode: "UNTRUSTED_MCP_ENDPOINT_ORIGIN",
|
|
858
|
+
origin: parsed.origin,
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
return { ok: true, origin: parsed.origin };
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Executes CLI-to-Cloud workspace onboarding commands (--init-workspace, --resend-otp, --verify-otp)
|
|
866
|
+
* enforcing S1 (idempotent key reuse), S2 (partial-failure key preservation), and S3 (zero raw secret in argv).
|
|
867
|
+
*
|
|
868
|
+
* @param {object} params
|
|
869
|
+
* @returns {Promise<object>}
|
|
870
|
+
*/
|
|
871
|
+
export async function runMcpCliOnboardingCommand({
|
|
872
|
+
mode = "",
|
|
873
|
+
email = "",
|
|
874
|
+
otpCode = "",
|
|
875
|
+
argv = [],
|
|
876
|
+
env = process.env,
|
|
877
|
+
baseUrl = "",
|
|
878
|
+
fetchImpl,
|
|
879
|
+
} = {}) {
|
|
880
|
+
const scanTokens = [
|
|
881
|
+
...(Array.isArray(argv) ? argv : []),
|
|
882
|
+
email,
|
|
883
|
+
otpCode,
|
|
884
|
+
].map((t) => String(t ?? "").trim());
|
|
885
|
+
|
|
886
|
+
for (const tok of scanTokens) {
|
|
887
|
+
if (
|
|
888
|
+
/^rg_(?:live|test)_/i.test(tok) ||
|
|
889
|
+
/(?:^|=)rg_(?:live|test)_/i.test(tok) ||
|
|
890
|
+
tok === "--api-key" ||
|
|
891
|
+
tok.startsWith("--api-key=")
|
|
892
|
+
) {
|
|
893
|
+
return {
|
|
894
|
+
ok: false,
|
|
895
|
+
exitCode: 1,
|
|
896
|
+
errorCode: "RAW_API_KEY_IN_ARGV_FORBIDDEN",
|
|
897
|
+
message:
|
|
898
|
+
"Pass REVCLAMP_API_KEY via environment variable, never as a CLI argument.",
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const effectiveEnv = env && typeof env === "object" ? env : process.env;
|
|
904
|
+
const resolvedBaseUrl =
|
|
905
|
+
baseUrl ||
|
|
906
|
+
effectiveEnv.REVCLAMP_API_URL ||
|
|
907
|
+
effectiveEnv.REVCLAMP_MCP_URL ||
|
|
908
|
+
effectiveEnv.REVCLAMP_WEB_URL ||
|
|
909
|
+
effectiveEnv.REVCLAMP_ONBOARDING_URL ||
|
|
910
|
+
"https://revclamp.com";
|
|
911
|
+
const originCheck = resolveValidatedOnboardingOrigin(resolvedBaseUrl);
|
|
912
|
+
if (!originCheck.ok) {
|
|
913
|
+
return {
|
|
914
|
+
ok: false,
|
|
915
|
+
exitCode: 1,
|
|
916
|
+
errorCode: originCheck.errorCode,
|
|
917
|
+
message: `Untrusted or invalid onboarding origin: ${String(resolvedBaseUrl)}`,
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
const cleanEmail = typeof email === "string" ? email.trim().toLowerCase() : "";
|
|
922
|
+
if (
|
|
923
|
+
cleanEmail.length < 3 ||
|
|
924
|
+
cleanEmail.length > 254 ||
|
|
925
|
+
!/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$/.test(cleanEmail)
|
|
926
|
+
) {
|
|
927
|
+
return {
|
|
928
|
+
ok: false,
|
|
929
|
+
exitCode: 1,
|
|
930
|
+
errorCode: "INVALID_EMAIL_ADDRESS",
|
|
931
|
+
message: "A valid email address (3..254 characters) is required.",
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const normalizedMode = String(mode || "")
|
|
936
|
+
.trim()
|
|
937
|
+
.replace(/^--+/, "")
|
|
938
|
+
.toLowerCase();
|
|
939
|
+
const effectiveFetch = fetchImpl || globalThis.fetch;
|
|
940
|
+
|
|
941
|
+
const postOnboardingJson = async (pathname, payloadObj, apiKeyHeader = "") => {
|
|
942
|
+
const targetUrl = `${originCheck.origin}${pathname}`;
|
|
943
|
+
const headers = {
|
|
944
|
+
"content-type": "application/json",
|
|
945
|
+
accept: "application/json",
|
|
946
|
+
};
|
|
947
|
+
if (apiKeyHeader) {
|
|
948
|
+
headers.authorization = `Bearer ${apiKeyHeader}`;
|
|
949
|
+
headers["x-revclamp-tenant-key"] = apiKeyHeader;
|
|
950
|
+
}
|
|
951
|
+
try {
|
|
952
|
+
const res = await effectiveFetch(targetUrl, {
|
|
953
|
+
method: "POST",
|
|
954
|
+
headers,
|
|
955
|
+
body: JSON.stringify(payloadObj),
|
|
956
|
+
redirect: "error",
|
|
957
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
958
|
+
});
|
|
959
|
+
const status = Number(res?.status || 0);
|
|
960
|
+
const httpOk =
|
|
961
|
+
typeof res?.ok === "boolean" ? res.ok : status >= 200 && status < 300;
|
|
962
|
+
const rawText = await readBoundedResponseText(res, MAX_PAYLOAD_BYTES);
|
|
963
|
+
let body = null;
|
|
964
|
+
try {
|
|
965
|
+
body = rawText ? JSON.parse(rawText) : null;
|
|
966
|
+
} catch {
|
|
967
|
+
body = null;
|
|
968
|
+
}
|
|
969
|
+
return {
|
|
970
|
+
httpOk,
|
|
971
|
+
status,
|
|
972
|
+
body,
|
|
973
|
+
errorCode:
|
|
974
|
+
body && typeof body === "object"
|
|
975
|
+
? body.errorCode || body.error || null
|
|
976
|
+
: null,
|
|
977
|
+
message:
|
|
978
|
+
body && typeof body === "object" ? body.message || null : null,
|
|
979
|
+
};
|
|
980
|
+
} catch (err) {
|
|
981
|
+
if (err instanceof Error && err.message === "UPSTREAM_RESPONSE_TOO_LARGE") {
|
|
982
|
+
return {
|
|
983
|
+
httpOk: false,
|
|
984
|
+
status: 502,
|
|
985
|
+
body: null,
|
|
986
|
+
errorCode: "UPSTREAM_RESPONSE_TOO_LARGE",
|
|
987
|
+
message: "Upstream onboarding response exceeded 32,768-byte ceiling.",
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
return {
|
|
991
|
+
httpOk: false,
|
|
992
|
+
status: 0,
|
|
993
|
+
body: null,
|
|
994
|
+
errorCode: "ONBOARDING_NETWORK_ERROR",
|
|
995
|
+
message: err instanceof Error ? err.message : "Network error",
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
};
|
|
999
|
+
|
|
1000
|
+
const envApiKey =
|
|
1001
|
+
typeof effectiveEnv.REVCLAMP_API_KEY === "string"
|
|
1002
|
+
? effectiveEnv.REVCLAMP_API_KEY.trim()
|
|
1003
|
+
: "";
|
|
1004
|
+
const hasValidEnvKey = /^rg_live_[a-zA-Z0-9_-]{12,128}$/.test(envApiKey);
|
|
1005
|
+
|
|
1006
|
+
if (normalizedMode === "init-workspace" || normalizedMode === "init") {
|
|
1007
|
+
let activeApiKey = "";
|
|
1008
|
+
let tenantId = null;
|
|
1009
|
+
let reusedExistingEnvKey = false;
|
|
1010
|
+
|
|
1011
|
+
const provisionFreshWorkspace = async () => {
|
|
1012
|
+
const provRes = await postOnboardingJson("/api/dashboard/provision", {
|
|
1013
|
+
tier: "free",
|
|
1014
|
+
acquisitionSurface: "mcp_cli",
|
|
1015
|
+
merchantEmail: cleanEmail,
|
|
1016
|
+
});
|
|
1017
|
+
const provisionedKey =
|
|
1018
|
+
provRes.body && typeof provRes.body.apiKey === "string"
|
|
1019
|
+
? provRes.body.apiKey.trim()
|
|
1020
|
+
: "";
|
|
1021
|
+
if (!provRes.httpOk || !provisionedKey) {
|
|
1022
|
+
return {
|
|
1023
|
+
ok: false,
|
|
1024
|
+
res: {
|
|
1025
|
+
ok: false,
|
|
1026
|
+
exitCode: 1,
|
|
1027
|
+
stage: "PROVISION_FAILED",
|
|
1028
|
+
errorCode: provRes.errorCode || "PROVISION_WORKSPACE_FAILED",
|
|
1029
|
+
status: provRes.status,
|
|
1030
|
+
message:
|
|
1031
|
+
provRes.message || "Failed to provision free Cloud Workspace.",
|
|
1032
|
+
},
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
return {
|
|
1036
|
+
ok: true,
|
|
1037
|
+
apiKey: provisionedKey,
|
|
1038
|
+
tenantId: provRes.body.tenantId || null,
|
|
1039
|
+
};
|
|
1040
|
+
};
|
|
1041
|
+
|
|
1042
|
+
if (hasValidEnvKey) {
|
|
1043
|
+
activeApiKey = envApiKey;
|
|
1044
|
+
reusedExistingEnvKey = true;
|
|
1045
|
+
} else {
|
|
1046
|
+
const freshProv = await provisionFreshWorkspace();
|
|
1047
|
+
if (!freshProv.ok) {
|
|
1048
|
+
return freshProv.res;
|
|
1049
|
+
}
|
|
1050
|
+
activeApiKey = freshProv.apiKey;
|
|
1051
|
+
tenantId = freshProv.tenantId;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
let otpRes = await postOnboardingJson(
|
|
1055
|
+
"/api/dashboard/send-otp",
|
|
1056
|
+
{
|
|
1057
|
+
email: cleanEmail,
|
|
1058
|
+
purpose: "dashboard_login",
|
|
1059
|
+
acquisitionSurface: "mcp_cli",
|
|
1060
|
+
},
|
|
1061
|
+
activeApiKey
|
|
1062
|
+
);
|
|
1063
|
+
|
|
1064
|
+
if (
|
|
1065
|
+
reusedExistingEnvKey &&
|
|
1066
|
+
(otpRes.status === 401 || otpRes.errorCode === "UNAUTHORIZED_TENANT_KEY")
|
|
1067
|
+
) {
|
|
1068
|
+
const freshProv = await provisionFreshWorkspace();
|
|
1069
|
+
if (!freshProv.ok) {
|
|
1070
|
+
return freshProv.res;
|
|
1071
|
+
}
|
|
1072
|
+
activeApiKey = freshProv.apiKey;
|
|
1073
|
+
tenantId = freshProv.tenantId;
|
|
1074
|
+
reusedExistingEnvKey = false;
|
|
1075
|
+
otpRes = await postOnboardingJson(
|
|
1076
|
+
"/api/dashboard/send-otp",
|
|
1077
|
+
{
|
|
1078
|
+
email: cleanEmail,
|
|
1079
|
+
purpose: "dashboard_login",
|
|
1080
|
+
acquisitionSurface: "mcp_cli",
|
|
1081
|
+
},
|
|
1082
|
+
activeApiKey
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
const cmdKeyToken = reusedExistingEnvKey
|
|
1087
|
+
? '"$REVCLAMP_API_KEY"'
|
|
1088
|
+
: activeApiKey;
|
|
1089
|
+
|
|
1090
|
+
if (!otpRes.httpOk || otpRes.body?.ok === false) {
|
|
1091
|
+
return {
|
|
1092
|
+
ok: false,
|
|
1093
|
+
exitCode: 1,
|
|
1094
|
+
stage: "SEND_OTP_FAILED",
|
|
1095
|
+
errorCode: otpRes.errorCode || "SEND_OTP_FAILED",
|
|
1096
|
+
status: otpRes.status,
|
|
1097
|
+
email: cleanEmail,
|
|
1098
|
+
tenantId: otpRes.body?.tenantId || tenantId,
|
|
1099
|
+
apiKey: activeApiKey,
|
|
1100
|
+
reusedExistingEnvKey,
|
|
1101
|
+
nextStepCommand: `REVCLAMP_API_KEY=${cmdKeyToken} npx -y revclamp-mcp@${PACKAGE_VERSION} --resend-otp ${cleanEmail}`,
|
|
1102
|
+
verifyCommandHint: `REVCLAMP_API_KEY=${cmdKeyToken} npx -y revclamp-mcp@${PACKAGE_VERSION} --verify-otp ${cleanEmail} <6-digit-code>`,
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
return {
|
|
1107
|
+
ok: true,
|
|
1108
|
+
exitCode: 0,
|
|
1109
|
+
stage: "OTP_DISPATCHED",
|
|
1110
|
+
email: cleanEmail,
|
|
1111
|
+
tenantId: otpRes.body?.tenantId || tenantId,
|
|
1112
|
+
apiKey: activeApiKey,
|
|
1113
|
+
reusedExistingEnvKey,
|
|
1114
|
+
nextStepCommand: `REVCLAMP_API_KEY=${cmdKeyToken} npx -y revclamp-mcp@${PACKAGE_VERSION} --verify-otp ${cleanEmail} <6-digit-code>`,
|
|
1115
|
+
resendCommand: `REVCLAMP_API_KEY=${cmdKeyToken} npx -y revclamp-mcp@${PACKAGE_VERSION} --resend-otp ${cleanEmail}`,
|
|
1116
|
+
upgradeUrl: "https://revclamp.com/pricing?source=mcp_cli",
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
if (normalizedMode === "resend-otp" || normalizedMode === "resend") {
|
|
1121
|
+
if (!envApiKey) {
|
|
1122
|
+
return {
|
|
1123
|
+
ok: false,
|
|
1124
|
+
exitCode: 1,
|
|
1125
|
+
errorCode: "MISSING_REVCLAMP_API_KEY_ENV",
|
|
1126
|
+
message:
|
|
1127
|
+
"Set REVCLAMP_API_KEY in the environment before running --resend-otp.",
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
if (!hasValidEnvKey) {
|
|
1131
|
+
return {
|
|
1132
|
+
ok: false,
|
|
1133
|
+
exitCode: 1,
|
|
1134
|
+
errorCode: "INVALID_REVCLAMP_API_KEY_ENV",
|
|
1135
|
+
message:
|
|
1136
|
+
"REVCLAMP_API_KEY must be a valid rg_live_ workspace key.",
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const otpRes = await postOnboardingJson(
|
|
1141
|
+
"/api/dashboard/send-otp",
|
|
1142
|
+
{
|
|
1143
|
+
email: cleanEmail,
|
|
1144
|
+
purpose: "dashboard_login",
|
|
1145
|
+
acquisitionSurface: "mcp_cli",
|
|
1146
|
+
},
|
|
1147
|
+
envApiKey
|
|
1148
|
+
);
|
|
1149
|
+
|
|
1150
|
+
if (!otpRes.httpOk || otpRes.body?.ok === false) {
|
|
1151
|
+
return {
|
|
1152
|
+
ok: false,
|
|
1153
|
+
exitCode: 1,
|
|
1154
|
+
stage: "SEND_OTP_FAILED",
|
|
1155
|
+
errorCode: otpRes.errorCode || "SEND_OTP_FAILED",
|
|
1156
|
+
status: otpRes.status,
|
|
1157
|
+
email: cleanEmail,
|
|
1158
|
+
tenantId: otpRes.body?.tenantId || null,
|
|
1159
|
+
reusedExistingEnvKey: true,
|
|
1160
|
+
nextStepCommand: `REVCLAMP_API_KEY="$REVCLAMP_API_KEY" npx -y revclamp-mcp@${PACKAGE_VERSION} --resend-otp ${cleanEmail}`,
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
return {
|
|
1165
|
+
ok: true,
|
|
1166
|
+
exitCode: 0,
|
|
1167
|
+
stage: "OTP_RESENT",
|
|
1168
|
+
email: cleanEmail,
|
|
1169
|
+
tenantId: otpRes.body?.tenantId || null,
|
|
1170
|
+
reusedExistingEnvKey: true,
|
|
1171
|
+
nextStepCommand: `REVCLAMP_API_KEY="$REVCLAMP_API_KEY" npx -y revclamp-mcp@${PACKAGE_VERSION} --verify-otp ${cleanEmail} <6-digit-code>`,
|
|
1172
|
+
upgradeUrl: "https://revclamp.com/pricing?source=mcp_cli",
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
if (normalizedMode === "verify-otp" || normalizedMode === "verify") {
|
|
1177
|
+
if (!envApiKey) {
|
|
1178
|
+
return {
|
|
1179
|
+
ok: false,
|
|
1180
|
+
exitCode: 1,
|
|
1181
|
+
errorCode: "MISSING_REVCLAMP_API_KEY_ENV",
|
|
1182
|
+
message:
|
|
1183
|
+
"Set REVCLAMP_API_KEY in the environment before running --verify-otp.",
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
if (!hasValidEnvKey) {
|
|
1187
|
+
return {
|
|
1188
|
+
ok: false,
|
|
1189
|
+
exitCode: 1,
|
|
1190
|
+
errorCode: "INVALID_REVCLAMP_API_KEY_ENV",
|
|
1191
|
+
message:
|
|
1192
|
+
"REVCLAMP_API_KEY must be a valid rg_live_ workspace key.",
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
const cleanCode =
|
|
1197
|
+
typeof otpCode === "string" || typeof otpCode === "number"
|
|
1198
|
+
? String(otpCode).trim()
|
|
1199
|
+
: "";
|
|
1200
|
+
if (!/^\d{6}$/.test(cleanCode)) {
|
|
1201
|
+
return {
|
|
1202
|
+
ok: false,
|
|
1203
|
+
exitCode: 1,
|
|
1204
|
+
errorCode: "INVALID_OTP_CODE_FORMAT",
|
|
1205
|
+
message: "OTP code must be a 6-digit numeric code.",
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
const verifyRes = await postOnboardingJson(
|
|
1210
|
+
"/api/dashboard/verify-otp",
|
|
1211
|
+
{
|
|
1212
|
+
email: cleanEmail,
|
|
1213
|
+
otpCode: cleanCode,
|
|
1214
|
+
code: cleanCode,
|
|
1215
|
+
purpose: "dashboard_login",
|
|
1216
|
+
acquisitionSurface: "mcp_cli",
|
|
1217
|
+
},
|
|
1218
|
+
envApiKey
|
|
1219
|
+
);
|
|
1220
|
+
|
|
1221
|
+
if (
|
|
1222
|
+
verifyRes.status === 200 &&
|
|
1223
|
+
verifyRes.body?.ok === true &&
|
|
1224
|
+
verifyRes.body?.verified === true
|
|
1225
|
+
) {
|
|
1226
|
+
return {
|
|
1227
|
+
ok: true,
|
|
1228
|
+
exitCode: 0,
|
|
1229
|
+
verified: true,
|
|
1230
|
+
stage: "WORKSPACE_VERIFIED",
|
|
1231
|
+
email: cleanEmail,
|
|
1232
|
+
tenantId: verifyRes.body?.tenantId || null,
|
|
1233
|
+
mcpServersConfig: {
|
|
1234
|
+
mcpServers: {
|
|
1235
|
+
revclamp: {
|
|
1236
|
+
command: "npx",
|
|
1237
|
+
args: ["-y", `revclamp-mcp@${PACKAGE_VERSION}`],
|
|
1238
|
+
env: {
|
|
1239
|
+
REVCLAMP_API_KEY: "<SET_FROM_ENV_RG_LIVE_KEY>",
|
|
1240
|
+
},
|
|
1241
|
+
},
|
|
1242
|
+
},
|
|
1243
|
+
},
|
|
1244
|
+
upgradeUrl: "https://revclamp.com/pricing?source=mcp_cli",
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
return {
|
|
1249
|
+
ok: false,
|
|
1250
|
+
exitCode: 1,
|
|
1251
|
+
verified: false,
|
|
1252
|
+
stage: "VERIFY_OTP_FAILED",
|
|
1253
|
+
errorCode: verifyRes.errorCode || "OTP_VERIFICATION_FAILED",
|
|
1254
|
+
status: verifyRes.status,
|
|
1255
|
+
message: verifyRes.message || "OTP verification failed.",
|
|
1256
|
+
nextStepCommand: `REVCLAMP_API_KEY="$REVCLAMP_API_KEY" npx -y revclamp-mcp@${PACKAGE_VERSION} --verify-otp ${cleanEmail} <6-digit-code>`,
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
return {
|
|
1261
|
+
ok: false,
|
|
1262
|
+
exitCode: 1,
|
|
1263
|
+
errorCode: "UNKNOWN_ONBOARDING_MODE",
|
|
1264
|
+
message: `Unsupported onboarding mode: ${String(mode)}`,
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
|
|
819
1268
|
export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
820
1269
|
const effectiveOptions = { ...options };
|
|
821
1270
|
const cleanedArgv = [];
|
|
822
1271
|
for (let i = 0; i < argv.length; i += 1) {
|
|
823
1272
|
const token = String(argv[i] ?? "");
|
|
1273
|
+
if (
|
|
1274
|
+
token === "--api-key" ||
|
|
1275
|
+
token.startsWith("--api-key=") ||
|
|
1276
|
+
/^rg_(?:live|test)_/i.test(token) ||
|
|
1277
|
+
/(?:^|=)rg_(?:live|test)_/i.test(token)
|
|
1278
|
+
) {
|
|
1279
|
+
const errOut = {
|
|
1280
|
+
ok: false,
|
|
1281
|
+
exitCode: 1,
|
|
1282
|
+
errorCode: "RAW_API_KEY_IN_ARGV_FORBIDDEN",
|
|
1283
|
+
message:
|
|
1284
|
+
"Pass REVCLAMP_API_KEY via environment variable, never as a CLI argument.",
|
|
1285
|
+
};
|
|
1286
|
+
console.error(JSON.stringify(errOut));
|
|
1287
|
+
return 1;
|
|
1288
|
+
}
|
|
824
1289
|
if (token === "--secret" || token.startsWith("--secret=")) {
|
|
825
1290
|
console.error(
|
|
826
1291
|
"Error: passing webhook secrets via CLI flags (--secret) is forbidden; set REVCLAMP_WEBHOOK_SECRET in the environment."
|
|
@@ -1346,6 +1811,83 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1346
1811
|
return !res || res.error || res.result?.isError === true ? 1 : 0;
|
|
1347
1812
|
}
|
|
1348
1813
|
|
|
1814
|
+
const initWorkspaceIdx = cleanedArgv.indexOf("--init-workspace");
|
|
1815
|
+
if (initWorkspaceIdx !== -1) {
|
|
1816
|
+
const rawEmailToken = String(cleanedArgv[initWorkspaceIdx + 1] ?? "").trim();
|
|
1817
|
+
const emailArg = rawEmailToken.startsWith("-") ? "" : rawEmailToken;
|
|
1818
|
+
const out = await runMcpCliOnboardingCommand({
|
|
1819
|
+
mode: "init-workspace",
|
|
1820
|
+
email: emailArg,
|
|
1821
|
+
argv,
|
|
1822
|
+
env: effectiveOptions.env || process.env,
|
|
1823
|
+
baseUrl:
|
|
1824
|
+
effectiveOptions.baseUrl ||
|
|
1825
|
+
effectiveOptions.endpointUrl ||
|
|
1826
|
+
effectiveOptions.apiUrl ||
|
|
1827
|
+
"",
|
|
1828
|
+
fetchImpl: effectiveOptions.fetchImpl,
|
|
1829
|
+
});
|
|
1830
|
+
if (out.ok) {
|
|
1831
|
+
console.log(JSON.stringify(out, null, 2));
|
|
1832
|
+
return 0;
|
|
1833
|
+
}
|
|
1834
|
+
console.error(JSON.stringify(out, null, 2));
|
|
1835
|
+
return 1;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
const resendOtpIdx = cleanedArgv.indexOf("--resend-otp");
|
|
1839
|
+
if (resendOtpIdx !== -1) {
|
|
1840
|
+
const rawEmailToken = String(cleanedArgv[resendOtpIdx + 1] ?? "").trim();
|
|
1841
|
+
const emailArg = rawEmailToken.startsWith("-") ? "" : rawEmailToken;
|
|
1842
|
+
const out = await runMcpCliOnboardingCommand({
|
|
1843
|
+
mode: "resend-otp",
|
|
1844
|
+
email: emailArg,
|
|
1845
|
+
argv,
|
|
1846
|
+
env: effectiveOptions.env || process.env,
|
|
1847
|
+
baseUrl:
|
|
1848
|
+
effectiveOptions.baseUrl ||
|
|
1849
|
+
effectiveOptions.endpointUrl ||
|
|
1850
|
+
effectiveOptions.apiUrl ||
|
|
1851
|
+
"",
|
|
1852
|
+
fetchImpl: effectiveOptions.fetchImpl,
|
|
1853
|
+
});
|
|
1854
|
+
if (out.ok) {
|
|
1855
|
+
console.log(JSON.stringify(out, null, 2));
|
|
1856
|
+
return 0;
|
|
1857
|
+
}
|
|
1858
|
+
console.error(JSON.stringify(out, null, 2));
|
|
1859
|
+
return 1;
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
const verifyOtpIdx = cleanedArgv.indexOf("--verify-otp");
|
|
1863
|
+
if (verifyOtpIdx !== -1) {
|
|
1864
|
+
const rawEmailToken = String(cleanedArgv[verifyOtpIdx + 1] ?? "").trim();
|
|
1865
|
+
const emailArg = rawEmailToken.startsWith("-") ? "" : rawEmailToken;
|
|
1866
|
+
const rawCodeToken = emailArg
|
|
1867
|
+
? String(cleanedArgv[verifyOtpIdx + 2] ?? "").trim()
|
|
1868
|
+
: "";
|
|
1869
|
+
const otpCodeArg = rawCodeToken.startsWith("-") ? "" : rawCodeToken;
|
|
1870
|
+
const out = await runMcpCliOnboardingCommand({
|
|
1871
|
+
mode: "verify-otp",
|
|
1872
|
+
email: emailArg,
|
|
1873
|
+
otpCode: otpCodeArg,
|
|
1874
|
+
argv,
|
|
1875
|
+
env: effectiveOptions.env || process.env,
|
|
1876
|
+
baseUrl:
|
|
1877
|
+
effectiveOptions.baseUrl ||
|
|
1878
|
+
effectiveOptions.endpointUrl ||
|
|
1879
|
+
effectiveOptions.apiUrl ||
|
|
1880
|
+
"",
|
|
1881
|
+
fetchImpl: effectiveOptions.fetchImpl,
|
|
1882
|
+
});
|
|
1883
|
+
if (out.ok) {
|
|
1884
|
+
console.log(JSON.stringify(out, null, 2));
|
|
1885
|
+
return 0;
|
|
1886
|
+
}
|
|
1887
|
+
console.error(JSON.stringify(out, null, 2));
|
|
1888
|
+
return 1;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1349
1891
|
const callIdx = cleanedArgv.indexOf("--call");
|
|
1350
1892
|
if (callIdx !== -1) {
|
|
1351
1893
|
if (cleanedArgv.length > callIdx + 3) {
|
package/local_engine.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* caller-supplied payloads. Never fabricates live consultation IDs or card-network
|
|
13
13
|
* dispute evidence unless `demo: true` is explicitly passed (`sample: true`).
|
|
14
14
|
* - Cloud Workspace Mode (`REVCLAMP_API_KEY` on https://api.revclamp.com/api/mcp):
|
|
15
|
-
* Unlocks the 90-day
|
|
15
|
+
* Unlocks the 90-day durable transactional usage ledger, 24/7 autonomous Stripe dispute &
|
|
16
16
|
* entitlement webhook outbox, and live European Commission VIES consultation IDs.
|
|
17
17
|
*/
|
|
18
18
|
|
|
@@ -145,7 +145,7 @@ export const LOCAL_MCP_RESOURCES = Object.freeze([
|
|
|
145
145
|
uri: "revclamp://docs/open-core-capabilities",
|
|
146
146
|
name: "RevClamp Open-Core vs Hosted Cloud Capabilities",
|
|
147
147
|
description:
|
|
148
|
-
"Reference summary of the 9 local deterministic MCP tools vs. hosted REVCLAMP_API_KEY capabilities (90-day
|
|
148
|
+
"Reference summary of the 9 local deterministic MCP tools vs. hosted REVCLAMP_API_KEY capabilities (90-day durable transactional ledger, 24/7 Stripe webhook outbox, and live EU VIES consultation IDs).",
|
|
149
149
|
mimeType: "application/json",
|
|
150
150
|
}),
|
|
151
151
|
]);
|
|
@@ -189,7 +189,7 @@ const EU_VIES_FORMAT_RULES = Object.freeze({
|
|
|
189
189
|
DK: /^DK\d{8}$/,
|
|
190
190
|
EE: /^EE\d{9}$/,
|
|
191
191
|
EL: /^EL\d{9}$/,
|
|
192
|
-
ES: /^ES[A-
|
|
192
|
+
ES: /^ES(?:[A-Z]\d{7}[A-Z0-9]|[0-9]\d{7}[A-Z])$/,
|
|
193
193
|
FI: /^FI\d{8}$/,
|
|
194
194
|
FR: /^FR[A-HJ-NP-Z0-9]{2}\d{9}$/,
|
|
195
195
|
HR: /^HR\d{11}$/,
|
|
@@ -214,6 +214,7 @@ const REVCLAMP_START_MARKER = "--- BEGIN REVCLAMP CE3 EVIDENCE ---";
|
|
|
214
214
|
const REVCLAMP_END_MARKER = "--- END REVCLAMP CE3 EVIDENCE ---";
|
|
215
215
|
const REVCLAMP_BRACKET_START_MARKER = "[RevClamp Verified Product Usage Log]";
|
|
216
216
|
const REVCLAMP_BRACKET_END_MARKER = "[/RevClamp Verified Product Usage Log]";
|
|
217
|
+
const REVCLAMP_BRACKET_ALT_END_MARKER = "[End RevClamp Log]";
|
|
217
218
|
const PRODUCTION_EMPTY_BLOCK_WRAPPER_CHARS =
|
|
218
219
|
REVCLAMP_BRACKET_START_MARKER.length + 1 + REVCLAMP_BRACKET_END_MARKER.length;
|
|
219
220
|
const MAX_ACCESS_ACTIVITY_LOG_CHARS = 18_000;
|
|
@@ -654,14 +655,20 @@ export function validateArgsAgainstSchema(schema, args) {
|
|
|
654
655
|
function normalizeCountryCode(raw) {
|
|
655
656
|
const clean = String(raw || "")
|
|
656
657
|
.trim()
|
|
657
|
-
.toUpperCase()
|
|
658
|
-
|
|
658
|
+
.toUpperCase()
|
|
659
|
+
.replace(/[\s.\-/:_]+/g, "");
|
|
660
|
+
if (clean === "GR") return "EL";
|
|
661
|
+
if (clean === "GBNIR") return "XI";
|
|
662
|
+
return clean;
|
|
659
663
|
}
|
|
660
664
|
|
|
661
665
|
function normalizeVatId(rawVat) {
|
|
662
|
-
|
|
666
|
+
let compact = String(rawVat || "")
|
|
663
667
|
.toUpperCase()
|
|
664
|
-
.replace(/[\s.\-/:]+/g, "");
|
|
668
|
+
.replace(/[\s.\-/:_]+/g, "");
|
|
669
|
+
if (compact.startsWith("GBNIR")) {
|
|
670
|
+
compact = `XI${compact.slice(5)}`;
|
|
671
|
+
}
|
|
665
672
|
if (compact.length < 4) {
|
|
666
673
|
return { validFormat: false, memberState: "", vatNumber: "", fullVatId: compact };
|
|
667
674
|
}
|
|
@@ -693,50 +700,107 @@ function sanitizeLogToken(raw, maxLen = 96) {
|
|
|
693
700
|
|
|
694
701
|
function stripExistingRevClampBlock(rawNotes) {
|
|
695
702
|
if (typeof rawNotes !== "string" || !rawNotes) {
|
|
696
|
-
return {
|
|
703
|
+
return {
|
|
704
|
+
ok: true,
|
|
705
|
+
hadBlock: false,
|
|
706
|
+
prefix: "",
|
|
707
|
+
suffix: "",
|
|
708
|
+
text: "",
|
|
709
|
+
blockUtf16CodeUnits: 0,
|
|
710
|
+
};
|
|
697
711
|
}
|
|
698
712
|
const hasCanonical =
|
|
699
713
|
rawNotes.includes(REVCLAMP_START_MARKER) || rawNotes.includes(REVCLAMP_END_MARKER);
|
|
714
|
+
const hasBracketPrimaryEnd = rawNotes.includes(REVCLAMP_BRACKET_END_MARKER);
|
|
715
|
+
const hasBracketAltEnd = rawNotes.includes(REVCLAMP_BRACKET_ALT_END_MARKER);
|
|
700
716
|
const hasBracket =
|
|
701
|
-
rawNotes.includes(REVCLAMP_BRACKET_START_MARKER) ||
|
|
702
|
-
rawNotes.includes(REVCLAMP_BRACKET_END_MARKER);
|
|
717
|
+
rawNotes.includes(REVCLAMP_BRACKET_START_MARKER) || hasBracketPrimaryEnd || hasBracketAltEnd;
|
|
703
718
|
if (!hasCanonical && !hasBracket) {
|
|
704
|
-
return {
|
|
719
|
+
return {
|
|
720
|
+
ok: true,
|
|
721
|
+
hadBlock: false,
|
|
722
|
+
prefix: rawNotes,
|
|
723
|
+
suffix: "",
|
|
724
|
+
text: rawNotes.trim(),
|
|
725
|
+
blockUtf16CodeUnits: 0,
|
|
726
|
+
};
|
|
705
727
|
}
|
|
706
|
-
if (hasCanonical && hasBracket) {
|
|
707
|
-
return {
|
|
728
|
+
if ((hasCanonical && hasBracket) || (hasBracketPrimaryEnd && hasBracketAltEnd)) {
|
|
729
|
+
return {
|
|
730
|
+
ok: false,
|
|
731
|
+
hadBlock: true,
|
|
732
|
+
prefix: rawNotes,
|
|
733
|
+
suffix: "",
|
|
734
|
+
text: rawNotes.trim(),
|
|
735
|
+
blockUtf16CodeUnits: 0,
|
|
736
|
+
};
|
|
708
737
|
}
|
|
709
738
|
const activeStart = hasCanonical ? REVCLAMP_START_MARKER : REVCLAMP_BRACKET_START_MARKER;
|
|
710
|
-
const activeEnd = hasCanonical
|
|
739
|
+
const activeEnd = hasCanonical
|
|
740
|
+
? REVCLAMP_END_MARKER
|
|
741
|
+
: hasBracketAltEnd
|
|
742
|
+
? REVCLAMP_BRACKET_ALT_END_MARKER
|
|
743
|
+
: REVCLAMP_BRACKET_END_MARKER;
|
|
711
744
|
let cursor = 0;
|
|
712
745
|
const prefixSegments = [];
|
|
713
746
|
let trailingSegment = "";
|
|
747
|
+
let blockUtf16CodeUnits = 0;
|
|
714
748
|
while (cursor < rawNotes.length) {
|
|
715
749
|
const startIdx = rawNotes.indexOf(activeStart, cursor);
|
|
716
750
|
if (startIdx === -1) {
|
|
717
751
|
if (rawNotes.indexOf(activeEnd, cursor) !== -1) {
|
|
718
|
-
return {
|
|
752
|
+
return {
|
|
753
|
+
ok: false,
|
|
754
|
+
hadBlock: true,
|
|
755
|
+
prefix: rawNotes,
|
|
756
|
+
suffix: "",
|
|
757
|
+
text: rawNotes.trim(),
|
|
758
|
+
blockUtf16CodeUnits: 0,
|
|
759
|
+
};
|
|
719
760
|
}
|
|
720
761
|
trailingSegment = rawNotes.slice(cursor);
|
|
721
762
|
break;
|
|
722
763
|
}
|
|
723
764
|
if (rawNotes.slice(cursor, startIdx).includes(activeEnd)) {
|
|
724
|
-
return {
|
|
765
|
+
return {
|
|
766
|
+
ok: false,
|
|
767
|
+
hadBlock: true,
|
|
768
|
+
prefix: rawNotes,
|
|
769
|
+
suffix: "",
|
|
770
|
+
text: rawNotes.trim(),
|
|
771
|
+
blockUtf16CodeUnits: 0,
|
|
772
|
+
};
|
|
725
773
|
}
|
|
726
774
|
const contentStart = startIdx + activeStart.length;
|
|
727
775
|
const endIdx = rawNotes.indexOf(activeEnd, contentStart);
|
|
728
776
|
if (endIdx === -1) {
|
|
729
|
-
return {
|
|
777
|
+
return {
|
|
778
|
+
ok: false,
|
|
779
|
+
hadBlock: true,
|
|
780
|
+
prefix: rawNotes,
|
|
781
|
+
suffix: "",
|
|
782
|
+
text: rawNotes.trim(),
|
|
783
|
+
blockUtf16CodeUnits: 0,
|
|
784
|
+
};
|
|
730
785
|
}
|
|
731
786
|
const nextStartIdx = rawNotes.indexOf(activeStart, contentStart);
|
|
732
787
|
if (nextStartIdx !== -1 && nextStartIdx < endIdx) {
|
|
733
|
-
return {
|
|
788
|
+
return {
|
|
789
|
+
ok: false,
|
|
790
|
+
hadBlock: true,
|
|
791
|
+
prefix: rawNotes,
|
|
792
|
+
suffix: "",
|
|
793
|
+
text: rawNotes.trim(),
|
|
794
|
+
blockUtf16CodeUnits: 0,
|
|
795
|
+
};
|
|
734
796
|
}
|
|
735
797
|
const beforeSegment = rawNotes.slice(cursor, startIdx).replace(/\n{1,2}$/, "");
|
|
736
798
|
if (beforeSegment.length > 0) {
|
|
737
799
|
prefixSegments.push(beforeSegment);
|
|
738
800
|
}
|
|
739
|
-
|
|
801
|
+
const blockEndCursor = endIdx + activeEnd.length;
|
|
802
|
+
blockUtf16CodeUnits += blockEndCursor - startIdx;
|
|
803
|
+
cursor = blockEndCursor;
|
|
740
804
|
if (rawNotes.slice(cursor, cursor + 2) === "\n\n") {
|
|
741
805
|
cursor += 2;
|
|
742
806
|
} else if (rawNotes[cursor] === "\n") {
|
|
@@ -752,6 +816,7 @@ function stripExistingRevClampBlock(rawNotes) {
|
|
|
752
816
|
prefix,
|
|
753
817
|
suffix,
|
|
754
818
|
text: keptTrimmed.join("\n\n").trim(),
|
|
819
|
+
blockUtf16CodeUnits,
|
|
755
820
|
};
|
|
756
821
|
}
|
|
757
822
|
|
|
@@ -949,14 +1014,37 @@ function evaluateLocalGhostUsage(subscriptionsEnv, usageEvent, dunningGracePerio
|
|
|
949
1014
|
reason: "UNMATCHED_CUSTOMER_ID",
|
|
950
1015
|
};
|
|
951
1016
|
}
|
|
1017
|
+
const hasPositiveCustomerIdMatch = Boolean(evCustomerId && subCus && subCus === evCustomerId);
|
|
1018
|
+
const hasPositiveEmailMatch = Boolean(evEmail && subEmail && subEmail === evEmail);
|
|
1019
|
+
if (!hasPositiveCustomerIdMatch && !hasPositiveEmailMatch) {
|
|
1020
|
+
return {
|
|
1021
|
+
ok: true,
|
|
1022
|
+
executionMode: "local_open_source_engine",
|
|
1023
|
+
isGhostUsage: false,
|
|
1024
|
+
decision: "HOLD_CUSTOMER_MISMATCH",
|
|
1025
|
+
reason: subCus ? "UNMATCHED_CUSTOMER_ID" : "UNMATCHED_CUSTOMER_EMAIL",
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
952
1028
|
const st = sub.status.trim();
|
|
953
1029
|
if (ENTITLED_SUB_STATUSES.has(st)) {
|
|
1030
|
+
const collectionPaused = Boolean(
|
|
1031
|
+
sub.pause_collection && typeof sub.pause_collection === "object"
|
|
1032
|
+
);
|
|
954
1033
|
return {
|
|
955
1034
|
ok: true,
|
|
956
1035
|
executionMode: "local_open_source_engine",
|
|
957
1036
|
isGhostUsage: false,
|
|
958
1037
|
decision: "ALLOW_ENTITLED",
|
|
959
1038
|
reason: "ACTIVE_SUBSCRIPTION_PRESENT",
|
|
1039
|
+
...(collectionPaused
|
|
1040
|
+
? {
|
|
1041
|
+
collectionPaused: true,
|
|
1042
|
+
pauseCollectionBehavior:
|
|
1043
|
+
typeof sub.pause_collection.behavior === "string"
|
|
1044
|
+
? sub.pause_collection.behavior
|
|
1045
|
+
: "void",
|
|
1046
|
+
}
|
|
1047
|
+
: {}),
|
|
960
1048
|
};
|
|
961
1049
|
}
|
|
962
1050
|
if (sub.items?.has_more === true) {
|
|
@@ -1093,7 +1181,36 @@ function parseStrictIpv4Octets(str) {
|
|
|
1093
1181
|
|
|
1094
1182
|
function normalizeIpForCe3Correlation(ip) {
|
|
1095
1183
|
if (typeof ip !== "string") return null;
|
|
1096
|
-
|
|
1184
|
+
let raw = ip.trim().toLowerCase();
|
|
1185
|
+
if (!raw || raw.includes(",")) return null;
|
|
1186
|
+
if (raw.startsWith("ipv4:")) {
|
|
1187
|
+
raw = raw.slice(5).trim();
|
|
1188
|
+
} else if (raw.startsWith("ipv6_64:")) {
|
|
1189
|
+
const v6Match = raw.match(
|
|
1190
|
+
/^ipv6_64:([0-9a-f]{1,4}):([0-9a-f]{1,4}):([0-9a-f]{1,4}):([0-9a-f]{1,4})$/
|
|
1191
|
+
);
|
|
1192
|
+
if (!v6Match) return null;
|
|
1193
|
+
return `ipv6_64:${v6Match
|
|
1194
|
+
.slice(1, 5)
|
|
1195
|
+
.map((w) => Number.parseInt(w, 16).toString(16).padStart(4, "0"))
|
|
1196
|
+
.join(":")}`;
|
|
1197
|
+
}
|
|
1198
|
+
const bracketPortMatch = raw.match(/^\[([^\]]+)\](?::(\d{1,5}))?$/);
|
|
1199
|
+
if (bracketPortMatch) {
|
|
1200
|
+
if (bracketPortMatch[2] !== undefined) {
|
|
1201
|
+
const portNum = Number(bracketPortMatch[2]);
|
|
1202
|
+
if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) return null;
|
|
1203
|
+
}
|
|
1204
|
+
raw = bracketPortMatch[1];
|
|
1205
|
+
} else {
|
|
1206
|
+
const v4PortMatch = raw.match(/^(\d{1,3}(?:\.\d{1,3}){3}):(\d{1,5})$/);
|
|
1207
|
+
if (v4PortMatch) {
|
|
1208
|
+
const portNum = Number(v4PortMatch[2]);
|
|
1209
|
+
if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) return null;
|
|
1210
|
+
raw = v4PortMatch[1];
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
const clean = raw.replace(/^\[|\]$/g, "").replace(/%[^\s%\]]+$/, "").trim();
|
|
1097
1214
|
if (!clean) return null;
|
|
1098
1215
|
const directV4 = parseStrictIpv4Octets(clean);
|
|
1099
1216
|
if (directV4) return `ipv4:${directV4.join(".")}`;
|
|
@@ -1224,14 +1341,30 @@ function extractNormalizedCe3Identifiers(tx) {
|
|
|
1224
1341
|
|
|
1225
1342
|
function extractPaymentCredential(tx) {
|
|
1226
1343
|
if (!tx || typeof tx !== "object") return "";
|
|
1227
|
-
const
|
|
1228
|
-
tx.card_fingerprint
|
|
1229
|
-
tx.payment_method_fingerprint
|
|
1230
|
-
tx.
|
|
1231
|
-
tx.
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1344
|
+
const fpCandidates = [
|
|
1345
|
+
tx.card_fingerprint,
|
|
1346
|
+
tx.payment_method_fingerprint,
|
|
1347
|
+
tx.payment_method_details?.card?.fingerprint,
|
|
1348
|
+
tx.payment_method && typeof tx.payment_method === "object"
|
|
1349
|
+
? tx.payment_method.card?.fingerprint
|
|
1350
|
+
: "",
|
|
1351
|
+
tx.card?.fingerprint,
|
|
1352
|
+
tx.source?.card?.fingerprint,
|
|
1353
|
+
];
|
|
1354
|
+
for (const cand of fpCandidates) {
|
|
1355
|
+
if (typeof cand === "string" && cand.trim()) {
|
|
1356
|
+
return sanitizeLogToken(cand.trim(), 96);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
const pmId =
|
|
1360
|
+
typeof tx.payment_method === "string"
|
|
1361
|
+
? tx.payment_method
|
|
1362
|
+
: tx.payment_method &&
|
|
1363
|
+
typeof tx.payment_method === "object" &&
|
|
1364
|
+
typeof tx.payment_method.id === "string"
|
|
1365
|
+
? tx.payment_method.id
|
|
1366
|
+
: "";
|
|
1367
|
+
return sanitizeLogToken(pmId, 96);
|
|
1235
1368
|
}
|
|
1236
1369
|
|
|
1237
1370
|
function evaluateLocalVisaCe3Qualification(disputeReason, disputedTransaction, priorTransactions) {
|
|
@@ -1372,7 +1505,34 @@ function evaluateLocalVisaCe3Qualification(disputeReason, disputedTransaction, p
|
|
|
1372
1505
|
};
|
|
1373
1506
|
}
|
|
1374
1507
|
|
|
1375
|
-
export
|
|
1508
|
+
export const LOCAL_STATELESS_CLOUD_UPGRADE_HINT = Object.freeze({
|
|
1509
|
+
mode: "LOCAL_STATELESS_OPEN_CORE",
|
|
1510
|
+
message:
|
|
1511
|
+
"Running in stateless local mode. Provision a free or paid Cloud Workspace for 90-day event persistence, automated webhook revocation, and Slack/Email alerts.",
|
|
1512
|
+
command: "npx --yes revclamp-mcp --init-workspace <work_email>",
|
|
1513
|
+
cliActivateCommand: "npx -y revclamp-mcp@1.20.11 --init-workspace you@company.com",
|
|
1514
|
+
upgradeUrl: "https://revclamp.com/pricing?source=mcp_cli",
|
|
1515
|
+
});
|
|
1516
|
+
|
|
1517
|
+
export function executeLocalMcpTool(toolName, rawArgs = {}, options = {}) {
|
|
1518
|
+
const result = executeLocalMcpToolCore(toolName, rawArgs);
|
|
1519
|
+
const suppressHint = Boolean(options?.suppressCloudUpgradeHint || options?.apiKey);
|
|
1520
|
+
if (
|
|
1521
|
+
!suppressHint &&
|
|
1522
|
+
result &&
|
|
1523
|
+
typeof result === "object" &&
|
|
1524
|
+
result.ok !== false &&
|
|
1525
|
+
!result.error
|
|
1526
|
+
) {
|
|
1527
|
+
return {
|
|
1528
|
+
...result,
|
|
1529
|
+
cloudUpgradeHint: LOCAL_STATELESS_CLOUD_UPGRADE_HINT,
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
return result;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
function executeLocalMcpToolCore(toolName, rawArgs = {}) {
|
|
1376
1536
|
const inputArgs =
|
|
1377
1537
|
rawArgs === undefined
|
|
1378
1538
|
? {}
|
|
@@ -1988,11 +2148,23 @@ export function executeLocalMcpTool(toolName, rawArgs = {}) {
|
|
|
1988
2148
|
const malformedExistingMarkers = Boolean(!strippedNotes.ok);
|
|
1989
2149
|
const warnings = malformedExistingMarkers ? ["MALFORMED_EXISTING_MARKERS"] : [];
|
|
1990
2150
|
const trimmedExistingNotes = existingMerchantNotes.trim();
|
|
2151
|
+
const normalizedTrimmedExisting = trimmedExistingNotes.replace(/\r?\n+/g, "\n");
|
|
1991
2152
|
const outsideMarkerText = `${strippedNotes.prefix || ""}\n${strippedNotes.suffix || ""}`;
|
|
2153
|
+
const outsideJoinedSingle = [strippedNotes.prefix || "", strippedNotes.suffix || ""]
|
|
2154
|
+
.filter(Boolean)
|
|
2155
|
+
.join("\n");
|
|
2156
|
+
const outsideJoinedDouble = [strippedNotes.prefix || "", strippedNotes.suffix || ""]
|
|
2157
|
+
.filter(Boolean)
|
|
2158
|
+
.join("\n\n");
|
|
2159
|
+
const outsideNormalizedNewlines = outsideJoinedSingle.replace(/\r?\n+/g, "\n");
|
|
1992
2160
|
const preservesMerchantNotes = Boolean(
|
|
1993
2161
|
nonDestructiveMarkersPresent &&
|
|
1994
2162
|
strippedNotes.text.length > 0 &&
|
|
1995
|
-
(trimmedExistingNotes === "" ||
|
|
2163
|
+
(trimmedExistingNotes === "" ||
|
|
2164
|
+
outsideMarkerText.includes(trimmedExistingNotes) ||
|
|
2165
|
+
outsideJoinedSingle.includes(trimmedExistingNotes) ||
|
|
2166
|
+
outsideJoinedDouble.includes(trimmedExistingNotes) ||
|
|
2167
|
+
outsideNormalizedNewlines.includes(normalizedTrimmedExisting))
|
|
1996
2168
|
);
|
|
1997
2169
|
|
|
1998
2170
|
const extractedNotesLen =
|
|
@@ -2004,12 +2176,10 @@ export function executeLocalMcpTool(toolName, rawArgs = {}) {
|
|
|
2004
2176
|
const preSeparatorUnits = capacityPrefix.length > 0 ? 2 : 0;
|
|
2005
2177
|
const postSeparatorUnits = capacitySuffix.length > 0 ? 2 : 0;
|
|
2006
2178
|
const merchantNotesUtf16CodeUnits = capacityPrefix.length + capacitySuffix.length;
|
|
2179
|
+
const wrapperOverheadUtf16CodeUnits =
|
|
2180
|
+
preSeparatorUnits + PRODUCTION_EMPTY_BLOCK_WRAPPER_CHARS + postSeparatorUnits;
|
|
2007
2181
|
const fixedWrapperUtf16CodeUnits =
|
|
2008
|
-
capacityPrefix.length +
|
|
2009
|
-
preSeparatorUnits +
|
|
2010
|
-
PRODUCTION_EMPTY_BLOCK_WRAPPER_CHARS +
|
|
2011
|
-
postSeparatorUnits +
|
|
2012
|
-
capacitySuffix.length;
|
|
2182
|
+
capacityPrefix.length + wrapperOverheadUtf16CodeUnits + capacitySuffix.length;
|
|
2013
2183
|
const merchantLogAtCapacity = Boolean(
|
|
2014
2184
|
fixedWrapperUtf16CodeUnits >= MAX_ACCESS_ACTIVITY_LOG_CHARS
|
|
2015
2185
|
);
|
|
@@ -2027,6 +2197,20 @@ export function executeLocalMcpTool(toolName, rawArgs = {}) {
|
|
|
2027
2197
|
const within18kCeiling = utf16CodeUnits <= MAX_ACCESS_ACTIVITY_LOG_CHARS;
|
|
2028
2198
|
const within20kHardCeiling = utf16CodeUnits <= 20_000;
|
|
2029
2199
|
const exceeds20kHardCeiling = utf16CodeUnits > 20_000;
|
|
2200
|
+
const segmentBreakdown = {
|
|
2201
|
+
prefixUtf16CodeUnits: capacityPrefix.length,
|
|
2202
|
+
preSeparatorUtf16CodeUnits: preSeparatorUnits,
|
|
2203
|
+
revClampBlockUtf16CodeUnits: strippedNotes.blockUtf16CodeUnits || 0,
|
|
2204
|
+
postSeparatorUtf16CodeUnits: postSeparatorUnits,
|
|
2205
|
+
suffixUtf16CodeUnits: capacitySuffix.length,
|
|
2206
|
+
wrapperOverheadUtf16CodeUnits,
|
|
2207
|
+
totalMergedUtf16CodeUnits: utf16CodeUnits,
|
|
2208
|
+
remainingSafeBudgetUtf16CodeUnits: remainingUtf16Units,
|
|
2209
|
+
overBudgetByUtf16CodeUnits: Math.max(
|
|
2210
|
+
0,
|
|
2211
|
+
utf16CodeUnits - MAX_ACCESS_ACTIVITY_LOG_CHARS
|
|
2212
|
+
),
|
|
2213
|
+
};
|
|
2030
2214
|
let overallVerdict = "PASS_CE3_READY";
|
|
2031
2215
|
if (!within18kCeiling) {
|
|
2032
2216
|
overallVerdict = "FAIL_UTF16_OVERFLOW";
|
|
@@ -2074,6 +2258,7 @@ export function executeLocalMcpTool(toolName, rawArgs = {}) {
|
|
|
2074
2258
|
merchantNotesPreserved: preservesMerchantNotes,
|
|
2075
2259
|
merchantNotesUtf16CodeUnits,
|
|
2076
2260
|
fixedWrapperUtf16CodeUnits,
|
|
2261
|
+
segmentBreakdown,
|
|
2077
2262
|
merchantLogAtCapacity,
|
|
2078
2263
|
stagingActionWhenAtCapacity,
|
|
2079
2264
|
priorMatchedCount,
|
|
@@ -2416,7 +2601,18 @@ export function executeLocalMcpTool(toolName, rawArgs = {}) {
|
|
|
2416
2601
|
};
|
|
2417
2602
|
}
|
|
2418
2603
|
|
|
2419
|
-
|
|
2604
|
+
const TRANSIENT_SIM_MODES = new Set([
|
|
2605
|
+
"MS_UNAVAILABLE",
|
|
2606
|
+
"GLOBAL_MAX_CONCURRENT_REQ",
|
|
2607
|
+
"MS_MAX_CONCURRENT_REQ",
|
|
2608
|
+
"MS_MAX_CONCURRENT_REQ_TIME",
|
|
2609
|
+
"GLOBAL_MAX_CONCURRENT_REQ_TIME",
|
|
2610
|
+
"SERVICE_UNAVAILABLE",
|
|
2611
|
+
"TIMEOUT",
|
|
2612
|
+
"SERVER_BUSY",
|
|
2613
|
+
"NETWORK_ERROR",
|
|
2614
|
+
]);
|
|
2615
|
+
if (TRANSIENT_SIM_MODES.has(viesMode)) {
|
|
2420
2616
|
return {
|
|
2421
2617
|
ok: true,
|
|
2422
2618
|
executionMode: "local_open_source_engine",
|
|
@@ -2434,7 +2630,25 @@ export function executeLocalMcpTool(toolName, rawArgs = {}) {
|
|
|
2434
2630
|
};
|
|
2435
2631
|
}
|
|
2436
2632
|
|
|
2437
|
-
if (viesMode === "
|
|
2633
|
+
if (viesMode === "INVALID_REQUESTER_INFO" || viesMode === "IP_BLOCKED") {
|
|
2634
|
+
return {
|
|
2635
|
+
ok: true,
|
|
2636
|
+
executionMode: "local_open_source_engine",
|
|
2637
|
+
sample: true,
|
|
2638
|
+
valid: null,
|
|
2639
|
+
formatValid: true,
|
|
2640
|
+
status: "HOLD_UNVERIFIED_VIES_FAULT",
|
|
2641
|
+
reason: `VIES_GATEWAY_FAULT_${viesMode}`,
|
|
2642
|
+
sellerCountry,
|
|
2643
|
+
normalizedVatId: norm.fullVatId,
|
|
2644
|
+
memberState: norm.memberState,
|
|
2645
|
+
reverseChargeEligible: false,
|
|
2646
|
+
zeroRateAllowed: false,
|
|
2647
|
+
consultationId: null,
|
|
2648
|
+
};
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
if (viesMode === "INVALID" || viesMode === "VAT_BLOCKED" || viesMode === "INVALID_INPUT") {
|
|
2438
2652
|
return {
|
|
2439
2653
|
ok: true,
|
|
2440
2654
|
executionMode: "local_open_source_engine",
|
|
@@ -2442,7 +2656,25 @@ export function executeLocalMcpTool(toolName, rawArgs = {}) {
|
|
|
2442
2656
|
valid: false,
|
|
2443
2657
|
formatValid: true,
|
|
2444
2658
|
status: "HOLD_INVALID_VAT",
|
|
2445
|
-
reason: "VIES_DEFINITE_INVALID"
|
|
2659
|
+
reason: viesMode === "INVALID" ? "VIES_DEFINITE_INVALID" : `VIES_${viesMode}`,
|
|
2660
|
+
sellerCountry,
|
|
2661
|
+
normalizedVatId: norm.fullVatId,
|
|
2662
|
+
memberState: norm.memberState,
|
|
2663
|
+
reverseChargeEligible: false,
|
|
2664
|
+
zeroRateAllowed: false,
|
|
2665
|
+
consultationId: null,
|
|
2666
|
+
};
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
if (viesMode !== "VALID" && viesMode !== "VERIFIED") {
|
|
2670
|
+
return {
|
|
2671
|
+
ok: true,
|
|
2672
|
+
executionMode: "local_open_source_engine",
|
|
2673
|
+
sample: true,
|
|
2674
|
+
valid: null,
|
|
2675
|
+
formatValid: true,
|
|
2676
|
+
status: "HOLD_UNVERIFIED_VIES_FAULT",
|
|
2677
|
+
reason: "UNRECOGNIZED_VIES_SIMULATION_MODE",
|
|
2446
2678
|
sellerCountry,
|
|
2447
2679
|
normalizedVatId: norm.fullVatId,
|
|
2448
2680
|
memberState: norm.memberState,
|
|
@@ -2559,20 +2791,21 @@ export function executeLocalMcpTool(toolName, rawArgs = {}) {
|
|
|
2559
2791
|
|
|
2560
2792
|
const effectiveTotalCents = Math.max(0, orderTotalCents - creditNotesCents);
|
|
2561
2793
|
const hasExplicitDepositCents = args.requiredDepositCents !== undefined;
|
|
2562
|
-
const
|
|
2794
|
+
const rawRequiredDepositCents = hasExplicitDepositCents
|
|
2563
2795
|
? args.requiredDepositCents
|
|
2564
2796
|
: Math.ceil((effectiveTotalCents * requiredDepositPercent) / 100);
|
|
2565
2797
|
if (
|
|
2566
|
-
!Number.isSafeInteger(
|
|
2567
|
-
|
|
2568
|
-
|
|
2798
|
+
!Number.isSafeInteger(rawRequiredDepositCents) ||
|
|
2799
|
+
rawRequiredDepositCents < 0 ||
|
|
2800
|
+
rawRequiredDepositCents > orderTotalCents
|
|
2569
2801
|
) {
|
|
2570
2802
|
const err = new Error(
|
|
2571
|
-
"INVALID_REQUIRED_DEPOSIT_CENTS: requiredDepositCents must be an integer between 0 and
|
|
2803
|
+
"INVALID_REQUIRED_DEPOSIT_CENTS: requiredDepositCents must be an integer between 0 and totalOrderCents."
|
|
2572
2804
|
);
|
|
2573
2805
|
err.code = -32602;
|
|
2574
2806
|
throw err;
|
|
2575
2807
|
}
|
|
2808
|
+
const requiredDepositCents = Math.min(rawRequiredDepositCents, effectiveTotalCents);
|
|
2576
2809
|
|
|
2577
2810
|
const shortfallRawCents = Math.max(0, effectiveTotalCents - paidAmountCents);
|
|
2578
2811
|
const overpaidCents = Math.max(0, paidAmountCents - effectiveTotalCents);
|
|
@@ -2898,7 +3131,9 @@ export function executeLocalMcpTool(toolName, rawArgs = {}) {
|
|
|
2898
3131
|
? args.existingAccessActivityLog
|
|
2899
3132
|
: typeof args.existingUncategorizedText === "string"
|
|
2900
3133
|
? args.existingUncategorizedText
|
|
2901
|
-
: ""
|
|
3134
|
+
: typeof args.existingMerchantNotes === "string"
|
|
3135
|
+
? args.existingMerchantNotes
|
|
3136
|
+
: "";
|
|
2902
3137
|
const strippedNotes = stripExistingRevClampBlock(rawExistingLog);
|
|
2903
3138
|
if (!strippedNotes.ok) {
|
|
2904
3139
|
const rawUtf8Bytes = Buffer.byteLength(rawExistingLog, "utf8");
|
|
@@ -2919,25 +3154,40 @@ export function executeLocalMcpTool(toolName, rawArgs = {}) {
|
|
|
2919
3154
|
},
|
|
2920
3155
|
};
|
|
2921
3156
|
}
|
|
2922
|
-
const
|
|
3157
|
+
const merchantPrefixLog = strippedNotes.prefix || "";
|
|
3158
|
+
const merchantSuffixLog = strippedNotes.suffix || "";
|
|
3159
|
+
const totalMerchantChars = merchantPrefixLog.length + merchantSuffixLog.length;
|
|
3160
|
+
const preSeparator = merchantPrefixLog.length > 0 ? "\n\n" : "";
|
|
3161
|
+
const postSeparator = merchantSuffixLog.length > 0 ? "\n\n" : "";
|
|
2923
3162
|
const headerLine = isDemo
|
|
2924
3163
|
? `[DEMO SAMPLE - NOT FOR DISPUTE SUBMISSION | dispute=${cleanDisputeId} | customer=${cleanCustomerId} | submit=false]`
|
|
2925
3164
|
: `[RevClamp Verified Product Usage Log | dispute=${cleanDisputeId} | customer=${cleanCustomerId} | submit=false]`;
|
|
2926
3165
|
|
|
2927
|
-
const
|
|
3166
|
+
const wrapperOverheadUtf16CodeUnits =
|
|
3167
|
+
preSeparator.length +
|
|
3168
|
+
REVCLAMP_START_MARKER.length +
|
|
3169
|
+
1 +
|
|
3170
|
+
REVCLAMP_END_MARKER.length +
|
|
3171
|
+
postSeparator.length;
|
|
3172
|
+
const fixedWrapperChars =
|
|
3173
|
+
merchantPrefixLog.length + wrapperOverheadUtf16CodeUnits + merchantSuffixLog.length;
|
|
2928
3174
|
const fixedEnvelopeChars =
|
|
2929
|
-
|
|
3175
|
+
merchantPrefixLog.length +
|
|
3176
|
+
preSeparator.length +
|
|
2930
3177
|
REVCLAMP_START_MARKER.length +
|
|
2931
3178
|
1 +
|
|
2932
3179
|
headerLine.length +
|
|
2933
3180
|
1 +
|
|
2934
|
-
REVCLAMP_END_MARKER.length
|
|
3181
|
+
REVCLAMP_END_MARKER.length +
|
|
3182
|
+
postSeparator.length +
|
|
3183
|
+
merchantSuffixLog.length;
|
|
2935
3184
|
const fixedEnvelopeUtf8Bytes = Buffer.byteLength(
|
|
2936
|
-
`${
|
|
3185
|
+
`${merchantPrefixLog}${preSeparator}${REVCLAMP_START_MARKER}\n${headerLine}\n${REVCLAMP_END_MARKER}${postSeparator}${merchantSuffixLog}`,
|
|
2937
3186
|
"utf8"
|
|
2938
3187
|
);
|
|
2939
3188
|
|
|
2940
3189
|
if (
|
|
3190
|
+
fixedWrapperChars >= MAX_ACCESS_ACTIVITY_LOG_CHARS ||
|
|
2941
3191
|
fixedEnvelopeChars > MAX_ACCESS_ACTIVITY_LOG_CHARS ||
|
|
2942
3192
|
fixedEnvelopeUtf8Bytes > MAX_LOG_UTF8_BYTES
|
|
2943
3193
|
) {
|
|
@@ -2949,6 +3199,23 @@ export function executeLocalMcpTool(toolName, rawArgs = {}) {
|
|
|
2949
3199
|
skipped: true,
|
|
2950
3200
|
reason: "EXISTING_MERCHANT_NOTES_EXCEED_18K_CEILING",
|
|
2951
3201
|
submit: false,
|
|
3202
|
+
segmentBreakdown: {
|
|
3203
|
+
prefixUtf16CodeUnits: merchantPrefixLog.length,
|
|
3204
|
+
preSeparatorUtf16CodeUnits: preSeparator.length,
|
|
3205
|
+
revClampBlockUtf16CodeUnits: 0,
|
|
3206
|
+
postSeparatorUtf16CodeUnits: postSeparator.length,
|
|
3207
|
+
suffixUtf16CodeUnits: merchantSuffixLog.length,
|
|
3208
|
+
wrapperOverheadUtf16CodeUnits,
|
|
3209
|
+
totalMergedUtf16CodeUnits: totalMerchantChars,
|
|
3210
|
+
remainingSafeBudgetUtf16CodeUnits: Math.max(
|
|
3211
|
+
0,
|
|
3212
|
+
MAX_ACCESS_ACTIVITY_LOG_CHARS - totalMerchantChars
|
|
3213
|
+
),
|
|
3214
|
+
overBudgetByUtf16CodeUnits: Math.max(
|
|
3215
|
+
0,
|
|
3216
|
+
fixedEnvelopeChars - MAX_ACCESS_ACTIVITY_LOG_CHARS
|
|
3217
|
+
),
|
|
3218
|
+
},
|
|
2952
3219
|
evidence: {},
|
|
2953
3220
|
};
|
|
2954
3221
|
}
|
|
@@ -2969,6 +3236,36 @@ export function executeLocalMcpTool(toolName, rawArgs = {}) {
|
|
|
2969
3236
|
currentUtf8Bytes += lineUtf8Bytes;
|
|
2970
3237
|
}
|
|
2971
3238
|
|
|
3239
|
+
if (packedLines.length === 0 && formattedLines.length > 0) {
|
|
3240
|
+
return {
|
|
3241
|
+
ok: true,
|
|
3242
|
+
executionMode: "local_open_source_engine",
|
|
3243
|
+
disputeId: cleanDisputeId,
|
|
3244
|
+
customerId: cleanCustomerId,
|
|
3245
|
+
skipped: true,
|
|
3246
|
+
reason: "EXISTING_MERCHANT_NOTES_EXCEED_18K_CEILING",
|
|
3247
|
+
submit: false,
|
|
3248
|
+
segmentBreakdown: {
|
|
3249
|
+
prefixUtf16CodeUnits: merchantPrefixLog.length,
|
|
3250
|
+
preSeparatorUtf16CodeUnits: preSeparator.length,
|
|
3251
|
+
revClampBlockUtf16CodeUnits: 0,
|
|
3252
|
+
postSeparatorUtf16CodeUnits: postSeparator.length,
|
|
3253
|
+
suffixUtf16CodeUnits: merchantSuffixLog.length,
|
|
3254
|
+
wrapperOverheadUtf16CodeUnits,
|
|
3255
|
+
totalMergedUtf16CodeUnits: totalMerchantChars,
|
|
3256
|
+
remainingSafeBudgetUtf16CodeUnits: Math.max(
|
|
3257
|
+
0,
|
|
3258
|
+
MAX_ACCESS_ACTIVITY_LOG_CHARS - totalMerchantChars
|
|
3259
|
+
),
|
|
3260
|
+
overBudgetByUtf16CodeUnits: Math.max(
|
|
3261
|
+
0,
|
|
3262
|
+
fixedEnvelopeChars + 1 + formattedLines[0].length - MAX_ACCESS_ACTIVITY_LOG_CHARS
|
|
3263
|
+
),
|
|
3264
|
+
},
|
|
3265
|
+
evidence: {},
|
|
3266
|
+
};
|
|
3267
|
+
}
|
|
3268
|
+
|
|
2972
3269
|
const block = [
|
|
2973
3270
|
REVCLAMP_START_MARKER,
|
|
2974
3271
|
headerLine,
|
|
@@ -2976,7 +3273,7 @@ export function executeLocalMcpTool(toolName, rawArgs = {}) {
|
|
|
2976
3273
|
REVCLAMP_END_MARKER,
|
|
2977
3274
|
].join("\n");
|
|
2978
3275
|
|
|
2979
|
-
const merged = `${
|
|
3276
|
+
const merged = `${merchantPrefixLog}${preSeparator}${block}${postSeparator}${merchantSuffixLog}`;
|
|
2980
3277
|
const ce3Qualification = evaluateLocalVisaCe3Qualification(
|
|
2981
3278
|
disputeReason,
|
|
2982
3279
|
args.disputedTransaction,
|
|
@@ -2994,6 +3291,20 @@ export function executeLocalMcpTool(toolName, rawArgs = {}) {
|
|
|
2994
3291
|
submit: false,
|
|
2995
3292
|
utf16CodeUnits: merged.length,
|
|
2996
3293
|
linesIncluded: packedLines.length,
|
|
3294
|
+
segmentBreakdown: {
|
|
3295
|
+
prefixUtf16CodeUnits: merchantPrefixLog.length,
|
|
3296
|
+
preSeparatorUtf16CodeUnits: preSeparator.length,
|
|
3297
|
+
revClampBlockUtf16CodeUnits: block.length,
|
|
3298
|
+
postSeparatorUtf16CodeUnits: postSeparator.length,
|
|
3299
|
+
suffixUtf16CodeUnits: merchantSuffixLog.length,
|
|
3300
|
+
wrapperOverheadUtf16CodeUnits,
|
|
3301
|
+
totalMergedUtf16CodeUnits: merged.length,
|
|
3302
|
+
remainingSafeBudgetUtf16CodeUnits: Math.max(
|
|
3303
|
+
0,
|
|
3304
|
+
MAX_ACCESS_ACTIVITY_LOG_CHARS - merged.length
|
|
3305
|
+
),
|
|
3306
|
+
overBudgetByUtf16CodeUnits: Math.max(0, merged.length - MAX_ACCESS_ACTIVITY_LOG_CHARS),
|
|
3307
|
+
},
|
|
2997
3308
|
evidence: {
|
|
2998
3309
|
access_activity_log: merged,
|
|
2999
3310
|
},
|
|
@@ -3150,7 +3461,11 @@ function toSafeLocalRpcId(rawId) {
|
|
|
3150
3461
|
return null;
|
|
3151
3462
|
}
|
|
3152
3463
|
|
|
3153
|
-
export function executeLocalMcpJsonRpc(
|
|
3464
|
+
export function executeLocalMcpJsonRpc(
|
|
3465
|
+
rpcRequest,
|
|
3466
|
+
packageVersion = "1.20.11",
|
|
3467
|
+
options = {}
|
|
3468
|
+
) {
|
|
3154
3469
|
if (!rpcRequest || typeof rpcRequest !== "object" || Array.isArray(rpcRequest)) {
|
|
3155
3470
|
return {
|
|
3156
3471
|
jsonrpc: "2.0",
|
|
@@ -3264,7 +3579,7 @@ export function executeLocalMcpJsonRpc(rpcRequest, packageVersion = "1.20.9") {
|
|
|
3264
3579
|
version: packageVersion,
|
|
3265
3580
|
localExecution: "9 deterministic tools in local_engine.mjs (0 network calls by default)",
|
|
3266
3581
|
cloudCapabilities: [
|
|
3267
|
-
"90-day
|
|
3582
|
+
"90-day durable transactional usage & audit ledger",
|
|
3268
3583
|
"24/7 autonomous Stripe dispute & entitlement webhook outbox",
|
|
3269
3584
|
"Live EU Commission VIES SOAP consultation numbers (viesMode: 'LIVE')",
|
|
3270
3585
|
],
|
|
@@ -3434,7 +3749,7 @@ export function executeLocalMcpJsonRpc(rpcRequest, packageVersion = "1.20.9") {
|
|
|
3434
3749
|
}
|
|
3435
3750
|
|
|
3436
3751
|
try {
|
|
3437
|
-
const structuredContent = executeLocalMcpTool(toolName, effectiveToolArgs);
|
|
3752
|
+
const structuredContent = executeLocalMcpTool(toolName, effectiveToolArgs, options);
|
|
3438
3753
|
let responseObj = {
|
|
3439
3754
|
jsonrpc: "2.0",
|
|
3440
3755
|
id: safeId,
|
package/mcp-config.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "revclamp-mcp",
|
|
3
|
-
"mcpName": "
|
|
4
|
-
"version": "1.20.
|
|
3
|
+
"mcpName": "com.revclamp/revclamp-mcp",
|
|
4
|
+
"version": "1.20.11",
|
|
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",
|
|
@@ -46,6 +46,6 @@
|
|
|
46
46
|
"node": ">=20.0.0"
|
|
47
47
|
},
|
|
48
48
|
"revclamp": {
|
|
49
|
-
"engineSha256": "
|
|
49
|
+
"engineSha256": "806f9291517805e75ce55636ad1e704e30c1b2a840bd1162c87ba7e92bd9a053"
|
|
50
50
|
}
|
|
51
51
|
}
|
package/server.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
-
"name": "
|
|
3
|
+
"name": "com.revclamp/revclamp-mcp",
|
|
4
4
|
"title": "RevClamp MCP Server",
|
|
5
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.11",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
15
15
|
"identifier": "revclamp-mcp",
|
|
16
|
-
"version": "1.20.
|
|
16
|
+
"version": "1.20.11",
|
|
17
17
|
"transport": {
|
|
18
18
|
"type": "stdio"
|
|
19
19
|
},
|