url-safety-validator-mcp 1.2.7 → 1.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/server.js +36 -6
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "url-safety-validator-mcp",
3
3
  "mcpName": "io.github.OjasKord/url-safety-validator-mcp",
4
- "version": "1.2.7",
4
+ "version": "1.2.8",
5
5
  "description": "AI-powered URL safety validator MCP server. SAFE/SUSPICIOUS/DANGEROUS verdict for agents.",
6
6
  "main": "src/server.js",
7
7
  "scripts": {
package/src/server.js CHANGED
@@ -5,7 +5,7 @@ const fs = require('fs');
5
5
  const crypto = require('crypto');
6
6
  const { Readable } = require('stream');
7
7
 
8
- const VERSION = '1.2.7';
8
+ const VERSION = '1.2.8';
9
9
  const PRO_UPGRADE_URL = 'https://buy.stripe.com/5kQeVc9Ah4n3c8c0h2ebu0t';
10
10
  const ENTERPRISE_UPGRADE_URL = 'https://buy.stripe.com/4gMdR88wddXDfko0h2ebu0u';
11
11
  const PORT = process.env.PORT || 3000;
@@ -25,18 +25,23 @@ const FREE_LIMIT = 10;
25
25
  let stats = { free_tier_calls_by_ip: {}, total_checks: 0, safe_count: 0, suspicious_count: 0, dangerous_count: 0, started_at: new Date().toISOString() };
26
26
  const apiKeys = new Map();
27
27
  const usageLog = [];
28
+ const toolUsageCounts = {};
29
+ const trialExtensions = new Map();
30
+ const TRIAL_EXTENSION_CALLS = 10;
28
31
 
29
32
  function loadStats() {
30
33
  try {
31
34
  const data = JSON.parse(fs.readFileSync(PERSIST_FILE, 'utf8'));
32
35
  stats = data.stats || stats;
33
36
  if (data.api_keys) data.api_keys.forEach(([k, v]) => apiKeys.set(k, v));
37
+ if (data.toolUsageCounts) Object.assign(toolUsageCounts, data.toolUsageCounts);
38
+ if (data.trialExtensions) data.trialExtensions.forEach(([k, v]) => trialExtensions.set(k, v));
34
39
  } catch(e) { /* fresh start */ }
35
40
  }
36
41
 
37
42
  function saveStats() {
38
43
  try {
39
- fs.writeFileSync(PERSIST_FILE, JSON.stringify({ stats, api_keys: [...apiKeys.entries()] }));
44
+ fs.writeFileSync(PERSIST_FILE, JSON.stringify({ stats, api_keys: [...apiKeys.entries()], toolUsageCounts, trialExtensions: [...trialExtensions.entries()] }));
40
45
  } catch(e) {}
41
46
  }
42
47
 
@@ -473,10 +478,8 @@ const server = http.createServer(async (req, res) => {
473
478
  const ipMap = stats.free_tier_calls_by_ip || {};
474
479
  const free_tier_unique_ips = Object.keys(ipMap).length;
475
480
  const free_tier_total_calls = Object.values(ipMap).reduce((t, m) => t + Object.values(m).reduce((a,b) => a+b, 0), 0);
476
- const toolCounts = {};
477
- usageLog.forEach(e => { toolCounts[e.tool] = (toolCounts[e.tool] || 0) + 1; });
478
481
  res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
479
- res.end(JSON.stringify({ version: VERSION, total_checks: stats.total_checks, safe_count: stats.safe_count, suspicious_count: stats.suspicious_count, dangerous_count: stats.dangerous_count, free_tier_unique_ips, free_tier_total_calls, paid_keys_issued: apiKeys.size, started_at: stats.started_at, tool_usage: toolCounts, recent_calls: usageLog.slice(-20).reverse() }));
482
+ res.end(JSON.stringify({ version: VERSION, total_checks: stats.total_checks, safe_count: stats.safe_count, suspicious_count: stats.suspicious_count, dangerous_count: stats.dangerous_count, free_tier_unique_ips, free_tier_total_calls, paid_keys_issued: apiKeys.size, started_at: stats.started_at, tool_usage: toolUsageCounts, recent_calls: usageLog.slice(-20).reverse(), trial_extensions_granted: trialExtensions.size }));
480
483
  return;
481
484
  }
482
485
 
@@ -486,6 +489,32 @@ const server = http.createServer(async (req, res) => {
486
489
  return;
487
490
  }
488
491
 
492
+ if (req.url === '/trial-extension' && req.method === 'POST') {
493
+ let body = ''; req.on('data', c => body += c);
494
+ req.on('end', async () => {
495
+ try {
496
+ const { name, email, use_case } = JSON.parse(body);
497
+ 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; }
498
+ const emailKey = 'trial:' + email.toLowerCase().trim();
499
+ 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: PRO_UPGRADE_URL, agent_action: 'INFORM_USER_TRIAL_ALREADY_USED' })); return; }
500
+ const clientIp = (req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown').split(',')[0].trim();
501
+ const month = getMonthKey();
502
+ if (!stats.free_tier_calls_by_ip[clientIp]) stats.free_tier_calls_by_ip[clientIp] = {};
503
+ const current = stats.free_tier_calls_by_ip[clientIp][month] || 0;
504
+ stats.free_tier_calls_by_ip[clientIp][month] = Math.max(0, current - TRIAL_EXTENSION_CALLS);
505
+ trialExtensions.set(emailKey, { name, email, use_case: use_case || '', ip: clientIp, granted_at: nowISO() });
506
+ saveStats();
507
+ await sendEmail('ojas@kordagencies.com', 'URL Safety Validator MCP -- Trial Extension: ' + name,
508
+ '<p><b>Name:</b> ' + name + '<br><b>Email:</b> ' + email + '<br><b>Use case:</b> ' + (use_case || 'Not provided') + '<br><b>IP:</b> ' + clientIp + '<br><b>Calls granted:</b> ' + TRIAL_EXTENSION_CALLS + '</p>');
509
+ await sendEmail(email, TRIAL_EXTENSION_CALLS + ' extra free calls added -- URL Safety Validator MCP',
510
+ '<p>Hi ' + name + ',</p><p>Your ' + TRIAL_EXTENSION_CALLS + ' extra free calls have been added. You can keep using URL Safety Validator MCP right now -- no action needed.</p><p>When you need more, Pro is $20/month for 500 calls (never expire): ' + PRO_UPGRADE_URL + '</p><p>Ojas<br>kordagencies.com</p>');
511
+ res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
512
+ 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: PRO_UPGRADE_URL }));
513
+ } catch(e) { res.writeHead(400, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: e.message, agent_action: 'RETRY_IN_2_MIN' })); }
514
+ });
515
+ return;
516
+ }
517
+
489
518
  if (req.url === '/webhook/stripe' && req.method === 'POST') {
490
519
  let rawBody = '';
491
520
  req.on('data', c => rawBody += c);
@@ -543,11 +572,12 @@ const server = http.createServer(async (req, res) => {
543
572
  } else {
544
573
  const tier = checkTier(clientIp, apiKey);
545
574
  if (!tier.allowed) {
546
- response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify({ error: 'Free tier limit reached. Get 500 calls for $20 at ' + PRO_UPGRADE_URL + ' -- calls never expire.', likely_cause: 'free tier monthly limit reached', retryable: false, retry_after_ms: null, fallback_tool: null, agent_action: 'Inform user that free quota is exhausted. Get 500 calls for $20 at ' + PRO_UPGRADE_URL + ' -- calls never expire.', category: 'rate_limit', trace_id: crypto.randomBytes(8).toString('hex'), upgrade_url: PRO_UPGRADE_URL, _disclaimer: LEGAL_DISCLAIMER }) }] } };
575
+ response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify({ error: 'Free tier limit of ' + FREE_LIMIT + ' calls/month reached. Option 1: POST /trial-extension with {"name":"...","email":"...","use_case":"..."} for 10 extra free calls. Option 2: Upgrade at ' + PRO_UPGRADE_URL + ' (500 calls, never expire).', likely_cause: 'free tier monthly limit reached', retryable: false, retry_after_ms: null, fallback_tool: null, agent_action: 'Inform user that free quota is exhausted.', category: 'rate_limit', trace_id: crypto.randomBytes(8).toString('hex'), upgrade_url: PRO_UPGRADE_URL, trial_extension: { endpoint: '/trial-extension', method: 'POST', body: { name: 'string', email: 'string', use_case: 'string' } }, _disclaimer: LEGAL_DISCLAIMER }) }] } };
547
576
  } else {
548
577
  recordCall(clientIp, apiKey);
549
578
  const result = await checkUrl(url);
550
579
  usageLog.push({ tool: 'check_url', ip: clientIp, tier: tier.paid ? 'paid' : 'free', timestamp: nowISO() });
580
+ toolUsageCounts['check_url'] = (toolUsageCounts['check_url'] || 0) + 1;
551
581
  if (tier.remaining <= 4 && !tier.paid) {
552
582
  result._notice = 'Warning: ' + (tier.remaining - 1) + ' free calls remaining this month. Get 500 calls for $20 at ' + PRO_UPGRADE_URL + ' -- calls never expire.';
553
583
  }