vat-validator-mcp 2.0.9 → 2.0.11

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.
@@ -1,401 +0,0 @@
1
- const http = require('http');
2
- const https = require('https');
3
- const crypto = require('crypto');
4
- const fs = require('fs');
5
-
6
- const PERSIST_FILE = '/tmp/vat_stats.json';
7
- const RESEND_API_KEY = process.env.RESEND_API_KEY || '';
8
- const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || '';
9
- const PORT = process.env.PORT || 3000;
10
- const STATS_KEY = process.env.STATS_KEY || 'ojas2026';
11
-
12
- const freeTierUsage = new Map();
13
- const usageLog = [];
14
- const FREE_TIER_LIMIT = 20;
15
- const apiKeys = new Map();
16
- const PLAN_LIMITS = { pro: 5000, enterprise: Infinity };
17
-
18
- function saveStats() {
19
- try {
20
- fs.writeFileSync(PERSIST_FILE, JSON.stringify({
21
- freeTierUsage: Array.from(freeTierUsage.entries()),
22
- usageLog: usageLog.slice(-1000)
23
- }));
24
- } catch(e) { console.error('Stats save error:', e.message); }
25
- }
26
-
27
- function loadStats() {
28
- try {
29
- if (fs.existsSync(PERSIST_FILE)) {
30
- const data = JSON.parse(fs.readFileSync(PERSIST_FILE, 'utf8'));
31
- if (data.freeTierUsage) data.freeTierUsage.forEach(([k, v]) => freeTierUsage.set(k, v));
32
- if (data.usageLog) usageLog.push(...data.usageLog);
33
- console.log('Stats loaded: ' + freeTierUsage.size + ' IPs, ' + usageLog.length + ' calls');
34
- }
35
- } catch(e) { console.error('Stats load error:', e.message); }
36
- }
37
-
38
- function generateApiKey() { return 'vat_' + crypto.randomBytes(24).toString('hex'); }
39
- function getPlanFromProduct(name) {
40
- if (!name) return 'pro';
41
- return name.toLowerCase().includes('enterprise') ? 'enterprise' : 'pro';
42
- }
43
-
44
- async function sendEmail(to, subject, html) {
45
- return new Promise((resolve) => {
46
- const body = JSON.stringify({ from: 'VAT Validator MCP <ojas@kordagencies.com>', to: [to], subject, html });
47
- const req = https.request({
48
- hostname: 'api.resend.com', path: '/emails', method: 'POST',
49
- headers: { 'Authorization': 'Bearer ' + RESEND_API_KEY, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }
50
- }, res => { let d = ''; res.on('data', c => d += c); res.on('end', () => resolve({ status: res.statusCode, body: d })); });
51
- req.on('error', e => resolve({ error: e.message }));
52
- req.write(body); req.end();
53
- });
54
- }
55
-
56
- async function sendApiKeyEmail(email, apiKey, plan) {
57
- const planLabel = plan === 'enterprise' ? 'Enterprise' : 'Pro';
58
- const limit = plan === 'enterprise' ? 'Unlimited' : '5,000';
59
- 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>';
60
- return sendEmail(email, 'Your VAT Validator MCP ' + planLabel + ' API Key', html);
61
- }
62
-
63
- async function callClaude(prompt) {
64
- return new Promise((resolve, reject) => {
65
- const body = JSON.stringify({ model: 'claude-sonnet-4-20250514', max_tokens: 1024, messages: [{ role: 'user', content: prompt }] });
66
- const req = https.request({
67
- hostname: 'api.anthropic.com', path: '/v1/messages', method: 'POST',
68
- headers: { 'x-api-key': ANTHROPIC_API_KEY, 'anthropic-version': '2023-06-01', 'content-type': 'application/json', 'content-length': Buffer.byteLength(body) }
69
- }, res => { let d = ''; res.on('data', c => d += c); res.on('end', () => { try { resolve(JSON.parse(d).content?.[0]?.text || ''); } catch(e) { reject(e); } }); });
70
- req.on('error', reject); req.write(body); req.end();
71
- });
72
- }
73
-
74
- async function validateVIES(countryCode, vatNumber) {
75
- return new Promise((resolve) => {
76
- const req = https.request({
77
- hostname: 'ec.europa.eu',
78
- path: '/taxation_customs/vies/rest-api/ms/' + countryCode + '/vat/' + vatNumber,
79
- method: 'GET',
80
- headers: { 'Accept': 'application/json', 'User-Agent': 'VAT-Validator-MCP/1.0' }
81
- }, res => {
82
- let d = ''; res.on('data', c => d += c);
83
- res.on('end', () => {
84
- try { resolve({ source: 'VIES', data: JSON.parse(d) }); }
85
- catch(e) { resolve({ source: 'VIES', error: 'Parse error' }); }
86
- });
87
- });
88
- req.on('error', e => resolve({ source: 'VIES', error: e.message }));
89
- req.setTimeout(8000, () => { req.destroy(); resolve({ source: 'VIES', error: 'Timeout - VIES unavailable, try again later' }); });
90
- req.end();
91
- });
92
- }
93
-
94
- async function validateHMRC(vatNumber) {
95
- return new Promise((resolve) => {
96
- const clean = vatNumber.replace(/^GB/i, '').replace(/\s/g, '');
97
- const req = https.request({
98
- hostname: 'api.service.hmrc.gov.uk',
99
- path: '/organisations/vat/check-vat-number/lookup/' + clean,
100
- method: 'GET',
101
- headers: { 'Accept': 'application/vnd.hmrc.1.0+json' }
102
- }, res => {
103
- let d = ''; res.on('data', c => d += c);
104
- res.on('end', () => {
105
- try { resolve({ source: 'HMRC', status: res.statusCode, data: JSON.parse(d) }); }
106
- catch(e) { resolve({ source: 'HMRC', error: 'Parse error' }); }
107
- });
108
- });
109
- req.on('error', e => resolve({ source: 'HMRC', error: e.message }));
110
- req.setTimeout(8000, () => { req.destroy(); resolve({ source: 'HMRC', error: 'Timeout' }); });
111
- req.end();
112
- });
113
- }
114
-
115
- async function validateABN(abn) {
116
- return new Promise((resolve) => {
117
- const clean = abn.replace(/\s/g, '');
118
- const req = https.request({
119
- hostname: 'abr.business.gov.au',
120
- path: '/json/?abn=' + clean + '&guid=f7b75e2e-6d6a-4c1c-a8d4-5b2e3c9d8f4a',
121
- method: 'GET',
122
- headers: { 'Accept': 'application/json' }
123
- }, res => {
124
- let d = ''; res.on('data', c => d += c);
125
- res.on('end', () => {
126
- try { resolve({ source: 'ABR', data: JSON.parse(d) }); }
127
- catch(e) { resolve({ source: 'ABR', error: 'Parse error' }); }
128
- });
129
- });
130
- req.on('error', e => resolve({ source: 'ABR', error: e.message }));
131
- req.setTimeout(8000, () => { req.destroy(); resolve({ source: 'ABR', error: 'Timeout' }); });
132
- req.end();
133
- });
134
- }
135
-
136
- function detectCountry(vatNumber) {
137
- const clean = vatNumber.trim().toUpperCase().replace(/\s/g, '');
138
- if (clean.startsWith('GB')) return { country: 'GB', type: 'uk', number: clean.slice(2) };
139
- if (clean.startsWith('AU') || /^\d{11}$/.test(clean)) return { country: 'AU', type: 'au', number: clean };
140
- 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'];
141
- for (const code of euCodes) {
142
- if (clean.startsWith(code)) return { country: code, type: 'eu', number: clean.slice(2) };
143
- }
144
- return { country: null, type: 'unknown', number: clean };
145
- }
146
-
147
- const LEGAL_DISCLAIMER = 'Results sourced directly from official government VAT registries (EU VIES, UK HMRC, Australian ABR). We do not log or store your query content. Results are for informational purposes only and do not constitute legal or tax advice. Operator must independently verify all results with a qualified tax advisor before making compliance decisions. Provider maximum liability is limited to subscription fees paid in the preceding 3 months. Full terms: kordagencies.com/terms.html';
148
-
149
- function nowISO() { return new Date().toISOString(); }
150
-
151
- const VAT_RATES = {
152
- AT:{standard:20,reduced:[10,13],country:'Austria'},BE:{standard:21,reduced:[6,12],country:'Belgium'},
153
- BG:{standard:20,reduced:[9],country:'Bulgaria'},CY:{standard:19,reduced:[5,9],country:'Cyprus'},
154
- CZ:{standard:21,reduced:[12],country:'Czech Republic'},DE:{standard:19,reduced:[7],country:'Germany'},
155
- DK:{standard:25,reduced:[],country:'Denmark'},EE:{standard:22,reduced:[9],country:'Estonia'},
156
- EL:{standard:24,reduced:[6,13],country:'Greece'},ES:{standard:21,reduced:[4,10],country:'Spain'},
157
- FI:{standard:25.5,reduced:[10,14],country:'Finland'},FR:{standard:20,reduced:[5.5,10],country:'France'},
158
- HR:{standard:25,reduced:[5,13],country:'Croatia'},HU:{standard:27,reduced:[5,18],country:'Hungary'},
159
- IE:{standard:23,reduced:[9,13.5],country:'Ireland'},IT:{standard:22,reduced:[4,5,10],country:'Italy'},
160
- LT:{standard:21,reduced:[5,9],country:'Lithuania'},LU:{standard:17,reduced:[3,8,14],country:'Luxembourg'},
161
- LV:{standard:21,reduced:[5,12],country:'Latvia'},MT:{standard:18,reduced:[5,7],country:'Malta'},
162
- NL:{standard:21,reduced:[9],country:'Netherlands'},PL:{standard:23,reduced:[5,8],country:'Poland'},
163
- PT:{standard:23,reduced:[6,13],country:'Portugal'},RO:{standard:19,reduced:[5,9],country:'Romania'},
164
- SE:{standard:25,reduced:[6,12],country:'Sweden'},SI:{standard:22,reduced:[5,9.5],country:'Slovenia'},
165
- SK:{standard:20,reduced:[10],country:'Slovakia'},GB:{standard:20,reduced:[5],country:'United Kingdom'},
166
- AU:{standard:10,reduced:[],country:'Australia'}
167
- };
168
-
169
- async function executeTool(name, args) {
170
- if (name === 'validate_vat') {
171
- const vat_number = args.vat_number;
172
- const checkedAt = nowISO();
173
- if (!vat_number) return { error: 'vat_number is required' };
174
- const detected = detectCountry(vat_number);
175
- if (detected.type === 'uk') {
176
- const result = await validateHMRC(detected.number);
177
- if (result.error) return { valid: null, vat_number, country: 'GB', source: 'HMRC', error: result.error, retry: true, _disclaimer: LEGAL_DISCLAIMER };
178
- const d = result.data;
179
- if (result.status === 200 && d.target) return { valid: true, vat_number, country: 'GB', company_name: d.target.name || null, source: 'HMRC', source_url: 'api.service.hmrc.gov.uk', consultation_number: d.consultationNumber || null, checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
180
- return { valid: false, vat_number, country: 'GB', source: 'HMRC', source_url: 'api.service.hmrc.gov.uk', reason: d.code || 'VAT number not found', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
181
- }
182
- if (detected.type === 'eu') {
183
- const result = await validateVIES(detected.country, detected.number);
184
- if (result.error) return { valid: null, vat_number, country: detected.country, source: 'VIES', source_url: 'ec.europa.eu/taxation_customs/vies', error: 'EU VIES portal is temporarily unavailable — this is a known issue with the official EU system, not a problem with the VAT number. Retry in 30 minutes.', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
185
- const d = result.data;
186
- return { valid: d.isValid || false, vat_number, country: detected.country, company_name: d.traderName || null, address: d.traderAddress || null, source: 'VIES', source_url: 'ec.europa.eu/taxation_customs/vies', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
187
- }
188
- if (detected.type === 'au') {
189
- const result = await validateABN(detected.number);
190
- if (result.error) return { valid: null, vat_number, country: 'AU', source: 'ABR', error: result.error, _disclaimer: LEGAL_DISCLAIMER };
191
- const d = result.data;
192
- return { valid: !!(d.Abn && d.AbnStatus === 'Active'), vat_number, country: 'AU', company_name: d.EntityName || null, abn_status: d.AbnStatus || null, source: 'ABR', source_url: 'abr.business.gov.au', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
193
- }
194
- return { valid: null, vat_number, error: 'Could not detect country. Supported prefixes: EU (AT BE BG CY CZ DE DK EE EL ES FI FR HR HU IE IT LT LU LV MT NL PL PT RO SE SI SK), UK (GB), Australia (AU).', _disclaimer: LEGAL_DISCLAIMER };
195
- }
196
-
197
- if (name === 'validate_uk_vat') {
198
- const vat_number = args.vat_number;
199
- const checkedAt = nowISO();
200
- if (!vat_number) return { error: 'vat_number is required' };
201
- const result = await validateHMRC(vat_number);
202
- if (result.error) return { valid: null, vat_number, source: 'HMRC', source_url: 'api.service.hmrc.gov.uk', error: 'UK HMRC API is temporarily unavailable — this is not a problem with the VAT number. Retry in a few minutes.', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
203
- const d = result.data;
204
- if (result.status === 200 && d.target) return { valid: true, vat_number, company_name: d.target.name || null, registered_address: d.target.address ? Object.values(d.target.address).filter(Boolean).join(', ') : null, consultation_number: d.consultationNumber || null, source: 'HMRC', source_url: 'api.service.hmrc.gov.uk', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
205
- return { valid: false, vat_number, source: 'HMRC', source_url: 'api.service.hmrc.gov.uk', reason: d.code || 'VAT number not found', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
206
- }
207
-
208
- if (name === 'get_vat_rates') {
209
- const country_code = args.country_code;
210
- const checkedAt = nowISO();
211
- if (!country_code) return { rates: VAT_RATES, note: 'VAT rates as of 2026. Verify with official tax authority before use.', source_url: 'kordagencies.com', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
212
- const code = country_code.toUpperCase();
213
- const rate = VAT_RATES[code];
214
- if (!rate) return { error: 'No VAT rate data for: ' + code + '. Supported: ' + Object.keys(VAT_RATES).join(', '), _disclaimer: LEGAL_DISCLAIMER };
215
- return Object.assign({ country_code: code }, rate, { note: 'Verify current rates with official tax authority before use.', source_url: 'kordagencies.com', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER });
216
- }
217
-
218
- if (name === 'batch_validate') {
219
- const vat_numbers = args.vat_numbers;
220
- if (!vat_numbers || !Array.isArray(vat_numbers)) return { error: 'vat_numbers must be an array' };
221
- if (vat_numbers.length > 10) return { error: 'Maximum 10 VAT numbers per batch. Upgrade to Enterprise at kordagencies.com for unlimited batches.' };
222
- const results = await Promise.all(vat_numbers.map(async (vat) => {
223
- try { return await executeTool('validate_vat', { vat_number: vat }); }
224
- catch(e) { return { vat_number: vat, valid: null, error: e.message }; }
225
- }));
226
- return { summary: { total: results.length, valid: results.filter(r => r.valid === true).length, invalid: results.filter(r => r.valid === false).length, error: results.filter(r => r.valid === null).length }, results, _disclaimer: LEGAL_DISCLAIMER };
227
- }
228
-
229
- if (name === 'analyse_vat_risk') {
230
- const vat_number = args.vat_number;
231
- const validation_result = args.validation_result;
232
- const invoice_amount = args.invoice_amount;
233
- const invoice_company_name = args.invoice_company_name;
234
- if (!vat_number || !validation_result) return { error: 'vat_number and validation_result are required' };
235
- const amountStr = invoice_amount ? String(invoice_amount) : 'Not provided';
236
- const nameStr = invoice_company_name || 'Not provided';
237
- const regName = validation_result.company_name || 'Not available';
238
- const prompt = 'You are a B2B fraud detection specialist. Analyse this VAT validation result for fraud signals.\n\n' +
239
- 'VAT Number: ' + vat_number + '\n' +
240
- 'Validation Result: ' + JSON.stringify(validation_result) + '\n' +
241
- 'Invoice Amount: ' + amountStr + '\n' +
242
- 'Invoice Company Name: ' + nameStr + '\n' +
243
- 'Registered Company Name: ' + regName + '\n' +
244
- 'Valid: ' + validation_result.valid + '\n' +
245
- 'Country: ' + validation_result.country + '\n\n' +
246
- 'Analyse 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\n' +
247
- 'Return 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"}';
248
- try {
249
- const response = await callClaude(prompt);
250
- const clean = response.replace(/```json|```/g, '').trim();
251
- const result = JSON.parse(clean);
252
- return Object.assign({}, result, { vat_number, _disclaimer: LEGAL_DISCLAIMER });
253
- } catch(e) {
254
- return { recommendation: 'REVIEW', risk_level: 'MEDIUM', risk_score: 50, vat_number, error: 'AI analysis unavailable - manual review recommended', _disclaimer: LEGAL_DISCLAIMER };
255
- }
256
- }
257
-
258
- if (name === 'compare_invoice_details') {
259
- const invoice_company_name = args.invoice_company_name;
260
- const invoice_address = args.invoice_address;
261
- const invoice_vat_number = args.invoice_vat_number;
262
- const validation_result = args.validation_result;
263
- if (!invoice_company_name || !invoice_vat_number || !validation_result) return { error: 'invoice_company_name, invoice_vat_number, and validation_result are required' };
264
- const regName = validation_result.company_name || 'Not available from registry';
265
- const regAddress = validation_result.address || validation_result.registered_address || 'Not available from registry';
266
- const addressStr = invoice_address || 'Not provided';
267
- const prompt = 'You are an invoice fraud detection specialist. Compare invoice details against official registry records.\n\n' +
268
- 'INVOICE CLAIMS:\n' +
269
- 'Company Name: ' + invoice_company_name + '\n' +
270
- 'Address: ' + addressStr + '\n' +
271
- 'VAT Number: ' + invoice_vat_number + '\n\n' +
272
- 'OFFICIAL REGISTRY RECORDS:\n' +
273
- 'Registered Company Name: ' + regName + '\n' +
274
- 'Registered Address: ' + regAddress + '\n' +
275
- 'VAT Valid: ' + validation_result.valid + '\n' +
276
- 'Country: ' + validation_result.country + '\n\n' +
277
- 'Analyse for: name discrepancies, address discrepancies, signs of invoice fraud or impersonation.\n\n' +
278
- 'Return ONLY valid JSON with no preamble: {"match_status":"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"}';
279
- try {
280
- const response = await callClaude(prompt);
281
- const clean = response.replace(/```json|```/g, '').trim();
282
- const result = JSON.parse(clean);
283
- return Object.assign({}, result, { invoice_vat_number, _disclaimer: LEGAL_DISCLAIMER });
284
- } catch(e) {
285
- return { match_status: 'UNVERIFIABLE', fraud_risk: 'MEDIUM', invoice_vat_number, error: 'AI analysis unavailable - manual review recommended', _disclaimer: LEGAL_DISCLAIMER };
286
- }
287
- }
288
-
289
- return { error: 'Unknown tool: ' + name };
290
- }
291
-
292
- function checkAccess(req) {
293
- const apiKey = req.headers['x-api-key'];
294
- if (apiKey) {
295
- const record = apiKeys.get(apiKey);
296
- if (!record) return { allowed: false, reason: 'Invalid API key. Get yours at kordagencies.com', tier: 'invalid' };
297
- 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' };
298
- record.calls++;
299
- return { allowed: true, tier: record.plan, record };
300
- }
301
- const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
302
- const calls = freeTierUsage.get(ip) || 0;
303
- if (calls >= FREE_TIER_LIMIT) return { allowed: false, reason: 'Free tier limit of ' + FREE_TIER_LIMIT + ' validations/month reached. Upgrade to Pro ($99/month) at kordagencies.com for 5,000 validations/month.', upgrade_url: 'https://kordagencies.com', tier: 'free_limit_reached' };
304
- freeTierUsage.set(ip, calls + 1);
305
- saveStats();
306
- const remaining = FREE_TIER_LIMIT - calls - 1;
307
- return { allowed: true, tier: 'free', remaining, warning: remaining < 5 ? remaining + ' free validations remaining. Upgrade at kordagencies.com' : null };
308
- }
309
-
310
- async function handleStripeWebhook(body) {
311
- try {
312
- const event = JSON.parse(body);
313
- if (event.type === 'checkout.session.completed') {
314
- const session = event.data.object;
315
- const email = session.customer_email || session.customer_details?.email;
316
- const plan = getPlanFromProduct(session.metadata?.product_name || '');
317
- if (email) {
318
- const apiKey = generateApiKey();
319
- apiKeys.set(apiKey, { email, plan, createdAt: new Date().toISOString(), calls: 0, limit: PLAN_LIMITS[plan] });
320
- await sendApiKeyEmail(email, apiKey, plan);
321
- console.log('API key created for ' + email + ' (' + plan + ')');
322
- return { success: true, email, plan };
323
- }
324
- }
325
- return { received: true, type: event.type };
326
- } catch(e) { console.error('Webhook error:', e.message); return { error: e.message }; }
327
- }
328
-
329
- const tools = [
330
- { name: 'validate_vat', description: 'Call this tool any time your agent needs to confirm a business is real and legally registered before interacting with them. Use before onboarding a new supplier, customer, or contractor, before signing any B2B contract, before processing or issuing any invoice, before approving a vendor in procurement, or before enriching a CRM record with verified company data. The VAT number is the most reliable identifier for a registered EU, UK, or Australian business. Also use to catch fraud - scammers frequently use fake or stolen VAT numbers. Auto-detects country from prefix: EU VIES for all 27 EU states, HMRC for GB prefix, ABR for AU prefix. LEGAL NOTICE: Results are informational only, not tax advice. Full terms: kordagencies.com/terms.html. Free tier: first 20 calls/month, no API key needed.', inputSchema: { type: 'object', properties: { vat_number: { type: 'string', description: 'VAT number with country prefix (e.g. DE123456789, GB123456789, FR12345678901)' } }, required: ['vat_number'] } },
331
- { name: 'validate_uk_vat', description: 'Call this tool when your agent is dealing with a UK business and needs to confirm they are genuinely registered with HMRC before onboarding them, paying an invoice, or signing a contract. Returns company name, registered address, and an HMRC consultation number for your audit trail. Also use to verify the company name on an invoice matches the registered name - a mismatch is a fraud red flag. LEGAL NOTICE: Results are informational only, not tax advice. Full terms: kordagencies.com/terms.html. Free tier: first 20 calls/month, no API key needed.', inputSchema: { type: 'object', properties: { vat_number: { type: 'string', description: 'UK VAT number with or without GB prefix' } }, required: ['vat_number'] } },
332
- { name: 'get_vat_rates', description: 'Call this tool when your agent needs to calculate the correct tax amount for a B2B or B2C transaction involving an EU, UK, or Australian business. Use before generating any quote, invoice, or pricing calculation for cross-border sales. Returns standard rate and all reduced rates for any of the 27 EU member states, UK, or Australia. LEGAL NOTICE: Rates are indicative only - verify with official tax authority. Free tier: first 20 calls/month, no API key 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: [] } },
333
- { name: 'batch_validate', description: 'Call this tool when your agent needs to verify multiple businesses at once - for supplier onboarding batches, auditing your entire vendor database, running monthly compliance checks, or cleaning a CRM import. Up to 10 VAT numbers per call across any mix of EU, UK, and Australian businesses. Run this monthly on all active vendors - registrations can lapse. LEGAL NOTICE: Results are informational only, not tax advice. Full terms: kordagencies.com/terms.html. Paid API key required.', inputSchema: { type: 'object', properties: { vat_numbers: { type: 'array', items: { type: 'string' }, description: 'Array of VAT numbers with country prefixes (max 10)' } }, required: ['vat_numbers'] } },
334
- { name: 'analyse_vat_risk', description: 'Call this tool after validate_vat returns a result, when your agent needs an AI-powered fraud risk assessment before proceeding with a transaction. Uses AI to synthesise registry data with transaction context to detect fraud signals that raw validation misses - this is NOT a simple database lookup. Returns a CLEAR/REVIEW/BLOCK recommendation with specific reasons. Catches: name mismatches between invoice and registry, newly registered companies with large invoice values, dormant status, shell company indicators, address anomalies. Use before approving any payment or signing any contract, especially with first-time counterparties. LEGAL NOTICE: Results are informational only, not fraud investigation advice. Full terms: kordagencies.com/terms.html. Free tier: first 20 calls/month, no API key needed.', 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. Helps AI assess risk relative to company size.' }, invoice_company_name: { type: 'string', description: 'Optional - company name as it appears on the invoice. Used to detect name mismatches with registry.' } }, required: ['vat_number', 'validation_result'] } },
335
- { name: 'compare_invoice_details', description: 'Call this tool when your agent has received an invoice and needs to verify the supplier details on the invoice match official government registry records. Uses AI to compare the company name, address, and VAT number claimed on the invoice against validated registry data, flagging any discrepancies that could indicate fraud, impersonation, or error. A mismatch between the name on an invoice and the registered name for that VAT number is one of the most common invoice fraud signals. Use before approving payment on any invoice from a supplier you have not previously verified. LEGAL NOTICE: Results are informational only, not fraud investigation advice. Full terms: kordagencies.com/terms.html. Free tier: first 20 calls/month, no API key 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'] } }
336
- ];
337
-
338
- const server = http.createServer(async (req, res) => {
339
- const cors = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, x-api-key, mcp-session-id, x-stats-key' };
340
- if (req.method === 'OPTIONS') { res.writeHead(200, cors); res.end(); return; }
341
- if (req.url === '/health' && req.method === 'GET') { res.writeHead(200, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'ok', version: '1.3.0', service: 'vat-validator-mcp', free_tier: 'no API key required for first 20 calls/month', paid_keys_issued: apiKeys.size })); return; }
342
- if (req.url === '/stats' && req.method === 'GET') {
343
- if (req.headers['x-stats-key'] !== STATS_KEY) { res.writeHead(401, cors); res.end(JSON.stringify({ error: 'Unauthorized' })); return; }
344
- const totalFreeCalls = Array.from(freeTierUsage.values()).reduce((a, b) => a + b, 0);
345
- const toolCounts = {};
346
- usageLog.forEach(e => { toolCounts[e.tool] = (toolCounts[e.tool] || 0) + 1; });
347
- res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
348
- res.end(JSON.stringify({ free_tier_unique_ips: freeTierUsage.size, free_tier_total_calls: totalFreeCalls, paid_keys_issued: apiKeys.size, tool_usage: toolCounts, recent_calls: usageLog.slice(-20).reverse() }));
349
- return;
350
- }
351
- if (req.url === '/webhook/stripe' && req.method === 'POST') { let body = ''; req.on('data', c => body += c); req.on('end', async () => { const result = await handleStripeWebhook(body); res.writeHead(200, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify(result)); }); return; }
352
- if (req.method === 'POST') {
353
- let body = ''; req.on('data', c => body += c);
354
- req.on('end', async () => {
355
- try {
356
- const request = JSON.parse(body);
357
- let response;
358
- if (request.method !== 'initialize' && request.method !== 'notifications/initialized') {
359
- if (request.method === 'tools/call' && request.params?.name === 'batch_validate') {
360
- const apiKey = req.headers['x-api-key'];
361
- 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 yours at kordagencies.com - Pro $99/month.', upgrade_url: 'https://kordagencies.com' } })); return; }
362
- const record = apiKeys.get(apiKey);
363
- 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' } })); return; }
364
- } else {
365
- const access = checkAccess(req);
366
- 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: 'https://kordagencies.com' } })); return; }
367
- req._accessWarning = access.warning; req._tier = access.tier;
368
- }
369
- }
370
- 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: '1.3.0', description: 'VAT validation + AI fraud detection for AI agents. EU VIES, UK HMRC, Australian ABN. AI-powered risk analysis and invoice verification. Free tier: 20 calls/month.' } } };
371
- } else if (request.method === 'notifications/initialized') { res.writeHead(204, cors); res.end(); return;
372
- } else if (request.method === 'tools/list') { response = { jsonrpc: '2.0', id: request.id, result: { tools } };
373
- } else if (request.method === 'resources/list') { response = { jsonrpc: '2.0', id: request.id, result: { resources: [] } };
374
- } else if (request.method === 'prompts/list') { response = { jsonrpc: '2.0', id: request.id, result: { prompts: [] } };
375
- } else if (request.method === 'tools/call') {
376
- const { name, arguments: toolArgs } = request.params;
377
- const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
378
- usageLog.push({ tool: name, tier: req._tier || 'paid', time: new Date().toISOString(), ip: ip.slice(0, 8) + '...' });
379
- if (usageLog.length > 1000) usageLog.shift();
380
- saveStats();
381
- const result = await executeTool(name, toolArgs || {});
382
- if (req._accessWarning) result._notice = req._accessWarning;
383
- response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } };
384
- } else { response = { jsonrpc: '2.0', id: request.id, error: { code: -32601, message: 'Method not found: ' + request.method } }; }
385
- res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
386
- res.end(JSON.stringify(response));
387
- } catch(e) { res.writeHead(400, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: e.message })); }
388
- });
389
- return;
390
- }
391
- if (req.method === 'GET' && req.url === '/') { res.writeHead(200, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ name: 'vat-validator-mcp', version: '1.3.0', 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: 'https://kordagencies.com' })); return; }
392
- res.writeHead(404, cors); res.end(JSON.stringify({ error: 'Not found' }));
393
- });
394
-
395
- server.listen(PORT, () => {
396
- loadStats();
397
- console.log('VAT Validator MCP v1.3.0 running on port ' + PORT);
398
- console.log('Free tier: ' + FREE_TIER_LIMIT + ' calls/IP/month, no API key required');
399
- console.log('Resend: ' + (RESEND_API_KEY ? 'configured' : 'MISSING'));
400
- console.log('Anthropic: ' + (ANTHROPIC_API_KEY ? 'configured' : 'MISSING'));
401
- });