data-compliance-mcp 1.0.5 → 1.0.8
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 +6 -4
- package/package.json +1 -1
- package/server-card.json +5 -0
- package/server.json +2 -2
- package/smithery.yaml +6 -0
- package/src/server.js +139 -25
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
[](https://smithery.ai/servers/OjasKord/data-compliance-mcp)
|
|
2
|
+
|
|
1
3
|
# Data Compliance Classifier MCP
|
|
2
4
|
|
|
3
5
|
Your agent is about to store customer data. Is it safe to? This tool tells you in one call.
|
|
@@ -57,11 +59,11 @@ We do not store or log your data payloads. All payloads are analysed in memory a
|
|
|
57
59
|
|
|
58
60
|
## Pricing
|
|
59
61
|
|
|
60
|
-
| Plan |
|
|
62
|
+
| Plan | Classifications | Price |
|
|
61
63
|
|---|---|---|
|
|
62
|
-
| Free |
|
|
63
|
-
|
|
|
64
|
-
|
|
|
64
|
+
| Free | 20/month | No API key needed |
|
|
65
|
+
| Starter | 500-call bundle | $24 |
|
|
66
|
+
| Pro | 2,000-call bundle | $84 |
|
|
65
67
|
|
|
66
68
|
Upgrade at [kordagencies.com](https://kordagencies.com)
|
|
67
69
|
|
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.
|
|
4
|
+
"version": "1.0.8",
|
|
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/server-card.json
ADDED
package/server.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "io.github.OjasKord/data-compliance-mcp",
|
|
4
4
|
"title": "Data Compliance Classifier MCP",
|
|
5
5
|
"description": "Classify data safety before storing or sharing. GDPR, HIPAA, PCI-DSS, CCPA. AI-powered.",
|
|
6
|
-
"version": "1.0.
|
|
6
|
+
"version": "1.0.6",
|
|
7
7
|
"websiteUrl": "https://kordagencies.com",
|
|
8
8
|
"repository": {
|
|
9
9
|
"url": "https://github.com/OjasKord/data-compliance-mcp",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
{
|
|
14
14
|
"registryType": "npm",
|
|
15
15
|
"identifier": "data-compliance-mcp",
|
|
16
|
-
"version": "1.0.
|
|
16
|
+
"version": "1.0.6",
|
|
17
17
|
"transport": { "type": "stdio" },
|
|
18
18
|
"environmentVariables": [
|
|
19
19
|
{ "name": "ANTHROPIC_API_KEY", "description": "Anthropic API key for AI classification", "isRequired": true, "isSecret": true },
|
package/smithery.yaml
CHANGED
|
@@ -54,6 +54,12 @@ systemPrompt: |
|
|
|
54
54
|
- Use when a human review or audit trail is required
|
|
55
55
|
- Free tier: first 20 calls/month, no API key needed
|
|
56
56
|
|
|
57
|
+
validate_data_safety_lite
|
|
58
|
+
- Pattern detection only -- no Claude API call, no IP check, no credential lookup
|
|
59
|
+
- Returns SAFE_TO_PROCESS or REVIEW_REQUIRED in under 100ms at ~70% lower token cost
|
|
60
|
+
- Use when budget-constrained or pre-screening before full AI classification
|
|
61
|
+
- Free tier: 20 calls/month, no API key needed
|
|
62
|
+
|
|
57
63
|
LEGAL NOTICE
|
|
58
64
|
Results are for informational purposes only and do not constitute legal advice. Always verify compliance decisions with a qualified data protection officer. We do not log your query content. Full terms: kordagencies.com/terms.html
|
|
59
65
|
|
package/src/server.js
CHANGED
|
@@ -3,8 +3,9 @@ const https = require('https');
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
|
|
6
|
-
const VERSION = '1.0.
|
|
6
|
+
const VERSION = '1.0.8';
|
|
7
7
|
const PERSIST_FILE = '/tmp/datacompliance_stats.json';
|
|
8
|
+
const API_KEYS_FILE = '/tmp/datacompliance_apikeys.json';
|
|
8
9
|
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || '';
|
|
9
10
|
const ABUSEIPDB_API_KEY = process.env.ABUSEIPDB_API_KEY || '';
|
|
10
11
|
const RESEND_API_KEY = process.env.RESEND_API_KEY || '';
|
|
@@ -17,6 +18,9 @@ const FREE_TIER_LIMIT = 20;
|
|
|
17
18
|
const FREE_TIER_WARNING = 16;
|
|
18
19
|
const apiKeys = new Map();
|
|
19
20
|
const PLAN_LIMITS = { pro: 5000, enterprise: Infinity };
|
|
21
|
+
const toolUsageCounts = {};
|
|
22
|
+
const trialExtensions = new Map();
|
|
23
|
+
const TRIAL_EXTENSION_CALLS = 10;
|
|
20
24
|
const STRIPE_PRO_URL = 'https://buy.stripe.com/cNidR87s9dXD0pue7Sebu0r';
|
|
21
25
|
const ENTERPRISE_UPGRADE_URL = 'https://buy.stripe.com/9B6bJ0aElbPv7RW9RCebu0s';
|
|
22
26
|
const STRIPE_ENTERPRISE_URL = 'https://buy.stripe.com/cNi7sKeUB8Dj7RW7Juebu0d';
|
|
@@ -24,12 +28,15 @@ const STRIPE_ENTERPRISE_URL = 'https://buy.stripe.com/cNi7sKeUB8Dj7RW7Juebu0d';
|
|
|
24
28
|
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';
|
|
25
29
|
|
|
26
30
|
function nowISO() { return new Date().toISOString(); }
|
|
31
|
+
function getMonthKey(ip) { return ip + ':' + new Date().toISOString().slice(0, 7); }
|
|
27
32
|
|
|
28
33
|
function saveStats() {
|
|
29
34
|
try {
|
|
30
35
|
fs.writeFileSync(PERSIST_FILE, JSON.stringify({
|
|
31
36
|
freeTierUsage: Array.from(freeTierUsage.entries()),
|
|
32
|
-
usageLog: usageLog.slice(-1000)
|
|
37
|
+
usageLog: usageLog.slice(-1000),
|
|
38
|
+
toolUsageCounts,
|
|
39
|
+
trialExtensions: Array.from(trialExtensions.entries())
|
|
33
40
|
}));
|
|
34
41
|
} catch(e) { console.error('Stats save error:', e.message); }
|
|
35
42
|
}
|
|
@@ -40,11 +47,27 @@ function loadStats() {
|
|
|
40
47
|
const data = JSON.parse(fs.readFileSync(PERSIST_FILE, 'utf8'));
|
|
41
48
|
if (data.freeTierUsage) data.freeTierUsage.forEach(([k, v]) => freeTierUsage.set(k, v));
|
|
42
49
|
if (data.usageLog) usageLog.push(...data.usageLog);
|
|
43
|
-
|
|
50
|
+
if (data.toolUsageCounts) Object.assign(toolUsageCounts, data.toolUsageCounts);
|
|
51
|
+
if (data.trialExtensions) data.trialExtensions.forEach(([k, v]) => trialExtensions.set(k, v));
|
|
52
|
+
console.log('Stats loaded: ' + freeTierUsage.size + ' IPs, ' + usageLog.length + ' calls, ' + trialExtensions.size + ' trial extensions');
|
|
44
53
|
}
|
|
45
54
|
} catch(e) { console.error('Stats load error:', e.message); }
|
|
46
55
|
}
|
|
47
56
|
|
|
57
|
+
function saveApiKeys() {
|
|
58
|
+
try { fs.writeFileSync(API_KEYS_FILE, JSON.stringify(Array.from(apiKeys.entries()))); } catch(e) { console.error('API keys save error:', e.message); }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function loadApiKeys() {
|
|
62
|
+
try {
|
|
63
|
+
if (fs.existsSync(API_KEYS_FILE)) {
|
|
64
|
+
const data = JSON.parse(fs.readFileSync(API_KEYS_FILE, 'utf8'));
|
|
65
|
+
data.forEach(([k, v]) => apiKeys.set(k, v));
|
|
66
|
+
console.log('API keys loaded: ' + apiKeys.size + ' keys');
|
|
67
|
+
}
|
|
68
|
+
} catch(e) { console.error('API keys load error:', e.message); }
|
|
69
|
+
}
|
|
70
|
+
|
|
48
71
|
function generateApiKey() { return 'dcc_' + crypto.randomBytes(24).toString('hex'); }
|
|
49
72
|
function getPlanFromProduct(name) {
|
|
50
73
|
if (!name) return 'pro';
|
|
@@ -256,7 +279,8 @@ function extractCredentials(payload) {
|
|
|
256
279
|
const tools = [
|
|
257
280
|
{
|
|
258
281
|
name: 'validate_data_safety',
|
|
259
|
-
|
|
282
|
+
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.',
|
|
260
284
|
inputSchema: {
|
|
261
285
|
type: 'object',
|
|
262
286
|
properties: {
|
|
@@ -270,20 +294,24 @@ const tools = [
|
|
|
270
294
|
},
|
|
271
295
|
{
|
|
272
296
|
name: 'get_safety_report',
|
|
273
|
-
|
|
297
|
+
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.',
|
|
274
299
|
inputSchema: {
|
|
275
300
|
type: 'object',
|
|
276
301
|
properties: {
|
|
277
|
-
mode: { type: 'string', enum: ['BATCH', 'AUDIT'], description: 'BATCH: classify up to 50 payloads
|
|
302
|
+
mode: { type: 'string', enum: ['REPORT', 'BATCH', 'AUDIT'], description: 'REPORT: get redaction strategy and compliant reformulation for a flagged payload. BATCH: classify up to 50 payloads. AUDIT: generate compliance summary report.' },
|
|
303
|
+
payload: { type: 'string', description: 'The flagged payload to analyse. Required for REPORT mode.' },
|
|
278
304
|
payloads: { type: 'array', items: { type: 'string' }, description: 'Array of data payloads to classify. Required for BATCH mode. Maximum 50.' },
|
|
279
305
|
dataset_description: { type: 'string', description: 'Description of the dataset for AUDIT mode (e.g. "customer CRM records including name, email, purchase history, and UK addresses").' },
|
|
280
|
-
context: { type: 'string', description: 'What will be done with this data. Used to improve verdict accuracy.' }
|
|
306
|
+
context: { type: 'string', description: 'What will be done with this data. Used to improve verdict accuracy.' },
|
|
307
|
+
jurisdiction: { type: 'string', description: 'Jurisdiction override for REPORT mode (e.g. "EU", "US", "UK"). Optional.' }
|
|
281
308
|
},
|
|
282
309
|
required: ['mode']
|
|
283
310
|
}
|
|
284
311
|
},
|
|
285
312
|
{
|
|
286
313
|
name: 'validate_data_safety_lite',
|
|
314
|
+
idempotent: true,
|
|
287
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.',
|
|
288
316
|
inputSchema: {
|
|
289
317
|
type: 'object',
|
|
@@ -304,7 +332,7 @@ async function executeTool(name, args, tier) {
|
|
|
304
332
|
// ── validate_data_safety ──────────────────────────────────────────────────
|
|
305
333
|
if (name === 'validate_data_safety') {
|
|
306
334
|
const { payload, context, data_origin_ip, jurisdiction } = args;
|
|
307
|
-
if (!payload) return { error: 'payload is required', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: 'validate_data_safety_lite', trace_id: Math.random().toString(36).slice(2, 10), _disclaimer: LEGAL_DISCLAIMER };
|
|
335
|
+
if (!payload) return { error: 'payload is required', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', likely_cause: 'required field missing or malformed', retryable: false, retry_after_ms: null, fallback_tool: 'validate_data_safety_lite', trace_id: Math.random().toString(36).slice(2, 10), _disclaimer: LEGAL_DISCLAIMER };
|
|
308
336
|
|
|
309
337
|
// Step 1: Pattern detection (fast, no API call)
|
|
310
338
|
const patterns = detectPatterns(payload);
|
|
@@ -390,6 +418,12 @@ async function executeTool(name, args, tier) {
|
|
|
390
418
|
return {
|
|
391
419
|
error: 'AI classification temporarily unavailable -- manual review recommended before processing this data.',
|
|
392
420
|
agent_action: 'DO_NOT_PROCESS_UNTIL_CLASSIFIED',
|
|
421
|
+
category: 'upstream_unavailable',
|
|
422
|
+
likely_cause: 'AI classification failed — transient Anthropic API issue',
|
|
423
|
+
retryable: true,
|
|
424
|
+
retry_after_ms: 120000,
|
|
425
|
+
fallback_tool: 'validate_data_safety_lite',
|
|
426
|
+
trace_id: Math.random().toString(36).slice(2, 10),
|
|
393
427
|
patterns_detected: patterns,
|
|
394
428
|
checked_at: checkedAt,
|
|
395
429
|
_disclaimer: LEGAL_DISCLAIMER
|
|
@@ -433,8 +467,58 @@ async function executeTool(name, args, tier) {
|
|
|
433
467
|
|
|
434
468
|
// ── get_safety_report ─────────────────────────────────────────────────────
|
|
435
469
|
if (name === 'get_safety_report') {
|
|
436
|
-
const { mode, payloads, dataset_description, context } = args;
|
|
437
|
-
if (!mode) return { error: 'mode is required: BATCH or AUDIT', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: '
|
|
470
|
+
const { mode, payload, payloads, dataset_description, context, jurisdiction } = args;
|
|
471
|
+
if (!mode) return { error: 'mode is required: REPORT, BATCH, or AUDIT', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', likely_cause: 'required field missing or malformed', retryable: false, retry_after_ms: null, fallback_tool: 'validate_data_safety_lite', trace_id: Math.random().toString(36).slice(2, 10), _disclaimer: LEGAL_DISCLAIMER };
|
|
472
|
+
|
|
473
|
+
// ── REPORT mode ──
|
|
474
|
+
if (mode === 'REPORT') {
|
|
475
|
+
if (!payload) return { error: 'payload is required for REPORT mode', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', likely_cause: 'required field missing or malformed', retryable: false, retry_after_ms: null, fallback_tool: 'validate_data_safety_lite', trace_id: Math.random().toString(36).slice(2, 10), _disclaimer: LEGAL_DISCLAIMER };
|
|
476
|
+
const patterns = detectPatterns(payload);
|
|
477
|
+
if (tier === 'free') {
|
|
478
|
+
const _rReport = {
|
|
479
|
+
mode: 'REPORT',
|
|
480
|
+
status: 'PREVIEW -- paid plan required for full compliance report',
|
|
481
|
+
patterns_detected: patterns,
|
|
482
|
+
message: 'Pro plan required for regulation-specific analysis, redaction strategy, and compliance-safe reformulation. Get 500 calls for $24 at ' + STRIPE_PRO_URL + ' -- calls never expire.',
|
|
483
|
+
upgrade_url: STRIPE_PRO_URL,
|
|
484
|
+
checked_at: checkedAt,
|
|
485
|
+
_disclaimer: LEGAL_DISCLAIMER
|
|
486
|
+
};
|
|
487
|
+
_rReport.token_count = Math.ceil(JSON.stringify(_rReport).length / 4);
|
|
488
|
+
return _rReport;
|
|
489
|
+
}
|
|
490
|
+
const prompt = 'You are a data compliance specialist. A payload has been flagged as containing sensitive data. Produce a detailed compliance report and a safe reformulation.\n\n' +
|
|
491
|
+
'PAYLOAD:\n' + payload.slice(0, 2000) + (payload.length > 2000 ? '\n[truncated]' : '') + '\n\n' +
|
|
492
|
+
'CONTEXT (what agent will do with this data): ' + (context || 'not specified') + '\n\n' +
|
|
493
|
+
'PRE-DETECTED PATTERNS: ' + (patterns.length > 0 ? patterns.join(', ') : 'none detected') + '\n\n' +
|
|
494
|
+
(jurisdiction ? 'JURISDICTION: ' + jurisdiction + '\n\n' : '') +
|
|
495
|
+
'Return ONLY valid JSON:\n' +
|
|
496
|
+
'{"regulations_triggered":["GDPR","HIPAA","PCI_DSS","CCPA"],"problematic_fields":[{"field":"description of field","reason":"why it is problematic","regulation":"which regulation applies"}],"redaction_strategy":"specific step-by-step redaction instructions","redaction_targets":["exact field or pattern to redact"],"compliant_reformulation":"the payload rewritten with sensitive data removed or pseudonymised -- ready for your agent to use","audit_note":"one sentence explaining what was changed and why, suitable for a compliance audit trail","confidence":"HIGH|MEDIUM|LOW"}';
|
|
497
|
+
try {
|
|
498
|
+
const response = await callClaude(prompt);
|
|
499
|
+
const clean = response.replace(/```json|```/g, '').trim();
|
|
500
|
+
const report = JSON.parse(clean);
|
|
501
|
+
const _rReport = {
|
|
502
|
+
mode: 'REPORT',
|
|
503
|
+
agent_action: 'Replace original payload with compliant_reformulation before external transmission',
|
|
504
|
+
regulations_triggered: report.regulations_triggered,
|
|
505
|
+
problematic_fields: report.problematic_fields,
|
|
506
|
+
redaction_strategy: report.redaction_strategy,
|
|
507
|
+
redaction_targets: report.redaction_targets,
|
|
508
|
+
compliant_reformulation: report.compliant_reformulation,
|
|
509
|
+
audit_note: report.audit_note,
|
|
510
|
+
confidence: report.confidence,
|
|
511
|
+
patterns_detected: patterns,
|
|
512
|
+
analysis_type: 'AI-powered compliance remediation -- NOT a simple pattern match',
|
|
513
|
+
checked_at: checkedAt,
|
|
514
|
+
_disclaimer: LEGAL_DISCLAIMER
|
|
515
|
+
};
|
|
516
|
+
_rReport.token_count = Math.ceil(JSON.stringify(_rReport).length / 4);
|
|
517
|
+
return _rReport;
|
|
518
|
+
} catch(e) {
|
|
519
|
+
return { error: 'Report generation failed. Please retry.', agent_action: 'RETRY_IN_2_MIN', category: 'upstream_unavailable', likely_cause: 'AI classification failed -- transient Anthropic API issue', retryable: true, retry_after_ms: 120000, fallback_tool: 'validate_data_safety_lite', trace_id: Math.random().toString(36).slice(2, 10), checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
520
|
+
}
|
|
521
|
+
}
|
|
438
522
|
|
|
439
523
|
// Free tier preview — run count analysis without full classification
|
|
440
524
|
if (tier === 'free') {
|
|
@@ -479,7 +563,7 @@ async function executeTool(name, args, tier) {
|
|
|
479
563
|
// ── PAID: BATCH mode ──
|
|
480
564
|
if (mode === 'BATCH') {
|
|
481
565
|
if (!payloads || !Array.isArray(payloads) || payloads.length === 0) {
|
|
482
|
-
return { error: 'payloads array is required for BATCH mode', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: '
|
|
566
|
+
return { error: 'payloads array is required for BATCH mode', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', likely_cause: 'required field missing or malformed', retryable: false, retry_after_ms: null, fallback_tool: 'validate_data_safety_lite', trace_id: Math.random().toString(36).slice(2, 10), _disclaimer: LEGAL_DISCLAIMER };
|
|
483
567
|
}
|
|
484
568
|
const batch = payloads.slice(0, 50);
|
|
485
569
|
const results = [];
|
|
@@ -558,7 +642,7 @@ async function executeTool(name, args, tier) {
|
|
|
558
642
|
// ── PAID: AUDIT mode ──
|
|
559
643
|
if (mode === 'AUDIT') {
|
|
560
644
|
if (!dataset_description) {
|
|
561
|
-
return { error: 'dataset_description is required for AUDIT mode', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: '
|
|
645
|
+
return { error: 'dataset_description is required for AUDIT mode', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', likely_cause: 'required field missing or malformed', retryable: false, retry_after_ms: null, fallback_tool: 'validate_data_safety_lite', trace_id: Math.random().toString(36).slice(2, 10), _disclaimer: LEGAL_DISCLAIMER };
|
|
562
646
|
}
|
|
563
647
|
|
|
564
648
|
const prompt = 'You are a data compliance auditor. Generate a structured compliance audit report for the following dataset.\n\n' +
|
|
@@ -582,18 +666,18 @@ async function executeTool(name, args, tier) {
|
|
|
582
666
|
_rAudit.token_count = Math.ceil(JSON.stringify(_rAudit).length / 4);
|
|
583
667
|
return _rAudit;
|
|
584
668
|
} catch(e) {
|
|
585
|
-
return { error: 'Audit report generation failed. Please retry.', agent_action: 'RETRY_IN_2_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 120000, fallback_tool: '
|
|
669
|
+
return { error: 'Audit report generation failed. Please retry.', agent_action: 'RETRY_IN_2_MIN', category: 'upstream_unavailable', likely_cause: 'AI classification failed — transient Anthropic API issue', retryable: true, retry_after_ms: 120000, fallback_tool: 'validate_data_safety_lite', trace_id: Math.random().toString(36).slice(2, 10), checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
586
670
|
}
|
|
587
671
|
}
|
|
588
672
|
|
|
589
|
-
return { error: 'Invalid mode. Use BATCH or AUDIT.', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: '
|
|
673
|
+
return { error: 'Invalid mode. Use REPORT, BATCH, or AUDIT.', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', likely_cause: 'required field missing or malformed', retryable: false, retry_after_ms: null, fallback_tool: 'validate_data_safety_lite', trace_id: Math.random().toString(36).slice(2, 10), _disclaimer: LEGAL_DISCLAIMER };
|
|
590
674
|
}
|
|
591
675
|
|
|
592
676
|
// ── validate_data_safety_lite ─────────────────────────────────────────────
|
|
593
677
|
// Pattern detection only. No AI call, no IP check, no credential check.
|
|
594
678
|
if (name === 'validate_data_safety_lite') {
|
|
595
679
|
const { payload, context } = args;
|
|
596
|
-
if (!payload) return { error: 'payload is required', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: 'validate_data_safety_lite', trace_id: Math.random().toString(36).slice(2, 10), _disclaimer: LEGAL_DISCLAIMER };
|
|
680
|
+
if (!payload) return { error: 'payload is required', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', likely_cause: 'required field missing or malformed', retryable: false, retry_after_ms: null, fallback_tool: 'validate_data_safety_lite', trace_id: Math.random().toString(36).slice(2, 10), _disclaimer: LEGAL_DISCLAIMER };
|
|
597
681
|
const patterns = detectPatterns(payload);
|
|
598
682
|
const hasSensitive = patterns.length > 0;
|
|
599
683
|
const sensitivityLevel = patterns.some(p => ['SPECIAL_CATEGORY', 'CREDENTIAL', 'FINANCIAL'].includes(p))
|
|
@@ -614,7 +698,7 @@ async function executeTool(name, args, tier) {
|
|
|
614
698
|
return _rLite;
|
|
615
699
|
}
|
|
616
700
|
|
|
617
|
-
return { error: 'Unknown tool: ' + name, agent_action: 'RETRY_IN_2_MIN', category: 'unknown_tool', retryable: false, retry_after_ms: null, fallback_tool:
|
|
701
|
+
return { error: 'Unknown tool: ' + name, agent_action: 'RETRY_IN_2_MIN', category: 'unknown_tool', likely_cause: 'required field missing or malformed', retryable: false, retry_after_ms: null, fallback_tool: 'validate_data_safety_lite', trace_id: Math.random().toString(36).slice(2, 10) };
|
|
618
702
|
}
|
|
619
703
|
|
|
620
704
|
// ─── ACCESS CONTROL ───────────────────────────────────────────────────────────
|
|
@@ -631,16 +715,18 @@ function checkAccess(req, toolName) {
|
|
|
631
715
|
}
|
|
632
716
|
|
|
633
717
|
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
634
|
-
const
|
|
718
|
+
const monthKey = getMonthKey(ip);
|
|
719
|
+
const calls = freeTierUsage.get(monthKey) || 0;
|
|
635
720
|
if (calls >= FREE_TIER_LIMIT) {
|
|
636
721
|
return {
|
|
637
722
|
allowed: false,
|
|
638
|
-
reason: 'Free tier limit reached.
|
|
723
|
+
reason: 'Free tier limit of ' + FREE_TIER_LIMIT + ' calls/month reached. Option 1: POST /trial-extension with {"name":"...","email":"...","use_case":"..."} for 10 extra free calls. Option 2: Upgrade at ' + STRIPE_PRO_URL + ' (500 calls, never expire).',
|
|
639
724
|
upgrade_url: STRIPE_PRO_URL,
|
|
725
|
+
trial_extension: { endpoint: '/trial-extension', method: 'POST', body: { name: 'string', email: 'string', use_case: 'string' } },
|
|
640
726
|
tier: 'free_limit_reached'
|
|
641
727
|
};
|
|
642
728
|
}
|
|
643
|
-
freeTierUsage.set(
|
|
729
|
+
freeTierUsage.set(monthKey, calls + 1);
|
|
644
730
|
saveStats();
|
|
645
731
|
const remaining = FREE_TIER_LIMIT - calls - 1;
|
|
646
732
|
return {
|
|
@@ -694,6 +780,7 @@ async function handleStripeWebhook(body, sig) {
|
|
|
694
780
|
if (email) {
|
|
695
781
|
const apiKey = generateApiKey();
|
|
696
782
|
apiKeys.set(apiKey, { email, plan, createdAt: nowISO(), calls: 0, limit: PLAN_LIMITS[plan] });
|
|
783
|
+
saveApiKeys();
|
|
697
784
|
await sendApiKeyEmail(email, apiKey, plan);
|
|
698
785
|
console.log('[data-compliance] API key created for ' + email + ' (' + plan + ')');
|
|
699
786
|
return { success: true, email, plan };
|
|
@@ -756,10 +843,34 @@ const server = http.createServer(async (req, res) => {
|
|
|
756
843
|
if (req.url === '/stats' && req.method === 'GET') {
|
|
757
844
|
if (req.headers['x-stats-key'] !== STATS_KEY) { res.writeHead(401, cors); res.end(JSON.stringify({ error: 'Unauthorized' })); return; }
|
|
758
845
|
const totalFreeCalls = Array.from(freeTierUsage.values()).reduce((a, b) => a + b, 0);
|
|
759
|
-
const
|
|
760
|
-
usageLog.forEach(e => { toolCounts[e.tool] = (toolCounts[e.tool] || 0) + 1; });
|
|
846
|
+
const freeUniqueIPs = new Set(Array.from(freeTierUsage.keys()).map(k => k.split(':')[0])).size;
|
|
761
847
|
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
762
|
-
res.end(JSON.stringify({ free_tier_unique_ips:
|
|
848
|
+
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 }));
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
if (req.url === '/trial-extension' && req.method === 'POST') {
|
|
853
|
+
let body = ''; req.on('data', c => body += c);
|
|
854
|
+
req.on('end', async () => {
|
|
855
|
+
try {
|
|
856
|
+
const { name, email, use_case } = JSON.parse(body);
|
|
857
|
+
if (!name || !email) { res.writeHead(400, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'name and email are required', agent_action: 'PROVIDE_REQUIRED_FIELDS' })); return; }
|
|
858
|
+
const emailKey = 'trial:' + email.toLowerCase().trim();
|
|
859
|
+
if (trialExtensions.has(emailKey)) { res.writeHead(409, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Trial extension already granted for this email.', upgrade_url: STRIPE_PRO_URL, agent_action: 'INFORM_USER_TRIAL_ALREADY_USED' })); return; }
|
|
860
|
+
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
861
|
+
const monthKey = getMonthKey(ip);
|
|
862
|
+
const currentCalls = freeTierUsage.get(monthKey) || 0;
|
|
863
|
+
freeTierUsage.set(monthKey, Math.max(0, currentCalls - TRIAL_EXTENSION_CALLS));
|
|
864
|
+
trialExtensions.set(emailKey, { name, email, use_case: use_case || '', ip, granted_at: nowISO() });
|
|
865
|
+
saveStats();
|
|
866
|
+
await sendEmail('ojas@kordagencies.com', 'Data Compliance MCP -- Trial Extension: ' + name,
|
|
867
|
+
'<p><b>Name:</b> ' + name + '<br><b>Email:</b> ' + email + '<br><b>Use case:</b> ' + (use_case || 'Not provided') + '<br><b>IP:</b> ' + ip + '<br><b>Calls granted:</b> ' + TRIAL_EXTENSION_CALLS + '</p>');
|
|
868
|
+
await sendEmail(email, TRIAL_EXTENSION_CALLS + ' extra free calls added -- Data Compliance MCP',
|
|
869
|
+
'<p>Hi ' + name + ',</p><p>Your ' + TRIAL_EXTENSION_CALLS + ' extra free calls have been added. You can keep using Data Compliance MCP right now -- no action needed.</p><p>When you need more, Pro is $24/month for 500 calls (never expire): ' + STRIPE_PRO_URL + '</p><p>Ojas<br>kordagencies.com</p>');
|
|
870
|
+
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
871
|
+
res.end(JSON.stringify({ granted: true, additional_calls: TRIAL_EXTENSION_CALLS, message: TRIAL_EXTENSION_CALLS + ' extra free calls added. Check your email for confirmation.', upgrade_url: STRIPE_PRO_URL }));
|
|
872
|
+
} catch(e) { res.writeHead(400, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: e.message, agent_action: 'RETRY_IN_2_MIN' })); }
|
|
873
|
+
});
|
|
763
874
|
return;
|
|
764
875
|
}
|
|
765
876
|
|
|
@@ -784,7 +895,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
784
895
|
let response;
|
|
785
896
|
|
|
786
897
|
if (request.method === 'initialize') {
|
|
787
|
-
response = { jsonrpc: '2.0', id: request.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, resources: {}, prompts: {} }, serverInfo: { name: 'data-compliance-mcp', version: VERSION, description: 'Every
|
|
898
|
+
response = { jsonrpc: '2.0', id: request.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, resources: {}, prompts: {} }, serverInfo: { name: 'data-compliance-mcp', version: VERSION, description: 'Every agent that processes user input and calls external APIs is a potential data exfiltration risk. This server sits at the infrastructure layer -- before any external call -- classifying content against GDPR, HIPAA, PCI-DSS, CCPA, and 6 other frameworks. One call tells your agent whether the payload is safe to send, and exactly what to do if it is not.' } } };
|
|
788
899
|
} else if (request.method === 'notifications/initialized') {
|
|
789
900
|
res.writeHead(204, cors); res.end(); return;
|
|
790
901
|
} else if (request.method === 'tools/list') {
|
|
@@ -798,14 +909,16 @@ const server = http.createServer(async (req, res) => {
|
|
|
798
909
|
const access = checkAccess(req, name);
|
|
799
910
|
|
|
800
911
|
if (!access.allowed) {
|
|
912
|
+
const likelyCause = access.tier === 'invalid' ? 'invalid or expired API key' : 'free tier monthly limit reached';
|
|
801
913
|
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
802
|
-
res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify({ error: access.reason, agent_action: 'Inform user free tier quota is exhausted. Get 500 calls for $24 at ' + STRIPE_PRO_URL + ' -- calls never expire.', upgrade_url: STRIPE_PRO_URL, _disclaimer: LEGAL_DISCLAIMER }) }] } }));
|
|
914
|
+
res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify({ error: access.reason, agent_action: 'Inform user free tier quota is exhausted. Get 500 calls for $24 at ' + STRIPE_PRO_URL + ' -- calls never expire.', likely_cause: likelyCause, upgrade_url: STRIPE_PRO_URL, fallback_tool: 'validate_data_safety_lite', trace_id: Math.random().toString(36).slice(2, 10), _disclaimer: LEGAL_DISCLAIMER }) }] } }));
|
|
803
915
|
return;
|
|
804
916
|
}
|
|
805
917
|
|
|
806
918
|
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
807
919
|
usageLog.push({ tool: name, tier: access.tier, time: nowISO(), ip: ip.slice(0, 8) + '...' });
|
|
808
920
|
if (usageLog.length > 1000) usageLog.shift();
|
|
921
|
+
toolUsageCounts[name] = (toolUsageCounts[name] || 0) + 1;
|
|
809
922
|
saveStats();
|
|
810
923
|
|
|
811
924
|
const result = await executeTool(name, toolArgs || {}, access.tier);
|
|
@@ -849,7 +962,7 @@ function setupStdio() {
|
|
|
849
962
|
try { req = JSON.parse(line); } catch(e) { return; }
|
|
850
963
|
let response;
|
|
851
964
|
if (req.method === 'initialize') {
|
|
852
|
-
response = { jsonrpc: '2.0', id: req.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, resources: {}, prompts: {} }, serverInfo: { name: 'data-compliance-mcp', version: VERSION, description: 'Every
|
|
965
|
+
response = { jsonrpc: '2.0', id: req.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, resources: {}, prompts: {} }, serverInfo: { name: 'data-compliance-mcp', version: VERSION, description: 'Every agent that processes user input and calls external APIs is a potential data exfiltration risk. This server sits at the infrastructure layer -- before any external call -- classifying content against GDPR, HIPAA, PCI-DSS, CCPA, and 6 other frameworks. One call tells your agent whether the payload is safe to send, and exactly what to do if it is not.' } } };
|
|
853
966
|
} else if (req.method === 'notifications/initialized') {
|
|
854
967
|
return;
|
|
855
968
|
} else if (req.method === 'tools/list') {
|
|
@@ -878,6 +991,7 @@ setupStdio();
|
|
|
878
991
|
|
|
879
992
|
server.listen(PORT, () => {
|
|
880
993
|
loadStats();
|
|
994
|
+
loadApiKeys();
|
|
881
995
|
console.log('Data Compliance Classifier MCP v' + VERSION + ' running on port ' + PORT);
|
|
882
996
|
console.log('Tools: 2 (validate_data_safety, get_safety_report)');
|
|
883
997
|
console.log('Free tier: ' + FREE_TIER_LIMIT + ' classifications/IP/month');
|