vat-validator-mcp 1.4.2 → 1.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/server.json +19 -10
- package/src/server.js +78 -26
- package/vat_server_v2_fixed.js +586 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vat-validator-mcp",
|
|
3
3
|
"mcpName": "io.github.OjasKord/vat-validator-mcp",
|
|
4
|
-
"version": "1.4.
|
|
4
|
+
"version": "1.4.3",
|
|
5
5
|
"description": "VAT number validation for AI agents. EU VIES, UK HMRC, Australian ABN in one call.",
|
|
6
6
|
"main": "src/server.js",
|
|
7
7
|
"scripts": {
|
package/server.json
CHANGED
|
@@ -3,15 +3,24 @@
|
|
|
3
3
|
"name": "io.github.OjasKord/vat-validator-mcp",
|
|
4
4
|
"title": "VAT Validator MCP",
|
|
5
5
|
"description": "Validate EU, UK, AU VAT numbers for AI agents. EU ViDA e-invoicing compliance.",
|
|
6
|
-
"version": "1.
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
6
|
+
"version": "1.4.3",
|
|
7
|
+
"websiteUrl": "https://kordagencies.com",
|
|
8
|
+
"repository": {
|
|
9
|
+
"url": "https://github.com/OjasKord/vat-validator-mcp",
|
|
10
|
+
"source": "github"
|
|
11
|
+
},
|
|
12
|
+
"packages": [
|
|
13
|
+
{
|
|
14
|
+
"registryType": "npm",
|
|
15
|
+
"identifier": "vat-validator-mcp",
|
|
16
|
+
"version": "1.4.3",
|
|
17
|
+
"transport": { "type": "stdio" },
|
|
18
|
+
"environmentVariables": [
|
|
19
|
+
{ "name": "ANTHROPIC_API_KEY", "description": "Anthropic API key for AI-powered fraud risk analysis", "isRequired": true, "isSecret": true },
|
|
20
|
+
{ "name": "HMRC_CLIENT_ID", "description": "HMRC Developer Hub client ID for UK VAT validation", "isRequired": false, "isSecret": true },
|
|
21
|
+
{ "name": "HMRC_CLIENT_SECRET", "description": "HMRC Developer Hub client secret for UK VAT validation", "isRequired": false, "isSecret": true }
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
],
|
|
16
25
|
"remotes": [{ "type": "streamable-http", "url": "https://vat-validator-mcp-production.up.railway.app" }]
|
|
17
26
|
}
|
package/src/server.js
CHANGED
|
@@ -4,6 +4,7 @@ const crypto = require('crypto');
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
|
|
6
6
|
const PERSIST_FILE = '/tmp/vat_stats.json';
|
|
7
|
+
const VERSION = '1.4.3';
|
|
7
8
|
const RESEND_API_KEY = process.env.RESEND_API_KEY || '';
|
|
8
9
|
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || '';
|
|
9
10
|
const PORT = process.env.PORT || 3000;
|
|
@@ -223,34 +224,34 @@ async function executeTool(name, args) {
|
|
|
223
224
|
if (name === 'validate_vat') {
|
|
224
225
|
const vat_number = args.vat_number;
|
|
225
226
|
const checkedAt = nowISO();
|
|
226
|
-
if (!vat_number) return { error: 'vat_number is required' };
|
|
227
|
+
if (!vat_number) return { error: 'vat_number is required', agent_action: 'PROVIDE_REQUIRED_FIELD' };
|
|
227
228
|
const detected = detectCountry(vat_number);
|
|
228
229
|
if (detected.type === 'uk') {
|
|
229
230
|
const result = await validateHMRC(detected.number);
|
|
230
|
-
if (result.error) return { valid: null, vat_number, country: 'GB', source: 'HMRC', error: result.error, retry: true, _disclaimer: LEGAL_DISCLAIMER };
|
|
231
|
+
if (result.error) return { valid: null, vat_number, country: 'GB', source: 'HMRC', error: result.error, agent_action: 'RETRY_IN_2_MIN', retry: true, _disclaimer: LEGAL_DISCLAIMER };
|
|
231
232
|
const d = result.data;
|
|
232
233
|
if (result.status === 200 && d.target) return { valid: true, vat_number, country: 'GB', company_name: d.target.name || null, source: 'HMRC', source_url: 'api.service.hmrc.gov.uk', consultation_number: d.consultationNumber || null, checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
233
234
|
return { valid: false, vat_number, country: 'GB', source: 'HMRC', source_url: 'api.service.hmrc.gov.uk', reason: d.code || 'VAT number not found', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
234
235
|
}
|
|
235
236
|
if (detected.type === 'eu') {
|
|
236
237
|
const result = await validateVIES(detected.country, detected.number);
|
|
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 };
|
|
238
|
+
if (result.error) return { valid: null, vat_number, agent_action: 'RETRY_IN_30_MIN', country: detected.country, source: 'VIES', source_url: 'ec.europa.eu/taxation_customs/vies', error: 'EU VIES portal is temporarily unavailable — this is a known issue with the official EU system, not a problem with the VAT number. Retry in 30 minutes.', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
238
239
|
const d = result.data;
|
|
239
240
|
return { valid: d.isValid || false, vat_number, country: detected.country, company_name: d.traderName || null, address: d.traderAddress || null, source: 'VIES', source_url: 'ec.europa.eu/taxation_customs/vies', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
240
241
|
}
|
|
241
242
|
if (detected.type === 'au') {
|
|
242
243
|
const result = await validateABN(detected.number);
|
|
243
|
-
if (result.error) return { valid: null, vat_number, country: 'AU', source: 'ABR', error: result.error, _disclaimer: LEGAL_DISCLAIMER };
|
|
244
|
+
if (result.error) return { valid: null, vat_number, country: 'AU', source: 'ABR', error: result.error, agent_action: 'RETRY_IN_2_MIN', _disclaimer: LEGAL_DISCLAIMER };
|
|
244
245
|
const d = result.data;
|
|
245
246
|
return { valid: !!(d.Abn && d.AbnStatus === 'Active'), vat_number, country: 'AU', company_name: d.EntityName || null, abn_status: d.AbnStatus || null, source: 'ABR', source_url: 'abr.business.gov.au', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
246
247
|
}
|
|
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 };
|
|
248
|
+
return { valid: null, vat_number, agent_action: 'PROVIDE_COUNTRY_PREFIX', error: 'Could not detect country. Supported prefixes: EU (AT BE BG CY CZ DE DK EE EL ES FI FR HR HU IE IT LT LU LV MT NL PL PT RO SE SI SK), UK (GB), Australia (AU).', _disclaimer: LEGAL_DISCLAIMER };
|
|
248
249
|
}
|
|
249
250
|
|
|
250
251
|
if (name === 'validate_uk_vat') {
|
|
251
252
|
const vat_number = args.vat_number;
|
|
252
253
|
const checkedAt = nowISO();
|
|
253
|
-
if (!vat_number) return { error: 'vat_number is required' };
|
|
254
|
+
if (!vat_number) return { error: 'vat_number is required', agent_action: 'PROVIDE_REQUIRED_FIELD' };
|
|
254
255
|
const result = await validateHMRC(vat_number);
|
|
255
256
|
if (result.error) return { valid: null, vat_number, source: 'HMRC', source_url: 'api.service.hmrc.gov.uk', error: 'UK HMRC API is temporarily unavailable — this is not a problem with the VAT number. Retry in a few minutes.', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
256
257
|
const d = result.data;
|
|
@@ -270,8 +271,8 @@ async function executeTool(name, args) {
|
|
|
270
271
|
|
|
271
272
|
if (name === 'batch_validate') {
|
|
272
273
|
const vat_numbers = args.vat_numbers;
|
|
273
|
-
if (!vat_numbers || !Array.isArray(vat_numbers)) return { error: 'vat_numbers must be an array' };
|
|
274
|
-
if (vat_numbers.length > 10) return { error: 'Maximum 10 VAT numbers per batch. Upgrade to Enterprise at kordagencies.com for unlimited batches.' };
|
|
274
|
+
if (!vat_numbers || !Array.isArray(vat_numbers)) return { error: 'vat_numbers must be an array', agent_action: 'PROVIDE_REQUIRED_FIELD' };
|
|
275
|
+
if (vat_numbers.length > 10) return { error: 'Maximum 10 VAT numbers per batch. Upgrade to Enterprise at kordagencies.com for unlimited batches.', agent_action: 'Reduce batch to 10 or fewer, or upgrade to Enterprise at kordagencies.com' };
|
|
275
276
|
const results = await Promise.all(vat_numbers.map(async (vat) => {
|
|
276
277
|
try { return await executeTool('validate_vat', { vat_number: vat }); }
|
|
277
278
|
catch(e) { return { vat_number: vat, valid: null, error: e.message }; }
|
|
@@ -284,7 +285,7 @@ async function executeTool(name, args) {
|
|
|
284
285
|
const validation_result = args.validation_result;
|
|
285
286
|
const invoice_amount = args.invoice_amount;
|
|
286
287
|
const invoice_company_name = args.invoice_company_name;
|
|
287
|
-
if (!vat_number || !validation_result) return { error: 'vat_number and validation_result are required' };
|
|
288
|
+
if (!vat_number || !validation_result) return { error: 'vat_number and validation_result are required', agent_action: 'PROVIDE_REQUIRED_FIELD' };
|
|
288
289
|
const prompt = 'You are a B2B fraud detection specialist. Analyse this VAT validation result for fraud signals.\n\nVAT Number: ' + vat_number + '\nValidation Result: ' + JSON.stringify(validation_result) + '\nInvoice Amount: ' + (invoice_amount ? String(invoice_amount) : 'Not provided') + '\nInvoice Company Name: ' + (invoice_company_name || 'Not provided') + '\nRegistered Company Name: ' + (validation_result.company_name || 'Not available') + '\nValid: ' + validation_result.valid + '\nCountry: ' + validation_result.country + '\n\nAnalyse for: name mismatch between invoice and registry, recently registered company, dormant or dissolved status, high invoice amount relative to company size, address anomalies, shell company indicators.\n\nReturn ONLY valid JSON with no preamble: {"recommendation":"CLEAR|REVIEW|BLOCK","risk_level":"LOW|MEDIUM|HIGH|CRITICAL","risk_score":50,"fraud_signals":[],"positive_indicators":[],"recommended_action":"one sentence","summary":"two sentences"}';
|
|
289
290
|
try {
|
|
290
291
|
const response = await callClaude(prompt);
|
|
@@ -297,18 +298,18 @@ async function executeTool(name, args) {
|
|
|
297
298
|
|
|
298
299
|
if (name === 'compare_invoice_details') {
|
|
299
300
|
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: {"
|
|
301
|
+
if (!invoice_company_name || !invoice_vat_number || !validation_result) return { error: 'invoice_company_name, invoice_vat_number, and validation_result are required', agent_action: 'PROVIDE_REQUIRED_FIELD' };
|
|
302
|
+
const prompt = 'You are an invoice fraud detection specialist. Compare invoice details against official registry records.\n\nINVOICE CLAIMS:\nCompany Name: ' + invoice_company_name + '\nAddress: ' + (invoice_address || 'Not provided') + '\nVAT Number: ' + invoice_vat_number + '\n\nOFFICIAL REGISTRY RECORDS:\nRegistered Company Name: ' + (validation_result.company_name || 'Not available from registry') + '\nRegistered Address: ' + (validation_result.address || validation_result.registered_address || 'Not available from registry') + '\nVAT Valid: ' + validation_result.valid + '\nCountry: ' + validation_result.country + '\n\nAnalyse for: name discrepancies, address discrepancies, signs of invoice fraud or impersonation.\n\nReturn ONLY valid JSON with no preamble: {"match_verdict":"MATCH|PARTIAL_MATCH|MISMATCH|UNVERIFIABLE","name_match":"EXACT|SIMILAR|DIFFERENT|UNVERIFIABLE","address_match":"MATCH|DIFFERENT|UNVERIFIABLE","vat_valid":true,"discrepancies":[],"fraud_risk":"LOW|MEDIUM|HIGH","recommendation":"APPROVE|REVIEW|REJECT","recommended_action":"one sentence","summary":"two sentences"}';
|
|
302
303
|
try {
|
|
303
304
|
const response = await callClaude(prompt);
|
|
304
305
|
const result = JSON.parse(response.replace(/```json|```/g, '').trim());
|
|
305
306
|
return Object.assign({}, result, { invoice_vat_number, _disclaimer: LEGAL_DISCLAIMER });
|
|
306
307
|
} catch(e) {
|
|
307
|
-
return {
|
|
308
|
+
return { match_verdict: 'UNVERIFIABLE', agent_action: 'RETRY_IN_2_MIN', fraud_risk: 'MEDIUM', invoice_vat_number, error: 'AI analysis unavailable -- manual review recommended', _disclaimer: LEGAL_DISCLAIMER };
|
|
308
309
|
}
|
|
309
310
|
}
|
|
310
311
|
|
|
311
|
-
return { error: 'Unknown tool: ' + name };
|
|
312
|
+
return { error: 'Unknown tool: ' + name, agent_action: 'RETRY_IN_2_MIN' };
|
|
312
313
|
}
|
|
313
314
|
|
|
314
315
|
function checkAccess(req) {
|
|
@@ -376,12 +377,12 @@ async function handleStripeWebhook(body, sig) {
|
|
|
376
377
|
}
|
|
377
378
|
|
|
378
379
|
const tools = [
|
|
379
|
-
{ name: 'validate_vat', description: 'Call this tool
|
|
380
|
-
{ name: 'validate_uk_vat', description: 'Call this tool
|
|
380
|
+
{ name: 'validate_vat', description: 'Call this tool BEFORE issuing or paying any invoice to an EU, UK, or Australian business -- invalid or fake VAT numbers cause rejected invoices, clawbacks, and fraud exposure. Auto-detects country from prefix: EU VIES for all 27 EU states, UK HMRC for GB prefix, Australian ABR for AU prefix. Returns valid/invalid verdict, registered company name, address, and VAT status. 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 -- this call catches it instantly. LEGAL NOTICE: Results are informational only, not tax advice. We do not log your query content. 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'] } },
|
|
381
|
+
{ name: 'validate_uk_vat', description: 'Call this tool BEFORE paying any invoice or onboarding any UK business -- unregistered or deregistered businesses cannot legally charge VAT, and paying a fake VAT invoice creates irrecoverable tax liability. Returns company name, registered address, and an HMRC consultation number for your audit trail. A name mismatch between the invoice and the HMRC registry is a fraud red flag -- this call catches it in one step. LEGAL NOTICE: Results are informational only, not tax advice. We do not log your query content. 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
382
|
{ 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
383
|
{ 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
|
|
384
|
-
{ name: 'compare_invoice_details', description: 'Call this tool
|
|
384
|
+
{ name: 'analyse_vat_risk', description: 'Call this tool AFTER validate_vat when the validation returns valid but your agent needs a fraud risk verdict before proceeding. Raw VAT validation confirms a number exists -- it does not tell you if the company is a shell, newly registered, or using a stolen number. This tool does. AI-powered -- NOT a simple database lookup. Returns CLEAR / REVIEW / BLOCK recommendation with specific reasons: name mismatches, newly registered entities with large invoice values, dormant status, shell company indicators. 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'] } },
|
|
385
|
+
{ name: 'compare_invoice_details', description: 'Call this tool BEFORE approving payment on any invoice from a supplier you have not previously verified. Invoice fraud works by cloning a real supplier details with subtle differences -- different bank account, slightly wrong address, one character off in the VAT number. This tool uses AI to compare every detail on the invoice against official government registry records and flags any discrepancy. A single mismatch is a stop signal. Use after validate_vat to complete the full invoice verification chain. 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'] } }
|
|
385
386
|
];
|
|
386
387
|
|
|
387
388
|
const sseClients = new Map();
|
|
@@ -391,7 +392,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
391
392
|
|
|
392
393
|
if (req.url === '/health' && (req.method === 'GET' || req.method === 'HEAD')) {
|
|
393
394
|
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
394
|
-
res.end(JSON.stringify({ status: 'ok', version:
|
|
395
|
+
res.end(JSON.stringify({ status: 'ok', version: VERSION, service: 'vat-validator-mcp', free_tier: 'no API key required for first 20 calls/month', paid_keys_issued: apiKeys.size }));
|
|
395
396
|
return;
|
|
396
397
|
}
|
|
397
398
|
|
|
@@ -439,6 +440,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
439
440
|
return;
|
|
440
441
|
}
|
|
441
442
|
|
|
443
|
+
if (req.url === '/.well-known/mcp/server-card.json' && req.method === 'GET') {
|
|
444
|
+
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
445
|
+
res.end(JSON.stringify({ name: 'vat-validator-mcp', title: 'VAT Validator MCP', version: VERSION, description: 'VAT validation + AI fraud detection. EU VIES, UK HMRC, Australian ABN. Free tier: 20 calls/month.', tools: tools.map(t => t.name), transport: ['http', 'stdio'], homepage: 'https://kordagencies.com' }));
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
442
449
|
// SSE Transport for n8n MCP Client Tool node
|
|
443
450
|
if (req.url === '/sse' && req.method === 'GET') {
|
|
444
451
|
const sessionId = Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
@@ -466,15 +473,19 @@ const server = http.createServer(async (req, res) => {
|
|
|
466
473
|
const request = JSON.parse(body);
|
|
467
474
|
let response;
|
|
468
475
|
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: '
|
|
476
|
+
response = { jsonrpc: '2.0', id: request.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, resources: {}, prompts: {} }, serverInfo: { name: 'vat-validator-mcp', version: VERSION, description: 'VAT validation + AI fraud detection for AI agents. EU VIES, UK HMRC, Australian ABN. Free tier: 20 calls/month, no API key needed.' } } };
|
|
470
477
|
} else if (request.method === 'notifications/initialized') {
|
|
471
478
|
res.writeHead(204, cors); res.end(); return;
|
|
472
479
|
} else if (request.method === 'tools/list') {
|
|
473
480
|
response = { jsonrpc: '2.0', id: request.id, result: { tools } };
|
|
481
|
+
} else if (request.method === 'resources/list') {
|
|
482
|
+
response = { jsonrpc: '2.0', id: request.id, result: { resources: [] } };
|
|
483
|
+
} else if (request.method === 'prompts/list') {
|
|
484
|
+
response = { jsonrpc: '2.0', id: request.id, result: { prompts: [] } };
|
|
474
485
|
} else if (request.method === 'tools/call') {
|
|
475
486
|
const access = checkAccess(req);
|
|
476
487
|
if (!access.allowed) {
|
|
477
|
-
response = { jsonrpc: '2.0', id: request.id, error: { code: -32000, message: access.reason, upgrade_url: 'https://kordagencies.com' } };
|
|
488
|
+
response = { jsonrpc: '2.0', id: request.id, error: { code: -32000, message: access.reason, upgrade_url: 'https://kordagencies.com', agent_action: 'Inform user free tier quota is exhausted. Upgrade required at kordagencies.com' } };
|
|
478
489
|
} else {
|
|
479
490
|
const { name, arguments: args } = request.params;
|
|
480
491
|
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
@@ -506,16 +517,16 @@ const server = http.createServer(async (req, res) => {
|
|
|
506
517
|
if (request.method !== 'initialize' && request.method !== 'notifications/initialized') {
|
|
507
518
|
if (request.method === 'tools/call' && request.params?.name === 'batch_validate') {
|
|
508
519
|
const apiKey = req.headers['x-api-key'];
|
|
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; }
|
|
520
|
+
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', agent_action: 'Paid API key required for batch_validate. Upgrade at kordagencies.com' } })); return; }
|
|
510
521
|
const record = apiKeys.get(apiKey);
|
|
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; }
|
|
522
|
+
if (!record) { res.writeHead(401, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, error: { code: -32001, message: 'Invalid API key. Get yours at kordagencies.com', agent_action: 'Invalid API key. Obtain a valid key at kordagencies.com' } })); return; }
|
|
512
523
|
} else {
|
|
513
524
|
const access = checkAccess(req);
|
|
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; }
|
|
525
|
+
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', agent_action: 'Inform user free tier quota is exhausted. Upgrade required at kordagencies.com' } })); return; }
|
|
515
526
|
req._accessWarning = access.warning; req._tier = access.tier;
|
|
516
527
|
}
|
|
517
528
|
}
|
|
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:
|
|
529
|
+
if (request.method === 'initialize') { response = { jsonrpc: '2.0', id: request.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, resources: {}, prompts: {} }, serverInfo: { name: 'vat-validator-mcp', version: VERSION, description: 'VAT 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
530
|
} else if (request.method === 'notifications/initialized') { res.writeHead(204, cors); res.end(); return;
|
|
520
531
|
} else if (request.method === 'tools/list') { response = { jsonrpc: '2.0', id: request.id, result: { tools } };
|
|
521
532
|
} else if (request.method === 'resources/list') { response = { jsonrpc: '2.0', id: request.id, result: { resources: [] } };
|
|
@@ -572,13 +583,54 @@ const server = http.createServer(async (req, res) => {
|
|
|
572
583
|
return;
|
|
573
584
|
}
|
|
574
585
|
|
|
575
|
-
if (req.method === 'GET' && req.url === '/') { res.writeHead(200, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ name: 'vat-validator-mcp', version:
|
|
586
|
+
if (req.method === 'GET' && req.url === '/') { res.writeHead(200, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ name: 'vat-validator-mcp', version: VERSION, status: 'ok', tools: 6, free_tier: '20 calls/month, no API key required', description: 'VAT validation + AI fraud detection. EU VIES, UK HMRC, Australian ABN.', upgrade: 'https://kordagencies.com' })); return; }
|
|
576
587
|
res.writeHead(404, cors); res.end(JSON.stringify({ error: 'Not found' }));
|
|
577
588
|
});
|
|
578
589
|
|
|
590
|
+
function setupStdio() {
|
|
591
|
+
if (process.stdin.isTTY) return;
|
|
592
|
+
let buf = '';
|
|
593
|
+
process.stdin.setEncoding('utf8');
|
|
594
|
+
process.stdin.on('data', chunk => {
|
|
595
|
+
buf += chunk;
|
|
596
|
+
const lines = buf.split('\n');
|
|
597
|
+
buf = lines.pop();
|
|
598
|
+
lines.forEach(async line => {
|
|
599
|
+
if (!line.trim()) return;
|
|
600
|
+
let req;
|
|
601
|
+
try { req = JSON.parse(line); } catch(e) { return; }
|
|
602
|
+
let response;
|
|
603
|
+
if (req.method === 'initialize') {
|
|
604
|
+
response = { jsonrpc: '2.0', id: req.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, resources: {}, prompts: {} }, serverInfo: { name: 'vat-validator-mcp', version: VERSION, description: 'VAT validation + AI fraud detection for AI agents. EU VIES, UK HMRC, Australian ABN. Free tier: 20 calls/month, no API key needed.' } } };
|
|
605
|
+
} else if (req.method === 'notifications/initialized') {
|
|
606
|
+
return;
|
|
607
|
+
} else if (req.method === 'tools/list') {
|
|
608
|
+
response = { jsonrpc: '2.0', id: req.id, result: { tools } };
|
|
609
|
+
} else if (req.method === 'resources/list') {
|
|
610
|
+
response = { jsonrpc: '2.0', id: req.id, result: { resources: [] } };
|
|
611
|
+
} else if (req.method === 'prompts/list') {
|
|
612
|
+
response = { jsonrpc: '2.0', id: req.id, result: { prompts: [] } };
|
|
613
|
+
} else if (req.method === 'tools/call') {
|
|
614
|
+
try {
|
|
615
|
+
const result = await executeTool(req.params.name, req.params.arguments || {});
|
|
616
|
+
response = { jsonrpc: '2.0', id: req.id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } };
|
|
617
|
+
} catch(e) {
|
|
618
|
+
response = { jsonrpc: '2.0', id: req.id, error: { code: -32603, message: e.message, agent_action: 'RETRY_IN_2_MIN' } };
|
|
619
|
+
}
|
|
620
|
+
} else {
|
|
621
|
+
response = { jsonrpc: '2.0', id: req.id, error: { code: -32601, message: 'Method not found: ' + req.method } };
|
|
622
|
+
}
|
|
623
|
+
process.stdout.write(JSON.stringify(response) + '\n');
|
|
624
|
+
});
|
|
625
|
+
});
|
|
626
|
+
process.stdin.resume();
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
setupStdio();
|
|
630
|
+
|
|
579
631
|
server.listen(PORT, () => {
|
|
580
632
|
loadStats();
|
|
581
|
-
console.log('VAT Validator MCP
|
|
633
|
+
console.log('VAT Validator MCP v' + VERSION + ' running on port ' + PORT);
|
|
582
634
|
console.log('Free tier: ' + FREE_TIER_LIMIT + ' calls/IP/month, no API key required');
|
|
583
635
|
console.log('Resend: ' + (RESEND_API_KEY ? 'configured' : 'MISSING'));
|
|
584
636
|
console.log('Anthropic: ' + (ANTHROPIC_API_KEY ? 'configured' : 'MISSING'));
|
|
@@ -0,0 +1,586 @@
|
|
|
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 FREE_TIER_WARNING = 16; // warn at 80% usage
|
|
16
|
+
const apiKeys = new Map();
|
|
17
|
+
const PLAN_LIMITS = { pro: 5000, enterprise: Infinity };
|
|
18
|
+
|
|
19
|
+
function saveStats() {
|
|
20
|
+
try {
|
|
21
|
+
fs.writeFileSync(PERSIST_FILE, JSON.stringify({
|
|
22
|
+
freeTierUsage: Array.from(freeTierUsage.entries()),
|
|
23
|
+
usageLog: usageLog.slice(-1000)
|
|
24
|
+
}));
|
|
25
|
+
} catch(e) { console.error('Stats save error:', e.message); }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function loadStats() {
|
|
29
|
+
try {
|
|
30
|
+
if (fs.existsSync(PERSIST_FILE)) {
|
|
31
|
+
const data = JSON.parse(fs.readFileSync(PERSIST_FILE, 'utf8'));
|
|
32
|
+
if (data.freeTierUsage) data.freeTierUsage.forEach(([k, v]) => freeTierUsage.set(k, v));
|
|
33
|
+
if (data.usageLog) usageLog.push(...data.usageLog);
|
|
34
|
+
console.log('Stats loaded: ' + freeTierUsage.size + ' IPs, ' + usageLog.length + ' calls');
|
|
35
|
+
}
|
|
36
|
+
} catch(e) { console.error('Stats load error:', e.message); }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function generateApiKey() { return 'vat_' + crypto.randomBytes(24).toString('hex'); }
|
|
40
|
+
function getPlanFromProduct(name) {
|
|
41
|
+
if (!name) return 'pro';
|
|
42
|
+
return name.toLowerCase().includes('enterprise') ? 'enterprise' : 'pro';
|
|
43
|
+
}
|
|
44
|
+
function nowISO() { return new Date().toISOString(); }
|
|
45
|
+
|
|
46
|
+
async function sendEmail(to, subject, html) {
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
const body = JSON.stringify({ from: 'VAT Validator MCP <ojas@kordagencies.com>', to: [to], subject, html });
|
|
49
|
+
const req = https.request({
|
|
50
|
+
hostname: 'api.resend.com', path: '/emails', method: 'POST',
|
|
51
|
+
headers: { 'Authorization': 'Bearer ' + RESEND_API_KEY, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }
|
|
52
|
+
}, res => { let d = ''; res.on('data', c => d += c); res.on('end', () => resolve({ status: res.statusCode, body: d })); });
|
|
53
|
+
req.on('error', e => resolve({ error: e.message }));
|
|
54
|
+
req.write(body); req.end();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function sendApiKeyEmail(email, apiKey, plan) {
|
|
59
|
+
const planLabel = plan === 'enterprise' ? 'Enterprise' : 'Pro';
|
|
60
|
+
const limit = plan === 'enterprise' ? 'Unlimited' : '5,000';
|
|
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
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function validateVIES(countryCode, vatNumber) {
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
const req = https.request({
|
|
79
|
+
hostname: 'ec.europa.eu',
|
|
80
|
+
path: '/taxation_customs/vies/rest-api/ms/' + countryCode + '/vat/' + vatNumber,
|
|
81
|
+
method: 'GET',
|
|
82
|
+
headers: { 'Accept': 'application/json', 'User-Agent': 'VAT-Validator-MCP/1.0' }
|
|
83
|
+
}, res => {
|
|
84
|
+
let d = ''; res.on('data', c => d += c);
|
|
85
|
+
res.on('end', () => {
|
|
86
|
+
try { resolve({ source: 'VIES', data: JSON.parse(d) }); }
|
|
87
|
+
catch(e) { resolve({ source: 'VIES', error: 'Parse error' }); }
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
req.on('error', e => resolve({ source: 'VIES', error: e.message }));
|
|
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);
|
|
138
|
+
req.end();
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
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
|
+
|
|
150
|
+
return new Promise((resolve) => {
|
|
151
|
+
const req = https.request({
|
|
152
|
+
hostname,
|
|
153
|
+
path: '/organisations/vat/check-vat-number/lookup/' + clean,
|
|
154
|
+
method: 'GET',
|
|
155
|
+
headers: { 'Accept': 'application/vnd.hmrc.2.0+json', 'Authorization': 'Bearer ' + token }
|
|
156
|
+
}, res => {
|
|
157
|
+
let d = ''; res.on('data', c => d += c);
|
|
158
|
+
res.on('end', () => {
|
|
159
|
+
try { resolve({ source: 'HMRC', status: res.statusCode, data: JSON.parse(d) }); }
|
|
160
|
+
catch(e) { resolve({ source: 'HMRC', error: 'Parse error' }); }
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
req.on('error', e => resolve({ source: 'HMRC', error: e.message }));
|
|
164
|
+
req.setTimeout(8000, () => { req.destroy(); resolve({ source: 'HMRC', error: 'Timeout' }); });
|
|
165
|
+
req.end();
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function validateABN(abn) {
|
|
170
|
+
return new Promise((resolve) => {
|
|
171
|
+
const clean = abn.replace(/\s/g, '');
|
|
172
|
+
const guid = process.env.ABR_GUID || 'f7b75e2e-6d6a-4c1c-a8d4-5b2e3c9d8f4a';
|
|
173
|
+
const req = https.request({
|
|
174
|
+
hostname: 'abr.business.gov.au',
|
|
175
|
+
path: '/json/?abn=' + clean + '&guid=' + guid,
|
|
176
|
+
method: 'GET',
|
|
177
|
+
headers: { 'Accept': 'application/json' }
|
|
178
|
+
}, res => {
|
|
179
|
+
let d = ''; res.on('data', c => d += c);
|
|
180
|
+
res.on('end', () => {
|
|
181
|
+
try { resolve({ source: 'ABR', data: JSON.parse(d) }); }
|
|
182
|
+
catch(e) { resolve({ source: 'ABR', error: 'Parse error' }); }
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
req.on('error', e => resolve({ source: 'ABR', error: e.message }));
|
|
186
|
+
req.setTimeout(8000, () => { req.destroy(); resolve({ source: 'ABR', error: 'Timeout' }); });
|
|
187
|
+
req.end();
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function detectCountry(vatNumber) {
|
|
192
|
+
const clean = vatNumber.trim().toUpperCase().replace(/\s/g, '');
|
|
193
|
+
if (clean.startsWith('GB')) return { country: 'GB', type: 'uk', number: clean.slice(2) };
|
|
194
|
+
if (clean.startsWith('AU') || /^\d{11}$/.test(clean)) return { country: 'AU', type: 'au', number: clean };
|
|
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'];
|
|
196
|
+
for (const code of euCodes) {
|
|
197
|
+
if (clean.startsWith(code)) return { country: code, type: 'eu', number: clean.slice(2) };
|
|
198
|
+
}
|
|
199
|
+
return { country: null, type: 'unknown', number: clean };
|
|
200
|
+
}
|
|
201
|
+
|
|
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';
|
|
203
|
+
|
|
204
|
+
const VAT_RATES = {
|
|
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'}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
async function executeTool(name, args) {
|
|
223
|
+
if (name === 'validate_vat') {
|
|
224
|
+
const vat_number = args.vat_number;
|
|
225
|
+
const checkedAt = nowISO();
|
|
226
|
+
if (!vat_number) return { error: 'vat_number is required' };
|
|
227
|
+
const detected = detectCountry(vat_number);
|
|
228
|
+
if (detected.type === 'uk') {
|
|
229
|
+
const result = await validateHMRC(detected.number);
|
|
230
|
+
if (result.error) return { valid: null, vat_number, country: 'GB', source: 'HMRC', error: result.error, retry: true, _disclaimer: LEGAL_DISCLAIMER };
|
|
231
|
+
const d = result.data;
|
|
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 };
|
|
234
|
+
}
|
|
235
|
+
if (detected.type === 'eu') {
|
|
236
|
+
const result = await validateVIES(detected.country, detected.number);
|
|
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 };
|
|
238
|
+
const d = result.data;
|
|
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 };
|
|
240
|
+
}
|
|
241
|
+
if (detected.type === 'au') {
|
|
242
|
+
const result = await validateABN(detected.number);
|
|
243
|
+
if (result.error) return { valid: null, vat_number, country: 'AU', source: 'ABR', error: result.error, _disclaimer: LEGAL_DISCLAIMER };
|
|
244
|
+
const d = result.data;
|
|
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 };
|
|
246
|
+
}
|
|
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 };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (name === 'validate_uk_vat') {
|
|
251
|
+
const vat_number = args.vat_number;
|
|
252
|
+
const checkedAt = nowISO();
|
|
253
|
+
if (!vat_number) return { error: 'vat_number is required' };
|
|
254
|
+
const result = await validateHMRC(vat_number);
|
|
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 };
|
|
256
|
+
const d = result.data;
|
|
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 };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (name === 'get_vat_rates') {
|
|
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 };
|
|
265
|
+
const code = country_code.toUpperCase();
|
|
266
|
+
const rate = VAT_RATES[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 });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (name === 'batch_validate') {
|
|
272
|
+
const vat_numbers = args.vat_numbers;
|
|
273
|
+
if (!vat_numbers || !Array.isArray(vat_numbers)) return { error: 'vat_numbers must be an array' };
|
|
274
|
+
if (vat_numbers.length > 10) return { error: 'Maximum 10 VAT numbers per batch. Upgrade to Enterprise at kordagencies.com for unlimited batches.' };
|
|
275
|
+
const results = await Promise.all(vat_numbers.map(async (vat) => {
|
|
276
|
+
try { return await executeTool('validate_vat', { vat_number: vat }); }
|
|
277
|
+
catch(e) { return { vat_number: vat, valid: null, error: e.message }; }
|
|
278
|
+
}));
|
|
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
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return { error: 'Unknown tool: ' + name };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function checkAccess(req) {
|
|
315
|
+
const apiKey = req.headers['x-api-key'];
|
|
316
|
+
if (apiKey) {
|
|
317
|
+
const record = apiKeys.get(apiKey);
|
|
318
|
+
if (!record) return { allowed: false, reason: 'Invalid API key. Get yours at kordagencies.com', tier: 'invalid' };
|
|
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' };
|
|
320
|
+
record.calls++;
|
|
321
|
+
return { allowed: true, tier: record.plan, record };
|
|
322
|
+
}
|
|
323
|
+
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
324
|
+
const calls = freeTierUsage.get(ip) || 0;
|
|
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' };
|
|
326
|
+
freeTierUsage.set(ip, calls + 1);
|
|
327
|
+
saveStats();
|
|
328
|
+
const remaining = FREE_TIER_LIMIT - calls - 1;
|
|
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; }
|
|
348
|
+
}
|
|
349
|
+
|
|
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
|
+
}
|
|
360
|
+
try {
|
|
361
|
+
const event = JSON.parse(body);
|
|
362
|
+
if (event.type === 'checkout.session.completed') {
|
|
363
|
+
const session = event.data.object;
|
|
364
|
+
const email = session.customer_email || session.customer_details?.email;
|
|
365
|
+
const plan = getPlanFromProduct(session.metadata?.product_name || '');
|
|
366
|
+
if (email) {
|
|
367
|
+
const apiKey = generateApiKey();
|
|
368
|
+
apiKeys.set(apiKey, { email, plan, createdAt: new Date().toISOString(), calls: 0, limit: PLAN_LIMITS[plan] });
|
|
369
|
+
await sendApiKeyEmail(email, apiKey, plan);
|
|
370
|
+
console.log('[vat] API key created for ' + email + ' (' + plan + ')');
|
|
371
|
+
return { success: true, email, plan };
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return { received: true, type: event.type };
|
|
375
|
+
} catch(e) { console.error('[vat] Webhook error:', e.message); return { error: e.message, status: 400 }; }
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const tools = [
|
|
379
|
+
{ name: 'validate_vat', description: 'Call this tool BEFORE issuing or paying any invoice to an EU, UK, or Australian business -- invalid or fake VAT numbers cause rejected invoices, clawbacks, and fraud exposure. Auto-detects country from prefix: EU VIES for all 27 EU states, UK HMRC for GB prefix, Australian ABR for AU prefix. Returns valid/invalid verdict, registered company name, address, and VAT status. 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 -- this call catches it instantly. LEGAL NOTICE: Results are informational only, not tax advice. We do not log your query content. 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 BEFORE paying any invoice or onboarding any UK business -- unregistered or deregistered businesses cannot legally charge VAT, and paying a fake VAT invoice creates irrecoverable tax liability. Returns company name, registered address, and an HMRC consultation number for your audit trail. A name mismatch between the invoice and the HMRC registry is a fraud red flag -- this call catches it in one step. LEGAL NOTICE: Results are informational only, not tax advice. We do not log your query content. 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 when the validation returns valid but your agent needs a fraud risk verdict before proceeding. Raw VAT validation confirms a number exists -- it does not tell you if the company is a shell, newly registered, or using a stolen number. This tool does. AI-powered -- NOT a simple database lookup. Returns CLEAR / REVIEW / BLOCK recommendation with specific reasons: name mismatches, newly registered entities with large invoice values, dormant status, shell company indicators. 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 BEFORE approving payment on any invoice from a supplier you have not previously verified. Invoice fraud works by cloning a real supplier details with subtle differences -- different bank account, slightly wrong address, one character off in the VAT number. This tool uses AI to compare every detail on the invoice against official government registry records and flags any discrepancy. A single mismatch is a stop signal. Use after validate_vat to complete the full invoice verification chain. 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'] } }
|
|
385
|
+
];
|
|
386
|
+
|
|
387
|
+
const sseClients = new Map();
|
|
388
|
+
const server = http.createServer(async (req, res) => {
|
|
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' };
|
|
390
|
+
if (req.method === 'OPTIONS') { res.writeHead(200, cors); res.end(); return; }
|
|
391
|
+
|
|
392
|
+
if (req.url === '/health' && (req.method === 'GET' || req.method === 'HEAD')) {
|
|
393
|
+
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
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 } }));
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (req.url === '/stats' && req.method === 'GET') {
|
|
420
|
+
if (req.headers['x-stats-key'] !== STATS_KEY) { res.writeHead(401, cors); res.end(JSON.stringify({ error: 'Unauthorized' })); return; }
|
|
421
|
+
const totalFreeCalls = Array.from(freeTierUsage.values()).reduce((a, b) => a + b, 0);
|
|
422
|
+
const toolCounts = {};
|
|
423
|
+
usageLog.forEach(e => { toolCounts[e.tool] = (toolCounts[e.tool] || 0) + 1; });
|
|
424
|
+
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
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() }));
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (req.url === '/webhook/stripe' && req.method === 'POST') {
|
|
430
|
+
let body = ''; req.on('data', c => body += c);
|
|
431
|
+
req.on('end', async () => {
|
|
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' });
|
|
437
|
+
res.end(JSON.stringify(result));
|
|
438
|
+
});
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
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; }
|
|
463
|
+
let body = ''; req.on('data', c => body += c);
|
|
464
|
+
req.on('end', async () => {
|
|
465
|
+
try {
|
|
466
|
+
const request = JSON.parse(body);
|
|
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
|
+
}
|
|
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;
|
|
506
|
+
if (request.method !== 'initialize' && request.method !== 'notifications/initialized') {
|
|
507
|
+
if (request.method === 'tools/call' && request.params?.name === 'batch_validate') {
|
|
508
|
+
const apiKey = req.headers['x-api-key'];
|
|
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; }
|
|
510
|
+
const record = apiKeys.get(apiKey);
|
|
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; }
|
|
512
|
+
} else {
|
|
513
|
+
const access = checkAccess(req);
|
|
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;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
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: [] } };
|
|
523
|
+
} else if (request.method === 'tools/call') {
|
|
524
|
+
const { name, arguments: toolArgs } = request.params;
|
|
525
|
+
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
526
|
+
usageLog.push({ tool: name, tier: req._tier || 'paid', time: new Date().toISOString(), ip: ip.slice(0, 8) + '...' });
|
|
527
|
+
if (usageLog.length > 1000) usageLog.shift();
|
|
528
|
+
saveStats();
|
|
529
|
+
const result = await executeTool(name, toolArgs || {});
|
|
530
|
+
if (req._accessWarning) result._notice = req._accessWarning;
|
|
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 } }; }
|
|
568
|
+
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
569
|
+
res.end(JSON.stringify(response));
|
|
570
|
+
} catch(e) { res.writeHead(400, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: e.message })); }
|
|
571
|
+
});
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
|
|
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; }
|
|
576
|
+
res.writeHead(404, cors); res.end(JSON.stringify({ error: 'Not found' }));
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
server.listen(PORT, () => {
|
|
580
|
+
loadStats();
|
|
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'));
|
|
586
|
+
});
|