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 CHANGED
@@ -1,19 +1,45 @@
1
1
  {
2
- "name": "vat-validator-mcp",
3
- "title": "VAT Validator MCP",
4
- "description": "Validate EU, UK, and Australian VAT numbers for AI agents. EU VIES, UK HMRC, Australian ABN. Required for EU ViDA e-invoicing compliance.",
5
- "version": "1.0.0",
6
- "homepage": "https://kordagencies.com",
7
- "license": "UNLICENSED",
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
- { "name": "validate_vat", "description": "Validate any EU, UK, or Australian VAT number" },
10
- { "name": "validate_uk_vat", "description": "Validate UK VAT number against HMRC with consultation number" },
11
- { "name": "get_vat_rates", "description": "Get VAT rates by country for EU, UK, Australia" },
12
- { "name": "batch_validate", "description": "Validate up to 10 VAT numbers in one call (paid)" }
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
- "pricing": {
15
- "free": "20 validations/month, no API key",
16
- "pro": "$99/month — 5,000 validations/month",
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.12",
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.12",
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.12",
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 API_KEYS_FILE = '/tmp/vat_apikeys.json';
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 FREE_TIER_LIMIT = 20;
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
- function saveApiKeys() {
60
- try { fs.writeFileSync(API_KEYS_FILE, JSON.stringify(Array.from(apiKeys.entries()))); } catch(e) { console.error('API keys save error:', e.message); }
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 loadApiKeys() {
76
+ async function redisSet(key, value) {
64
77
  try {
65
- if (fs.existsSync(API_KEYS_FILE)) {
66
- const entries = JSON.parse(fs.readFileSync(API_KEYS_FILE, 'utf8'));
67
- entries.forEach(([k, v]) => apiKeys.set(k, v));
68
- console.log('API keys loaded: ' + apiKeys.size + ' keys');
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
- } catch(e) { console.error('API keys load error:', e.message); }
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(name) {
75
- if (!name) return 'pro';
76
- return name.toLowerCase().includes('enterprise') ? 'enterprise' : 'pro';
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 === 'enterprise' ? 'Enterprise' : 'Pro';
94
- const limit = plan === 'enterprise' ? 'Unlimited' : '5,000';
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 + ' 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>';
96
- return sendEmail(email, 'Your VAT Validator MCP ' + planLabel + ' API Key', html);
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) return { allowed: false, reason: 'Invalid API key. Get yours at kordagencies.com', tier: 'invalid' };
356
- 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' };
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
- return { allowed: true, tier: record.plan, record };
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 { allowed: false, reason: 'Free tier limit of ' + FREE_TIER_LIMIT + ' calls/month reached. Option 1: POST /trial-extension with {"name":"...","email":"...","use_case":"..."} for 10 extra free calls. Option 2: Upgrade to Pro at ' + PRO_UPGRADE_URL + ' (500 calls, never expire).', upgrade_url: PRO_UPGRADE_URL, trial_extension: { endpoint: '/trial-extension', method: 'POST', body: { name: 'string', email: 'string', use_case: 'string' } }, tier: 'free_limit_reached' };
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 < 5 ? remaining + ' free validations remaining this month. Need more? POST /trial-extension with your email for 10 extra free calls, or upgrade at ' + PRO_UPGRADE_URL + ' (500 calls, never expire).' : null;
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 email = session.customer_email || session.customer_details?.email;
404
- const plan = getPlanFromProduct(session.metadata?.product_name || '');
405
- if (email) {
406
- const apiKey = generateApiKey();
407
- apiKeys.set(apiKey, { email, plan, createdAt: new Date().toISOString(), calls: 0, limit: PLAN_LIMITS[plan] });
408
- saveApiKeys();
409
- await sendApiKeyEmail(email, apiKey, plan);
410
- console.log('[vat] API key created for ' + email + ' (' + plan + ')');
411
- return { success: true, email, plan };
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 20 calls/month', paid_keys_issued: apiKeys.size }));
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.', upgrade_url: PRO_UPGRADE_URL, agent_action: 'INFORM_USER_TRIAL_ALREADY_USED' })); return; }
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, Pro is $8/month for 500 calls (never expire): ' + PRO_UPGRADE_URL + '</p><p>Ojas<br>kordagencies.com</p>');
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.', upgrade_url: PRO_UPGRADE_URL }));
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.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.' } };
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
- if (request.params?.name === 'batch_validate') {
591
- const apiKey = req.headers['x-api-key'];
592
- 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; }
593
- const record = apiKeys.get(apiKey);
594
- 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; }
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 ' + PRO_UPGRADE_URL + ' -- calls never expire. Includes full registered address and HMRC consultation number.';
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 ' + PRO_UPGRADE_URL + ' -- calls never expire. Includes full fraud signal breakdown, positive indicators, and recommended action.';
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 ' + PRO_UPGRADE_URL + ' -- calls never expire. Includes full discrepancy analysis and recommended action.';
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 ' + PRO_UPGRADE_URL + ' -- calls never expire.';
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: '20 calls/month, no API key required', description: 'VAT validation + AI fraud detection. EU VIES, UK HMRC, Australian ABN.', upgrade: PRO_UPGRADE_URL })); return; }
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
- loadApiKeys();
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
  });