url-safety-validator-mcp 1.2.1 → 1.2.2

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.
@@ -0,0 +1,13 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(git add *)",
5
+ "Bash(git commit *)",
6
+ "Bash(git push *)",
7
+ "Bash(railway up *)",
8
+ "Bash(curl -sf https://url-safety-validator-mcp-production.up.railway.app/health)",
9
+ "Bash(curl -si -X OPTIONS https://url-safety-validator-mcp-production.up.railway.app/health -H \"Origin: https://bizfile.forsenia.in\")",
10
+ "Bash(curl -si https://url-safety-validator-mcp-production.up.railway.app/health)"
11
+ ]
12
+ }
13
+ }
package/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  All notable changes to URL Safety Validator MCP are documented here.
4
4
 
5
+ ## [1.2.2] — 2026-04-25
6
+
7
+ ### Fixed
8
+ - CRITICAL: Stripe webhook now sends API key via Resend email on `checkout.session.completed` -- paying customers were not receiving their keys
9
+ - `agent_action` field added to `check_url` result (BLOCK / FLAG_AND_PROCEED / ALLOW) -- field was promised in tool description but missing from response
10
+ - `agent_action` and `likely_cause` added to all error responses
11
+ - `/stats` endpoint now returns `tool_usage` and `recent_calls` fields -- dashboard was showing `--` for both
12
+
13
+ ### Improved
14
+ - `check_url` tool description updated: source hostnames, latency signal, corrected agent_action guidance
15
+ - `serverInfo` description added to both HTTP and stdio initialize responses -- improves Smithery and Claude Desktop discoverability
16
+ - `source_url` corrected from kordagencies.com to webrisk.googleapis.com
17
+
5
18
  ## [1.0.0] — 2026-04-22
6
19
 
7
20
  ### Initial Release
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.1",
4
+ "version": "1.2.2",
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/server.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.OjasKord/url-safety-validator-mcp",
4
- "version": "1.2.0",
4
+ "title": "URL Safety Validator MCP",
5
5
  "description": "AI URL safety validator: SAFE/SUSPICIOUS/DANGEROUS verdict, trust score, threat intel.",
6
- "title": "URL Safety Validator",
6
+ "version": "1.2.2",
7
7
  "websiteUrl": "https://kordagencies.com",
8
8
  "repository": {
9
9
  "url": "https://github.com/OjasKord/url-safety-validator-mcp",
@@ -12,36 +12,15 @@
12
12
  "packages": [
13
13
  {
14
14
  "registryType": "npm",
15
- "registryBaseUrl": "https://registry.npmjs.org",
16
15
  "identifier": "url-safety-validator-mcp",
17
- "version": "1.2.0",
16
+ "version": "1.2.2",
18
17
  "transport": { "type": "stdio" },
19
18
  "environmentVariables": [
20
- {
21
- "name": "ANTHROPIC_API_KEY",
22
- "description": "Anthropic API key for AI trust scoring",
23
- "isRequired": true,
24
- "isSecret": true
25
- },
26
- {
27
- "name": "GOOGLE_WEB_RISK_API_KEY",
28
- "description": "Google Web Risk API key (commercial). Server degrades gracefully without it.",
29
- "isRequired": false,
30
- "isSecret": true
31
- },
32
- {
33
- "name": "GOOGLE_SAFE_BROWSING_API_KEY",
34
- "description": "Google Safe Browsing API key (free tier available).",
35
- "isRequired": false,
36
- "isSecret": true
37
- }
19
+ { "name": "ANTHROPIC_API_KEY", "description": "Anthropic API key for AI trust scoring", "isRequired": true, "isSecret": true },
20
+ { "name": "GOOGLE_WEB_RISK_API_KEY", "description": "Google Web Risk API key (commercial). Degrades gracefully without it.", "isRequired": false, "isSecret": true },
21
+ { "name": "GOOGLE_SAFE_BROWSING_API_KEY", "description": "Google Safe Browsing API key (free tier available).", "isRequired": false, "isSecret": true }
38
22
  ]
39
23
  }
40
24
  ],
41
- "remotes": [
42
- {
43
- "type": "streamable-http",
44
- "url": "https://url-safety-validator-mcp-production.up.railway.app"
45
- }
46
- ]
25
+ "remotes": [{ "type": "streamable-http", "url": "https://url-safety-validator-mcp-production.up.railway.app" }]
47
26
  }
package/src/server.js CHANGED
@@ -5,13 +5,14 @@ const fs = require('fs');
5
5
  const crypto = require('crypto');
6
6
  const { Readable } = require('stream');
7
7
 
8
- const VERSION = '1.2.1';
8
+ const VERSION = '1.2.2';
9
9
  const PORT = process.env.PORT || 3000;
10
10
  const STATS_KEY = process.env.STATS_KEY || 'ojas2026';
11
11
  const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || '';
12
12
  const GOOGLE_WEB_RISK_API_KEY = process.env.GOOGLE_WEB_RISK_API_KEY || '';
13
13
  const GOOGLE_SAFE_BROWSING_API_KEY = process.env.GOOGLE_SAFE_BROWSING_API_KEY || '';
14
14
  const STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET || '';
15
+ const RESEND_API_KEY = process.env.RESEND_API_KEY || '';
15
16
  const PERSIST_FILE = '/tmp/urlsafety_stats.json';
16
17
 
17
18
  const LEGAL_DISCLAIMER = 'Results sourced from Google Web Risk, Google Safe Browsing, and AI analysis. We do not log or store your query content. Results are for informational purposes only and do not constitute security advice. Verdict is a risk signal -- not a guarantee of safety or danger. Provider maximum liability is limited to subscription fees paid in the preceding 3 months. Full terms: kordagencies.com/terms.html';
@@ -21,6 +22,7 @@ const FREE_LIMIT = 10;
21
22
  // ─── Stats ────────────────────────────────────────────────────────────────────
22
23
  let stats = { free_tier_calls_by_ip: {}, total_checks: 0, safe_count: 0, suspicious_count: 0, dangerous_count: 0, started_at: new Date().toISOString() };
23
24
  const apiKeys = new Map();
25
+ const usageLog = [];
24
26
 
25
27
  function loadStats() {
26
28
  try {
@@ -40,6 +42,26 @@ loadStats();
40
42
 
41
43
  function nowISO() { return new Date().toISOString(); }
42
44
 
45
+ // ─── Email ────────────────────────────────────────────────────────────────────
46
+ async function sendEmail(to, subject, html) {
47
+ return new Promise((resolve) => {
48
+ const body = JSON.stringify({ from: 'URL Safety Validator <ojas@kordagencies.com>', to: [to], subject, html });
49
+ const req = https.request({
50
+ hostname: 'api.resend.com', path: '/emails', method: 'POST',
51
+ headers: { 'Authorization': 'Bearer ' + RESEND_API_KEY, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }
52
+ }, res => { let d = ''; res.on('data', c => d += c); res.on('end', () => resolve({ status: res.statusCode, body: d })); });
53
+ req.on('error', e => resolve({ error: e.message }));
54
+ req.write(body); req.end();
55
+ });
56
+ }
57
+
58
+ async function sendApiKeyEmail(email, apiKey, plan) {
59
+ const planLabel = plan === 'enterprise' ? 'Enterprise' : 'Pro';
60
+ const limit = plan === 'enterprise' ? 'Unlimited' : '500';
61
+ const html = '<!DOCTYPE html><html><body style="font-family:monospace;background:#080A0F;color:#E8EDF5;padding:40px;max-width:600px;margin:0 auto"><div style="border:1px solid rgba(0,229,195,0.3);border-radius:8px;padding:32px"><div style="color:#00E5C3;font-size:13px;letter-spacing:0.2em;text-transform:uppercase;margin-bottom:24px">URL Safety Validator MCP -- ' + planLabel + ' Plan</div><h1 style="font-size:24px;font-weight:700;margin-bottom:8px;color:#FFFFFF">Your API key is ready.</h1><div style="background:#141B24;border:1px solid rgba(255,255,255,0.1);border-radius:6px;padding:20px;margin-bottom:24px"><div style="color:#5A6478;font-size:11px;text-transform:uppercase;margin-bottom:8px">Your API Key</div><div style="color:#00E5C3;font-size:14px;word-break:break-all">' + apiKey + '</div></div><div style="background:#141B24;border:1px solid rgba(255,255,255,0.1);border-radius:6px;padding:20px;margin-bottom:24px"><div style="color:#5A6478;font-size:11px;text-transform:uppercase;margin-bottom:8px">MCP Config</div><div style="color:#86EFAC;font-size:12px">{"url-safety-validator":{"url":"https://url-safety-validator-mcp-production.up.railway.app","headers":{"x-api-key":"' + apiKey + '"}}}</div></div><div style="background:#141B24;border:1px solid rgba(255,255,255,0.1);border-radius:6px;padding:20px;margin-bottom:24px"><div style="color:#E8EDF5;font-size:13px">Plan: ' + planLabel + ' | URL checks: ' + limit + '/month</div></div><div style="background:#0D1219;border-radius:6px;padding:16px;margin-bottom:24px;font-size:11px;color:#5A6478;line-height:1.7">Results are informational only. Verdict is a risk signal not a safety guarantee. Liability capped at 3 months fees. Full terms: kordagencies.com/terms.html</div><p style="color:#5A6478;font-size:12px">Questions? ojas@kordagencies.com</p></div></body></html>';
62
+ return sendEmail(email, 'Your URL Safety Validator MCP ' + planLabel + ' API Key', html);
63
+ }
64
+
43
65
  // ─── Free/Paid Tier ───────────────────────────────────────────────────────────
44
66
  function getMonthKey() {
45
67
  const d = new Date();
@@ -239,7 +261,13 @@ Rules:
239
261
  async function checkUrl(rawUrl) {
240
262
  const parsed = parseUrl(rawUrl);
241
263
  if (!parsed.valid) {
242
- return { error: 'Invalid URL format. Provide a full URL like https://example.com', url: rawUrl };
264
+ return {
265
+ error: 'Invalid URL format. Provide a full URL like https://example.com',
266
+ url: rawUrl,
267
+ agent_action: 'Fix the URL format before retrying. Ensure it starts with https:// or http://',
268
+ likely_cause: 'URL missing protocol prefix or contains invalid characters',
269
+ _disclaimer: LEGAL_DISCLAIMER
270
+ };
243
271
  }
244
272
 
245
273
  const { href, hostname, protocol } = parsed;
@@ -280,6 +308,7 @@ async function checkUrl(rawUrl) {
280
308
  url: href,
281
309
  hostname,
282
310
  verdict,
311
+ agent_action: verdict === 'DANGEROUS' ? 'BLOCK' : verdict === 'SUSPICIOUS' ? 'FLAG_AND_PROCEED' : 'ALLOW',
283
312
  trust_score,
284
313
  ssl_valid: ssl.valid_ssl,
285
314
  domain_age_days: domainAge.available ? domainAge.domain_age_days : null,
@@ -294,7 +323,7 @@ async function checkUrl(rawUrl) {
294
323
  google_safe_browsing: safeBrowsing.available ? { flagged: safeBrowsing.flagged, threat_types: safeBrowsing.threat_types } : { available: false, reason: safeBrowsing.reason }
295
324
  },
296
325
  checked_at: nowISO(),
297
- source_url: 'https://kordagencies.com',
326
+ source_url: 'webrisk.googleapis.com',
298
327
  _disclaimer: LEGAL_DISCLAIMER
299
328
  };
300
329
 
@@ -305,7 +334,7 @@ async function checkUrl(rawUrl) {
305
334
  // ─── Tool definition ──────────────────────────────────────────────────────────
306
335
  const TOOL_DEFINITION = {
307
336
  name: 'check_url',
308
- description: 'Call this tool BEFORE your agent fetches, visits, or passes any URL from an untrusted source -- emails, scraped pages, user inputs, API responses, documents. Returns SAFE/SUSPICIOUS/DANGEROUS verdict, AI trust score 0-100, threat categories, SSL status, domain age, and redirect chain detection. AI-powered analysis -- NOT a simple database lookup. Cross-checks Google Web Risk and Google Safe Browsing in one call. LEGAL NOTICE: Verdict is a risk signal, not a guarantee. We do not log your query content. Full terms: kordagencies.com/terms.html. Free tier: first 10 calls/month, no API key needed.',
337
+ description: 'Call this tool BEFORE your agent fetches, visits, clicks, or passes any URL from an untrusted source -- emails, scraped pages, user inputs, documents, API responses, or redirects. A DANGEROUS verdict halts the workflow. A SUSPICIOUS verdict flags for review. A SAFE verdict lets your agent proceed. Returns: verdict (SAFE / SUSPICIOUS / DANGEROUS), trust_score (0-100), threat_categories (phishing / malware / social engineering), ssl_valid, domain_age_days, redirect_chain_detected, reasoning, and a machine-readable agent_action (BLOCK / FLAG_AND_PROCEED / ALLOW). AI-powered -- NOT a simple blocklist lookup. Cross-checks Google Web Risk (webrisk.googleapis.com) and Google Safe Browsing in real time. One call before any URL interaction eliminates an entire class of agent security failures. On error, check agent_action: BLOCK if safety cannot be confirmed; PROCEED_WITH_CAUTION for partial signal failures where Web Risk and AI both returned SAFE but ancillary checks (domain age, SSL) are unavailable. Typical response: 3-8s (four parallel external checks plus AI scoring). LEGAL NOTICE: Verdict is a risk signal, not a guarantee. We do not log your query content. Full terms: kordagencies.com/terms.html. Free tier: first 10 calls/month, no API key needed.',
309
338
  inputSchema: {
310
339
  type: 'object',
311
340
  properties: {
@@ -331,7 +360,7 @@ function verifyStripeSignature(body, sig, secret) {
331
360
  const cors = {
332
361
  'Access-Control-Allow-Origin': '*',
333
362
  'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
334
- 'Access-Control-Allow-Headers': 'Content-Type, x-api-key, Authorization'
363
+ 'Access-Control-Allow-Headers': 'Content-Type, x-api-key, x-stats-key'
335
364
  };
336
365
 
337
366
  // ─── MCP stdio transport ──────────────────────────────────────────────────────
@@ -350,7 +379,7 @@ function setupStdio() {
350
379
  const request = JSON.parse(line);
351
380
  let response;
352
381
  if (request.method === 'initialize') {
353
- response = { jsonrpc: '2.0', id: request.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, resources: {}, prompts: {} }, serverInfo: { name: 'url-safety-validator-mcp', version: VERSION } } };
382
+ response = { jsonrpc: '2.0', id: request.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, resources: {}, prompts: {} }, serverInfo: { name: 'url-safety-validator-mcp', version: VERSION, description: 'Real-time URL safety checking for AI agents. Cross-checks Google Web Risk and AI analysis before your agent visits, fetches, or passes any URL. One call eliminates an entire class of agent security failures. 1 tool. Free tier: 10 calls/month.' } } };
354
383
  } else if (request.method === 'notifications/initialized') {
355
384
  continue;
356
385
  } else if (request.method === 'tools/list') {
@@ -380,7 +409,7 @@ function setupStdio() {
380
409
 
381
410
  // ─── HTTP server ──────────────────────────────────────────────────────────────
382
411
  const server = http.createServer(async (req, res) => {
383
- if (req.method === 'OPTIONS') { res.writeHead(204, cors); res.end(); return; }
412
+ if (req.method === 'OPTIONS') { res.writeHead(200, cors); res.end(); return; }
384
413
 
385
414
  if (req.url === '/health' && (req.method === 'GET' || req.method === 'HEAD')) {
386
415
  res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
@@ -427,8 +456,10 @@ const server = http.createServer(async (req, res) => {
427
456
  const ipMap = stats.free_tier_calls_by_ip || {};
428
457
  const free_tier_unique_ips = Object.keys(ipMap).length;
429
458
  const free_tier_total_calls = Object.values(ipMap).reduce((t, m) => t + Object.values(m).reduce((a,b) => a+b, 0), 0);
459
+ const toolCounts = {};
460
+ usageLog.forEach(e => { toolCounts[e.tool] = (toolCounts[e.tool] || 0) + 1; });
430
461
  res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
431
- 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 }));
462
+ 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() }));
432
463
  return;
433
464
  }
434
465
 
@@ -451,10 +482,13 @@ const server = http.createServer(async (req, res) => {
451
482
  if (event.type === 'checkout.session.completed') {
452
483
  const session = event.data.object;
453
484
  const key = 'usv_' + crypto.randomBytes(16).toString('hex');
454
- const email = session.customer_details?.email || 'unknown';
485
+ const email = session.customer_details?.email || session.customer_email || 'unknown';
455
486
  apiKeys.set(key, { email, created_at: nowISO(), plan: 'pro' });
456
487
  saveStats();
457
- console.log(`New paid key issued: ${email}`);
488
+ console.log('[stripe] API key issued to: ' + email);
489
+ if (email && email !== 'unknown') {
490
+ sendApiKeyEmail(email, key, 'pro').catch(err => console.error('[stripe] Email send failed:', err.message));
491
+ }
458
492
  }
459
493
  res.writeHead(200, cors); res.end(JSON.stringify({ received: true }));
460
494
  } catch(e) {
@@ -476,7 +510,7 @@ const server = http.createServer(async (req, res) => {
476
510
  let response;
477
511
 
478
512
  if (request.method === 'initialize') {
479
- response = { jsonrpc: '2.0', id: request.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, resources: {}, prompts: {} }, serverInfo: { name: 'url-safety-validator-mcp', version: VERSION } } };
513
+ response = { jsonrpc: '2.0', id: request.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, resources: {}, prompts: {} }, serverInfo: { name: 'url-safety-validator-mcp', version: VERSION, description: 'Real-time URL safety checking for AI agents. Cross-checks Google Web Risk and AI analysis before your agent visits, fetches, or passes any URL. One call eliminates an entire class of agent security failures. 1 tool. Free tier: 10 calls/month.' } } };
480
514
  } else if (request.method === 'notifications/initialized') {
481
515
  res.writeHead(204, cors); res.end(); return;
482
516
  } else if (request.method === 'tools/list') {
@@ -488,19 +522,17 @@ const server = http.createServer(async (req, res) => {
488
522
  } else if (request.method === 'tools/call' && request.params?.name === 'check_url') {
489
523
  const url = request.params?.arguments?.url;
490
524
  if (!url) {
491
- response = { jsonrpc: '2.0', id: request.id, error: { code: -32602, message: 'url parameter required' } };
525
+ response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify({ error: 'url parameter required', agent_action: 'Retry with a url parameter value. Example: {"url":"https://example.com"}', likely_cause: 'Missing required url argument in tool call', _disclaimer: LEGAL_DISCLAIMER }) }] } };
492
526
  } else {
493
527
  const tier = checkTier(clientIp, apiKey);
494
528
  if (!tier.allowed) {
495
- response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify({ error: 'Free tier limit of 10 calls/month reached. You have seen it work -- upgrade to Pro ($29/month) at kordagencies.com.', upgrade_url: 'https://kordagencies.com' }) }] } };
529
+ response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify({ error: 'Free tier limit of 10 calls/month reached', agent_action: 'Inform user that free quota is exhausted. Upgrade available at kordagencies.com.', upgrade_url: 'https://kordagencies.com', _disclaimer: LEGAL_DISCLAIMER }) }] } };
496
530
  } else {
497
- if (tier.remaining <= 4 && !tier.paid) {
498
- // will add notice to result
499
- }
500
531
  recordCall(clientIp, apiKey);
501
532
  const result = await checkUrl(url);
533
+ usageLog.push({ tool: 'check_url', ip: clientIp, tier: tier.paid ? 'paid' : 'free', timestamp: nowISO() });
502
534
  if (tier.remaining <= 4 && !tier.paid) {
503
- result._notice = `Warning: ${tier.remaining - 1} free calls remaining this month. Upgrade to Pro at kordagencies.com to avoid interruption.`;
535
+ result._notice = 'Warning: ' + (tier.remaining - 1) + ' free calls remaining this month. Upgrade to Pro at kordagencies.com to avoid interruption.';
504
536
  }
505
537
  response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } };
506
538
  }