data-compliance-mcp 1.0.6 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/README.md +47 -4
- package/package.json +1 -1
- package/src/server.js +127 -20
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -59,11 +59,11 @@ We do not store or log your data payloads. All payloads are analysed in memory a
|
|
|
59
59
|
|
|
60
60
|
## Pricing
|
|
61
61
|
|
|
62
|
-
| Plan |
|
|
62
|
+
| Plan | Classifications | Price |
|
|
63
63
|
|---|---|---|
|
|
64
|
-
| Free |
|
|
65
|
-
|
|
|
66
|
-
|
|
|
64
|
+
| Free | 20/month | No API key needed |
|
|
65
|
+
| Starter | 500-call bundle | $24 |
|
|
66
|
+
| Pro | 2,000-call bundle | $84 |
|
|
67
67
|
|
|
68
68
|
Upgrade at [kordagencies.com](https://kordagencies.com)
|
|
69
69
|
|
|
@@ -92,6 +92,49 @@ With paid API key:
|
|
|
92
92
|
}
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
+
## Harness Integration
|
|
96
|
+
|
|
97
|
+
### Claude Code / Claude Desktop (.mcp.json)
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"mcpServers": {
|
|
101
|
+
"data-compliance": {
|
|
102
|
+
"type": "http",
|
|
103
|
+
"url": "https://data-compliance-mcp-production.up.railway.app"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### LangChain (Python)
|
|
110
|
+
```python
|
|
111
|
+
from langchain_mcp_adapters.client import MultiServerMCPClient
|
|
112
|
+
client = MultiServerMCPClient({
|
|
113
|
+
"data-compliance": {
|
|
114
|
+
"url": "https://data-compliance-mcp-production.up.railway.app",
|
|
115
|
+
"transport": "http"
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
tools = await client.get_tools()
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### OpenAI Agents SDK (Python)
|
|
122
|
+
```python
|
|
123
|
+
from agents import Agent, HostedMCPTool
|
|
124
|
+
agent = Agent(
|
|
125
|
+
name="Assistant",
|
|
126
|
+
tools=[HostedMCPTool(tool_config={
|
|
127
|
+
"type": "mcp",
|
|
128
|
+
"server_label": "data-compliance",
|
|
129
|
+
"server_url": "https://data-compliance-mcp-production.up.railway.app",
|
|
130
|
+
"require_approval": "never"
|
|
131
|
+
})]
|
|
132
|
+
)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### LangGraph
|
|
136
|
+
Same as LangChain above — langchain-mcp-adapters works with LangGraph natively.
|
|
137
|
+
|
|
95
138
|
## Example call
|
|
96
139
|
|
|
97
140
|
```bash
|
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.9",
|
|
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,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.9';
|
|
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';
|
|
@@ -257,7 +280,7 @@ const tools = [
|
|
|
257
280
|
{
|
|
258
281
|
name: 'validate_data_safety',
|
|
259
282
|
idempotent: true,
|
|
260
|
-
description: '
|
|
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.',
|
|
261
284
|
inputSchema: {
|
|
262
285
|
type: 'object',
|
|
263
286
|
properties: {
|
|
@@ -272,14 +295,16 @@ const tools = [
|
|
|
272
295
|
{
|
|
273
296
|
name: 'get_safety_report',
|
|
274
297
|
idempotent: true,
|
|
275
|
-
description: '
|
|
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.',
|
|
276
299
|
inputSchema: {
|
|
277
300
|
type: 'object',
|
|
278
301
|
properties: {
|
|
279
|
-
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.' },
|
|
280
304
|
payloads: { type: 'array', items: { type: 'string' }, description: 'Array of data payloads to classify. Required for BATCH mode. Maximum 50.' },
|
|
281
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").' },
|
|
282
|
-
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.' }
|
|
283
308
|
},
|
|
284
309
|
required: ['mode']
|
|
285
310
|
}
|
|
@@ -442,8 +467,58 @@ async function executeTool(name, args, tier) {
|
|
|
442
467
|
|
|
443
468
|
// ── get_safety_report ─────────────────────────────────────────────────────
|
|
444
469
|
if (name === 'get_safety_report') {
|
|
445
|
-
const { mode, payloads, dataset_description, context } = args;
|
|
446
|
-
if (!mode) return { error: 'mode is required: 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 };
|
|
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
|
+
}
|
|
447
522
|
|
|
448
523
|
// Free tier preview — run count analysis without full classification
|
|
449
524
|
if (tier === 'free') {
|
|
@@ -595,7 +670,7 @@ async function executeTool(name, args, tier) {
|
|
|
595
670
|
}
|
|
596
671
|
}
|
|
597
672
|
|
|
598
|
-
return { error: 'Invalid mode. Use 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 };
|
|
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 };
|
|
599
674
|
}
|
|
600
675
|
|
|
601
676
|
// ── validate_data_safety_lite ─────────────────────────────────────────────
|
|
@@ -639,17 +714,20 @@ function checkAccess(req, toolName) {
|
|
|
639
714
|
return { allowed: true, tier: record.plan };
|
|
640
715
|
}
|
|
641
716
|
|
|
642
|
-
const
|
|
643
|
-
const
|
|
717
|
+
const rawIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
718
|
+
const ip = rawIp.split(',')[0].trim();
|
|
719
|
+
const monthKey = getMonthKey(ip);
|
|
720
|
+
const calls = freeTierUsage.get(monthKey) || 0;
|
|
644
721
|
if (calls >= FREE_TIER_LIMIT) {
|
|
645
722
|
return {
|
|
646
723
|
allowed: false,
|
|
647
|
-
reason: 'Free tier limit reached.
|
|
724
|
+
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).',
|
|
648
725
|
upgrade_url: STRIPE_PRO_URL,
|
|
726
|
+
trial_extension: { endpoint: '/trial-extension', method: 'POST', body: { name: 'string', email: 'string', use_case: 'string' } },
|
|
649
727
|
tier: 'free_limit_reached'
|
|
650
728
|
};
|
|
651
729
|
}
|
|
652
|
-
freeTierUsage.set(
|
|
730
|
+
freeTierUsage.set(monthKey, calls + 1);
|
|
653
731
|
saveStats();
|
|
654
732
|
const remaining = FREE_TIER_LIMIT - calls - 1;
|
|
655
733
|
return {
|
|
@@ -703,6 +781,7 @@ async function handleStripeWebhook(body, sig) {
|
|
|
703
781
|
if (email) {
|
|
704
782
|
const apiKey = generateApiKey();
|
|
705
783
|
apiKeys.set(apiKey, { email, plan, createdAt: nowISO(), calls: 0, limit: PLAN_LIMITS[plan] });
|
|
784
|
+
saveApiKeys();
|
|
706
785
|
await sendApiKeyEmail(email, apiKey, plan);
|
|
707
786
|
console.log('[data-compliance] API key created for ' + email + ' (' + plan + ')');
|
|
708
787
|
return { success: true, email, plan };
|
|
@@ -765,10 +844,35 @@ const server = http.createServer(async (req, res) => {
|
|
|
765
844
|
if (req.url === '/stats' && req.method === 'GET') {
|
|
766
845
|
if (req.headers['x-stats-key'] !== STATS_KEY) { res.writeHead(401, cors); res.end(JSON.stringify({ error: 'Unauthorized' })); return; }
|
|
767
846
|
const totalFreeCalls = Array.from(freeTierUsage.values()).reduce((a, b) => a + b, 0);
|
|
768
|
-
const
|
|
769
|
-
usageLog.forEach(e => { toolCounts[e.tool] = (toolCounts[e.tool] || 0) + 1; });
|
|
847
|
+
const freeUniqueIPs = new Set(Array.from(freeTierUsage.keys()).map(k => k.split(':')[0])).size;
|
|
770
848
|
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
771
|
-
res.end(JSON.stringify({ free_tier_unique_ips:
|
|
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 }));
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
if (req.url === '/trial-extension' && req.method === 'POST') {
|
|
854
|
+
let body = ''; req.on('data', c => body += c);
|
|
855
|
+
req.on('end', async () => {
|
|
856
|
+
try {
|
|
857
|
+
const { name, email, use_case } = JSON.parse(body);
|
|
858
|
+
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; }
|
|
859
|
+
const emailKey = 'trial:' + email.toLowerCase().trim();
|
|
860
|
+
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; }
|
|
861
|
+
const rawIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
862
|
+
const ip = rawIp.split(',')[0].trim();
|
|
863
|
+
const monthKey = getMonthKey(ip);
|
|
864
|
+
const currentCalls = freeTierUsage.get(monthKey) || 0;
|
|
865
|
+
freeTierUsage.set(monthKey, Math.max(0, currentCalls - TRIAL_EXTENSION_CALLS));
|
|
866
|
+
trialExtensions.set(emailKey, { name, email, use_case: use_case || '', ip, granted_at: nowISO() });
|
|
867
|
+
saveStats();
|
|
868
|
+
await sendEmail('ojas@kordagencies.com', 'Data Compliance MCP -- Trial Extension: ' + name,
|
|
869
|
+
'<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>');
|
|
870
|
+
await sendEmail(email, TRIAL_EXTENSION_CALLS + ' extra free calls added -- Data Compliance MCP',
|
|
871
|
+
'<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>');
|
|
872
|
+
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
873
|
+
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 }));
|
|
874
|
+
} catch(e) { res.writeHead(400, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: e.message, agent_action: 'RETRY_IN_2_MIN' })); }
|
|
875
|
+
});
|
|
772
876
|
return;
|
|
773
877
|
}
|
|
774
878
|
|
|
@@ -793,7 +897,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
793
897
|
let response;
|
|
794
898
|
|
|
795
899
|
if (request.method === 'initialize') {
|
|
796
|
-
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
|
|
900
|
+
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.' } } };
|
|
797
901
|
} else if (request.method === 'notifications/initialized') {
|
|
798
902
|
res.writeHead(204, cors); res.end(); return;
|
|
799
903
|
} else if (request.method === 'tools/list') {
|
|
@@ -813,9 +917,11 @@ const server = http.createServer(async (req, res) => {
|
|
|
813
917
|
return;
|
|
814
918
|
}
|
|
815
919
|
|
|
816
|
-
const
|
|
920
|
+
const rawIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
921
|
+
const ip = rawIp.split(',')[0].trim();
|
|
817
922
|
usageLog.push({ tool: name, tier: access.tier, time: nowISO(), ip: ip.slice(0, 8) + '...' });
|
|
818
923
|
if (usageLog.length > 1000) usageLog.shift();
|
|
924
|
+
toolUsageCounts[name] = (toolUsageCounts[name] || 0) + 1;
|
|
819
925
|
saveStats();
|
|
820
926
|
|
|
821
927
|
const result = await executeTool(name, toolArgs || {}, access.tier);
|
|
@@ -859,7 +965,7 @@ function setupStdio() {
|
|
|
859
965
|
try { req = JSON.parse(line); } catch(e) { return; }
|
|
860
966
|
let response;
|
|
861
967
|
if (req.method === 'initialize') {
|
|
862
|
-
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
|
|
968
|
+
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.' } } };
|
|
863
969
|
} else if (req.method === 'notifications/initialized') {
|
|
864
970
|
return;
|
|
865
971
|
} else if (req.method === 'tools/list') {
|
|
@@ -888,6 +994,7 @@ setupStdio();
|
|
|
888
994
|
|
|
889
995
|
server.listen(PORT, () => {
|
|
890
996
|
loadStats();
|
|
997
|
+
loadApiKeys();
|
|
891
998
|
console.log('Data Compliance Classifier MCP v' + VERSION + ' running on port ' + PORT);
|
|
892
999
|
console.log('Tools: 2 (validate_data_safety, get_safety_report)');
|
|
893
1000
|
console.log('Free tier: ' + FREE_TIER_LIMIT + ' classifications/IP/month');
|