vat-validator-mcp 2.0.4 → 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/src/server.js +56 -29
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/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,37 +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
|
-
body: JSON.stringify({ value: JSON.stringify(value) })
|
|
98
|
-
}
|
|
99
|
-
);
|
|
100
|
-
} catch(e) {}
|
|
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); }
|
|
101
98
|
}
|
|
102
99
|
|
|
103
100
|
async function redisExpire(key, seconds) {
|
|
104
101
|
try {
|
|
105
|
-
await fetch(
|
|
102
|
+
const res = await fetch(
|
|
106
103
|
`${UPSTASH_URL}/expire/${encodeURIComponent(key)}/${seconds}`,
|
|
107
104
|
{ method: 'POST', headers: { Authorization: `Bearer ${UPSTASH_TOKEN}` } }
|
|
108
105
|
);
|
|
109
|
-
|
|
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); }
|
|
110
109
|
}
|
|
111
110
|
|
|
112
111
|
async function appendSessionLog(ip, tool) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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); }
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
async function redisKeys(pattern) {
|
|
@@ -126,6 +127,7 @@ async function redisKeys(pattern) {
|
|
|
126
127
|
{ headers: { Authorization: `Bearer ${UPSTASH_TOKEN}` } }
|
|
127
128
|
);
|
|
128
129
|
const data = await res.json();
|
|
130
|
+
if (data.error) console.error('[Redis] redisKeys error:', data.error, 'pattern:', pattern);
|
|
129
131
|
return data.result || [];
|
|
130
132
|
} catch(e) { return []; }
|
|
131
133
|
}
|
|
@@ -146,6 +148,22 @@ async function loadApiKeysFromRedis(prefix) {
|
|
|
146
148
|
console.log(`Loaded ${apiKeys.size} API keys from Redis`);
|
|
147
149
|
}
|
|
148
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
|
+
|
|
149
167
|
function generateApiKey() { return 'vat_' + crypto.randomBytes(24).toString('hex'); }
|
|
150
168
|
function getPlanFromProduct(productName) {
|
|
151
169
|
if (!productName) return 'bundle_500';
|
|
@@ -598,7 +616,8 @@ function checkAccess(req) {
|
|
|
598
616
|
saveKeyToRedis(apiKey, record, REDIS_PREFIX).catch(() => {});
|
|
599
617
|
return { allowed: true, paid: true, plan: record.plan };
|
|
600
618
|
}
|
|
601
|
-
const
|
|
619
|
+
const rawIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
620
|
+
const ip = rawIp.split(',')[0].trim();
|
|
602
621
|
const monthKey = getMonthKey(ip);
|
|
603
622
|
const calls = freeTierUsage.get(monthKey) || 0;
|
|
604
623
|
if (calls >= FREE_TIER_LIMIT) return {
|
|
@@ -624,6 +643,7 @@ function checkAccess(req) {
|
|
|
624
643
|
};
|
|
625
644
|
freeTierUsage.set(monthKey, calls + 1);
|
|
626
645
|
saveStats();
|
|
646
|
+
saveFreeTierToRedis().catch(() => {});
|
|
627
647
|
const remaining = FREE_TIER_LIMIT - calls - 1;
|
|
628
648
|
const warningMsg = remaining < 10 ? remaining + ' free validations remaining this month. Get 500 calls for $8 at ' + BUNDLE_500_URL + ' -- calls never expire.' : null;
|
|
629
649
|
return { allowed: true, tier: 'free', remaining, warning: warningMsg };
|
|
@@ -699,7 +719,7 @@ const tools = [
|
|
|
699
719
|
{
|
|
700
720
|
name: 'validate_vat',
|
|
701
721
|
idempotent: true,
|
|
702
|
-
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.',
|
|
703
723
|
inputSchema: {
|
|
704
724
|
type: 'object',
|
|
705
725
|
properties: {
|
|
@@ -713,7 +733,7 @@ const tools = [
|
|
|
713
733
|
{
|
|
714
734
|
name: 'get_vat_rates',
|
|
715
735
|
idempotent: true,
|
|
716
|
-
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.",
|
|
717
737
|
inputSchema: {
|
|
718
738
|
type: 'object',
|
|
719
739
|
properties: {
|
|
@@ -880,12 +900,13 @@ const server = http.createServer(async (req, res) => {
|
|
|
880
900
|
response = { jsonrpc: '2.0', id: request.id, error: { code: -32000, message: access.error || 'Access denied', data: access, agent_action: 'PAUSE_AND_NOTIFY_USER' } };
|
|
881
901
|
} else {
|
|
882
902
|
const { name, arguments: args } = request.params;
|
|
883
|
-
const
|
|
903
|
+
const rawIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
904
|
+
const ip = rawIp.split(',')[0].trim();
|
|
884
905
|
usageLog.push({ tool: name, tier: access.tier || access.plan || 'paid', time: new Date().toISOString(), ip: ip.slice(0, 8) + '...' });
|
|
885
906
|
if (usageLog.length > 1000) usageLog.shift();
|
|
886
907
|
toolUsageCounts[name] = (toolUsageCounts[name] || 0) + 1;
|
|
887
908
|
saveStats();
|
|
888
|
-
appendSessionLog(ip, name).catch(() =>
|
|
909
|
+
appendSessionLog(ip, name).catch((e) => console.error('[SessionLog] appendSessionLog failed:', e));
|
|
889
910
|
const result = await executeTool(name, args || {});
|
|
890
911
|
if (access.plan === 'metered' && access.stripeCustomerId) {
|
|
891
912
|
reportMeteredUsage(access.stripeCustomerId, 'vat_query').catch(() => {});
|
|
@@ -929,12 +950,13 @@ const server = http.createServer(async (req, res) => {
|
|
|
929
950
|
} else if (request.method === 'prompts/list') { response = { jsonrpc: '2.0', id: request.id, result: { prompts: [] } };
|
|
930
951
|
} else if (request.method === 'tools/call') {
|
|
931
952
|
const { name, arguments: toolArgs } = request.params;
|
|
932
|
-
const
|
|
953
|
+
const rawIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
954
|
+
const ip = rawIp.split(',')[0].trim();
|
|
933
955
|
usageLog.push({ tool: name, tier: req._tier || req._accessResult?.plan || 'paid', time: new Date().toISOString(), ip: ip.slice(0, 8) + '...' });
|
|
934
956
|
if (usageLog.length > 1000) usageLog.shift();
|
|
935
957
|
toolUsageCounts[name] = (toolUsageCounts[name] || 0) + 1;
|
|
936
958
|
saveStats();
|
|
937
|
-
appendSessionLog(ip, name).catch(() =>
|
|
959
|
+
appendSessionLog(ip, name).catch((e) => console.error('[SessionLog] appendSessionLog failed:', e));
|
|
938
960
|
const result = await executeTool(name, toolArgs || {});
|
|
939
961
|
if (req._accessWarning) result._notice = req._accessWarning;
|
|
940
962
|
|
|
@@ -1062,9 +1084,14 @@ function setupStdio() {
|
|
|
1062
1084
|
|
|
1063
1085
|
setupStdio();
|
|
1064
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
|
+
|
|
1065
1091
|
server.listen(PORT, async () => {
|
|
1066
1092
|
loadStats();
|
|
1067
1093
|
await loadApiKeysFromRedis('vat');
|
|
1094
|
+
await loadFreeTierFromRedis();
|
|
1068
1095
|
console.log('VAT Validator MCP v' + VERSION + ' running on port ' + PORT);
|
|
1069
1096
|
console.log('Free tier: ' + FREE_TIER_LIMIT + ' calls/IP/month, no API key required');
|
|
1070
1097
|
console.log('Resend: ' + (RESEND_API_KEY ? 'configured' : 'MISSING'));
|