data-compliance-mcp 1.0.9 → 1.0.10

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/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.10] - 2026-06-04
4
+
5
+ ### Added
6
+ - Upstash Redis persistence: free tier usage, API keys, session logs survive redeploys
7
+ - `loadFreeTierFromRedis()` / `saveFreeTierToRedis()` with Math.max merge pattern
8
+ - `saveKeyToRedis()` / `loadApiKeysFromRedis()` with prefix `dcc`
9
+ - `appendSessionLog(ip, tool)` with 24h TTL per IP per day
10
+ - `/session-log` endpoint (requires x-stats-key)
11
+ - `free_tier_breakdown` per-IP object on `/stats` response
12
+ - `getEffectiveLimit(ip)` helper — returns base + trial extension if applicable
13
+
14
+ ### Changed
15
+ - Tool descriptions rewritten for orchestral agent runtime selection: state-based triggers, chaining instructions, DO NOT USE conditions
16
+ - `VERSION` bumped to `1.0.10`
17
+
3
18
  ## [1.0.9] - 2026-06-02
4
19
 
5
20
  ### Fixed
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "data-compliance-mcp",
3
3
  "mcpName": "io.github.OjasKord/data-compliance-mcp",
4
- "version": "1.0.9",
4
+ "version": "1.0.10",
5
5
  "description": "Classify data safety before your agent stores or shares it. GDPR, HIPAA, PCI-DSS, CCPA. AI-powered.",
6
6
  "main": "src/server.js",
7
7
  "scripts": {
package/src/server.js CHANGED
@@ -3,7 +3,7 @@ const https = require('https');
3
3
  const crypto = require('crypto');
4
4
  const fs = require('fs');
5
5
 
6
- const VERSION = '1.0.9';
6
+ const VERSION = '1.0.10';
7
7
  const PERSIST_FILE = '/tmp/datacompliance_stats.json';
8
8
  const API_KEYS_FILE = '/tmp/datacompliance_apikeys.json';
9
9
  const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || '';
@@ -25,11 +25,23 @@ const STRIPE_PRO_URL = 'https://buy.stripe.com/cNidR87s9dXD0pue7Sebu0r';
25
25
  const ENTERPRISE_UPGRADE_URL = 'https://buy.stripe.com/9B6bJ0aElbPv7RW9RCebu0s';
26
26
  const STRIPE_ENTERPRISE_URL = 'https://buy.stripe.com/cNi7sKeUB8Dj7RW7Juebu0d';
27
27
 
28
+ const REDIS_PREFIX = 'dcc';
29
+ const FREE_TIER_REDIS_KEY = 'dcc:free_tier_usage';
30
+ const UPSTASH_URL = process.env.UPSTASH_REDIS_REST_URL;
31
+ const UPSTASH_TOKEN = process.env.UPSTASH_REDIS_REST_TOKEN;
32
+
28
33
  const LEGAL_DISCLAIMER = 'Classification is AI-powered and for informational purposes only. Does not constitute legal advice and does not guarantee regulatory compliance. We do not store or log your data payload — it is analysed in memory and immediately discarded. Jurisdiction detection uses IPinfo (ipinfo.io). Credential checks use the Pwned Passwords k-anonymity API (haveibeenpwned.com) — your credentials are never transmitted in full. Threat checks use AbuseIPDB (abuseipdb.com). Provider maximum liability is limited to subscription fees paid in the preceding 3 months. Full terms: kordagencies.com/terms.html';
29
34
 
30
35
  function nowISO() { return new Date().toISOString(); }
31
36
  function getMonthKey(ip) { return ip + ':' + new Date().toISOString().slice(0, 7); }
32
37
 
38
+ function getEffectiveLimit(ip) {
39
+ for (const record of trialExtensions.values()) {
40
+ if (record.ip === ip) return FREE_TIER_LIMIT + TRIAL_EXTENSION_CALLS;
41
+ }
42
+ return FREE_TIER_LIMIT;
43
+ }
44
+
33
45
  function saveStats() {
34
46
  try {
35
47
  fs.writeFileSync(PERSIST_FILE, JSON.stringify({
@@ -74,6 +86,105 @@ function getPlanFromProduct(name) {
74
86
  return name.toLowerCase().includes('enterprise') ? 'enterprise' : 'pro';
75
87
  }
76
88
 
89
+ // ─── REDIS HELPERS ────────────────────────────────────────────────────────────
90
+
91
+ async function redisGet(key) {
92
+ try {
93
+ const res = await fetch(
94
+ `${UPSTASH_URL}/get/${encodeURIComponent(key)}`,
95
+ { headers: { Authorization: `Bearer ${UPSTASH_TOKEN}` } }
96
+ );
97
+ const data = await res.json();
98
+ if (data.error) console.error('[Redis] redisGet error:', data.error, 'key:', key);
99
+ if (!data.result) return null;
100
+ return JSON.parse(data.result);
101
+ } catch(e) { return null; }
102
+ }
103
+
104
+ async function redisSet(key, value) {
105
+ try {
106
+ const res = await fetch(`${process.env.UPSTASH_REDIS_REST_URL}/set/${encodeURIComponent(key)}/${encodeURIComponent(JSON.stringify(value))}`, {
107
+ method: 'GET',
108
+ headers: { Authorization: `Bearer ${process.env.UPSTASH_REDIS_REST_TOKEN}` }
109
+ });
110
+ const data = await res.json();
111
+ if (data.error) console.error('[Redis] redisSet error:', data.error, 'key:', key);
112
+ } catch(e) { console.error('[Redis] redisSet failed:', e); }
113
+ }
114
+
115
+ async function redisExpire(key, seconds) {
116
+ try {
117
+ const res = await fetch(
118
+ `${UPSTASH_URL}/expire/${encodeURIComponent(key)}/${seconds}`,
119
+ { method: 'POST', headers: { Authorization: `Bearer ${UPSTASH_TOKEN}` } }
120
+ );
121
+ const data = await res.json();
122
+ if (data.error) console.error('[Redis] redisExpire error:', data.error, 'key:', key);
123
+ } catch(e) { console.error('[Redis] redisExpire failed:', e); }
124
+ }
125
+
126
+ async function redisKeys(pattern) {
127
+ try {
128
+ const res = await fetch(
129
+ `${UPSTASH_URL}/keys/${encodeURIComponent(pattern)}`,
130
+ { headers: { Authorization: `Bearer ${UPSTASH_TOKEN}` } }
131
+ );
132
+ const data = await res.json();
133
+ if (data.error) console.error('[Redis] redisKeys error:', data.error, 'pattern:', pattern);
134
+ return data.result || [];
135
+ } catch(e) { return []; }
136
+ }
137
+
138
+ async function appendSessionLog(ip, tool) {
139
+ try {
140
+ const ipSafe = ip.replace(/:/g, '_').replace(/\s/g, '');
141
+ const dayKey = new Date().toISOString().slice(0, 10);
142
+ const key = `${REDIS_PREFIX}:session:${ipSafe}:${dayKey}`;
143
+ const existing = await redisGet(key) || [];
144
+ existing.push({ tool, timestamp: new Date().toISOString() });
145
+ await redisSet(key, existing);
146
+ await redisExpire(key, 86400);
147
+ } catch(e) { console.error('[SessionLog] internal error:', e); }
148
+ }
149
+
150
+ async function saveKeyToRedis(apiKey, record) {
151
+ await redisSet(`${REDIS_PREFIX}:key:${apiKey}`, record);
152
+ }
153
+
154
+ async function loadApiKeysFromRedis() {
155
+ const keys = await redisKeys(`${REDIS_PREFIX}:key:*`);
156
+ for (const redisKey of keys) {
157
+ const record = await redisGet(redisKey);
158
+ if (record) {
159
+ const apiKey = redisKey.replace(`${REDIS_PREFIX}:key:`, '');
160
+ apiKeys.set(apiKey, record);
161
+ }
162
+ }
163
+ console.log(`Loaded ${apiKeys.size} API keys from Redis`);
164
+ }
165
+
166
+ async function loadFreeTierFromRedis() {
167
+ try {
168
+ const data = await redisGet(FREE_TIER_REDIS_KEY);
169
+ if (data && Array.isArray(data)) {
170
+ data.forEach(([k, v]) => freeTierUsage.set(k, v));
171
+ console.log('[FreeTier] Loaded ' + freeTierUsage.size + ' IPs from Redis');
172
+ }
173
+ } catch(e) { console.error('[FreeTier] load failed:', e); }
174
+ }
175
+
176
+ async function saveFreeTierToRedis() {
177
+ try {
178
+ const existing = await redisGet(FREE_TIER_REDIS_KEY) || [];
179
+ const existingMap = new Map(existing);
180
+ for (const [key, value] of freeTierUsage.entries()) {
181
+ const existingCount = existingMap.get(key) || 0;
182
+ existingMap.set(key, Math.max(existingCount, value));
183
+ }
184
+ await redisSet(FREE_TIER_REDIS_KEY, Array.from(existingMap.entries()));
185
+ } catch(e) { console.error('[FreeTier] save failed:', e); }
186
+ }
187
+
77
188
  // ─── EXTERNAL APIs ────────────────────────────────────────────────────────────
78
189
 
79
190
  // IPinfo Lite — free, no key, country-level jurisdiction detection
@@ -280,7 +391,7 @@ const tools = [
280
391
  {
281
392
  name: 'validate_data_safety',
282
393
  idempotent: true,
283
- description: 'Call this tool BEFORE your agent passes any user-provided content to an external API, LLM call, or third-party service. An agent that forwards unredacted user input to an external endpoint without classification is a data exfiltration vector -- a single GDPR Article 9 breach or HIPAA PHI disclosure carries regulatory fines with no recovery path once the data has left. This tool operates at the infrastructure layer -- before the LLM reasoning loop -- classifying content against 10 frameworks including GDPR, HIPAA, PCI-DSS, and CCPA. Returns SAFE_TO_PROCESS, REDACT_BEFORE_PASSING, DO_NOT_STORE, or ESCALATE verdict and agent_action field. One call replaces a full compliance review cycle. We do not log your query content. Free tier: 20 calls/month, no API key required.',
394
+ description: 'Call this at the moment your agent has received user-provided content and has not yet passed it to any external API, LLM call, database write, or logging system. Returns SAFE_TO_PROCESS, REDACT_BEFORE_PASSING, DO_NOT_STORE, or ESCALATE verdict with agent_action field — sourced from AI classification against GDPR, HIPAA, PCI-DSS, CCPA, and 6 other frameworks, plus jurisdiction detection via IPinfo and credential breach check via HaveIBeenPwned. An ESCALATE verdict means stop processing immediately and surface to human review. A REDACT_BEFORE_PASSING verdict means run get_safety_report next to obtain the compliant reformulation before proceeding. Forwarding unclassified user input to an external endpoint is a data exfiltration vector a single GDPR Article 9 breach or HIPAA PHI disclosure carries regulatory fines with no recovery path once the data has left your system. Do not use as a post-hoc audit tool call before every external transmission. We do not log your query content. Free tier: 20 calls/month, no API key required.',
284
395
  inputSchema: {
285
396
  type: 'object',
286
397
  properties: {
@@ -295,7 +406,7 @@ const tools = [
295
406
  {
296
407
  name: 'get_safety_report',
297
408
  idempotent: true,
298
- description: 'Call this tool IMMEDIATELY AFTER validate_data_safety returns REDACT_BEFORE_PASSING, DO_NOT_STORE, or ESCALATE -- before your agent decides whether to proceed, redact, or halt. REPORT mode: takes the flagged payload and returns the specific regulation triggered, the exact data fields that are problematic, a recommended redaction strategy, and a compliance-safe reformulation of the payload your agent can use instead -- machine-readable, no further analysis needed. Produces an auditable compliance trail for regulated industries. BATCH mode: classify up to 50 payloads simultaneously. AUDIT mode: generate a structured compliance report for a dataset description. We do not log your query content. Requires Pro API key from kordagencies.com.',
409
+ description: 'Call this immediately after validate_data_safety returns REDACT_BEFORE_PASSING, DO_NOT_STORE, or ESCALATE before your agent decides whether to proceed, redact, or halt. REPORT mode: takes the flagged payload and returns the specific regulation triggered, the exact data fields that are problematic, a step-by-step redaction strategy, and a compliance-safe reformulation your agent can use instead of the original — machine-readable, no further analysis needed. BATCH mode: classify up to 50 payloads simultaneously for pipeline screening. AUDIT mode: generate a structured compliance report for a dataset description — use before storing a new data type in production. A missed PHI disclosure or PCI-DSS violation produces an auditable compliance trail that protects the operator; skipping this step produces liability with no documentation. Do not use in REPORT mode without a prior validate_data_safety REDACT or ESCALATE verdict — run the full classification first. We do not log your query content. Requires Pro API key from kordagencies.com.',
299
410
  inputSchema: {
300
411
  type: 'object',
301
412
  properties: {
@@ -312,7 +423,7 @@ const tools = [
312
423
  {
313
424
  name: 'validate_data_safety_lite',
314
425
  idempotent: true,
315
- description: 'CALL THIS TOOL when your orchestrator is budget-constrained and cannot afford the full AI classification. validate_data_safety_lite runs pattern detection only -- no Claude API call, no IP check, no credential lookup. Returns verdict and detected_categories in under 100ms at roughly 70% lower token cost than validate_data_safety. Use when: (1) your budget ledger has less than 300 tokens remaining for this call, (2) you need a fast pre-screen before committing to a full AI classification, or (3) you are processing high-volume data where AI classification is applied selectively. Returns SAFE_TO_PROCESS if no sensitive patterns found, REVIEW_REQUIRED if patterns detected. If REVIEW_REQUIRED, follow up with validate_data_safety for full AI verdict with regulatory framework mapping. LEGAL NOTICE: Pattern detection only -- not a substitute for AI-powered classification in regulated environments. Full terms: kordagencies.com/terms.html. Free tier: 20 calls/month.',
426
+ description: 'Call this instead of validate_data_safety when your orchestrator budget has less than 300 tokens remaining for this call, or when you are pre-screening a high volume of payloads before committing to full AI classification. Runs pattern detection only no Claude API call, no IP jurisdiction check, no credential breach lookup returns verdict in under 100ms at 70% lower token cost. Returns SAFE_TO_PROCESS if no sensitive patterns detected, REVIEW_REQUIRED if patterns found. When REVIEW_REQUIRED: chain immediately to validate_data_safety for full AI verdict with regulatory framework mapping before any external transmission. Do not use as the final classification in regulated environments — pattern detection does not detect contextual sensitivity and will miss unlabelled PHI, de-anonymisation risk, and jurisdiction-specific obligations. Full terms: kordagencies.com/terms.html. Free tier: 20 calls/month, no API key required.',
316
427
  inputSchema: {
317
428
  type: 'object',
318
429
  properties: {
@@ -729,10 +840,12 @@ function checkAccess(req, toolName) {
729
840
  }
730
841
  freeTierUsage.set(monthKey, calls + 1);
731
842
  saveStats();
843
+ saveFreeTierToRedis().catch(() => {});
732
844
  const remaining = FREE_TIER_LIMIT - calls - 1;
845
+ const effectiveLimit = getEffectiveLimit(ip);
733
846
  return {
734
847
  allowed: true, tier: 'free', remaining,
735
- warning: remaining <= 4 ? remaining + ' free classification' + (remaining === 1 ? '' : 's') + ' remaining this month. Get 500 calls for $24 at ' + STRIPE_PRO_URL + ' -- calls never expire.' : null
848
+ warning: remaining <= 4 ? remaining + ' free classification' + (remaining === 1 ? '' : 's') + ' remaining this month (limit: ' + effectiveLimit + '). Get 500 calls for $24 at ' + STRIPE_PRO_URL + ' -- calls never expire.' : null
736
849
  };
737
850
  }
738
851
 
@@ -780,7 +893,9 @@ async function handleStripeWebhook(body, sig) {
780
893
  const plan = getPlanFromProduct(session.metadata?.product_name || '');
781
894
  if (email) {
782
895
  const apiKey = generateApiKey();
783
- apiKeys.set(apiKey, { email, plan, createdAt: nowISO(), calls: 0, limit: PLAN_LIMITS[plan] });
896
+ const record = { email, plan, createdAt: nowISO(), calls: 0, limit: PLAN_LIMITS[plan] };
897
+ apiKeys.set(apiKey, record);
898
+ await saveKeyToRedis(apiKey, record);
784
899
  saveApiKeys();
785
900
  await sendApiKeyEmail(email, apiKey, plan);
786
901
  console.log('[data-compliance] API key created for ' + email + ' (' + plan + ')');
@@ -845,8 +960,37 @@ const server = http.createServer(async (req, res) => {
845
960
  if (req.headers['x-stats-key'] !== STATS_KEY) { res.writeHead(401, cors); res.end(JSON.stringify({ error: 'Unauthorized' })); return; }
846
961
  const totalFreeCalls = Array.from(freeTierUsage.values()).reduce((a, b) => a + b, 0);
847
962
  const freeUniqueIPs = new Set(Array.from(freeTierUsage.keys()).map(k => k.split(':')[0])).size;
963
+ const monthPrefix = new Date().toISOString().slice(0, 7);
964
+ const breakdown = {};
965
+ for (const [key, count] of freeTierUsage.entries()) {
966
+ if (key.includes(':' + monthPrefix)) {
967
+ const ip = key.split(':')[0];
968
+ breakdown[ip.slice(0, 10) + '...'] = count;
969
+ }
970
+ }
848
971
  res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
849
- res.end(JSON.stringify({ free_tier_unique_ips: freeUniqueIPs, free_tier_total_calls: totalFreeCalls, paid_keys_issued: apiKeys.size, tool_usage: toolUsageCounts, recent_calls: usageLog.slice(-20).reverse(), trial_extensions_granted: trialExtensions.size }));
972
+ res.end(JSON.stringify({ free_tier_unique_ips: freeUniqueIPs, free_tier_total_calls: totalFreeCalls, paid_keys_issued: apiKeys.size, tool_usage: toolUsageCounts, recent_calls: usageLog.slice(-20).reverse(), trial_extensions_granted: trialExtensions.size, free_tier_breakdown: breakdown }));
973
+ return;
974
+ }
975
+
976
+ if (req.url === '/session-log' && req.method === 'GET') {
977
+ if (req.headers['x-stats-key'] !== STATS_KEY) { res.writeHead(401, cors); res.end(JSON.stringify({ error: 'Unauthorized' })); return; }
978
+ (async () => {
979
+ const keys = await redisKeys(`${REDIS_PREFIX}:session:*`);
980
+ const sessions = [];
981
+ for (const key of keys) {
982
+ const calls = await redisGet(key) || [];
983
+ if (!calls.length) continue;
984
+ const withoutPrefix = key.slice(`${REDIS_PREFIX}:session:`.length);
985
+ const dateIdx = withoutPrefix.lastIndexOf(':');
986
+ const ipPart = withoutPrefix.slice(0, dateIdx);
987
+ const date = withoutPrefix.slice(dateIdx + 1);
988
+ sessions.push({ ip: ipPart.slice(0, 8), date, calls, first_call: calls[0]?.timestamp || '', last_call: calls[calls.length - 1]?.timestamp || '' });
989
+ }
990
+ sessions.sort((a, b) => new Date(b.first_call) - new Date(a.first_call));
991
+ res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
992
+ res.end(JSON.stringify(sessions));
993
+ })();
850
994
  return;
851
995
  }
852
996
 
@@ -923,6 +1067,7 @@ const server = http.createServer(async (req, res) => {
923
1067
  if (usageLog.length > 1000) usageLog.shift();
924
1068
  toolUsageCounts[name] = (toolUsageCounts[name] || 0) + 1;
925
1069
  saveStats();
1070
+ appendSessionLog(ip, name).catch((e) => console.error('[SessionLog] appendSessionLog failed:', e));
926
1071
 
927
1072
  const result = await executeTool(name, toolArgs || {}, access.tier);
928
1073
  if (access.warning) result._notice = access.warning;
@@ -992,9 +1137,11 @@ function setupStdio() {
992
1137
 
993
1138
  setupStdio();
994
1139
 
995
- server.listen(PORT, () => {
1140
+ server.listen(PORT, async () => {
996
1141
  loadStats();
997
1142
  loadApiKeys();
1143
+ await loadApiKeysFromRedis();
1144
+ await loadFreeTierFromRedis();
998
1145
  console.log('Data Compliance Classifier MCP v' + VERSION + ' running on port ' + PORT);
999
1146
  console.log('Tools: 2 (validate_data_safety, get_safety_report)');
1000
1147
  console.log('Free tier: ' + FREE_TIER_LIMIT + ' classifications/IP/month');