vat-validator-mcp 1.4.4 → 1.4.6
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/LICENSE +18 -7
- package/package.json +2 -2
- package/server.json +2 -2
- package/src/server.js +38 -28
package/LICENSE
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2026
|
|
3
|
+
Copyright (c) 2026 Kord Agencies Pte Ltd
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
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.6",
|
|
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": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"compliance"
|
|
30
30
|
],
|
|
31
31
|
"author": "Kord Agencies Pte Ltd <ojas@kordagencies.com>",
|
|
32
|
-
"license": "
|
|
32
|
+
"license": "MIT",
|
|
33
33
|
"homepage": "https://kordagencies.com",
|
|
34
34
|
"repository": {
|
|
35
35
|
"type": "git",
|
package/server.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "io.github.OjasKord/vat-validator-mcp",
|
|
4
4
|
"title": "VAT Validator MCP",
|
|
5
5
|
"description": "Validate EU, UK, AU VAT numbers for AI agents. EU ViDA e-invoicing compliance.",
|
|
6
|
-
"version": "1.4.
|
|
6
|
+
"version": "1.4.5",
|
|
7
7
|
"websiteUrl": "https://kordagencies.com",
|
|
8
8
|
"repository": {
|
|
9
9
|
"url": "https://github.com/OjasKord/vat-validator-mcp",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
{
|
|
14
14
|
"registryType": "npm",
|
|
15
15
|
"identifier": "vat-validator-mcp",
|
|
16
|
-
"version": "1.4.
|
|
16
|
+
"version": "1.4.5",
|
|
17
17
|
"transport": { "type": "stdio" },
|
|
18
18
|
"environmentVariables": [
|
|
19
19
|
{ "name": "ANTHROPIC_API_KEY", "description": "Anthropic API key for AI-powered fraud risk analysis", "isRequired": true, "isSecret": true },
|
package/src/server.js
CHANGED
|
@@ -4,7 +4,9 @@ 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.
|
|
7
|
+
const VERSION = '1.4.6';
|
|
8
|
+
const PRO_UPGRADE_URL = 'https://buy.stripe.com/28EeVceUB06N1ty3teebu0l';
|
|
9
|
+
const ENTERPRISE_UPGRADE_URL = 'https://buy.stripe.com/00w14m7s96vb1ty5Bmebu0m';
|
|
8
10
|
const RESEND_API_KEY = process.env.RESEND_API_KEY || '';
|
|
9
11
|
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || '';
|
|
10
12
|
const PORT = process.env.PORT || 3000;
|
|
@@ -224,36 +226,36 @@ async function executeTool(name, args) {
|
|
|
224
226
|
if (name === 'validate_vat') {
|
|
225
227
|
const vat_number = args.vat_number;
|
|
226
228
|
const checkedAt = nowISO();
|
|
227
|
-
if (!vat_number) return { error: 'vat_number is required', agent_action: 'PROVIDE_REQUIRED_FIELD' };
|
|
229
|
+
if (!vat_number) return { error: 'vat_number is required', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: 'validate_vat', trace_id: Math.random().toString(36).slice(2, 10) };
|
|
228
230
|
const detected = detectCountry(vat_number);
|
|
229
231
|
if (detected.type === 'uk') {
|
|
230
232
|
const result = await validateHMRC(detected.number);
|
|
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 };
|
|
233
|
+
if (result.error) return { valid: null, vat_number, country: 'GB', source: 'HMRC', error: result.error, agent_action: 'RETRY_IN_2_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 120000, fallback_tool: 'validate_vat', trace_id: Math.random().toString(36).slice(2, 10), retry: true, _disclaimer: LEGAL_DISCLAIMER };
|
|
232
234
|
const d = result.data;
|
|
233
235
|
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 };
|
|
234
236
|
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 };
|
|
235
237
|
}
|
|
236
238
|
if (detected.type === 'eu') {
|
|
237
239
|
const result = await validateVIES(detected.country, detected.number);
|
|
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 };
|
|
240
|
+
if (result.error) return { valid: null, vat_number, agent_action: 'RETRY_IN_30_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 1800000, fallback_tool: 'validate_vat', trace_id: Math.random().toString(36).slice(2, 10), country: detected.country, source: 'VIES', source_url: 'ec.europa.eu/taxation_customs/vies', error: 'EU VIES portal is temporarily unavailable — this is a known issue with the official EU system, not a problem with the VAT number. Retry in 30 minutes.', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
239
241
|
const d = result.data;
|
|
240
242
|
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 };
|
|
241
243
|
}
|
|
242
244
|
if (detected.type === 'au') {
|
|
243
245
|
const result = await validateABN(detected.number);
|
|
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 };
|
|
246
|
+
if (result.error) return { valid: null, vat_number, country: 'AU', source: 'ABR', error: result.error, agent_action: 'RETRY_IN_2_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 120000, fallback_tool: 'validate_vat', trace_id: Math.random().toString(36).slice(2, 10), _disclaimer: LEGAL_DISCLAIMER };
|
|
245
247
|
const d = result.data;
|
|
246
248
|
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 };
|
|
247
249
|
}
|
|
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 };
|
|
250
|
+
return { valid: null, vat_number, agent_action: 'PROVIDE_COUNTRY_PREFIX', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: 'validate_vat', trace_id: Math.random().toString(36).slice(2, 10), error: 'Could not detect country. Supported prefixes: EU (AT BE BG CY CZ DE DK EE EL ES FI FR HR HU IE IT LT LU LV MT NL PL PT RO SE SI SK), UK (GB), Australia (AU).', _disclaimer: LEGAL_DISCLAIMER };
|
|
249
251
|
}
|
|
250
252
|
|
|
251
253
|
if (name === 'validate_uk_vat') {
|
|
252
254
|
const vat_number = args.vat_number;
|
|
253
255
|
const checkedAt = nowISO();
|
|
254
|
-
if (!vat_number) return { error: 'vat_number is required', agent_action: 'PROVIDE_REQUIRED_FIELD' };
|
|
256
|
+
if (!vat_number) return { error: 'vat_number is required', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: 'validate_uk_vat', trace_id: Math.random().toString(36).slice(2, 10) };
|
|
255
257
|
const result = await validateHMRC(vat_number);
|
|
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 };
|
|
258
|
+
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.', agent_action: 'RETRY_IN_2_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 120000, fallback_tool: 'validate_uk_vat', trace_id: Math.random().toString(36).slice(2, 10), checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
257
259
|
const d = result.data;
|
|
258
260
|
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 };
|
|
259
261
|
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 };
|
|
@@ -265,17 +267,17 @@ async function executeTool(name, args) {
|
|
|
265
267
|
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 };
|
|
266
268
|
const code = country_code.toUpperCase();
|
|
267
269
|
const rate = VAT_RATES[code];
|
|
268
|
-
if (!rate) return { error: 'No VAT rate data for: ' + code + '. Supported: ' + Object.keys(VAT_RATES).join(', '), _disclaimer: LEGAL_DISCLAIMER };
|
|
270
|
+
if (!rate) return { error: 'No VAT rate data for: ' + code + '. Supported: ' + Object.keys(VAT_RATES).join(', '), agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: 'get_vat_rates', trace_id: Math.random().toString(36).slice(2, 10), _disclaimer: LEGAL_DISCLAIMER };
|
|
269
271
|
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 });
|
|
270
272
|
}
|
|
271
273
|
|
|
272
274
|
if (name === 'batch_validate') {
|
|
273
275
|
const vat_numbers = args.vat_numbers;
|
|
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' };
|
|
276
|
+
if (!vat_numbers || !Array.isArray(vat_numbers)) return { error: 'vat_numbers must be an array', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: 'batch_validate', trace_id: Math.random().toString(36).slice(2, 10) };
|
|
277
|
+
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', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: 'batch_validate', trace_id: Math.random().toString(36).slice(2, 10) };
|
|
276
278
|
const results = await Promise.all(vat_numbers.map(async (vat) => {
|
|
277
279
|
try { return await executeTool('validate_vat', { vat_number: vat }); }
|
|
278
|
-
catch(e) { return { vat_number: vat, valid: null, error: e.message }; }
|
|
280
|
+
catch(e) { return { vat_number: vat, valid: null, error: e.message, category: 'upstream_unavailable', retryable: true, retry_after_ms: 120000, fallback_tool: 'validate_vat', trace_id: Math.random().toString(36).slice(2, 10) }; }
|
|
279
281
|
}));
|
|
280
282
|
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 };
|
|
281
283
|
}
|
|
@@ -285,20 +287,20 @@ async function executeTool(name, args) {
|
|
|
285
287
|
const validation_result = args.validation_result;
|
|
286
288
|
const invoice_amount = args.invoice_amount;
|
|
287
289
|
const invoice_company_name = args.invoice_company_name;
|
|
288
|
-
if (!vat_number || !validation_result) return { error: 'vat_number and validation_result are required', agent_action: 'PROVIDE_REQUIRED_FIELD' };
|
|
290
|
+
if (!vat_number || !validation_result) return { error: 'vat_number and validation_result are required', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: 'analyse_vat_risk', trace_id: Math.random().toString(36).slice(2, 10) };
|
|
289
291
|
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"}';
|
|
290
292
|
try {
|
|
291
293
|
const response = await callClaude(prompt);
|
|
292
294
|
const result = JSON.parse(response.replace(/```json|```/g, '').trim());
|
|
293
295
|
return Object.assign({}, result, { vat_number, _disclaimer: LEGAL_DISCLAIMER });
|
|
294
296
|
} catch(e) {
|
|
295
|
-
return { recommendation: 'REVIEW', risk_level: 'MEDIUM', risk_score: 50, vat_number, error: 'AI analysis unavailable - manual review recommended', _disclaimer: LEGAL_DISCLAIMER };
|
|
297
|
+
return { recommendation: 'REVIEW', risk_level: 'MEDIUM', risk_score: 50, vat_number, error: 'AI analysis unavailable - manual review recommended', agent_action: 'RETRY_IN_2_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 120000, fallback_tool: 'analyse_vat_risk', trace_id: Math.random().toString(36).slice(2, 10), _disclaimer: LEGAL_DISCLAIMER };
|
|
296
298
|
}
|
|
297
299
|
}
|
|
298
300
|
|
|
299
301
|
if (name === 'compare_invoice_details') {
|
|
300
302
|
const { invoice_company_name, invoice_address, invoice_vat_number, validation_result } = args;
|
|
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' };
|
|
303
|
+
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', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: 'compare_invoice_details', trace_id: Math.random().toString(36).slice(2, 10) };
|
|
302
304
|
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"}';
|
|
303
305
|
try {
|
|
304
306
|
const response = await callClaude(prompt);
|
|
@@ -306,11 +308,11 @@ async function executeTool(name, args) {
|
|
|
306
308
|
const agentAction = result.recommendation === 'REJECT' ? 'BLOCK_PAYMENT' : result.recommendation === 'REVIEW' ? 'MANUAL_REVIEW_REQUIRED' : 'PROCEED_WITH_PAYMENT';
|
|
307
309
|
return Object.assign({}, result, { invoice_vat_number, agent_action: agentAction, discrepancies: result.discrepancies || [], _disclaimer: LEGAL_DISCLAIMER });
|
|
308
310
|
} catch(e) {
|
|
309
|
-
return { match_verdict: 'UNVERIFIABLE', agent_action: 'RETRY_IN_2_MIN', fraud_risk: 'MEDIUM', invoice_vat_number, discrepancies: [], error: 'AI analysis unavailable -- manual review recommended', _disclaimer: LEGAL_DISCLAIMER };
|
|
311
|
+
return { match_verdict: 'UNVERIFIABLE', agent_action: 'RETRY_IN_2_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 120000, fallback_tool: 'compare_invoice_details', trace_id: Math.random().toString(36).slice(2, 10), fraud_risk: 'MEDIUM', invoice_vat_number, discrepancies: [], error: 'AI analysis unavailable -- manual review recommended', _disclaimer: LEGAL_DISCLAIMER };
|
|
310
312
|
}
|
|
311
313
|
}
|
|
312
314
|
|
|
313
|
-
return { error: 'Unknown tool: ' + name, agent_action: 'RETRY_IN_2_MIN' };
|
|
315
|
+
return { error: 'Unknown tool: ' + name, agent_action: 'RETRY_IN_2_MIN', category: 'unknown_tool', retryable: false, retry_after_ms: null, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10) };
|
|
314
316
|
}
|
|
315
317
|
|
|
316
318
|
function checkAccess(req) {
|
|
@@ -324,11 +326,11 @@ function checkAccess(req) {
|
|
|
324
326
|
}
|
|
325
327
|
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
326
328
|
const calls = freeTierUsage.get(ip) || 0;
|
|
327
|
-
if (calls >= FREE_TIER_LIMIT) return { allowed: false, reason: 'Free tier limit
|
|
329
|
+
if (calls >= FREE_TIER_LIMIT) return { allowed: false, reason: 'Free tier limit reached. Get 500 calls for $8 at ' + PRO_UPGRADE_URL + ' -- calls never expire.', upgrade_url: PRO_UPGRADE_URL, tier: 'free_limit_reached' };
|
|
328
330
|
freeTierUsage.set(ip, calls + 1);
|
|
329
331
|
saveStats();
|
|
330
332
|
const remaining = FREE_TIER_LIMIT - calls - 1;
|
|
331
|
-
return { allowed: true, tier: 'free', remaining, warning: remaining < 5 ? remaining + ' free validations remaining.
|
|
333
|
+
return { allowed: true, tier: 'free', remaining, warning: remaining < 5 ? remaining + ' free validations remaining this month. Get 500 calls for $8 at ' + PRO_UPGRADE_URL + ' -- calls never expire.' : null };
|
|
332
334
|
}
|
|
333
335
|
|
|
334
336
|
function verifyStripeSignature(body, sig, secret) {
|
|
@@ -397,6 +399,14 @@ const server = http.createServer(async (req, res) => {
|
|
|
397
399
|
return;
|
|
398
400
|
}
|
|
399
401
|
|
|
402
|
+
if (req.url === '/ready' && (req.method === 'GET' || req.method === 'HEAD')) {
|
|
403
|
+
const checks = { anthropic: !!ANTHROPIC_API_KEY, hmrc_client_id: !!(process.env.HMRC_CLIENT_ID), hmrc_client_secret: !!(process.env.HMRC_CLIENT_SECRET) };
|
|
404
|
+
const ready = checks.anthropic && checks.hmrc_client_id && checks.hmrc_client_secret;
|
|
405
|
+
res.writeHead(ready ? 200 : 503, { ...cors, 'Content-Type': 'application/json' });
|
|
406
|
+
res.end(JSON.stringify({ status: ready ? 'ready' : 'not_ready', version: VERSION, checks }));
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
400
410
|
if (req.url === '/deps' && req.method === 'GET') {
|
|
401
411
|
const depCheck = (hostname, path, headers) => new Promise((resolve) => {
|
|
402
412
|
const r = https.request({ hostname, path, method: 'GET', headers: Object.assign({ 'User-Agent': 'VAT-Validator-MCP-HealthCheck/1.0' }, headers || {}) }, (res2) => {
|
|
@@ -443,7 +453,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
443
453
|
|
|
444
454
|
if (req.url === '/.well-known/mcp/server-card.json' && req.method === 'GET') {
|
|
445
455
|
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
446
|
-
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:
|
|
456
|
+
res.end(JSON.stringify({ name: 'vat-validator-mcp', title: 'VAT Validator MCP', version: VERSION, description: 'VAT validation + AI fraud detection. EU VIES, UK HMRC, Australian ABN. Free tier: 20 calls/month.', tools: tools.map(t => t.name), transport: 'streamable-http', homepage: 'https://kordagencies.com', token_footprint_min: 100, token_footprint_max: 600, token_footprint_avg: 200, idempotent_tools: ['validate_vat', 'validate_uk_vat', 'get_vat_rates', 'batch_validate', 'analyse_vat_risk', 'compare_invoice_details'], circuit_breaker: false, health_endpoint: '/health', ready_endpoint: '/ready' }));
|
|
447
457
|
return;
|
|
448
458
|
}
|
|
449
459
|
|
|
@@ -486,7 +496,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
486
496
|
} else if (request.method === 'tools/call') {
|
|
487
497
|
const access = checkAccess(req);
|
|
488
498
|
if (!access.allowed) {
|
|
489
|
-
response = { jsonrpc: '2.0', id: request.id, error: { code: -32000, message: access.reason, upgrade_url:
|
|
499
|
+
response = { jsonrpc: '2.0', id: request.id, error: { code: -32000, message: access.reason, upgrade_url: PRO_UPGRADE_URL, agent_action: 'Inform user free tier quota is exhausted. Get 500 calls for $8 at ' + PRO_UPGRADE_URL + ' -- calls never expire.' } };
|
|
490
500
|
} else {
|
|
491
501
|
const { name, arguments: args } = request.params;
|
|
492
502
|
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
@@ -518,12 +528,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
518
528
|
if (request.method !== 'initialize' && request.method !== 'notifications/initialized') {
|
|
519
529
|
if (request.method === 'tools/call' && request.params?.name === 'batch_validate') {
|
|
520
530
|
const apiKey = req.headers['x-api-key'];
|
|
521
|
-
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
|
|
531
|
+
if (!apiKey) { res.writeHead(402, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, error: { code: -32002, message: 'batch_validate requires a paid API key. Get 500 calls for $8 at ' + PRO_UPGRADE_URL + ' -- calls never expire.', upgrade_url: PRO_UPGRADE_URL, agent_action: 'Paid API key required for batch_validate. Get 500 calls for $8 at ' + PRO_UPGRADE_URL } })); return; }
|
|
522
532
|
const record = apiKeys.get(apiKey);
|
|
523
533
|
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; }
|
|
524
534
|
} else {
|
|
525
535
|
const access = checkAccess(req);
|
|
526
|
-
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:
|
|
536
|
+
if (!access.allowed) { res.writeHead(429, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, error: { code: -32000, message: access.reason, upgrade_url: PRO_UPGRADE_URL, agent_action: 'Inform user free tier quota is exhausted. Get 500 calls for $8 at ' + PRO_UPGRADE_URL + ' -- calls never expire.' } })); return; }
|
|
527
537
|
req._accessWarning = access.warning; req._tier = access.tier;
|
|
528
538
|
}
|
|
529
539
|
}
|
|
@@ -552,7 +562,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
552
562
|
// Gate address on free tier — company name + valid status visible
|
|
553
563
|
const gated = ['registered_address', 'address', 'consultation_number'];
|
|
554
564
|
gated.forEach(f => delete result[f]);
|
|
555
|
-
result._upgrade_note = 'Free tier: ' + remaining + ' of ' + FREE_TIER_LIMIT + ' calls remaining.
|
|
565
|
+
result._upgrade_note = 'Free tier: ' + remaining + ' of ' + FREE_TIER_LIMIT + ' calls remaining. Get 500 calls for $8 at ' + PRO_UPGRADE_URL + ' -- calls never expire. Includes full registered address and HMRC consultation number.';
|
|
556
566
|
result._gated_fields = gated;
|
|
557
567
|
}
|
|
558
568
|
|
|
@@ -560,7 +570,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
560
570
|
// Gate full reasoning — verdict visible, details gated
|
|
561
571
|
const gated = ['fraud_signals', 'positive_indicators', 'recommended_action', 'summary'];
|
|
562
572
|
gated.forEach(f => delete result[f]);
|
|
563
|
-
result._upgrade_note = 'Free tier: ' + remaining + ' of ' + FREE_TIER_LIMIT + ' calls remaining.
|
|
573
|
+
result._upgrade_note = 'Free tier: ' + remaining + ' of ' + FREE_TIER_LIMIT + ' calls remaining. Get 500 calls for $8 at ' + PRO_UPGRADE_URL + ' -- calls never expire. Includes full fraud signal breakdown, positive indicators, and recommended action.';
|
|
564
574
|
result._gated_fields = gated;
|
|
565
575
|
}
|
|
566
576
|
|
|
@@ -568,11 +578,11 @@ const server = http.createServer(async (req, res) => {
|
|
|
568
578
|
// Gate detail fields — match_status visible, discrepancies gated
|
|
569
579
|
const gated = ['discrepancies', 'name_match', 'address_match', 'recommended_action', 'summary'];
|
|
570
580
|
gated.forEach(f => delete result[f]);
|
|
571
|
-
result._upgrade_note = 'Free tier: ' + remaining + ' of ' + FREE_TIER_LIMIT + ' calls remaining.
|
|
581
|
+
result._upgrade_note = 'Free tier: ' + remaining + ' of ' + FREE_TIER_LIMIT + ' calls remaining. Get 500 calls for $8 at ' + PRO_UPGRADE_URL + ' -- calls never expire. Includes full discrepancy analysis and recommended action.';
|
|
572
582
|
result._gated_fields = gated;
|
|
573
583
|
}
|
|
574
584
|
|
|
575
|
-
if (isWarning) result._notice = 'Warning: only ' + remaining + ' free call' + (remaining === 1 ? '' : 's') + ' left this month.
|
|
585
|
+
if (isWarning) result._notice = 'Warning: only ' + remaining + ' free call' + (remaining === 1 ? '' : 's') + ' left this month. Get 500 calls for $8 at ' + PRO_UPGRADE_URL + ' -- calls never expire.';
|
|
576
586
|
}
|
|
577
587
|
|
|
578
588
|
response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } };
|
|
@@ -584,7 +594,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
584
594
|
return;
|
|
585
595
|
}
|
|
586
596
|
|
|
587
|
-
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:
|
|
597
|
+
if (req.method === 'GET' && req.url === '/') { res.writeHead(200, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ name: 'vat-validator-mcp', version: VERSION, status: 'ok', tools: 6, free_tier: '20 calls/month, no API key required', description: 'VAT validation + AI fraud detection. EU VIES, UK HMRC, Australian ABN.', upgrade: PRO_UPGRADE_URL })); return; }
|
|
588
598
|
res.writeHead(404, cors); res.end(JSON.stringify({ error: 'Not found' }));
|
|
589
599
|
});
|
|
590
600
|
|