url-safety-validator-mcp 1.2.30 → 1.2.33
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 +9 -0
- package/glama.json +16 -4
- package/package.json +1 -1
- package/src/server.js +32 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to URL Safety Validator MCP are documented here.
|
|
4
4
|
|
|
5
|
+
## [1.2.33] — 2026-07-16
|
|
6
|
+
- fix: Redis env var mismatch — code only read UPSTASH_REDIS_REST_URL/_TOKEN but Railway has this service configured as REDIS_URL/REDIS_TOKEN, silently killing Redis (dedup, uptime heartbeat, public-stats, fleet cross-server detection all affected). Now falls back to REDIS_URL/REDIS_TOKEN if the UPSTASH_-named vars aren't set. Confirmed both name pairs point at the same shared Upstash instance as the rest of the fleet — no cross-instance risk. Var names intentionally left as-is on Railway (REDIS_URL/REDIS_TOKEN); do not rename, the code now handles both.
|
|
7
|
+
- fix: redisSet() was reading process.env.UPSTASH_REDIS_REST_URL/_TOKEN directly instead of the module consts, bypassing the fallback above — now uses the consts like every other Redis helper.
|
|
8
|
+
- feat: gate-hit email circuit breaker — hard cap of 3 gate-hit emails/hour, server-wide, independent of Redis. Backstop for the incident where broken Redis dedup let 111 gate hits fire 111 separate emails and exhausted the fleet's shared Resend quota.
|
|
9
|
+
- chore: this fix was first committed as 1.2.32, but npm already had a differently-coded 1.2.32 published from before this incident (package.json was one version ahead of the live/constant pair — a known benign pattern until it collided with an actual same-day fix). Renumbered to 1.2.33 to avoid two different codebases sharing one version number.
|
|
10
|
+
|
|
11
|
+
## [1.2.31] — 2026-06-29
|
|
12
|
+
- feat: add GET /.well-known/glama.json ownership endpoint for Glama registry verification
|
|
13
|
+
|
|
5
14
|
## [1.2.30] — 2026-06-28
|
|
6
15
|
- fix: gate email dedup — notifyGateHit now writes url:gate_email:{ip} to Redis with 1-hour TTL; retries within the hour suppressed
|
|
7
16
|
- fix: 402 gate response agent_action changed to HALT_WORKFLOW; added retryable: false, retry_after_ms: null
|
package/glama.json
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
"
|
|
2
|
+
"$schema": "https://glama.ai/mcp/servers/schema.json",
|
|
3
|
+
"name": "URL Safety Validator MCP",
|
|
4
|
+
"description": "AI-powered URL safety validator. Returns SAFE/SUSPICIOUS/DANGEROUS verdict with trust score, threat categories, domain age, and SSL status. Cross-checks Google Web Risk and Google Safe Browsing. Call before any agent navigates or shares a URL.",
|
|
4
5
|
"license": "UNLICENSED",
|
|
5
|
-
"
|
|
6
|
-
|
|
6
|
+
"categories": [
|
|
7
|
+
"security",
|
|
8
|
+
"legal-and-compliance"
|
|
9
|
+
],
|
|
10
|
+
"remote": {
|
|
11
|
+
"transport": "sse",
|
|
12
|
+
"url": "https://url-safety-validator-mcp-production.up.railway.app/sse"
|
|
13
|
+
},
|
|
14
|
+
"links": {
|
|
15
|
+
"homepage": "https://kordagencies.com",
|
|
16
|
+
"npm": "https://www.npmjs.com/package/url-safety-validator-mcp",
|
|
17
|
+
"repository": "https://github.com/OjasKord/url-safety-validator-mcp"
|
|
18
|
+
}
|
|
7
19
|
}
|
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.
|
|
4
|
+
"version": "1.2.33",
|
|
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.
|
|
8
|
+
const VERSION = '1.2.33';
|
|
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 ALLOWED_PAYMENT_LINK_IDS = ['plink_1TQzIHD6WvRe6sn3820kFk07', 'plink_1TQzJdD6WvRe6sn3GN8mQkj9'];
|
|
@@ -49,8 +49,11 @@ function checkPerMinuteLimit(ip, toolName, limit) {
|
|
|
49
49
|
|
|
50
50
|
const REDIS_PREFIX = 'url';
|
|
51
51
|
const FREE_TIER_REDIS_KEY = 'url:free_tier_usage';
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
// Railway has this service's vars set as REDIS_URL/REDIS_TOKEN, not the
|
|
53
|
+
// UPSTASH_-prefixed names the rest of the fleet uses -- accept either so a
|
|
54
|
+
// dashboard naming mismatch can't silently kill Redis again (2026-07-16 incident).
|
|
55
|
+
const UPSTASH_URL = process.env.UPSTASH_REDIS_REST_URL || process.env.REDIS_URL;
|
|
56
|
+
const UPSTASH_TOKEN = process.env.UPSTASH_REDIS_REST_TOKEN || process.env.REDIS_TOKEN;
|
|
54
57
|
const FIRST_DEPLOYED = '2026-04-22T06:38:09Z';
|
|
55
58
|
const LIFETIME_CALLS_REDIS_KEY = 'url:lifetime_calls';
|
|
56
59
|
const UPTIME_HEARTBEAT_KEY = 'url:uptime:heartbeat_count';
|
|
@@ -97,6 +100,22 @@ function truncateIp(ip) {
|
|
|
97
100
|
return parts.length === 4 ? parts.slice(0, 3).join('.') + '.0' : ip;
|
|
98
101
|
}
|
|
99
102
|
|
|
103
|
+
// Redis-independent backstop -- 2026-07-16 incident: Redis was silently down
|
|
104
|
+
// (env var mismatch), the dedup check below fell through on every call, and
|
|
105
|
+
// one abusive client turned 111 gate hits into 111 separate emails that blew
|
|
106
|
+
// the fleet's shared Resend quota. This cap fires even if Redis is fully down.
|
|
107
|
+
const GATE_EMAIL_HOURLY_CAP = 3;
|
|
108
|
+
let gateEmailHourBucket = null;
|
|
109
|
+
let gateEmailHourCount = 0;
|
|
110
|
+
|
|
111
|
+
function gateEmailCircuitBreakerAllows() {
|
|
112
|
+
const hourKey = new Date().toISOString().slice(0, 13); // YYYY-MM-DDTHH
|
|
113
|
+
if (gateEmailHourBucket !== hourKey) { gateEmailHourBucket = hourKey; gateEmailHourCount = 0; }
|
|
114
|
+
if (gateEmailHourCount >= GATE_EMAIL_HOURLY_CAP) return false;
|
|
115
|
+
gateEmailHourCount++;
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
100
119
|
async function notifyGateHit(serverName, ip, toolName, totalCalls, stripeUrl) {
|
|
101
120
|
const ip24 = truncateIp(ip);
|
|
102
121
|
const dedupKey = REDIS_PREFIX + ':gate_email:' + ip24;
|
|
@@ -105,7 +124,8 @@ async function notifyGateHit(serverName, ip, toolName, totalCalls, stripeUrl) {
|
|
|
105
124
|
if (recent) { console.log('[GateNotify] suppressed duplicate for ' + ip24); return; }
|
|
106
125
|
await redisSet(dedupKey, new Date().toISOString());
|
|
107
126
|
await redisExpire(dedupKey, 3600);
|
|
108
|
-
} catch(e) { /* Redis unavailable — fall through
|
|
127
|
+
} catch(e) { /* Redis unavailable — fall through to circuit breaker below */ }
|
|
128
|
+
if (!gateEmailCircuitBreakerAllows()) { console.log('[GateNotify] circuit breaker: hourly cap (' + GATE_EMAIL_HOURLY_CAP + ') reached, suppressing email for ' + ip24); return; }
|
|
109
129
|
const html = '<p>Server: ' + serverName + '</p><p>IP: ' + ip24 + '</p><p>Tool: ' + (toolName || 'unknown') + '</p><p>Calls this month: ' + totalCalls + '</p><p>Time: ' + new Date().toISOString() + '</p><p>Upgrade: ' + stripeUrl + '</p>';
|
|
110
130
|
sendEmail('ojas@kordagencies.com', '[Gate Hit] ' + serverName + ' — ' + ip24 + ' hit free tier limit', html)
|
|
111
131
|
.catch(e => console.error('[GateNotify] failed:', e.message));
|
|
@@ -165,9 +185,9 @@ async function redisGet(key) {
|
|
|
165
185
|
|
|
166
186
|
async function redisSet(key, value) {
|
|
167
187
|
try {
|
|
168
|
-
const res = await fetch(`${
|
|
188
|
+
const res = await fetch(`${UPSTASH_URL}/set/${encodeURIComponent(key)}/${encodeURIComponent(JSON.stringify(value))}`, {
|
|
169
189
|
method: 'GET',
|
|
170
|
-
headers: { Authorization: `Bearer ${
|
|
190
|
+
headers: { Authorization: `Bearer ${UPSTASH_TOKEN}` }
|
|
171
191
|
});
|
|
172
192
|
const data = await res.json();
|
|
173
193
|
if (data.error) console.error('[Redis] redisSet error:', data.error, 'key:', key);
|
|
@@ -814,6 +834,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
814
834
|
return;
|
|
815
835
|
}
|
|
816
836
|
|
|
837
|
+
if (req.url === '/.well-known/glama.json' && req.method === 'GET') {
|
|
838
|
+
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
839
|
+
res.end(JSON.stringify({ "$schema": "https://glama.ai/mcp/schemas/connector.json", "maintainers": [{ "email": "ojas@kordagencies.com" }] }));
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
|
|
817
843
|
if (req.url === '/trial-extension' && req.method === 'POST') {
|
|
818
844
|
let body = ''; req.on('data', c => body += c);
|
|
819
845
|
req.on('end', async () => {
|