vat-validator-mcp 1.4.12 → 1.4.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/glama.json +40 -14
- package/package.json +4 -1
- package/railway +0 -0
- package/server.json +2 -2
- package/src/server.js +259 -63
package/glama.json
CHANGED
|
@@ -1,19 +1,45 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
"
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
|
|
2
|
+
"$schema": "https://glama.ai/mcp/servers/schema.json",
|
|
3
|
+
"name": "VAT Validator MCP",
|
|
4
|
+
"description": "AI-powered VAT fraud detection and live VAT validation via EU VIES (27 member states), UK HMRC, and AU ABR. Call before invoice approval, supplier onboarding, or cross-border payment. Detects missing trader fraud, carousel fraud, deregistered entity re-use. Returns CLEAR/REVIEW/BLOCK verdict.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"categories": [
|
|
7
|
+
"finance",
|
|
8
|
+
"government-data",
|
|
9
|
+
"legal-and-compliance"
|
|
10
|
+
],
|
|
11
|
+
"remote": {
|
|
12
|
+
"transport": "sse",
|
|
13
|
+
"url": "https://vat-validator-mcp-production.up.railway.app/sse"
|
|
14
|
+
},
|
|
8
15
|
"tools": [
|
|
9
|
-
{
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
{
|
|
17
|
+
"name": "validate_vat",
|
|
18
|
+
"description": "Validates EU VAT numbers against EU VIES (all 27 member states) and AU ABR in real time. Returns valid/invalid, registered company name, address."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "validate_uk_vat",
|
|
22
|
+
"description": "Validates UK VAT numbers against HMRC VAT API v2 via OAuth2. Returns valid/invalid, registered business name, address."
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"name": "get_vat_rates",
|
|
26
|
+
"description": "Returns current standard, reduced, and zero VAT rates for all 27 EU member states and UK."
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "batch_validate",
|
|
30
|
+
"description": "Validates multiple VAT numbers against EU VIES and HMRC in one call. Returns per-number verdicts in structured JSON."
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "analyse_vat_risk",
|
|
34
|
+
"description": "AI-powered VAT fraud risk scoring. Detects missing trader fraud, carousel fraud, deregistered entity re-use. Returns CLEAR/REVIEW/BLOCK recommendation, risk score 0-100, fraud signals, agent_action (PROCEED/VERIFY_MANUALLY/HOLD)."
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "compare_invoice_details",
|
|
38
|
+
"description": "Cross-checks invoice VAT details against live VIES and HMRC registry data. Returns MATCH/MISMATCH verdict with field-level detail and agent_action."
|
|
39
|
+
}
|
|
13
40
|
],
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"enterprise": "$299/month — unlimited + batch"
|
|
41
|
+
"links": {
|
|
42
|
+
"homepage": "https://kordagencies.com",
|
|
43
|
+
"npm": "https://www.npmjs.com/package/vat-validator-mcp"
|
|
18
44
|
}
|
|
19
45
|
}
|
package/package.json
CHANGED
|
@@ -1,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.13",
|
|
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": {
|
|
@@ -40,5 +40,8 @@
|
|
|
40
40
|
},
|
|
41
41
|
"engines": {
|
|
42
42
|
"node": ">=18.0.0"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"stripe": "^22.1.1"
|
|
43
46
|
}
|
|
44
47
|
}
|
package/railway
ADDED
|
File without changes
|
package/server.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "io.github.OjasKord/vat-validator-mcp",
|
|
4
4
|
"title": "VAT Validator MCP",
|
|
5
5
|
"description": "Validate EU, UK, AU VAT numbers for AI agents. EU ViDA e-invoicing compliance.",
|
|
6
|
-
"version": "1.4.
|
|
6
|
+
"version": "1.4.13",
|
|
7
7
|
"websiteUrl": "https://kordagencies.com",
|
|
8
8
|
"repository": {
|
|
9
9
|
"url": "https://github.com/OjasKord/vat-validator-mcp",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
{
|
|
14
14
|
"registryType": "npm",
|
|
15
15
|
"identifier": "vat-validator-mcp",
|
|
16
|
-
"version": "1.4.
|
|
16
|
+
"version": "1.4.13",
|
|
17
17
|
"transport": { "type": "stdio" },
|
|
18
18
|
"environmentVariables": [
|
|
19
19
|
{ "name": "ANTHROPIC_API_KEY", "description": "Anthropic API key for AI-powered fraud risk analysis", "isRequired": true, "isSecret": true },
|
package/src/server.js
CHANGED
|
@@ -2,26 +2,28 @@ const http = require('http');
|
|
|
2
2
|
const https = require('https');
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const fs = require('fs');
|
|
5
|
+
const Stripe = require('stripe');
|
|
6
|
+
const stripe = Stripe(process.env.STRIPE_SECRET_KEY);
|
|
5
7
|
|
|
6
8
|
const PERSIST_FILE = '/tmp/vat_stats.json';
|
|
7
|
-
const
|
|
8
|
-
const VERSION = '1.4.12';
|
|
9
|
-
const PRO_UPGRADE_URL = 'https://buy.stripe.com/28EeVceUB06N1ty3teebu0l';
|
|
10
|
-
const ENTERPRISE_UPGRADE_URL = 'https://buy.stripe.com/00w14m7s96vb1ty5Bmebu0m';
|
|
9
|
+
const VERSION = '1.4.13';
|
|
11
10
|
const RESEND_API_KEY = process.env.RESEND_API_KEY || '';
|
|
12
11
|
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || '';
|
|
13
12
|
const PORT = process.env.PORT || 3000;
|
|
14
13
|
const STATS_KEY = process.env.STATS_KEY || 'ojas2026';
|
|
14
|
+
const REDIS_PREFIX = 'vat';
|
|
15
|
+
const FREE_TIER_LIMIT = 50;
|
|
16
|
+
const METERED_SUBSCRIBE_URL = 'https://vat-validator-mcp-production.up.railway.app/subscribe';
|
|
17
|
+
const BUNDLE_500_URL = 'https://buy.stripe.com/28EeVceUB06N1ty3teebu0l';
|
|
18
|
+
const BUNDLE_2000_URL = 'https://buy.stripe.com/00w14m7s96vb1ty5Bmebu0m';
|
|
15
19
|
|
|
16
20
|
const freeTierUsage = new Map();
|
|
17
21
|
const usageLog = [];
|
|
18
22
|
const toolUsageCounts = {};
|
|
19
23
|
const trialExtensions = new Map();
|
|
20
|
-
const
|
|
21
|
-
const FREE_TIER_WARNING = 16;
|
|
24
|
+
const FREE_TIER_WARNING = 40;
|
|
22
25
|
const TRIAL_EXTENSION_CALLS = 10;
|
|
23
26
|
const apiKeys = new Map();
|
|
24
|
-
const PLAN_LIMITS = { pro: 5000, enterprise: Infinity };
|
|
25
27
|
|
|
26
28
|
function saveStats() {
|
|
27
29
|
try {
|
|
@@ -56,27 +58,98 @@ function getEffectiveLimit(ip) {
|
|
|
56
58
|
return FREE_TIER_LIMIT;
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
const UPSTASH_URL = process.env.UPSTASH_REDIS_REST_URL;
|
|
62
|
+
const UPSTASH_TOKEN = process.env.UPSTASH_REDIS_REST_TOKEN;
|
|
63
|
+
|
|
64
|
+
async function redisGet(key) {
|
|
65
|
+
try {
|
|
66
|
+
const res = await fetch(
|
|
67
|
+
`${UPSTASH_URL}/get/${encodeURIComponent(key)}`,
|
|
68
|
+
{ headers: { Authorization: `Bearer ${UPSTASH_TOKEN}` } }
|
|
69
|
+
);
|
|
70
|
+
const data = await res.json();
|
|
71
|
+
if (!data.result) return null;
|
|
72
|
+
return JSON.parse(data.result);
|
|
73
|
+
} catch(e) { return null; }
|
|
61
74
|
}
|
|
62
75
|
|
|
63
|
-
function
|
|
76
|
+
async function redisSet(key, value) {
|
|
64
77
|
try {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
78
|
+
await fetch(
|
|
79
|
+
`${UPSTASH_URL}/set/${encodeURIComponent(key)}`,
|
|
80
|
+
{
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: {
|
|
83
|
+
Authorization: `Bearer ${UPSTASH_TOKEN}`,
|
|
84
|
+
'Content-Type': 'application/json'
|
|
85
|
+
},
|
|
86
|
+
body: JSON.stringify({ value: JSON.stringify(value) })
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
} catch(e) {}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function redisKeys(pattern) {
|
|
93
|
+
try {
|
|
94
|
+
const res = await fetch(
|
|
95
|
+
`${UPSTASH_URL}/keys/${encodeURIComponent(pattern)}`,
|
|
96
|
+
{ headers: { Authorization: `Bearer ${UPSTASH_TOKEN}` } }
|
|
97
|
+
);
|
|
98
|
+
const data = await res.json();
|
|
99
|
+
return data.result || [];
|
|
100
|
+
} catch(e) { return []; }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function saveKeyToRedis(apiKey, record, prefix) {
|
|
104
|
+
await redisSet(`${prefix}:key:${apiKey}`, record);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function loadApiKeysFromRedis(prefix) {
|
|
108
|
+
const keys = await redisKeys(`${prefix}:key:*`);
|
|
109
|
+
for (const redisKey of keys) {
|
|
110
|
+
const record = await redisGet(redisKey);
|
|
111
|
+
if (record) {
|
|
112
|
+
const apiKey = redisKey.replace(`${prefix}:key:`, '');
|
|
113
|
+
apiKeys.set(apiKey, record);
|
|
69
114
|
}
|
|
70
|
-
}
|
|
115
|
+
}
|
|
116
|
+
console.log(`Loaded ${apiKeys.size} API keys from Redis`);
|
|
71
117
|
}
|
|
72
118
|
|
|
73
119
|
function generateApiKey() { return 'vat_' + crypto.randomBytes(24).toString('hex'); }
|
|
74
|
-
function getPlanFromProduct(
|
|
75
|
-
if (!
|
|
76
|
-
|
|
120
|
+
function getPlanFromProduct(productName) {
|
|
121
|
+
if (!productName) return 'bundle_500';
|
|
122
|
+
const n = productName.toLowerCase();
|
|
123
|
+
if (n.includes('metered') || n.includes('pay as you go') || n === 'metered') return 'metered';
|
|
124
|
+
if (n.includes('2000') || n.includes('2,000') || n.includes('enterprise')) return 'bundle_2000';
|
|
125
|
+
return 'bundle_500';
|
|
77
126
|
}
|
|
78
127
|
function nowISO() { return new Date().toISOString(); }
|
|
79
128
|
|
|
129
|
+
function checkAndResetPeriod(record) {
|
|
130
|
+
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
|
|
131
|
+
if (Date.now() - record.periodStart > thirtyDays) {
|
|
132
|
+
record.calls = 0;
|
|
133
|
+
record.periodStart = Date.now();
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function reportMeteredUsage(customerId, eventName) {
|
|
140
|
+
try {
|
|
141
|
+
await stripe.billing.meterEvents.create({
|
|
142
|
+
event_name: eventName,
|
|
143
|
+
payload: {
|
|
144
|
+
stripe_customer_id: customerId,
|
|
145
|
+
value: '1'
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
} catch(e) {
|
|
149
|
+
console.error('Stripe metered usage report failed:', e.message);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
80
153
|
async function sendEmail(to, subject, html) {
|
|
81
154
|
return new Promise((resolve) => {
|
|
82
155
|
const body = JSON.stringify({ from: 'VAT Validator MCP <ojas@kordagencies.com>', to: [to], subject, html });
|
|
@@ -90,10 +163,10 @@ async function sendEmail(to, subject, html) {
|
|
|
90
163
|
}
|
|
91
164
|
|
|
92
165
|
async function sendApiKeyEmail(email, apiKey, plan) {
|
|
93
|
-
const planLabel = plan === '
|
|
94
|
-
const
|
|
95
|
-
const html = '<!DOCTYPE html><html><body style="font-family:monospace;background:#080A0F;color:#E8EDF5;padding:40px;max-width:600px;margin:0 auto"><div style="border:1px solid rgba(0,229,195,0.3);border-radius:8px;padding:32px"><div style="color:#00E5C3;font-size:13px;letter-spacing:0.2em;text-transform:uppercase;margin-bottom:24px">VAT Validator MCP - ' + planLabel + '
|
|
96
|
-
return sendEmail(email, 'Your VAT Validator MCP
|
|
166
|
+
const planLabel = plan === 'metered' ? 'Pay-as-you-go' : plan === 'bundle_2000' ? 'Bundle 2000' : 'Bundle 500';
|
|
167
|
+
const limitNote = plan === 'metered' ? 'Pay only for what you use — billed monthly' : plan === 'bundle_2000' ? '2,000 calls included' : '500 calls included';
|
|
168
|
+
const html = '<!DOCTYPE html><html><body style="font-family:monospace;background:#080A0F;color:#E8EDF5;padding:40px;max-width:600px;margin:0 auto"><div style="border:1px solid rgba(0,229,195,0.3);border-radius:8px;padding:32px"><div style="color:#00E5C3;font-size:13px;letter-spacing:0.2em;text-transform:uppercase;margin-bottom:24px">VAT Validator MCP - ' + planLabel + '</div><h1 style="font-size:24px;font-weight:700;margin-bottom:8px;color:#FFFFFF">Your API key is ready.</h1><div style="background:#141B24;border:1px solid rgba(255,255,255,0.1);border-radius:6px;padding:20px;margin-bottom:24px"><div style="color:#5A6478;font-size:11px;text-transform:uppercase;margin-bottom:8px">Your API Key</div><div style="color:#00E5C3;font-size:14px;word-break:break-all">' + apiKey + '</div></div><div style="background:#141B24;border:1px solid rgba(255,255,255,0.1);border-radius:6px;padding:20px;margin-bottom:24px"><div style="color:#5A6478;font-size:11px;text-transform:uppercase;margin-bottom:8px">MCP Config</div><div style="color:#86EFAC;font-size:12px">{"vat-validator":{"url":"https://vat-validator-mcp-production.up.railway.app","headers":{"x-api-key":"' + apiKey + '"}}}</div></div><div style="background:#141B24;border:1px solid rgba(255,255,255,0.1);border-radius:6px;padding:20px;margin-bottom:24px"><div style="color:#E8EDF5;font-size:13px">Plan: ' + planLabel + '<br>' + limitNote + '</div></div><div style="background:#0D1219;border-radius:6px;padding:16px;margin-bottom:24px;font-size:11px;color:#5A6478;line-height:1.7">Results are informational only. Verify with a qualified tax advisor. Liability capped at 3 months fees. Full terms: kordagencies.com/terms.html</div><p style="color:#5A6478;font-size:12px">Questions? ojas@kordagencies.com</p></div></body></html>';
|
|
169
|
+
return sendEmail(email, 'Your VAT Validator MCP API Key — ' + planLabel, html);
|
|
97
170
|
}
|
|
98
171
|
|
|
99
172
|
async function callClaude(prompt) {
|
|
@@ -352,19 +425,69 @@ function checkAccess(req) {
|
|
|
352
425
|
const apiKey = req.headers['x-api-key'];
|
|
353
426
|
if (apiKey) {
|
|
354
427
|
const record = apiKeys.get(apiKey);
|
|
355
|
-
if (!record)
|
|
356
|
-
|
|
428
|
+
if (!record) {
|
|
429
|
+
return { allowed: false, error: 'Invalid API key' };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const wasReset = checkAndResetPeriod(record);
|
|
433
|
+
if (wasReset) {
|
|
434
|
+
saveKeyToRedis(apiKey, record, REDIS_PREFIX).catch(() => {});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (record.plan === 'metered') {
|
|
438
|
+
record.calls++;
|
|
439
|
+
saveKeyToRedis(apiKey, record, REDIS_PREFIX).catch(() => {});
|
|
440
|
+
return {
|
|
441
|
+
allowed: true,
|
|
442
|
+
paid: true,
|
|
443
|
+
plan: 'metered',
|
|
444
|
+
stripeCustomerId: record.stripeCustomerId
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (record.calls >= record.limit) {
|
|
449
|
+
return {
|
|
450
|
+
allowed: false,
|
|
451
|
+
error: `Bundle exhausted. You have used all ${record.limit} calls in this bundle. Purchase another bundle or switch to pay-as-you-go.`,
|
|
452
|
+
subscribe_url: METERED_SUBSCRIBE_URL,
|
|
453
|
+
bundle_500_url: BUNDLE_500_URL,
|
|
454
|
+
bundle_2000_url: BUNDLE_2000_URL,
|
|
455
|
+
agent_action: 'PAUSE_AND_NOTIFY_USER'
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
357
459
|
record.calls++;
|
|
358
|
-
|
|
460
|
+
saveKeyToRedis(apiKey, record, REDIS_PREFIX).catch(() => {});
|
|
461
|
+
return { allowed: true, paid: true, plan: record.plan };
|
|
359
462
|
}
|
|
360
463
|
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
361
464
|
const monthKey = getMonthKey(ip);
|
|
362
465
|
const calls = freeTierUsage.get(monthKey) || 0;
|
|
363
|
-
if (calls >= FREE_TIER_LIMIT) return {
|
|
466
|
+
if (calls >= FREE_TIER_LIMIT) return {
|
|
467
|
+
allowed: false,
|
|
468
|
+
error: 'Free tier limit of 50 calls/month reached.',
|
|
469
|
+
options: {
|
|
470
|
+
pay_as_you_go: {
|
|
471
|
+
description: 'No commitment. Pay only for what you use. Billed monthly at end of period.',
|
|
472
|
+
pricing: {
|
|
473
|
+
vat_query: '$0.010 per query'
|
|
474
|
+
},
|
|
475
|
+
subscribe_url: METERED_SUBSCRIBE_URL
|
|
476
|
+
},
|
|
477
|
+
bundle: {
|
|
478
|
+
description: 'Buy a fixed call bundle. No subscription.',
|
|
479
|
+
options: [
|
|
480
|
+
{ calls: 500, price: '$8', url: BUNDLE_500_URL },
|
|
481
|
+
{ calls: 2000, price: '$28', url: BUNDLE_2000_URL }
|
|
482
|
+
]
|
|
483
|
+
}
|
|
484
|
+
},
|
|
485
|
+
agent_action: 'PAUSE_AND_NOTIFY_USER'
|
|
486
|
+
};
|
|
364
487
|
freeTierUsage.set(monthKey, calls + 1);
|
|
365
488
|
saveStats();
|
|
366
489
|
const remaining = FREE_TIER_LIMIT - calls - 1;
|
|
367
|
-
const warningMsg = remaining <
|
|
490
|
+
const warningMsg = remaining < 10 ? remaining + ' free validations remaining this month. Get 500 calls for $8 at ' + BUNDLE_500_URL + ' -- calls never expire.' : null;
|
|
368
491
|
return { allowed: true, tier: 'free', remaining, warning: warningMsg };
|
|
369
492
|
}
|
|
370
493
|
|
|
@@ -400,16 +523,35 @@ async function handleStripeWebhook(body, sig) {
|
|
|
400
523
|
const event = JSON.parse(body);
|
|
401
524
|
if (event.type === 'checkout.session.completed') {
|
|
402
525
|
const session = event.data.object;
|
|
403
|
-
const
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
526
|
+
const plan = getPlanFromProduct(session.metadata?.product_name);
|
|
527
|
+
const apiKey = generateApiKey();
|
|
528
|
+
const limit = plan === 'metered' ? null : plan === 'bundle_2000' ? 2000 : 500;
|
|
529
|
+
const record = {
|
|
530
|
+
email: session.customer_details?.email || 'unknown',
|
|
531
|
+
plan,
|
|
532
|
+
calls: 0,
|
|
533
|
+
periodStart: Date.now(),
|
|
534
|
+
limit,
|
|
535
|
+
stripeCustomerId: session.customer || null,
|
|
536
|
+
createdAt: Date.now()
|
|
537
|
+
};
|
|
538
|
+
apiKeys.set(apiKey, record);
|
|
539
|
+
await saveKeyToRedis(apiKey, record, REDIS_PREFIX);
|
|
540
|
+
await sendApiKeyEmail(record.email, apiKey, plan);
|
|
541
|
+
console.log('[vat] API key created for ' + record.email + ' (' + plan + ')');
|
|
542
|
+
return { success: true, email: record.email, plan };
|
|
543
|
+
}
|
|
544
|
+
if (event.type === 'customer.subscription.created') {
|
|
545
|
+
const sub = event.data.object;
|
|
546
|
+
const customerId = sub.customer;
|
|
547
|
+
for (const [key, record] of apiKeys.entries()) {
|
|
548
|
+
if (record.stripeCustomerId === customerId && !record.subscriptionId) {
|
|
549
|
+
record.subscriptionId = sub.id;
|
|
550
|
+
await saveKeyToRedis(key, record, REDIS_PREFIX);
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
412
553
|
}
|
|
554
|
+
return { received: true, type: event.type };
|
|
413
555
|
}
|
|
414
556
|
return { received: true, type: event.type };
|
|
415
557
|
} catch(e) { console.error('[vat] Webhook error:', e.message); return { error: e.message, status: 400 }; }
|
|
@@ -431,7 +573,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
431
573
|
|
|
432
574
|
if (req.url === '/health' && (req.method === 'GET' || req.method === 'HEAD')) {
|
|
433
575
|
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
434
|
-
res.end(JSON.stringify({ status: 'ok', version: VERSION, service: 'vat-validator-mcp', free_tier: 'no API key required for first
|
|
576
|
+
res.end(JSON.stringify({ status: 'ok', version: VERSION, service: 'vat-validator-mcp', free_tier: 'no API key required for first ' + FREE_TIER_LIMIT + ' calls/month', paid_keys_issued: apiKeys.size }));
|
|
435
577
|
return;
|
|
436
578
|
}
|
|
437
579
|
|
|
@@ -480,7 +622,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
480
622
|
const { name, email, use_case } = JSON.parse(body);
|
|
481
623
|
if (!name || !email) { res.writeHead(400, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'name and email are required', agent_action: 'PROVIDE_REQUIRED_FIELDS' })); return; }
|
|
482
624
|
const emailKey = 'trial:' + email.toLowerCase().trim();
|
|
483
|
-
if (trialExtensions.has(emailKey)) { res.writeHead(409, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Trial extension already granted for this email.',
|
|
625
|
+
if (trialExtensions.has(emailKey)) { res.writeHead(409, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Trial extension already granted for this email.', bundle_url: BUNDLE_500_URL, agent_action: 'INFORM_USER_TRIAL_ALREADY_USED' })); return; }
|
|
484
626
|
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
485
627
|
const monthKey = getMonthKey(ip);
|
|
486
628
|
const currentCalls = freeTierUsage.get(monthKey) || 0;
|
|
@@ -490,9 +632,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
490
632
|
await sendEmail('ojas@kordagencies.com', 'VAT Validator -- Trial Extension: ' + name,
|
|
491
633
|
'<p><b>Name:</b> ' + name + '<br><b>Email:</b> ' + email + '<br><b>Use case:</b> ' + (use_case || 'Not provided') + '<br><b>IP:</b> ' + ip + '<br><b>Calls granted:</b> ' + TRIAL_EXTENSION_CALLS + '</p>');
|
|
492
634
|
await sendEmail(email, TRIAL_EXTENSION_CALLS + ' extra free calls added -- VAT Validator MCP',
|
|
493
|
-
'<p>Hi ' + name + ',</p><p>Your ' + TRIAL_EXTENSION_CALLS + ' extra free calls have been added. You can keep using VAT Validator MCP right now -- no action needed.</p><p>When you need more,
|
|
635
|
+
'<p>Hi ' + name + ',</p><p>Your ' + TRIAL_EXTENSION_CALLS + ' extra free calls have been added. You can keep using VAT Validator MCP right now -- no action needed.</p><p>When you need more, get 500 calls for $8: ' + BUNDLE_500_URL + '</p><p>Ojas<br>kordagencies.com</p>');
|
|
494
636
|
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
495
|
-
res.end(JSON.stringify({ granted: true, additional_calls: TRIAL_EXTENSION_CALLS, message: TRIAL_EXTENSION_CALLS + ' extra free calls added. Check your email for confirmation.',
|
|
637
|
+
res.end(JSON.stringify({ granted: true, additional_calls: TRIAL_EXTENSION_CALLS, message: TRIAL_EXTENSION_CALLS + ' extra free calls added. Check your email for confirmation.', bundle_url: BUNDLE_500_URL }));
|
|
496
638
|
} catch(e) { res.writeHead(400, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: e.message, agent_action: 'RETRY_IN_2_MIN' })); }
|
|
497
639
|
});
|
|
498
640
|
return;
|
|
@@ -556,15 +698,18 @@ const server = http.createServer(async (req, res) => {
|
|
|
556
698
|
} else if (request.method === 'tools/call') {
|
|
557
699
|
const access = checkAccess(req);
|
|
558
700
|
if (!access.allowed) {
|
|
559
|
-
response = { jsonrpc: '2.0', id: request.id, error: { code: -32000, message: access.
|
|
701
|
+
response = { jsonrpc: '2.0', id: request.id, error: { code: -32000, message: access.error || 'Access denied', data: access, agent_action: 'PAUSE_AND_NOTIFY_USER' } };
|
|
560
702
|
} else {
|
|
561
703
|
const { name, arguments: args } = request.params;
|
|
562
704
|
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
563
|
-
usageLog.push({ tool: name, tier: access.tier, time: new Date().toISOString(), ip: ip.slice(0, 8) + '...' });
|
|
705
|
+
usageLog.push({ tool: name, tier: access.tier || access.plan || 'paid', time: new Date().toISOString(), ip: ip.slice(0, 8) + '...' });
|
|
564
706
|
if (usageLog.length > 1000) usageLog.shift();
|
|
565
707
|
toolUsageCounts[name] = (toolUsageCounts[name] || 0) + 1;
|
|
566
708
|
saveStats();
|
|
567
709
|
const result = await executeTool(name, args || {});
|
|
710
|
+
if (access.plan === 'metered' && access.stripeCustomerId) {
|
|
711
|
+
reportMeteredUsage(access.stripeCustomerId, 'vat_query').catch(() => {});
|
|
712
|
+
}
|
|
568
713
|
response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } };
|
|
569
714
|
}
|
|
570
715
|
} else {
|
|
@@ -587,16 +732,15 @@ const server = http.createServer(async (req, res) => {
|
|
|
587
732
|
const request = JSON.parse(body);
|
|
588
733
|
let response;
|
|
589
734
|
if (request.method === 'tools/call') {
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
} else {
|
|
596
|
-
const access = checkAccess(req);
|
|
597
|
-
if (!access.allowed) { res.writeHead(429, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, error: { code: -32000, message: access.reason, upgrade_url: PRO_UPGRADE_URL, agent_action: 'Inform user free tier quota is exhausted. Get 500 calls for $8 at ' + PRO_UPGRADE_URL + ' -- calls never expire.' } })); return; }
|
|
598
|
-
req._accessWarning = access.warning; req._tier = access.tier;
|
|
735
|
+
const access = checkAccess(req);
|
|
736
|
+
if (!access.allowed) {
|
|
737
|
+
res.writeHead(429, { ...cors, 'Content-Type': 'application/json' });
|
|
738
|
+
res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, error: { code: -32000, message: access.error || 'Access denied', data: access, agent_action: 'PAUSE_AND_NOTIFY_USER' } }));
|
|
739
|
+
return;
|
|
599
740
|
}
|
|
741
|
+
req._accessWarning = access.warning;
|
|
742
|
+
req._tier = access.tier;
|
|
743
|
+
req._accessResult = access;
|
|
600
744
|
}
|
|
601
745
|
if (request.method === 'initialize') { response = { jsonrpc: '2.0', id: request.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, resources: {}, prompts: {} }, serverInfo: { name: 'vat-validator-mcp', version: VERSION, description: 'Every accounts-payable pipeline reaches a moment where an agent must validate a VAT registration or approve an invoice without being able to reason its way to a reliable answer. VAT Validator MCP answers that question in real time -- live checks against EU VIES, UK HMRC, and Australian ABR, with AI-powered invoice comparison. An agent acting on stale VAT data has no defence against a tax authority. Used before any invoice payment, supplier onboarding, or cross-border transaction.' } } };
|
|
602
746
|
} else if (request.method === 'notifications/initialized') { res.writeHead(204, cors); res.end(); return;
|
|
@@ -606,46 +750,46 @@ const server = http.createServer(async (req, res) => {
|
|
|
606
750
|
} else if (request.method === 'tools/call') {
|
|
607
751
|
const { name, arguments: toolArgs } = request.params;
|
|
608
752
|
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
609
|
-
usageLog.push({ tool: name, tier: req._tier || 'paid', time: new Date().toISOString(), ip: ip.slice(0, 8) + '...' });
|
|
753
|
+
usageLog.push({ tool: name, tier: req._tier || req._accessResult?.plan || 'paid', time: new Date().toISOString(), ip: ip.slice(0, 8) + '...' });
|
|
610
754
|
if (usageLog.length > 1000) usageLog.shift();
|
|
611
755
|
toolUsageCounts[name] = (toolUsageCounts[name] || 0) + 1;
|
|
612
756
|
saveStats();
|
|
613
757
|
const result = await executeTool(name, toolArgs || {});
|
|
614
758
|
if (req._accessWarning) result._notice = req._accessWarning;
|
|
615
759
|
|
|
760
|
+
if (req._accessResult && req._accessResult.plan === 'metered' && req._accessResult.stripeCustomerId) {
|
|
761
|
+
reportMeteredUsage(req._accessResult.stripeCustomerId, 'vat_query').catch(() => {});
|
|
762
|
+
}
|
|
763
|
+
|
|
616
764
|
// Partial response for free tier
|
|
617
765
|
if (req._tier === 'free' && !result.error) {
|
|
618
|
-
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
619
766
|
const used = freeTierUsage.get(getMonthKey(ip)) || 0;
|
|
620
767
|
const remaining = FREE_TIER_LIMIT - used;
|
|
621
768
|
const isWarning = used >= FREE_TIER_WARNING;
|
|
622
769
|
const effectiveLimit = getEffectiveLimit(ip);
|
|
623
770
|
|
|
624
771
|
if (name === 'validate_vat' || name === 'validate_uk_vat') {
|
|
625
|
-
// Gate address on free tier — company name + valid status visible
|
|
626
772
|
const gated = ['registered_address', 'address', 'consultation_number'];
|
|
627
773
|
gated.forEach(f => delete result[f]);
|
|
628
|
-
result._upgrade_note = 'Free tier: ' + remaining + ' of ' + effectiveLimit + ' calls remaining. Get 500 calls for $8 at ' +
|
|
774
|
+
result._upgrade_note = 'Free tier: ' + remaining + ' of ' + effectiveLimit + ' calls remaining. Get 500 calls for $8 at ' + BUNDLE_500_URL + ' -- calls never expire. Includes full registered address and HMRC consultation number.';
|
|
629
775
|
result._gated_fields = gated;
|
|
630
776
|
}
|
|
631
777
|
|
|
632
778
|
if (name === 'analyse_vat_risk') {
|
|
633
|
-
// Gate full reasoning — verdict visible, details gated
|
|
634
779
|
const gated = ['fraud_signals', 'positive_indicators', 'recommended_action', 'summary'];
|
|
635
780
|
gated.forEach(f => delete result[f]);
|
|
636
|
-
result._upgrade_note = 'Free tier: ' + remaining + ' of ' + effectiveLimit + ' calls remaining. Get 500 calls for $8 at ' +
|
|
781
|
+
result._upgrade_note = 'Free tier: ' + remaining + ' of ' + effectiveLimit + ' calls remaining. Get 500 calls for $8 at ' + BUNDLE_500_URL + ' -- calls never expire. Includes full fraud signal breakdown, positive indicators, and recommended action.';
|
|
637
782
|
result._gated_fields = gated;
|
|
638
783
|
}
|
|
639
784
|
|
|
640
785
|
if (name === 'compare_invoice_details') {
|
|
641
|
-
// Gate detail fields — match_status visible, discrepancies gated
|
|
642
786
|
const gated = ['discrepancies', 'name_match', 'address_match', 'recommended_action', 'summary'];
|
|
643
787
|
gated.forEach(f => delete result[f]);
|
|
644
|
-
result._upgrade_note = 'Free tier: ' + remaining + ' of ' + effectiveLimit + ' calls remaining. Get 500 calls for $8 at ' +
|
|
788
|
+
result._upgrade_note = 'Free tier: ' + remaining + ' of ' + effectiveLimit + ' calls remaining. Get 500 calls for $8 at ' + BUNDLE_500_URL + ' -- calls never expire. Includes full discrepancy analysis and recommended action.';
|
|
645
789
|
result._gated_fields = gated;
|
|
646
790
|
}
|
|
647
791
|
|
|
648
|
-
if (isWarning) result._notice = 'Warning: only ' + remaining + ' free call' + (remaining === 1 ? '' : 's') + ' left this month. Get 500 calls for $8 at ' +
|
|
792
|
+
if (isWarning) result._notice = 'Warning: only ' + remaining + ' free call' + (remaining === 1 ? '' : 's') + ' left this month. Get 500 calls for $8 at ' + BUNDLE_500_URL + ' -- calls never expire.';
|
|
649
793
|
}
|
|
650
794
|
|
|
651
795
|
response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } };
|
|
@@ -657,7 +801,57 @@ const server = http.createServer(async (req, res) => {
|
|
|
657
801
|
return;
|
|
658
802
|
}
|
|
659
803
|
|
|
660
|
-
if (req.method === 'GET' && req.url === '/') { res.writeHead(200, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ name: 'vat-validator-mcp', version: VERSION, status: 'ok', tools: 6, free_tier: '
|
|
804
|
+
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: '50 calls/month, no API key required', description: 'VAT validation + AI fraud detection. EU VIES, UK HMRC, Australian ABN.', subscribe_url: METERED_SUBSCRIBE_URL, bundle_500_url: BUNDLE_500_URL, bundle_2000_url: BUNDLE_2000_URL })); return; }
|
|
805
|
+
|
|
806
|
+
if (req.url === '/subscribe' && req.method === 'GET') {
|
|
807
|
+
try {
|
|
808
|
+
const session = await stripe.checkout.sessions.create({
|
|
809
|
+
mode: 'subscription',
|
|
810
|
+
line_items: [
|
|
811
|
+
{ price: 'price_1TUkxWD6WvRe6sn3eFTaokqx' }
|
|
812
|
+
],
|
|
813
|
+
success_url: 'https://vat-validator-mcp-production.up.railway.app/subscribed',
|
|
814
|
+
cancel_url: 'https://kordagencies.com/vat-validator.html',
|
|
815
|
+
metadata: { product_name: 'metered' }
|
|
816
|
+
});
|
|
817
|
+
res.writeHead(302, { Location: session.url });
|
|
818
|
+
res.end();
|
|
819
|
+
} catch(e) {
|
|
820
|
+
res.writeHead(500, { ...cors, 'Content-Type': 'application/json' });
|
|
821
|
+
res.end(JSON.stringify({ error: 'Could not create checkout session', details: e.message }));
|
|
822
|
+
}
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (req.url === '/subscribed' && req.method === 'GET') {
|
|
827
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
828
|
+
res.end(`<!DOCTYPE html>
|
|
829
|
+
<html>
|
|
830
|
+
<head>
|
|
831
|
+
<meta charset="UTF-8">
|
|
832
|
+
<title>Subscription confirmed</title>
|
|
833
|
+
<style>
|
|
834
|
+
body{background:#070910;color:#00E5C3;
|
|
835
|
+
font-family:'DM Mono',monospace;padding:3rem;
|
|
836
|
+
max-width:600px;margin:0 auto}
|
|
837
|
+
h2{font-weight:400;margin-bottom:1rem}
|
|
838
|
+
p{color:#8895AA;font-size:13px;line-height:1.6;
|
|
839
|
+
margin-bottom:0.8rem}
|
|
840
|
+
a{color:#00E5C3}
|
|
841
|
+
</style>
|
|
842
|
+
</head>
|
|
843
|
+
<body>
|
|
844
|
+
<h2>Subscription confirmed.</h2>
|
|
845
|
+
<p>Your API key will arrive by email within 60 seconds.</p>
|
|
846
|
+
<p>Add it to your agent config as the
|
|
847
|
+
<span style="color:#fff">x-api-key</span> header.</p>
|
|
848
|
+
<p>Full documentation at
|
|
849
|
+
<a href="https://kordagencies.com">kordagencies.com</a></p>
|
|
850
|
+
</body>
|
|
851
|
+
</html>`);
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
|
|
661
855
|
res.writeHead(404, cors); res.end(JSON.stringify({ error: 'Not found' }));
|
|
662
856
|
});
|
|
663
857
|
|
|
@@ -702,12 +896,14 @@ function setupStdio() {
|
|
|
702
896
|
|
|
703
897
|
setupStdio();
|
|
704
898
|
|
|
705
|
-
server.listen(PORT, () => {
|
|
899
|
+
server.listen(PORT, async () => {
|
|
706
900
|
loadStats();
|
|
707
|
-
|
|
901
|
+
await loadApiKeysFromRedis('vat');
|
|
708
902
|
console.log('VAT Validator MCP v' + VERSION + ' running on port ' + PORT);
|
|
709
903
|
console.log('Free tier: ' + FREE_TIER_LIMIT + ' calls/IP/month, no API key required');
|
|
710
904
|
console.log('Resend: ' + (RESEND_API_KEY ? 'configured' : 'MISSING'));
|
|
711
905
|
console.log('Anthropic: ' + (ANTHROPIC_API_KEY ? 'configured' : 'MISSING'));
|
|
712
906
|
console.log('ABR GUID: ' + (process.env.ABR_GUID ? 'custom GUID set' : 'using fallback demo GUID — set ABR_GUID env var'));
|
|
907
|
+
console.log('Upstash Redis: ' + (UPSTASH_URL ? 'configured' : 'MISSING - set UPSTASH_REDIS_REST_URL'));
|
|
908
|
+
console.log('Stripe: ' + (process.env.STRIPE_SECRET_KEY ? 'configured' : 'MISSING - set STRIPE_SECRET_KEY'));
|
|
713
909
|
});
|