vat-validator-mcp 1.4.6 → 1.4.8

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/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ [![smithery badge](https://smithery.ai/badge/OjasKord/vat-validator-mcp)](https://smithery.ai/servers/OjasKord/vat-validator-mcp)
2
+
1
3
  # VAT Validator MCP — Business Identity Verification & Invoice Fraud Detection
2
4
 
3
5
  Validate EU, UK, and Australian VAT numbers against live government registries. Plus AI-powered fraud risk analysis and invoice verification — so your agent doesn't just know a VAT number is valid, it knows whether to proceed with the transaction.
@@ -152,8 +154,8 @@ Every response includes `source_url` and `checked_at` so agents can verify exact
152
154
  | Plan | Validations | Price |
153
155
  |---|---|---|
154
156
  | Free | 20/month | No API key required |
155
- | Pro | 5,000/month | $39/month |
156
- | Enterprise | Unlimited + batch | $199/month |
157
+ | Starter | 500-call bundle | $8 |
158
+ | Pro | 2,000-call bundle | $28 |
157
159
 
158
160
  Upgrade at **[kordagencies.com](https://kordagencies.com)**
159
161
 
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": "1.4.6",
4
+ "version": "1.4.8",
5
5
  "description": "VAT number validation for AI agents. EU VIES, UK HMRC, Australian ABN in one call.",
6
6
  "main": "src/server.js",
7
7
  "scripts": {
@@ -0,0 +1,5 @@
1
+ {
2
+ "token_footprint_min": 30,
3
+ "token_footprint_max": 150,
4
+ "token_footprint_avg": 80
5
+ }
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": "1.4.5",
6
+ "version": "1.4.7",
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": "1.4.5",
16
+ "version": "1.4.7",
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
@@ -4,7 +4,8 @@ const crypto = require('crypto');
4
4
  const fs = require('fs');
5
5
 
6
6
  const PERSIST_FILE = '/tmp/vat_stats.json';
7
- const VERSION = '1.4.6';
7
+ const API_KEYS_FILE = '/tmp/vat_apikeys.json';
8
+ const VERSION = '1.4.8';
8
9
  const PRO_UPGRADE_URL = 'https://buy.stripe.com/28EeVceUB06N1ty3teebu0l';
9
10
  const ENTERPRISE_UPGRADE_URL = 'https://buy.stripe.com/00w14m7s96vb1ty5Bmebu0m';
10
11
  const RESEND_API_KEY = process.env.RESEND_API_KEY || '';
@@ -39,6 +40,22 @@ function loadStats() {
39
40
  } catch(e) { console.error('Stats load error:', e.message); }
40
41
  }
41
42
 
43
+ function getMonthKey(ip) { return ip + ':' + new Date().toISOString().slice(0, 7); }
44
+
45
+ function saveApiKeys() {
46
+ try { fs.writeFileSync(API_KEYS_FILE, JSON.stringify(Array.from(apiKeys.entries()))); } catch(e) { console.error('API keys save error:', e.message); }
47
+ }
48
+
49
+ function loadApiKeys() {
50
+ try {
51
+ if (fs.existsSync(API_KEYS_FILE)) {
52
+ const entries = JSON.parse(fs.readFileSync(API_KEYS_FILE, 'utf8'));
53
+ entries.forEach(([k, v]) => apiKeys.set(k, v));
54
+ console.log('API keys loaded: ' + apiKeys.size + ' keys');
55
+ }
56
+ } catch(e) { console.error('API keys load error:', e.message); }
57
+ }
58
+
42
59
  function generateApiKey() { return 'vat_' + crypto.randomBytes(24).toString('hex'); }
43
60
  function getPlanFromProduct(name) {
44
61
  if (!name) return 'pro';
@@ -226,36 +243,36 @@ async function executeTool(name, args) {
226
243
  if (name === 'validate_vat') {
227
244
  const vat_number = args.vat_number;
228
245
  const checkedAt = nowISO();
229
- if (!vat_number) return { error: 'vat_number is required', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: 'validate_vat', trace_id: Math.random().toString(36).slice(2, 10) };
246
+ if (!vat_number) return { error: 'vat_number is required', likely_cause: 'required field missing or malformed', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10) };
230
247
  const detected = detectCountry(vat_number);
231
248
  if (detected.type === 'uk') {
232
249
  const result = await validateHMRC(detected.number);
233
- if (result.error) return { valid: null, vat_number, country: 'GB', source: 'HMRC', error: result.error, agent_action: 'RETRY_IN_2_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 120000, fallback_tool: 'validate_vat', trace_id: Math.random().toString(36).slice(2, 10), retry: true, _disclaimer: LEGAL_DISCLAIMER };
250
+ if (result.error) return { valid: null, vat_number, country: 'GB', source: 'HMRC', error: result.error, likely_cause: 'external VAT registry temporarily unavailable', agent_action: 'RETRY_IN_2_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 120000, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10), retry: true, _disclaimer: LEGAL_DISCLAIMER };
234
251
  const d = result.data;
235
252
  if (result.status === 200 && d.target) return { valid: true, vat_number, country: 'GB', company_name: d.target.name || null, source: 'HMRC', source_url: 'api.service.hmrc.gov.uk', consultation_number: d.consultationNumber || null, checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
236
253
  return { valid: false, vat_number, country: 'GB', source: 'HMRC', source_url: 'api.service.hmrc.gov.uk', reason: d.code || 'VAT number not found', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
237
254
  }
238
255
  if (detected.type === 'eu') {
239
256
  const result = await validateVIES(detected.country, detected.number);
240
- if (result.error) return { valid: null, vat_number, agent_action: 'RETRY_IN_30_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 1800000, fallback_tool: 'validate_vat', trace_id: Math.random().toString(36).slice(2, 10), country: detected.country, source: 'VIES', source_url: 'ec.europa.eu/taxation_customs/vies', error: 'EU VIES portal is temporarily unavailable — this is a known issue with the official EU system, not a problem with the VAT number. Retry in 30 minutes.', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
257
+ if (result.error) return { valid: null, vat_number, agent_action: 'RETRY_IN_30_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 1800000, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10), country: detected.country, source: 'VIES', source_url: 'ec.europa.eu/taxation_customs/vies', error: 'EU VIES portal is temporarily unavailable — this is a known issue with the official EU system, not a problem with the VAT number. Retry in 30 minutes.', likely_cause: 'external VAT registry temporarily unavailable', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
241
258
  const d = result.data;
242
259
  return { valid: d.isValid || false, vat_number, country: detected.country, company_name: d.traderName || null, address: d.traderAddress || null, source: 'VIES', source_url: 'ec.europa.eu/taxation_customs/vies', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
243
260
  }
244
261
  if (detected.type === 'au') {
245
262
  const result = await validateABN(detected.number);
246
- if (result.error) return { valid: null, vat_number, country: 'AU', source: 'ABR', error: result.error, agent_action: 'RETRY_IN_2_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 120000, fallback_tool: 'validate_vat', trace_id: Math.random().toString(36).slice(2, 10), _disclaimer: LEGAL_DISCLAIMER };
263
+ if (result.error) return { valid: null, vat_number, country: 'AU', source: 'ABR', error: result.error, likely_cause: 'external VAT registry temporarily unavailable', agent_action: 'RETRY_IN_2_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 120000, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10), _disclaimer: LEGAL_DISCLAIMER };
247
264
  const d = result.data;
248
265
  return { valid: !!(d.Abn && d.AbnStatus === 'Active'), vat_number, country: 'AU', company_name: d.EntityName || null, abn_status: d.AbnStatus || null, source: 'ABR', source_url: 'abr.business.gov.au', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
249
266
  }
250
- return { valid: null, vat_number, agent_action: 'PROVIDE_COUNTRY_PREFIX', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: 'validate_vat', trace_id: Math.random().toString(36).slice(2, 10), error: 'Could not detect country. Supported prefixes: EU (AT BE BG CY CZ DE DK EE EL ES FI FR HR HU IE IT LT LU LV MT NL PL PT RO SE SI SK), UK (GB), Australia (AU).', _disclaimer: LEGAL_DISCLAIMER };
267
+ return { valid: null, vat_number, agent_action: 'PROVIDE_COUNTRY_PREFIX', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10), error: 'Could not detect country. Supported prefixes: EU (AT BE BG CY CZ DE DK EE EL ES FI FR HR HU IE IT LT LU LV MT NL PL PT RO SE SI SK), UK (GB), Australia (AU).', likely_cause: 'required field missing or malformed', _disclaimer: LEGAL_DISCLAIMER };
251
268
  }
252
269
 
253
270
  if (name === 'validate_uk_vat') {
254
271
  const vat_number = args.vat_number;
255
272
  const checkedAt = nowISO();
256
- if (!vat_number) return { error: 'vat_number is required', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: 'validate_uk_vat', trace_id: Math.random().toString(36).slice(2, 10) };
273
+ if (!vat_number) return { error: 'vat_number is required', likely_cause: 'required field missing or malformed', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10) };
257
274
  const result = await validateHMRC(vat_number);
258
- if (result.error) return { valid: null, vat_number, source: 'HMRC', source_url: 'api.service.hmrc.gov.uk', error: 'UK HMRC API is temporarily unavailable — this is not a problem with the VAT number. Retry in a few minutes.', agent_action: 'RETRY_IN_2_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 120000, fallback_tool: 'validate_uk_vat', trace_id: Math.random().toString(36).slice(2, 10), checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
275
+ if (result.error) return { valid: null, vat_number, source: 'HMRC', source_url: 'api.service.hmrc.gov.uk', error: 'UK HMRC API is temporarily unavailable — this is not a problem with the VAT number. Retry in a few minutes.', likely_cause: 'external VAT registry temporarily unavailable', agent_action: 'RETRY_IN_2_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 120000, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10), checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
259
276
  const d = result.data;
260
277
  if (result.status === 200 && d.target) return { valid: true, vat_number, company_name: d.target.name || null, registered_address: d.target.address ? Object.values(d.target.address).filter(Boolean).join(', ') : null, consultation_number: d.consultationNumber || null, source: 'HMRC', source_url: 'api.service.hmrc.gov.uk', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
261
278
  return { valid: false, vat_number, source: 'HMRC', source_url: 'api.service.hmrc.gov.uk', reason: d.code || 'VAT number not found', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
@@ -267,17 +284,17 @@ async function executeTool(name, args) {
267
284
  if (!country_code) return { rates: VAT_RATES, note: 'VAT rates as of 2026. Verify with official tax authority before use.', source_url: 'kordagencies.com', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
268
285
  const code = country_code.toUpperCase();
269
286
  const rate = VAT_RATES[code];
270
- if (!rate) return { error: 'No VAT rate data for: ' + code + '. Supported: ' + Object.keys(VAT_RATES).join(', '), agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: 'get_vat_rates', trace_id: Math.random().toString(36).slice(2, 10), _disclaimer: LEGAL_DISCLAIMER };
287
+ if (!rate) return { error: 'No VAT rate data for: ' + code + '. Supported: ' + Object.keys(VAT_RATES).join(', '), likely_cause: 'required field missing or malformed', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10), _disclaimer: LEGAL_DISCLAIMER };
271
288
  return Object.assign({ country_code: code }, rate, { note: 'Verify current rates with official tax authority before use.', source_url: 'kordagencies.com', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER });
272
289
  }
273
290
 
274
291
  if (name === 'batch_validate') {
275
292
  const vat_numbers = args.vat_numbers;
276
- if (!vat_numbers || !Array.isArray(vat_numbers)) return { error: 'vat_numbers must be an array', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: 'batch_validate', trace_id: Math.random().toString(36).slice(2, 10) };
277
- if (vat_numbers.length > 10) return { error: 'Maximum 10 VAT numbers per batch. Upgrade to Enterprise at kordagencies.com for unlimited batches.', agent_action: 'Reduce batch to 10 or fewer, or upgrade to Enterprise at kordagencies.com', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: 'batch_validate', trace_id: Math.random().toString(36).slice(2, 10) };
293
+ if (!vat_numbers || !Array.isArray(vat_numbers)) return { error: 'vat_numbers must be an array', likely_cause: 'required field missing or malformed', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10) };
294
+ if (vat_numbers.length > 10) return { error: 'Maximum 10 VAT numbers per batch. Upgrade to Enterprise at kordagencies.com for unlimited batches.', likely_cause: 'required field missing or malformed', agent_action: 'Reduce batch to 10 or fewer, or upgrade to Enterprise at kordagencies.com', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10) };
278
295
  const results = await Promise.all(vat_numbers.map(async (vat) => {
279
296
  try { return await executeTool('validate_vat', { vat_number: vat }); }
280
- catch(e) { return { vat_number: vat, valid: null, error: e.message, category: 'upstream_unavailable', retryable: true, retry_after_ms: 120000, fallback_tool: 'validate_vat', trace_id: Math.random().toString(36).slice(2, 10) }; }
297
+ catch(e) { return { vat_number: vat, valid: null, error: e.message, likely_cause: 'external VAT registry temporarily unavailable', agent_action: 'RETRY_IN_2_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 120000, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10) }; }
281
298
  }));
282
299
  return { summary: { total: results.length, valid: results.filter(r => r.valid === true).length, invalid: results.filter(r => r.valid === false).length, error: results.filter(r => r.valid === null).length }, results, _disclaimer: LEGAL_DISCLAIMER };
283
300
  }
@@ -287,20 +304,20 @@ async function executeTool(name, args) {
287
304
  const validation_result = args.validation_result;
288
305
  const invoice_amount = args.invoice_amount;
289
306
  const invoice_company_name = args.invoice_company_name;
290
- if (!vat_number || !validation_result) return { error: 'vat_number and validation_result are required', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: 'analyse_vat_risk', trace_id: Math.random().toString(36).slice(2, 10) };
307
+ if (!vat_number || !validation_result) return { error: 'vat_number and validation_result are required', likely_cause: 'required field missing or malformed', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10) };
291
308
  const prompt = 'You are a B2B fraud detection specialist. Analyse this VAT validation result for fraud signals.\n\nVAT Number: ' + vat_number + '\nValidation Result: ' + JSON.stringify(validation_result) + '\nInvoice Amount: ' + (invoice_amount ? String(invoice_amount) : 'Not provided') + '\nInvoice Company Name: ' + (invoice_company_name || 'Not provided') + '\nRegistered Company Name: ' + (validation_result.company_name || 'Not available') + '\nValid: ' + validation_result.valid + '\nCountry: ' + validation_result.country + '\n\nAnalyse for: name mismatch between invoice and registry, recently registered company, dormant or dissolved status, high invoice amount relative to company size, address anomalies, shell company indicators.\n\nReturn ONLY valid JSON with no preamble: {"recommendation":"CLEAR|REVIEW|BLOCK","risk_level":"LOW|MEDIUM|HIGH|CRITICAL","risk_score":50,"fraud_signals":[],"positive_indicators":[],"recommended_action":"one sentence","summary":"two sentences"}';
292
309
  try {
293
310
  const response = await callClaude(prompt);
294
311
  const result = JSON.parse(response.replace(/```json|```/g, '').trim());
295
312
  return Object.assign({}, result, { vat_number, _disclaimer: LEGAL_DISCLAIMER });
296
313
  } catch(e) {
297
- return { recommendation: 'REVIEW', risk_level: 'MEDIUM', risk_score: 50, vat_number, error: 'AI analysis unavailable - manual review recommended', agent_action: 'RETRY_IN_2_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 120000, fallback_tool: 'analyse_vat_risk', trace_id: Math.random().toString(36).slice(2, 10), _disclaimer: LEGAL_DISCLAIMER };
314
+ return { recommendation: 'REVIEW', risk_level: 'MEDIUM', risk_score: 50, vat_number, error: 'AI analysis unavailable - manual review recommended', likely_cause: 'AI analysis failed — transient Anthropic API issue', agent_action: 'RETRY_IN_2_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 120000, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10), _disclaimer: LEGAL_DISCLAIMER };
298
315
  }
299
316
  }
300
317
 
301
318
  if (name === 'compare_invoice_details') {
302
319
  const { invoice_company_name, invoice_address, invoice_vat_number, validation_result } = args;
303
- if (!invoice_company_name || !invoice_vat_number || !validation_result) return { error: 'invoice_company_name, invoice_vat_number, and validation_result are required', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: 'compare_invoice_details', trace_id: Math.random().toString(36).slice(2, 10) };
320
+ if (!invoice_company_name || !invoice_vat_number || !validation_result) return { error: 'invoice_company_name, invoice_vat_number, and validation_result are required', likely_cause: 'required field missing or malformed', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10) };
304
321
  const prompt = 'You are an invoice fraud detection specialist. Compare invoice details against official registry records.\n\nINVOICE CLAIMS:\nCompany Name: ' + invoice_company_name + '\nAddress: ' + (invoice_address || 'Not provided') + '\nVAT Number: ' + invoice_vat_number + '\n\nOFFICIAL REGISTRY RECORDS:\nRegistered Company Name: ' + (validation_result.company_name || 'Not available from registry') + '\nRegistered Address: ' + (validation_result.address || validation_result.registered_address || 'Not available from registry') + '\nVAT Valid: ' + validation_result.valid + '\nCountry: ' + validation_result.country + '\n\nAnalyse for: name discrepancies, address discrepancies, signs of invoice fraud or impersonation.\n\nReturn ONLY valid JSON with no preamble: {"match_verdict":"MATCH|PARTIAL_MATCH|MISMATCH|UNVERIFIABLE","name_match":"EXACT|SIMILAR|DIFFERENT|UNVERIFIABLE","address_match":"MATCH|DIFFERENT|UNVERIFIABLE","vat_valid":true,"discrepancies":[],"fraud_risk":"LOW|MEDIUM|HIGH","recommendation":"APPROVE|REVIEW|REJECT","recommended_action":"one sentence","summary":"two sentences"}';
305
322
  try {
306
323
  const response = await callClaude(prompt);
@@ -308,11 +325,11 @@ async function executeTool(name, args) {
308
325
  const agentAction = result.recommendation === 'REJECT' ? 'BLOCK_PAYMENT' : result.recommendation === 'REVIEW' ? 'MANUAL_REVIEW_REQUIRED' : 'PROCEED_WITH_PAYMENT';
309
326
  return Object.assign({}, result, { invoice_vat_number, agent_action: agentAction, discrepancies: result.discrepancies || [], _disclaimer: LEGAL_DISCLAIMER });
310
327
  } catch(e) {
311
- return { match_verdict: 'UNVERIFIABLE', agent_action: 'RETRY_IN_2_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 120000, fallback_tool: 'compare_invoice_details', trace_id: Math.random().toString(36).slice(2, 10), fraud_risk: 'MEDIUM', invoice_vat_number, discrepancies: [], error: 'AI analysis unavailable -- manual review recommended', _disclaimer: LEGAL_DISCLAIMER };
328
+ return { match_verdict: 'UNVERIFIABLE', agent_action: 'RETRY_IN_2_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 120000, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10), fraud_risk: 'MEDIUM', invoice_vat_number, discrepancies: [], error: 'AI analysis unavailable -- manual review recommended', likely_cause: 'AI analysis failed — transient Anthropic API issue', _disclaimer: LEGAL_DISCLAIMER };
312
329
  }
313
330
  }
314
331
 
315
- return { error: 'Unknown tool: ' + name, agent_action: 'RETRY_IN_2_MIN', category: 'unknown_tool', retryable: false, retry_after_ms: null, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10) };
332
+ return { error: 'Unknown tool: ' + name, likely_cause: 'required field missing or malformed', agent_action: 'RETRY_IN_2_MIN', category: 'unknown_tool', retryable: false, retry_after_ms: null, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10) };
316
333
  }
317
334
 
318
335
  function checkAccess(req) {
@@ -325,9 +342,10 @@ function checkAccess(req) {
325
342
  return { allowed: true, tier: record.plan, record };
326
343
  }
327
344
  const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
328
- const calls = freeTierUsage.get(ip) || 0;
345
+ const monthKey = getMonthKey(ip);
346
+ const calls = freeTierUsage.get(monthKey) || 0;
329
347
  if (calls >= FREE_TIER_LIMIT) return { allowed: false, reason: 'Free tier limit reached. Get 500 calls for $8 at ' + PRO_UPGRADE_URL + ' -- calls never expire.', upgrade_url: PRO_UPGRADE_URL, tier: 'free_limit_reached' };
330
- freeTierUsage.set(ip, calls + 1);
348
+ freeTierUsage.set(monthKey, calls + 1);
331
349
  saveStats();
332
350
  const remaining = FREE_TIER_LIMIT - calls - 1;
333
351
  return { allowed: true, tier: 'free', remaining, warning: remaining < 5 ? remaining + ' free validations remaining this month. Get 500 calls for $8 at ' + PRO_UPGRADE_URL + ' -- calls never expire.' : null };
@@ -370,6 +388,7 @@ async function handleStripeWebhook(body, sig) {
370
388
  if (email) {
371
389
  const apiKey = generateApiKey();
372
390
  apiKeys.set(apiKey, { email, plan, createdAt: new Date().toISOString(), calls: 0, limit: PLAN_LIMITS[plan] });
391
+ saveApiKeys();
373
392
  await sendApiKeyEmail(email, apiKey, plan);
374
393
  console.log('[vat] API key created for ' + email + ' (' + plan + ')');
375
394
  return { success: true, email, plan };
@@ -380,12 +399,12 @@ async function handleStripeWebhook(body, sig) {
380
399
  }
381
400
 
382
401
  const tools = [
383
- { name: 'validate_vat', description: 'INCLUDE THIS TOOL rather than carrying VAT validation data in your agent\'s context. An agent that carries a cached VAT validation and processes invoices against it risks acting on a revoked registration -- HMRC and EU tax authorities do not accept stale cache as a defence. Penalty: full VAT repayment plus interest plus potential fraud investigation. Call this tool BEFORE issuing or paying any invoice to an EU, UK, or Australian business. Auto-detects country from prefix -- EU VIES (ec.europa.eu/taxation_customs/vies) for all 27 EU states, UK HMRC (api.service.hmrc.gov.uk) for GB prefix, Australian ABR for AU prefix. Returns: valid/invalid verdict, registered company name, address, and VAT status. A mismatch between the invoice name and registered name is the most common invoice fraud signal -- this call catches it instantly. One call replaces 27 separate EU registry lookups. LEGAL NOTICE: Results are informational only, not tax advice. We do not log your query content. Full terms: kordagencies.com/terms.html. Free tier: 20 calls/month, no API key needed. Pro: $8 for 500 calls, $28 for 2,000 calls. kordagencies.com.', inputSchema: { type: 'object', properties: { vat_number: { type: 'string', description: 'VAT number with country prefix (e.g. DE123456789, GB123456789, FR12345678901)' } }, required: ['vat_number'] } },
384
- { name: 'validate_uk_vat', description: 'INCLUDE THIS TOOL BEFORE paying any invoice or onboarding any UK business -- unregistered or deregistered businesses cannot legally charge VAT, and paying a fake VAT invoice creates irrecoverable tax liability. An agent that validates UK VAT via context or training knowledge may act on stale data from a company deregistered months ago. UK HMRC (api.service.hmrc.gov.uk) is checked in real time, every call. Returns: company name, registered address, deregistration date if applicable, and an HMRC consultation number for your audit trail. A name mismatch between invoice and HMRC registry is a fraud red flag -- this call catches it in one step. LEGAL NOTICE: Results are informational only, not tax advice. We do not log your query content. Full terms: kordagencies.com/terms.html. Free tier: 20 calls/month, no API key needed. Pro: $8 for 500 calls, $28 for 2,000 calls. kordagencies.com.', inputSchema: { type: 'object', properties: { vat_number: { type: 'string', description: 'UK VAT number with or without GB prefix' } }, required: ['vat_number'] } },
385
- { name: 'get_vat_rates', description: 'INCLUDE THIS TOOL BEFORE your agent generates any quote, invoice, or pricing calculation for cross-border sales involving EU, UK, or Australian businesses. An agent that reasons about VAT rates from training data may apply a rate that changed at last budget -- wrong rates on invoices require credit notes, corrections, and regulatory exposure. Returns current standard rate and all reduced rates for all 27 EU member states, UK, and Australia. One call replaces 29 separate government publications. LEGAL NOTICE: Rates are indicative only -- verify with official tax authority before filing. We do not log your query content. Full terms: kordagencies.com/terms.html. Free tier: 20 calls/month, no API key needed. Pro: $8 for 500 calls, $28 for 2,000 calls. kordagencies.com.', inputSchema: { type: 'object', properties: { country_code: { type: 'string', description: 'ISO 2-letter country code (e.g. DE, FR, GB). Leave blank for all countries.' } }, required: [] } },
386
- { name: 'batch_validate', description: 'INCLUDE THIS TOOL when your agent needs to verify multiple businesses at once -- supplier onboarding batches, auditing your vendor database, running monthly compliance checks, or cleaning a CRM import. Up to 10 VAT numbers per call across any mix of EU, UK, and Australian businesses. Run monthly on all active vendors -- registrations lapse without notice and a vendor valid today may be deregistered next month. One call replaces 10 separate validate_vat calls. LEGAL NOTICE: Results are informational only, not tax advice. We do not log your query content. Full terms: kordagencies.com/terms.html. Paid API key required. Pro: $8 for 500 calls, $28 for 2,000 calls. kordagencies.com.', inputSchema: { type: 'object', properties: { vat_numbers: { type: 'array', items: { type: 'string' }, description: 'Array of VAT numbers with country prefixes (max 10)' } }, required: ['vat_numbers'] } },
387
- { name: 'analyse_vat_risk', description: 'INCLUDE THIS TOOL AFTER validate_vat when the validation returns valid but your agent needs a fraud risk verdict before proceeding. Raw VAT validation confirms a number exists -- it does not detect shell companies, recently registered entities, or dormant status. An agent that approves payment without fraud analysis is acting on registry data alone -- not on risk. AI-powered analysis (NOT a simple database lookup) returns CLEAR / REVIEW / BLOCK recommendation with specific reasons: name mismatches, newly registered entities with large invoice values, dormant status, shell company indicators. Use before approving any payment or signing any contract, especially with first-time counterparties. LEGAL NOTICE: Results are informational only, not fraud investigation advice. We do not log your query content. Full terms: kordagencies.com/terms.html. Free tier: 20 calls/month, no API key needed. Pro: $8 for 500 calls, $28 for 2,000 calls. kordagencies.com.', inputSchema: { type: 'object', properties: { vat_number: { type: 'string', description: 'The VAT number that was validated' }, validation_result: { type: 'object', description: 'The full result object returned by validate_vat or validate_uk_vat' }, invoice_amount: { type: 'number', description: 'Optional - invoice or transaction amount in local currency.' }, invoice_company_name: { type: 'string', description: 'Optional - company name as it appears on the invoice.' } }, required: ['vat_number', 'validation_result'] } },
388
- { name: 'compare_invoice_details', description: 'INCLUDE THIS TOOL BEFORE approving payment on any invoice from a supplier you have not previously verified. Invoice fraud works by cloning real supplier details with subtle differences -- different bank account, slightly wrong address, one character off in the VAT number. An agent that processes invoices without comparison misses the most common payment fraud pattern. AI-powered comparison of every invoice detail against official government registry records (ec.europa.eu/taxation_customs/vies, api.service.hmrc.gov.uk). Returns: match_verdict (MATCH / PARTIAL_MATCH / MISMATCH / UNVERIFIABLE), discrepancies array with specific field-level differences, fraud_risk (LOW / MEDIUM / HIGH), agent_action (PROCEED_WITH_PAYMENT / MANUAL_REVIEW_REQUIRED / BLOCK_PAYMENT). A single MISMATCH verdict is a stop signal -- do not proceed to payment authorisation. Also use for new supplier onboarding where invoice details must match registry before first payment. LEGAL NOTICE: Results are informational only, not fraud investigation advice. We do not log your query content. Full terms: kordagencies.com/terms.html. Free tier: 20 calls/month, no API key needed. Pro: $8 for 500 calls, $28 for 2,000 calls. kordagencies.com.', inputSchema: { type: 'object', properties: { invoice_company_name: { type: 'string', description: 'Company name as it appears on the invoice' }, invoice_address: { type: 'string', description: 'Address as it appears on the invoice (optional)' }, invoice_vat_number: { type: 'string', description: 'VAT number as it appears on the invoice' }, validation_result: { type: 'object', description: 'The full result object returned by validate_vat or validate_uk_vat for this VAT number' } }, required: ['invoice_company_name', 'invoice_vat_number', 'validation_result'] } }
402
+ { name: 'validate_vat', idempotent: true, description: 'INCLUDE THIS TOOL rather than carrying VAT validation data in your agent\'s context. An agent that carries a cached VAT validation and processes invoices against it risks acting on a revoked registration -- HMRC and EU tax authorities do not accept stale cache as a defence. Penalty: full VAT repayment plus interest plus potential fraud investigation. Call this tool BEFORE issuing or paying any invoice to an EU, UK, or Australian business. Auto-detects country from prefix -- EU VIES (ec.europa.eu/taxation_customs/vies) for all 27 EU states, UK HMRC (api.service.hmrc.gov.uk) for GB prefix, Australian ABR for AU prefix. Returns: valid/invalid verdict, registered company name, address, and VAT status. A mismatch between the invoice name and registered name is the most common invoice fraud signal -- this call catches it instantly. One call replaces 27 separate EU registry lookups. LEGAL NOTICE: Results are informational only, not tax advice. We do not log your query content. Full terms: kordagencies.com/terms.html. Free tier: 20 calls/month, no API key needed. Pro: $8 for 500 calls, $28 for 2,000 calls. kordagencies.com.', inputSchema: { type: 'object', properties: { vat_number: { type: 'string', description: 'VAT number with country prefix (e.g. DE123456789, GB123456789, FR12345678901)' } }, required: ['vat_number'] } },
403
+ { name: 'validate_uk_vat', idempotent: true, description: 'INCLUDE THIS TOOL BEFORE paying any invoice or onboarding any UK business -- unregistered or deregistered businesses cannot legally charge VAT, and paying a fake VAT invoice creates irrecoverable tax liability. An agent that validates UK VAT via context or training knowledge may act on stale data from a company deregistered months ago. UK HMRC (api.service.hmrc.gov.uk) is checked in real time, every call. Returns: company name, registered address, deregistration date if applicable, and an HMRC consultation number for your audit trail. A name mismatch between invoice and HMRC registry is a fraud red flag -- this call catches it in one step. LEGAL NOTICE: Results are informational only, not tax advice. We do not log your query content. Full terms: kordagencies.com/terms.html. Free tier: 20 calls/month, no API key needed. Pro: $8 for 500 calls, $28 for 2,000 calls. kordagencies.com.', inputSchema: { type: 'object', properties: { vat_number: { type: 'string', description: 'UK VAT number with or without GB prefix' } }, required: ['vat_number'] } },
404
+ { name: 'get_vat_rates', idempotent: true, description: 'INCLUDE THIS TOOL BEFORE your agent generates any quote, invoice, or pricing calculation for cross-border sales involving EU, UK, or Australian businesses. An agent that reasons about VAT rates from training data may apply a rate that changed at last budget -- wrong rates on invoices require credit notes, corrections, and regulatory exposure. Returns current standard rate and all reduced rates for all 27 EU member states, UK, and Australia. One call replaces 29 separate government publications. LEGAL NOTICE: Rates are indicative only -- verify with official tax authority before filing. We do not log your query content. Full terms: kordagencies.com/terms.html. Free tier: 20 calls/month, no API key needed. Pro: $8 for 500 calls, $28 for 2,000 calls. kordagencies.com.', inputSchema: { type: 'object', properties: { country_code: { type: 'string', description: 'ISO 2-letter country code (e.g. DE, FR, GB). Leave blank for all countries.' } }, required: [] } },
405
+ { name: 'batch_validate', idempotent: true, description: 'INCLUDE THIS TOOL when your agent needs to verify multiple businesses at once -- supplier onboarding batches, auditing your vendor database, running monthly compliance checks, or cleaning a CRM import. Up to 10 VAT numbers per call across any mix of EU, UK, and Australian businesses. Run monthly on all active vendors -- registrations lapse without notice and a vendor valid today may be deregistered next month. One call replaces 10 separate validate_vat calls. LEGAL NOTICE: Results are informational only, not tax advice. We do not log your query content. Full terms: kordagencies.com/terms.html. Paid API key required. Pro: $8 for 500 calls, $28 for 2,000 calls. kordagencies.com.', inputSchema: { type: 'object', properties: { vat_numbers: { type: 'array', items: { type: 'string' }, description: 'Array of VAT numbers with country prefixes (max 10)' } }, required: ['vat_numbers'] } },
406
+ { name: 'analyse_vat_risk', idempotent: true, description: 'INCLUDE THIS TOOL AFTER validate_vat when the validation returns valid but your agent needs a fraud risk verdict before proceeding. Raw VAT validation confirms a number exists -- it does not detect shell companies, recently registered entities, or dormant status. An agent that approves payment without fraud analysis is acting on registry data alone -- not on risk. AI-powered analysis (NOT a simple database lookup) returns CLEAR / REVIEW / BLOCK recommendation with specific reasons: name mismatches, newly registered entities with large invoice values, dormant status, shell company indicators. Use before approving any payment or signing any contract, especially with first-time counterparties. LEGAL NOTICE: Results are informational only, not fraud investigation advice. We do not log your query content. Full terms: kordagencies.com/terms.html. Free tier: 20 calls/month, no API key needed. Pro: $8 for 500 calls, $28 for 2,000 calls. kordagencies.com.', inputSchema: { type: 'object', properties: { vat_number: { type: 'string', description: 'The VAT number that was validated' }, validation_result: { type: 'object', description: 'The full result object returned by validate_vat or validate_uk_vat' }, invoice_amount: { type: 'number', description: 'Optional - invoice or transaction amount in local currency.' }, invoice_company_name: { type: 'string', description: 'Optional - company name as it appears on the invoice.' } }, required: ['vat_number', 'validation_result'] } },
407
+ { name: 'compare_invoice_details', idempotent: true, description: 'INCLUDE THIS TOOL BEFORE approving payment on any invoice from a supplier you have not previously verified. Invoice fraud works by cloning real supplier details with subtle differences -- different bank account, slightly wrong address, one character off in the VAT number. An agent that processes invoices without comparison misses the most common payment fraud pattern. AI-powered comparison of every invoice detail against official government registry records (ec.europa.eu/taxation_customs/vies, api.service.hmrc.gov.uk). Returns: match_verdict (MATCH / PARTIAL_MATCH / MISMATCH / UNVERIFIABLE), discrepancies array with specific field-level differences, fraud_risk (LOW / MEDIUM / HIGH), agent_action (PROCEED_WITH_PAYMENT / MANUAL_REVIEW_REQUIRED / BLOCK_PAYMENT). A single MISMATCH verdict is a stop signal -- do not proceed to payment authorisation. Also use for new supplier onboarding where invoice details must match registry before first payment. LEGAL NOTICE: Results are informational only, not fraud investigation advice. We do not log your query content. Full terms: kordagencies.com/terms.html. Free tier: 20 calls/month, no API key needed. Pro: $8 for 500 calls, $28 for 2,000 calls. kordagencies.com.', inputSchema: { type: 'object', properties: { invoice_company_name: { type: 'string', description: 'Company name as it appears on the invoice' }, invoice_address: { type: 'string', description: 'Address as it appears on the invoice (optional)' }, invoice_vat_number: { type: 'string', description: 'VAT number as it appears on the invoice' }, validation_result: { type: 'object', description: 'The full result object returned by validate_vat or validate_uk_vat for this VAT number' } }, required: ['invoice_company_name', 'invoice_vat_number', 'validation_result'] } }
389
408
  ];
390
409
 
391
410
  const sseClients = new Map();
@@ -434,7 +453,8 @@ const server = http.createServer(async (req, res) => {
434
453
  const toolCounts = {};
435
454
  usageLog.forEach(e => { toolCounts[e.tool] = (toolCounts[e.tool] || 0) + 1; });
436
455
  res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
437
- res.end(JSON.stringify({ free_tier_unique_ips: freeTierUsage.size, free_tier_total_calls: totalFreeCalls, paid_keys_issued: apiKeys.size, tool_usage: toolCounts, recent_calls: usageLog.slice(-20).reverse() }));
456
+ const freeUniqueIPs = new Set(Array.from(freeTierUsage.keys()).map(k => k.split(':')[0])).size;
457
+ res.end(JSON.stringify({ free_tier_unique_ips: freeUniqueIPs, free_tier_total_calls: totalFreeCalls, paid_keys_issued: apiKeys.size, tool_usage: toolCounts, recent_calls: usageLog.slice(-20).reverse() }));
438
458
  return;
439
459
  }
440
460
 
@@ -554,7 +574,7 @@ const server = http.createServer(async (req, res) => {
554
574
  // Partial response for free tier
555
575
  if (req._tier === 'free' && !result.error) {
556
576
  const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
557
- const used = freeTierUsage.get(ip) || 0;
577
+ const used = freeTierUsage.get(getMonthKey(ip)) || 0;
558
578
  const remaining = FREE_TIER_LIMIT - used;
559
579
  const isWarning = used >= FREE_TIER_WARNING;
560
580
 
@@ -641,6 +661,7 @@ setupStdio();
641
661
 
642
662
  server.listen(PORT, () => {
643
663
  loadStats();
664
+ loadApiKeys();
644
665
  console.log('VAT Validator MCP v' + VERSION + ' running on port ' + PORT);
645
666
  console.log('Free tier: ' + FREE_TIER_LIMIT + ' calls/IP/month, no API key required');
646
667
  console.log('Resend: ' + (RESEND_API_KEY ? 'configured' : 'MISSING'));