vat-validator-mcp 1.4.8 → 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 +77 -0
- package/package.json +1 -1
- package/server.json +2 -2
- package/smithery.yaml +1 -1
- package/src/server.js +69 -26
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:**
|
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
|
@@ -5,7 +5,7 @@ const fs = require('fs');
|
|
|
5
5
|
|
|
6
6
|
const PERSIST_FILE = '/tmp/vat_stats.json';
|
|
7
7
|
const API_KEYS_FILE = '/tmp/vat_apikeys.json';
|
|
8
|
-
const VERSION = '1.4.
|
|
8
|
+
const VERSION = '1.4.11';
|
|
9
9
|
const PRO_UPGRADE_URL = 'https://buy.stripe.com/28EeVceUB06N1ty3teebu0l';
|
|
10
10
|
const ENTERPRISE_UPGRADE_URL = 'https://buy.stripe.com/00w14m7s96vb1ty5Bmebu0m';
|
|
11
11
|
const RESEND_API_KEY = process.env.RESEND_API_KEY || '';
|
|
@@ -15,8 +15,11 @@ const STATS_KEY = process.env.STATS_KEY || 'ojas2026';
|
|
|
15
15
|
|
|
16
16
|
const freeTierUsage = new Map();
|
|
17
17
|
const usageLog = [];
|
|
18
|
+
const toolUsageCounts = {};
|
|
19
|
+
const trialExtensions = new Map();
|
|
18
20
|
const FREE_TIER_LIMIT = 20;
|
|
19
|
-
const FREE_TIER_WARNING = 16;
|
|
21
|
+
const FREE_TIER_WARNING = 16;
|
|
22
|
+
const TRIAL_EXTENSION_CALLS = 10;
|
|
20
23
|
const apiKeys = new Map();
|
|
21
24
|
const PLAN_LIMITS = { pro: 5000, enterprise: Infinity };
|
|
22
25
|
|
|
@@ -24,7 +27,9 @@ function saveStats() {
|
|
|
24
27
|
try {
|
|
25
28
|
fs.writeFileSync(PERSIST_FILE, JSON.stringify({
|
|
26
29
|
freeTierUsage: Array.from(freeTierUsage.entries()),
|
|
27
|
-
usageLog: usageLog.slice(-1000)
|
|
30
|
+
usageLog: usageLog.slice(-1000),
|
|
31
|
+
toolUsageCounts,
|
|
32
|
+
trialExtensions: Array.from(trialExtensions.entries())
|
|
28
33
|
}));
|
|
29
34
|
} catch(e) { console.error('Stats save error:', e.message); }
|
|
30
35
|
}
|
|
@@ -35,13 +40,22 @@ function loadStats() {
|
|
|
35
40
|
const data = JSON.parse(fs.readFileSync(PERSIST_FILE, 'utf8'));
|
|
36
41
|
if (data.freeTierUsage) data.freeTierUsage.forEach(([k, v]) => freeTierUsage.set(k, v));
|
|
37
42
|
if (data.usageLog) usageLog.push(...data.usageLog);
|
|
38
|
-
|
|
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');
|
|
39
46
|
}
|
|
40
47
|
} catch(e) { console.error('Stats load error:', e.message); }
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
function getMonthKey(ip) { return ip + ':' + new Date().toISOString().slice(0, 7); }
|
|
44
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
|
+
|
|
45
59
|
function saveApiKeys() {
|
|
46
60
|
try { fs.writeFileSync(API_KEYS_FILE, JSON.stringify(Array.from(apiKeys.entries()))); } catch(e) { console.error('API keys save error:', e.message); }
|
|
47
61
|
}
|
|
@@ -249,20 +263,21 @@ async function executeTool(name, args) {
|
|
|
249
263
|
const result = await validateHMRC(detected.number);
|
|
250
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 };
|
|
251
265
|
const d = result.data;
|
|
252
|
-
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 };
|
|
253
|
-
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 };
|
|
254
268
|
}
|
|
255
269
|
if (detected.type === 'eu') {
|
|
256
270
|
const result = await validateVIES(detected.country, detected.number);
|
|
257
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 };
|
|
258
272
|
const d = result.data;
|
|
259
|
-
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 };
|
|
260
274
|
}
|
|
261
275
|
if (detected.type === 'au') {
|
|
262
276
|
const result = await validateABN(detected.number);
|
|
263
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 };
|
|
264
278
|
const d = result.data;
|
|
265
|
-
|
|
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 };
|
|
266
281
|
}
|
|
267
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 };
|
|
268
283
|
}
|
|
@@ -274,18 +289,18 @@ async function executeTool(name, args) {
|
|
|
274
289
|
const result = await validateHMRC(vat_number);
|
|
275
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 };
|
|
276
291
|
const d = result.data;
|
|
277
|
-
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 };
|
|
278
|
-
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 };
|
|
279
294
|
}
|
|
280
295
|
|
|
281
296
|
if (name === 'get_vat_rates') {
|
|
282
297
|
const country_code = args.country_code;
|
|
283
298
|
const checkedAt = nowISO();
|
|
284
|
-
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 };
|
|
285
300
|
const code = country_code.toUpperCase();
|
|
286
301
|
const rate = VAT_RATES[code];
|
|
287
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 };
|
|
288
|
-
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 });
|
|
289
304
|
}
|
|
290
305
|
|
|
291
306
|
if (name === 'batch_validate') {
|
|
@@ -296,7 +311,7 @@ async function executeTool(name, args) {
|
|
|
296
311
|
try { return await executeTool('validate_vat', { vat_number: vat }); }
|
|
297
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) }; }
|
|
298
313
|
}));
|
|
299
|
-
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 };
|
|
300
315
|
}
|
|
301
316
|
|
|
302
317
|
if (name === 'analyse_vat_risk') {
|
|
@@ -309,7 +324,8 @@ async function executeTool(name, args) {
|
|
|
309
324
|
try {
|
|
310
325
|
const response = await callClaude(prompt);
|
|
311
326
|
const result = JSON.parse(response.replace(/```json|```/g, '').trim());
|
|
312
|
-
|
|
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 });
|
|
313
329
|
} catch(e) {
|
|
314
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 };
|
|
315
331
|
}
|
|
@@ -322,7 +338,7 @@ async function executeTool(name, args) {
|
|
|
322
338
|
try {
|
|
323
339
|
const response = await callClaude(prompt);
|
|
324
340
|
const result = JSON.parse(response.replace(/```json|```/g, '').trim());
|
|
325
|
-
const agentAction = result.
|
|
341
|
+
const agentAction = result.match_verdict === 'MATCH' ? 'PROCEED' : 'INVESTIGATE';
|
|
326
342
|
return Object.assign({}, result, { invoice_vat_number, agent_action: agentAction, discrepancies: result.discrepancies || [], _disclaimer: LEGAL_DISCLAIMER });
|
|
327
343
|
} catch(e) {
|
|
328
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 };
|
|
@@ -344,11 +360,12 @@ function checkAccess(req) {
|
|
|
344
360
|
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
345
361
|
const monthKey = getMonthKey(ip);
|
|
346
362
|
const calls = freeTierUsage.get(monthKey) || 0;
|
|
347
|
-
if (calls >= FREE_TIER_LIMIT) return { allowed: false, reason: 'Free tier limit reached.
|
|
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' };
|
|
348
364
|
freeTierUsage.set(monthKey, calls + 1);
|
|
349
365
|
saveStats();
|
|
350
366
|
const remaining = FREE_TIER_LIMIT - calls - 1;
|
|
351
|
-
|
|
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 };
|
|
352
369
|
}
|
|
353
370
|
|
|
354
371
|
function verifyStripeSignature(body, sig, secret) {
|
|
@@ -450,11 +467,34 @@ const server = http.createServer(async (req, res) => {
|
|
|
450
467
|
if (req.url === '/stats' && req.method === 'GET') {
|
|
451
468
|
if (req.headers['x-stats-key'] !== STATS_KEY) { res.writeHead(401, cors); res.end(JSON.stringify({ error: 'Unauthorized' })); return; }
|
|
452
469
|
const totalFreeCalls = Array.from(freeTierUsage.values()).reduce((a, b) => a + b, 0);
|
|
453
|
-
const toolCounts = {};
|
|
454
|
-
usageLog.forEach(e => { toolCounts[e.tool] = (toolCounts[e.tool] || 0) + 1; });
|
|
455
|
-
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
456
470
|
const freeUniqueIPs = new Set(Array.from(freeTierUsage.keys()).map(k => k.split(':')[0])).size;
|
|
457
|
-
res.
|
|
471
|
+
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
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
|
+
});
|
|
458
498
|
return;
|
|
459
499
|
}
|
|
460
500
|
|
|
@@ -522,6 +562,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
522
562
|
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
523
563
|
usageLog.push({ tool: name, tier: access.tier, time: new Date().toISOString(), ip: ip.slice(0, 8) + '...' });
|
|
524
564
|
if (usageLog.length > 1000) usageLog.shift();
|
|
565
|
+
toolUsageCounts[name] = (toolUsageCounts[name] || 0) + 1;
|
|
525
566
|
saveStats();
|
|
526
567
|
const result = await executeTool(name, args || {});
|
|
527
568
|
response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } };
|
|
@@ -545,8 +586,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
545
586
|
try {
|
|
546
587
|
const request = JSON.parse(body);
|
|
547
588
|
let response;
|
|
548
|
-
if (request.method
|
|
549
|
-
if (request.
|
|
589
|
+
if (request.method === 'tools/call') {
|
|
590
|
+
if (request.params?.name === 'batch_validate') {
|
|
550
591
|
const apiKey = req.headers['x-api-key'];
|
|
551
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; }
|
|
552
593
|
const record = apiKeys.get(apiKey);
|
|
@@ -567,6 +608,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
567
608
|
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
568
609
|
usageLog.push({ tool: name, tier: req._tier || 'paid', time: new Date().toISOString(), ip: ip.slice(0, 8) + '...' });
|
|
569
610
|
if (usageLog.length > 1000) usageLog.shift();
|
|
611
|
+
toolUsageCounts[name] = (toolUsageCounts[name] || 0) + 1;
|
|
570
612
|
saveStats();
|
|
571
613
|
const result = await executeTool(name, toolArgs || {});
|
|
572
614
|
if (req._accessWarning) result._notice = req._accessWarning;
|
|
@@ -577,12 +619,13 @@ const server = http.createServer(async (req, res) => {
|
|
|
577
619
|
const used = freeTierUsage.get(getMonthKey(ip)) || 0;
|
|
578
620
|
const remaining = FREE_TIER_LIMIT - used;
|
|
579
621
|
const isWarning = used >= FREE_TIER_WARNING;
|
|
622
|
+
const effectiveLimit = getEffectiveLimit(ip);
|
|
580
623
|
|
|
581
624
|
if (name === 'validate_vat' || name === 'validate_uk_vat') {
|
|
582
625
|
// Gate address on free tier — company name + valid status visible
|
|
583
626
|
const gated = ['registered_address', 'address', 'consultation_number'];
|
|
584
627
|
gated.forEach(f => delete result[f]);
|
|
585
|
-
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.';
|
|
586
629
|
result._gated_fields = gated;
|
|
587
630
|
}
|
|
588
631
|
|
|
@@ -590,7 +633,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
590
633
|
// Gate full reasoning — verdict visible, details gated
|
|
591
634
|
const gated = ['fraud_signals', 'positive_indicators', 'recommended_action', 'summary'];
|
|
592
635
|
gated.forEach(f => delete result[f]);
|
|
593
|
-
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.';
|
|
594
637
|
result._gated_fields = gated;
|
|
595
638
|
}
|
|
596
639
|
|
|
@@ -598,7 +641,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
598
641
|
// Gate detail fields — match_status visible, discrepancies gated
|
|
599
642
|
const gated = ['discrepancies', 'name_match', 'address_match', 'recommended_action', 'summary'];
|
|
600
643
|
gated.forEach(f => delete result[f]);
|
|
601
|
-
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.';
|
|
602
645
|
result._gated_fields = gated;
|
|
603
646
|
}
|
|
604
647
|
|