url-safety-validator-mcp 1.2.13 → 1.2.15

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 CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  All notable changes to URL Safety Validator MCP are documented here.
4
4
 
5
+ ## [1.2.15] — 2026-06-11
6
+ - fix: bump version past existing npm publish (1.2.14 already on registry)
7
+
8
+ ## [1.2.14] — 2026-06-11
9
+ - feat: per-tool kill switch + per-minute rate limiting on AI tools
10
+
5
11
  ## [1.2.13] — 2026-06-08
6
12
  - fix: BEFORE trigger language, consequence-first limit error
7
13
 
package/icon.svg ADDED
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
2
+ <rect width="32" height="32" rx="6" fill="#080A0F"/>
3
+ <circle cx="16" cy="16" r="10" fill="none" stroke="#00E5C3" stroke-width="2"/>
4
+ <polyline points="11,16 14.5,19.5 21,12.5" fill="none" stroke="#00E5C3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
5
+ </svg>
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.13",
4
+ "version": "1.2.15",
5
5
  "description": "URL safety checker for AI agents. Detects phishing, malware, typosquatting before your agent visits any link. BLOCK/ALLOW verdict in one call.",
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.13';
8
+ const VERSION = '1.2.15';
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;
@@ -29,6 +29,22 @@ const toolUsageCounts = {};
29
29
  const trialExtensions = new Map();
30
30
  const TRIAL_EXTENSION_CALLS = 10;
31
31
 
32
+ const perMinuteUsage = new Map();
33
+
34
+ function checkPerMinuteLimit(ip, toolName, limit) {
35
+ const minuteKey = ip + ':' + toolName + ':' + new Date().toISOString().slice(0, 16);
36
+ const count = perMinuteUsage.get(minuteKey) || 0;
37
+ if (count >= limit) return false;
38
+ perMinuteUsage.set(minuteKey, count + 1);
39
+ if (perMinuteUsage.size > 10000) {
40
+ const currentMinute = new Date().toISOString().slice(0, 16);
41
+ for (const [key] of perMinuteUsage) {
42
+ if (!key.includes(currentMinute)) perMinuteUsage.delete(key);
43
+ }
44
+ }
45
+ return true;
46
+ }
47
+
32
48
  const REDIS_PREFIX = 'url';
33
49
  const FREE_TIER_REDIS_KEY = 'url:free_tier_usage';
34
50
  const UPSTASH_URL = process.env.UPSTASH_REDIS_REST_URL;
@@ -515,11 +531,16 @@ function setupStdio() {
515
531
  } else if (request.method === 'prompts/list') {
516
532
  response = { jsonrpc: '2.0', id: request.id, result: { prompts: [] } };
517
533
  } else if (request.method === 'tools/call' && request.params?.name === 'check_url') {
518
- const url = request.params?.arguments?.url;
519
- if (!url) { response = { jsonrpc: '2.0', id: request.id, error: { code: -32602, message: 'url parameter required' } }; }
520
- else {
521
- const result = await checkUrl(url);
522
- response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } };
534
+ const _ks = 'TOOL_DISABLED_CHECK_URL';
535
+ if (process.env[_ks] === 'true') {
536
+ response = { jsonrpc: '2.0', id: request.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 }) }] } };
537
+ } else {
538
+ const url = request.params?.arguments?.url;
539
+ if (!url) { response = { jsonrpc: '2.0', id: request.id, error: { code: -32602, message: 'url parameter required' } }; }
540
+ else {
541
+ const result = await checkUrl(url);
542
+ response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } };
543
+ }
523
544
  }
524
545
  } else {
525
546
  response = { jsonrpc: '2.0', id: request.id, error: { code: -32601, message: 'Method not found: ' + request.method } };
@@ -760,6 +781,11 @@ const server = http.createServer(async (req, res) => {
760
781
  } else if (request.method === 'prompts/list') {
761
782
  response = { jsonrpc: '2.0', id: request.id, result: { prompts: [] } };
762
783
  } else if (request.method === 'tools/call' && request.params?.name === 'check_url') {
784
+ if (process.env['TOOL_DISABLED_CHECK_URL'] === 'true') {
785
+ response = { jsonrpc: '2.0', id: request.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 }) }] } };
786
+ } else if (!checkPerMinuteLimit(clientIp, 'check_url', 5)) {
787
+ response = { 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' }) }] } };
788
+ } else {
763
789
  const url = request.params?.arguments?.url;
764
790
  if (!url) {
765
791
  response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify({ error: 'url parameter required', likely_cause: 'required field missing or malformed URL provided', retryable: false, retry_after_ms: null, fallback_tool: null, agent_action: 'Retry with a url parameter value. Example: {"url":"https://example.com"}', category: 'invalid_input', trace_id: crypto.randomBytes(8).toString('hex'), _disclaimer: LEGAL_DISCLAIMER }) }] } };
@@ -781,6 +807,7 @@ const server = http.createServer(async (req, res) => {
781
807
  response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } };
782
808
  }
783
809
  }
810
+ }
784
811
  } else {
785
812
  response = { jsonrpc: '2.0', id: request.id, error: { code: -32601, message: 'Method not found: ' + request.method } };
786
813
  }