vat-validator-mcp 1.4.12 → 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/glama.json CHANGED
@@ -1,19 +1,45 @@
1
1
  {
2
- "name": "vat-validator-mcp",
3
- "title": "VAT Validator MCP",
4
- "description": "Validate EU, UK, and Australian VAT numbers for AI agents. EU VIES, UK HMRC, Australian ABN. Required for EU ViDA e-invoicing compliance.",
5
- "version": "1.0.0",
6
- "homepage": "https://kordagencies.com",
7
- "license": "UNLICENSED",
2
+ "$schema": "https://glama.ai/mcp/servers/schema.json",
3
+ "name": "VAT Validator MCP",
4
+ "description": "AI-powered VAT fraud detection and live VAT validation via EU VIES (27 member states), UK HMRC, and AU ABR. Call before invoice approval, supplier onboarding, or cross-border payment. Detects missing trader fraud, carousel fraud, deregistered entity re-use. Returns CLEAR/REVIEW/BLOCK verdict.",
5
+ "license": "MIT",
6
+ "categories": [
7
+ "finance",
8
+ "government-data",
9
+ "legal-and-compliance"
10
+ ],
11
+ "remote": {
12
+ "transport": "sse",
13
+ "url": "https://vat-validator-mcp-production.up.railway.app/sse"
14
+ },
8
15
  "tools": [
9
- { "name": "validate_vat", "description": "Validate any EU, UK, or Australian VAT number" },
10
- { "name": "validate_uk_vat", "description": "Validate UK VAT number against HMRC with consultation number" },
11
- { "name": "get_vat_rates", "description": "Get VAT rates by country for EU, UK, Australia" },
12
- { "name": "batch_validate", "description": "Validate up to 10 VAT numbers in one call (paid)" }
16
+ {
17
+ "name": "validate_vat",
18
+ "description": "Validates EU VAT numbers against EU VIES (all 27 member states) and AU ABR in real time. Returns valid/invalid, registered company name, address."
19
+ },
20
+ {
21
+ "name": "validate_uk_vat",
22
+ "description": "Validates UK VAT numbers against HMRC VAT API v2 via OAuth2. Returns valid/invalid, registered business name, address."
23
+ },
24
+ {
25
+ "name": "get_vat_rates",
26
+ "description": "Returns current standard, reduced, and zero VAT rates for all 27 EU member states and UK."
27
+ },
28
+ {
29
+ "name": "batch_validate",
30
+ "description": "Validates multiple VAT numbers against EU VIES and HMRC in one call. Returns per-number verdicts in structured JSON."
31
+ },
32
+ {
33
+ "name": "analyse_vat_risk",
34
+ "description": "AI-powered VAT fraud risk scoring. Detects missing trader fraud, carousel fraud, deregistered entity re-use. Returns CLEAR/REVIEW/BLOCK recommendation, risk score 0-100, fraud signals, agent_action (PROCEED/VERIFY_MANUALLY/HOLD)."
35
+ },
36
+ {
37
+ "name": "compare_invoice_details",
38
+ "description": "Cross-checks invoice VAT details against live VIES and HMRC registry data. Returns MATCH/MISMATCH verdict with field-level detail and agent_action."
39
+ }
13
40
  ],
14
- "pricing": {
15
- "free": "20 validations/month, no API key",
16
- "pro": "$99/month — 5,000 validations/month",
17
- "enterprise": "$299/month — unlimited + batch"
41
+ "links": {
42
+ "homepage": "https://kordagencies.com",
43
+ "npm": "https://www.npmjs.com/package/vat-validator-mcp"
18
44
  }
19
45
  }
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": "1.4.12",
5
- "description": "VAT number validation for AI agents. EU VIES, UK HMRC, Australian ABN in one call.",
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"
@@ -40,5 +40,8 @@
40
40
  },
41
41
  "engines": {
42
42
  "node": ">=18.0.0"
43
+ },
44
+ "dependencies": {
45
+ "stripe": "^22.1.1"
43
46
  }
44
47
  }
package/railway ADDED
File without changes
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.12",
6
+ "version": "1.4.13",
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.12",
16
+ "version": "1.4.13",
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/smithery.yaml CHANGED
@@ -1,4 +1,4 @@
1
- description: "AI-powered VAT fraud detection and live VAT validation via EU VIES (27 member states), UK HMRC, and AU ABR. Call before invoice approval, supplier onboarding, or cross-border payment. Detects missing trader fraud, carousel fraud, deregistered entity re-use. Returns CLEAR/REVIEW/BLOCK verdict."
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 (20 validations/month)."
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 to give agents a complete picture before any B2B transaction.
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
- - Validates any EU, UK, or Australian VAT number against live government registries
59
- - Auto-detects country from prefix
60
- - Free tier: first 20 calls/month, no API key needed
61
-
62
- validate_uk_vat
63
- - UK-specific HMRC validation with consultation number for audit trail
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
- - Current VAT rates for all 27 EU countries, UK, and Australia
68
- - Free tier: first 20 calls/month, no API key needed
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
- batch_validate
71
- - Validate up to 10 VAT numbers in one call
72
- - Use for supplier audits and onboarding batches
73
- - Paid API key required
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
- analyse_vat_risk (AI-POWERED)
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
- compare_invoice_details (AI-POWERED)
83
- - Call when processing an invoice to verify supplier details match registry records
84
- - AI compares invoice name/address/VAT against official registered data
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
- 20 calls/month with no API key.
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
@@ -2,26 +2,28 @@ const http = require('http');
2
2
  const https = require('https');
3
3
  const crypto = require('crypto');
4
4
  const fs = require('fs');
5
+ const Stripe = require('stripe');
6
+ const stripe = Stripe(process.env.STRIPE_SECRET_KEY);
5
7
 
6
8
  const PERSIST_FILE = '/tmp/vat_stats.json';
7
- const API_KEYS_FILE = '/tmp/vat_apikeys.json';
8
- const VERSION = '1.4.12';
9
- const PRO_UPGRADE_URL = 'https://buy.stripe.com/28EeVceUB06N1ty3teebu0l';
10
- const ENTERPRISE_UPGRADE_URL = 'https://buy.stripe.com/00w14m7s96vb1ty5Bmebu0m';
9
+ const VERSION = '2.0.1';
11
10
  const RESEND_API_KEY = process.env.RESEND_API_KEY || '';
12
11
  const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || '';
13
12
  const PORT = process.env.PORT || 3000;
14
13
  const STATS_KEY = process.env.STATS_KEY || 'ojas2026';
14
+ const REDIS_PREFIX = 'vat';
15
+ const FREE_TIER_LIMIT = 50;
16
+ const METERED_SUBSCRIBE_URL = 'https://vat-validator-mcp-production.up.railway.app/subscribe';
17
+ const BUNDLE_500_URL = 'https://buy.stripe.com/28EeVceUB06N1ty3teebu0l';
18
+ const BUNDLE_2000_URL = 'https://buy.stripe.com/00w14m7s96vb1ty5Bmebu0m';
15
19
 
16
20
  const freeTierUsage = new Map();
17
21
  const usageLog = [];
18
22
  const toolUsageCounts = {};
19
23
  const trialExtensions = new Map();
20
- const FREE_TIER_LIMIT = 20;
21
- const FREE_TIER_WARNING = 16;
24
+ const FREE_TIER_WARNING = 40;
22
25
  const TRIAL_EXTENSION_CALLS = 10;
23
26
  const apiKeys = new Map();
24
- const PLAN_LIMITS = { pro: 5000, enterprise: Infinity };
25
27
 
26
28
  function saveStats() {
27
29
  try {
@@ -56,27 +58,98 @@ function getEffectiveLimit(ip) {
56
58
  return FREE_TIER_LIMIT;
57
59
  }
58
60
 
59
- function saveApiKeys() {
60
- try { fs.writeFileSync(API_KEYS_FILE, JSON.stringify(Array.from(apiKeys.entries()))); } catch(e) { console.error('API keys save error:', e.message); }
61
+ const UPSTASH_URL = process.env.UPSTASH_REDIS_REST_URL;
62
+ const UPSTASH_TOKEN = process.env.UPSTASH_REDIS_REST_TOKEN;
63
+
64
+ async function redisGet(key) {
65
+ try {
66
+ const res = await fetch(
67
+ `${UPSTASH_URL}/get/${encodeURIComponent(key)}`,
68
+ { headers: { Authorization: `Bearer ${UPSTASH_TOKEN}` } }
69
+ );
70
+ const data = await res.json();
71
+ if (!data.result) return null;
72
+ return JSON.parse(data.result);
73
+ } catch(e) { return null; }
74
+ }
75
+
76
+ async function redisSet(key, value) {
77
+ try {
78
+ await fetch(
79
+ `${UPSTASH_URL}/set/${encodeURIComponent(key)}`,
80
+ {
81
+ method: 'POST',
82
+ headers: {
83
+ Authorization: `Bearer ${UPSTASH_TOKEN}`,
84
+ 'Content-Type': 'application/json'
85
+ },
86
+ body: JSON.stringify({ value: JSON.stringify(value) })
87
+ }
88
+ );
89
+ } catch(e) {}
61
90
  }
62
91
 
63
- function loadApiKeys() {
92
+ async function redisKeys(pattern) {
64
93
  try {
65
- if (fs.existsSync(API_KEYS_FILE)) {
66
- const entries = JSON.parse(fs.readFileSync(API_KEYS_FILE, 'utf8'));
67
- entries.forEach(([k, v]) => apiKeys.set(k, v));
68
- console.log('API keys loaded: ' + apiKeys.size + ' keys');
94
+ const res = await fetch(
95
+ `${UPSTASH_URL}/keys/${encodeURIComponent(pattern)}`,
96
+ { headers: { Authorization: `Bearer ${UPSTASH_TOKEN}` } }
97
+ );
98
+ const data = await res.json();
99
+ return data.result || [];
100
+ } catch(e) { return []; }
101
+ }
102
+
103
+ async function saveKeyToRedis(apiKey, record, prefix) {
104
+ await redisSet(`${prefix}:key:${apiKey}`, record);
105
+ }
106
+
107
+ async function loadApiKeysFromRedis(prefix) {
108
+ const keys = await redisKeys(`${prefix}:key:*`);
109
+ for (const redisKey of keys) {
110
+ const record = await redisGet(redisKey);
111
+ if (record) {
112
+ const apiKey = redisKey.replace(`${prefix}:key:`, '');
113
+ apiKeys.set(apiKey, record);
69
114
  }
70
- } catch(e) { console.error('API keys load error:', e.message); }
115
+ }
116
+ console.log(`Loaded ${apiKeys.size} API keys from Redis`);
71
117
  }
72
118
 
73
119
  function generateApiKey() { return 'vat_' + crypto.randomBytes(24).toString('hex'); }
74
- function getPlanFromProduct(name) {
75
- if (!name) return 'pro';
76
- return name.toLowerCase().includes('enterprise') ? 'enterprise' : 'pro';
120
+ function getPlanFromProduct(productName) {
121
+ if (!productName) return 'bundle_500';
122
+ const n = productName.toLowerCase();
123
+ if (n.includes('metered') || n.includes('pay as you go') || n === 'metered') return 'metered';
124
+ if (n.includes('2000') || n.includes('2,000') || n.includes('enterprise')) return 'bundle_2000';
125
+ return 'bundle_500';
77
126
  }
78
127
  function nowISO() { return new Date().toISOString(); }
79
128
 
129
+ function checkAndResetPeriod(record) {
130
+ const thirtyDays = 30 * 24 * 60 * 60 * 1000;
131
+ if (Date.now() - record.periodStart > thirtyDays) {
132
+ record.calls = 0;
133
+ record.periodStart = Date.now();
134
+ return true;
135
+ }
136
+ return false;
137
+ }
138
+
139
+ async function reportMeteredUsage(customerId, eventName) {
140
+ try {
141
+ await stripe.billing.meterEvents.create({
142
+ event_name: eventName,
143
+ payload: {
144
+ stripe_customer_id: customerId,
145
+ value: '1'
146
+ }
147
+ });
148
+ } catch(e) {
149
+ console.error('Stripe metered usage report failed:', e.message);
150
+ }
151
+ }
152
+
80
153
  async function sendEmail(to, subject, html) {
81
154
  return new Promise((resolve) => {
82
155
  const body = JSON.stringify({ from: 'VAT Validator MCP <ojas@kordagencies.com>', to: [to], subject, html });
@@ -90,10 +163,10 @@ async function sendEmail(to, subject, html) {
90
163
  }
91
164
 
92
165
  async function sendApiKeyEmail(email, apiKey, plan) {
93
- const planLabel = plan === 'enterprise' ? 'Enterprise' : 'Pro';
94
- const limit = plan === 'enterprise' ? 'Unlimited' : '5,000';
95
- const html = '<!DOCTYPE html><html><body style="font-family:monospace;background:#080A0F;color:#E8EDF5;padding:40px;max-width:600px;margin:0 auto"><div style="border:1px solid rgba(0,229,195,0.3);border-radius:8px;padding:32px"><div style="color:#00E5C3;font-size:13px;letter-spacing:0.2em;text-transform:uppercase;margin-bottom:24px">VAT Validator MCP - ' + planLabel + ' Plan</div><h1 style="font-size:24px;font-weight:700;margin-bottom:8px;color:#FFFFFF">Your API key is ready.</h1><div style="background:#141B24;border:1px solid rgba(255,255,255,0.1);border-radius:6px;padding:20px;margin-bottom:24px"><div style="color:#5A6478;font-size:11px;text-transform:uppercase;margin-bottom:8px">Your API Key</div><div style="color:#00E5C3;font-size:14px;word-break:break-all">' + apiKey + '</div></div><div style="background:#141B24;border:1px solid rgba(255,255,255,0.1);border-radius:6px;padding:20px;margin-bottom:24px"><div style="color:#5A6478;font-size:11px;text-transform:uppercase;margin-bottom:8px">MCP Config</div><div style="color:#86EFAC;font-size:12px">{"vat-validator":{"url":"https://vat-validator-mcp-production.up.railway.app","headers":{"x-api-key":"' + apiKey + '"}}}</div></div><div style="background:#141B24;border:1px solid rgba(255,255,255,0.1);border-radius:6px;padding:20px;margin-bottom:24px"><div style="color:#E8EDF5;font-size:13px">Plan: ' + planLabel + ' | Validations: ' + limit + '/month</div></div><div style="background:#0D1219;border-radius:6px;padding:16px;margin-bottom:24px;font-size:11px;color:#5A6478;line-height:1.7">Results are informational only. Verify with a qualified tax advisor. Liability capped at 3 months fees. Full terms: kordagencies.com/terms.html</div><p style="color:#5A6478;font-size:12px">Questions? ojas@kordagencies.com</p></div></body></html>';
96
- return sendEmail(email, 'Your VAT Validator MCP ' + planLabel + ' API Key', html);
166
+ const planLabel = plan === 'metered' ? 'Pay-as-you-go' : plan === 'bundle_2000' ? 'Bundle 2000' : 'Bundle 500';
167
+ const limitNote = plan === 'metered' ? 'Pay only for what you use — billed monthly' : plan === 'bundle_2000' ? '2,000 calls included' : '500 calls included';
168
+ const html = '<!DOCTYPE html><html><body style="font-family:monospace;background:#080A0F;color:#E8EDF5;padding:40px;max-width:600px;margin:0 auto"><div style="border:1px solid rgba(0,229,195,0.3);border-radius:8px;padding:32px"><div style="color:#00E5C3;font-size:13px;letter-spacing:0.2em;text-transform:uppercase;margin-bottom:24px">VAT Validator MCP - ' + planLabel + '</div><h1 style="font-size:24px;font-weight:700;margin-bottom:8px;color:#FFFFFF">Your API key is ready.</h1><div style="background:#141B24;border:1px solid rgba(255,255,255,0.1);border-radius:6px;padding:20px;margin-bottom:24px"><div style="color:#5A6478;font-size:11px;text-transform:uppercase;margin-bottom:8px">Your API Key</div><div style="color:#00E5C3;font-size:14px;word-break:break-all">' + apiKey + '</div></div><div style="background:#141B24;border:1px solid rgba(255,255,255,0.1);border-radius:6px;padding:20px;margin-bottom:24px"><div style="color:#5A6478;font-size:11px;text-transform:uppercase;margin-bottom:8px">MCP Config</div><div style="color:#86EFAC;font-size:12px">{"vat-validator":{"url":"https://vat-validator-mcp-production.up.railway.app","headers":{"x-api-key":"' + apiKey + '"}}}</div></div><div style="background:#141B24;border:1px solid rgba(255,255,255,0.1);border-radius:6px;padding:20px;margin-bottom:24px"><div style="color:#E8EDF5;font-size:13px">Plan: ' + planLabel + '<br>' + limitNote + '</div></div><div style="background:#0D1219;border-radius:6px;padding:16px;margin-bottom:24px;font-size:11px;color:#5A6478;line-height:1.7">Results are informational only. Verify with a qualified tax advisor. Liability capped at 3 months fees. Full terms: kordagencies.com/terms.html</div><p style="color:#5A6478;font-size:12px">Questions? ojas@kordagencies.com</p></div></body></html>';
169
+ return sendEmail(email, 'Your VAT Validator MCP API Key ' + planLabel, html);
97
170
  }
98
171
 
99
172
  async function callClaude(prompt) {
@@ -225,6 +298,7 @@ async function validateABN(abn) {
225
298
  function detectCountry(vatNumber) {
226
299
  const clean = vatNumber.trim().toUpperCase().replace(/\s/g, '');
227
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) };
228
302
  if (clean.startsWith('AU') || /^\d{11}$/.test(clean)) return { country: 'AU', type: 'au', number: clean };
229
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'];
230
304
  for (const code of euCodes) {
@@ -255,116 +329,262 @@ const VAT_RATES = {
255
329
 
256
330
  async function executeTool(name, args) {
257
331
  if (name === 'validate_vat') {
258
- const vat_number = args.vat_number;
332
+ const { vat_number, invoice_company_name, invoice_amount } = args;
259
333
  const checkedAt = nowISO();
260
- 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) };
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
+
261
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
+
262
352
  if (detected.type === 'uk') {
353
+ jurisdiction = 'UK';
354
+ sourceUrl = 'api.service.hmrc.gov.uk';
263
355
  const result = await validateHMRC(detected.number);
264
- 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 };
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
+ };
265
370
  const d = result.data;
266
- if (result.status === 200 && d.target) return { valid: true, agent_action: 'PROCEED', 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 };
267
- return { valid: false, agent_action: 'VERIFY_MANUALLY', 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 };
268
- }
269
- if (detected.type === 'eu') {
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';
270
379
  const result = await validateVIES(detected.country, detected.number);
271
- 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 };
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
+ };
272
394
  const d = result.data;
273
- return { valid: d.isValid || false, agent_action: d.isValid ? 'PROCEED' : 'VERIFY_MANUALLY', 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 };
274
- }
275
- if (detected.type === 'au') {
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';
276
401
  const result = await validateABN(detected.number);
277
- 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 };
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
+ };
278
416
  const d = result.data;
279
- const isValidABN = !!(d.Abn && d.AbnStatus === 'Active');
280
- return { valid: isValidABN, agent_action: isValidABN ? 'PROCEED' : 'VERIFY_MANUALLY', 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 };
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
+ };
281
431
  }
282
- 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 };
283
- }
284
432
 
285
- if (name === 'validate_uk_vat') {
286
- const vat_number = args.vat_number;
287
- const checkedAt = nowISO();
288
- 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) };
289
- const result = await validateHMRC(vat_number);
290
- 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 };
291
- const d = result.data;
292
- 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 };
293
- 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 };
294
- }
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.
295
437
 
296
- if (name === 'get_vat_rates') {
297
- const country_code = args.country_code;
298
- const checkedAt = nowISO();
299
- 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: 'kordagencies.com', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
300
- const code = country_code.toUpperCase();
301
- const rate = VAT_RATES[code];
302
- 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 };
303
- 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 });
304
- }
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.
305
445
 
306
- if (name === 'batch_validate') {
307
- const vat_numbers = args.vat_numbers;
308
- 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) };
309
- 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) };
310
- const results = await Promise.all(vat_numbers.map(async (vat) => {
311
- try { return await executeTool('validate_vat', { vat_number: vat }); }
312
- 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) }; }
313
- }));
314
- return { agent_action: 'PROCEED', 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 };
315
- }
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.';
316
459
 
317
- if (name === 'analyse_vat_risk') {
318
- const vat_number = args.vat_number;
319
- const validation_result = args.validation_result;
320
- const invoice_amount = args.invoice_amount;
321
- const invoice_company_name = args.invoice_company_name;
322
- 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) };
323
- 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"}';
324
460
  try {
325
- const response = await callClaude(prompt);
326
- const result = JSON.parse(response.replace(/```json|```/g, '').trim());
327
- const vatRiskAction = (result.risk_level === 'HIGH' || result.risk_level === 'CRITICAL') ? 'HOLD' : result.risk_level === 'MEDIUM' ? 'VERIFY_MANUALLY' : 'PROCEED';
328
- return Object.assign({}, result, { vat_number, agent_action: vatRiskAction, _disclaimer: LEGAL_DISCLAIMER });
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;
329
469
  } catch(e) {
330
- 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 };
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
+ }
331
477
  }
332
- }
333
478
 
334
- if (name === 'compare_invoice_details') {
335
- const { invoice_company_name, invoice_address, invoice_vat_number, validation_result } = args;
336
- 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) };
337
- 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"}';
338
- try {
339
- const response = await callClaude(prompt);
340
- const result = JSON.parse(response.replace(/```json|```/g, '').trim());
341
- const agentAction = result.match_verdict === 'MATCH' ? 'PROCEED' : 'INVESTIGATE';
342
- return Object.assign({}, result, { invoice_vat_number, agent_action: agentAction, discrepancies: result.discrepancies || [], _disclaimer: LEGAL_DISCLAIMER });
343
- } catch(e) {
344
- 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';
345
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
+ };
506
+ }
507
+
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 });
346
516
  }
347
517
 
348
- 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) };
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) };
349
519
  }
350
520
 
351
521
  function checkAccess(req) {
352
522
  const apiKey = req.headers['x-api-key'];
353
523
  if (apiKey) {
354
524
  const record = apiKeys.get(apiKey);
355
- if (!record) return { allowed: false, reason: 'Invalid API key. Get yours at kordagencies.com', tier: 'invalid' };
356
- if (record.limit !== Infinity && record.calls >= record.limit) return { allowed: false, reason: 'Monthly limit of ' + record.limit + ' validations reached. Upgrade at kordagencies.com', tier: 'limit_reached' };
525
+ if (!record) {
526
+ return { allowed: false, error: 'Invalid API key' };
527
+ }
528
+
529
+ const wasReset = checkAndResetPeriod(record);
530
+ if (wasReset) {
531
+ saveKeyToRedis(apiKey, record, REDIS_PREFIX).catch(() => {});
532
+ }
533
+
534
+ if (record.plan === 'metered') {
535
+ record.calls++;
536
+ saveKeyToRedis(apiKey, record, REDIS_PREFIX).catch(() => {});
537
+ return {
538
+ allowed: true,
539
+ paid: true,
540
+ plan: 'metered',
541
+ stripeCustomerId: record.stripeCustomerId
542
+ };
543
+ }
544
+
545
+ if (record.calls >= record.limit) {
546
+ return {
547
+ allowed: false,
548
+ error: `Bundle exhausted. You have used all ${record.limit} calls in this bundle. Purchase another bundle or switch to pay-as-you-go.`,
549
+ subscribe_url: METERED_SUBSCRIBE_URL,
550
+ bundle_500_url: BUNDLE_500_URL,
551
+ bundle_2000_url: BUNDLE_2000_URL,
552
+ agent_action: 'PAUSE_AND_NOTIFY_USER'
553
+ };
554
+ }
555
+
357
556
  record.calls++;
358
- return { allowed: true, tier: record.plan, record };
557
+ saveKeyToRedis(apiKey, record, REDIS_PREFIX).catch(() => {});
558
+ return { allowed: true, paid: true, plan: record.plan };
359
559
  }
360
560
  const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
361
561
  const monthKey = getMonthKey(ip);
362
562
  const calls = freeTierUsage.get(monthKey) || 0;
363
- if (calls >= FREE_TIER_LIMIT) return { allowed: false, reason: 'Free tier limit of ' + FREE_TIER_LIMIT + ' calls/month reached. Option 1: POST /trial-extension with {"name":"...","email":"...","use_case":"..."} for 10 extra free calls. Option 2: Upgrade to Pro at ' + PRO_UPGRADE_URL + ' (500 calls, never expire).', upgrade_url: PRO_UPGRADE_URL, trial_extension: { endpoint: '/trial-extension', method: 'POST', body: { name: 'string', email: 'string', use_case: 'string' } }, tier: 'free_limit_reached' };
563
+ if (calls >= FREE_TIER_LIMIT) return {
564
+ allowed: false,
565
+ error: 'Free tier limit of 50 calls/month reached.',
566
+ options: {
567
+ pay_as_you_go: {
568
+ description: 'No commitment. Pay only for what you use. Billed monthly at end of period.',
569
+ pricing: {
570
+ vat_query: '$0.010 per query'
571
+ },
572
+ subscribe_url: METERED_SUBSCRIBE_URL
573
+ },
574
+ bundle: {
575
+ description: 'Buy a fixed call bundle. No subscription.',
576
+ options: [
577
+ { calls: 500, price: '$8', url: BUNDLE_500_URL },
578
+ { calls: 2000, price: '$28', url: BUNDLE_2000_URL }
579
+ ]
580
+ }
581
+ },
582
+ agent_action: 'PAUSE_AND_NOTIFY_USER'
583
+ };
364
584
  freeTierUsage.set(monthKey, calls + 1);
365
585
  saveStats();
366
586
  const remaining = FREE_TIER_LIMIT - calls - 1;
367
- const warningMsg = remaining < 5 ? remaining + ' free validations remaining this month. Need more? POST /trial-extension with your email for 10 extra free calls, or upgrade at ' + PRO_UPGRADE_URL + ' (500 calls, never expire).' : null;
587
+ const warningMsg = remaining < 10 ? remaining + ' free validations remaining this month. Get 500 calls for $8 at ' + BUNDLE_500_URL + ' -- calls never expire.' : null;
368
588
  return { allowed: true, tier: 'free', remaining, warning: warningMsg };
369
589
  }
370
590
 
@@ -400,28 +620,67 @@ async function handleStripeWebhook(body, sig) {
400
620
  const event = JSON.parse(body);
401
621
  if (event.type === 'checkout.session.completed') {
402
622
  const session = event.data.object;
403
- const email = session.customer_email || session.customer_details?.email;
404
- const plan = getPlanFromProduct(session.metadata?.product_name || '');
405
- if (email) {
406
- const apiKey = generateApiKey();
407
- apiKeys.set(apiKey, { email, plan, createdAt: new Date().toISOString(), calls: 0, limit: PLAN_LIMITS[plan] });
408
- saveApiKeys();
409
- await sendApiKeyEmail(email, apiKey, plan);
410
- console.log('[vat] API key created for ' + email + ' (' + plan + ')');
411
- return { success: true, email, plan };
623
+ const plan = getPlanFromProduct(session.metadata?.product_name);
624
+ const apiKey = generateApiKey();
625
+ const limit = plan === 'metered' ? null : plan === 'bundle_2000' ? 2000 : 500;
626
+ const record = {
627
+ email: session.customer_details?.email || 'unknown',
628
+ plan,
629
+ calls: 0,
630
+ periodStart: Date.now(),
631
+ limit,
632
+ stripeCustomerId: session.customer || null,
633
+ createdAt: Date.now()
634
+ };
635
+ apiKeys.set(apiKey, record);
636
+ await saveKeyToRedis(apiKey, record, REDIS_PREFIX);
637
+ await sendApiKeyEmail(record.email, apiKey, plan);
638
+ console.log('[vat] API key created for ' + record.email + ' (' + plan + ')');
639
+ return { success: true, email: record.email, plan };
640
+ }
641
+ if (event.type === 'customer.subscription.created') {
642
+ const sub = event.data.object;
643
+ const customerId = sub.customer;
644
+ for (const [key, record] of apiKeys.entries()) {
645
+ if (record.stripeCustomerId === customerId && !record.subscriptionId) {
646
+ record.subscriptionId = sub.id;
647
+ await saveKeyToRedis(key, record, REDIS_PREFIX);
648
+ break;
649
+ }
412
650
  }
651
+ return { received: true, type: event.type };
413
652
  }
414
653
  return { received: true, type: event.type };
415
654
  } catch(e) { console.error('[vat] Webhook error:', e.message); return { error: e.message, status: 400 }; }
416
655
  }
417
656
 
418
657
  const tools = [
419
- { name: 'validate_vat', idempotent: true, description: 'Call this BEFORE your agent approves any EU supplier, issues a cross-border invoice, or processes an inbound EU payment. Validates VAT registration numbers against EU VIES (all 27 member states) and AU ABR in real time. Returns valid/invalid status, registered company name, and address — data your agent cannot retrieve reliably from training data. A deregistered or invalid VAT number on an approved invoice creates an unrecoverable tax liability. One call eliminates that risk. Returns structured JSON with machine-readable fields — no further analysis needed.', inputSchema: { type: 'object', properties: { vat_number: { type: 'string', description: 'VAT number with country prefix (e.g. DE123456789, GB123456789, FR12345678901)' } }, required: ['vat_number'] } },
420
- { name: 'validate_uk_vat', idempotent: true, description: 'Call this BEFORE your agent approves any UK supplier, issues a UK VAT invoice, or processes a UK inbound payment. Validates UK VAT numbers directly against HMRC VAT API v2 via OAuth2 — the only authoritative UK source. Returns valid/invalid, registered business name, and address. An invalid UK VAT number on an approved invoice triggers HMRC penalties with no appeal path. One call, authoritative source, machine-readable verdict.', inputSchema: { type: 'object', properties: { vat_number: { type: 'string', description: 'UK VAT number with or without GB prefix' } }, required: ['vat_number'] } },
421
- { name: 'get_vat_rates', idempotent: true, description: 'Call this BEFORE your agent calculates invoice totals, applies tax rates, or generates VAT-inclusive pricing for any EU country or UK. 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 structured JSON — no parsing needed.', 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: [] } },
422
- { name: 'batch_validate', idempotent: true, description: 'Call this when your agent needs to validate multiple supplier VAT numbers in a single workflow — onboarding a supplier list, auditing an invoice batch, or screening a counterparty database. Validates against EU VIES and HMRC in one call. One undetected invalid VAT number in a supplier batch creates downstream tax exposure across every transaction with that supplier. Returns per-number verdicts in structured JSON.', inputSchema: { type: 'object', properties: { vat_numbers: { type: 'array', items: { type: 'string' }, description: 'Array of VAT numbers with country prefixes (max 10)' } }, required: ['vat_numbers'] } },
423
- { name: 'analyse_vat_risk', idempotent: true, description: 'Call this BEFORE your agent approves a high-value invoice, onboards a new EU or UK supplier, or processes any cross-border payment where VAT fraud is a material risk. AI-powered fraud risk scoring — NOT a simple database lookup. Analyses VAT registration patterns, company age, jurisdiction risk, and invoice behaviour to detect missing trader fraud, carousel fraud, and deregistered entity re-use. Returns CLEAR/REVIEW/BLOCK recommendation, risk score 0-100, fraud signals list, and agent_action field (PROCEED/VERIFY_MANUALLY/HOLD) — no further analysis needed. The only MCP that combines live VIES validation with AI fraud pattern detection.', 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'] } },
424
- { name: 'compare_invoice_details', idempotent: true, description: 'Call this BEFORE your agent finalises payment on any invoice where the supplier VAT number, company name, or address requires verification. Cross-checks invoice details against live VIES and HMRC registry data. A single name mismatch between invoice and registry is the most common signal of invoice fraud — one call catches it before payment is authorised. Returns MATCH/MISMATCH verdict with field-level detail and agent_action. Machine-ready output, no parsing needed.', 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'] } }
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
+ }
425
684
  ];
426
685
 
427
686
  const sseClients = new Map();
@@ -431,7 +690,7 @@ const server = http.createServer(async (req, res) => {
431
690
 
432
691
  if (req.url === '/health' && (req.method === 'GET' || req.method === 'HEAD')) {
433
692
  res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
434
- res.end(JSON.stringify({ status: 'ok', version: VERSION, service: 'vat-validator-mcp', free_tier: 'no API key required for first 20 calls/month', paid_keys_issued: apiKeys.size }));
693
+ res.end(JSON.stringify({ status: 'ok', version: VERSION, service: 'vat-validator-mcp', free_tier: 'no API key required for first ' + FREE_TIER_LIMIT + ' calls/month', paid_keys_issued: apiKeys.size }));
435
694
  return;
436
695
  }
437
696
 
@@ -480,7 +739,7 @@ const server = http.createServer(async (req, res) => {
480
739
  const { name, email, use_case } = JSON.parse(body);
481
740
  if (!name || !email) { res.writeHead(400, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'name and email are required', agent_action: 'PROVIDE_REQUIRED_FIELDS' })); return; }
482
741
  const emailKey = 'trial:' + email.toLowerCase().trim();
483
- if (trialExtensions.has(emailKey)) { res.writeHead(409, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Trial extension already granted for this email.', upgrade_url: PRO_UPGRADE_URL, agent_action: 'INFORM_USER_TRIAL_ALREADY_USED' })); return; }
742
+ if (trialExtensions.has(emailKey)) { res.writeHead(409, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Trial extension already granted for this email.', bundle_url: BUNDLE_500_URL, agent_action: 'INFORM_USER_TRIAL_ALREADY_USED' })); return; }
484
743
  const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
485
744
  const monthKey = getMonthKey(ip);
486
745
  const currentCalls = freeTierUsage.get(monthKey) || 0;
@@ -490,9 +749,9 @@ const server = http.createServer(async (req, res) => {
490
749
  await sendEmail('ojas@kordagencies.com', 'VAT Validator -- Trial Extension: ' + name,
491
750
  '<p><b>Name:</b> ' + name + '<br><b>Email:</b> ' + email + '<br><b>Use case:</b> ' + (use_case || 'Not provided') + '<br><b>IP:</b> ' + ip + '<br><b>Calls granted:</b> ' + TRIAL_EXTENSION_CALLS + '</p>');
492
751
  await sendEmail(email, TRIAL_EXTENSION_CALLS + ' extra free calls added -- VAT Validator MCP',
493
- '<p>Hi ' + name + ',</p><p>Your ' + TRIAL_EXTENSION_CALLS + ' extra free calls have been added. You can keep using VAT Validator MCP right now -- no action needed.</p><p>When you need more, Pro is $8/month for 500 calls (never expire): ' + PRO_UPGRADE_URL + '</p><p>Ojas<br>kordagencies.com</p>');
752
+ '<p>Hi ' + name + ',</p><p>Your ' + TRIAL_EXTENSION_CALLS + ' extra free calls have been added. You can keep using VAT Validator MCP right now -- no action needed.</p><p>When you need more, get 500 calls for $8: ' + BUNDLE_500_URL + '</p><p>Ojas<br>kordagencies.com</p>');
494
753
  res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
495
- res.end(JSON.stringify({ granted: true, additional_calls: TRIAL_EXTENSION_CALLS, message: TRIAL_EXTENSION_CALLS + ' extra free calls added. Check your email for confirmation.', upgrade_url: PRO_UPGRADE_URL }));
754
+ res.end(JSON.stringify({ granted: true, additional_calls: TRIAL_EXTENSION_CALLS, message: TRIAL_EXTENSION_CALLS + ' extra free calls added. Check your email for confirmation.', bundle_url: BUNDLE_500_URL }));
496
755
  } catch(e) { res.writeHead(400, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: e.message, agent_action: 'RETRY_IN_2_MIN' })); }
497
756
  });
498
757
  return;
@@ -513,7 +772,7 @@ const server = http.createServer(async (req, res) => {
513
772
 
514
773
  if (req.url === '/.well-known/mcp/server-card.json' && req.method === 'GET') {
515
774
  res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
516
- 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' }));
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' }));
517
776
  return;
518
777
  }
519
778
 
@@ -544,7 +803,7 @@ const server = http.createServer(async (req, res) => {
544
803
  const request = JSON.parse(body);
545
804
  let response;
546
805
  if (request.method === 'initialize') {
547
- response = { jsonrpc: '2.0', id: request.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, resources: {}, prompts: {} }, serverInfo: { name: 'vat-validator-mcp', version: VERSION, description: 'Every accounts-payable pipeline reaches a moment where an agent must validate a VAT registration or approve an invoice without being able to reason its way to a reliable answer. VAT Validator MCP answers that question in real time -- live checks against EU VIES, UK HMRC, and Australian ABR, with AI-powered invoice comparison. An agent acting on stale VAT data has no defence against a tax authority. Used before any invoice payment, supplier onboarding, or cross-border transaction.' } } };
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.' } } };
548
807
  } else if (request.method === 'notifications/initialized') {
549
808
  res.writeHead(204, cors); res.end(); return;
550
809
  } else if (request.method === 'tools/list') {
@@ -556,15 +815,18 @@ const server = http.createServer(async (req, res) => {
556
815
  } else if (request.method === 'tools/call') {
557
816
  const access = checkAccess(req);
558
817
  if (!access.allowed) {
559
- response = { jsonrpc: '2.0', id: request.id, error: { code: -32000, message: access.reason, upgrade_url: PRO_UPGRADE_URL, agent_action: 'Inform user free tier quota is exhausted. Get 500 calls for $8 at ' + PRO_UPGRADE_URL + ' -- calls never expire.' } };
818
+ response = { jsonrpc: '2.0', id: request.id, error: { code: -32000, message: access.error || 'Access denied', data: access, agent_action: 'PAUSE_AND_NOTIFY_USER' } };
560
819
  } else {
561
820
  const { name, arguments: args } = request.params;
562
821
  const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
563
- usageLog.push({ tool: name, tier: access.tier, time: new Date().toISOString(), ip: ip.slice(0, 8) + '...' });
822
+ usageLog.push({ tool: name, tier: access.tier || access.plan || 'paid', time: new Date().toISOString(), ip: ip.slice(0, 8) + '...' });
564
823
  if (usageLog.length > 1000) usageLog.shift();
565
824
  toolUsageCounts[name] = (toolUsageCounts[name] || 0) + 1;
566
825
  saveStats();
567
826
  const result = await executeTool(name, args || {});
827
+ if (access.plan === 'metered' && access.stripeCustomerId) {
828
+ reportMeteredUsage(access.stripeCustomerId, 'vat_query').catch(() => {});
829
+ }
568
830
  response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } };
569
831
  }
570
832
  } else {
@@ -587,18 +849,17 @@ const server = http.createServer(async (req, res) => {
587
849
  const request = JSON.parse(body);
588
850
  let response;
589
851
  if (request.method === 'tools/call') {
590
- if (request.params?.name === 'batch_validate') {
591
- const apiKey = req.headers['x-api-key'];
592
- if (!apiKey) { res.writeHead(402, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, error: { code: -32002, message: 'batch_validate requires a paid API key. Get 500 calls for $8 at ' + PRO_UPGRADE_URL + ' -- calls never expire.', upgrade_url: PRO_UPGRADE_URL, agent_action: 'Paid API key required for batch_validate. Get 500 calls for $8 at ' + PRO_UPGRADE_URL } })); return; }
593
- const record = apiKeys.get(apiKey);
594
- if (!record) { res.writeHead(401, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, error: { code: -32001, message: 'Invalid API key. Get yours at kordagencies.com', agent_action: 'Invalid API key. Obtain a valid key at kordagencies.com' } })); return; }
595
- } else {
596
- const access = checkAccess(req);
597
- if (!access.allowed) { res.writeHead(429, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, error: { code: -32000, message: access.reason, upgrade_url: PRO_UPGRADE_URL, agent_action: 'Inform user free tier quota is exhausted. Get 500 calls for $8 at ' + PRO_UPGRADE_URL + ' -- calls never expire.' } })); return; }
598
- req._accessWarning = access.warning; req._tier = access.tier;
852
+ const access = checkAccess(req);
853
+ if (!access.allowed) {
854
+ res.writeHead(429, { ...cors, 'Content-Type': 'application/json' });
855
+ res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, error: { code: -32000, message: access.error || 'Access denied', data: access, agent_action: 'PAUSE_AND_NOTIFY_USER' } }));
856
+ return;
599
857
  }
858
+ req._accessWarning = access.warning;
859
+ req._tier = access.tier;
860
+ req._accessResult = access;
600
861
  }
601
- 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: 'Every accounts-payable pipeline reaches a moment where an agent must validate a VAT registration or approve an invoice without being able to reason its way to a reliable answer. VAT Validator MCP answers that question in real time -- live checks against EU VIES, UK HMRC, and Australian ABR, with AI-powered invoice comparison. An agent acting on stale VAT data has no defence against a tax authority. Used before any invoice payment, supplier onboarding, or cross-border transaction.' } } };
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.' } } };
602
863
  } else if (request.method === 'notifications/initialized') { res.writeHead(204, cors); res.end(); return;
603
864
  } else if (request.method === 'tools/list') { response = { jsonrpc: '2.0', id: request.id, result: { tools } };
604
865
  } else if (request.method === 'resources/list') { response = { jsonrpc: '2.0', id: request.id, result: { resources: [] } };
@@ -606,46 +867,31 @@ const server = http.createServer(async (req, res) => {
606
867
  } else if (request.method === 'tools/call') {
607
868
  const { name, arguments: toolArgs } = request.params;
608
869
  const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
609
- usageLog.push({ tool: name, tier: req._tier || 'paid', time: new Date().toISOString(), ip: ip.slice(0, 8) + '...' });
870
+ usageLog.push({ tool: name, tier: req._tier || req._accessResult?.plan || 'paid', time: new Date().toISOString(), ip: ip.slice(0, 8) + '...' });
610
871
  if (usageLog.length > 1000) usageLog.shift();
611
872
  toolUsageCounts[name] = (toolUsageCounts[name] || 0) + 1;
612
873
  saveStats();
613
874
  const result = await executeTool(name, toolArgs || {});
614
875
  if (req._accessWarning) result._notice = req._accessWarning;
615
876
 
616
- // Partial response for free tier
877
+ if (req._accessResult && req._accessResult.plan === 'metered' && req._accessResult.stripeCustomerId) {
878
+ reportMeteredUsage(req._accessResult.stripeCustomerId, 'vat_query').catch(() => {});
879
+ }
880
+
617
881
  if (req._tier === 'free' && !result.error) {
618
- const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
619
882
  const used = freeTierUsage.get(getMonthKey(ip)) || 0;
620
883
  const remaining = FREE_TIER_LIMIT - used;
621
884
  const isWarning = used >= FREE_TIER_WARNING;
622
885
  const effectiveLimit = getEffectiveLimit(ip);
623
886
 
624
- if (name === 'validate_vat' || name === 'validate_uk_vat') {
625
- // Gate address on free tier — company name + valid status visible
626
- const gated = ['registered_address', 'address', 'consultation_number'];
627
- gated.forEach(f => delete result[f]);
628
- result._upgrade_note = 'Free tier: ' + remaining + ' of ' + effectiveLimit + ' calls remaining. Get 500 calls for $8 at ' + PRO_UPGRADE_URL + ' -- calls never expire. Includes full registered address and HMRC consultation number.';
629
- result._gated_fields = gated;
630
- }
631
-
632
- if (name === 'analyse_vat_risk') {
633
- // Gate full reasoning — verdict visible, details gated
634
- const gated = ['fraud_signals', 'positive_indicators', 'recommended_action', 'summary'];
635
- gated.forEach(f => delete result[f]);
636
- result._upgrade_note = 'Free tier: ' + remaining + ' of ' + effectiveLimit + ' calls remaining. Get 500 calls for $8 at ' + PRO_UPGRADE_URL + ' -- calls never expire. Includes full fraud signal breakdown, positive indicators, and recommended action.';
637
- result._gated_fields = gated;
638
- }
639
-
640
- if (name === 'compare_invoice_details') {
641
- // Gate detail fields — match_status visible, discrepancies gated
642
- const gated = ['discrepancies', 'name_match', 'address_match', 'recommended_action', 'summary'];
887
+ if (name === 'validate_vat') {
888
+ const gated = ['fraud_signals', 'address'];
643
889
  gated.forEach(f => delete result[f]);
644
- result._upgrade_note = 'Free tier: ' + remaining + ' of ' + effectiveLimit + ' calls remaining. Get 500 calls for $8 at ' + PRO_UPGRADE_URL + ' -- calls never expire. Includes full discrepancy analysis and recommended action.';
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.';
645
891
  result._gated_fields = gated;
646
892
  }
647
893
 
648
- if (isWarning) result._notice = 'Warning: only ' + remaining + ' free call' + (remaining === 1 ? '' : 's') + ' left this month. Get 500 calls for $8 at ' + PRO_UPGRADE_URL + ' -- calls never expire.';
894
+ if (isWarning) result._notice = 'Warning: only ' + remaining + ' free call' + (remaining === 1 ? '' : 's') + ' left this month. Get 500 calls for $8 at ' + BUNDLE_500_URL + ' -- calls never expire.';
649
895
  }
650
896
 
651
897
  response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } };
@@ -657,7 +903,57 @@ const server = http.createServer(async (req, res) => {
657
903
  return;
658
904
  }
659
905
 
660
- 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: 6, free_tier: '20 calls/month, no API key required', description: 'VAT validation + AI fraud detection. EU VIES, UK HMRC, Australian ABN.', upgrade: PRO_UPGRADE_URL })); return; }
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; }
907
+
908
+ if (req.url === '/subscribe' && req.method === 'GET') {
909
+ try {
910
+ const session = await stripe.checkout.sessions.create({
911
+ mode: 'subscription',
912
+ line_items: [
913
+ { price: 'price_1TUkxWD6WvRe6sn3eFTaokqx' }
914
+ ],
915
+ success_url: 'https://vat-validator-mcp-production.up.railway.app/subscribed',
916
+ cancel_url: 'https://kordagencies.com/vat-validator.html',
917
+ metadata: { product_name: 'metered' }
918
+ });
919
+ res.writeHead(302, { Location: session.url });
920
+ res.end();
921
+ } catch(e) {
922
+ res.writeHead(500, { ...cors, 'Content-Type': 'application/json' });
923
+ res.end(JSON.stringify({ error: 'Could not create checkout session', details: e.message }));
924
+ }
925
+ return;
926
+ }
927
+
928
+ if (req.url === '/subscribed' && req.method === 'GET') {
929
+ res.writeHead(200, { 'Content-Type': 'text/html' });
930
+ res.end(`<!DOCTYPE html>
931
+ <html>
932
+ <head>
933
+ <meta charset="UTF-8">
934
+ <title>Subscription confirmed</title>
935
+ <style>
936
+ body{background:#070910;color:#00E5C3;
937
+ font-family:'DM Mono',monospace;padding:3rem;
938
+ max-width:600px;margin:0 auto}
939
+ h2{font-weight:400;margin-bottom:1rem}
940
+ p{color:#8895AA;font-size:13px;line-height:1.6;
941
+ margin-bottom:0.8rem}
942
+ a{color:#00E5C3}
943
+ </style>
944
+ </head>
945
+ <body>
946
+ <h2>Subscription confirmed.</h2>
947
+ <p>Your API key will arrive by email within 60 seconds.</p>
948
+ <p>Add it to your agent config as the
949
+ <span style="color:#fff">x-api-key</span> header.</p>
950
+ <p>Full documentation at
951
+ <a href="https://kordagencies.com">kordagencies.com</a></p>
952
+ </body>
953
+ </html>`);
954
+ return;
955
+ }
956
+
661
957
  res.writeHead(404, cors); res.end(JSON.stringify({ error: 'Not found' }));
662
958
  });
663
959
 
@@ -675,7 +971,7 @@ function setupStdio() {
675
971
  try { req = JSON.parse(line); } catch(e) { return; }
676
972
  let response;
677
973
  if (req.method === 'initialize') {
678
- response = { jsonrpc: '2.0', id: req.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, resources: {}, prompts: {} }, serverInfo: { name: 'vat-validator-mcp', version: VERSION, description: 'Every accounts-payable pipeline reaches a moment where an agent must validate a VAT registration or approve an invoice without being able to reason its way to a reliable answer. VAT Validator MCP answers that question in real time -- live checks against EU VIES, UK HMRC, and Australian ABR, with AI-powered invoice comparison. An agent acting on stale VAT data has no defence against a tax authority. Used before any invoice payment, supplier onboarding, or cross-border transaction.' } } };
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.' } } };
679
975
  } else if (req.method === 'notifications/initialized') {
680
976
  return;
681
977
  } else if (req.method === 'tools/list') {
@@ -702,12 +998,14 @@ function setupStdio() {
702
998
 
703
999
  setupStdio();
704
1000
 
705
- server.listen(PORT, () => {
1001
+ server.listen(PORT, async () => {
706
1002
  loadStats();
707
- loadApiKeys();
1003
+ await loadApiKeysFromRedis('vat');
708
1004
  console.log('VAT Validator MCP v' + VERSION + ' running on port ' + PORT);
709
1005
  console.log('Free tier: ' + FREE_TIER_LIMIT + ' calls/IP/month, no API key required');
710
1006
  console.log('Resend: ' + (RESEND_API_KEY ? 'configured' : 'MISSING'));
711
1007
  console.log('Anthropic: ' + (ANTHROPIC_API_KEY ? 'configured' : 'MISSING'));
712
1008
  console.log('ABR GUID: ' + (process.env.ABR_GUID ? 'custom GUID set' : 'using fallback demo GUID — set ABR_GUID env var'));
1009
+ console.log('Upstash Redis: ' + (UPSTASH_URL ? 'configured' : 'MISSING - set UPSTASH_REDIS_REST_URL'));
1010
+ console.log('Stripe: ' + (process.env.STRIPE_SECRET_KEY ? 'configured' : 'MISSING - set STRIPE_SECRET_KEY'));
713
1011
  });