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.
Files changed (3) hide show
  1. package/icon.svg +5 -0
  2. package/package.json +1 -1
  3. 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.17",
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.17';
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
- usageLog.push({ tool: name, tier: access.tier || access.plan || 'paid', time: new Date().toISOString(), ip: ip.slice(0, 8) + '...' });
920
- if (usageLog.length > 1000) usageLog.shift();
921
- toolUsageCounts[name] = (toolUsageCounts[name] || 0) + 1;
922
- saveStats();
923
- appendSessionLog(ip, name).catch((e) => console.error('[SessionLog] appendSessionLog failed:', e));
924
- const result = await executeTool(name, args || {});
925
- if (access.plan === 'metered' && access.stripeCustomerId) {
926
- reportMeteredUsage(access.stripeCustomerId, 'vat_query').catch(() => {});
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 result = await executeTool(req.params.name, req.params.arguments || {});
1137
- response = { jsonrpc: '2.0', id: req.id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } };
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
  }