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 +9 -8
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/server.js +86 -17
package/README.md
CHANGED
|
@@ -28,12 +28,12 @@ sources directly:
|
|
|
28
28
|
|
|
29
29
|
| Tool | Free Tier | Use When |
|
|
30
30
|
|---|---|---|
|
|
31
|
-
| validate_vat |
|
|
32
|
-
| validate_uk_vat |
|
|
33
|
-
| get_vat_rates |
|
|
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 |
|
|
36
|
-
| compare_invoice_details |
|
|
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 |
|
|
69
|
-
|
|
|
70
|
-
|
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
91
|
-
{
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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'));
|