vat-validator-mcp 2.0.2 → 2.0.4

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/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.2",
4
+ "version": "2.0.4",
5
5
  "description": "VAT number validator for AI agents. EU VIES, UK HMRC, AU ABR — auto-detects jurisdiction. Fraud risk scoring and invoice name cross-check in one call.",
6
6
  "main": "src/server.js",
7
7
  "scripts": {
package/server.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "name": "io.github.OjasKord/vat-validator-mcp",
4
4
  "title": "VAT Validator MCP",
5
5
  "description": "Validate EU, UK, AU VAT numbers for AI agents. EU ViDA e-invoicing compliance.",
6
- "version": "2.0.1",
6
+ "version": "2.0.4",
7
7
  "websiteUrl": "https://kordagencies.com",
8
8
  "repository": {
9
9
  "url": "https://github.com/OjasKord/vat-validator-mcp",
@@ -13,7 +13,7 @@
13
13
  {
14
14
  "registryType": "npm",
15
15
  "identifier": "vat-validator-mcp",
16
- "version": "2.0.1",
16
+ "version": "2.0.4",
17
17
  "transport": { "type": "stdio" },
18
18
  "environmentVariables": [
19
19
  { "name": "ANTHROPIC_API_KEY", "description": "Anthropic API key for AI-powered fraud risk analysis", "isRequired": true, "isSecret": true },
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.2';
10
+ const VERSION = '2.0.4';
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');
@@ -100,6 +100,25 @@ async function redisSet(key, value) {
100
100
  } catch(e) {}
101
101
  }
102
102
 
103
+ async function redisExpire(key, seconds) {
104
+ try {
105
+ await fetch(
106
+ `${UPSTASH_URL}/expire/${encodeURIComponent(key)}/${seconds}`,
107
+ { method: 'POST', headers: { Authorization: `Bearer ${UPSTASH_TOKEN}` } }
108
+ );
109
+ } catch(e) {}
110
+ }
111
+
112
+ async function appendSessionLog(ip, tool) {
113
+ const ipSafe = ip.replace(/:/g, '_').replace(/\s/g, '');
114
+ const dayKey = new Date().toISOString().slice(0, 10);
115
+ const key = `${REDIS_PREFIX}:session:${ipSafe}:${dayKey}`;
116
+ const existing = await redisGet(key) || [];
117
+ existing.push({ tool, timestamp: new Date().toISOString() });
118
+ await redisSet(key, existing);
119
+ await redisExpire(key, 86400);
120
+ }
121
+
103
122
  async function redisKeys(pattern) {
104
123
  try {
105
124
  const res = await fetch(
@@ -211,6 +230,15 @@ async function validateVIES(countryCode, vatNumber) {
211
230
  });
212
231
  }
213
232
 
233
+ async function hmrcFetchWithRetry(url, options, maxRetries = 3) {
234
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
235
+ const response = await fetch(url, options);
236
+ if (response.status !== 429) return response;
237
+ if (attempt === maxRetries) return response;
238
+ await new Promise(resolve => setTimeout(resolve, attempt * 1000));
239
+ }
240
+ }
241
+
214
242
  function getFraudPreventionHeaders() {
215
243
  return {
216
244
  'Gov-Client-Connection-Method': 'BATCH_PROCESS_DIRECT',
@@ -223,7 +251,7 @@ function getFraudPreventionHeaders() {
223
251
  'Gov-Client-User-IDs': 'os=railway-service',
224
252
  'Gov-Vendor-License-IDs': 'vat-validator-mcp=not-applicable',
225
253
  'Gov-Vendor-Product-Name': 'VAT%20Validator%20MCP',
226
- 'Gov-Vendor-Version': 'vat-validator-mcp=2.0.2'
254
+ 'Gov-Vendor-Version': 'vat-validator-mcp=2.0.3'
227
255
  };
228
256
  }
229
257
 
@@ -245,32 +273,23 @@ async function getHMRCToken() {
245
273
 
246
274
  const body = `client_secret=${encodeURIComponent(clientSecret)}&client_id=${encodeURIComponent(clientId)}&grant_type=client_credentials&scope=read%3Avat`;
247
275
 
248
- return new Promise((resolve) => {
249
- const req = https.request({
250
- hostname,
251
- path: '/oauth/token',
276
+ try {
277
+ const response = await hmrcFetchWithRetry(`https://${hostname}/oauth/token`, {
252
278
  method: 'POST',
253
- headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': Buffer.byteLength(body), ...getFraudPreventionHeaders() }
254
- }, res => {
255
- let d = ''; res.on('data', c => d += c);
256
- res.on('end', () => {
257
- try {
258
- const json = JSON.parse(d);
259
- if (json.access_token) {
260
- hmrcToken = json.access_token;
261
- hmrcTokenExpiry = now + (json.expires_in || 14400) * 1000;
262
- resolve(hmrcToken);
263
- } else {
264
- resolve(null);
265
- }
266
- } catch(e) { resolve(null); }
267
- });
279
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded', ...getFraudPreventionHeaders() },
280
+ body,
281
+ signal: AbortSignal.timeout(8000)
268
282
  });
269
- req.on('error', () => resolve(null));
270
- req.setTimeout(8000, () => { req.destroy(); resolve(null); });
271
- req.write(body);
272
- req.end();
273
- });
283
+ const json = await response.json();
284
+ if (json.access_token) {
285
+ hmrcToken = json.access_token;
286
+ hmrcTokenExpiry = now + (json.expires_in || 14400) * 1000;
287
+ return hmrcToken;
288
+ }
289
+ return null;
290
+ } catch(e) {
291
+ return null;
292
+ }
274
293
  }
275
294
 
276
295
  async function validateHMRC(vatNumber) {
@@ -281,23 +300,18 @@ async function validateHMRC(vatNumber) {
281
300
  const sandbox = process.env.HMRC_SANDBOX === 'true';
282
301
  const hostname = sandbox ? 'test-api.service.hmrc.gov.uk' : 'api.service.hmrc.gov.uk';
283
302
 
284
- return new Promise((resolve) => {
285
- const req = https.request({
286
- hostname,
287
- path: '/organisations/vat/check-vat-number/lookup/' + clean,
303
+ try {
304
+ const response = await hmrcFetchWithRetry(`https://${hostname}/organisations/vat/check-vat-number/lookup/${clean}`, {
288
305
  method: 'GET',
289
- headers: { 'Accept': 'application/vnd.hmrc.2.0+json', 'Authorization': 'Bearer ' + token, ...getFraudPreventionHeaders() }
290
- }, res => {
291
- let d = ''; res.on('data', c => d += c);
292
- res.on('end', () => {
293
- try { resolve({ source: 'HMRC', status: res.statusCode, data: JSON.parse(d) }); }
294
- catch(e) { resolve({ source: 'HMRC', error: 'Parse error' }); }
295
- });
306
+ headers: { 'Accept': 'application/vnd.hmrc.2.0+json', 'Authorization': 'Bearer ' + token, ...getFraudPreventionHeaders() },
307
+ signal: AbortSignal.timeout(8000)
296
308
  });
297
- req.on('error', e => resolve({ source: 'HMRC', error: e.message }));
298
- req.setTimeout(8000, () => { req.destroy(); resolve({ source: 'HMRC', error: 'Timeout' }); });
299
- req.end();
300
- });
309
+ const data = await response.json();
310
+ return { source: 'HMRC', status: response.status, data };
311
+ } catch(e) {
312
+ if (e.name === 'TimeoutError' || e.name === 'AbortError') return { source: 'HMRC', error: 'Timeout' };
313
+ return { source: 'HMRC', error: e.message };
314
+ }
301
315
  }
302
316
 
303
317
  async function validateABN(abn) {
@@ -759,6 +773,27 @@ const server = http.createServer(async (req, res) => {
759
773
  return;
760
774
  }
761
775
 
776
+ if (req.url === '/session-log' && req.method === 'GET') {
777
+ if (req.headers['x-stats-key'] !== STATS_KEY) { res.writeHead(401, cors); res.end(JSON.stringify({ error: 'Unauthorized' })); return; }
778
+ (async () => {
779
+ const keys = await redisKeys(`${REDIS_PREFIX}:session:*`);
780
+ const sessions = [];
781
+ for (const key of keys) {
782
+ const calls = await redisGet(key) || [];
783
+ if (!calls.length) continue;
784
+ const withoutPrefix = key.slice(`${REDIS_PREFIX}:session:`.length);
785
+ const dateIdx = withoutPrefix.lastIndexOf(':');
786
+ const ipPart = withoutPrefix.slice(0, dateIdx);
787
+ const date = withoutPrefix.slice(dateIdx + 1);
788
+ sessions.push({ ip: ipPart.slice(0, 8), date, calls, first_call: calls[0]?.timestamp || '', last_call: calls[calls.length - 1]?.timestamp || '' });
789
+ }
790
+ sessions.sort((a, b) => new Date(b.first_call) - new Date(a.first_call));
791
+ res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
792
+ res.end(JSON.stringify(sessions));
793
+ })();
794
+ return;
795
+ }
796
+
762
797
  if (req.url === '/trial-extension' && req.method === 'POST') {
763
798
  let body = ''; req.on('data', c => body += c);
764
799
  req.on('end', async () => {
@@ -850,6 +885,7 @@ const server = http.createServer(async (req, res) => {
850
885
  if (usageLog.length > 1000) usageLog.shift();
851
886
  toolUsageCounts[name] = (toolUsageCounts[name] || 0) + 1;
852
887
  saveStats();
888
+ appendSessionLog(ip, name).catch(() => {});
853
889
  const result = await executeTool(name, args || {});
854
890
  if (access.plan === 'metered' && access.stripeCustomerId) {
855
891
  reportMeteredUsage(access.stripeCustomerId, 'vat_query').catch(() => {});
@@ -898,6 +934,7 @@ const server = http.createServer(async (req, res) => {
898
934
  if (usageLog.length > 1000) usageLog.shift();
899
935
  toolUsageCounts[name] = (toolUsageCounts[name] || 0) + 1;
900
936
  saveStats();
937
+ appendSessionLog(ip, name).catch(() => {});
901
938
  const result = await executeTool(name, toolArgs || {});
902
939
  if (req._accessWarning) result._notice = req._accessWarning;
903
940