vat-validator-mcp 1.4.7 → 1.4.11
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 +79 -2
- package/package.json +1 -1
- package/server.json +2 -2
- package/smithery.yaml +1 -1
- package/src/server.js +92 -28
package/README.md
CHANGED
|
@@ -22,6 +22,49 @@ Or via Smithery:
|
|
|
22
22
|
npx -y @smithery/cli@latest mcp add OjasKord/vat-validator-mcp
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
## Harness Integration
|
|
26
|
+
|
|
27
|
+
### Claude Code / Claude Desktop (.mcp.json)
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"mcpServers": {
|
|
31
|
+
"vat-validator": {
|
|
32
|
+
"type": "http",
|
|
33
|
+
"url": "https://vat-validator-mcp-production.up.railway.app"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### LangChain (Python)
|
|
40
|
+
```python
|
|
41
|
+
from langchain_mcp_adapters.client import MultiServerMCPClient
|
|
42
|
+
client = MultiServerMCPClient({
|
|
43
|
+
"vat-validator": {
|
|
44
|
+
"url": "https://vat-validator-mcp-production.up.railway.app",
|
|
45
|
+
"transport": "http"
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
tools = await client.get_tools()
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### OpenAI Agents SDK (Python)
|
|
52
|
+
```python
|
|
53
|
+
from agents import Agent, HostedMCPTool
|
|
54
|
+
agent = Agent(
|
|
55
|
+
name="Assistant",
|
|
56
|
+
tools=[HostedMCPTool(tool_config={
|
|
57
|
+
"type": "mcp",
|
|
58
|
+
"server_label": "vat-validator",
|
|
59
|
+
"server_url": "https://vat-validator-mcp-production.up.railway.app",
|
|
60
|
+
"require_approval": "never"
|
|
61
|
+
})]
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### LangGraph
|
|
66
|
+
Same as LangChain above — langchain-mcp-adapters works with LangGraph natively.
|
|
67
|
+
|
|
25
68
|
## Why Use This
|
|
26
69
|
|
|
27
70
|
A VAT number is the most reliable identifier for a registered business in the EU, UK, and Australia. Validating it confirms the company is real and legally registered. But validation alone isn't enough — scammers use valid VAT numbers with mismatched company names, or invoice from newly registered shells. The AI tools in this server catch what raw validation misses.
|
|
@@ -86,6 +129,40 @@ AI comparison of invoice details against official registry records. Flags discre
|
|
|
86
129
|
}
|
|
87
130
|
```
|
|
88
131
|
|
|
132
|
+
## Add to Your Agent
|
|
133
|
+
|
|
134
|
+
### Claude Code / Claude Desktop (.mcp.json)
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"mcpServers": {
|
|
138
|
+
"vat-validator": {
|
|
139
|
+
"type": "sse",
|
|
140
|
+
"url": "https://vat-validator-mcp-production.up.railway.app/sse"
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### LangChain / LangGraph (Python)
|
|
147
|
+
```python
|
|
148
|
+
from langchain_mcp_adapters.client import MultiServerMCPClient
|
|
149
|
+
client = MultiServerMCPClient({
|
|
150
|
+
"vat-validator": {
|
|
151
|
+
"url": "https://vat-validator-mcp-production.up.railway.app/sse",
|
|
152
|
+
"transport": "sse"
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
tools = await client.get_tools()
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### OpenAI Agents SDK (Python)
|
|
159
|
+
```python
|
|
160
|
+
from agents.mcp import MCPServerSse
|
|
161
|
+
mcp_server = MCPServerSse(
|
|
162
|
+
params={"url": "https://vat-validator-mcp-production.up.railway.app/sse"}
|
|
163
|
+
)
|
|
164
|
+
```
|
|
165
|
+
|
|
89
166
|
## Example Responses
|
|
90
167
|
|
|
91
168
|
**validate_vat:**
|
|
@@ -154,8 +231,8 @@ Every response includes `source_url` and `checked_at` so agents can verify exact
|
|
|
154
231
|
| Plan | Validations | Price |
|
|
155
232
|
|---|---|---|
|
|
156
233
|
| Free | 20/month | No API key required |
|
|
157
|
-
|
|
|
158
|
-
|
|
|
234
|
+
| Starter | 500-call bundle | $8 |
|
|
235
|
+
| Pro | 2,000-call bundle | $28 |
|
|
159
236
|
|
|
160
237
|
Upgrade at **[kordagencies.com](https://kordagencies.com)**
|
|
161
238
|
|
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.
|
|
4
|
+
"version": "1.4.11",
|
|
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": {
|
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.
|
|
6
|
+
"version": "1.4.8",
|
|
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.
|
|
16
|
+
"version": "1.4.8",
|
|
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/smithery.yaml
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
description: "VAT number
|
|
1
|
+
description: "VAT number validation via EU VIES (27 member states), UK HMRC, and AU ABR. Call before invoice approval, supplier onboarding, or cross-border payment. Returns valid/invalid, company name, and CLEAR/REVIEW/BLOCK fraud assessment."
|
|
2
2
|
startCommand:
|
|
3
3
|
type: http
|
|
4
4
|
url: https://vat-validator-mcp-production.up.railway.app
|
package/src/server.js
CHANGED
|
@@ -4,7 +4,8 @@ const crypto = require('crypto');
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
|
|
6
6
|
const PERSIST_FILE = '/tmp/vat_stats.json';
|
|
7
|
-
const
|
|
7
|
+
const API_KEYS_FILE = '/tmp/vat_apikeys.json';
|
|
8
|
+
const VERSION = '1.4.11';
|
|
8
9
|
const PRO_UPGRADE_URL = 'https://buy.stripe.com/28EeVceUB06N1ty3teebu0l';
|
|
9
10
|
const ENTERPRISE_UPGRADE_URL = 'https://buy.stripe.com/00w14m7s96vb1ty5Bmebu0m';
|
|
10
11
|
const RESEND_API_KEY = process.env.RESEND_API_KEY || '';
|
|
@@ -14,8 +15,11 @@ const STATS_KEY = process.env.STATS_KEY || 'ojas2026';
|
|
|
14
15
|
|
|
15
16
|
const freeTierUsage = new Map();
|
|
16
17
|
const usageLog = [];
|
|
18
|
+
const toolUsageCounts = {};
|
|
19
|
+
const trialExtensions = new Map();
|
|
17
20
|
const FREE_TIER_LIMIT = 20;
|
|
18
|
-
const FREE_TIER_WARNING = 16;
|
|
21
|
+
const FREE_TIER_WARNING = 16;
|
|
22
|
+
const TRIAL_EXTENSION_CALLS = 10;
|
|
19
23
|
const apiKeys = new Map();
|
|
20
24
|
const PLAN_LIMITS = { pro: 5000, enterprise: Infinity };
|
|
21
25
|
|
|
@@ -23,7 +27,9 @@ function saveStats() {
|
|
|
23
27
|
try {
|
|
24
28
|
fs.writeFileSync(PERSIST_FILE, JSON.stringify({
|
|
25
29
|
freeTierUsage: Array.from(freeTierUsage.entries()),
|
|
26
|
-
usageLog: usageLog.slice(-1000)
|
|
30
|
+
usageLog: usageLog.slice(-1000),
|
|
31
|
+
toolUsageCounts,
|
|
32
|
+
trialExtensions: Array.from(trialExtensions.entries())
|
|
27
33
|
}));
|
|
28
34
|
} catch(e) { console.error('Stats save error:', e.message); }
|
|
29
35
|
}
|
|
@@ -34,11 +40,36 @@ function loadStats() {
|
|
|
34
40
|
const data = JSON.parse(fs.readFileSync(PERSIST_FILE, 'utf8'));
|
|
35
41
|
if (data.freeTierUsage) data.freeTierUsage.forEach(([k, v]) => freeTierUsage.set(k, v));
|
|
36
42
|
if (data.usageLog) usageLog.push(...data.usageLog);
|
|
37
|
-
|
|
43
|
+
if (data.toolUsageCounts) Object.assign(toolUsageCounts, data.toolUsageCounts);
|
|
44
|
+
if (data.trialExtensions) data.trialExtensions.forEach(([k, v]) => trialExtensions.set(k, v));
|
|
45
|
+
console.log('Stats loaded: ' + freeTierUsage.size + ' IPs, ' + usageLog.length + ' calls, ' + trialExtensions.size + ' trial extensions');
|
|
38
46
|
}
|
|
39
47
|
} catch(e) { console.error('Stats load error:', e.message); }
|
|
40
48
|
}
|
|
41
49
|
|
|
50
|
+
function getMonthKey(ip) { return ip + ':' + new Date().toISOString().slice(0, 7); }
|
|
51
|
+
|
|
52
|
+
function getEffectiveLimit(ip) {
|
|
53
|
+
for (const record of trialExtensions.values()) {
|
|
54
|
+
if (record.ip === ip) return FREE_TIER_LIMIT + TRIAL_EXTENSION_CALLS;
|
|
55
|
+
}
|
|
56
|
+
return FREE_TIER_LIMIT;
|
|
57
|
+
}
|
|
58
|
+
|
|
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
|
+
}
|
|
62
|
+
|
|
63
|
+
function loadApiKeys() {
|
|
64
|
+
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');
|
|
69
|
+
}
|
|
70
|
+
} catch(e) { console.error('API keys load error:', e.message); }
|
|
71
|
+
}
|
|
72
|
+
|
|
42
73
|
function generateApiKey() { return 'vat_' + crypto.randomBytes(24).toString('hex'); }
|
|
43
74
|
function getPlanFromProduct(name) {
|
|
44
75
|
if (!name) return 'pro';
|
|
@@ -232,20 +263,21 @@ async function executeTool(name, args) {
|
|
|
232
263
|
const result = await validateHMRC(detected.number);
|
|
233
264
|
if (result.error) return { valid: null, vat_number, country: 'GB', source: 'HMRC', error: result.error, likely_cause: 'external VAT registry temporarily unavailable', agent_action: 'RETRY_IN_2_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 120000, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10), retry: true, _disclaimer: LEGAL_DISCLAIMER };
|
|
234
265
|
const d = result.data;
|
|
235
|
-
if (result.status === 200 && d.target) return { valid: true, vat_number, country: 'GB', company_name: d.target.name || null, source: 'HMRC', source_url: 'api.service.hmrc.gov.uk', consultation_number: d.consultationNumber || null, checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
236
|
-
return { valid: false, vat_number, country: 'GB', source: 'HMRC', source_url: 'api.service.hmrc.gov.uk', reason: d.code || 'VAT number not found', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
266
|
+
if (result.status === 200 && d.target) return { valid: true, agent_action: 'PROCEED', vat_number, country: 'GB', company_name: d.target.name || null, source: 'HMRC', source_url: 'api.service.hmrc.gov.uk', consultation_number: d.consultationNumber || null, checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
267
|
+
return { valid: false, agent_action: 'VERIFY_MANUALLY', vat_number, country: 'GB', source: 'HMRC', source_url: 'api.service.hmrc.gov.uk', reason: d.code || 'VAT number not found', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
237
268
|
}
|
|
238
269
|
if (detected.type === 'eu') {
|
|
239
270
|
const result = await validateVIES(detected.country, detected.number);
|
|
240
271
|
if (result.error) return { valid: null, vat_number, agent_action: 'RETRY_IN_30_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 1800000, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10), country: detected.country, source: 'VIES', source_url: 'ec.europa.eu/taxation_customs/vies', error: 'EU VIES portal is temporarily unavailable — this is a known issue with the official EU system, not a problem with the VAT number. Retry in 30 minutes.', likely_cause: 'external VAT registry temporarily unavailable', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
241
272
|
const d = result.data;
|
|
242
|
-
return { valid: d.isValid || false, vat_number, country: detected.country, company_name: d.traderName || null, address: d.traderAddress || null, source: 'VIES', source_url: 'ec.europa.eu/taxation_customs/vies', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
273
|
+
return { valid: d.isValid || false, agent_action: d.isValid ? 'PROCEED' : 'VERIFY_MANUALLY', vat_number, country: detected.country, company_name: d.traderName || null, address: d.traderAddress || null, source: 'VIES', source_url: 'ec.europa.eu/taxation_customs/vies', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
243
274
|
}
|
|
244
275
|
if (detected.type === 'au') {
|
|
245
276
|
const result = await validateABN(detected.number);
|
|
246
277
|
if (result.error) return { valid: null, vat_number, country: 'AU', source: 'ABR', error: result.error, likely_cause: 'external VAT registry temporarily unavailable', agent_action: 'RETRY_IN_2_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 120000, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10), _disclaimer: LEGAL_DISCLAIMER };
|
|
247
278
|
const d = result.data;
|
|
248
|
-
|
|
279
|
+
const isValidABN = !!(d.Abn && d.AbnStatus === 'Active');
|
|
280
|
+
return { valid: isValidABN, agent_action: isValidABN ? 'PROCEED' : 'VERIFY_MANUALLY', vat_number, country: 'AU', company_name: d.EntityName || null, abn_status: d.AbnStatus || null, source: 'ABR', source_url: 'abr.business.gov.au', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
249
281
|
}
|
|
250
282
|
return { valid: null, vat_number, agent_action: 'PROVIDE_COUNTRY_PREFIX', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10), error: 'Could not detect country. Supported prefixes: EU (AT BE BG CY CZ DE DK EE EL ES FI FR HR HU IE IT LT LU LV MT NL PL PT RO SE SI SK), UK (GB), Australia (AU).', likely_cause: 'required field missing or malformed', _disclaimer: LEGAL_DISCLAIMER };
|
|
251
283
|
}
|
|
@@ -257,18 +289,18 @@ async function executeTool(name, args) {
|
|
|
257
289
|
const result = await validateHMRC(vat_number);
|
|
258
290
|
if (result.error) return { valid: null, vat_number, source: 'HMRC', source_url: 'api.service.hmrc.gov.uk', error: 'UK HMRC API is temporarily unavailable — this is not a problem with the VAT number. Retry in a few minutes.', likely_cause: 'external VAT registry temporarily unavailable', agent_action: 'RETRY_IN_2_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 120000, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10), checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
259
291
|
const d = result.data;
|
|
260
|
-
if (result.status === 200 && d.target) return { valid: true, vat_number, company_name: d.target.name || null, registered_address: d.target.address ? Object.values(d.target.address).filter(Boolean).join(', ') : null, consultation_number: d.consultationNumber || null, source: 'HMRC', source_url: 'api.service.hmrc.gov.uk', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
261
|
-
return { valid: false, vat_number, source: 'HMRC', source_url: 'api.service.hmrc.gov.uk', reason: d.code || 'VAT number not found', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
292
|
+
if (result.status === 200 && d.target) return { valid: true, agent_action: 'PROCEED', vat_number, company_name: d.target.name || null, registered_address: d.target.address ? Object.values(d.target.address).filter(Boolean).join(', ') : null, consultation_number: d.consultationNumber || null, source: 'HMRC', source_url: 'api.service.hmrc.gov.uk', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
293
|
+
return { valid: false, agent_action: 'VERIFY_MANUALLY', vat_number, source: 'HMRC', source_url: 'api.service.hmrc.gov.uk', reason: d.code || 'VAT number not found', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
262
294
|
}
|
|
263
295
|
|
|
264
296
|
if (name === 'get_vat_rates') {
|
|
265
297
|
const country_code = args.country_code;
|
|
266
298
|
const checkedAt = nowISO();
|
|
267
|
-
if (!country_code) return { rates: VAT_RATES, note: 'VAT rates as of 2026. Verify with official tax authority before use.', source_url: 'kordagencies.com', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
299
|
+
if (!country_code) return { agent_action: 'PROCEED', rates: VAT_RATES, note: 'VAT rates as of 2026. Verify with official tax authority before use.', source_url: 'kordagencies.com', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
268
300
|
const code = country_code.toUpperCase();
|
|
269
301
|
const rate = VAT_RATES[code];
|
|
270
302
|
if (!rate) return { error: 'No VAT rate data for: ' + code + '. Supported: ' + Object.keys(VAT_RATES).join(', '), likely_cause: 'required field missing or malformed', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10), _disclaimer: LEGAL_DISCLAIMER };
|
|
271
|
-
return Object.assign({ country_code: code }, rate, { note: 'Verify current rates with official tax authority before use.', source_url: 'kordagencies.com', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER });
|
|
303
|
+
return Object.assign({ agent_action: 'PROCEED', country_code: code }, rate, { note: 'Verify current rates with official tax authority before use.', source_url: 'kordagencies.com', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER });
|
|
272
304
|
}
|
|
273
305
|
|
|
274
306
|
if (name === 'batch_validate') {
|
|
@@ -279,7 +311,7 @@ async function executeTool(name, args) {
|
|
|
279
311
|
try { return await executeTool('validate_vat', { vat_number: vat }); }
|
|
280
312
|
catch(e) { return { vat_number: vat, valid: null, error: e.message, likely_cause: 'external VAT registry temporarily unavailable', agent_action: 'RETRY_IN_2_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 120000, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10) }; }
|
|
281
313
|
}));
|
|
282
|
-
return { summary: { total: results.length, valid: results.filter(r => r.valid === true).length, invalid: results.filter(r => r.valid === false).length, error: results.filter(r => r.valid === null).length }, results, _disclaimer: LEGAL_DISCLAIMER };
|
|
314
|
+
return { agent_action: 'PROCEED', summary: { total: results.length, valid: results.filter(r => r.valid === true).length, invalid: results.filter(r => r.valid === false).length, error: results.filter(r => r.valid === null).length }, results, _disclaimer: LEGAL_DISCLAIMER };
|
|
283
315
|
}
|
|
284
316
|
|
|
285
317
|
if (name === 'analyse_vat_risk') {
|
|
@@ -292,7 +324,8 @@ async function executeTool(name, args) {
|
|
|
292
324
|
try {
|
|
293
325
|
const response = await callClaude(prompt);
|
|
294
326
|
const result = JSON.parse(response.replace(/```json|```/g, '').trim());
|
|
295
|
-
|
|
327
|
+
const vatRiskAction = (result.risk_level === 'HIGH' || result.risk_level === 'CRITICAL') ? 'HOLD' : result.risk_level === 'MEDIUM' ? 'VERIFY_MANUALLY' : 'PROCEED';
|
|
328
|
+
return Object.assign({}, result, { vat_number, agent_action: vatRiskAction, _disclaimer: LEGAL_DISCLAIMER });
|
|
296
329
|
} catch(e) {
|
|
297
330
|
return { recommendation: 'REVIEW', risk_level: 'MEDIUM', risk_score: 50, vat_number, error: 'AI analysis unavailable - manual review recommended', likely_cause: 'AI analysis failed — transient Anthropic API issue', agent_action: 'RETRY_IN_2_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 120000, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10), _disclaimer: LEGAL_DISCLAIMER };
|
|
298
331
|
}
|
|
@@ -305,7 +338,7 @@ async function executeTool(name, args) {
|
|
|
305
338
|
try {
|
|
306
339
|
const response = await callClaude(prompt);
|
|
307
340
|
const result = JSON.parse(response.replace(/```json|```/g, '').trim());
|
|
308
|
-
const agentAction = result.
|
|
341
|
+
const agentAction = result.match_verdict === 'MATCH' ? 'PROCEED' : 'INVESTIGATE';
|
|
309
342
|
return Object.assign({}, result, { invoice_vat_number, agent_action: agentAction, discrepancies: result.discrepancies || [], _disclaimer: LEGAL_DISCLAIMER });
|
|
310
343
|
} catch(e) {
|
|
311
344
|
return { match_verdict: 'UNVERIFIABLE', agent_action: 'RETRY_IN_2_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 120000, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10), fraud_risk: 'MEDIUM', invoice_vat_number, discrepancies: [], error: 'AI analysis unavailable -- manual review recommended', likely_cause: 'AI analysis failed — transient Anthropic API issue', _disclaimer: LEGAL_DISCLAIMER };
|
|
@@ -325,12 +358,14 @@ function checkAccess(req) {
|
|
|
325
358
|
return { allowed: true, tier: record.plan, record };
|
|
326
359
|
}
|
|
327
360
|
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
361
|
+
const monthKey = getMonthKey(ip);
|
|
362
|
+
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' };
|
|
364
|
+
freeTierUsage.set(monthKey, calls + 1);
|
|
331
365
|
saveStats();
|
|
332
366
|
const remaining = FREE_TIER_LIMIT - calls - 1;
|
|
333
|
-
|
|
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;
|
|
368
|
+
return { allowed: true, tier: 'free', remaining, warning: warningMsg };
|
|
334
369
|
}
|
|
335
370
|
|
|
336
371
|
function verifyStripeSignature(body, sig, secret) {
|
|
@@ -370,6 +405,7 @@ async function handleStripeWebhook(body, sig) {
|
|
|
370
405
|
if (email) {
|
|
371
406
|
const apiKey = generateApiKey();
|
|
372
407
|
apiKeys.set(apiKey, { email, plan, createdAt: new Date().toISOString(), calls: 0, limit: PLAN_LIMITS[plan] });
|
|
408
|
+
saveApiKeys();
|
|
373
409
|
await sendApiKeyEmail(email, apiKey, plan);
|
|
374
410
|
console.log('[vat] API key created for ' + email + ' (' + plan + ')');
|
|
375
411
|
return { success: true, email, plan };
|
|
@@ -431,10 +467,34 @@ const server = http.createServer(async (req, res) => {
|
|
|
431
467
|
if (req.url === '/stats' && req.method === 'GET') {
|
|
432
468
|
if (req.headers['x-stats-key'] !== STATS_KEY) { res.writeHead(401, cors); res.end(JSON.stringify({ error: 'Unauthorized' })); return; }
|
|
433
469
|
const totalFreeCalls = Array.from(freeTierUsage.values()).reduce((a, b) => a + b, 0);
|
|
434
|
-
const
|
|
435
|
-
usageLog.forEach(e => { toolCounts[e.tool] = (toolCounts[e.tool] || 0) + 1; });
|
|
470
|
+
const freeUniqueIPs = new Set(Array.from(freeTierUsage.keys()).map(k => k.split(':')[0])).size;
|
|
436
471
|
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
437
|
-
res.end(JSON.stringify({ free_tier_unique_ips:
|
|
472
|
+
res.end(JSON.stringify({ free_tier_unique_ips: freeUniqueIPs, free_tier_total_calls: totalFreeCalls, paid_keys_issued: apiKeys.size, tool_usage: toolUsageCounts, recent_calls: usageLog.slice(-20).reverse(), trial_extensions_granted: trialExtensions.size }));
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (req.url === '/trial-extension' && req.method === 'POST') {
|
|
477
|
+
let body = ''; req.on('data', c => body += c);
|
|
478
|
+
req.on('end', async () => {
|
|
479
|
+
try {
|
|
480
|
+
const { name, email, use_case } = JSON.parse(body);
|
|
481
|
+
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
|
+
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; }
|
|
484
|
+
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
485
|
+
const monthKey = getMonthKey(ip);
|
|
486
|
+
const currentCalls = freeTierUsage.get(monthKey) || 0;
|
|
487
|
+
freeTierUsage.set(monthKey, Math.max(0, currentCalls - TRIAL_EXTENSION_CALLS));
|
|
488
|
+
trialExtensions.set(emailKey, { name, email, use_case: use_case || '', ip, granted_at: nowISO() });
|
|
489
|
+
saveStats();
|
|
490
|
+
await sendEmail('ojas@kordagencies.com', 'VAT Validator -- Trial Extension: ' + name,
|
|
491
|
+
'<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
|
+
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>');
|
|
494
|
+
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 }));
|
|
496
|
+
} catch(e) { res.writeHead(400, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: e.message, agent_action: 'RETRY_IN_2_MIN' })); }
|
|
497
|
+
});
|
|
438
498
|
return;
|
|
439
499
|
}
|
|
440
500
|
|
|
@@ -502,6 +562,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
502
562
|
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
503
563
|
usageLog.push({ tool: name, tier: access.tier, time: new Date().toISOString(), ip: ip.slice(0, 8) + '...' });
|
|
504
564
|
if (usageLog.length > 1000) usageLog.shift();
|
|
565
|
+
toolUsageCounts[name] = (toolUsageCounts[name] || 0) + 1;
|
|
505
566
|
saveStats();
|
|
506
567
|
const result = await executeTool(name, args || {});
|
|
507
568
|
response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } };
|
|
@@ -525,8 +586,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
525
586
|
try {
|
|
526
587
|
const request = JSON.parse(body);
|
|
527
588
|
let response;
|
|
528
|
-
if (request.method
|
|
529
|
-
if (request.
|
|
589
|
+
if (request.method === 'tools/call') {
|
|
590
|
+
if (request.params?.name === 'batch_validate') {
|
|
530
591
|
const apiKey = req.headers['x-api-key'];
|
|
531
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; }
|
|
532
593
|
const record = apiKeys.get(apiKey);
|
|
@@ -547,6 +608,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
547
608
|
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
548
609
|
usageLog.push({ tool: name, tier: req._tier || 'paid', time: new Date().toISOString(), ip: ip.slice(0, 8) + '...' });
|
|
549
610
|
if (usageLog.length > 1000) usageLog.shift();
|
|
611
|
+
toolUsageCounts[name] = (toolUsageCounts[name] || 0) + 1;
|
|
550
612
|
saveStats();
|
|
551
613
|
const result = await executeTool(name, toolArgs || {});
|
|
552
614
|
if (req._accessWarning) result._notice = req._accessWarning;
|
|
@@ -554,15 +616,16 @@ const server = http.createServer(async (req, res) => {
|
|
|
554
616
|
// Partial response for free tier
|
|
555
617
|
if (req._tier === 'free' && !result.error) {
|
|
556
618
|
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
557
|
-
const used = freeTierUsage.get(ip) || 0;
|
|
619
|
+
const used = freeTierUsage.get(getMonthKey(ip)) || 0;
|
|
558
620
|
const remaining = FREE_TIER_LIMIT - used;
|
|
559
621
|
const isWarning = used >= FREE_TIER_WARNING;
|
|
622
|
+
const effectiveLimit = getEffectiveLimit(ip);
|
|
560
623
|
|
|
561
624
|
if (name === 'validate_vat' || name === 'validate_uk_vat') {
|
|
562
625
|
// Gate address on free tier — company name + valid status visible
|
|
563
626
|
const gated = ['registered_address', 'address', 'consultation_number'];
|
|
564
627
|
gated.forEach(f => delete result[f]);
|
|
565
|
-
result._upgrade_note = 'Free tier: ' + remaining + ' of ' +
|
|
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.';
|
|
566
629
|
result._gated_fields = gated;
|
|
567
630
|
}
|
|
568
631
|
|
|
@@ -570,7 +633,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
570
633
|
// Gate full reasoning — verdict visible, details gated
|
|
571
634
|
const gated = ['fraud_signals', 'positive_indicators', 'recommended_action', 'summary'];
|
|
572
635
|
gated.forEach(f => delete result[f]);
|
|
573
|
-
result._upgrade_note = 'Free tier: ' + remaining + ' of ' +
|
|
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.';
|
|
574
637
|
result._gated_fields = gated;
|
|
575
638
|
}
|
|
576
639
|
|
|
@@ -578,7 +641,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
578
641
|
// Gate detail fields — match_status visible, discrepancies gated
|
|
579
642
|
const gated = ['discrepancies', 'name_match', 'address_match', 'recommended_action', 'summary'];
|
|
580
643
|
gated.forEach(f => delete result[f]);
|
|
581
|
-
result._upgrade_note = 'Free tier: ' + remaining + ' of ' +
|
|
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.';
|
|
582
645
|
result._gated_fields = gated;
|
|
583
646
|
}
|
|
584
647
|
|
|
@@ -641,6 +704,7 @@ setupStdio();
|
|
|
641
704
|
|
|
642
705
|
server.listen(PORT, () => {
|
|
643
706
|
loadStats();
|
|
707
|
+
loadApiKeys();
|
|
644
708
|
console.log('VAT Validator MCP v' + VERSION + ' running on port ' + PORT);
|
|
645
709
|
console.log('Free tier: ' + FREE_TIER_LIMIT + ' calls/IP/month, no API key required');
|
|
646
710
|
console.log('Resend: ' + (RESEND_API_KEY ? 'configured' : 'MISSING'));
|