vat-validator-mcp 1.0.0 → 1.4.2
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/README.md +155 -27
- package/cd +0 -0
- package/del +0 -0
- package/git +0 -0
- package/main +0 -0
- package/node +0 -0
- package/package.json +31 -5
- package/patch_vat_v134.js +84 -0
- package/smithery.yaml +63 -34
- package/src/server.js +335 -267
- package/src/server.js.bak +437 -0
- package/src/vat_server_v130_clean.js +401 -0
package/src/server.js
CHANGED
|
@@ -4,14 +4,15 @@ const crypto = require('crypto');
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
|
|
6
6
|
const PERSIST_FILE = '/tmp/vat_stats.json';
|
|
7
|
-
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || '';
|
|
8
7
|
const RESEND_API_KEY = process.env.RESEND_API_KEY || '';
|
|
8
|
+
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || '';
|
|
9
9
|
const PORT = process.env.PORT || 3000;
|
|
10
10
|
const STATS_KEY = process.env.STATS_KEY || 'ojas2026';
|
|
11
11
|
|
|
12
12
|
const freeTierUsage = new Map();
|
|
13
13
|
const usageLog = [];
|
|
14
14
|
const FREE_TIER_LIMIT = 20;
|
|
15
|
+
const FREE_TIER_WARNING = 16; // warn at 80% usage
|
|
15
16
|
const apiKeys = new Map();
|
|
16
17
|
const PLAN_LIMITS = { pro: 5000, enterprise: Infinity };
|
|
17
18
|
|
|
@@ -30,24 +31,24 @@ function loadStats() {
|
|
|
30
31
|
const data = JSON.parse(fs.readFileSync(PERSIST_FILE, 'utf8'));
|
|
31
32
|
if (data.freeTierUsage) data.freeTierUsage.forEach(([k, v]) => freeTierUsage.set(k, v));
|
|
32
33
|
if (data.usageLog) usageLog.push(...data.usageLog);
|
|
33
|
-
console.log(
|
|
34
|
+
console.log('Stats loaded: ' + freeTierUsage.size + ' IPs, ' + usageLog.length + ' calls');
|
|
34
35
|
}
|
|
35
36
|
} catch(e) { console.error('Stats load error:', e.message); }
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
function generateApiKey() { return 'vat_' + crypto.randomBytes(24).toString('hex'); }
|
|
39
|
-
|
|
40
40
|
function getPlanFromProduct(name) {
|
|
41
41
|
if (!name) return 'pro';
|
|
42
42
|
return name.toLowerCase().includes('enterprise') ? 'enterprise' : 'pro';
|
|
43
43
|
}
|
|
44
|
+
function nowISO() { return new Date().toISOString(); }
|
|
44
45
|
|
|
45
46
|
async function sendEmail(to, subject, html) {
|
|
46
47
|
return new Promise((resolve) => {
|
|
47
48
|
const body = JSON.stringify({ from: 'VAT Validator MCP <ojas@kordagencies.com>', to: [to], subject, html });
|
|
48
49
|
const req = https.request({
|
|
49
50
|
hostname: 'api.resend.com', path: '/emails', method: 'POST',
|
|
50
|
-
headers: { 'Authorization':
|
|
51
|
+
headers: { 'Authorization': 'Bearer ' + RESEND_API_KEY, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }
|
|
51
52
|
}, res => { let d = ''; res.on('data', c => d += c); res.on('end', () => resolve({ status: res.statusCode, body: d })); });
|
|
52
53
|
req.on('error', e => resolve({ error: e.message }));
|
|
53
54
|
req.write(body); req.end();
|
|
@@ -57,39 +58,101 @@ async function sendEmail(to, subject, html) {
|
|
|
57
58
|
async function sendApiKeyEmail(email, apiKey, plan) {
|
|
58
59
|
const planLabel = plan === 'enterprise' ? 'Enterprise' : 'Pro';
|
|
59
60
|
const limit = plan === 'enterprise' ? 'Unlimited' : '5,000';
|
|
60
|
-
const html =
|
|
61
|
-
return sendEmail(email,
|
|
61
|
+
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>';
|
|
62
|
+
return sendEmail(email, 'Your VAT Validator MCP ' + planLabel + ' API Key', html);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function callClaude(prompt) {
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
const body = JSON.stringify({ model: 'claude-sonnet-4-6', max_tokens: 1024, messages: [{ role: 'user', content: prompt }] });
|
|
68
|
+
const req = https.request({
|
|
69
|
+
hostname: 'api.anthropic.com', path: '/v1/messages', method: 'POST',
|
|
70
|
+
headers: { 'x-api-key': ANTHROPIC_API_KEY, 'anthropic-version': '2023-06-01', 'content-type': 'application/json', 'content-length': Buffer.byteLength(body) }
|
|
71
|
+
}, res => { let d = ''; res.on('data', c => d += c); res.on('end', () => { try { resolve(JSON.parse(d).content?.[0]?.text || ''); } catch(e) { reject(e); } }); });
|
|
72
|
+
req.on('error', reject); req.write(body); req.end();
|
|
73
|
+
});
|
|
62
74
|
}
|
|
63
75
|
|
|
64
|
-
// Validate EU VAT number via VIES REST API
|
|
65
76
|
async function validateVIES(countryCode, vatNumber) {
|
|
66
77
|
return new Promise((resolve) => {
|
|
67
|
-
const path = `/taxation_customs/vies/rest-api/ms/${countryCode}/vat/${vatNumber}`;
|
|
68
78
|
const req = https.request({
|
|
69
|
-
hostname: 'ec.europa.eu',
|
|
79
|
+
hostname: 'ec.europa.eu',
|
|
80
|
+
path: '/taxation_customs/vies/rest-api/ms/' + countryCode + '/vat/' + vatNumber,
|
|
81
|
+
method: 'GET',
|
|
70
82
|
headers: { 'Accept': 'application/json', 'User-Agent': 'VAT-Validator-MCP/1.0' }
|
|
71
83
|
}, res => {
|
|
72
84
|
let d = ''; res.on('data', c => d += c);
|
|
73
85
|
res.on('end', () => {
|
|
74
86
|
try { resolve({ source: 'VIES', data: JSON.parse(d) }); }
|
|
75
|
-
catch(e) { resolve({ source: 'VIES', error: 'Parse error'
|
|
87
|
+
catch(e) { resolve({ source: 'VIES', error: 'Parse error' }); }
|
|
76
88
|
});
|
|
77
89
|
});
|
|
78
90
|
req.on('error', e => resolve({ source: 'VIES', error: e.message }));
|
|
79
|
-
req.setTimeout(8000, () => { req.destroy(); resolve({ source: 'VIES', error: 'Timeout
|
|
91
|
+
req.setTimeout(8000, () => { req.destroy(); resolve({ source: 'VIES', error: 'Timeout - VIES unavailable, try again later' }); });
|
|
92
|
+
req.end();
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// HMRC OAuth 2.0 token cache
|
|
97
|
+
let hmrcToken = null;
|
|
98
|
+
let hmrcTokenExpiry = 0;
|
|
99
|
+
|
|
100
|
+
async function getHMRCToken() {
|
|
101
|
+
const now = Date.now();
|
|
102
|
+
// Refresh if missing or within 5 minutes of expiry
|
|
103
|
+
if (hmrcToken && now < hmrcTokenExpiry - 300000) return hmrcToken;
|
|
104
|
+
|
|
105
|
+
const clientId = process.env.HMRC_CLIENT_ID || '';
|
|
106
|
+
const clientSecret = process.env.HMRC_CLIENT_SECRET || '';
|
|
107
|
+
const sandbox = process.env.HMRC_SANDBOX === 'true';
|
|
108
|
+
const hostname = sandbox ? 'test-api.service.hmrc.gov.uk' : 'api.service.hmrc.gov.uk';
|
|
109
|
+
|
|
110
|
+
if (!clientId || !clientSecret) return null;
|
|
111
|
+
|
|
112
|
+
const body = `client_secret=${encodeURIComponent(clientSecret)}&client_id=${encodeURIComponent(clientId)}&grant_type=client_credentials&scope=read%3Avat`;
|
|
113
|
+
|
|
114
|
+
return new Promise((resolve) => {
|
|
115
|
+
const req = https.request({
|
|
116
|
+
hostname,
|
|
117
|
+
path: '/oauth/token',
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': Buffer.byteLength(body) }
|
|
120
|
+
}, res => {
|
|
121
|
+
let d = ''; res.on('data', c => d += c);
|
|
122
|
+
res.on('end', () => {
|
|
123
|
+
try {
|
|
124
|
+
const json = JSON.parse(d);
|
|
125
|
+
if (json.access_token) {
|
|
126
|
+
hmrcToken = json.access_token;
|
|
127
|
+
hmrcTokenExpiry = now + (json.expires_in || 14400) * 1000;
|
|
128
|
+
resolve(hmrcToken);
|
|
129
|
+
} else {
|
|
130
|
+
resolve(null);
|
|
131
|
+
}
|
|
132
|
+
} catch(e) { resolve(null); }
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
req.on('error', () => resolve(null));
|
|
136
|
+
req.setTimeout(8000, () => { req.destroy(); resolve(null); });
|
|
137
|
+
req.write(body);
|
|
80
138
|
req.end();
|
|
81
139
|
});
|
|
82
140
|
}
|
|
83
141
|
|
|
84
|
-
// Validate UK VAT number via HMRC API (no key needed for basic check)
|
|
85
142
|
async function validateHMRC(vatNumber) {
|
|
143
|
+
const clean = vatNumber.replace(/^GB/i, '').replace(/\s/g, '');
|
|
144
|
+
const token = await getHMRCToken();
|
|
145
|
+
if (!token) return { source: 'HMRC', error: 'HMRC credentials not configured' };
|
|
146
|
+
|
|
147
|
+
const sandbox = process.env.HMRC_SANDBOX === 'true';
|
|
148
|
+
const hostname = sandbox ? 'test-api.service.hmrc.gov.uk' : 'api.service.hmrc.gov.uk';
|
|
149
|
+
|
|
86
150
|
return new Promise((resolve) => {
|
|
87
|
-
const clean = vatNumber.replace(/^GB/i, '').replace(/\s/g, '');
|
|
88
151
|
const req = https.request({
|
|
89
|
-
hostname
|
|
90
|
-
path:
|
|
152
|
+
hostname,
|
|
153
|
+
path: '/organisations/vat/check-vat-number/lookup/' + clean,
|
|
91
154
|
method: 'GET',
|
|
92
|
-
headers: { 'Accept': 'application/vnd.hmrc.
|
|
155
|
+
headers: { 'Accept': 'application/vnd.hmrc.2.0+json', 'Authorization': 'Bearer ' + token }
|
|
93
156
|
}, res => {
|
|
94
157
|
let d = ''; res.on('data', c => d += c);
|
|
95
158
|
res.on('end', () => {
|
|
@@ -103,13 +166,14 @@ async function validateHMRC(vatNumber) {
|
|
|
103
166
|
});
|
|
104
167
|
}
|
|
105
168
|
|
|
106
|
-
// Validate Australian ABN via ABR API
|
|
107
169
|
async function validateABN(abn) {
|
|
108
170
|
return new Promise((resolve) => {
|
|
109
171
|
const clean = abn.replace(/\s/g, '');
|
|
110
|
-
const
|
|
172
|
+
const guid = process.env.ABR_GUID || 'f7b75e2e-6d6a-4c1c-a8d4-5b2e3c9d8f4a';
|
|
111
173
|
const req = https.request({
|
|
112
|
-
hostname: 'abr.business.gov.au',
|
|
174
|
+
hostname: 'abr.business.gov.au',
|
|
175
|
+
path: '/json/?abn=' + clean + '&guid=' + guid,
|
|
176
|
+
method: 'GET',
|
|
113
177
|
headers: { 'Accept': 'application/json' }
|
|
114
178
|
}, res => {
|
|
115
179
|
let d = ''; res.on('data', c => d += c);
|
|
@@ -124,12 +188,10 @@ async function validateABN(abn) {
|
|
|
124
188
|
});
|
|
125
189
|
}
|
|
126
190
|
|
|
127
|
-
// Detect country from VAT number prefix and route appropriately
|
|
128
191
|
function detectCountry(vatNumber) {
|
|
129
192
|
const clean = vatNumber.trim().toUpperCase().replace(/\s/g, '');
|
|
130
193
|
if (clean.startsWith('GB')) return { country: 'GB', type: 'uk', number: clean.slice(2) };
|
|
131
194
|
if (clean.startsWith('AU') || /^\d{11}$/.test(clean)) return { country: 'AU', type: 'au', number: clean };
|
|
132
|
-
// EU country codes
|
|
133
195
|
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'];
|
|
134
196
|
for (const code of euCodes) {
|
|
135
197
|
if (clean.startsWith(code)) return { country: code, type: 'eu', number: clean.slice(2) };
|
|
@@ -137,161 +199,113 @@ function detectCountry(vatNumber) {
|
|
|
137
199
|
return { country: null, type: 'unknown', number: clean };
|
|
138
200
|
}
|
|
139
201
|
|
|
140
|
-
const LEGAL_DISCLAIMER = '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.
|
|
202
|
+
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';
|
|
141
203
|
|
|
142
204
|
const VAT_RATES = {
|
|
143
|
-
AT:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
IT: { standard: 22, reduced: [4, 5, 10], country: 'Italy' },
|
|
159
|
-
LT: { standard: 21, reduced: [5, 9], country: 'Lithuania' },
|
|
160
|
-
LU: { standard: 17, reduced: [3, 8, 14], country: 'Luxembourg' },
|
|
161
|
-
LV: { standard: 21, reduced: [5, 12], country: 'Latvia' },
|
|
162
|
-
MT: { standard: 18, reduced: [5, 7], country: 'Malta' },
|
|
163
|
-
NL: { standard: 21, reduced: [9], country: 'Netherlands' },
|
|
164
|
-
PL: { standard: 23, reduced: [5, 8], country: 'Poland' },
|
|
165
|
-
PT: { standard: 23, reduced: [6, 13], country: 'Portugal' },
|
|
166
|
-
RO: { standard: 19, reduced: [5, 9], country: 'Romania' },
|
|
167
|
-
SE: { standard: 25, reduced: [6, 12], country: 'Sweden' },
|
|
168
|
-
SI: { standard: 22, reduced: [5, 9.5], country: 'Slovenia' },
|
|
169
|
-
SK: { standard: 20, reduced: [10], country: 'Slovakia' },
|
|
170
|
-
GB: { standard: 20, reduced: [5], country: 'United Kingdom' },
|
|
171
|
-
AU: { standard: 10, reduced: [], country: 'Australia' }
|
|
205
|
+
AT:{standard:20,reduced:[10,13],country:'Austria'},BE:{standard:21,reduced:[6,12],country:'Belgium'},
|
|
206
|
+
BG:{standard:20,reduced:[9],country:'Bulgaria'},CY:{standard:19,reduced:[5,9],country:'Cyprus'},
|
|
207
|
+
CZ:{standard:21,reduced:[12],country:'Czech Republic'},DE:{standard:19,reduced:[7],country:'Germany'},
|
|
208
|
+
DK:{standard:25,reduced:[],country:'Denmark'},EE:{standard:22,reduced:[9],country:'Estonia'},
|
|
209
|
+
EL:{standard:24,reduced:[6,13],country:'Greece'},ES:{standard:21,reduced:[4,10],country:'Spain'},
|
|
210
|
+
FI:{standard:25.5,reduced:[10,14],country:'Finland'},FR:{standard:20,reduced:[5.5,10],country:'France'},
|
|
211
|
+
HR:{standard:25,reduced:[5,13],country:'Croatia'},HU:{standard:27,reduced:[5,18],country:'Hungary'},
|
|
212
|
+
IE:{standard:23,reduced:[9,13.5],country:'Ireland'},IT:{standard:22,reduced:[4,5,10],country:'Italy'},
|
|
213
|
+
LT:{standard:21,reduced:[5,9],country:'Lithuania'},LU:{standard:17,reduced:[3,8,14],country:'Luxembourg'},
|
|
214
|
+
LV:{standard:21,reduced:[5,12],country:'Latvia'},MT:{standard:18,reduced:[5,7],country:'Malta'},
|
|
215
|
+
NL:{standard:21,reduced:[9],country:'Netherlands'},PL:{standard:23,reduced:[5,8],country:'Poland'},
|
|
216
|
+
PT:{standard:23,reduced:[6,13],country:'Portugal'},RO:{standard:19,reduced:[5,9],country:'Romania'},
|
|
217
|
+
SE:{standard:25,reduced:[6,12],country:'Sweden'},SI:{standard:22,reduced:[5,9.5],country:'Slovenia'},
|
|
218
|
+
SK:{standard:20,reduced:[10],country:'Slovakia'},GB:{standard:20,reduced:[5],country:'United Kingdom'},
|
|
219
|
+
AU:{standard:10,reduced:[],country:'Australia'}
|
|
172
220
|
};
|
|
173
221
|
|
|
174
222
|
async function executeTool(name, args) {
|
|
175
|
-
|
|
176
223
|
if (name === 'validate_vat') {
|
|
177
|
-
const
|
|
224
|
+
const vat_number = args.vat_number;
|
|
225
|
+
const checkedAt = nowISO();
|
|
178
226
|
if (!vat_number) return { error: 'vat_number is required' };
|
|
179
|
-
|
|
180
227
|
const detected = detectCountry(vat_number);
|
|
181
|
-
|
|
182
228
|
if (detected.type === 'uk') {
|
|
183
229
|
const result = await validateHMRC(detected.number);
|
|
184
|
-
if (result.error) return {
|
|
185
|
-
valid: null, vat_number, country: 'GB', source: 'HMRC',
|
|
186
|
-
error: result.error, retry: true,
|
|
187
|
-
_disclaimer: LEGAL_DISCLAIMER
|
|
188
|
-
};
|
|
230
|
+
if (result.error) return { valid: null, vat_number, country: 'GB', source: 'HMRC', error: result.error, retry: true, _disclaimer: LEGAL_DISCLAIMER };
|
|
189
231
|
const d = result.data;
|
|
190
|
-
if (result.status === 200 && d.target) {
|
|
191
|
-
|
|
192
|
-
valid: true, vat_number, country: 'GB',
|
|
193
|
-
company_name: d.target.name || null,
|
|
194
|
-
address: d.target.vatNumber ? `GB${d.target.vatNumber}` : null,
|
|
195
|
-
source: 'HMRC', consultation_number: d.consultationNumber || null,
|
|
196
|
-
_disclaimer: LEGAL_DISCLAIMER
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
return { valid: false, vat_number, country: 'GB', source: 'HMRC', reason: d.code || 'VAT number not found', _disclaimer: LEGAL_DISCLAIMER };
|
|
232
|
+
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 };
|
|
233
|
+
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 };
|
|
200
234
|
}
|
|
201
|
-
|
|
202
235
|
if (detected.type === 'eu') {
|
|
203
236
|
const result = await validateVIES(detected.country, detected.number);
|
|
204
|
-
if (result.error) return {
|
|
205
|
-
valid: null, vat_number, country: detected.country, source: 'VIES',
|
|
206
|
-
error: result.error, retry: result.error.includes('Timeout'),
|
|
207
|
-
note: 'VIES experiences frequent downtime during filing periods. Retry in 30 minutes or use batch_validate during off-peak hours.',
|
|
208
|
-
_disclaimer: LEGAL_DISCLAIMER
|
|
209
|
-
};
|
|
237
|
+
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 };
|
|
210
238
|
const d = result.data;
|
|
211
|
-
return {
|
|
212
|
-
valid: d.isValid || false, vat_number, country: detected.country,
|
|
213
|
-
company_name: d.traderName || null,
|
|
214
|
-
address: d.traderAddress || null,
|
|
215
|
-
source: 'VIES',
|
|
216
|
-
request_date: d.requestDate || null,
|
|
217
|
-
_disclaimer: LEGAL_DISCLAIMER
|
|
218
|
-
};
|
|
239
|
+
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 };
|
|
219
240
|
}
|
|
220
|
-
|
|
221
241
|
if (detected.type === 'au') {
|
|
222
242
|
const result = await validateABN(detected.number);
|
|
223
243
|
if (result.error) return { valid: null, vat_number, country: 'AU', source: 'ABR', error: result.error, _disclaimer: LEGAL_DISCLAIMER };
|
|
224
244
|
const d = result.data;
|
|
225
|
-
|
|
226
|
-
return {
|
|
227
|
-
valid: isValid, vat_number, country: 'AU',
|
|
228
|
-
company_name: d.EntityName || null,
|
|
229
|
-
abn_status: d.AbnStatus || null,
|
|
230
|
-
entity_type: d.EntityTypeName || null,
|
|
231
|
-
source: 'ABR',
|
|
232
|
-
_disclaimer: LEGAL_DISCLAIMER
|
|
233
|
-
};
|
|
245
|
+
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 };
|
|
234
246
|
}
|
|
235
|
-
|
|
236
|
-
return {
|
|
237
|
-
valid: null, vat_number,
|
|
238
|
-
error: 'Could not detect country from VAT number prefix. Supported: 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).',
|
|
239
|
-
_disclaimer: LEGAL_DISCLAIMER
|
|
240
|
-
};
|
|
247
|
+
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 };
|
|
241
248
|
}
|
|
242
249
|
|
|
243
250
|
if (name === 'validate_uk_vat') {
|
|
244
|
-
const
|
|
251
|
+
const vat_number = args.vat_number;
|
|
252
|
+
const checkedAt = nowISO();
|
|
245
253
|
if (!vat_number) return { error: 'vat_number is required' };
|
|
246
254
|
const result = await validateHMRC(vat_number);
|
|
247
|
-
if (result.error) return { valid: null, vat_number, source: 'HMRC', error:
|
|
255
|
+
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 };
|
|
248
256
|
const d = result.data;
|
|
249
|
-
if (result.status === 200 && d.target) {
|
|
250
|
-
|
|
251
|
-
valid: true, vat_number,
|
|
252
|
-
company_name: d.target.name || null,
|
|
253
|
-
registered_address: d.target.address ? Object.values(d.target.address).filter(Boolean).join(', ') : null,
|
|
254
|
-
consultation_number: d.consultationNumber || null,
|
|
255
|
-
source: 'HMRC',
|
|
256
|
-
_disclaimer: LEGAL_DISCLAIMER
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
return { valid: false, vat_number, source: 'HMRC', reason: d.code || 'VAT number not found or not registered', _disclaimer: LEGAL_DISCLAIMER };
|
|
257
|
+
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 };
|
|
258
|
+
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 };
|
|
260
259
|
}
|
|
261
260
|
|
|
262
261
|
if (name === 'get_vat_rates') {
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
rates: VAT_RATES,
|
|
267
|
-
note: 'VAT rates as of 2026. Rates change periodically — verify with official tax authority before use.',
|
|
268
|
-
_disclaimer: LEGAL_DISCLAIMER
|
|
269
|
-
};
|
|
270
|
-
}
|
|
262
|
+
const country_code = args.country_code;
|
|
263
|
+
const checkedAt = nowISO();
|
|
264
|
+
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 };
|
|
271
265
|
const code = country_code.toUpperCase();
|
|
272
266
|
const rate = VAT_RATES[code];
|
|
273
|
-
if (!rate) return { error:
|
|
274
|
-
return { country_code: code,
|
|
267
|
+
if (!rate) return { error: 'No VAT rate data for: ' + code + '. Supported: ' + Object.keys(VAT_RATES).join(', '), _disclaimer: LEGAL_DISCLAIMER };
|
|
268
|
+
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 });
|
|
275
269
|
}
|
|
276
270
|
|
|
277
271
|
if (name === 'batch_validate') {
|
|
278
|
-
const
|
|
272
|
+
const vat_numbers = args.vat_numbers;
|
|
279
273
|
if (!vat_numbers || !Array.isArray(vat_numbers)) return { error: 'vat_numbers must be an array' };
|
|
280
|
-
if (vat_numbers.length > 10) return { error: 'Maximum 10 VAT numbers per batch
|
|
274
|
+
if (vat_numbers.length > 10) return { error: 'Maximum 10 VAT numbers per batch. Upgrade to Enterprise at kordagencies.com for unlimited batches.' };
|
|
281
275
|
const results = await Promise.all(vat_numbers.map(async (vat) => {
|
|
282
|
-
try {
|
|
283
|
-
|
|
284
|
-
} catch(e) {
|
|
285
|
-
return { vat_number: vat, valid: null, error: e.message };
|
|
286
|
-
}
|
|
276
|
+
try { return await executeTool('validate_vat', { vat_number: vat }); }
|
|
277
|
+
catch(e) { return { vat_number: vat, valid: null, error: e.message }; }
|
|
287
278
|
}));
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
279
|
+
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 };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (name === 'analyse_vat_risk') {
|
|
283
|
+
const vat_number = args.vat_number;
|
|
284
|
+
const validation_result = args.validation_result;
|
|
285
|
+
const invoice_amount = args.invoice_amount;
|
|
286
|
+
const invoice_company_name = args.invoice_company_name;
|
|
287
|
+
if (!vat_number || !validation_result) return { error: 'vat_number and validation_result are required' };
|
|
288
|
+
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"}';
|
|
289
|
+
try {
|
|
290
|
+
const response = await callClaude(prompt);
|
|
291
|
+
const result = JSON.parse(response.replace(/```json|```/g, '').trim());
|
|
292
|
+
return Object.assign({}, result, { vat_number, _disclaimer: LEGAL_DISCLAIMER });
|
|
293
|
+
} catch(e) {
|
|
294
|
+
return { recommendation: 'REVIEW', risk_level: 'MEDIUM', risk_score: 50, vat_number, error: 'AI analysis unavailable - manual review recommended', _disclaimer: LEGAL_DISCLAIMER };
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (name === 'compare_invoice_details') {
|
|
299
|
+
const { invoice_company_name, invoice_address, invoice_vat_number, validation_result } = args;
|
|
300
|
+
if (!invoice_company_name || !invoice_vat_number || !validation_result) return { error: 'invoice_company_name, invoice_vat_number, and validation_result are required' };
|
|
301
|
+
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_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"}';
|
|
302
|
+
try {
|
|
303
|
+
const response = await callClaude(prompt);
|
|
304
|
+
const result = JSON.parse(response.replace(/```json|```/g, '').trim());
|
|
305
|
+
return Object.assign({}, result, { invoice_vat_number, _disclaimer: LEGAL_DISCLAIMER });
|
|
306
|
+
} catch(e) {
|
|
307
|
+
return { match_status: 'UNVERIFIABLE', fraud_risk: 'MEDIUM', invoice_vat_number, error: 'AI analysis unavailable - manual review recommended', _disclaimer: LEGAL_DISCLAIMER };
|
|
308
|
+
}
|
|
295
309
|
}
|
|
296
310
|
|
|
297
311
|
return { error: 'Unknown tool: ' + name };
|
|
@@ -302,24 +316,47 @@ function checkAccess(req) {
|
|
|
302
316
|
if (apiKey) {
|
|
303
317
|
const record = apiKeys.get(apiKey);
|
|
304
318
|
if (!record) return { allowed: false, reason: 'Invalid API key. Get yours at kordagencies.com', tier: 'invalid' };
|
|
305
|
-
if (record.limit !== Infinity && record.calls >= record.limit) {
|
|
306
|
-
return { allowed: false, reason: `Monthly limit of ${record.limit} validations reached. Upgrade at kordagencies.com`, tier: 'limit_reached' };
|
|
307
|
-
}
|
|
319
|
+
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' };
|
|
308
320
|
record.calls++;
|
|
309
321
|
return { allowed: true, tier: record.plan, record };
|
|
310
322
|
}
|
|
311
323
|
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
312
324
|
const calls = freeTierUsage.get(ip) || 0;
|
|
313
|
-
if (calls >= FREE_TIER_LIMIT) {
|
|
314
|
-
return { allowed: false, reason: `Free tier limit of ${FREE_TIER_LIMIT} VAT 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' };
|
|
315
|
-
}
|
|
325
|
+
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' };
|
|
316
326
|
freeTierUsage.set(ip, calls + 1);
|
|
317
327
|
saveStats();
|
|
318
328
|
const remaining = FREE_TIER_LIMIT - calls - 1;
|
|
319
|
-
return { allowed: true, tier: 'free', remaining, warning: remaining < 5 ?
|
|
329
|
+
return { allowed: true, tier: 'free', remaining, warning: remaining < 5 ? remaining + ' free validations remaining. Upgrade at kordagencies.com' : null };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function verifyStripeSignature(body, sig, secret) {
|
|
333
|
+
if (!secret) return false;
|
|
334
|
+
if (!sig) return false;
|
|
335
|
+
try {
|
|
336
|
+
const parts = sig.split(',').reduce((acc, part) => {
|
|
337
|
+
const [k, v] = part.split('=');
|
|
338
|
+
acc[k] = v;
|
|
339
|
+
return acc;
|
|
340
|
+
}, {});
|
|
341
|
+
const timestamp = parts['t'];
|
|
342
|
+
const expected = parts['v1'];
|
|
343
|
+
if (!timestamp || !expected) return false;
|
|
344
|
+
const signed = timestamp + '.' + body;
|
|
345
|
+
const computed = crypto.createHmac('sha256', secret).update(signed, 'utf8').digest('hex');
|
|
346
|
+
return crypto.timingSafeEqual(Buffer.from(computed), Buffer.from(expected));
|
|
347
|
+
} catch(e) { return false; }
|
|
320
348
|
}
|
|
321
349
|
|
|
322
|
-
async function handleStripeWebhook(body) {
|
|
350
|
+
async function handleStripeWebhook(body, sig) {
|
|
351
|
+
const secret = process.env.STRIPE_WEBHOOK_SECRET;
|
|
352
|
+
if (!secret) {
|
|
353
|
+
console.error('[vat] STRIPE_WEBHOOK_SECRET not set — rejecting webhook');
|
|
354
|
+
return { error: 'Webhook secret not configured', status: 400 };
|
|
355
|
+
}
|
|
356
|
+
if (!verifyStripeSignature(body, sig, secret)) {
|
|
357
|
+
console.error('[vat] Invalid Stripe signature — rejecting webhook');
|
|
358
|
+
return { error: 'Invalid signature', status: 400 };
|
|
359
|
+
}
|
|
323
360
|
try {
|
|
324
361
|
const event = JSON.parse(body);
|
|
325
362
|
if (event.type === 'checkout.session.completed') {
|
|
@@ -329,78 +366,53 @@ async function handleStripeWebhook(body) {
|
|
|
329
366
|
if (email) {
|
|
330
367
|
const apiKey = generateApiKey();
|
|
331
368
|
apiKeys.set(apiKey, { email, plan, createdAt: new Date().toISOString(), calls: 0, limit: PLAN_LIMITS[plan] });
|
|
332
|
-
|
|
333
|
-
console.log(
|
|
334
|
-
console.log('Email result:', JSON.stringify(emailResult));
|
|
369
|
+
await sendApiKeyEmail(email, apiKey, plan);
|
|
370
|
+
console.log('[vat] API key created for ' + email + ' (' + plan + ')');
|
|
335
371
|
return { success: true, email, plan };
|
|
336
372
|
}
|
|
337
373
|
}
|
|
338
374
|
return { received: true, type: event.type };
|
|
339
|
-
} catch(e) {
|
|
340
|
-
console.error('Webhook error:', e.message);
|
|
341
|
-
return { error: e.message };
|
|
342
|
-
}
|
|
375
|
+
} catch(e) { console.error('[vat] Webhook error:', e.message); return { error: e.message, status: 400 }; }
|
|
343
376
|
}
|
|
344
377
|
|
|
345
378
|
const tools = [
|
|
346
|
-
{
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
vat_number: { type: 'string', description: 'VAT number including country prefix (e.g. DE123456789, GB123456789, FR12345678901, AU51824753556)' }
|
|
353
|
-
},
|
|
354
|
-
required: ['vat_number']
|
|
355
|
-
}
|
|
356
|
-
},
|
|
357
|
-
{
|
|
358
|
-
name: 'validate_uk_vat',
|
|
359
|
-
description: 'Call this tool when your agent needs to validate a UK VAT number specifically against HMRC live records — for example before issuing a UK invoice, verifying a UK supplier, or completing UK tax compliance checks. Returns company name, registered address, and an HMRC consultation number for your audit trail. More detailed than validate_vat for UK numbers. LEGAL NOTICE: Results are informational only. Not a substitute for professional tax advice. Full terms: kordagencies.com/terms.html. Free tier: first 20 calls/month require no API key.',
|
|
360
|
-
inputSchema: {
|
|
361
|
-
type: 'object',
|
|
362
|
-
properties: {
|
|
363
|
-
vat_number: { type: 'string', description: 'UK VAT number — with or without GB prefix (e.g. GB123456789 or 123456789)' }
|
|
364
|
-
},
|
|
365
|
-
required: ['vat_number']
|
|
366
|
-
}
|
|
367
|
-
},
|
|
368
|
-
{
|
|
369
|
-
name: 'get_vat_rates',
|
|
370
|
-
description: 'Call this tool when your agent needs to apply the correct VAT rate to an invoice, quote, or pricing calculation for a specific EU or UK country. Returns standard rate, all reduced rates, and country name. Covers all 27 EU member states plus UK and Australia. Use before calculating invoice totals when selling cross-border. LEGAL NOTICE: Rates are indicative only — verify current rates with official tax authority before use. Full terms: kordagencies.com/terms.html. Free tier: first 20 calls/month require no API key.',
|
|
371
|
-
inputSchema: {
|
|
372
|
-
type: 'object',
|
|
373
|
-
properties: {
|
|
374
|
-
country_code: { type: 'string', description: 'ISO 2-letter country code (e.g. DE, FR, GB, IT). Leave blank to get all countries.' }
|
|
375
|
-
},
|
|
376
|
-
required: []
|
|
377
|
-
}
|
|
378
|
-
},
|
|
379
|
-
{
|
|
380
|
-
name: 'batch_validate',
|
|
381
|
-
description: 'Call this tool when your agent needs to validate multiple VAT numbers in a single call — for example when onboarding a batch of new suppliers, auditing an existing supplier list, or running periodic compliance checks on all active counterparties. Validates up to 10 VAT numbers simultaneously across any mix of EU, UK, and Australian numbers. Returns a summary (valid/invalid/error counts) plus individual results. LEGAL NOTICE: Results are informational only. Not a substitute for professional tax compliance review. Full terms: kordagencies.com/terms.html. Paid API key required.',
|
|
382
|
-
inputSchema: {
|
|
383
|
-
type: 'object',
|
|
384
|
-
properties: {
|
|
385
|
-
vat_numbers: { type: 'array', items: { type: 'string' }, description: 'Array of VAT numbers to validate (max 10). Include country prefix for each (e.g. ["DE123456789", "GB123456789", "FR12345678901"])' }
|
|
386
|
-
},
|
|
387
|
-
required: ['vat_numbers']
|
|
388
|
-
}
|
|
389
|
-
}
|
|
379
|
+
{ 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'] } },
|
|
380
|
+
{ 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'] } },
|
|
381
|
+
{ 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: [] } },
|
|
382
|
+
{ 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'] } },
|
|
383
|
+
{ 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.' }, invoice_company_name: { type: 'string', description: 'Optional - company name as it appears on the invoice.' } }, required: ['vat_number', 'validation_result'] } },
|
|
384
|
+
{ 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'] } }
|
|
390
385
|
];
|
|
391
386
|
|
|
387
|
+
const sseClients = new Map();
|
|
392
388
|
const server = http.createServer(async (req, res) => {
|
|
393
|
-
const cors = {
|
|
394
|
-
'Access-Control-Allow-Origin': '*',
|
|
395
|
-
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
396
|
-
'Access-Control-Allow-Headers': 'Content-Type, x-api-key, mcp-session-id, x-stats-key'
|
|
397
|
-
};
|
|
398
|
-
|
|
389
|
+
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' };
|
|
399
390
|
if (req.method === 'OPTIONS') { res.writeHead(200, cors); res.end(); return; }
|
|
400
391
|
|
|
401
|
-
if (req.url === '/health' && req.method === 'GET') {
|
|
392
|
+
if (req.url === '/health' && (req.method === 'GET' || req.method === 'HEAD')) {
|
|
402
393
|
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
403
|
-
res.end(JSON.stringify({ status: 'ok', version: '1.
|
|
394
|
+
res.end(JSON.stringify({ status: 'ok', version: '1.4.2', service: 'vat-validator-mcp', free_tier: 'no API key required for first 20 calls/month', paid_keys_issued: apiKeys.size }));
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (req.url === '/deps' && req.method === 'GET') {
|
|
399
|
+
const depCheck = (hostname, path, headers) => new Promise((resolve) => {
|
|
400
|
+
const r = https.request({ hostname, path, method: 'GET', headers: Object.assign({ 'User-Agent': 'VAT-Validator-MCP-HealthCheck/1.0' }, headers || {}) }, (res2) => {
|
|
401
|
+
res2.resume();
|
|
402
|
+
resolve({ ok: res2.statusCode < 500, status: res2.statusCode });
|
|
403
|
+
});
|
|
404
|
+
r.on('error', () => resolve({ ok: false, status: 0, error: 'unreachable' }));
|
|
405
|
+
r.setTimeout(5000, () => { r.destroy(); resolve({ ok: false, status: 0, error: 'timeout' }); });
|
|
406
|
+
r.end();
|
|
407
|
+
});
|
|
408
|
+
const [vies, hmrc, abr, ai] = await Promise.all([
|
|
409
|
+
depCheck('ec.europa.eu', '/taxation_customs/vies/rest-api/ms/DE/vat/123456789'),
|
|
410
|
+
getHMRCToken().then(t => t ? { ok: true, status: 200, note: 'OAuth token acquired' } : { ok: false, status: 0, error: 'token fetch failed' }),
|
|
411
|
+
depCheck('abr.business.gov.au', '/json/?abn=12345678901&guid=' + (process.env.ABR_GUID || 'f7b75e2e-6d6a-4c1c-a8d4-5b2e3c9d8f4a')),
|
|
412
|
+
depCheck('api.anthropic.com', '/v1/models', { 'x-api-key': ANTHROPIC_API_KEY, 'anthropic-version': '2023-06-01' })
|
|
413
|
+
]);
|
|
414
|
+
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
415
|
+
res.end(JSON.stringify({ server: 'vat-validator-mcp', checked_at: nowISO(), dependencies: { vies, hmrc, abr, anthropic: ai } }));
|
|
404
416
|
return;
|
|
405
417
|
}
|
|
406
418
|
|
|
@@ -410,109 +422,165 @@ const server = http.createServer(async (req, res) => {
|
|
|
410
422
|
const toolCounts = {};
|
|
411
423
|
usageLog.forEach(e => { toolCounts[e.tool] = (toolCounts[e.tool] || 0) + 1; });
|
|
412
424
|
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
413
|
-
res.end(JSON.stringify({
|
|
414
|
-
free_tier_unique_ips: freeTierUsage.size,
|
|
415
|
-
free_tier_total_calls: totalFreeCalls,
|
|
416
|
-
paid_keys_issued: apiKeys.size,
|
|
417
|
-
tool_usage: toolCounts,
|
|
418
|
-
recent_calls: usageLog.slice(-20).reverse()
|
|
419
|
-
}));
|
|
425
|
+
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() }));
|
|
420
426
|
return;
|
|
421
427
|
}
|
|
422
428
|
|
|
423
429
|
if (req.url === '/webhook/stripe' && req.method === 'POST') {
|
|
424
430
|
let body = ''; req.on('data', c => body += c);
|
|
425
431
|
req.on('end', async () => {
|
|
426
|
-
const
|
|
427
|
-
|
|
432
|
+
const sig = req.headers['stripe-signature'] || '';
|
|
433
|
+
const result = await handleStripeWebhook(body, sig);
|
|
434
|
+
const status = result.status || 200;
|
|
435
|
+
delete result.status;
|
|
436
|
+
res.writeHead(status, { ...cors, 'Content-Type': 'application/json' });
|
|
428
437
|
res.end(JSON.stringify(result));
|
|
429
438
|
});
|
|
430
439
|
return;
|
|
431
440
|
}
|
|
432
441
|
|
|
433
|
-
|
|
442
|
+
// SSE Transport for n8n MCP Client Tool node
|
|
443
|
+
if (req.url === '/sse' && req.method === 'GET') {
|
|
444
|
+
const sessionId = Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
445
|
+
res.writeHead(200, {
|
|
446
|
+
...cors,
|
|
447
|
+
'Content-Type': 'text/event-stream',
|
|
448
|
+
'Cache-Control': 'no-cache',
|
|
449
|
+
'Connection': 'keep-alive',
|
|
450
|
+
'X-Accel-Buffering': 'no'
|
|
451
|
+
});
|
|
452
|
+
res.write('event: endpoint\n');
|
|
453
|
+
res.write('data: /messages?sessionId=' + sessionId + '\n\n');
|
|
454
|
+
sseClients.set(sessionId, res);
|
|
455
|
+
req.on('close', () => sseClients.delete(sessionId));
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (req.url.startsWith('/messages') && req.method === 'POST') {
|
|
460
|
+
const sessionId = new URL(req.url, 'http://localhost').searchParams.get('sessionId');
|
|
461
|
+
const sseRes = sseClients.get(sessionId);
|
|
462
|
+
if (!sseRes) { res.writeHead(400, cors); res.end(JSON.stringify({ error: 'Unknown sessionId' })); return; }
|
|
434
463
|
let body = ''; req.on('data', c => body += c);
|
|
435
464
|
req.on('end', async () => {
|
|
436
465
|
try {
|
|
437
466
|
const request = JSON.parse(body);
|
|
438
467
|
let response;
|
|
468
|
+
if (request.method === 'initialize') {
|
|
469
|
+
response = { jsonrpc: '2.0', id: request.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'vat-validator-mcp', version: '1.4.2' } } };
|
|
470
|
+
} else if (request.method === 'notifications/initialized') {
|
|
471
|
+
res.writeHead(204, cors); res.end(); return;
|
|
472
|
+
} else if (request.method === 'tools/list') {
|
|
473
|
+
response = { jsonrpc: '2.0', id: request.id, result: { tools } };
|
|
474
|
+
} else if (request.method === 'tools/call') {
|
|
475
|
+
const access = checkAccess(req);
|
|
476
|
+
if (!access.allowed) {
|
|
477
|
+
response = { jsonrpc: '2.0', id: request.id, error: { code: -32000, message: access.reason, upgrade_url: 'https://kordagencies.com' } };
|
|
478
|
+
} else {
|
|
479
|
+
const { name, arguments: args } = request.params;
|
|
480
|
+
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
481
|
+
usageLog.push({ tool: name, tier: access.tier, time: new Date().toISOString(), ip: ip.slice(0, 8) + '...' });
|
|
482
|
+
if (usageLog.length > 1000) usageLog.shift();
|
|
483
|
+
saveStats();
|
|
484
|
+
const result = await executeTool(name, args || {});
|
|
485
|
+
response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } };
|
|
486
|
+
}
|
|
487
|
+
} else {
|
|
488
|
+
response = { jsonrpc: '2.0', id: request.id, error: { code: -32601, message: 'Method not found: ' + request.method } };
|
|
489
|
+
}
|
|
490
|
+
sseRes.write('event: message\n');
|
|
491
|
+
sseRes.write('data: ' + JSON.stringify(response) + '\n\n');
|
|
492
|
+
res.writeHead(202, cors); res.end();
|
|
493
|
+
} catch(e) {
|
|
494
|
+
res.writeHead(400, cors); res.end(JSON.stringify({ error: e.message }));
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
439
499
|
|
|
500
|
+
if (req.method === 'POST') {
|
|
501
|
+
let body = ''; req.on('data', c => body += c);
|
|
502
|
+
req.on('end', async () => {
|
|
503
|
+
try {
|
|
504
|
+
const request = JSON.parse(body);
|
|
505
|
+
let response;
|
|
440
506
|
if (request.method !== 'initialize' && request.method !== 'notifications/initialized') {
|
|
441
507
|
if (request.method === 'tools/call' && request.params?.name === 'batch_validate') {
|
|
442
|
-
// batch_validate requires paid key
|
|
443
508
|
const apiKey = req.headers['x-api-key'];
|
|
444
|
-
if (!apiKey) {
|
|
445
|
-
res.writeHead(402, { ...cors, 'Content-Type': 'application/json' });
|
|
446
|
-
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 for 5,000 validations.', upgrade_url: 'https://kordagencies.com' } }));
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
509
|
+
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; }
|
|
449
510
|
const record = apiKeys.get(apiKey);
|
|
450
|
-
if (!record) {
|
|
451
|
-
res.writeHead(401, { ...cors, 'Content-Type': 'application/json' });
|
|
452
|
-
res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, error: { code: -32001, message: 'Invalid API key. Get yours at kordagencies.com' } }));
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
511
|
+
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; }
|
|
455
512
|
} else {
|
|
456
513
|
const access = checkAccess(req);
|
|
457
|
-
if (!access.allowed) {
|
|
458
|
-
|
|
459
|
-
res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, error: { code: -32000, message: access.reason, upgrade_url: 'https://kordagencies.com' } }));
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
req._accessWarning = access.warning;
|
|
463
|
-
req._tier = access.tier;
|
|
514
|
+
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; }
|
|
515
|
+
req._accessWarning = access.warning; req._tier = access.tier;
|
|
464
516
|
}
|
|
465
517
|
}
|
|
466
|
-
|
|
467
|
-
if (request.method === '
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
serverInfo: { name: 'vat-validator-mcp', version: '1.0.0', description: 'VAT number validation for AI agents. Validates EU VIES, UK HMRC, and Australian ABN in one call. Required for EU ViDA e-invoicing compliance. Free tier: 20 validations/month.' }
|
|
472
|
-
}};
|
|
473
|
-
} else if (request.method === 'notifications/initialized') {
|
|
474
|
-
res.writeHead(204, cors); res.end(); return;
|
|
475
|
-
} else if (request.method === 'tools/list') {
|
|
476
|
-
response = { jsonrpc: '2.0', id: request.id, result: { tools } };
|
|
477
|
-
} else if (request.method === 'resources/list') {
|
|
478
|
-
response = { jsonrpc: '2.0', id: request.id, result: { resources: [] } };
|
|
479
|
-
} else if (request.method === 'prompts/list') {
|
|
480
|
-
response = { jsonrpc: '2.0', id: request.id, result: { prompts: [] } };
|
|
518
|
+
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.4.2', 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.' } } };
|
|
519
|
+
} else if (request.method === 'notifications/initialized') { res.writeHead(204, cors); res.end(); return;
|
|
520
|
+
} else if (request.method === 'tools/list') { response = { jsonrpc: '2.0', id: request.id, result: { tools } };
|
|
521
|
+
} else if (request.method === 'resources/list') { response = { jsonrpc: '2.0', id: request.id, result: { resources: [] } };
|
|
522
|
+
} else if (request.method === 'prompts/list') { response = { jsonrpc: '2.0', id: request.id, result: { prompts: [] } };
|
|
481
523
|
} else if (request.method === 'tools/call') {
|
|
482
|
-
const { name, arguments:
|
|
524
|
+
const { name, arguments: toolArgs } = request.params;
|
|
483
525
|
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
484
526
|
usageLog.push({ tool: name, tier: req._tier || 'paid', time: new Date().toISOString(), ip: ip.slice(0, 8) + '...' });
|
|
485
527
|
if (usageLog.length > 1000) usageLog.shift();
|
|
486
528
|
saveStats();
|
|
487
|
-
const result = await executeTool(name,
|
|
529
|
+
const result = await executeTool(name, toolArgs || {});
|
|
488
530
|
if (req._accessWarning) result._notice = req._accessWarning;
|
|
489
|
-
response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } };
|
|
490
|
-
} else {
|
|
491
|
-
response = { jsonrpc: '2.0', id: request.id, error: { code: -32601, message: 'Method not found: ' + request.method } };
|
|
492
|
-
}
|
|
493
531
|
|
|
532
|
+
// Partial response for free tier
|
|
533
|
+
if (req._tier === 'free' && !result.error) {
|
|
534
|
+
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
535
|
+
const used = freeTierUsage.get(ip) || 0;
|
|
536
|
+
const remaining = FREE_TIER_LIMIT - used;
|
|
537
|
+
const isWarning = used >= FREE_TIER_WARNING;
|
|
538
|
+
|
|
539
|
+
if (name === 'validate_vat' || name === 'validate_uk_vat') {
|
|
540
|
+
// Gate address on free tier — company name + valid status visible
|
|
541
|
+
const gated = ['registered_address', 'address', 'consultation_number'];
|
|
542
|
+
gated.forEach(f => delete result[f]);
|
|
543
|
+
result._upgrade_note = 'Free tier: ' + remaining + ' of ' + FREE_TIER_LIMIT + ' calls remaining. Upgrade to Pro ($39/month) at kordagencies.com for full registered address and HMRC consultation number.';
|
|
544
|
+
result._gated_fields = gated;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (name === 'analyse_vat_risk') {
|
|
548
|
+
// Gate full reasoning — verdict visible, details gated
|
|
549
|
+
const gated = ['fraud_signals', 'positive_indicators', 'recommended_action', 'summary'];
|
|
550
|
+
gated.forEach(f => delete result[f]);
|
|
551
|
+
result._upgrade_note = 'Free tier: ' + remaining + ' of ' + FREE_TIER_LIMIT + ' calls remaining. Upgrade to Pro ($39/month) at kordagencies.com for full fraud signal breakdown, positive indicators, and recommended action.';
|
|
552
|
+
result._gated_fields = gated;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (name === 'compare_invoice_details') {
|
|
556
|
+
// Gate detail fields — match_status visible, discrepancies gated
|
|
557
|
+
const gated = ['discrepancies', 'name_match', 'address_match', 'recommended_action', 'summary'];
|
|
558
|
+
gated.forEach(f => delete result[f]);
|
|
559
|
+
result._upgrade_note = 'Free tier: ' + remaining + ' of ' + FREE_TIER_LIMIT + ' calls remaining. Upgrade to Pro ($39/month) at kordagencies.com for full discrepancy analysis and recommended action.';
|
|
560
|
+
result._gated_fields = gated;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (isWarning) result._notice = 'Warning: only ' + remaining + ' free call' + (remaining === 1 ? '' : 's') + ' left this month. Upgrade to Pro at kordagencies.com to avoid interruption.';
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } };
|
|
567
|
+
} else { response = { jsonrpc: '2.0', id: request.id, error: { code: -32601, message: 'Method not found: ' + request.method } }; }
|
|
494
568
|
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
495
569
|
res.end(JSON.stringify(response));
|
|
496
|
-
} catch(e) {
|
|
497
|
-
res.writeHead(400, { ...cors, 'Content-Type': 'application/json' });
|
|
498
|
-
res.end(JSON.stringify({ error: e.message }));
|
|
499
|
-
}
|
|
570
|
+
} catch(e) { res.writeHead(400, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: e.message })); }
|
|
500
571
|
});
|
|
501
572
|
return;
|
|
502
573
|
}
|
|
503
574
|
|
|
504
|
-
if (req.method === 'GET' && req.url === '/') {
|
|
505
|
-
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
506
|
-
res.end(JSON.stringify({ name: 'vat-validator-mcp', version: '1.0.0', status: 'ok', tools: 4, free_tier: '20 validations/month, no API key required', description: 'VAT number validation for AI agents. EU VIES, UK HMRC, Australian ABN. Required for EU ViDA e-invoicing compliance.', upgrade: 'https://kordagencies.com' }));
|
|
507
|
-
return;
|
|
508
|
-
}
|
|
509
|
-
|
|
575
|
+
if (req.method === 'GET' && req.url === '/') { res.writeHead(200, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ name: 'vat-validator-mcp', version: '1.4.2', 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; }
|
|
510
576
|
res.writeHead(404, cors); res.end(JSON.stringify({ error: 'Not found' }));
|
|
511
577
|
});
|
|
512
578
|
|
|
513
579
|
server.listen(PORT, () => {
|
|
514
580
|
loadStats();
|
|
515
|
-
console.log(
|
|
516
|
-
console.log(
|
|
517
|
-
console.log(
|
|
581
|
+
console.log('VAT Validator MCP v1.4.2 running on port ' + PORT);
|
|
582
|
+
console.log('Free tier: ' + FREE_TIER_LIMIT + ' calls/IP/month, no API key required');
|
|
583
|
+
console.log('Resend: ' + (RESEND_API_KEY ? 'configured' : 'MISSING'));
|
|
584
|
+
console.log('Anthropic: ' + (ANTHROPIC_API_KEY ? 'configured' : 'MISSING'));
|
|
585
|
+
console.log('ABR GUID: ' + (process.env.ABR_GUID ? 'custom GUID set' : 'using fallback demo GUID — set ABR_GUID env var'));
|
|
518
586
|
});
|