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 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
- | Pro | 5,000/month | $39/month |
158
- | Enterprise | Unlimited + batch | $199/month |
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.7",
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.7",
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.7",
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 validator for AI agents. EU VIES, UK HMRC, ABR. Invoice fraud detection included."
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 VERSION = '1.4.7';
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; // warn at 80% usage
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
- console.log('Stats loaded: ' + freeTierUsage.size + ' IPs, ' + usageLog.length + ' calls');
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
- return { valid: !!(d.Abn && d.AbnStatus === 'Active'), 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 };
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
- return Object.assign({}, result, { vat_number, _disclaimer: LEGAL_DISCLAIMER });
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.recommendation === 'REJECT' ? 'BLOCK_PAYMENT' : result.recommendation === 'REVIEW' ? 'MANUAL_REVIEW_REQUIRED' : 'PROCEED_WITH_PAYMENT';
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 calls = freeTierUsage.get(ip) || 0;
329
- if (calls >= FREE_TIER_LIMIT) return { allowed: false, reason: 'Free tier limit reached. Get 500 calls for $8 at ' + PRO_UPGRADE_URL + ' -- calls never expire.', upgrade_url: PRO_UPGRADE_URL, tier: 'free_limit_reached' };
330
- freeTierUsage.set(ip, calls + 1);
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
- return { allowed: true, tier: 'free', remaining, warning: remaining < 5 ? remaining + ' free validations remaining this month. Get 500 calls for $8 at ' + PRO_UPGRADE_URL + ' -- calls never expire.' : null };
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 toolCounts = {};
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: freeTierUsage.size, free_tier_total_calls: totalFreeCalls, paid_keys_issued: apiKeys.size, tool_usage: toolCounts, recent_calls: usageLog.slice(-20).reverse() }));
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 !== 'initialize' && request.method !== 'notifications/initialized') {
529
- if (request.method === 'tools/call' && request.params?.name === 'batch_validate') {
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 ' + FREE_TIER_LIMIT + ' calls remaining. Get 500 calls for $8 at ' + PRO_UPGRADE_URL + ' -- calls never expire. Includes full registered address and HMRC consultation number.';
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 ' + FREE_TIER_LIMIT + ' calls remaining. Get 500 calls for $8 at ' + PRO_UPGRADE_URL + ' -- calls never expire. Includes full fraud signal breakdown, positive indicators, and recommended action.';
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 ' + FREE_TIER_LIMIT + ' calls remaining. Get 500 calls for $8 at ' + PRO_UPGRADE_URL + ' -- calls never expire. Includes full discrepancy analysis and recommended action.';
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'));