url-safety-validator-mcp 1.2.0 → 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.
- package/.claude/settings.local.json +13 -0
- package/CHANGELOG.md +13 -0
- package/package.json +24 -6
- package/server.json +7 -28
- package/src/server.js +59 -20
|
@@ -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,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.2",
|
|
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,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
|
-
"
|
|
4
|
+
"title": "URL Safety Validator MCP",
|
|
5
5
|
"description": "AI URL safety validator: SAFE/SUSPICIOUS/DANGEROUS verdict, trust score, threat intel.",
|
|
6
|
-
"
|
|
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.
|
|
16
|
+
"version": "1.2.2",
|
|
18
17
|
"transport": { "type": "stdio" },
|
|
19
18
|
"environmentVariables": [
|
|
20
|
-
{
|
|
21
|
-
|
|
22
|
-
|
|
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.
|
|
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();
|
|
@@ -208,8 +230,7 @@ Return this exact JSON structure:
|
|
|
208
230
|
|
|
209
231
|
Rules:
|
|
210
232
|
- 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
|
|
233
|
+
- If Google Web Risk flagged it OR Google Safe Browsing confirmed it, verdict MUST be DANGEROUS
|
|
213
234
|
- Domain age under 30 days = add "newly_registered" to threat_categories and lower trust_score by at least 20
|
|
214
235
|
- No SSL on a login-looking URL = lower score significantly
|
|
215
236
|
- Consider the full picture -- a newly registered domain with no database hits is still SUSPICIOUS not SAFE`;
|
|
@@ -240,7 +261,13 @@ Rules:
|
|
|
240
261
|
async function checkUrl(rawUrl) {
|
|
241
262
|
const parsed = parseUrl(rawUrl);
|
|
242
263
|
if (!parsed.valid) {
|
|
243
|
-
return {
|
|
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
|
+
};
|
|
244
271
|
}
|
|
245
272
|
|
|
246
273
|
const { href, hostname, protocol } = parsed;
|
|
@@ -281,6 +308,7 @@ async function checkUrl(rawUrl) {
|
|
|
281
308
|
url: href,
|
|
282
309
|
hostname,
|
|
283
310
|
verdict,
|
|
311
|
+
agent_action: verdict === 'DANGEROUS' ? 'BLOCK' : verdict === 'SUSPICIOUS' ? 'FLAG_AND_PROCEED' : 'ALLOW',
|
|
284
312
|
trust_score,
|
|
285
313
|
ssl_valid: ssl.valid_ssl,
|
|
286
314
|
domain_age_days: domainAge.available ? domainAge.domain_age_days : null,
|
|
@@ -295,7 +323,7 @@ async function checkUrl(rawUrl) {
|
|
|
295
323
|
google_safe_browsing: safeBrowsing.available ? { flagged: safeBrowsing.flagged, threat_types: safeBrowsing.threat_types } : { available: false, reason: safeBrowsing.reason }
|
|
296
324
|
},
|
|
297
325
|
checked_at: nowISO(),
|
|
298
|
-
source_url: '
|
|
326
|
+
source_url: 'webrisk.googleapis.com',
|
|
299
327
|
_disclaimer: LEGAL_DISCLAIMER
|
|
300
328
|
};
|
|
301
329
|
|
|
@@ -306,7 +334,7 @@ async function checkUrl(rawUrl) {
|
|
|
306
334
|
// ─── Tool definition ──────────────────────────────────────────────────────────
|
|
307
335
|
const TOOL_DEFINITION = {
|
|
308
336
|
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,
|
|
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.',
|
|
310
338
|
inputSchema: {
|
|
311
339
|
type: 'object',
|
|
312
340
|
properties: {
|
|
@@ -332,7 +360,7 @@ function verifyStripeSignature(body, sig, secret) {
|
|
|
332
360
|
const cors = {
|
|
333
361
|
'Access-Control-Allow-Origin': '*',
|
|
334
362
|
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
335
|
-
'Access-Control-Allow-Headers': 'Content-Type, x-api-key,
|
|
363
|
+
'Access-Control-Allow-Headers': 'Content-Type, x-api-key, x-stats-key'
|
|
336
364
|
};
|
|
337
365
|
|
|
338
366
|
// ─── MCP stdio transport ──────────────────────────────────────────────────────
|
|
@@ -351,7 +379,7 @@ function setupStdio() {
|
|
|
351
379
|
const request = JSON.parse(line);
|
|
352
380
|
let response;
|
|
353
381
|
if (request.method === 'initialize') {
|
|
354
|
-
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.' } } };
|
|
355
383
|
} else if (request.method === 'notifications/initialized') {
|
|
356
384
|
continue;
|
|
357
385
|
} else if (request.method === 'tools/list') {
|
|
@@ -381,7 +409,7 @@ function setupStdio() {
|
|
|
381
409
|
|
|
382
410
|
// ─── HTTP server ──────────────────────────────────────────────────────────────
|
|
383
411
|
const server = http.createServer(async (req, res) => {
|
|
384
|
-
if (req.method === 'OPTIONS') { res.writeHead(
|
|
412
|
+
if (req.method === 'OPTIONS') { res.writeHead(200, cors); res.end(); return; }
|
|
385
413
|
|
|
386
414
|
if (req.url === '/health' && (req.method === 'GET' || req.method === 'HEAD')) {
|
|
387
415
|
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
@@ -404,7 +432,15 @@ const server = http.createServer(async (req, res) => {
|
|
|
404
432
|
? depCheck('webrisk.googleapis.com', `/v1/uris:search?threatTypes=MALWARE&uri=https%3A%2F%2Fexample.com&key=${GOOGLE_WEB_RISK_API_KEY}`)
|
|
405
433
|
: Promise.resolve({ ok: false, status: 0, error: 'key not set' }),
|
|
406
434
|
GOOGLE_SAFE_BROWSING_API_KEY
|
|
407
|
-
?
|
|
435
|
+
? (() => {
|
|
436
|
+
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' }] } });
|
|
437
|
+
return new Promise((resolve) => {
|
|
438
|
+
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 }); });
|
|
439
|
+
r.on('error', () => resolve({ ok: false, status: 0, error: 'unreachable' }));
|
|
440
|
+
r.setTimeout(5000, () => { r.destroy(); resolve({ ok: false, status: 0, error: 'timeout' }); });
|
|
441
|
+
r.write(sbBody); r.end();
|
|
442
|
+
});
|
|
443
|
+
})()
|
|
408
444
|
: Promise.resolve({ ok: false, status: 0, error: 'key not set' }),
|
|
409
445
|
depCheck('rdap.org', '/domain/example.com'),
|
|
410
446
|
depCheck('api.anthropic.com', '/v1/models', ANTHROPIC_API_KEY ? { 'x-api-key': ANTHROPIC_API_KEY, 'anthropic-version': '2023-06-01' } : {})
|
|
@@ -420,8 +456,10 @@ const server = http.createServer(async (req, res) => {
|
|
|
420
456
|
const ipMap = stats.free_tier_calls_by_ip || {};
|
|
421
457
|
const free_tier_unique_ips = Object.keys(ipMap).length;
|
|
422
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; });
|
|
423
461
|
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
424
|
-
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() }));
|
|
425
463
|
return;
|
|
426
464
|
}
|
|
427
465
|
|
|
@@ -444,10 +482,13 @@ const server = http.createServer(async (req, res) => {
|
|
|
444
482
|
if (event.type === 'checkout.session.completed') {
|
|
445
483
|
const session = event.data.object;
|
|
446
484
|
const key = 'usv_' + crypto.randomBytes(16).toString('hex');
|
|
447
|
-
const email = session.customer_details?.email || 'unknown';
|
|
485
|
+
const email = session.customer_details?.email || session.customer_email || 'unknown';
|
|
448
486
|
apiKeys.set(key, { email, created_at: nowISO(), plan: 'pro' });
|
|
449
487
|
saveStats();
|
|
450
|
-
console.log(
|
|
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
|
+
}
|
|
451
492
|
}
|
|
452
493
|
res.writeHead(200, cors); res.end(JSON.stringify({ received: true }));
|
|
453
494
|
} catch(e) {
|
|
@@ -469,7 +510,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
469
510
|
let response;
|
|
470
511
|
|
|
471
512
|
if (request.method === 'initialize') {
|
|
472
|
-
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.' } } };
|
|
473
514
|
} else if (request.method === 'notifications/initialized') {
|
|
474
515
|
res.writeHead(204, cors); res.end(); return;
|
|
475
516
|
} else if (request.method === 'tools/list') {
|
|
@@ -481,19 +522,17 @@ const server = http.createServer(async (req, res) => {
|
|
|
481
522
|
} else if (request.method === 'tools/call' && request.params?.name === 'check_url') {
|
|
482
523
|
const url = request.params?.arguments?.url;
|
|
483
524
|
if (!url) {
|
|
484
|
-
response = { jsonrpc: '2.0', id: request.id,
|
|
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 }) }] } };
|
|
485
526
|
} else {
|
|
486
527
|
const tier = checkTier(clientIp, apiKey);
|
|
487
528
|
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
|
|
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 }) }] } };
|
|
489
530
|
} else {
|
|
490
|
-
if (tier.remaining <= 4 && !tier.paid) {
|
|
491
|
-
// will add notice to result
|
|
492
|
-
}
|
|
493
531
|
recordCall(clientIp, apiKey);
|
|
494
532
|
const result = await checkUrl(url);
|
|
533
|
+
usageLog.push({ tool: 'check_url', ip: clientIp, tier: tier.paid ? 'paid' : 'free', timestamp: nowISO() });
|
|
495
534
|
if (tier.remaining <= 4 && !tier.paid) {
|
|
496
|
-
result._notice =
|
|
535
|
+
result._notice = 'Warning: ' + (tier.remaining - 1) + ' free calls remaining this month. Upgrade to Pro at kordagencies.com to avoid interruption.';
|
|
497
536
|
}
|
|
498
537
|
response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } };
|
|
499
538
|
}
|