vat-validator-mcp 2.0.3 → 2.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,12 +28,12 @@ sources directly:
28
28
 
29
29
  | Tool | Free Tier | Use When |
30
30
  |---|---|---|
31
- | validate_vat | 20/month | Before approving any EU supplier or invoice |
32
- | validate_uk_vat | 20/month | Before approving any UK supplier or invoice |
33
- | get_vat_rates | 20/month | Before calculating cross-border invoice totals |
31
+ | validate_vat | 50/month | Before approving any EU supplier or invoice |
32
+ | validate_uk_vat | 50/month | Before approving any UK supplier or invoice |
33
+ | get_vat_rates | 50/month | Before calculating cross-border invoice totals |
34
34
  | batch_validate | Paid | Validating a supplier list or invoice batch |
35
- | analyse_vat_risk | 20/month | Before approving any high-value cross-border invoice |
36
- | compare_invoice_details | 20/month | Before authorising payment on any supplier invoice |
35
+ | analyse_vat_risk | 50/month | Before approving any high-value cross-border invoice |
36
+ | compare_invoice_details | 50/month | Before authorising payment on any supplier invoice |
37
37
 
38
38
  ## Add to Your Agent
39
39
 
@@ -65,9 +65,10 @@ mcp_server = MCPServerSse(
65
65
 
66
66
  | Tier | Calls | Price |
67
67
  |---|---|---|
68
- | Free | 20/month | No card required |
69
- | Pro | Unlimited | $39/month |
70
- | Enterprise | Unlimited + priority | $199/month |
68
+ | Free | 50/month | No card required |
69
+ | Pay-as-you-go | Unlimited | $0.010/query, billed monthly |
70
+ | Bundle 500 | 500 calls, never expire | $8 |
71
+ | Bundle 2000 | 2000 calls, never expire | $28 |
71
72
 
72
73
  Upgrade: https://kordagencies.com
73
74
 
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": "2.0.3",
4
+ "version": "2.0.9",
5
5
  "description": "VAT number validator for AI agents. EU VIES, UK HMRC, AU ABR — auto-detects jurisdiction. Fraud risk scoring and invoice name cross-check in one call.",
6
6
  "main": "src/server.js",
7
7
  "scripts": {
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": "2.0.2",
6
+ "version": "2.0.4",
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": "2.0.2",
16
+ "version": "2.0.4",
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
@@ -7,7 +7,7 @@ const Stripe = require('stripe');
7
7
  const stripe = Stripe(process.env.STRIPE_SECRET_KEY);
8
8
 
9
9
  const PERSIST_FILE = '/tmp/vat_stats.json';
10
- const VERSION = '2.0.3';
10
+ const VERSION = '2.0.9';
11
11
 
12
12
  // Persistent device ID for HMRC fraud prevention headers (BATCH_PROCESS_DIRECT)
13
13
  const DEVICE_ID_FILE = path.join(__dirname, '..', 'device-id.txt');
@@ -23,6 +23,7 @@ const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || '';
23
23
  const PORT = process.env.PORT || 3000;
24
24
  const STATS_KEY = process.env.STATS_KEY || 'ojas2026';
25
25
  const REDIS_PREFIX = 'vat';
26
+ const FREE_TIER_REDIS_KEY = 'vat:free_tier_usage';
26
27
  const FREE_TIER_LIMIT = 50;
27
28
  const METERED_SUBSCRIBE_URL = 'https://vat-validator-mcp-production.up.railway.app/subscribe';
28
29
  const BUNDLE_500_URL = 'https://buy.stripe.com/28EeVceUB06N1ty3teebu0l';
@@ -79,6 +80,7 @@ async function redisGet(key) {
79
80
  { headers: { Authorization: `Bearer ${UPSTASH_TOKEN}` } }
80
81
  );
81
82
  const data = await res.json();
83
+ if (data.error) console.error('[Redis] redisGet error:', data.error, 'key:', key);
82
84
  if (!data.result) return null;
83
85
  return JSON.parse(data.result);
84
86
  } catch(e) { return null; }
@@ -86,18 +88,36 @@ async function redisGet(key) {
86
88
 
87
89
  async function redisSet(key, value) {
88
90
  try {
89
- await fetch(
90
- `${UPSTASH_URL}/set/${encodeURIComponent(key)}`,
91
- {
92
- method: 'POST',
93
- headers: {
94
- Authorization: `Bearer ${UPSTASH_TOKEN}`,
95
- 'Content-Type': 'application/json'
96
- },
97
- body: JSON.stringify({ value: JSON.stringify(value) })
98
- }
91
+ const res = await fetch(`${process.env.UPSTASH_REDIS_REST_URL}/set/${encodeURIComponent(key)}/${encodeURIComponent(JSON.stringify(value))}`, {
92
+ method: 'GET',
93
+ headers: { Authorization: `Bearer ${process.env.UPSTASH_REDIS_REST_TOKEN}` }
94
+ });
95
+ const data = await res.json();
96
+ if (data.error) console.error('[Redis] redisSet error:', data.error, 'key:', key);
97
+ } catch(e) { console.error('[Redis] redisSet failed:', e); }
98
+ }
99
+
100
+ async function redisExpire(key, seconds) {
101
+ try {
102
+ const res = await fetch(
103
+ `${UPSTASH_URL}/expire/${encodeURIComponent(key)}/${seconds}`,
104
+ { method: 'POST', headers: { Authorization: `Bearer ${UPSTASH_TOKEN}` } }
99
105
  );
100
- } catch(e) {}
106
+ const data = await res.json();
107
+ if (data.error) console.error('[Redis] redisExpire error:', data.error, 'key:', key);
108
+ } catch(e) { console.error('[Redis] redisExpire failed:', e); }
109
+ }
110
+
111
+ async function appendSessionLog(ip, tool) {
112
+ try {
113
+ const ipSafe = ip.replace(/:/g, '_').replace(/\s/g, '');
114
+ const dayKey = new Date().toISOString().slice(0, 10);
115
+ const key = `${REDIS_PREFIX}:session:${ipSafe}:${dayKey}`;
116
+ const existing = await redisGet(key) || [];
117
+ existing.push({ tool, timestamp: new Date().toISOString() });
118
+ await redisSet(key, existing);
119
+ await redisExpire(key, 86400);
120
+ } catch(e) { console.error('[SessionLog] internal error:', e); }
101
121
  }
102
122
 
103
123
  async function redisKeys(pattern) {
@@ -107,6 +127,7 @@ async function redisKeys(pattern) {
107
127
  { headers: { Authorization: `Bearer ${UPSTASH_TOKEN}` } }
108
128
  );
109
129
  const data = await res.json();
130
+ if (data.error) console.error('[Redis] redisKeys error:', data.error, 'pattern:', pattern);
110
131
  return data.result || [];
111
132
  } catch(e) { return []; }
112
133
  }
@@ -127,6 +148,22 @@ async function loadApiKeysFromRedis(prefix) {
127
148
  console.log(`Loaded ${apiKeys.size} API keys from Redis`);
128
149
  }
129
150
 
151
+ async function loadFreeTierFromRedis() {
152
+ try {
153
+ const data = await redisGet(FREE_TIER_REDIS_KEY);
154
+ if (data && Array.isArray(data)) {
155
+ data.forEach(([k, v]) => freeTierUsage.set(k, v));
156
+ console.log('[FreeTier] Loaded ' + freeTierUsage.size + ' IPs from Redis');
157
+ }
158
+ } catch(e) { console.error('[FreeTier] load failed:', e); }
159
+ }
160
+
161
+ async function saveFreeTierToRedis() {
162
+ try {
163
+ await redisSet(FREE_TIER_REDIS_KEY, Array.from(freeTierUsage.entries()));
164
+ } catch(e) { console.error('[FreeTier] save failed:', e); }
165
+ }
166
+
130
167
  function generateApiKey() { return 'vat_' + crypto.randomBytes(24).toString('hex'); }
131
168
  function getPlanFromProduct(productName) {
132
169
  if (!productName) return 'bundle_500';
@@ -579,7 +616,8 @@ function checkAccess(req) {
579
616
  saveKeyToRedis(apiKey, record, REDIS_PREFIX).catch(() => {});
580
617
  return { allowed: true, paid: true, plan: record.plan };
581
618
  }
582
- const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
619
+ const rawIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
620
+ const ip = rawIp.split(',')[0].trim();
583
621
  const monthKey = getMonthKey(ip);
584
622
  const calls = freeTierUsage.get(monthKey) || 0;
585
623
  if (calls >= FREE_TIER_LIMIT) return {
@@ -605,6 +643,7 @@ function checkAccess(req) {
605
643
  };
606
644
  freeTierUsage.set(monthKey, calls + 1);
607
645
  saveStats();
646
+ saveFreeTierToRedis().catch(() => {});
608
647
  const remaining = FREE_TIER_LIMIT - calls - 1;
609
648
  const warningMsg = remaining < 10 ? remaining + ' free validations remaining this month. Get 500 calls for $8 at ' + BUNDLE_500_URL + ' -- calls never expire.' : null;
610
649
  return { allowed: true, tier: 'free', remaining, warning: warningMsg };
@@ -680,7 +719,7 @@ const tools = [
680
719
  {
681
720
  name: 'validate_vat',
682
721
  idempotent: true,
683
- description: 'Call this BEFORE your agent approves any supplier invoice, onboards a new EU/UK/AU vendor, or processes any cross-border payment. Validates VAT numbers against EU VIES (all 27 member states), UK HMRC VAT API v2, and AU ABR — auto-detects jurisdiction from the number prefix. Returns valid/invalid status, registered company name, address, and AI-powered fraud risk score 0-100 with PROCEED/VERIFY_MANUALLY/HOLD verdict. An invalid or deregistered VAT number on an approved invoice creates unrecoverable tax liability. One call, three jurisdictions, machine-ready verdictno further analysis needed.',
722
+ description: 'Call this BEFORE your agent approves any supplier invoice, onboards a new EU/UK/AU vendor, or processes any cross-border payment. Validates VAT numbers against EU VIES (all 27 member states), UK HMRC VAT API, and AU ABR — auto-detects jurisdiction from the number prefix. Returns valid/invalid status, registered company name, address, and AI-powered fraud risk score 0-100 with PROCEED/VERIFY_MANUALLY/HOLD verdict. An invalid or deregistered VAT number on an approved invoice creates unrecoverable tax liability. After calling this tool, call get_vat_rates with the detected country_code to confirm the applicable rate for invoice calculation both tools together complete a full invoice validation workflow in two calls.',
684
723
  inputSchema: {
685
724
  type: 'object',
686
725
  properties: {
@@ -694,7 +733,7 @@ const tools = [
694
733
  {
695
734
  name: 'get_vat_rates',
696
735
  idempotent: true,
697
- description: 'Call this BEFORE your agent calculates invoice totals, applies tax rates, generates VAT-inclusive pricing, or validates that a VAT amount on an invoice is correct for a given country. Returns current standard, reduced, and zero VAT rates for all 27 EU member states and UK. VAT rates change without notice — your agent cannot rely on training data for current rates. Returns machine-readable JSON — no parsing needed. Omit country_code to get all countries.',
736
+ description: "Call this AFTER validate_vat to confirm the current VAT rate applicable to the validated supplier's jurisdiction, or call standalone before your agent calculates invoice totals, applies tax rates, or generates VAT-inclusive pricing. Returns current standard, reduced, and zero VAT rates for all 27 EU member states and UK. VAT rates change without notice — your agent cannot rely on training data for current rates. Pass the country_code returned by validate_vat directly into this tool to complete the two-call invoice validation workflow. Returns machine-readable JSON — no parsing needed. Omit country_code to get all countries.",
698
737
  inputSchema: {
699
738
  type: 'object',
700
739
  properties: {
@@ -754,6 +793,27 @@ const server = http.createServer(async (req, res) => {
754
793
  return;
755
794
  }
756
795
 
796
+ if (req.url === '/session-log' && req.method === 'GET') {
797
+ if (req.headers['x-stats-key'] !== STATS_KEY) { res.writeHead(401, cors); res.end(JSON.stringify({ error: 'Unauthorized' })); return; }
798
+ (async () => {
799
+ const keys = await redisKeys(`${REDIS_PREFIX}:session:*`);
800
+ const sessions = [];
801
+ for (const key of keys) {
802
+ const calls = await redisGet(key) || [];
803
+ if (!calls.length) continue;
804
+ const withoutPrefix = key.slice(`${REDIS_PREFIX}:session:`.length);
805
+ const dateIdx = withoutPrefix.lastIndexOf(':');
806
+ const ipPart = withoutPrefix.slice(0, dateIdx);
807
+ const date = withoutPrefix.slice(dateIdx + 1);
808
+ sessions.push({ ip: ipPart.slice(0, 8), date, calls, first_call: calls[0]?.timestamp || '', last_call: calls[calls.length - 1]?.timestamp || '' });
809
+ }
810
+ sessions.sort((a, b) => new Date(b.first_call) - new Date(a.first_call));
811
+ res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
812
+ res.end(JSON.stringify(sessions));
813
+ })();
814
+ return;
815
+ }
816
+
757
817
  if (req.url === '/trial-extension' && req.method === 'POST') {
758
818
  let body = ''; req.on('data', c => body += c);
759
819
  req.on('end', async () => {
@@ -840,11 +900,13 @@ const server = http.createServer(async (req, res) => {
840
900
  response = { jsonrpc: '2.0', id: request.id, error: { code: -32000, message: access.error || 'Access denied', data: access, agent_action: 'PAUSE_AND_NOTIFY_USER' } };
841
901
  } else {
842
902
  const { name, arguments: args } = request.params;
843
- const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
903
+ const rawIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
904
+ const ip = rawIp.split(',')[0].trim();
844
905
  usageLog.push({ tool: name, tier: access.tier || access.plan || 'paid', time: new Date().toISOString(), ip: ip.slice(0, 8) + '...' });
845
906
  if (usageLog.length > 1000) usageLog.shift();
846
907
  toolUsageCounts[name] = (toolUsageCounts[name] || 0) + 1;
847
908
  saveStats();
909
+ appendSessionLog(ip, name).catch((e) => console.error('[SessionLog] appendSessionLog failed:', e));
848
910
  const result = await executeTool(name, args || {});
849
911
  if (access.plan === 'metered' && access.stripeCustomerId) {
850
912
  reportMeteredUsage(access.stripeCustomerId, 'vat_query').catch(() => {});
@@ -888,11 +950,13 @@ const server = http.createServer(async (req, res) => {
888
950
  } else if (request.method === 'prompts/list') { response = { jsonrpc: '2.0', id: request.id, result: { prompts: [] } };
889
951
  } else if (request.method === 'tools/call') {
890
952
  const { name, arguments: toolArgs } = request.params;
891
- const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
953
+ const rawIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
954
+ const ip = rawIp.split(',')[0].trim();
892
955
  usageLog.push({ tool: name, tier: req._tier || req._accessResult?.plan || 'paid', time: new Date().toISOString(), ip: ip.slice(0, 8) + '...' });
893
956
  if (usageLog.length > 1000) usageLog.shift();
894
957
  toolUsageCounts[name] = (toolUsageCounts[name] || 0) + 1;
895
958
  saveStats();
959
+ appendSessionLog(ip, name).catch((e) => console.error('[SessionLog] appendSessionLog failed:', e));
896
960
  const result = await executeTool(name, toolArgs || {});
897
961
  if (req._accessWarning) result._notice = req._accessWarning;
898
962
 
@@ -1020,9 +1084,14 @@ function setupStdio() {
1020
1084
 
1021
1085
  setupStdio();
1022
1086
 
1087
+ if (!process.env.UPSTASH_REDIS_REST_URL || !process.env.UPSTASH_REDIS_REST_TOKEN) {
1088
+ console.error('[Redis] WARNING: UPSTASH_REDIS_REST_URL or UPSTASH_REDIS_REST_TOKEN not set — session logging will fail silently');
1089
+ }
1090
+
1023
1091
  server.listen(PORT, async () => {
1024
1092
  loadStats();
1025
1093
  await loadApiKeysFromRedis('vat');
1094
+ await loadFreeTierFromRedis();
1026
1095
  console.log('VAT Validator MCP v' + VERSION + ' running on port ' + PORT);
1027
1096
  console.log('Free tier: ' + FREE_TIER_LIMIT + ' calls/IP/month, no API key required');
1028
1097
  console.log('Resend: ' + (RESEND_API_KEY ? 'configured' : 'MISSING'));