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 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.8",
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
@@ -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';
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; // warn at 80% usage
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
- 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');
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
- 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 };
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
- 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 });
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.recommendation === 'REJECT' ? 'BLOCK_PAYMENT' : result.recommendation === 'REVIEW' ? 'MANUAL_REVIEW_REQUIRED' : 'PROCEED_WITH_PAYMENT';
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. Get 500 calls for $8 at ' + PRO_UPGRADE_URL + ' -- calls never expire.', upgrade_url: PRO_UPGRADE_URL, tier: 'free_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
- 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 };
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.end(JSON.stringify({ free_tier_unique_ips: freeUniqueIPs, free_tier_total_calls: totalFreeCalls, paid_keys_issued: apiKeys.size, tool_usage: toolCounts, recent_calls: usageLog.slice(-20).reverse() }));
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 !== 'initialize' && request.method !== 'notifications/initialized') {
549
- if (request.method === 'tools/call' && request.params?.name === 'batch_validate') {
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 ' + 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.';
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 ' + 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.';
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 ' + 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.';
602
645
  result._gated_fields = gated;
603
646
  }
604
647