vat-validator-mcp 2.0.17 → 2.0.19
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/icon.svg +5 -0
- package/package.json +1 -1
- package/src/server.js +55 -12
package/icon.svg
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
|
2
|
+
<rect width="32" height="32" rx="6" fill="#080A0F"/>
|
|
3
|
+
<circle cx="16" cy="16" r="10" fill="none" stroke="#00E5C3" stroke-width="2"/>
|
|
4
|
+
<polyline points="11,16 14.5,19.5 21,12.5" fill="none" stroke="#00E5C3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
5
|
+
</svg>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vat-validator-mcp",
|
|
3
3
|
"mcpName": "io.github.OjasKord/vat-validator-mcp",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.19",
|
|
5
5
|
"description": "VAT number validator for AI agents. EU VIES, UK HMRC, AU ABR. Fraud risk scoring and name cross-check. PROCEED/HOLD verdict before any invoice payment.",
|
|
6
6
|
"main": "src/server.js",
|
|
7
7
|
"scripts": {
|
package/src/server.js
CHANGED
|
@@ -7,7 +7,7 @@ const Stripe = require('stripe');
|
|
|
7
7
|
const stripe = Stripe(process.env.STRIPE_SECRET_KEY);
|
|
8
8
|
|
|
9
9
|
const PERSIST_FILE = '/tmp/vat_stats.json';
|
|
10
|
-
const VERSION = '2.0.
|
|
10
|
+
const VERSION = '2.0.19';
|
|
11
11
|
|
|
12
12
|
// Persistent device ID for HMRC fraud prevention headers (BATCH_PROCESS_DIRECT)
|
|
13
13
|
const DEVICE_ID_FILE = path.join(__dirname, '..', 'device-id.txt');
|
|
@@ -37,6 +37,22 @@ const FREE_TIER_WARNING = 40;
|
|
|
37
37
|
const TRIAL_EXTENSION_CALLS = 10;
|
|
38
38
|
const apiKeys = new Map();
|
|
39
39
|
|
|
40
|
+
const perMinuteUsage = new Map();
|
|
41
|
+
|
|
42
|
+
function checkPerMinuteLimit(ip, toolName, limit) {
|
|
43
|
+
const minuteKey = ip + ':' + toolName + ':' + new Date().toISOString().slice(0, 16);
|
|
44
|
+
const count = perMinuteUsage.get(minuteKey) || 0;
|
|
45
|
+
if (count >= limit) return false;
|
|
46
|
+
perMinuteUsage.set(minuteKey, count + 1);
|
|
47
|
+
if (perMinuteUsage.size > 10000) {
|
|
48
|
+
const currentMinute = new Date().toISOString().slice(0, 16);
|
|
49
|
+
for (const [key] of perMinuteUsage) {
|
|
50
|
+
if (!key.includes(currentMinute)) perMinuteUsage.delete(key);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
40
56
|
function saveStats() {
|
|
41
57
|
try {
|
|
42
58
|
fs.writeFileSync(PERSIST_FILE, JSON.stringify({
|
|
@@ -916,16 +932,23 @@ const server = http.createServer(async (req, res) => {
|
|
|
916
932
|
const { name, arguments: args } = request.params;
|
|
917
933
|
const rawIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
918
934
|
const ip = rawIp.split(',')[0].trim();
|
|
919
|
-
|
|
920
|
-
if (
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
935
|
+
const killSwitchKey = 'TOOL_DISABLED_' + name.toUpperCase().replace(/[^A-Z0-9]/g, '_');
|
|
936
|
+
if (process.env[killSwitchKey] === 'true') {
|
|
937
|
+
response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify({ error: 'This tool is temporarily unavailable for maintenance.', agent_action: 'RETRY_IN_30_MIN', retryable: true, retry_after_ms: 1800000 }) }] } };
|
|
938
|
+
} else if (name === 'validate_vat' && !checkPerMinuteLimit(ip, name, 5)) {
|
|
939
|
+
response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify({ error: 'Rate limit exceeded — maximum 5 calls per minute per IP on AI-powered tools. Your workflow is calling this tool too rapidly.', agent_action: 'RETRY_IN_60_SEC', retryable: true, retry_after_ms: 60000, limit: 5, window: '1 minute' }) }] } };
|
|
940
|
+
} else {
|
|
941
|
+
usageLog.push({ tool: name, tier: access.tier || access.plan || 'paid', time: new Date().toISOString(), ip: ip.slice(0, 8) + '...' });
|
|
942
|
+
if (usageLog.length > 1000) usageLog.shift();
|
|
943
|
+
toolUsageCounts[name] = (toolUsageCounts[name] || 0) + 1;
|
|
944
|
+
saveStats();
|
|
945
|
+
appendSessionLog(ip, name).catch((e) => console.error('[SessionLog] appendSessionLog failed:', e));
|
|
946
|
+
const result = await executeTool(name, args || {});
|
|
947
|
+
if (access.plan === 'metered' && access.stripeCustomerId) {
|
|
948
|
+
reportMeteredUsage(access.stripeCustomerId, 'vat_query').catch(() => {});
|
|
949
|
+
}
|
|
950
|
+
response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } };
|
|
927
951
|
}
|
|
928
|
-
response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } };
|
|
929
952
|
}
|
|
930
953
|
} else {
|
|
931
954
|
response = { jsonrpc: '2.0', id: request.id, error: { code: -32601, message: 'Method not found: ' + request.method } };
|
|
@@ -998,6 +1021,20 @@ const server = http.createServer(async (req, res) => {
|
|
|
998
1021
|
const request = JSON.parse(body);
|
|
999
1022
|
let response;
|
|
1000
1023
|
if (request.method === 'tools/call') {
|
|
1024
|
+
const _toolNameKs = request.params?.name;
|
|
1025
|
+
const _ksKey = 'TOOL_DISABLED_' + (_toolNameKs || '').toUpperCase().replace(/[^A-Z0-9]/g, '_');
|
|
1026
|
+
if (process.env[_ksKey] === 'true') {
|
|
1027
|
+
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
1028
|
+
res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify({ error: 'This tool is temporarily unavailable for maintenance.', agent_action: 'RETRY_IN_30_MIN', retryable: true, retry_after_ms: 1800000 }) }] } }));
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
const _rawIpKs = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
1032
|
+
const _clientIpKs = _rawIpKs.split(',')[0].trim();
|
|
1033
|
+
if (_toolNameKs === 'validate_vat' && !checkPerMinuteLimit(_clientIpKs, _toolNameKs, 5)) {
|
|
1034
|
+
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
1035
|
+
res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify({ error: 'Rate limit exceeded — maximum 5 calls per minute per IP on AI-powered tools. Your workflow is calling this tool too rapidly.', agent_action: 'RETRY_IN_60_SEC', retryable: true, retry_after_ms: 60000, limit: 5, window: '1 minute' }) }] } }));
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1001
1038
|
const access = checkAccess(req);
|
|
1002
1039
|
if (!access.allowed) {
|
|
1003
1040
|
res.writeHead(429, { ...cors, 'Content-Type': 'application/json' });
|
|
@@ -1133,8 +1170,14 @@ function setupStdio() {
|
|
|
1133
1170
|
response = { jsonrpc: '2.0', id: req.id, result: { prompts: [] } };
|
|
1134
1171
|
} else if (req.method === 'tools/call') {
|
|
1135
1172
|
try {
|
|
1136
|
-
const
|
|
1137
|
-
|
|
1173
|
+
const _name = req.params.name;
|
|
1174
|
+
const _ks = 'TOOL_DISABLED_' + (_name || '').toUpperCase().replace(/[^A-Z0-9]/g, '_');
|
|
1175
|
+
if (process.env[_ks] === 'true') {
|
|
1176
|
+
response = { jsonrpc: '2.0', id: req.id, result: { content: [{ type: 'text', text: JSON.stringify({ error: 'This tool is temporarily unavailable for maintenance.', agent_action: 'RETRY_IN_30_MIN', retryable: true, retry_after_ms: 1800000 }) }] } };
|
|
1177
|
+
} else {
|
|
1178
|
+
const result = await executeTool(_name, req.params.arguments || {});
|
|
1179
|
+
response = { jsonrpc: '2.0', id: req.id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } };
|
|
1180
|
+
}
|
|
1138
1181
|
} catch(e) {
|
|
1139
1182
|
response = { jsonrpc: '2.0', id: req.id, error: { code: -32603, message: e.message, agent_action: 'RETRY_IN_2_MIN' } };
|
|
1140
1183
|
}
|