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 +24 -6
- package/server.json +8 -2
- package/src/server.js +21 -14
package/package.json
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
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
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
?
|
|
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
|
|
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
|
|
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
|
|
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
|
|