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 +1 -1
- package/server.json +2 -2
- package/src/server.js +26 -18
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
|
+
"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.
|
|
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.
|
|
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.
|
|
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:
|
|
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
|
|