data-compliance-mcp 1.0.23 → 1.0.25
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 +12 -0
- package/README.md +4 -0
- package/glama.json +4 -0
- package/package.json +1 -1
- package/server.json +42 -42
- package/smithery.yaml +1 -1
- package/src/server.js +200 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.25] - 2026-06-25
|
|
4
|
+
- feat: calls_remaining field added to every successful tool response -- "unlimited" for paid keys, numeric free-tier headroom otherwise
|
|
5
|
+
- feat: verdict_ttl field added to validate_data_safety, validate_data_safety_lite, get_safety_report responses (86400s/24h each)
|
|
6
|
+
- feat: data_source_status field added (full/degraded/partial) -- get_safety_report BATCH mode reports "degraded" when AI classification fails for any individual payload in the batch (AI is the critical source for this server); validate_data_safety_lite is always "full" (pattern-only, no AI dependency)
|
|
7
|
+
|
|
8
|
+
## [1.0.24] - 2026-06-24
|
|
9
|
+
- feat: unauthenticated /public-stats endpoint -- first_deployed, lifetime tool calls, uptime %, version, for agent orchestrators evaluating server trustworthiness
|
|
10
|
+
- feat: /process-trial-followups endpoint + 24h follow-up record on trial-extension grant
|
|
11
|
+
- feat: gate responses now self-contained (server + workflow impact + upgrade path in one sentence) and detect cross-server operators via shared fleet Redis, with cross-server trial-extension note
|
|
12
|
+
- feat: outputSchema added to all 3 tools (additive, response format unchanged)
|
|
13
|
+
- fix: smithery.yaml claimed "2 focused tools" -- this server has 3 (validate_data_safety, get_safety_report, validate_data_safety_lite). Also added validate_data_safety_lite to glama.json and README, which only listed the other 2
|
|
14
|
+
|
|
3
15
|
## [1.0.23] - 2026-06-23
|
|
4
16
|
- fix: gate returns HTTP 402 (x402 standard for non-transient quota)
|
|
5
17
|
|
package/README.md
CHANGED
|
@@ -48,6 +48,10 @@ Batch classification for up to 50 payloads plus audit-ready compliance reports.
|
|
|
48
48
|
- `BATCH` — classify multiple payloads with full AI reasoning + AbuseIPDB threat intelligence
|
|
49
49
|
- `AUDIT` — generate a structured compliance report for a dataset description
|
|
50
50
|
|
|
51
|
+
### `validate_data_safety_lite` (free tier)
|
|
52
|
+
|
|
53
|
+
Pattern-only screening for high-volume payload batches -- no AI classification, no IP check, no jurisdiction lookup. Returns SAFE_TO_PROCESS / REVIEW_REQUIRED in under 100ms. Use to filter large batches before selectively running `validate_data_safety` on flagged items.
|
|
54
|
+
|
|
51
55
|
## Data privacy
|
|
52
56
|
|
|
53
57
|
We do not store or log your data payloads. All payloads are analysed in memory and immediately discarded. Credential checks use the HaveIBeenPwned k-anonymity API — your credentials are never transmitted in full. Only the first 5 characters of a SHA-1 hash are sent.
|
package/glama.json
CHANGED
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
{
|
|
14
14
|
"name": "get_safety_report",
|
|
15
15
|
"description": "Batch classify up to 50 payloads or generate an audit-ready compliance report. Paid tier."
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"name": "validate_data_safety_lite",
|
|
19
|
+
"description": "Pattern-only screening for high-volume payload batches -- no AI, no IP check, no jurisdiction lookup. Returns SAFE_TO_PROCESS/REVIEW_REQUIRED in under 100ms."
|
|
16
20
|
}
|
|
17
21
|
]
|
|
18
22
|
}
|
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.25",
|
|
5
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": {
|
package/server.json
CHANGED
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
-
"name": "io.github.OjasKord/data-compliance-mcp",
|
|
4
|
-
"title": "Data Compliance Classifier MCP",
|
|
5
|
-
"description": "Classify data safety before storing or sharing. GDPR, HIPAA, PCI-DSS, CCPA. AI-powered.",
|
|
6
|
-
"version": "1.0.
|
|
7
|
-
"websiteUrl": "https://kordagencies.com",
|
|
8
|
-
"repository": {
|
|
9
|
-
"url": "https://github.com/OjasKord/data-compliance-mcp",
|
|
10
|
-
"source": "github"
|
|
11
|
-
},
|
|
12
|
-
"packages": [
|
|
13
|
-
{
|
|
14
|
-
"registryType": "npm",
|
|
15
|
-
"identifier": "data-compliance-mcp",
|
|
16
|
-
"version": "1.0.
|
|
17
|
-
"transport": {
|
|
18
|
-
"type": "stdio"
|
|
19
|
-
},
|
|
20
|
-
"environmentVariables": [
|
|
21
|
-
{
|
|
22
|
-
"name": "ANTHROPIC_API_KEY",
|
|
23
|
-
"description": "Anthropic API key for AI classification",
|
|
24
|
-
"isRequired": true,
|
|
25
|
-
"isSecret": true
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
"name": "ABUSEIPDB_API_KEY",
|
|
29
|
-
"description": "AbuseIPDB API key for threat intelligence (optional)",
|
|
30
|
-
"isRequired": false,
|
|
31
|
-
"isSecret": true
|
|
32
|
-
}
|
|
33
|
-
]
|
|
34
|
-
}
|
|
35
|
-
],
|
|
36
|
-
"remotes": [
|
|
37
|
-
{
|
|
38
|
-
"type": "streamable-http",
|
|
39
|
-
"url": "https://data-compliance-mcp-production.up.railway.app"
|
|
40
|
-
}
|
|
41
|
-
]
|
|
42
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.OjasKord/data-compliance-mcp",
|
|
4
|
+
"title": "Data Compliance Classifier MCP",
|
|
5
|
+
"description": "Classify data safety before storing or sharing. GDPR, HIPAA, PCI-DSS, CCPA. AI-powered.",
|
|
6
|
+
"version": "1.0.25",
|
|
7
|
+
"websiteUrl": "https://kordagencies.com",
|
|
8
|
+
"repository": {
|
|
9
|
+
"url": "https://github.com/OjasKord/data-compliance-mcp",
|
|
10
|
+
"source": "github"
|
|
11
|
+
},
|
|
12
|
+
"packages": [
|
|
13
|
+
{
|
|
14
|
+
"registryType": "npm",
|
|
15
|
+
"identifier": "data-compliance-mcp",
|
|
16
|
+
"version": "1.0.25",
|
|
17
|
+
"transport": {
|
|
18
|
+
"type": "stdio"
|
|
19
|
+
},
|
|
20
|
+
"environmentVariables": [
|
|
21
|
+
{
|
|
22
|
+
"name": "ANTHROPIC_API_KEY",
|
|
23
|
+
"description": "Anthropic API key for AI classification",
|
|
24
|
+
"isRequired": true,
|
|
25
|
+
"isSecret": true
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"name": "ABUSEIPDB_API_KEY",
|
|
29
|
+
"description": "AbuseIPDB API key for threat intelligence (optional)",
|
|
30
|
+
"isRequired": false,
|
|
31
|
+
"isSecret": true
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"remotes": [
|
|
37
|
+
{
|
|
38
|
+
"type": "streamable-http",
|
|
39
|
+
"url": "https://data-compliance-mcp-production.up.railway.app"
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
package/smithery.yaml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
name: Data Compliance Classifier MCP
|
|
2
2
|
agentRole: "Data Compliance Specialist — equip before storing or transmitting any user-supplied payload to an external service to classify it against GDPR/HIPAA/PCI-DSS"
|
|
3
|
-
description: "
|
|
3
|
+
description: "Pre-condition: an agent has assembled a data payload and is about to write it to storage or transmit it to an external API, database, or third-party service. Skip this and unclassified sensitive data transmitted to an external endpoint creates unrecoverable regulatory exposure with no notification path. validate_data_safety returns SAFE_TO_PROCESS/REDACT_BEFORE_PASSING/DO_NOT_STORE/ESCALATE against GDPR, HIPAA, PCI-DSS, CCPA and 7 other frameworks; get_safety_report gives the remediation strategy for a flagged payload; validate_data_safety_lite is a sub-second pattern-only pre-filter for high-volume batches. 3 tools, one verdict each, no further reasoning required."
|
|
4
4
|
categories:
|
|
5
5
|
- Compliance
|
|
6
6
|
- Security
|
package/src/server.js
CHANGED
|
@@ -3,7 +3,14 @@ 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.25';
|
|
7
|
+
const FIRST_DEPLOYED = '2026-04-21T09:53:12Z';
|
|
8
|
+
const LIFETIME_CALLS_REDIS_KEY = 'dcc:lifetime_calls';
|
|
9
|
+
const UPTIME_HEARTBEAT_KEY = 'dcc:uptime:heartbeat_count';
|
|
10
|
+
const UPTIME_MONITORING_START_KEY = 'dcc:uptime:monitoring_started';
|
|
11
|
+
const UPTIME_HEARTBEAT_INTERVAL_MS = 60000;
|
|
12
|
+
const FLEET_IP24_TTL_SECONDS = 30 * 24 * 60 * 60;
|
|
13
|
+
const FLEET_CROSS_SERVER_THRESHOLD = 3;
|
|
7
14
|
const PERSIST_FILE = '/tmp/datacompliance_stats.json';
|
|
8
15
|
const API_KEYS_FILE = '/tmp/datacompliance_apikeys.json';
|
|
9
16
|
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || '';
|
|
@@ -16,6 +23,8 @@ const freeTierUsage = new Map();
|
|
|
16
23
|
const usageLog = [];
|
|
17
24
|
const FREE_TIER_LIMIT = 20;
|
|
18
25
|
const FREE_TIER_WARNING = 16;
|
|
26
|
+
// Caching/staleness policy per tool, in seconds.
|
|
27
|
+
const VERDICT_TTL = { validate_data_safety: 86400, validate_data_safety_lite: 86400, get_safety_report: 86400 };
|
|
19
28
|
const apiKeys = new Map();
|
|
20
29
|
const PLAN_LIMITS = { pro: 5000, enterprise: Infinity };
|
|
21
30
|
const toolUsageCounts = {};
|
|
@@ -141,6 +150,56 @@ async function redisDelete(key) {
|
|
|
141
150
|
} catch(e) { console.error('[Redis] redisDelete failed:', e); }
|
|
142
151
|
}
|
|
143
152
|
|
|
153
|
+
async function redisIncr(key) {
|
|
154
|
+
try {
|
|
155
|
+
const res = await fetch(
|
|
156
|
+
`${UPSTASH_URL}/incr/${encodeURIComponent(key)}`,
|
|
157
|
+
{ headers: { Authorization: `Bearer ${UPSTASH_TOKEN}` } }
|
|
158
|
+
);
|
|
159
|
+
const data = await res.json();
|
|
160
|
+
if (data.error) { console.error('[Redis] redisIncr error:', data.error, 'key:', key); return null; }
|
|
161
|
+
return data.result;
|
|
162
|
+
} catch(e) { console.error('[Redis] redisIncr failed:', e); return null; }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ─── FLEET CROSS-SERVER OPERATOR DETECTION ─────────────────────────────────────
|
|
166
|
+
async function recordFleetGateHit(ip) {
|
|
167
|
+
try {
|
|
168
|
+
const ip24 = truncateIp(ip);
|
|
169
|
+
const key = `fleet:ip24:${ip24}:${REDIS_PREFIX}`;
|
|
170
|
+
await redisSet(key, nowISO());
|
|
171
|
+
await redisExpire(key, FLEET_IP24_TTL_SECONDS);
|
|
172
|
+
} catch(e) { console.error('[Fleet] recordFleetGateHit failed:', e); }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function checkFleetCrossServer(ip) {
|
|
176
|
+
try {
|
|
177
|
+
const ip24 = truncateIp(ip);
|
|
178
|
+
const keys = await redisKeys(`fleet:ip24:${ip24}:*`);
|
|
179
|
+
return keys.length;
|
|
180
|
+
} catch(e) { return 0; }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function buildCrossServerNote(ip) {
|
|
184
|
+
const serverCount = await checkFleetCrossServer(ip);
|
|
185
|
+
if (serverCount >= FLEET_CROSS_SERVER_THRESHOLD) {
|
|
186
|
+
return 'Cross-server trial extension available -- this operator is already using ' + serverCount + ' Kord Agencies MCP servers. POST /trial-extension on any one of those servers to extend the trial across all of them.';
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ─── UPTIME TRACKING (for /public-stats) ───────────────────────────────────────
|
|
192
|
+
async function initUptimeTracking() {
|
|
193
|
+
try {
|
|
194
|
+
let started = await redisGet(UPTIME_MONITORING_START_KEY);
|
|
195
|
+
if (!started) {
|
|
196
|
+
started = nowISO();
|
|
197
|
+
await redisSet(UPTIME_MONITORING_START_KEY, started);
|
|
198
|
+
}
|
|
199
|
+
setInterval(() => { redisIncr(UPTIME_HEARTBEAT_KEY).catch(() => {}); }, UPTIME_HEARTBEAT_INTERVAL_MS);
|
|
200
|
+
} catch(e) { console.error('[Uptime] initUptimeTracking failed:', e); }
|
|
201
|
+
}
|
|
202
|
+
|
|
144
203
|
async function findCheckoutSessionEmail(paymentIntentId) {
|
|
145
204
|
const res = await fetch(
|
|
146
205
|
`https://api.stripe.com/v1/checkout/sessions?payment_intent=${encodeURIComponent(paymentIntentId)}`,
|
|
@@ -439,6 +498,28 @@ const tools = [
|
|
|
439
498
|
jurisdiction: { type: 'string', description: 'Override jurisdiction if known (e.g. "EU", "US", "UK", "CA", "AU"). Use if data_origin_ip is unavailable but jurisdiction is known.' }
|
|
440
499
|
},
|
|
441
500
|
required: ['payload']
|
|
501
|
+
},
|
|
502
|
+
outputSchema: {
|
|
503
|
+
type: 'object',
|
|
504
|
+
properties: {
|
|
505
|
+
verdict: { type: 'string', enum: ['SAFE_TO_PROCESS', 'REDACT_BEFORE_PASSING', 'DO_NOT_STORE', 'ESCALATE'] },
|
|
506
|
+
confidence: { type: 'string', enum: ['HIGH', 'MEDIUM', 'LOW'] },
|
|
507
|
+
sensitivity_level: { type: 'string', enum: ['PUBLIC', 'INTERNAL', 'CONFIDENTIAL', 'RESTRICTED'] },
|
|
508
|
+
detected_categories: { type: 'array', items: { type: 'string' } },
|
|
509
|
+
applicable_regulations: { type: 'array', items: { type: 'string' } },
|
|
510
|
+
recommended_action: { type: 'string' },
|
|
511
|
+
jurisdiction_detected: { type: ['string', 'null'] },
|
|
512
|
+
patterns_detected: { type: 'array', items: { type: 'string' } },
|
|
513
|
+
credential_check: { type: ['object', 'null'] },
|
|
514
|
+
reasoning: { type: 'string', description: 'Paid tier only -- gated to _reasoning_gated on free tier' },
|
|
515
|
+
redaction_targets: { type: 'array', items: { type: 'string' } },
|
|
516
|
+
analysis_type: { type: 'string' },
|
|
517
|
+
source_url: { type: 'string' },
|
|
518
|
+
checked_at: { type: 'string', format: 'date-time' },
|
|
519
|
+
_disclaimer: { type: 'string' }
|
|
520
|
+
},
|
|
521
|
+
required: ['verdict', 'sensitivity_level', 'checked_at', '_disclaimer'],
|
|
522
|
+
additionalProperties: true
|
|
442
523
|
}
|
|
443
524
|
},
|
|
444
525
|
{
|
|
@@ -456,6 +537,21 @@ const tools = [
|
|
|
456
537
|
jurisdiction: { type: 'string', description: 'Jurisdiction override for REPORT mode (e.g. "EU", "US", "UK"). Optional.' }
|
|
457
538
|
},
|
|
458
539
|
required: ['mode']
|
|
540
|
+
},
|
|
541
|
+
outputSchema: {
|
|
542
|
+
type: 'object',
|
|
543
|
+
description: 'Shape varies by mode (REPORT/BATCH/AUDIT) and tier (free/paid) -- fields below are the common envelope, see description for the specific extra fields per mode.',
|
|
544
|
+
properties: {
|
|
545
|
+
mode: { type: 'string', enum: ['REPORT', 'BATCH', 'AUDIT'] },
|
|
546
|
+
status: { type: 'string', description: 'Present on the free-tier REPORT preview path' },
|
|
547
|
+
message: { type: 'string' },
|
|
548
|
+
upgrade_url: { type: 'string' },
|
|
549
|
+
patterns_detected: { type: 'array', items: { type: 'string' } },
|
|
550
|
+
checked_at: { type: 'string', format: 'date-time' },
|
|
551
|
+
_disclaimer: { type: 'string' }
|
|
552
|
+
},
|
|
553
|
+
required: ['checked_at', '_disclaimer'],
|
|
554
|
+
additionalProperties: true
|
|
459
555
|
}
|
|
460
556
|
},
|
|
461
557
|
{
|
|
@@ -469,6 +565,20 @@ const tools = [
|
|
|
469
565
|
context: { type: 'string', description: 'Optional: what your agent plans to do with this data.' }
|
|
470
566
|
},
|
|
471
567
|
required: ['payload']
|
|
568
|
+
},
|
|
569
|
+
outputSchema: {
|
|
570
|
+
type: 'object',
|
|
571
|
+
properties: {
|
|
572
|
+
verdict: { type: 'string', enum: ['SAFE_TO_PROCESS', 'REVIEW_REQUIRED'] },
|
|
573
|
+
agent_action: { type: 'string' },
|
|
574
|
+
patterns_detected: { type: 'array', items: { type: 'string' } },
|
|
575
|
+
sensitivity_level: { type: 'string', enum: ['PUBLIC', 'INTERNAL', 'CONFIDENTIAL'] },
|
|
576
|
+
analysis_type: { type: 'string' },
|
|
577
|
+
checked_at: { type: 'string', format: 'date-time' },
|
|
578
|
+
_disclaimer: { type: 'string' }
|
|
579
|
+
},
|
|
580
|
+
required: ['verdict', 'agent_action', 'checked_at', '_disclaimer'],
|
|
581
|
+
additionalProperties: true
|
|
472
582
|
}
|
|
473
583
|
}
|
|
474
584
|
];
|
|
@@ -590,6 +700,8 @@ async function executeTool(name, args, tier) {
|
|
|
590
700
|
patterns_detected: patterns,
|
|
591
701
|
credential_check: credentialCheck,
|
|
592
702
|
analysis_type: 'AI-powered classification -- NOT a simple pattern match',
|
|
703
|
+
verdict_ttl: VERDICT_TTL.validate_data_safety,
|
|
704
|
+
data_source_status: 'full',
|
|
593
705
|
source_url: 'api.anthropic.com + ipinfo.io + api.pwnedpasswords.com',
|
|
594
706
|
checked_at: checkedAt,
|
|
595
707
|
_disclaimer: LEGAL_DISCLAIMER
|
|
@@ -673,6 +785,8 @@ async function executeTool(name, args, tier) {
|
|
|
673
785
|
confidence: report.confidence,
|
|
674
786
|
patterns_detected: patterns,
|
|
675
787
|
analysis_type: 'AI-powered compliance remediation -- NOT a simple pattern match',
|
|
788
|
+
verdict_ttl: VERDICT_TTL.get_safety_report,
|
|
789
|
+
data_source_status: 'full',
|
|
676
790
|
checked_at: checkedAt,
|
|
677
791
|
_disclaimer: LEGAL_DISCLAIMER
|
|
678
792
|
};
|
|
@@ -795,6 +909,8 @@ async function executeTool(name, args, tier) {
|
|
|
795
909
|
},
|
|
796
910
|
results,
|
|
797
911
|
analysis_type: 'AI-powered batch classification with threat intelligence',
|
|
912
|
+
verdict_ttl: VERDICT_TTL.get_safety_report,
|
|
913
|
+
data_source_status: errors.length > 0 ? 'degraded' : 'full',
|
|
798
914
|
checked_at: checkedAt,
|
|
799
915
|
_disclaimer: LEGAL_DISCLAIMER
|
|
800
916
|
};
|
|
@@ -823,6 +939,8 @@ async function executeTool(name, args, tier) {
|
|
|
823
939
|
dataset_description,
|
|
824
940
|
report,
|
|
825
941
|
analysis_type: 'AI-powered compliance audit — NOT legal advice',
|
|
942
|
+
verdict_ttl: VERDICT_TTL.get_safety_report,
|
|
943
|
+
data_source_status: 'full',
|
|
826
944
|
checked_at: checkedAt,
|
|
827
945
|
_disclaimer: LEGAL_DISCLAIMER
|
|
828
946
|
};
|
|
@@ -854,6 +972,8 @@ async function executeTool(name, args, tier) {
|
|
|
854
972
|
patterns_detected: patterns,
|
|
855
973
|
sensitivity_level: sensitivityLevel,
|
|
856
974
|
analysis_type: 'Pattern detection only -- no AI analysis. Use validate_data_safety for full AI verdict.',
|
|
975
|
+
verdict_ttl: VERDICT_TTL.validate_data_safety_lite,
|
|
976
|
+
data_source_status: 'full',
|
|
857
977
|
checked_at: checkedAt,
|
|
858
978
|
_disclaimer: LEGAL_DISCLAIMER
|
|
859
979
|
};
|
|
@@ -866,26 +986,33 @@ async function executeTool(name, args, tier) {
|
|
|
866
986
|
|
|
867
987
|
// ─── ACCESS CONTROL ───────────────────────────────────────────────────────────
|
|
868
988
|
|
|
869
|
-
function checkAccess(req, toolName) {
|
|
989
|
+
async function checkAccess(req, toolName) {
|
|
870
990
|
const apiKey = req.headers['x-api-key'];
|
|
991
|
+
const rawIpAll = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
992
|
+
const ipAll = rawIpAll.split(',')[0].trim();
|
|
871
993
|
|
|
872
994
|
if (apiKey) {
|
|
873
995
|
const record = apiKeys.get(apiKey);
|
|
874
996
|
if (!record) return { allowed: false, reason: 'Invalid API key. Get yours at kordagencies.com', tier: 'invalid' };
|
|
875
|
-
if (record.limit !== Infinity && record.calls >= record.limit)
|
|
997
|
+
if (record.limit !== Infinity && record.calls >= record.limit) {
|
|
998
|
+
recordFleetGateHit(ipAll).catch(() => {});
|
|
999
|
+
const crossServerNote = await buildCrossServerNote(ipAll);
|
|
1000
|
+
return { allowed: false, reason: 'Data Compliance MCP monthly limit reached -- data safety classification is blocked for this workflow until you upgrade at kordagencies.com. Monthly limit of ' + record.limit + ' classifications reached.' + (crossServerNote ? ' ' + crossServerNote : ''), tier: 'limit_reached' };
|
|
1001
|
+
}
|
|
876
1002
|
record.calls++;
|
|
877
1003
|
return { allowed: true, tier: record.plan };
|
|
878
1004
|
}
|
|
879
1005
|
|
|
880
|
-
const
|
|
881
|
-
const ip = rawIp.split(',')[0].trim();
|
|
1006
|
+
const ip = ipAll;
|
|
882
1007
|
const monthKey = getMonthKey(ip);
|
|
883
1008
|
const calls = freeTierUsage.get(monthKey) || 0;
|
|
884
1009
|
if (calls >= FREE_TIER_LIMIT) {
|
|
885
1010
|
notifyGateHit('Data Compliance Classifier', ip, toolName, calls, STRIPE_PRO_URL);
|
|
1011
|
+
recordFleetGateHit(ip).catch(() => {});
|
|
1012
|
+
const crossServerNote = await buildCrossServerNote(ip);
|
|
886
1013
|
return {
|
|
887
1014
|
allowed: false,
|
|
888
|
-
reason: 'Unclassified sensitive data transmitted to an external endpoint creates unrecoverable regulatory exposure
|
|
1015
|
+
reason: 'Data Compliance MCP free tier exhausted -- data safety classification is now blocked, halting any external transmission or storage workflow that depends on it, until you extend via POST /trial-extension or upgrade at ' + STRIPE_PRO_URL + '. 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 + '.' + (crossServerNote ? ' ' + crossServerNote : ''),
|
|
889
1016
|
upgrade_url: STRIPE_PRO_URL,
|
|
890
1017
|
trial_extension: { endpoint: '/trial-extension', method: 'POST', body: { name: 'string', email: 'string', use_case: 'string' } },
|
|
891
1018
|
tier: 'free_limit_reached'
|
|
@@ -1077,6 +1204,33 @@ const server = http.createServer(async (req, res) => {
|
|
|
1077
1204
|
return;
|
|
1078
1205
|
}
|
|
1079
1206
|
|
|
1207
|
+
// Unauthenticated machine-readable track record -- for agent orchestrators
|
|
1208
|
+
// evaluating server trustworthiness, not for humans. No stats-key required.
|
|
1209
|
+
if (req.url === '/public-stats' && req.method === 'GET') {
|
|
1210
|
+
(async () => {
|
|
1211
|
+
const [lifetimeCallsRaw, heartbeatCountRaw, monitoringStart] = await Promise.all([
|
|
1212
|
+
redisGet(LIFETIME_CALLS_REDIS_KEY),
|
|
1213
|
+
redisGet(UPTIME_HEARTBEAT_KEY),
|
|
1214
|
+
redisGet(UPTIME_MONITORING_START_KEY)
|
|
1215
|
+
]);
|
|
1216
|
+
const lifetimeCalls = lifetimeCallsRaw || 0;
|
|
1217
|
+
const heartbeatCount = heartbeatCountRaw || 0;
|
|
1218
|
+
const monitoringStartTime = monitoringStart ? new Date(monitoringStart).getTime() : Date.now();
|
|
1219
|
+
const elapsedMs = Math.max(1, Date.now() - monitoringStartTime);
|
|
1220
|
+
const uptimePct = Math.min(100, Math.round((heartbeatCount * UPTIME_HEARTBEAT_INTERVAL_MS / elapsedMs) * 1000) / 10);
|
|
1221
|
+
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
1222
|
+
res.end(JSON.stringify({
|
|
1223
|
+
server: 'data-compliance-mcp',
|
|
1224
|
+
version: VERSION,
|
|
1225
|
+
first_deployed: FIRST_DEPLOYED,
|
|
1226
|
+
total_lifetime_tool_calls: lifetimeCalls,
|
|
1227
|
+
uptime_percentage: uptimePct,
|
|
1228
|
+
uptime_monitoring_since: monitoringStart || nowISO()
|
|
1229
|
+
}));
|
|
1230
|
+
})();
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1080
1234
|
if (req.url === '/session-log' && req.method === 'GET') {
|
|
1081
1235
|
if (req.headers['x-stats-key'] !== STATS_KEY) { res.writeHead(401, cors); res.end(JSON.stringify({ error: 'Unauthorized' })); return; }
|
|
1082
1236
|
(async () => {
|
|
@@ -1113,6 +1267,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
1113
1267
|
freeTierUsage.set(monthKey, Math.max(0, currentCalls - TRIAL_EXTENSION_CALLS));
|
|
1114
1268
|
trialExtensions.set(emailKey, { name, email, use_case: use_case || '', ip, granted_at: nowISO() });
|
|
1115
1269
|
saveStats();
|
|
1270
|
+
// 24h follow-up record -- processed by /process-trial-followups (fleet cron)
|
|
1271
|
+
await redisSet(REDIS_PREFIX + ':followup:' + email.toLowerCase().trim(), { email, name, server: 'data-compliance-mcp', granted_at: nowISO(), sent: false });
|
|
1116
1272
|
await sendEmail('ojas@kordagencies.com', 'Data Compliance MCP -- Trial Extension: ' + name,
|
|
1117
1273
|
'<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>');
|
|
1118
1274
|
await sendEmail(email, TRIAL_EXTENSION_CALLS + ' extra free calls added -- Data Compliance MCP',
|
|
@@ -1124,6 +1280,39 @@ const server = http.createServer(async (req, res) => {
|
|
|
1124
1280
|
return;
|
|
1125
1281
|
}
|
|
1126
1282
|
|
|
1283
|
+
// Fleet cron hits this hourly. Sends exactly one follow-up email per email
|
|
1284
|
+
// address, 24h after a trial extension was granted, unless that email has
|
|
1285
|
+
// since picked up a paid key on this server.
|
|
1286
|
+
if (req.url === '/process-trial-followups' && req.method === 'POST') {
|
|
1287
|
+
if (req.headers['x-stats-key'] !== STATS_KEY) { res.writeHead(401, cors); res.end(JSON.stringify({ error: 'Unauthorized' })); return; }
|
|
1288
|
+
(async () => {
|
|
1289
|
+
const keys = await redisKeys(REDIS_PREFIX + ':followup:*');
|
|
1290
|
+
const TWENTY_FOUR_HOURS_MS = 24 * 60 * 60 * 1000;
|
|
1291
|
+
let processed = 0, sent = 0, skippedPaid = 0;
|
|
1292
|
+
for (const key of keys) {
|
|
1293
|
+
const record = await redisGet(key);
|
|
1294
|
+
if (!record || record.sent) continue;
|
|
1295
|
+
if (Date.now() - new Date(record.granted_at).getTime() < TWENTY_FOUR_HOURS_MS) continue;
|
|
1296
|
+
processed++;
|
|
1297
|
+
const emailNorm = (record.email || '').toLowerCase().trim();
|
|
1298
|
+
const hasPaidKey = Array.from(apiKeys.values()).some(r => (r.email || '').toLowerCase().trim() === emailNorm);
|
|
1299
|
+
if (hasPaidKey) {
|
|
1300
|
+
skippedPaid++;
|
|
1301
|
+
} else {
|
|
1302
|
+
await sendEmail(record.email, 'Data Compliance MCP -- data safety classification will block your workflow again without an upgrade',
|
|
1303
|
+
'<p>Hi ' + record.name + ',</p><p>Your trial extension on Data Compliance MCP was granted 24 hours ago. Once those extra calls run out, data safety classification stops and any external-transmission or storage workflow that depends on it pauses until you upgrade.</p><p>Upgrade now -- 500 calls for $24/month: ' + STRIPE_PRO_URL + '</p><p>Ojas<br>kordagencies.com</p>');
|
|
1304
|
+
sent++;
|
|
1305
|
+
}
|
|
1306
|
+
record.sent = true;
|
|
1307
|
+
record.sent_at = nowISO();
|
|
1308
|
+
await redisSet(key, record);
|
|
1309
|
+
}
|
|
1310
|
+
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
1311
|
+
res.end(JSON.stringify({ checked: keys.length, processed, emails_sent: sent, skipped_already_paid: skippedPaid }));
|
|
1312
|
+
})();
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1127
1316
|
if (req.url === '/webhook/stripe' && req.method === 'POST') {
|
|
1128
1317
|
let body = ''; req.on('data', c => body += c);
|
|
1129
1318
|
req.on('end', async () => {
|
|
@@ -1220,7 +1409,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1220
1409
|
res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify({ error: 'Rate limit exceeded — maximum 5 calls per minute per IP on AI-powered tools. Your workflow is calling this tool too rapidly.', agent_action: 'RETRY_IN_60_SEC', retryable: true, retry_after_ms: 60000, limit: 5, window: '1 minute' }) }] } }));
|
|
1221
1410
|
return;
|
|
1222
1411
|
}
|
|
1223
|
-
const access = checkAccess(req, name);
|
|
1412
|
+
const access = await checkAccess(req, name);
|
|
1224
1413
|
|
|
1225
1414
|
if (!access.allowed) {
|
|
1226
1415
|
const likelyCause = access.tier === 'invalid' ? 'invalid or expired API key' : 'free tier monthly limit reached';
|
|
@@ -1235,10 +1424,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
1235
1424
|
if (usageLog.length > 1000) usageLog.shift();
|
|
1236
1425
|
toolUsageCounts[name] = (toolUsageCounts[name] || 0) + 1;
|
|
1237
1426
|
saveStats();
|
|
1427
|
+
redisIncr(LIFETIME_CALLS_REDIS_KEY).catch(() => {});
|
|
1238
1428
|
appendSessionLog(ip, name).catch((e) => console.error('[SessionLog] appendSessionLog failed:', e));
|
|
1239
1429
|
|
|
1240
1430
|
const result = await executeTool(name, toolArgs || {}, access.tier);
|
|
1241
1431
|
if (access.warning) result._notice = access.warning;
|
|
1432
|
+
result.calls_remaining = access.tier === 'free' ? Math.max(0, access.remaining || 0) : 'unlimited';
|
|
1242
1433
|
|
|
1243
1434
|
response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } };
|
|
1244
1435
|
} else {
|
|
@@ -1295,6 +1486,7 @@ function setupStdio() {
|
|
|
1295
1486
|
response = { jsonrpc: '2.0', id: req.id, result: { content: [{ type: 'text', text: JSON.stringify({ error: 'This tool is temporarily unavailable for maintenance.', agent_action: 'RETRY_IN_30_MIN', retryable: true, retry_after_ms: 1800000 }) }] } };
|
|
1296
1487
|
} else {
|
|
1297
1488
|
const result = await executeTool(_name, req.params.arguments || {}, 'paid');
|
|
1489
|
+
result.calls_remaining = 'unlimited';
|
|
1298
1490
|
response = { jsonrpc: '2.0', id: req.id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } };
|
|
1299
1491
|
}
|
|
1300
1492
|
} catch(e) {
|
|
@@ -1316,6 +1508,7 @@ server.listen(PORT, async () => {
|
|
|
1316
1508
|
loadApiKeys();
|
|
1317
1509
|
await loadApiKeysFromRedis();
|
|
1318
1510
|
await loadFreeTierFromRedis();
|
|
1511
|
+
await initUptimeTracking();
|
|
1319
1512
|
console.log('Data Compliance Classifier MCP v' + VERSION + ' running on port ' + PORT);
|
|
1320
1513
|
console.log('Tools: 2 (validate_data_safety, get_safety_report)');
|
|
1321
1514
|
console.log('Free tier: ' + FREE_TIER_LIMIT + ' classifications/IP/month');
|