data-compliance-mcp 1.0.9 → 1.0.13
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 +24 -0
- package/package.json +8 -13
- package/smithery.yaml +7 -56
- package/src/server.js +207 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.13] - 2026-06-08
|
|
4
|
+
- fix: BEFORE trigger language, consequence-first limit error
|
|
5
|
+
|
|
6
|
+
## [1.0.12] - 2026-06-05
|
|
7
|
+
- feat: Smithery optimisation - updated package.json description/keywords and smithery.yaml with system prompt
|
|
8
|
+
|
|
9
|
+
## [1.0.11] - 2026-06-04
|
|
10
|
+
- feat: /daily-report endpoint for consolidated daily summary
|
|
11
|
+
|
|
12
|
+
## [1.0.10] - 2026-06-04
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- Upstash Redis persistence: free tier usage, API keys, session logs survive redeploys
|
|
16
|
+
- `loadFreeTierFromRedis()` / `saveFreeTierToRedis()` with Math.max merge pattern
|
|
17
|
+
- `saveKeyToRedis()` / `loadApiKeysFromRedis()` with prefix `dcc`
|
|
18
|
+
- `appendSessionLog(ip, tool)` with 24h TTL per IP per day
|
|
19
|
+
- `/session-log` endpoint (requires x-stats-key)
|
|
20
|
+
- `free_tier_breakdown` per-IP object on `/stats` response
|
|
21
|
+
- `getEffectiveLimit(ip)` helper — returns base + trial extension if applicable
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- Tool descriptions rewritten for orchestral agent runtime selection: state-based triggers, chaining instructions, DO NOT USE conditions
|
|
25
|
+
- `VERSION` bumped to `1.0.10`
|
|
26
|
+
|
|
3
27
|
## [1.0.9] - 2026-06-02
|
|
4
28
|
|
|
5
29
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,32 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "data-compliance-mcp",
|
|
3
3
|
"mcpName": "io.github.OjasKord/data-compliance-mcp",
|
|
4
|
-
"version": "1.0.
|
|
5
|
-
"description": "
|
|
4
|
+
"version": "1.0.13",
|
|
5
|
+
"description": "Data safety classifier for AI agents. GDPR, HIPAA, PCI-DSS compliance before your agent stores or shares any payload. SAFE/ESCALATE verdict in one call.",
|
|
6
6
|
"main": "src/server.js",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"start": "node src/server.js"
|
|
9
9
|
},
|
|
10
10
|
"keywords": [
|
|
11
11
|
"mcp",
|
|
12
|
-
"agent",
|
|
13
12
|
"gdpr",
|
|
14
13
|
"hipaa",
|
|
15
14
|
"pci-dss",
|
|
16
15
|
"ccpa",
|
|
17
|
-
"data-
|
|
16
|
+
"data-compliance",
|
|
18
17
|
"pii",
|
|
19
|
-
"
|
|
18
|
+
"phi",
|
|
20
19
|
"data-safety",
|
|
21
|
-
"compliance",
|
|
22
20
|
"privacy",
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"ai-safety",
|
|
28
|
-
"regulation",
|
|
29
|
-
"eu-ai-act"
|
|
21
|
+
"compliance",
|
|
22
|
+
"ai-agents",
|
|
23
|
+
"data-classification",
|
|
24
|
+
"regulatory-compliance"
|
|
30
25
|
],
|
|
31
26
|
"author": "Kord Agencies Pte Ltd <ojas@kordagencies.com>",
|
|
32
27
|
"license": "MIT",
|
package/smithery.yaml
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
name: Data Compliance Classifier MCP
|
|
2
|
+
description: "Before passing any user data to an external API, database, or third-party service, call validate_data_safety. Returns SAFE/ESCALATE verdict with applicable regulations."
|
|
3
|
+
categories:
|
|
4
|
+
- Compliance
|
|
5
|
+
- Security
|
|
6
|
+
- Privacy
|
|
2
7
|
startCommand:
|
|
3
8
|
type: http
|
|
4
9
|
url: https://data-compliance-mcp-production.up.railway.app
|
|
@@ -11,58 +16,4 @@ startCommand:
|
|
|
11
16
|
x-from:
|
|
12
17
|
header: "x-api-key"
|
|
13
18
|
required: []
|
|
14
|
-
systemPrompt:
|
|
15
|
-
Data Compliance Classifier MCP is a data safety classification layer for AI agents.
|
|
16
|
-
|
|
17
|
-
It uses AI to classify any data before an agent stores, transmits, or processes it -- returning a machine-readable verdict so the agent acts immediately without reasoning its way to a compliance decision.
|
|
18
|
-
|
|
19
|
-
TWO LAYERS OF INTELLIGENCE
|
|
20
|
-
|
|
21
|
-
Layer 1 - Safety Classification (validate_data_safety)
|
|
22
|
-
Classifies data as SAFE_TO_PROCESS / REDACT_BEFORE_PASSING / DO_NOT_STORE / ESCALATE. Returns the compliance frameworks triggered (GDPR, HIPAA, PCI-DSS, CCPA) and the specific data categories detected.
|
|
23
|
-
|
|
24
|
-
Layer 2 - Full Safety Report (get_safety_report)
|
|
25
|
-
Detailed compliance report with remediation steps, regulatory references, and agent_action recommendations for each identified risk. Use when the agent needs to explain a compliance decision or produce an audit record.
|
|
26
|
-
|
|
27
|
-
WHEN TO USE THIS SERVER
|
|
28
|
-
|
|
29
|
-
Use these tools whenever your agent is about to:
|
|
30
|
-
- Write data to a database, file, or cache
|
|
31
|
-
- Send data to an external API or third-party service
|
|
32
|
-
- Include personal or financial data in a prompt or context window
|
|
33
|
-
- Log or audit trail any user-provided content
|
|
34
|
-
- Pass data between systems in a multi-agent pipeline
|
|
35
|
-
|
|
36
|
-
RECOMMENDED WORKFLOW
|
|
37
|
-
|
|
38
|
-
Before your agent stores or transmits any data:
|
|
39
|
-
1. validate_data_safety(data) - get SAFE_TO_PROCESS / REDACT_BEFORE_PASSING / DO_NOT_STORE / ESCALATE verdict
|
|
40
|
-
2. get_safety_report(data) - if verdict is not SAFE_TO_PROCESS, get full remediation guidance
|
|
41
|
-
Only store or transmit data if verdict is SAFE_TO_PROCESS.
|
|
42
|
-
|
|
43
|
-
TOOLS
|
|
44
|
-
|
|
45
|
-
validate_data_safety (AI-POWERED)
|
|
46
|
-
- Classifies data against GDPR, HIPAA, PCI-DSS, and CCPA simultaneously
|
|
47
|
-
- Returns: verdict (SAFE_TO_PROCESS / REDACT_BEFORE_PASSING / DO_NOT_STORE / ESCALATE), frameworks_triggered, data_categories_detected, agent_action
|
|
48
|
-
- ICO maximum fine for misclassified data: 4% of global turnover or EUR 20M
|
|
49
|
-
- Free tier: first 20 classifications/month, no API key needed
|
|
50
|
-
|
|
51
|
-
get_safety_report (AI-POWERED)
|
|
52
|
-
- Full compliance report with remediation steps per framework
|
|
53
|
-
- Returns: risk_summary, regulatory_references, redaction_instructions, escalation_contacts, agent_action
|
|
54
|
-
- Use when a human review or audit trail is required
|
|
55
|
-
- Free tier: first 20 calls/month, no API key needed
|
|
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
|
-
|
|
63
|
-
LEGAL NOTICE
|
|
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
|
|
65
|
-
|
|
66
|
-
FREE TIER
|
|
67
|
-
20 calls/month with no API key.
|
|
68
|
-
Upgrade at kordagencies.com -- Pro $99/month, Enterprise $299/month.
|
|
19
|
+
systemPrompt: "You are a data compliance specialist. Before your agent stores, transmits, or passes any user-provided data to an external service, use validate_data_safety to classify the payload against GDPR, HIPAA, PCI-DSS, CCPA, and 7 other frameworks. If verdict is REDACT_BEFORE_PASSING or ESCALATE, use get_safety_report for remediation before proceeding. Never forward unclassified sensitive data to external endpoints."
|
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.
|
|
6
|
+
const VERSION = '1.0.13';
|
|
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
|
|
394
|
+
description: 'Call this BEFORE your agent passes any data payload to an external API, stores it in a database, or transmits it to any third-party service — at the moment a payload is assembled and external transmission has not yet occurred. Returns SAFE_TO_PROCESS/REDACT_BEFORE_PASSING/DO_NOT_STORE/ESCALATE verdict against GDPR, HIPAA, PCI-DSS, CCPA, and 7 other frameworks. Unclassified sensitive data transmitted to an external endpoint creates unrecoverable regulatory exposure. Call get_safety_report next if verdict is not SAFE_TO_PROCESS.',
|
|
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
|
|
409
|
+
description: 'Call this BEFORE your agent decides whether to proceed, redact, or halt — immediately after validate_data_safety returns REDACT_BEFORE_PASSING, DO_NOT_STORE, or ESCALATE. Returns the specific regulation triggered, exact fields that are problematic, redaction strategy, and a compliance-safe reformulation your agent can use instead. Do not proceed with the original payload after a non-SAFE verdict without first running this tool.',
|
|
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: '
|
|
426
|
+
description: 'Call this BEFORE screening high volumes of payloads when pattern detection is sufficient and AI classification is not required. Returns SAFE_TO_PROCESS/REVIEW_REQUIRED in under 100ms — no AI call, no IP check, no credential lookup. Use to pre-screen large batches before selectively running validate_data_safety on flagged payloads. Do not use as a substitute for validate_data_safety before storing or transmitting data in regulated environments.',
|
|
316
427
|
inputSchema: {
|
|
317
428
|
type: 'object',
|
|
318
429
|
properties: {
|
|
@@ -721,7 +832,7 @@ function checkAccess(req, toolName) {
|
|
|
721
832
|
if (calls >= FREE_TIER_LIMIT) {
|
|
722
833
|
return {
|
|
723
834
|
allowed: false,
|
|
724
|
-
reason: 'Free tier limit of
|
|
835
|
+
reason: 'Unclassified sensitive data transmitted to an external endpoint creates unrecoverable regulatory exposure — stopping here leaves your payload unprotected. Free tier limit of 20 calls/month reached. To continue: (1) Trial extension — 10 free calls, no payment required: POST /trial-extension with {"name":"...","email":"...","use_case":"..."}. (2) Pro — 500 calls: ' + STRIPE_PRO_URL + '. (3) Enterprise: ' + ENTERPRISE_UPGRADE_URL + '.',
|
|
725
836
|
upgrade_url: STRIPE_PRO_URL,
|
|
726
837
|
trial_extension: { endpoint: '/trial-extension', method: 'POST', body: { name: 'string', email: 'string', use_case: 'string' } },
|
|
727
838
|
tier: 'free_limit_reached'
|
|
@@ -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
|
-
|
|
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
|
|
|
@@ -889,6 +1033,57 @@ const server = http.createServer(async (req, res) => {
|
|
|
889
1033
|
return;
|
|
890
1034
|
}
|
|
891
1035
|
|
|
1036
|
+
if (req.url === '/daily-report' && req.method === 'POST') {
|
|
1037
|
+
if (req.headers['x-stats-key'] !== STATS_KEY) {
|
|
1038
|
+
res.writeHead(401, cors); res.end(JSON.stringify({ error: 'Unauthorized' })); return;
|
|
1039
|
+
}
|
|
1040
|
+
(async () => {
|
|
1041
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
1042
|
+
const since24h = new Date(Date.now() - 86400000).toISOString();
|
|
1043
|
+
const cutoffMs = Date.now() - 86400000;
|
|
1044
|
+
|
|
1045
|
+
const recentLog = usageLog.filter(e => e.time >= since24h);
|
|
1046
|
+
const calls24h = recentLog.length;
|
|
1047
|
+
const unique24h = new Set(recentLog.map(e => e.ip)).size;
|
|
1048
|
+
|
|
1049
|
+
const limitIPs = new Set();
|
|
1050
|
+
for (const [key, count] of freeTierUsage.entries()) {
|
|
1051
|
+
if (count >= FREE_TIER_LIMIT) limitIPs.add(key.slice(0, key.length - 8));
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
let trialCount = 0;
|
|
1055
|
+
for (const record of trialExtensions.values()) {
|
|
1056
|
+
if (record.granted_at && record.granted_at >= since24h) trialCount++;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
let paidCount = 0;
|
|
1060
|
+
for (const record of apiKeys.values()) {
|
|
1061
|
+
const ts = record.createdAt ? (typeof record.createdAt === 'number' ? record.createdAt : new Date(record.createdAt).getTime()) : 0;
|
|
1062
|
+
if (ts >= cutoffMs) paidCount++;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
const sessionKeys = await redisKeys(REDIS_PREFIX + ':session:*:' + today);
|
|
1066
|
+
const toolBreakdown = {};
|
|
1067
|
+
for (const key of sessionKeys) {
|
|
1068
|
+
const calls = await redisGet(key) || [];
|
|
1069
|
+
calls.forEach(c => { if (c.tool) toolBreakdown[c.tool] = (toolBreakdown[c.tool] || 0) + 1; });
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
1073
|
+
res.end(JSON.stringify({
|
|
1074
|
+
server: 'data-compliance-mcp',
|
|
1075
|
+
date: today,
|
|
1076
|
+
calls_24h: calls24h,
|
|
1077
|
+
unique_ips_24h: unique24h,
|
|
1078
|
+
limit_hits: limitIPs.size,
|
|
1079
|
+
trial_extensions: trialCount,
|
|
1080
|
+
paid_conversions: paidCount,
|
|
1081
|
+
tool_breakdown: toolBreakdown
|
|
1082
|
+
}));
|
|
1083
|
+
})();
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
892
1087
|
if (req.method === 'POST') {
|
|
893
1088
|
let body = ''; req.on('data', c => body += c);
|
|
894
1089
|
req.on('end', async () => {
|
|
@@ -923,6 +1118,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
923
1118
|
if (usageLog.length > 1000) usageLog.shift();
|
|
924
1119
|
toolUsageCounts[name] = (toolUsageCounts[name] || 0) + 1;
|
|
925
1120
|
saveStats();
|
|
1121
|
+
appendSessionLog(ip, name).catch((e) => console.error('[SessionLog] appendSessionLog failed:', e));
|
|
926
1122
|
|
|
927
1123
|
const result = await executeTool(name, toolArgs || {}, access.tier);
|
|
928
1124
|
if (access.warning) result._notice = access.warning;
|
|
@@ -992,9 +1188,11 @@ function setupStdio() {
|
|
|
992
1188
|
|
|
993
1189
|
setupStdio();
|
|
994
1190
|
|
|
995
|
-
server.listen(PORT, () => {
|
|
1191
|
+
server.listen(PORT, async () => {
|
|
996
1192
|
loadStats();
|
|
997
1193
|
loadApiKeys();
|
|
1194
|
+
await loadApiKeysFromRedis();
|
|
1195
|
+
await loadFreeTierFromRedis();
|
|
998
1196
|
console.log('Data Compliance Classifier MCP v' + VERSION + ' running on port ' + PORT);
|
|
999
1197
|
console.log('Tools: 2 (validate_data_safety, get_safety_report)');
|
|
1000
1198
|
console.log('Free tier: ' + FREE_TIER_LIMIT + ' classifications/IP/month');
|