vat-validator-mcp 1.4.13 → 2.0.1
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 +2 -2
- package/smithery.yaml +25 -55
- package/src/server.js +202 -100
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vat-validator-mcp",
|
|
3
3
|
"mcpName": "io.github.OjasKord/vat-validator-mcp",
|
|
4
|
-
"version": "
|
|
5
|
-
"description": "VAT number
|
|
4
|
+
"version": "2.0.1",
|
|
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": {
|
|
8
8
|
"start": "node src/server.js"
|
package/smithery.yaml
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
description: "
|
|
1
|
+
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."
|
|
2
2
|
startCommand:
|
|
3
3
|
type: http
|
|
4
4
|
url: https://vat-validator-mcp-production.up.railway.app
|
|
@@ -7,24 +7,15 @@ startCommand:
|
|
|
7
7
|
properties:
|
|
8
8
|
apiKey:
|
|
9
9
|
type: string
|
|
10
|
-
description: "VAT Validator MCP API key from kordagencies.com. Leave blank for free tier (
|
|
10
|
+
description: "VAT Validator MCP API key from kordagencies.com. Leave blank for free tier (50 validations/month)."
|
|
11
11
|
x-from:
|
|
12
12
|
header: "x-api-key"
|
|
13
13
|
required: []
|
|
14
14
|
|
|
15
15
|
systemPrompt: |
|
|
16
|
-
VAT Validator MCP is a business identity verification and invoice fraud detection layer for AI agents.
|
|
16
|
+
VAT Validator MCP v2 is a business identity verification and invoice fraud detection layer for AI agents.
|
|
17
17
|
|
|
18
|
-
It combines official government VAT registry data (EU VIES, UK HMRC, Australian ABR) with AI-powered fraud analysis
|
|
19
|
-
|
|
20
|
-
TWO LAYERS OF INTELLIGENCE
|
|
21
|
-
|
|
22
|
-
Layer 1 - Government Registry Validation (validate_vat, validate_uk_vat, get_vat_rates, batch_validate)
|
|
23
|
-
Confirms a business is real and registered with official authorities.
|
|
24
|
-
|
|
25
|
-
Layer 2 - AI-Powered Fraud Analysis (analyse_vat_risk, compare_invoice_details)
|
|
26
|
-
Uses AI to synthesise registry data with transaction context to detect fraud signals that raw validation misses.
|
|
27
|
-
This is not a database lookup - it is intelligent analysis that catches subtle red flags.
|
|
18
|
+
It combines official government VAT registry data (EU VIES, UK HMRC, Australian ABR) with AI-powered fraud analysis in a single self-contained call. No chained tool calls required — validate_vat handles jurisdiction detection, live registry lookup, and fraud scoring internally.
|
|
28
19
|
|
|
29
20
|
WHEN TO USE THIS SERVER
|
|
30
21
|
|
|
@@ -36,55 +27,35 @@ systemPrompt: |
|
|
|
36
27
|
- Apply B2B tax exemptions in e-commerce
|
|
37
28
|
- Audit or enrich CRM company records
|
|
38
29
|
- Run periodic compliance checks on active counterparties
|
|
39
|
-
|
|
40
|
-
RECOMMENDED WORKFLOW FOR INVOICE PROCESSING
|
|
41
|
-
|
|
42
|
-
When your agent receives an invoice from a new or unverified supplier:
|
|
43
|
-
1. validate_vat - confirm the VAT number is real and active
|
|
44
|
-
2. compare_invoice_details - AI checks if invoice details match registry (catches impersonation fraud)
|
|
45
|
-
3. analyse_vat_risk - AI risk assessment with CLEAR/REVIEW/BLOCK recommendation
|
|
46
|
-
Only proceed with payment if recommendation is CLEAR.
|
|
47
|
-
|
|
48
|
-
RECOMMENDED WORKFLOW FOR SUPPLIER ONBOARDING
|
|
49
|
-
|
|
50
|
-
When adding a new supplier to your approved vendor list:
|
|
51
|
-
1. validate_vat - confirm registration
|
|
52
|
-
2. analyse_vat_risk - AI fraud signal check
|
|
53
|
-
3. batch_validate - periodic re-validation of all active suppliers monthly
|
|
30
|
+
- Calculate or verify VAT amounts on an invoice
|
|
54
31
|
|
|
55
32
|
TOOLS
|
|
56
33
|
|
|
57
34
|
validate_vat
|
|
58
|
-
-
|
|
59
|
-
-
|
|
60
|
-
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
-
|
|
64
|
-
- Free tier: first 20 calls/month, no API key needed
|
|
35
|
+
- Auto-detects jurisdiction from VAT number prefix: 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 (ABN)
|
|
36
|
+
- Validates against EU VIES, UK HMRC VAT API v2 (OAuth), or AU ABR
|
|
37
|
+
- Runs AI fraud risk analysis internally — returns score 0-100 and PROCEED/VERIFY_MANUALLY/HOLD verdict
|
|
38
|
+
- Optional: pass invoice_company_name to cross-check against registry and detect name mismatches
|
|
39
|
+
- Optional: pass invoice_amount to weight fraud risk scoring
|
|
40
|
+
- Free tier: first 50 calls/month, no API key needed
|
|
65
41
|
|
|
66
42
|
get_vat_rates
|
|
67
|
-
-
|
|
68
|
-
-
|
|
43
|
+
- Returns current standard and reduced VAT rates for all 27 EU member states, UK, and Australia
|
|
44
|
+
- Omit country_code to retrieve all countries at once
|
|
45
|
+
- Free tier: first 50 calls/month, no API key needed
|
|
69
46
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
47
|
+
WORKFLOW FOR INVOICE PROCESSING
|
|
48
|
+
|
|
49
|
+
When your agent receives an invoice from a new or unverified supplier:
|
|
50
|
+
1. Call validate_vat with vat_number AND invoice_company_name (and optionally invoice_amount)
|
|
51
|
+
2. Check agent_action: PROCEED = clear to pay, VERIFY_MANUALLY = human review needed, HOLD = block payment
|
|
52
|
+
That is the complete workflow — one call returns everything needed.
|
|
74
53
|
|
|
75
|
-
|
|
76
|
-
- Call after validate_vat when you need a fraud risk assessment
|
|
77
|
-
- AI analyses registry data + transaction context for fraud signals
|
|
78
|
-
- Returns CLEAR/REVIEW/BLOCK recommendation with specific reasons
|
|
79
|
-
- Catches: name mismatches, newly registered companies, dormant status, shell company indicators
|
|
80
|
-
- Free tier: first 20 calls/month, no API key needed
|
|
54
|
+
WORKFLOW FOR VAT CALCULATION
|
|
81
55
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
- Flags discrepancies that indicate fraud, impersonation, or error
|
|
86
|
-
- Returns APPROVE/REVIEW/REJECT recommendation
|
|
87
|
-
- Free tier: first 20 calls/month, no API key needed
|
|
56
|
+
When your agent needs to calculate or verify a VAT amount:
|
|
57
|
+
1. Call get_vat_rates with the relevant country_code
|
|
58
|
+
2. Apply the returned standard or reduced rate
|
|
88
59
|
|
|
89
60
|
LEGAL NOTICE
|
|
90
61
|
All results are for informational purposes only and do not constitute legal or tax advice.
|
|
@@ -92,5 +63,4 @@ systemPrompt: |
|
|
|
92
63
|
Full terms: kordagencies.com/terms.html
|
|
93
64
|
|
|
94
65
|
FREE TIER
|
|
95
|
-
|
|
96
|
-
Upgrade at kordagencies.com - Pro $99/month (5,000 calls), Enterprise $299/month (unlimited + batch).
|
|
66
|
+
50 calls/month with no API key. Upgrade at kordagencies.com.
|
package/src/server.js
CHANGED
|
@@ -6,7 +6,7 @@ const Stripe = require('stripe');
|
|
|
6
6
|
const stripe = Stripe(process.env.STRIPE_SECRET_KEY);
|
|
7
7
|
|
|
8
8
|
const PERSIST_FILE = '/tmp/vat_stats.json';
|
|
9
|
-
const VERSION = '
|
|
9
|
+
const VERSION = '2.0.1';
|
|
10
10
|
const RESEND_API_KEY = process.env.RESEND_API_KEY || '';
|
|
11
11
|
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || '';
|
|
12
12
|
const PORT = process.env.PORT || 3000;
|
|
@@ -298,6 +298,7 @@ async function validateABN(abn) {
|
|
|
298
298
|
function detectCountry(vatNumber) {
|
|
299
299
|
const clean = vatNumber.trim().toUpperCase().replace(/\s/g, '');
|
|
300
300
|
if (clean.startsWith('GB')) return { country: 'GB', type: 'uk', number: clean.slice(2) };
|
|
301
|
+
if (clean.startsWith('ABN')) return { country: 'AU', type: 'au', number: clean.slice(3) };
|
|
301
302
|
if (clean.startsWith('AU') || /^\d{11}$/.test(clean)) return { country: 'AU', type: 'au', number: clean };
|
|
302
303
|
const euCodes = ['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'];
|
|
303
304
|
for (const code of euCodes) {
|
|
@@ -328,97 +329,193 @@ const VAT_RATES = {
|
|
|
328
329
|
|
|
329
330
|
async function executeTool(name, args) {
|
|
330
331
|
if (name === 'validate_vat') {
|
|
331
|
-
const vat_number = args
|
|
332
|
+
const { vat_number, invoice_company_name, invoice_amount } = args;
|
|
332
333
|
const checkedAt = nowISO();
|
|
333
|
-
|
|
334
|
+
|
|
335
|
+
if (!vat_number) return {
|
|
336
|
+
error: 'vat_number is required',
|
|
337
|
+
agent_action: 'PROVIDE_REQUIRED_FIELD',
|
|
338
|
+
category: 'invalid_input',
|
|
339
|
+
retryable: false,
|
|
340
|
+
retry_after_ms: null,
|
|
341
|
+
fallback_tool: null,
|
|
342
|
+
trace_id: Math.random().toString(36).slice(2, 10)
|
|
343
|
+
};
|
|
344
|
+
|
|
334
345
|
const detected = detectCountry(vat_number);
|
|
346
|
+
let valid = false;
|
|
347
|
+
let company_name = null;
|
|
348
|
+
let address = null;
|
|
349
|
+
let jurisdiction = '';
|
|
350
|
+
let sourceUrl = '';
|
|
351
|
+
|
|
335
352
|
if (detected.type === 'uk') {
|
|
353
|
+
jurisdiction = 'UK';
|
|
354
|
+
sourceUrl = 'api.service.hmrc.gov.uk';
|
|
336
355
|
const result = await validateHMRC(detected.number);
|
|
337
|
-
if (result.error) return {
|
|
356
|
+
if (result.error) return {
|
|
357
|
+
error: result.error,
|
|
358
|
+
vat_number,
|
|
359
|
+
jurisdiction,
|
|
360
|
+
agent_action: 'RETRY_IN_2_MIN',
|
|
361
|
+
category: 'upstream_unavailable',
|
|
362
|
+
retryable: true,
|
|
363
|
+
retry_after_ms: 120000,
|
|
364
|
+
fallback_tool: null,
|
|
365
|
+
trace_id: Math.random().toString(36).slice(2, 10),
|
|
366
|
+
source_url: sourceUrl,
|
|
367
|
+
checked_at: checkedAt,
|
|
368
|
+
_disclaimer: LEGAL_DISCLAIMER
|
|
369
|
+
};
|
|
338
370
|
const d = result.data;
|
|
339
|
-
if (result.status === 200 && d.target)
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
371
|
+
if (result.status === 200 && d.target) {
|
|
372
|
+
valid = true;
|
|
373
|
+
company_name = d.target.name || null;
|
|
374
|
+
address = d.target.address ? Object.values(d.target.address).filter(Boolean).join(', ') : null;
|
|
375
|
+
}
|
|
376
|
+
} else if (detected.type === 'eu') {
|
|
377
|
+
jurisdiction = 'EU';
|
|
378
|
+
sourceUrl = 'ec.europa.eu/taxation_customs/vies';
|
|
343
379
|
const result = await validateVIES(detected.country, detected.number);
|
|
344
|
-
if (result.error) return {
|
|
380
|
+
if (result.error) return {
|
|
381
|
+
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.',
|
|
382
|
+
vat_number,
|
|
383
|
+
jurisdiction,
|
|
384
|
+
agent_action: 'RETRY_IN_30_MIN',
|
|
385
|
+
category: 'upstream_unavailable',
|
|
386
|
+
retryable: true,
|
|
387
|
+
retry_after_ms: 1800000,
|
|
388
|
+
fallback_tool: null,
|
|
389
|
+
trace_id: Math.random().toString(36).slice(2, 10),
|
|
390
|
+
source_url: sourceUrl,
|
|
391
|
+
checked_at: checkedAt,
|
|
392
|
+
_disclaimer: LEGAL_DISCLAIMER
|
|
393
|
+
};
|
|
345
394
|
const d = result.data;
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
395
|
+
valid = d.isValid || false;
|
|
396
|
+
company_name = d.traderName || null;
|
|
397
|
+
address = d.traderAddress || null;
|
|
398
|
+
} else if (detected.type === 'au') {
|
|
399
|
+
jurisdiction = 'AU';
|
|
400
|
+
sourceUrl = 'abr.business.gov.au';
|
|
349
401
|
const result = await validateABN(detected.number);
|
|
350
|
-
if (result.error) return {
|
|
402
|
+
if (result.error) return {
|
|
403
|
+
error: result.error,
|
|
404
|
+
vat_number,
|
|
405
|
+
jurisdiction,
|
|
406
|
+
agent_action: 'RETRY_IN_2_MIN',
|
|
407
|
+
category: 'upstream_unavailable',
|
|
408
|
+
retryable: true,
|
|
409
|
+
retry_after_ms: 120000,
|
|
410
|
+
fallback_tool: null,
|
|
411
|
+
trace_id: Math.random().toString(36).slice(2, 10),
|
|
412
|
+
source_url: sourceUrl,
|
|
413
|
+
checked_at: checkedAt,
|
|
414
|
+
_disclaimer: LEGAL_DISCLAIMER
|
|
415
|
+
};
|
|
351
416
|
const d = result.data;
|
|
352
|
-
|
|
353
|
-
|
|
417
|
+
valid = !!(d.Abn && d.AbnStatus === 'Active');
|
|
418
|
+
company_name = d.EntityName || null;
|
|
419
|
+
} else {
|
|
420
|
+
return {
|
|
421
|
+
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 or ABN).',
|
|
422
|
+
vat_number,
|
|
423
|
+
agent_action: 'PROVIDE_COUNTRY_PREFIX',
|
|
424
|
+
category: 'invalid_input',
|
|
425
|
+
retryable: false,
|
|
426
|
+
retry_after_ms: null,
|
|
427
|
+
fallback_tool: null,
|
|
428
|
+
trace_id: Math.random().toString(36).slice(2, 10),
|
|
429
|
+
_disclaimer: LEGAL_DISCLAIMER
|
|
430
|
+
};
|
|
354
431
|
}
|
|
355
|
-
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 };
|
|
356
|
-
}
|
|
357
432
|
|
|
358
|
-
|
|
359
|
-
const
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
const result = await validateHMRC(vat_number);
|
|
363
|
-
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 };
|
|
364
|
-
const d = result.data;
|
|
365
|
-
if (result.status === 200 && d.target) return { valid: true, agent_action: 'PROCEED', 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 };
|
|
366
|
-
return { valid: false, agent_action: 'VERIFY_MANUALLY', vat_number, source: 'HMRC', source_url: 'api.service.hmrc.gov.uk', reason: d.code || 'VAT number not found', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
367
|
-
}
|
|
433
|
+
// AI fraud risk analysis — runs internally, result is always returned in one call
|
|
434
|
+
const nameSection = invoice_company_name ? `Invoice Company Name: ${invoice_company_name}\n` : '';
|
|
435
|
+
const amountSection = invoice_amount != null ? `Invoice Amount: ${invoice_amount}\n` : '';
|
|
436
|
+
const prompt = `You are a B2B fraud detection specialist. Analyze this VAT validation result for fraud risk.
|
|
368
437
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
return Object.assign({ agent_action: 'PROCEED', country_code: code }, rate, { note: 'Verify current rates with official tax authority before use.', source_url: 'kordagencies.com', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER });
|
|
377
|
-
}
|
|
438
|
+
VAT Number: ${vat_number}
|
|
439
|
+
Jurisdiction: ${jurisdiction}
|
|
440
|
+
Valid/Active: ${valid}
|
|
441
|
+
Registered Company Name: ${company_name || 'Not available from registry'}
|
|
442
|
+
Registered Address: ${address || 'Not available from registry'}
|
|
443
|
+
${nameSection}${amountSection}
|
|
444
|
+
Analyze for: registration status, jurisdiction risk factors, name mismatch between invoice and registry (if invoice company name provided), address anomalies, shell company indicators, missing trader fraud patterns, recently registered entity risk.
|
|
378
445
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
446
|
+
Name match rules: if no invoice_company_name was provided set name_match to "NOT_CHECKED". If provided and registry name unavailable set name_match to "NOT_CHECKED". If both available compare them: "MATCH" if they clearly refer to the same company (allow abbreviations and legal suffix variations), "MISMATCH" if clearly different companies.
|
|
447
|
+
|
|
448
|
+
recommendation must be exactly one of: CLEAR, REVIEW, or BLOCK. No other values permitted. CLEAR = valid, low risk. REVIEW = valid but requires manual verification. BLOCK = invalid or high/critical risk.
|
|
449
|
+
|
|
450
|
+
Return ONLY valid JSON with no preamble or markdown:
|
|
451
|
+
{"fraud_risk_score":0,"fraud_risk_level":"LOW","fraud_signals":[],"name_match":"NOT_CHECKED","recommendation":"CLEAR","summary":"one sentence plain English"}`;
|
|
452
|
+
|
|
453
|
+
let fraudRiskScore = 50;
|
|
454
|
+
let fraudRiskLevel = 'MEDIUM';
|
|
455
|
+
let fraudSignals = [];
|
|
456
|
+
let nameMatch = 'NOT_CHECKED';
|
|
457
|
+
let recommendation = 'REVIEW';
|
|
458
|
+
let summary = 'Manual review recommended — AI analysis unavailable.';
|
|
389
459
|
|
|
390
|
-
if (name === 'analyse_vat_risk') {
|
|
391
|
-
const vat_number = args.vat_number;
|
|
392
|
-
const validation_result = args.validation_result;
|
|
393
|
-
const invoice_amount = args.invoice_amount;
|
|
394
|
-
const invoice_company_name = args.invoice_company_name;
|
|
395
|
-
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) };
|
|
396
|
-
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"}';
|
|
397
460
|
try {
|
|
398
|
-
const
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
461
|
+
const aiResponse = await callClaude(prompt);
|
|
462
|
+
const parsed = JSON.parse(aiResponse.replace(/```json|```/g, '').trim());
|
|
463
|
+
fraudRiskScore = typeof parsed.fraud_risk_score === 'number' ? parsed.fraud_risk_score : fraudRiskScore;
|
|
464
|
+
fraudRiskLevel = parsed.fraud_risk_level || fraudRiskLevel;
|
|
465
|
+
fraudSignals = Array.isArray(parsed.fraud_signals) ? parsed.fraud_signals : [];
|
|
466
|
+
nameMatch = parsed.name_match || nameMatch;
|
|
467
|
+
recommendation = parsed.recommendation || recommendation;
|
|
468
|
+
summary = parsed.summary || summary;
|
|
402
469
|
} catch(e) {
|
|
403
|
-
|
|
470
|
+
if (!valid) {
|
|
471
|
+
fraudRiskLevel = 'HIGH';
|
|
472
|
+
fraudRiskScore = 75;
|
|
473
|
+
fraudSignals = ['VAT number invalid or deregistered'];
|
|
474
|
+
recommendation = 'BLOCK';
|
|
475
|
+
summary = 'VAT number is invalid or deregistered.';
|
|
476
|
+
}
|
|
404
477
|
}
|
|
405
|
-
}
|
|
406
478
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
const agentAction = result.match_verdict === 'MATCH' ? 'PROCEED' : 'INVESTIGATE';
|
|
415
|
-
return Object.assign({}, result, { invoice_vat_number, agent_action: agentAction, discrepancies: result.discrepancies || [], _disclaimer: LEGAL_DISCLAIMER });
|
|
416
|
-
} catch(e) {
|
|
417
|
-
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 };
|
|
479
|
+
let agentAction;
|
|
480
|
+
if (!valid || fraudRiskLevel === 'CRITICAL' || nameMatch === 'MISMATCH') {
|
|
481
|
+
agentAction = 'HOLD';
|
|
482
|
+
} else if (fraudRiskLevel === 'HIGH' || fraudRiskLevel === 'MEDIUM') {
|
|
483
|
+
agentAction = 'VERIFY_MANUALLY';
|
|
484
|
+
} else {
|
|
485
|
+
agentAction = 'PROCEED';
|
|
418
486
|
}
|
|
487
|
+
|
|
488
|
+
return {
|
|
489
|
+
agent_action: agentAction,
|
|
490
|
+
valid,
|
|
491
|
+
vat_number,
|
|
492
|
+
jurisdiction,
|
|
493
|
+
company_name,
|
|
494
|
+
address,
|
|
495
|
+
fraud_risk_score: fraudRiskScore,
|
|
496
|
+
fraud_risk_level: fraudRiskLevel,
|
|
497
|
+
fraud_signals: fraudSignals,
|
|
498
|
+
name_match: nameMatch,
|
|
499
|
+
recommendation,
|
|
500
|
+
summary,
|
|
501
|
+
source_url: sourceUrl,
|
|
502
|
+
checked_at: checkedAt,
|
|
503
|
+
_disclaimer: LEGAL_DISCLAIMER,
|
|
504
|
+
ai_notice: 'AI-powered fraud analysis — NOT a simple database lookup'
|
|
505
|
+
};
|
|
419
506
|
}
|
|
420
507
|
|
|
421
|
-
|
|
508
|
+
if (name === 'get_vat_rates') {
|
|
509
|
+
const country_code = args.country_code;
|
|
510
|
+
const checkedAt = nowISO();
|
|
511
|
+
if (!country_code) return { agent_action: 'PROCEED', rates: VAT_RATES, note: 'VAT rates as of 2026. Verify with official tax authority before use.', source_url: 'taxation-customs.ec.europa.eu/tedb/taxes-list.html', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
512
|
+
const code = country_code.toUpperCase();
|
|
513
|
+
const rate = VAT_RATES[code];
|
|
514
|
+
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: null, trace_id: Math.random().toString(36).slice(2, 10), _disclaimer: LEGAL_DISCLAIMER };
|
|
515
|
+
return Object.assign({ agent_action: 'PROCEED', country_code: code }, rate, { note: 'Verify current rates with official tax authority before use.', source_url: 'taxation-customs.ec.europa.eu/tedb/taxes-list.html', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER });
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
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) };
|
|
422
519
|
}
|
|
423
520
|
|
|
424
521
|
function checkAccess(req) {
|
|
@@ -558,12 +655,32 @@ async function handleStripeWebhook(body, sig) {
|
|
|
558
655
|
}
|
|
559
656
|
|
|
560
657
|
const tools = [
|
|
561
|
-
{
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
658
|
+
{
|
|
659
|
+
name: 'validate_vat',
|
|
660
|
+
idempotent: true,
|
|
661
|
+
description: 'Call this BEFORE your agent approves any supplier invoice, onboards a new EU/UK/AU vendor, or processes any cross-border payment. Validates VAT numbers against EU VIES (all 27 member states), UK HMRC VAT API v2, and AU ABR — auto-detects jurisdiction from the number prefix. Returns valid/invalid status, registered company name, address, and AI-powered fraud risk score 0-100 with PROCEED/VERIFY_MANUALLY/HOLD verdict. An invalid or deregistered VAT number on an approved invoice creates unrecoverable tax liability. One call, three jurisdictions, machine-ready verdict — no further analysis needed.',
|
|
662
|
+
inputSchema: {
|
|
663
|
+
type: 'object',
|
|
664
|
+
properties: {
|
|
665
|
+
vat_number: { type: 'string', description: 'VAT number with country prefix. EU: DE123456789. UK: GB123456789. AU: ABN12345678901.' },
|
|
666
|
+
invoice_company_name: { type: 'string', description: 'Company name as it appears on the invoice — if provided, cross-checks against registry and flags mismatches.' },
|
|
667
|
+
invoice_amount: { type: 'number', description: 'Invoice amount in local currency — used in fraud risk weighting.' }
|
|
668
|
+
},
|
|
669
|
+
required: ['vat_number']
|
|
670
|
+
}
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
name: 'get_vat_rates',
|
|
674
|
+
idempotent: true,
|
|
675
|
+
description: 'Call this BEFORE your agent calculates invoice totals, applies tax rates, generates VAT-inclusive pricing, or validates that a VAT amount on an invoice is correct for a given country. Returns current standard, reduced, and zero VAT rates for all 27 EU member states and UK. VAT rates change without notice — your agent cannot rely on training data for current rates. Returns machine-readable JSON — no parsing needed. Omit country_code to get all countries.',
|
|
676
|
+
inputSchema: {
|
|
677
|
+
type: 'object',
|
|
678
|
+
properties: {
|
|
679
|
+
country_code: { type: 'string', description: 'ISO 2-letter code e.g. DE, FR, GB. Omit for all countries.' }
|
|
680
|
+
},
|
|
681
|
+
required: []
|
|
682
|
+
}
|
|
683
|
+
}
|
|
567
684
|
];
|
|
568
685
|
|
|
569
686
|
const sseClients = new Map();
|
|
@@ -655,7 +772,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
655
772
|
|
|
656
773
|
if (req.url === '/.well-known/mcp/server-card.json' && req.method === 'GET') {
|
|
657
774
|
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
658
|
-
res.end(JSON.stringify({ name: 'vat-validator-mcp', title: 'VAT Validator MCP', version: VERSION, description: 'VAT
|
|
775
|
+
res.end(JSON.stringify({ name: 'vat-validator-mcp', title: 'VAT Validator MCP', version: VERSION, 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.', 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', 'get_vat_rates'], circuit_breaker: false, health_endpoint: '/health', ready_endpoint: '/ready' }));
|
|
659
776
|
return;
|
|
660
777
|
}
|
|
661
778
|
|
|
@@ -686,7 +803,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
686
803
|
const request = JSON.parse(body);
|
|
687
804
|
let response;
|
|
688
805
|
if (request.method === 'initialize') {
|
|
689
|
-
response = { jsonrpc: '2.0', id: request.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, resources: {}, prompts: {} }, serverInfo: { name: 'vat-validator-mcp', version: VERSION, description: '
|
|
806
|
+
response = { jsonrpc: '2.0', id: request.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, resources: {}, prompts: {} }, serverInfo: { name: 'vat-validator-mcp', version: VERSION, description: 'VAT Validator MCP v2. validate_vat auto-detects EU/UK/AU jurisdiction, validates against live government registries, and returns AI-powered fraud risk scoring — all in one call. No chained inputs, no prior state required.' } } };
|
|
690
807
|
} else if (request.method === 'notifications/initialized') {
|
|
691
808
|
res.writeHead(204, cors); res.end(); return;
|
|
692
809
|
} else if (request.method === 'tools/list') {
|
|
@@ -742,7 +859,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
742
859
|
req._tier = access.tier;
|
|
743
860
|
req._accessResult = access;
|
|
744
861
|
}
|
|
745
|
-
if (request.method === 'initialize') { response = { jsonrpc: '2.0', id: request.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, resources: {}, prompts: {} }, serverInfo: { name: 'vat-validator-mcp', version: VERSION, description: '
|
|
862
|
+
if (request.method === 'initialize') { response = { jsonrpc: '2.0', id: request.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, resources: {}, prompts: {} }, serverInfo: { name: 'vat-validator-mcp', version: VERSION, description: 'VAT Validator MCP v2. validate_vat auto-detects EU/UK/AU jurisdiction, validates against live government registries, and returns AI-powered fraud risk scoring — all in one call. No chained inputs, no prior state required.' } } };
|
|
746
863
|
} else if (request.method === 'notifications/initialized') { res.writeHead(204, cors); res.end(); return;
|
|
747
864
|
} else if (request.method === 'tools/list') { response = { jsonrpc: '2.0', id: request.id, result: { tools } };
|
|
748
865
|
} else if (request.method === 'resources/list') { response = { jsonrpc: '2.0', id: request.id, result: { resources: [] } };
|
|
@@ -761,31 +878,16 @@ const server = http.createServer(async (req, res) => {
|
|
|
761
878
|
reportMeteredUsage(req._accessResult.stripeCustomerId, 'vat_query').catch(() => {});
|
|
762
879
|
}
|
|
763
880
|
|
|
764
|
-
// Partial response for free tier
|
|
765
881
|
if (req._tier === 'free' && !result.error) {
|
|
766
882
|
const used = freeTierUsage.get(getMonthKey(ip)) || 0;
|
|
767
883
|
const remaining = FREE_TIER_LIMIT - used;
|
|
768
884
|
const isWarning = used >= FREE_TIER_WARNING;
|
|
769
885
|
const effectiveLimit = getEffectiveLimit(ip);
|
|
770
886
|
|
|
771
|
-
if (name === 'validate_vat'
|
|
772
|
-
const gated = ['
|
|
773
|
-
gated.forEach(f => delete result[f]);
|
|
774
|
-
result._upgrade_note = 'Free tier: ' + remaining + ' of ' + effectiveLimit + ' calls remaining. Get 500 calls for $8 at ' + BUNDLE_500_URL + ' -- calls never expire. Includes full registered address and HMRC consultation number.';
|
|
775
|
-
result._gated_fields = gated;
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
if (name === 'analyse_vat_risk') {
|
|
779
|
-
const gated = ['fraud_signals', 'positive_indicators', 'recommended_action', 'summary'];
|
|
780
|
-
gated.forEach(f => delete result[f]);
|
|
781
|
-
result._upgrade_note = 'Free tier: ' + remaining + ' of ' + effectiveLimit + ' calls remaining. Get 500 calls for $8 at ' + BUNDLE_500_URL + ' -- calls never expire. Includes full fraud signal breakdown, positive indicators, and recommended action.';
|
|
782
|
-
result._gated_fields = gated;
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
if (name === 'compare_invoice_details') {
|
|
786
|
-
const gated = ['discrepancies', 'name_match', 'address_match', 'recommended_action', 'summary'];
|
|
887
|
+
if (name === 'validate_vat') {
|
|
888
|
+
const gated = ['fraud_signals', 'address'];
|
|
787
889
|
gated.forEach(f => delete result[f]);
|
|
788
|
-
result._upgrade_note = 'Free tier: ' + remaining + ' of ' + effectiveLimit + ' calls remaining. Get 500 calls for $8 at ' + BUNDLE_500_URL + ' -- calls never expire. Includes full
|
|
890
|
+
result._upgrade_note = 'Free tier: ' + remaining + ' of ' + effectiveLimit + ' calls remaining. Get 500 calls for $8 at ' + BUNDLE_500_URL + ' -- calls never expire. Includes full registered address and detailed fraud signal breakdown.';
|
|
789
891
|
result._gated_fields = gated;
|
|
790
892
|
}
|
|
791
893
|
|
|
@@ -801,7 +903,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
801
903
|
return;
|
|
802
904
|
}
|
|
803
905
|
|
|
804
|
-
if (req.method === 'GET' && req.url === '/') { res.writeHead(200, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ name: 'vat-validator-mcp', version: VERSION, status: 'ok', tools:
|
|
906
|
+
if (req.method === 'GET' && req.url === '/') { res.writeHead(200, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ name: 'vat-validator-mcp', version: VERSION, status: 'ok', tools: 2, free_tier: '50 calls/month, no API key required', description: 'VAT validation + AI fraud detection. EU VIES, UK HMRC, Australian ABN.', subscribe_url: METERED_SUBSCRIBE_URL, bundle_500_url: BUNDLE_500_URL, bundle_2000_url: BUNDLE_2000_URL })); return; }
|
|
805
907
|
|
|
806
908
|
if (req.url === '/subscribe' && req.method === 'GET') {
|
|
807
909
|
try {
|
|
@@ -869,7 +971,7 @@ function setupStdio() {
|
|
|
869
971
|
try { req = JSON.parse(line); } catch(e) { return; }
|
|
870
972
|
let response;
|
|
871
973
|
if (req.method === 'initialize') {
|
|
872
|
-
response = { jsonrpc: '2.0', id: req.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, resources: {}, prompts: {} }, serverInfo: { name: 'vat-validator-mcp', version: VERSION, description: '
|
|
974
|
+
response = { jsonrpc: '2.0', id: req.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, resources: {}, prompts: {} }, serverInfo: { name: 'vat-validator-mcp', version: VERSION, description: 'VAT Validator MCP v2. validate_vat auto-detects EU/UK/AU jurisdiction, validates against live government registries, and returns AI-powered fraud risk scoring — all in one call. No chained inputs, no prior state required.' } } };
|
|
873
975
|
} else if (req.method === 'notifications/initialized') {
|
|
874
976
|
return;
|
|
875
977
|
} else if (req.method === 'tools/list') {
|