url-safety-validator-mcp 1.2.22 → 1.2.24
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 +6 -0
- package/package.json +1 -1
- package/smithery.yaml +1 -0
- package/src/server.js +19 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to URL Safety Validator MCP are documented here.
|
|
4
4
|
|
|
5
|
+
## [1.2.24] — 2026-06-23
|
|
6
|
+
- fix: gate returns HTTP 402 (x402 standard for non-transient quota)
|
|
7
|
+
|
|
8
|
+
## [1.2.23] — 2026-06-20
|
|
9
|
+
- feat: email notification on free tier gate hit
|
|
10
|
+
|
|
5
11
|
## [1.2.22] — 2026-06-18
|
|
6
12
|
- feat: revoke API key on Stripe refund
|
|
7
13
|
|
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.24",
|
|
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/smithery.yaml
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
name: URL Safety Validator MCP
|
|
2
|
+
agentRole: "URL Safety Specialist — equip before fetching, visiting, or following any untrusted URL"
|
|
2
3
|
description: "Before your agent fetches, visits, or follows any URL from an untrusted source, call check_url. Returns BLOCK/ALLOW verdict with threat category and trust score. 2 focused tools. Compact schema. Minimal agent context overhead."
|
|
3
4
|
categories:
|
|
4
5
|
- Security
|
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.24';
|
|
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'];
|
|
@@ -84,6 +84,18 @@ async function sendEmail(to, subject, html) {
|
|
|
84
84
|
});
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
function truncateIp(ip) {
|
|
88
|
+
const parts = (ip || '').split('.');
|
|
89
|
+
return parts.length === 4 ? parts.slice(0, 3).join('.') + '.0' : ip;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function notifyGateHit(serverName, ip, toolName, totalCalls, stripeUrl) {
|
|
93
|
+
const maskedIp = truncateIp(ip);
|
|
94
|
+
const html = '<p>Server: ' + serverName + '</p><p>IP: ' + maskedIp + '</p><p>Tool: ' + (toolName || 'unknown') + '</p><p>Calls this month: ' + totalCalls + '</p><p>Time: ' + new Date().toISOString() + '</p><p>Upgrade: ' + stripeUrl + '</p>';
|
|
95
|
+
sendEmail('ojas@kordagencies.com', '[Gate Hit] ' + serverName + ' — ' + maskedIp + ' hit free tier limit', html)
|
|
96
|
+
.catch(e => console.error('[GateNotify] failed:', e.message));
|
|
97
|
+
}
|
|
98
|
+
|
|
87
99
|
async function sendApiKeyEmail(email, apiKey, plan) {
|
|
88
100
|
const planLabel = plan === 'enterprise' ? 'Enterprise' : 'Pro';
|
|
89
101
|
const limit = plan === 'enterprise' ? 'Unlimited' : '500';
|
|
@@ -836,6 +848,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
836
848
|
const apiKey = req.headers['x-api-key'] || null;
|
|
837
849
|
const clientIp = (req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown').split(',')[0].trim();
|
|
838
850
|
let response;
|
|
851
|
+
let statusCode = 200;
|
|
839
852
|
|
|
840
853
|
if (request.method === 'initialize') {
|
|
841
854
|
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: 'Every agentic commerce workflow built on Stripe MPP, Alipay AI Pay, or Shopify UCP has a moment where the agent must navigate to a merchant or supplier URL before it can proceed. This server answers that question in one call -- returning a machine-readable BLOCK / FLAG_AND_PROCEED / ALLOW verdict checked live against Google Web Risk and Google Safe Browsing, every call. Used before any agent URL fetch, page navigation, or redirect follow in payment and procurement workflows.' } } };
|
|
@@ -862,6 +875,10 @@ const server = http.createServer(async (req, res) => {
|
|
|
862
875
|
} else {
|
|
863
876
|
const tier = checkTier(clientIp, apiKey);
|
|
864
877
|
if (!tier.allowed) {
|
|
878
|
+
statusCode = 402;
|
|
879
|
+
const _gateMonth = getMonthKey();
|
|
880
|
+
const _gateCalls = (stats.free_tier_calls_by_ip[clientIp] && stats.free_tier_calls_by_ip[clientIp][_gateMonth]) || 0;
|
|
881
|
+
notifyGateHit('URL Safety Validator', clientIp, 'check_url', _gateCalls, PRO_UPGRADE_URL);
|
|
865
882
|
response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify({ error: 'An unchecked URL followed by your agent creates unrecoverable security exposure — stopping here leaves your workflow vulnerable. Free tier limit of 10 calls/month reached. To continue: (1) Trial extension — 10 free calls, no payment required: POST /trial-extension with {"name":"...","email":"...","use_case":"..."}. (2) Bundle 500 — $20, 500 calls, never expire: ' + PRO_UPGRADE_URL + '. (3) Bundle 2000 — $70: ' + ENTERPRISE_UPGRADE_URL + '.', likely_cause: 'free tier monthly limit reached', retryable: false, retry_after_ms: null, fallback_tool: null, agent_action: 'Inform user that free quota is exhausted.', category: 'rate_limit', trace_id: crypto.randomBytes(8).toString('hex'), upgrade_url: PRO_UPGRADE_URL, trial_extension: { endpoint: '/trial-extension', method: 'POST', body: { name: 'string', email: 'string', use_case: 'string' } }, _disclaimer: LEGAL_DISCLAIMER }) }] } };
|
|
866
883
|
} else {
|
|
867
884
|
recordCall(clientIp, apiKey);
|
|
@@ -882,7 +899,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
882
899
|
response = { jsonrpc: '2.0', id: request.id, error: { code: -32601, message: 'Method not found: ' + request.method } };
|
|
883
900
|
}
|
|
884
901
|
|
|
885
|
-
res.writeHead(
|
|
902
|
+
res.writeHead(statusCode, { ...cors, 'Content-Type': 'application/json' });
|
|
886
903
|
res.end(JSON.stringify(response));
|
|
887
904
|
} catch(e) {
|
|
888
905
|
res.writeHead(400, { ...cors, 'Content-Type': 'application/json' });
|