vat-validator-mcp 1.4.4 → 1.4.5

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": "1.4.4",
4
+ "version": "1.4.5",
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": {
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.4",
6
+ "version": "1.4.5",
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.4",
16
+ "version": "1.4.5",
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,7 @@ 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.4';
7
+ const VERSION = '1.4.5';
8
8
  const RESEND_API_KEY = process.env.RESEND_API_KEY || '';
9
9
  const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || '';
10
10
  const PORT = process.env.PORT || 3000;
@@ -224,36 +224,36 @@ async function executeTool(name, args) {
224
224
  if (name === 'validate_vat') {
225
225
  const vat_number = args.vat_number;
226
226
  const checkedAt = nowISO();
227
- if (!vat_number) return { error: 'vat_number is required', agent_action: 'PROVIDE_REQUIRED_FIELD' };
227
+ 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) };
228
228
  const detected = detectCountry(vat_number);
229
229
  if (detected.type === 'uk') {
230
230
  const result = await validateHMRC(detected.number);
231
- if (result.error) return { valid: null, vat_number, country: 'GB', source: 'HMRC', error: result.error, agent_action: 'RETRY_IN_2_MIN', retry: true, _disclaimer: LEGAL_DISCLAIMER };
231
+ 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 };
232
232
  const d = result.data;
233
233
  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 };
234
234
  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 };
235
235
  }
236
236
  if (detected.type === 'eu') {
237
237
  const result = await validateVIES(detected.country, detected.number);
238
- if (result.error) return { valid: null, vat_number, agent_action: 'RETRY_IN_30_MIN', 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 };
238
+ 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 };
239
239
  const d = result.data;
240
240
  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 };
241
241
  }
242
242
  if (detected.type === 'au') {
243
243
  const result = await validateABN(detected.number);
244
- if (result.error) return { valid: null, vat_number, country: 'AU', source: 'ABR', error: result.error, agent_action: 'RETRY_IN_2_MIN', _disclaimer: LEGAL_DISCLAIMER };
244
+ 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 };
245
245
  const d = result.data;
246
246
  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 };
247
247
  }
248
- return { valid: null, vat_number, agent_action: 'PROVIDE_COUNTRY_PREFIX', 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 };
248
+ 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 };
249
249
  }
250
250
 
251
251
  if (name === 'validate_uk_vat') {
252
252
  const vat_number = args.vat_number;
253
253
  const checkedAt = nowISO();
254
- if (!vat_number) return { error: 'vat_number is required', agent_action: 'PROVIDE_REQUIRED_FIELD' };
254
+ 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) };
255
255
  const result = await validateHMRC(vat_number);
256
- 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.', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
256
+ 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 };
257
257
  const d = result.data;
258
258
  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 };
259
259
  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 };
@@ -265,17 +265,17 @@ async function executeTool(name, args) {
265
265
  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 };
266
266
  const code = country_code.toUpperCase();
267
267
  const rate = VAT_RATES[code];
268
- if (!rate) return { error: 'No VAT rate data for: ' + code + '. Supported: ' + Object.keys(VAT_RATES).join(', '), _disclaimer: LEGAL_DISCLAIMER };
268
+ 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 };
269
269
  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 });
270
270
  }
271
271
 
272
272
  if (name === 'batch_validate') {
273
273
  const vat_numbers = args.vat_numbers;
274
- if (!vat_numbers || !Array.isArray(vat_numbers)) return { error: 'vat_numbers must be an array', agent_action: 'PROVIDE_REQUIRED_FIELD' };
275
- 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' };
274
+ 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) };
275
+ 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) };
276
276
  const results = await Promise.all(vat_numbers.map(async (vat) => {
277
277
  try { return await executeTool('validate_vat', { vat_number: vat }); }
278
- catch(e) { return { vat_number: vat, valid: null, error: e.message }; }
278
+ 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) }; }
279
279
  }));
280
280
  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 };
281
281
  }
@@ -285,20 +285,20 @@ async function executeTool(name, args) {
285
285
  const validation_result = args.validation_result;
286
286
  const invoice_amount = args.invoice_amount;
287
287
  const invoice_company_name = args.invoice_company_name;
288
- if (!vat_number || !validation_result) return { error: 'vat_number and validation_result are required', agent_action: 'PROVIDE_REQUIRED_FIELD' };
288
+ 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) };
289
289
  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"}';
290
290
  try {
291
291
  const response = await callClaude(prompt);
292
292
  const result = JSON.parse(response.replace(/```json|```/g, '').trim());
293
293
  return Object.assign({}, result, { vat_number, _disclaimer: LEGAL_DISCLAIMER });
294
294
  } catch(e) {
295
- return { recommendation: 'REVIEW', risk_level: 'MEDIUM', risk_score: 50, vat_number, error: 'AI analysis unavailable - manual review recommended', _disclaimer: LEGAL_DISCLAIMER };
295
+ 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 };
296
296
  }
297
297
  }
298
298
 
299
299
  if (name === 'compare_invoice_details') {
300
300
  const { invoice_company_name, invoice_address, invoice_vat_number, validation_result } = args;
301
- 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' };
301
+ 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) };
302
302
  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"}';
303
303
  try {
304
304
  const response = await callClaude(prompt);
@@ -306,11 +306,11 @@ async function executeTool(name, args) {
306
306
  const agentAction = result.recommendation === 'REJECT' ? 'BLOCK_PAYMENT' : result.recommendation === 'REVIEW' ? 'MANUAL_REVIEW_REQUIRED' : 'PROCEED_WITH_PAYMENT';
307
307
  return Object.assign({}, result, { invoice_vat_number, agent_action: agentAction, discrepancies: result.discrepancies || [], _disclaimer: LEGAL_DISCLAIMER });
308
308
  } catch(e) {
309
- return { match_verdict: 'UNVERIFIABLE', agent_action: 'RETRY_IN_2_MIN', fraud_risk: 'MEDIUM', invoice_vat_number, discrepancies: [], error: 'AI analysis unavailable -- manual review recommended', _disclaimer: LEGAL_DISCLAIMER };
309
+ 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 };
310
310
  }
311
311
  }
312
312
 
313
- return { error: 'Unknown tool: ' + name, agent_action: 'RETRY_IN_2_MIN' };
313
+ 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) };
314
314
  }
315
315
 
316
316
  function checkAccess(req) {
@@ -397,6 +397,14 @@ const server = http.createServer(async (req, res) => {
397
397
  return;
398
398
  }
399
399
 
400
+ if (req.url === '/ready' && (req.method === 'GET' || req.method === 'HEAD')) {
401
+ const checks = { anthropic: !!ANTHROPIC_API_KEY, hmrc_client_id: !!(process.env.HMRC_CLIENT_ID), hmrc_client_secret: !!(process.env.HMRC_CLIENT_SECRET) };
402
+ const ready = checks.anthropic && checks.hmrc_client_id && checks.hmrc_client_secret;
403
+ res.writeHead(ready ? 200 : 503, { ...cors, 'Content-Type': 'application/json' });
404
+ res.end(JSON.stringify({ status: ready ? 'ready' : 'not_ready', version: VERSION, checks }));
405
+ return;
406
+ }
407
+
400
408
  if (req.url === '/deps' && req.method === 'GET') {
401
409
  const depCheck = (hostname, path, headers) => new Promise((resolve) => {
402
410
  const r = https.request({ hostname, path, method: 'GET', headers: Object.assign({ 'User-Agent': 'VAT-Validator-MCP-HealthCheck/1.0' }, headers || {}) }, (res2) => {
@@ -443,7 +451,7 @@ const server = http.createServer(async (req, res) => {
443
451
 
444
452
  if (req.url === '/.well-known/mcp/server-card.json' && req.method === 'GET') {
445
453
  res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
446
- res.end(JSON.stringify({ name: 'vat-validator-mcp', title: 'VAT Validator MCP', version: VERSION, description: 'VAT validation + AI fraud detection. EU VIES, UK HMRC, Australian ABN. Free tier: 20 calls/month.', tools: tools.map(t => t.name), transport: ['http', 'stdio'], homepage: 'https://kordagencies.com' }));
454
+ res.end(JSON.stringify({ name: 'vat-validator-mcp', title: 'VAT Validator MCP', version: VERSION, description: 'VAT validation + AI fraud detection. EU VIES, UK HMRC, Australian ABN. Free tier: 20 calls/month.', tools: tools.map(t => t.name), transport: 'streamable-http', homepage: 'https://kordagencies.com', token_footprint_min: 100, token_footprint_max: 600, token_footprint_avg: 200, idempotent_tools: ['validate_vat', 'validate_uk_vat', 'get_vat_rates', 'batch_validate', 'analyse_vat_risk', 'compare_invoice_details'], circuit_breaker: false, health_endpoint: '/health', ready_endpoint: '/ready' }));
447
455
  return;
448
456
  }
449
457