url-safety-validator-mcp 1.1.0 → 1.2.1

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/package.json CHANGED
@@ -1,14 +1,12 @@
1
1
  {
2
2
  "name": "url-safety-validator-mcp",
3
- "version": "1.1.0",
3
+ "mcpName": "io.github.OjasKord/url-safety-validator-mcp",
4
+ "version": "1.2.1",
4
5
  "description": "AI-powered URL safety validator MCP server. SAFE/SUSPICIOUS/DANGEROUS verdict for agents.",
5
6
  "main": "src/server.js",
6
7
  "scripts": {
7
8
  "start": "node src/server.js"
8
9
  },
9
- "license": "UNLICENSED",
10
- "homepage": "https://kordagencies.com",
11
- "mcpName": "io.github.OjasKord/url-safety-validator-mcp",
12
10
  "keywords": [
13
11
  "mcp",
14
12
  "agent",
@@ -19,8 +17,28 @@
19
17
  "malware",
20
18
  "security",
21
19
  "threat-intelligence",
22
- "web-risk"
20
+ "web-risk",
21
+ "url-checker",
22
+ "link-safety",
23
+ "phishing-detection",
24
+ "google-web-risk",
25
+ "safe-browsing",
26
+ "malware-detection",
27
+ "url-scanner",
28
+ "cybersecurity"
23
29
  ],
24
- "author": "Kord Agencies Pte Ltd",
30
+ "author": "Kord Agencies Pte Ltd <ojas@kordagencies.com>",
31
+ "license": "UNLICENSED",
32
+ "homepage": "https://kordagencies.com",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/OjasKord/url-safety-validator-mcp.git"
36
+ },
37
+ "bugs": {
38
+ "url": "https://github.com/OjasKord/url-safety-validator-mcp/issues"
39
+ },
40
+ "engines": {
41
+ "node": ">=18.0.0"
42
+ },
25
43
  "dependencies": {}
26
44
  }
package/server.json CHANGED
@@ -1,7 +1,7 @@
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.0.0",
4
+ "version": "1.2.0",
5
5
  "description": "AI URL safety validator: SAFE/SUSPICIOUS/DANGEROUS verdict, trust score, threat intel.",
6
6
  "title": "URL Safety Validator",
7
7
  "websiteUrl": "https://kordagencies.com",
@@ -14,7 +14,7 @@
14
14
  "registryType": "npm",
15
15
  "registryBaseUrl": "https://registry.npmjs.org",
16
16
  "identifier": "url-safety-validator-mcp",
17
- "version": "1.0.0",
17
+ "version": "1.2.0",
18
18
  "transport": { "type": "stdio" },
19
19
  "environmentVariables": [
20
20
  {
@@ -28,6 +28,12 @@
28
28
  "description": "Google Web Risk API key (commercial). Server degrades gracefully without it.",
29
29
  "isRequired": false,
30
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
31
37
  }
32
38
  ]
33
39
  }
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.1.0';
8
+ const VERSION = '1.2.1';
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 || '';
@@ -14,7 +14,7 @@ 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
15
  const PERSIST_FILE = '/tmp/urlsafety_stats.json';
16
16
 
17
- 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';
17
+ 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';
18
18
 
19
19
  const FREE_LIMIT = 10;
20
20
 
@@ -208,11 +208,10 @@ Return this exact JSON structure:
208
208
 
209
209
  Rules:
210
210
  - trust_score 0-29 = DANGEROUS, 30-64 = SUSPICIOUS, 65-100 = SAFE
211
- - If Google Web Risk flagged it OR PhishTank confirmed it, verdict MUST be DANGEROUS
212
- - If URLhaus found it as malware, verdict MUST be DANGEROUS
211
+ - If Google Web Risk flagged it OR Google Safe Browsing confirmed it, verdict MUST be DANGEROUS
213
212
  - Domain age under 30 days = add "newly_registered" to threat_categories and lower trust_score by at least 20
214
213
  - No SSL on a login-looking URL = lower score significantly
215
- - Consider the full picture a newly registered domain with no database hits is still SUSPICIOUS not SAFE`;
214
+ - Consider the full picture -- a newly registered domain with no database hits is still SUSPICIOUS not SAFE`;
216
215
 
217
216
  const body = JSON.stringify({
218
217
  model: 'claude-sonnet-4-6',
@@ -249,14 +248,14 @@ async function checkUrl(rawUrl) {
249
248
  checkGoogleWebRisk(href),
250
249
  checkGoogleSafeBrowsing(href),
251
250
  checkDomainAge(hostname),
252
- protocol === 'https:' ? checkSSL(hostname) : Promise.resolve({ valid_ssl: false, error: 'HTTP only no SSL' })
251
+ protocol === 'https:' ? checkSSL(hostname) : Promise.resolve({ valid_ssl: false, error: 'HTTP only -- no SSL' })
253
252
  ]);
254
253
 
255
254
  const signals = { google_web_risk: webRisk, google_safe_browsing: safeBrowsing, domain_age: domainAge, ssl };
256
255
 
257
256
  const ai = await getAITrustScore(href, hostname, signals);
258
257
 
259
- // Determine final verdict hard overrides
258
+ // Determine final verdict -- hard overrides
260
259
  let verdict = ai.available ? ai.verdict : 'SUSPICIOUS';
261
260
  let trust_score = ai.available ? ai.trust_score : 40;
262
261
 
@@ -287,9 +286,9 @@ async function checkUrl(rawUrl) {
287
286
  domain_registered: domainAge.available ? domainAge.registration_date : null,
288
287
  redirect_chain_detected,
289
288
  threat_categories: ai.available ? ai.threat_categories : [],
290
- reasoning: ai.available ? ai.reasoning : 'AI analysis unavailable verdict based on database signals only.',
289
+ reasoning: ai.available ? ai.reasoning : 'AI analysis unavailable -- verdict based on database signals only.',
291
290
  ai_confidence: ai.available ? ai.confidence : null,
292
- analysis_type: 'AI-powered NOT a simple database lookup',
291
+ analysis_type: 'AI-powered -- NOT a simple database lookup',
293
292
  database_signals: {
294
293
  google_web_risk: webRisk.available ? { flagged: webRisk.flagged, threat_types: webRisk.threat_types } : { available: false, reason: webRisk.reason },
295
294
  google_safe_browsing: safeBrowsing.available ? { flagged: safeBrowsing.flagged, threat_types: safeBrowsing.threat_types } : { available: false, reason: safeBrowsing.reason }
@@ -306,7 +305,7 @@ async function checkUrl(rawUrl) {
306
305
  // ─── Tool definition ──────────────────────────────────────────────────────────
307
306
  const TOOL_DEFINITION = {
308
307
  name: 'check_url',
309
- 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, URLhaus, and PhishTank 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.',
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.',
310
309
  inputSchema: {
311
310
  type: 'object',
312
311
  properties: {
@@ -404,7 +403,15 @@ const server = http.createServer(async (req, res) => {
404
403
  ? depCheck('webrisk.googleapis.com', `/v1/uris:search?threatTypes=MALWARE&uri=https%3A%2F%2Fexample.com&key=${GOOGLE_WEB_RISK_API_KEY}`)
405
404
  : Promise.resolve({ ok: false, status: 0, error: 'key not set' }),
406
405
  GOOGLE_SAFE_BROWSING_API_KEY
407
- ? depCheck('safebrowsing.googleapis.com', `/v4/threatMatches:find?key=${GOOGLE_SAFE_BROWSING_API_KEY}`)
406
+ ? (() => {
407
+ const sbBody = JSON.stringify({ client: { clientId: 'kord-dep-check', clientVersion: '1.0' }, threatInfo: { threatTypes: ['MALWARE'], platformTypes: ['ANY_PLATFORM'], threatEntryTypes: ['URL'], threatEntries: [{ url: 'https://example.com' }] } });
408
+ return new Promise((resolve) => {
409
+ const r = https.request({ hostname: 'safebrowsing.googleapis.com', path: `/v4/threatMatches:find?key=${GOOGLE_SAFE_BROWSING_API_KEY}`, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(sbBody) } }, (res2) => { res2.resume(); resolve({ ok: res2.statusCode < 500, status: res2.statusCode }); });
410
+ r.on('error', () => resolve({ ok: false, status: 0, error: 'unreachable' }));
411
+ r.setTimeout(5000, () => { r.destroy(); resolve({ ok: false, status: 0, error: 'timeout' }); });
412
+ r.write(sbBody); r.end();
413
+ });
414
+ })()
408
415
  : Promise.resolve({ ok: false, status: 0, error: 'key not set' }),
409
416
  depCheck('rdap.org', '/domain/example.com'),
410
417
  depCheck('api.anthropic.com', '/v1/models', ANTHROPIC_API_KEY ? { 'x-api-key': ANTHROPIC_API_KEY, 'anthropic-version': '2023-06-01' } : {})
@@ -457,7 +464,7 @@ const server = http.createServer(async (req, res) => {
457
464
  return;
458
465
  }
459
466
 
460
- // HTTP POST MCP handler mandatory
467
+ // HTTP POST MCP handler -- mandatory
461
468
  if (req.method === 'POST' && req.url !== '/webhook/stripe') {
462
469
  let body = '';
463
470
  req.on('data', c => body += c);
@@ -485,7 +492,7 @@ const server = http.createServer(async (req, res) => {
485
492
  } else {
486
493
  const tier = checkTier(clientIp, apiKey);
487
494
  if (!tier.allowed) {
488
- 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' }) }] } };
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' }) }] } };
489
496
  } else {
490
497
  if (tier.remaining <= 4 && !tier.paid) {
491
498
  // will add notice to result
@@ -518,7 +525,7 @@ const server = http.createServer(async (req, res) => {
518
525
 
519
526
  server.listen(PORT, () => {
520
527
  console.log(`URL Safety Validator MCP v${VERSION} running on port ${PORT}`);
521
- console.log(`Google Web Risk: ${GOOGLE_WEB_RISK_API_KEY ? 'configured' : 'NOT SET set GOOGLE_WEB_RISK_API_KEY'}`);
528
+ console.log(`Google Web Risk: ${GOOGLE_WEB_RISK_API_KEY ? 'configured' : 'NOT SET -- set GOOGLE_WEB_RISK_API_KEY'}`);
522
529
  console.log(`Anthropic API: ${ANTHROPIC_API_KEY ? 'configured' : 'NOT SET'}`);
523
530
  });
524
531