vat-validator-mcp 2.0.2 → 2.0.4
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/package.json +1 -1
- package/server.json +2 -2
- package/src/server.js +78 -41
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.4",
|
|
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.4';
|
|
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');
|
|
@@ -100,6 +100,25 @@ async function redisSet(key, value) {
|
|
|
100
100
|
} catch(e) {}
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
async function redisExpire(key, seconds) {
|
|
104
|
+
try {
|
|
105
|
+
await fetch(
|
|
106
|
+
`${UPSTASH_URL}/expire/${encodeURIComponent(key)}/${seconds}`,
|
|
107
|
+
{ method: 'POST', headers: { Authorization: `Bearer ${UPSTASH_TOKEN}` } }
|
|
108
|
+
);
|
|
109
|
+
} catch(e) {}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function appendSessionLog(ip, tool) {
|
|
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
|
+
}
|
|
121
|
+
|
|
103
122
|
async function redisKeys(pattern) {
|
|
104
123
|
try {
|
|
105
124
|
const res = await fetch(
|
|
@@ -211,6 +230,15 @@ async function validateVIES(countryCode, vatNumber) {
|
|
|
211
230
|
});
|
|
212
231
|
}
|
|
213
232
|
|
|
233
|
+
async function hmrcFetchWithRetry(url, options, maxRetries = 3) {
|
|
234
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
235
|
+
const response = await fetch(url, options);
|
|
236
|
+
if (response.status !== 429) return response;
|
|
237
|
+
if (attempt === maxRetries) return response;
|
|
238
|
+
await new Promise(resolve => setTimeout(resolve, attempt * 1000));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
214
242
|
function getFraudPreventionHeaders() {
|
|
215
243
|
return {
|
|
216
244
|
'Gov-Client-Connection-Method': 'BATCH_PROCESS_DIRECT',
|
|
@@ -223,7 +251,7 @@ function getFraudPreventionHeaders() {
|
|
|
223
251
|
'Gov-Client-User-IDs': 'os=railway-service',
|
|
224
252
|
'Gov-Vendor-License-IDs': 'vat-validator-mcp=not-applicable',
|
|
225
253
|
'Gov-Vendor-Product-Name': 'VAT%20Validator%20MCP',
|
|
226
|
-
'Gov-Vendor-Version': 'vat-validator-mcp=2.0.
|
|
254
|
+
'Gov-Vendor-Version': 'vat-validator-mcp=2.0.3'
|
|
227
255
|
};
|
|
228
256
|
}
|
|
229
257
|
|
|
@@ -245,32 +273,23 @@ async function getHMRCToken() {
|
|
|
245
273
|
|
|
246
274
|
const body = `client_secret=${encodeURIComponent(clientSecret)}&client_id=${encodeURIComponent(clientId)}&grant_type=client_credentials&scope=read%3Avat`;
|
|
247
275
|
|
|
248
|
-
|
|
249
|
-
const
|
|
250
|
-
hostname,
|
|
251
|
-
path: '/oauth/token',
|
|
276
|
+
try {
|
|
277
|
+
const response = await hmrcFetchWithRetry(`https://${hostname}/oauth/token`, {
|
|
252
278
|
method: 'POST',
|
|
253
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded',
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
res.on('end', () => {
|
|
257
|
-
try {
|
|
258
|
-
const json = JSON.parse(d);
|
|
259
|
-
if (json.access_token) {
|
|
260
|
-
hmrcToken = json.access_token;
|
|
261
|
-
hmrcTokenExpiry = now + (json.expires_in || 14400) * 1000;
|
|
262
|
-
resolve(hmrcToken);
|
|
263
|
-
} else {
|
|
264
|
-
resolve(null);
|
|
265
|
-
}
|
|
266
|
-
} catch(e) { resolve(null); }
|
|
267
|
-
});
|
|
279
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded', ...getFraudPreventionHeaders() },
|
|
280
|
+
body,
|
|
281
|
+
signal: AbortSignal.timeout(8000)
|
|
268
282
|
});
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
283
|
+
const json = await response.json();
|
|
284
|
+
if (json.access_token) {
|
|
285
|
+
hmrcToken = json.access_token;
|
|
286
|
+
hmrcTokenExpiry = now + (json.expires_in || 14400) * 1000;
|
|
287
|
+
return hmrcToken;
|
|
288
|
+
}
|
|
289
|
+
return null;
|
|
290
|
+
} catch(e) {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
274
293
|
}
|
|
275
294
|
|
|
276
295
|
async function validateHMRC(vatNumber) {
|
|
@@ -281,23 +300,18 @@ async function validateHMRC(vatNumber) {
|
|
|
281
300
|
const sandbox = process.env.HMRC_SANDBOX === 'true';
|
|
282
301
|
const hostname = sandbox ? 'test-api.service.hmrc.gov.uk' : 'api.service.hmrc.gov.uk';
|
|
283
302
|
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
hostname,
|
|
287
|
-
path: '/organisations/vat/check-vat-number/lookup/' + clean,
|
|
303
|
+
try {
|
|
304
|
+
const response = await hmrcFetchWithRetry(`https://${hostname}/organisations/vat/check-vat-number/lookup/${clean}`, {
|
|
288
305
|
method: 'GET',
|
|
289
|
-
headers: { 'Accept': 'application/vnd.hmrc.2.0+json', 'Authorization': 'Bearer ' + token, ...getFraudPreventionHeaders() }
|
|
290
|
-
|
|
291
|
-
let d = ''; res.on('data', c => d += c);
|
|
292
|
-
res.on('end', () => {
|
|
293
|
-
try { resolve({ source: 'HMRC', status: res.statusCode, data: JSON.parse(d) }); }
|
|
294
|
-
catch(e) { resolve({ source: 'HMRC', error: 'Parse error' }); }
|
|
295
|
-
});
|
|
306
|
+
headers: { 'Accept': 'application/vnd.hmrc.2.0+json', 'Authorization': 'Bearer ' + token, ...getFraudPreventionHeaders() },
|
|
307
|
+
signal: AbortSignal.timeout(8000)
|
|
296
308
|
});
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
309
|
+
const data = await response.json();
|
|
310
|
+
return { source: 'HMRC', status: response.status, data };
|
|
311
|
+
} catch(e) {
|
|
312
|
+
if (e.name === 'TimeoutError' || e.name === 'AbortError') return { source: 'HMRC', error: 'Timeout' };
|
|
313
|
+
return { source: 'HMRC', error: e.message };
|
|
314
|
+
}
|
|
301
315
|
}
|
|
302
316
|
|
|
303
317
|
async function validateABN(abn) {
|
|
@@ -759,6 +773,27 @@ const server = http.createServer(async (req, res) => {
|
|
|
759
773
|
return;
|
|
760
774
|
}
|
|
761
775
|
|
|
776
|
+
if (req.url === '/session-log' && req.method === 'GET') {
|
|
777
|
+
if (req.headers['x-stats-key'] !== STATS_KEY) { res.writeHead(401, cors); res.end(JSON.stringify({ error: 'Unauthorized' })); return; }
|
|
778
|
+
(async () => {
|
|
779
|
+
const keys = await redisKeys(`${REDIS_PREFIX}:session:*`);
|
|
780
|
+
const sessions = [];
|
|
781
|
+
for (const key of keys) {
|
|
782
|
+
const calls = await redisGet(key) || [];
|
|
783
|
+
if (!calls.length) continue;
|
|
784
|
+
const withoutPrefix = key.slice(`${REDIS_PREFIX}:session:`.length);
|
|
785
|
+
const dateIdx = withoutPrefix.lastIndexOf(':');
|
|
786
|
+
const ipPart = withoutPrefix.slice(0, dateIdx);
|
|
787
|
+
const date = withoutPrefix.slice(dateIdx + 1);
|
|
788
|
+
sessions.push({ ip: ipPart.slice(0, 8), date, calls, first_call: calls[0]?.timestamp || '', last_call: calls[calls.length - 1]?.timestamp || '' });
|
|
789
|
+
}
|
|
790
|
+
sessions.sort((a, b) => new Date(b.first_call) - new Date(a.first_call));
|
|
791
|
+
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
792
|
+
res.end(JSON.stringify(sessions));
|
|
793
|
+
})();
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
|
|
762
797
|
if (req.url === '/trial-extension' && req.method === 'POST') {
|
|
763
798
|
let body = ''; req.on('data', c => body += c);
|
|
764
799
|
req.on('end', async () => {
|
|
@@ -850,6 +885,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
850
885
|
if (usageLog.length > 1000) usageLog.shift();
|
|
851
886
|
toolUsageCounts[name] = (toolUsageCounts[name] || 0) + 1;
|
|
852
887
|
saveStats();
|
|
888
|
+
appendSessionLog(ip, name).catch(() => {});
|
|
853
889
|
const result = await executeTool(name, args || {});
|
|
854
890
|
if (access.plan === 'metered' && access.stripeCustomerId) {
|
|
855
891
|
reportMeteredUsage(access.stripeCustomerId, 'vat_query').catch(() => {});
|
|
@@ -898,6 +934,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
898
934
|
if (usageLog.length > 1000) usageLog.shift();
|
|
899
935
|
toolUsageCounts[name] = (toolUsageCounts[name] || 0) + 1;
|
|
900
936
|
saveStats();
|
|
937
|
+
appendSessionLog(ip, name).catch(() => {});
|
|
901
938
|
const result = await executeTool(name, toolArgs || {});
|
|
902
939
|
if (req._accessWarning) result._notice = req._accessWarning;
|
|
903
940
|
|