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