vat-validator-mcp 1.4.12 → 2.0.1
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/glama.json +40 -14
- package/package.json +5 -2
- package/railway +0 -0
- package/server.json +2 -2
- package/smithery.yaml +25 -55
- package/src/server.js +456 -158
package/glama.json
CHANGED
|
@@ -1,19 +1,45 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
"
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
|
|
2
|
+
"$schema": "https://glama.ai/mcp/servers/schema.json",
|
|
3
|
+
"name": "VAT Validator MCP",
|
|
4
|
+
"description": "AI-powered VAT fraud detection and live VAT validation via EU VIES (27 member states), UK HMRC, and AU ABR. Call before invoice approval, supplier onboarding, or cross-border payment. Detects missing trader fraud, carousel fraud, deregistered entity re-use. Returns CLEAR/REVIEW/BLOCK verdict.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"categories": [
|
|
7
|
+
"finance",
|
|
8
|
+
"government-data",
|
|
9
|
+
"legal-and-compliance"
|
|
10
|
+
],
|
|
11
|
+
"remote": {
|
|
12
|
+
"transport": "sse",
|
|
13
|
+
"url": "https://vat-validator-mcp-production.up.railway.app/sse"
|
|
14
|
+
},
|
|
8
15
|
"tools": [
|
|
9
|
-
{
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
{
|
|
17
|
+
"name": "validate_vat",
|
|
18
|
+
"description": "Validates EU VAT numbers against EU VIES (all 27 member states) and AU ABR in real time. Returns valid/invalid, registered company name, address."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "validate_uk_vat",
|
|
22
|
+
"description": "Validates UK VAT numbers against HMRC VAT API v2 via OAuth2. Returns valid/invalid, registered business name, address."
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"name": "get_vat_rates",
|
|
26
|
+
"description": "Returns current standard, reduced, and zero VAT rates for all 27 EU member states and UK."
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "batch_validate",
|
|
30
|
+
"description": "Validates multiple VAT numbers against EU VIES and HMRC in one call. Returns per-number verdicts in structured JSON."
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "analyse_vat_risk",
|
|
34
|
+
"description": "AI-powered VAT fraud risk scoring. Detects missing trader fraud, carousel fraud, deregistered entity re-use. Returns CLEAR/REVIEW/BLOCK recommendation, risk score 0-100, fraud signals, agent_action (PROCEED/VERIFY_MANUALLY/HOLD)."
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "compare_invoice_details",
|
|
38
|
+
"description": "Cross-checks invoice VAT details against live VIES and HMRC registry data. Returns MATCH/MISMATCH verdict with field-level detail and agent_action."
|
|
39
|
+
}
|
|
13
40
|
],
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"enterprise": "$299/month — unlimited + batch"
|
|
41
|
+
"links": {
|
|
42
|
+
"homepage": "https://kordagencies.com",
|
|
43
|
+
"npm": "https://www.npmjs.com/package/vat-validator-mcp"
|
|
18
44
|
}
|
|
19
45
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vat-validator-mcp",
|
|
3
3
|
"mcpName": "io.github.OjasKord/vat-validator-mcp",
|
|
4
|
-
"version": "
|
|
5
|
-
"description": "VAT number
|
|
4
|
+
"version": "2.0.1",
|
|
5
|
+
"description": "VAT number validator for AI agents. EU VIES, UK HMRC, AU ABR — auto-detects jurisdiction. Fraud risk scoring and invoice name cross-check in one call.",
|
|
6
6
|
"main": "src/server.js",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"start": "node src/server.js"
|
|
@@ -40,5 +40,8 @@
|
|
|
40
40
|
},
|
|
41
41
|
"engines": {
|
|
42
42
|
"node": ">=18.0.0"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"stripe": "^22.1.1"
|
|
43
46
|
}
|
|
44
47
|
}
|
package/railway
ADDED
|
File without changes
|
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.13",
|
|
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.13",
|
|
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: "
|
|
1
|
+
description: "VAT number validator for AI agents. EU VIES, UK HMRC, AU ABR — auto-detects jurisdiction. Fraud risk scoring and invoice name cross-check in one call."
|
|
2
2
|
startCommand:
|
|
3
3
|
type: http
|
|
4
4
|
url: https://vat-validator-mcp-production.up.railway.app
|
|
@@ -7,24 +7,15 @@ startCommand:
|
|
|
7
7
|
properties:
|
|
8
8
|
apiKey:
|
|
9
9
|
type: string
|
|
10
|
-
description: "VAT Validator MCP API key from kordagencies.com. Leave blank for free tier (
|
|
10
|
+
description: "VAT Validator MCP API key from kordagencies.com. Leave blank for free tier (50 validations/month)."
|
|
11
11
|
x-from:
|
|
12
12
|
header: "x-api-key"
|
|
13
13
|
required: []
|
|
14
14
|
|
|
15
15
|
systemPrompt: |
|
|
16
|
-
VAT Validator MCP is a business identity verification and invoice fraud detection layer for AI agents.
|
|
16
|
+
VAT Validator MCP v2 is a business identity verification and invoice fraud detection layer for AI agents.
|
|
17
17
|
|
|
18
|
-
It combines official government VAT registry data (EU VIES, UK HMRC, Australian ABR) with AI-powered fraud analysis
|
|
19
|
-
|
|
20
|
-
TWO LAYERS OF INTELLIGENCE
|
|
21
|
-
|
|
22
|
-
Layer 1 - Government Registry Validation (validate_vat, validate_uk_vat, get_vat_rates, batch_validate)
|
|
23
|
-
Confirms a business is real and registered with official authorities.
|
|
24
|
-
|
|
25
|
-
Layer 2 - AI-Powered Fraud Analysis (analyse_vat_risk, compare_invoice_details)
|
|
26
|
-
Uses AI to synthesise registry data with transaction context to detect fraud signals that raw validation misses.
|
|
27
|
-
This is not a database lookup - it is intelligent analysis that catches subtle red flags.
|
|
18
|
+
It combines official government VAT registry data (EU VIES, UK HMRC, Australian ABR) with AI-powered fraud analysis in a single self-contained call. No chained tool calls required — validate_vat handles jurisdiction detection, live registry lookup, and fraud scoring internally.
|
|
28
19
|
|
|
29
20
|
WHEN TO USE THIS SERVER
|
|
30
21
|
|
|
@@ -36,55 +27,35 @@ systemPrompt: |
|
|
|
36
27
|
- Apply B2B tax exemptions in e-commerce
|
|
37
28
|
- Audit or enrich CRM company records
|
|
38
29
|
- Run periodic compliance checks on active counterparties
|
|
39
|
-
|
|
40
|
-
RECOMMENDED WORKFLOW FOR INVOICE PROCESSING
|
|
41
|
-
|
|
42
|
-
When your agent receives an invoice from a new or unverified supplier:
|
|
43
|
-
1. validate_vat - confirm the VAT number is real and active
|
|
44
|
-
2. compare_invoice_details - AI checks if invoice details match registry (catches impersonation fraud)
|
|
45
|
-
3. analyse_vat_risk - AI risk assessment with CLEAR/REVIEW/BLOCK recommendation
|
|
46
|
-
Only proceed with payment if recommendation is CLEAR.
|
|
47
|
-
|
|
48
|
-
RECOMMENDED WORKFLOW FOR SUPPLIER ONBOARDING
|
|
49
|
-
|
|
50
|
-
When adding a new supplier to your approved vendor list:
|
|
51
|
-
1. validate_vat - confirm registration
|
|
52
|
-
2. analyse_vat_risk - AI fraud signal check
|
|
53
|
-
3. batch_validate - periodic re-validation of all active suppliers monthly
|
|
30
|
+
- Calculate or verify VAT amounts on an invoice
|
|
54
31
|
|
|
55
32
|
TOOLS
|
|
56
33
|
|
|
57
34
|
validate_vat
|
|
58
|
-
-
|
|
59
|
-
-
|
|
60
|
-
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
-
|
|
64
|
-
- Free tier: first 20 calls/month, no API key needed
|
|
35
|
+
- Auto-detects jurisdiction from VAT number prefix: 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 (ABN)
|
|
36
|
+
- Validates against EU VIES, UK HMRC VAT API v2 (OAuth), or AU ABR
|
|
37
|
+
- Runs AI fraud risk analysis internally — returns score 0-100 and PROCEED/VERIFY_MANUALLY/HOLD verdict
|
|
38
|
+
- Optional: pass invoice_company_name to cross-check against registry and detect name mismatches
|
|
39
|
+
- Optional: pass invoice_amount to weight fraud risk scoring
|
|
40
|
+
- Free tier: first 50 calls/month, no API key needed
|
|
65
41
|
|
|
66
42
|
get_vat_rates
|
|
67
|
-
-
|
|
68
|
-
-
|
|
43
|
+
- Returns current standard and reduced VAT rates for all 27 EU member states, UK, and Australia
|
|
44
|
+
- Omit country_code to retrieve all countries at once
|
|
45
|
+
- Free tier: first 50 calls/month, no API key needed
|
|
69
46
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
47
|
+
WORKFLOW FOR INVOICE PROCESSING
|
|
48
|
+
|
|
49
|
+
When your agent receives an invoice from a new or unverified supplier:
|
|
50
|
+
1. Call validate_vat with vat_number AND invoice_company_name (and optionally invoice_amount)
|
|
51
|
+
2. Check agent_action: PROCEED = clear to pay, VERIFY_MANUALLY = human review needed, HOLD = block payment
|
|
52
|
+
That is the complete workflow — one call returns everything needed.
|
|
74
53
|
|
|
75
|
-
|
|
76
|
-
- Call after validate_vat when you need a fraud risk assessment
|
|
77
|
-
- AI analyses registry data + transaction context for fraud signals
|
|
78
|
-
- Returns CLEAR/REVIEW/BLOCK recommendation with specific reasons
|
|
79
|
-
- Catches: name mismatches, newly registered companies, dormant status, shell company indicators
|
|
80
|
-
- Free tier: first 20 calls/month, no API key needed
|
|
54
|
+
WORKFLOW FOR VAT CALCULATION
|
|
81
55
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
- Flags discrepancies that indicate fraud, impersonation, or error
|
|
86
|
-
- Returns APPROVE/REVIEW/REJECT recommendation
|
|
87
|
-
- Free tier: first 20 calls/month, no API key needed
|
|
56
|
+
When your agent needs to calculate or verify a VAT amount:
|
|
57
|
+
1. Call get_vat_rates with the relevant country_code
|
|
58
|
+
2. Apply the returned standard or reduced rate
|
|
88
59
|
|
|
89
60
|
LEGAL NOTICE
|
|
90
61
|
All results are for informational purposes only and do not constitute legal or tax advice.
|
|
@@ -92,5 +63,4 @@ systemPrompt: |
|
|
|
92
63
|
Full terms: kordagencies.com/terms.html
|
|
93
64
|
|
|
94
65
|
FREE TIER
|
|
95
|
-
|
|
96
|
-
Upgrade at kordagencies.com - Pro $99/month (5,000 calls), Enterprise $299/month (unlimited + batch).
|
|
66
|
+
50 calls/month with no API key. Upgrade at kordagencies.com.
|
package/src/server.js
CHANGED
|
@@ -2,26 +2,28 @@ const http = require('http');
|
|
|
2
2
|
const https = require('https');
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const fs = require('fs');
|
|
5
|
+
const Stripe = require('stripe');
|
|
6
|
+
const stripe = Stripe(process.env.STRIPE_SECRET_KEY);
|
|
5
7
|
|
|
6
8
|
const PERSIST_FILE = '/tmp/vat_stats.json';
|
|
7
|
-
const
|
|
8
|
-
const VERSION = '1.4.12';
|
|
9
|
-
const PRO_UPGRADE_URL = 'https://buy.stripe.com/28EeVceUB06N1ty3teebu0l';
|
|
10
|
-
const ENTERPRISE_UPGRADE_URL = 'https://buy.stripe.com/00w14m7s96vb1ty5Bmebu0m';
|
|
9
|
+
const VERSION = '2.0.1';
|
|
11
10
|
const RESEND_API_KEY = process.env.RESEND_API_KEY || '';
|
|
12
11
|
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || '';
|
|
13
12
|
const PORT = process.env.PORT || 3000;
|
|
14
13
|
const STATS_KEY = process.env.STATS_KEY || 'ojas2026';
|
|
14
|
+
const REDIS_PREFIX = 'vat';
|
|
15
|
+
const FREE_TIER_LIMIT = 50;
|
|
16
|
+
const METERED_SUBSCRIBE_URL = 'https://vat-validator-mcp-production.up.railway.app/subscribe';
|
|
17
|
+
const BUNDLE_500_URL = 'https://buy.stripe.com/28EeVceUB06N1ty3teebu0l';
|
|
18
|
+
const BUNDLE_2000_URL = 'https://buy.stripe.com/00w14m7s96vb1ty5Bmebu0m';
|
|
15
19
|
|
|
16
20
|
const freeTierUsage = new Map();
|
|
17
21
|
const usageLog = [];
|
|
18
22
|
const toolUsageCounts = {};
|
|
19
23
|
const trialExtensions = new Map();
|
|
20
|
-
const
|
|
21
|
-
const FREE_TIER_WARNING = 16;
|
|
24
|
+
const FREE_TIER_WARNING = 40;
|
|
22
25
|
const TRIAL_EXTENSION_CALLS = 10;
|
|
23
26
|
const apiKeys = new Map();
|
|
24
|
-
const PLAN_LIMITS = { pro: 5000, enterprise: Infinity };
|
|
25
27
|
|
|
26
28
|
function saveStats() {
|
|
27
29
|
try {
|
|
@@ -56,27 +58,98 @@ function getEffectiveLimit(ip) {
|
|
|
56
58
|
return FREE_TIER_LIMIT;
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
const UPSTASH_URL = process.env.UPSTASH_REDIS_REST_URL;
|
|
62
|
+
const UPSTASH_TOKEN = process.env.UPSTASH_REDIS_REST_TOKEN;
|
|
63
|
+
|
|
64
|
+
async function redisGet(key) {
|
|
65
|
+
try {
|
|
66
|
+
const res = await fetch(
|
|
67
|
+
`${UPSTASH_URL}/get/${encodeURIComponent(key)}`,
|
|
68
|
+
{ headers: { Authorization: `Bearer ${UPSTASH_TOKEN}` } }
|
|
69
|
+
);
|
|
70
|
+
const data = await res.json();
|
|
71
|
+
if (!data.result) return null;
|
|
72
|
+
return JSON.parse(data.result);
|
|
73
|
+
} catch(e) { return null; }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function redisSet(key, value) {
|
|
77
|
+
try {
|
|
78
|
+
await fetch(
|
|
79
|
+
`${UPSTASH_URL}/set/${encodeURIComponent(key)}`,
|
|
80
|
+
{
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: {
|
|
83
|
+
Authorization: `Bearer ${UPSTASH_TOKEN}`,
|
|
84
|
+
'Content-Type': 'application/json'
|
|
85
|
+
},
|
|
86
|
+
body: JSON.stringify({ value: JSON.stringify(value) })
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
} catch(e) {}
|
|
61
90
|
}
|
|
62
91
|
|
|
63
|
-
function
|
|
92
|
+
async function redisKeys(pattern) {
|
|
64
93
|
try {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
94
|
+
const res = await fetch(
|
|
95
|
+
`${UPSTASH_URL}/keys/${encodeURIComponent(pattern)}`,
|
|
96
|
+
{ headers: { Authorization: `Bearer ${UPSTASH_TOKEN}` } }
|
|
97
|
+
);
|
|
98
|
+
const data = await res.json();
|
|
99
|
+
return data.result || [];
|
|
100
|
+
} catch(e) { return []; }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function saveKeyToRedis(apiKey, record, prefix) {
|
|
104
|
+
await redisSet(`${prefix}:key:${apiKey}`, record);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function loadApiKeysFromRedis(prefix) {
|
|
108
|
+
const keys = await redisKeys(`${prefix}:key:*`);
|
|
109
|
+
for (const redisKey of keys) {
|
|
110
|
+
const record = await redisGet(redisKey);
|
|
111
|
+
if (record) {
|
|
112
|
+
const apiKey = redisKey.replace(`${prefix}:key:`, '');
|
|
113
|
+
apiKeys.set(apiKey, record);
|
|
69
114
|
}
|
|
70
|
-
}
|
|
115
|
+
}
|
|
116
|
+
console.log(`Loaded ${apiKeys.size} API keys from Redis`);
|
|
71
117
|
}
|
|
72
118
|
|
|
73
119
|
function generateApiKey() { return 'vat_' + crypto.randomBytes(24).toString('hex'); }
|
|
74
|
-
function getPlanFromProduct(
|
|
75
|
-
if (!
|
|
76
|
-
|
|
120
|
+
function getPlanFromProduct(productName) {
|
|
121
|
+
if (!productName) return 'bundle_500';
|
|
122
|
+
const n = productName.toLowerCase();
|
|
123
|
+
if (n.includes('metered') || n.includes('pay as you go') || n === 'metered') return 'metered';
|
|
124
|
+
if (n.includes('2000') || n.includes('2,000') || n.includes('enterprise')) return 'bundle_2000';
|
|
125
|
+
return 'bundle_500';
|
|
77
126
|
}
|
|
78
127
|
function nowISO() { return new Date().toISOString(); }
|
|
79
128
|
|
|
129
|
+
function checkAndResetPeriod(record) {
|
|
130
|
+
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
|
|
131
|
+
if (Date.now() - record.periodStart > thirtyDays) {
|
|
132
|
+
record.calls = 0;
|
|
133
|
+
record.periodStart = Date.now();
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function reportMeteredUsage(customerId, eventName) {
|
|
140
|
+
try {
|
|
141
|
+
await stripe.billing.meterEvents.create({
|
|
142
|
+
event_name: eventName,
|
|
143
|
+
payload: {
|
|
144
|
+
stripe_customer_id: customerId,
|
|
145
|
+
value: '1'
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
} catch(e) {
|
|
149
|
+
console.error('Stripe metered usage report failed:', e.message);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
80
153
|
async function sendEmail(to, subject, html) {
|
|
81
154
|
return new Promise((resolve) => {
|
|
82
155
|
const body = JSON.stringify({ from: 'VAT Validator MCP <ojas@kordagencies.com>', to: [to], subject, html });
|
|
@@ -90,10 +163,10 @@ async function sendEmail(to, subject, html) {
|
|
|
90
163
|
}
|
|
91
164
|
|
|
92
165
|
async function sendApiKeyEmail(email, apiKey, plan) {
|
|
93
|
-
const planLabel = plan === '
|
|
94
|
-
const
|
|
95
|
-
const html = '<!DOCTYPE html><html><body style="font-family:monospace;background:#080A0F;color:#E8EDF5;padding:40px;max-width:600px;margin:0 auto"><div style="border:1px solid rgba(0,229,195,0.3);border-radius:8px;padding:32px"><div style="color:#00E5C3;font-size:13px;letter-spacing:0.2em;text-transform:uppercase;margin-bottom:24px">VAT Validator MCP - ' + planLabel + '
|
|
96
|
-
return sendEmail(email, 'Your VAT Validator MCP
|
|
166
|
+
const planLabel = plan === 'metered' ? 'Pay-as-you-go' : plan === 'bundle_2000' ? 'Bundle 2000' : 'Bundle 500';
|
|
167
|
+
const limitNote = plan === 'metered' ? 'Pay only for what you use — billed monthly' : plan === 'bundle_2000' ? '2,000 calls included' : '500 calls included';
|
|
168
|
+
const html = '<!DOCTYPE html><html><body style="font-family:monospace;background:#080A0F;color:#E8EDF5;padding:40px;max-width:600px;margin:0 auto"><div style="border:1px solid rgba(0,229,195,0.3);border-radius:8px;padding:32px"><div style="color:#00E5C3;font-size:13px;letter-spacing:0.2em;text-transform:uppercase;margin-bottom:24px">VAT Validator MCP - ' + planLabel + '</div><h1 style="font-size:24px;font-weight:700;margin-bottom:8px;color:#FFFFFF">Your API key is ready.</h1><div style="background:#141B24;border:1px solid rgba(255,255,255,0.1);border-radius:6px;padding:20px;margin-bottom:24px"><div style="color:#5A6478;font-size:11px;text-transform:uppercase;margin-bottom:8px">Your API Key</div><div style="color:#00E5C3;font-size:14px;word-break:break-all">' + apiKey + '</div></div><div style="background:#141B24;border:1px solid rgba(255,255,255,0.1);border-radius:6px;padding:20px;margin-bottom:24px"><div style="color:#5A6478;font-size:11px;text-transform:uppercase;margin-bottom:8px">MCP Config</div><div style="color:#86EFAC;font-size:12px">{"vat-validator":{"url":"https://vat-validator-mcp-production.up.railway.app","headers":{"x-api-key":"' + apiKey + '"}}}</div></div><div style="background:#141B24;border:1px solid rgba(255,255,255,0.1);border-radius:6px;padding:20px;margin-bottom:24px"><div style="color:#E8EDF5;font-size:13px">Plan: ' + planLabel + '<br>' + limitNote + '</div></div><div style="background:#0D1219;border-radius:6px;padding:16px;margin-bottom:24px;font-size:11px;color:#5A6478;line-height:1.7">Results are informational only. Verify with a qualified tax advisor. Liability capped at 3 months fees. Full terms: kordagencies.com/terms.html</div><p style="color:#5A6478;font-size:12px">Questions? ojas@kordagencies.com</p></div></body></html>';
|
|
169
|
+
return sendEmail(email, 'Your VAT Validator MCP API Key — ' + planLabel, html);
|
|
97
170
|
}
|
|
98
171
|
|
|
99
172
|
async function callClaude(prompt) {
|
|
@@ -225,6 +298,7 @@ async function validateABN(abn) {
|
|
|
225
298
|
function detectCountry(vatNumber) {
|
|
226
299
|
const clean = vatNumber.trim().toUpperCase().replace(/\s/g, '');
|
|
227
300
|
if (clean.startsWith('GB')) return { country: 'GB', type: 'uk', number: clean.slice(2) };
|
|
301
|
+
if (clean.startsWith('ABN')) return { country: 'AU', type: 'au', number: clean.slice(3) };
|
|
228
302
|
if (clean.startsWith('AU') || /^\d{11}$/.test(clean)) return { country: 'AU', type: 'au', number: clean };
|
|
229
303
|
const euCodes = ['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'];
|
|
230
304
|
for (const code of euCodes) {
|
|
@@ -255,116 +329,262 @@ const VAT_RATES = {
|
|
|
255
329
|
|
|
256
330
|
async function executeTool(name, args) {
|
|
257
331
|
if (name === 'validate_vat') {
|
|
258
|
-
const vat_number = args
|
|
332
|
+
const { vat_number, invoice_company_name, invoice_amount } = args;
|
|
259
333
|
const checkedAt = nowISO();
|
|
260
|
-
|
|
334
|
+
|
|
335
|
+
if (!vat_number) return {
|
|
336
|
+
error: 'vat_number is required',
|
|
337
|
+
agent_action: 'PROVIDE_REQUIRED_FIELD',
|
|
338
|
+
category: 'invalid_input',
|
|
339
|
+
retryable: false,
|
|
340
|
+
retry_after_ms: null,
|
|
341
|
+
fallback_tool: null,
|
|
342
|
+
trace_id: Math.random().toString(36).slice(2, 10)
|
|
343
|
+
};
|
|
344
|
+
|
|
261
345
|
const detected = detectCountry(vat_number);
|
|
346
|
+
let valid = false;
|
|
347
|
+
let company_name = null;
|
|
348
|
+
let address = null;
|
|
349
|
+
let jurisdiction = '';
|
|
350
|
+
let sourceUrl = '';
|
|
351
|
+
|
|
262
352
|
if (detected.type === 'uk') {
|
|
353
|
+
jurisdiction = 'UK';
|
|
354
|
+
sourceUrl = 'api.service.hmrc.gov.uk';
|
|
263
355
|
const result = await validateHMRC(detected.number);
|
|
264
|
-
if (result.error) return {
|
|
356
|
+
if (result.error) return {
|
|
357
|
+
error: result.error,
|
|
358
|
+
vat_number,
|
|
359
|
+
jurisdiction,
|
|
360
|
+
agent_action: 'RETRY_IN_2_MIN',
|
|
361
|
+
category: 'upstream_unavailable',
|
|
362
|
+
retryable: true,
|
|
363
|
+
retry_after_ms: 120000,
|
|
364
|
+
fallback_tool: null,
|
|
365
|
+
trace_id: Math.random().toString(36).slice(2, 10),
|
|
366
|
+
source_url: sourceUrl,
|
|
367
|
+
checked_at: checkedAt,
|
|
368
|
+
_disclaimer: LEGAL_DISCLAIMER
|
|
369
|
+
};
|
|
265
370
|
const d = result.data;
|
|
266
|
-
if (result.status === 200 && d.target)
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
371
|
+
if (result.status === 200 && d.target) {
|
|
372
|
+
valid = true;
|
|
373
|
+
company_name = d.target.name || null;
|
|
374
|
+
address = d.target.address ? Object.values(d.target.address).filter(Boolean).join(', ') : null;
|
|
375
|
+
}
|
|
376
|
+
} else if (detected.type === 'eu') {
|
|
377
|
+
jurisdiction = 'EU';
|
|
378
|
+
sourceUrl = 'ec.europa.eu/taxation_customs/vies';
|
|
270
379
|
const result = await validateVIES(detected.country, detected.number);
|
|
271
|
-
if (result.error) return {
|
|
380
|
+
if (result.error) return {
|
|
381
|
+
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.',
|
|
382
|
+
vat_number,
|
|
383
|
+
jurisdiction,
|
|
384
|
+
agent_action: 'RETRY_IN_30_MIN',
|
|
385
|
+
category: 'upstream_unavailable',
|
|
386
|
+
retryable: true,
|
|
387
|
+
retry_after_ms: 1800000,
|
|
388
|
+
fallback_tool: null,
|
|
389
|
+
trace_id: Math.random().toString(36).slice(2, 10),
|
|
390
|
+
source_url: sourceUrl,
|
|
391
|
+
checked_at: checkedAt,
|
|
392
|
+
_disclaimer: LEGAL_DISCLAIMER
|
|
393
|
+
};
|
|
272
394
|
const d = result.data;
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
395
|
+
valid = d.isValid || false;
|
|
396
|
+
company_name = d.traderName || null;
|
|
397
|
+
address = d.traderAddress || null;
|
|
398
|
+
} else if (detected.type === 'au') {
|
|
399
|
+
jurisdiction = 'AU';
|
|
400
|
+
sourceUrl = 'abr.business.gov.au';
|
|
276
401
|
const result = await validateABN(detected.number);
|
|
277
|
-
if (result.error) return {
|
|
402
|
+
if (result.error) return {
|
|
403
|
+
error: result.error,
|
|
404
|
+
vat_number,
|
|
405
|
+
jurisdiction,
|
|
406
|
+
agent_action: 'RETRY_IN_2_MIN',
|
|
407
|
+
category: 'upstream_unavailable',
|
|
408
|
+
retryable: true,
|
|
409
|
+
retry_after_ms: 120000,
|
|
410
|
+
fallback_tool: null,
|
|
411
|
+
trace_id: Math.random().toString(36).slice(2, 10),
|
|
412
|
+
source_url: sourceUrl,
|
|
413
|
+
checked_at: checkedAt,
|
|
414
|
+
_disclaimer: LEGAL_DISCLAIMER
|
|
415
|
+
};
|
|
278
416
|
const d = result.data;
|
|
279
|
-
|
|
280
|
-
|
|
417
|
+
valid = !!(d.Abn && d.AbnStatus === 'Active');
|
|
418
|
+
company_name = d.EntityName || null;
|
|
419
|
+
} else {
|
|
420
|
+
return {
|
|
421
|
+
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 or ABN).',
|
|
422
|
+
vat_number,
|
|
423
|
+
agent_action: 'PROVIDE_COUNTRY_PREFIX',
|
|
424
|
+
category: 'invalid_input',
|
|
425
|
+
retryable: false,
|
|
426
|
+
retry_after_ms: null,
|
|
427
|
+
fallback_tool: null,
|
|
428
|
+
trace_id: Math.random().toString(36).slice(2, 10),
|
|
429
|
+
_disclaimer: LEGAL_DISCLAIMER
|
|
430
|
+
};
|
|
281
431
|
}
|
|
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 };
|
|
283
|
-
}
|
|
284
432
|
|
|
285
|
-
|
|
286
|
-
const
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
const result = await validateHMRC(vat_number);
|
|
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 };
|
|
291
|
-
const d = result.data;
|
|
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 };
|
|
294
|
-
}
|
|
433
|
+
// AI fraud risk analysis — runs internally, result is always returned in one call
|
|
434
|
+
const nameSection = invoice_company_name ? `Invoice Company Name: ${invoice_company_name}\n` : '';
|
|
435
|
+
const amountSection = invoice_amount != null ? `Invoice Amount: ${invoice_amount}\n` : '';
|
|
436
|
+
const prompt = `You are a B2B fraud detection specialist. Analyze this VAT validation result for fraud risk.
|
|
295
437
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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 });
|
|
304
|
-
}
|
|
438
|
+
VAT Number: ${vat_number}
|
|
439
|
+
Jurisdiction: ${jurisdiction}
|
|
440
|
+
Valid/Active: ${valid}
|
|
441
|
+
Registered Company Name: ${company_name || 'Not available from registry'}
|
|
442
|
+
Registered Address: ${address || 'Not available from registry'}
|
|
443
|
+
${nameSection}${amountSection}
|
|
444
|
+
Analyze for: registration status, jurisdiction risk factors, name mismatch between invoice and registry (if invoice company name provided), address anomalies, shell company indicators, missing trader fraud patterns, recently registered entity risk.
|
|
305
445
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
446
|
+
Name match rules: if no invoice_company_name was provided set name_match to "NOT_CHECKED". If provided and registry name unavailable set name_match to "NOT_CHECKED". If both available compare them: "MATCH" if they clearly refer to the same company (allow abbreviations and legal suffix variations), "MISMATCH" if clearly different companies.
|
|
447
|
+
|
|
448
|
+
recommendation must be exactly one of: CLEAR, REVIEW, or BLOCK. No other values permitted. CLEAR = valid, low risk. REVIEW = valid but requires manual verification. BLOCK = invalid or high/critical risk.
|
|
449
|
+
|
|
450
|
+
Return ONLY valid JSON with no preamble or markdown:
|
|
451
|
+
{"fraud_risk_score":0,"fraud_risk_level":"LOW","fraud_signals":[],"name_match":"NOT_CHECKED","recommendation":"CLEAR","summary":"one sentence plain English"}`;
|
|
452
|
+
|
|
453
|
+
let fraudRiskScore = 50;
|
|
454
|
+
let fraudRiskLevel = 'MEDIUM';
|
|
455
|
+
let fraudSignals = [];
|
|
456
|
+
let nameMatch = 'NOT_CHECKED';
|
|
457
|
+
let recommendation = 'REVIEW';
|
|
458
|
+
let summary = 'Manual review recommended — AI analysis unavailable.';
|
|
316
459
|
|
|
317
|
-
if (name === 'analyse_vat_risk') {
|
|
318
|
-
const vat_number = args.vat_number;
|
|
319
|
-
const validation_result = args.validation_result;
|
|
320
|
-
const invoice_amount = args.invoice_amount;
|
|
321
|
-
const invoice_company_name = args.invoice_company_name;
|
|
322
|
-
if (!vat_number || !validation_result) return { error: 'vat_number and validation_result are required', 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) };
|
|
323
|
-
const prompt = 'You are a B2B fraud detection specialist. Analyse this VAT validation result for fraud signals.\n\nVAT Number: ' + vat_number + '\nValidation Result: ' + JSON.stringify(validation_result) + '\nInvoice Amount: ' + (invoice_amount ? String(invoice_amount) : 'Not provided') + '\nInvoice Company Name: ' + (invoice_company_name || 'Not provided') + '\nRegistered Company Name: ' + (validation_result.company_name || 'Not available') + '\nValid: ' + validation_result.valid + '\nCountry: ' + validation_result.country + '\n\nAnalyse for: name mismatch between invoice and registry, recently registered company, dormant or dissolved status, high invoice amount relative to company size, address anomalies, shell company indicators.\n\nReturn ONLY valid JSON with no preamble: {"recommendation":"CLEAR|REVIEW|BLOCK","risk_level":"LOW|MEDIUM|HIGH|CRITICAL","risk_score":50,"fraud_signals":[],"positive_indicators":[],"recommended_action":"one sentence","summary":"two sentences"}';
|
|
324
460
|
try {
|
|
325
|
-
const
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
461
|
+
const aiResponse = await callClaude(prompt);
|
|
462
|
+
const parsed = JSON.parse(aiResponse.replace(/```json|```/g, '').trim());
|
|
463
|
+
fraudRiskScore = typeof parsed.fraud_risk_score === 'number' ? parsed.fraud_risk_score : fraudRiskScore;
|
|
464
|
+
fraudRiskLevel = parsed.fraud_risk_level || fraudRiskLevel;
|
|
465
|
+
fraudSignals = Array.isArray(parsed.fraud_signals) ? parsed.fraud_signals : [];
|
|
466
|
+
nameMatch = parsed.name_match || nameMatch;
|
|
467
|
+
recommendation = parsed.recommendation || recommendation;
|
|
468
|
+
summary = parsed.summary || summary;
|
|
329
469
|
} catch(e) {
|
|
330
|
-
|
|
470
|
+
if (!valid) {
|
|
471
|
+
fraudRiskLevel = 'HIGH';
|
|
472
|
+
fraudRiskScore = 75;
|
|
473
|
+
fraudSignals = ['VAT number invalid or deregistered'];
|
|
474
|
+
recommendation = 'BLOCK';
|
|
475
|
+
summary = 'VAT number is invalid or deregistered.';
|
|
476
|
+
}
|
|
331
477
|
}
|
|
332
|
-
}
|
|
333
478
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const agentAction = result.match_verdict === 'MATCH' ? 'PROCEED' : 'INVESTIGATE';
|
|
342
|
-
return Object.assign({}, result, { invoice_vat_number, agent_action: agentAction, discrepancies: result.discrepancies || [], _disclaimer: LEGAL_DISCLAIMER });
|
|
343
|
-
} catch(e) {
|
|
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 };
|
|
479
|
+
let agentAction;
|
|
480
|
+
if (!valid || fraudRiskLevel === 'CRITICAL' || nameMatch === 'MISMATCH') {
|
|
481
|
+
agentAction = 'HOLD';
|
|
482
|
+
} else if (fraudRiskLevel === 'HIGH' || fraudRiskLevel === 'MEDIUM') {
|
|
483
|
+
agentAction = 'VERIFY_MANUALLY';
|
|
484
|
+
} else {
|
|
485
|
+
agentAction = 'PROCEED';
|
|
345
486
|
}
|
|
487
|
+
|
|
488
|
+
return {
|
|
489
|
+
agent_action: agentAction,
|
|
490
|
+
valid,
|
|
491
|
+
vat_number,
|
|
492
|
+
jurisdiction,
|
|
493
|
+
company_name,
|
|
494
|
+
address,
|
|
495
|
+
fraud_risk_score: fraudRiskScore,
|
|
496
|
+
fraud_risk_level: fraudRiskLevel,
|
|
497
|
+
fraud_signals: fraudSignals,
|
|
498
|
+
name_match: nameMatch,
|
|
499
|
+
recommendation,
|
|
500
|
+
summary,
|
|
501
|
+
source_url: sourceUrl,
|
|
502
|
+
checked_at: checkedAt,
|
|
503
|
+
_disclaimer: LEGAL_DISCLAIMER,
|
|
504
|
+
ai_notice: 'AI-powered fraud analysis — NOT a simple database lookup'
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (name === 'get_vat_rates') {
|
|
509
|
+
const country_code = args.country_code;
|
|
510
|
+
const checkedAt = nowISO();
|
|
511
|
+
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: 'taxation-customs.ec.europa.eu/tedb/taxes-list.html', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
512
|
+
const code = country_code.toUpperCase();
|
|
513
|
+
const rate = VAT_RATES[code];
|
|
514
|
+
if (!rate) return { error: 'No VAT rate data for: ' + code + '. Supported: ' + Object.keys(VAT_RATES).join(', '), 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 };
|
|
515
|
+
return Object.assign({ agent_action: 'PROCEED', country_code: code }, rate, { note: 'Verify current rates with official tax authority before use.', source_url: 'taxation-customs.ec.europa.eu/tedb/taxes-list.html', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER });
|
|
346
516
|
}
|
|
347
517
|
|
|
348
|
-
return { error: 'Unknown tool: ' + name,
|
|
518
|
+
return { error: 'Unknown tool: ' + name, agent_action: 'RETRY_IN_2_MIN', category: 'unknown_tool', retryable: false, retry_after_ms: null, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10) };
|
|
349
519
|
}
|
|
350
520
|
|
|
351
521
|
function checkAccess(req) {
|
|
352
522
|
const apiKey = req.headers['x-api-key'];
|
|
353
523
|
if (apiKey) {
|
|
354
524
|
const record = apiKeys.get(apiKey);
|
|
355
|
-
if (!record)
|
|
356
|
-
|
|
525
|
+
if (!record) {
|
|
526
|
+
return { allowed: false, error: 'Invalid API key' };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const wasReset = checkAndResetPeriod(record);
|
|
530
|
+
if (wasReset) {
|
|
531
|
+
saveKeyToRedis(apiKey, record, REDIS_PREFIX).catch(() => {});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (record.plan === 'metered') {
|
|
535
|
+
record.calls++;
|
|
536
|
+
saveKeyToRedis(apiKey, record, REDIS_PREFIX).catch(() => {});
|
|
537
|
+
return {
|
|
538
|
+
allowed: true,
|
|
539
|
+
paid: true,
|
|
540
|
+
plan: 'metered',
|
|
541
|
+
stripeCustomerId: record.stripeCustomerId
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (record.calls >= record.limit) {
|
|
546
|
+
return {
|
|
547
|
+
allowed: false,
|
|
548
|
+
error: `Bundle exhausted. You have used all ${record.limit} calls in this bundle. Purchase another bundle or switch to pay-as-you-go.`,
|
|
549
|
+
subscribe_url: METERED_SUBSCRIBE_URL,
|
|
550
|
+
bundle_500_url: BUNDLE_500_URL,
|
|
551
|
+
bundle_2000_url: BUNDLE_2000_URL,
|
|
552
|
+
agent_action: 'PAUSE_AND_NOTIFY_USER'
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
357
556
|
record.calls++;
|
|
358
|
-
|
|
557
|
+
saveKeyToRedis(apiKey, record, REDIS_PREFIX).catch(() => {});
|
|
558
|
+
return { allowed: true, paid: true, plan: record.plan };
|
|
359
559
|
}
|
|
360
560
|
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
361
561
|
const monthKey = getMonthKey(ip);
|
|
362
562
|
const calls = freeTierUsage.get(monthKey) || 0;
|
|
363
|
-
if (calls >= FREE_TIER_LIMIT) return {
|
|
563
|
+
if (calls >= FREE_TIER_LIMIT) return {
|
|
564
|
+
allowed: false,
|
|
565
|
+
error: 'Free tier limit of 50 calls/month reached.',
|
|
566
|
+
options: {
|
|
567
|
+
pay_as_you_go: {
|
|
568
|
+
description: 'No commitment. Pay only for what you use. Billed monthly at end of period.',
|
|
569
|
+
pricing: {
|
|
570
|
+
vat_query: '$0.010 per query'
|
|
571
|
+
},
|
|
572
|
+
subscribe_url: METERED_SUBSCRIBE_URL
|
|
573
|
+
},
|
|
574
|
+
bundle: {
|
|
575
|
+
description: 'Buy a fixed call bundle. No subscription.',
|
|
576
|
+
options: [
|
|
577
|
+
{ calls: 500, price: '$8', url: BUNDLE_500_URL },
|
|
578
|
+
{ calls: 2000, price: '$28', url: BUNDLE_2000_URL }
|
|
579
|
+
]
|
|
580
|
+
}
|
|
581
|
+
},
|
|
582
|
+
agent_action: 'PAUSE_AND_NOTIFY_USER'
|
|
583
|
+
};
|
|
364
584
|
freeTierUsage.set(monthKey, calls + 1);
|
|
365
585
|
saveStats();
|
|
366
586
|
const remaining = FREE_TIER_LIMIT - calls - 1;
|
|
367
|
-
const warningMsg = remaining <
|
|
587
|
+
const warningMsg = remaining < 10 ? remaining + ' free validations remaining this month. Get 500 calls for $8 at ' + BUNDLE_500_URL + ' -- calls never expire.' : null;
|
|
368
588
|
return { allowed: true, tier: 'free', remaining, warning: warningMsg };
|
|
369
589
|
}
|
|
370
590
|
|
|
@@ -400,28 +620,67 @@ async function handleStripeWebhook(body, sig) {
|
|
|
400
620
|
const event = JSON.parse(body);
|
|
401
621
|
if (event.type === 'checkout.session.completed') {
|
|
402
622
|
const session = event.data.object;
|
|
403
|
-
const
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
623
|
+
const plan = getPlanFromProduct(session.metadata?.product_name);
|
|
624
|
+
const apiKey = generateApiKey();
|
|
625
|
+
const limit = plan === 'metered' ? null : plan === 'bundle_2000' ? 2000 : 500;
|
|
626
|
+
const record = {
|
|
627
|
+
email: session.customer_details?.email || 'unknown',
|
|
628
|
+
plan,
|
|
629
|
+
calls: 0,
|
|
630
|
+
periodStart: Date.now(),
|
|
631
|
+
limit,
|
|
632
|
+
stripeCustomerId: session.customer || null,
|
|
633
|
+
createdAt: Date.now()
|
|
634
|
+
};
|
|
635
|
+
apiKeys.set(apiKey, record);
|
|
636
|
+
await saveKeyToRedis(apiKey, record, REDIS_PREFIX);
|
|
637
|
+
await sendApiKeyEmail(record.email, apiKey, plan);
|
|
638
|
+
console.log('[vat] API key created for ' + record.email + ' (' + plan + ')');
|
|
639
|
+
return { success: true, email: record.email, plan };
|
|
640
|
+
}
|
|
641
|
+
if (event.type === 'customer.subscription.created') {
|
|
642
|
+
const sub = event.data.object;
|
|
643
|
+
const customerId = sub.customer;
|
|
644
|
+
for (const [key, record] of apiKeys.entries()) {
|
|
645
|
+
if (record.stripeCustomerId === customerId && !record.subscriptionId) {
|
|
646
|
+
record.subscriptionId = sub.id;
|
|
647
|
+
await saveKeyToRedis(key, record, REDIS_PREFIX);
|
|
648
|
+
break;
|
|
649
|
+
}
|
|
412
650
|
}
|
|
651
|
+
return { received: true, type: event.type };
|
|
413
652
|
}
|
|
414
653
|
return { received: true, type: event.type };
|
|
415
654
|
} catch(e) { console.error('[vat] Webhook error:', e.message); return { error: e.message, status: 400 }; }
|
|
416
655
|
}
|
|
417
656
|
|
|
418
657
|
const tools = [
|
|
419
|
-
{
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
658
|
+
{
|
|
659
|
+
name: 'validate_vat',
|
|
660
|
+
idempotent: true,
|
|
661
|
+
description: 'Call this BEFORE your agent approves any supplier invoice, onboards a new EU/UK/AU vendor, or processes any cross-border payment. Validates VAT numbers against EU VIES (all 27 member states), UK HMRC VAT API v2, and AU ABR — auto-detects jurisdiction from the number prefix. Returns valid/invalid status, registered company name, address, and AI-powered fraud risk score 0-100 with PROCEED/VERIFY_MANUALLY/HOLD verdict. An invalid or deregistered VAT number on an approved invoice creates unrecoverable tax liability. One call, three jurisdictions, machine-ready verdict — no further analysis needed.',
|
|
662
|
+
inputSchema: {
|
|
663
|
+
type: 'object',
|
|
664
|
+
properties: {
|
|
665
|
+
vat_number: { type: 'string', description: 'VAT number with country prefix. EU: DE123456789. UK: GB123456789. AU: ABN12345678901.' },
|
|
666
|
+
invoice_company_name: { type: 'string', description: 'Company name as it appears on the invoice — if provided, cross-checks against registry and flags mismatches.' },
|
|
667
|
+
invoice_amount: { type: 'number', description: 'Invoice amount in local currency — used in fraud risk weighting.' }
|
|
668
|
+
},
|
|
669
|
+
required: ['vat_number']
|
|
670
|
+
}
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
name: 'get_vat_rates',
|
|
674
|
+
idempotent: true,
|
|
675
|
+
description: 'Call this BEFORE your agent calculates invoice totals, applies tax rates, generates VAT-inclusive pricing, or validates that a VAT amount on an invoice is correct for a given country. Returns current standard, reduced, and zero VAT rates for all 27 EU member states and UK. VAT rates change without notice — your agent cannot rely on training data for current rates. Returns machine-readable JSON — no parsing needed. Omit country_code to get all countries.',
|
|
676
|
+
inputSchema: {
|
|
677
|
+
type: 'object',
|
|
678
|
+
properties: {
|
|
679
|
+
country_code: { type: 'string', description: 'ISO 2-letter code e.g. DE, FR, GB. Omit for all countries.' }
|
|
680
|
+
},
|
|
681
|
+
required: []
|
|
682
|
+
}
|
|
683
|
+
}
|
|
425
684
|
];
|
|
426
685
|
|
|
427
686
|
const sseClients = new Map();
|
|
@@ -431,7 +690,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
431
690
|
|
|
432
691
|
if (req.url === '/health' && (req.method === 'GET' || req.method === 'HEAD')) {
|
|
433
692
|
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
434
|
-
res.end(JSON.stringify({ status: 'ok', version: VERSION, service: 'vat-validator-mcp', free_tier: 'no API key required for first
|
|
693
|
+
res.end(JSON.stringify({ status: 'ok', version: VERSION, service: 'vat-validator-mcp', free_tier: 'no API key required for first ' + FREE_TIER_LIMIT + ' calls/month', paid_keys_issued: apiKeys.size }));
|
|
435
694
|
return;
|
|
436
695
|
}
|
|
437
696
|
|
|
@@ -480,7 +739,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
480
739
|
const { name, email, use_case } = JSON.parse(body);
|
|
481
740
|
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
741
|
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.',
|
|
742
|
+
if (trialExtensions.has(emailKey)) { res.writeHead(409, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Trial extension already granted for this email.', bundle_url: BUNDLE_500_URL, agent_action: 'INFORM_USER_TRIAL_ALREADY_USED' })); return; }
|
|
484
743
|
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
485
744
|
const monthKey = getMonthKey(ip);
|
|
486
745
|
const currentCalls = freeTierUsage.get(monthKey) || 0;
|
|
@@ -490,9 +749,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
490
749
|
await sendEmail('ojas@kordagencies.com', 'VAT Validator -- Trial Extension: ' + name,
|
|
491
750
|
'<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
751
|
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,
|
|
752
|
+
'<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, get 500 calls for $8: ' + BUNDLE_500_URL + '</p><p>Ojas<br>kordagencies.com</p>');
|
|
494
753
|
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.',
|
|
754
|
+
res.end(JSON.stringify({ granted: true, additional_calls: TRIAL_EXTENSION_CALLS, message: TRIAL_EXTENSION_CALLS + ' extra free calls added. Check your email for confirmation.', bundle_url: BUNDLE_500_URL }));
|
|
496
755
|
} catch(e) { res.writeHead(400, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: e.message, agent_action: 'RETRY_IN_2_MIN' })); }
|
|
497
756
|
});
|
|
498
757
|
return;
|
|
@@ -513,7 +772,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
513
772
|
|
|
514
773
|
if (req.url === '/.well-known/mcp/server-card.json' && req.method === 'GET') {
|
|
515
774
|
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
516
|
-
res.end(JSON.stringify({ name: 'vat-validator-mcp', title: 'VAT Validator MCP', version: VERSION, description: 'VAT
|
|
775
|
+
res.end(JSON.stringify({ name: 'vat-validator-mcp', title: 'VAT Validator MCP', version: VERSION, description: 'VAT number validator for AI agents. EU VIES, UK HMRC, AU ABR — auto-detects jurisdiction. Fraud risk scoring and invoice name cross-check in one call.', tools: tools.map(t => t.name), transport: 'streamable-http', homepage: 'https://kordagencies.com', token_footprint_min: 100, token_footprint_max: 600, token_footprint_avg: 200, idempotent_tools: ['validate_vat', 'get_vat_rates'], circuit_breaker: false, health_endpoint: '/health', ready_endpoint: '/ready' }));
|
|
517
776
|
return;
|
|
518
777
|
}
|
|
519
778
|
|
|
@@ -544,7 +803,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
544
803
|
const request = JSON.parse(body);
|
|
545
804
|
let response;
|
|
546
805
|
if (request.method === 'initialize') {
|
|
547
|
-
response = { jsonrpc: '2.0', id: request.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, resources: {}, prompts: {} }, serverInfo: { name: 'vat-validator-mcp', version: VERSION, description: '
|
|
806
|
+
response = { jsonrpc: '2.0', id: request.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, resources: {}, prompts: {} }, serverInfo: { name: 'vat-validator-mcp', version: VERSION, description: 'VAT Validator MCP v2. validate_vat auto-detects EU/UK/AU jurisdiction, validates against live government registries, and returns AI-powered fraud risk scoring — all in one call. No chained inputs, no prior state required.' } } };
|
|
548
807
|
} else if (request.method === 'notifications/initialized') {
|
|
549
808
|
res.writeHead(204, cors); res.end(); return;
|
|
550
809
|
} else if (request.method === 'tools/list') {
|
|
@@ -556,15 +815,18 @@ const server = http.createServer(async (req, res) => {
|
|
|
556
815
|
} else if (request.method === 'tools/call') {
|
|
557
816
|
const access = checkAccess(req);
|
|
558
817
|
if (!access.allowed) {
|
|
559
|
-
response = { jsonrpc: '2.0', id: request.id, error: { code: -32000, message: access.
|
|
818
|
+
response = { jsonrpc: '2.0', id: request.id, error: { code: -32000, message: access.error || 'Access denied', data: access, agent_action: 'PAUSE_AND_NOTIFY_USER' } };
|
|
560
819
|
} else {
|
|
561
820
|
const { name, arguments: args } = request.params;
|
|
562
821
|
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
563
|
-
usageLog.push({ tool: name, tier: access.tier, time: new Date().toISOString(), ip: ip.slice(0, 8) + '...' });
|
|
822
|
+
usageLog.push({ tool: name, tier: access.tier || access.plan || 'paid', time: new Date().toISOString(), ip: ip.slice(0, 8) + '...' });
|
|
564
823
|
if (usageLog.length > 1000) usageLog.shift();
|
|
565
824
|
toolUsageCounts[name] = (toolUsageCounts[name] || 0) + 1;
|
|
566
825
|
saveStats();
|
|
567
826
|
const result = await executeTool(name, args || {});
|
|
827
|
+
if (access.plan === 'metered' && access.stripeCustomerId) {
|
|
828
|
+
reportMeteredUsage(access.stripeCustomerId, 'vat_query').catch(() => {});
|
|
829
|
+
}
|
|
568
830
|
response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } };
|
|
569
831
|
}
|
|
570
832
|
} else {
|
|
@@ -587,18 +849,17 @@ const server = http.createServer(async (req, res) => {
|
|
|
587
849
|
const request = JSON.parse(body);
|
|
588
850
|
let response;
|
|
589
851
|
if (request.method === 'tools/call') {
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
} else {
|
|
596
|
-
const access = checkAccess(req);
|
|
597
|
-
if (!access.allowed) { res.writeHead(429, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, error: { code: -32000, message: access.reason, upgrade_url: PRO_UPGRADE_URL, agent_action: 'Inform user free tier quota is exhausted. Get 500 calls for $8 at ' + PRO_UPGRADE_URL + ' -- calls never expire.' } })); return; }
|
|
598
|
-
req._accessWarning = access.warning; req._tier = access.tier;
|
|
852
|
+
const access = checkAccess(req);
|
|
853
|
+
if (!access.allowed) {
|
|
854
|
+
res.writeHead(429, { ...cors, 'Content-Type': 'application/json' });
|
|
855
|
+
res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, error: { code: -32000, message: access.error || 'Access denied', data: access, agent_action: 'PAUSE_AND_NOTIFY_USER' } }));
|
|
856
|
+
return;
|
|
599
857
|
}
|
|
858
|
+
req._accessWarning = access.warning;
|
|
859
|
+
req._tier = access.tier;
|
|
860
|
+
req._accessResult = access;
|
|
600
861
|
}
|
|
601
|
-
if (request.method === 'initialize') { response = { jsonrpc: '2.0', id: request.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, resources: {}, prompts: {} }, serverInfo: { name: 'vat-validator-mcp', version: VERSION, description: '
|
|
862
|
+
if (request.method === 'initialize') { response = { jsonrpc: '2.0', id: request.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, resources: {}, prompts: {} }, serverInfo: { name: 'vat-validator-mcp', version: VERSION, description: 'VAT Validator MCP v2. validate_vat auto-detects EU/UK/AU jurisdiction, validates against live government registries, and returns AI-powered fraud risk scoring — all in one call. No chained inputs, no prior state required.' } } };
|
|
602
863
|
} else if (request.method === 'notifications/initialized') { res.writeHead(204, cors); res.end(); return;
|
|
603
864
|
} else if (request.method === 'tools/list') { response = { jsonrpc: '2.0', id: request.id, result: { tools } };
|
|
604
865
|
} else if (request.method === 'resources/list') { response = { jsonrpc: '2.0', id: request.id, result: { resources: [] } };
|
|
@@ -606,46 +867,31 @@ const server = http.createServer(async (req, res) => {
|
|
|
606
867
|
} else if (request.method === 'tools/call') {
|
|
607
868
|
const { name, arguments: toolArgs } = request.params;
|
|
608
869
|
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
609
|
-
usageLog.push({ tool: name, tier: req._tier || 'paid', time: new Date().toISOString(), ip: ip.slice(0, 8) + '...' });
|
|
870
|
+
usageLog.push({ tool: name, tier: req._tier || req._accessResult?.plan || 'paid', time: new Date().toISOString(), ip: ip.slice(0, 8) + '...' });
|
|
610
871
|
if (usageLog.length > 1000) usageLog.shift();
|
|
611
872
|
toolUsageCounts[name] = (toolUsageCounts[name] || 0) + 1;
|
|
612
873
|
saveStats();
|
|
613
874
|
const result = await executeTool(name, toolArgs || {});
|
|
614
875
|
if (req._accessWarning) result._notice = req._accessWarning;
|
|
615
876
|
|
|
616
|
-
|
|
877
|
+
if (req._accessResult && req._accessResult.plan === 'metered' && req._accessResult.stripeCustomerId) {
|
|
878
|
+
reportMeteredUsage(req._accessResult.stripeCustomerId, 'vat_query').catch(() => {});
|
|
879
|
+
}
|
|
880
|
+
|
|
617
881
|
if (req._tier === 'free' && !result.error) {
|
|
618
|
-
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
619
882
|
const used = freeTierUsage.get(getMonthKey(ip)) || 0;
|
|
620
883
|
const remaining = FREE_TIER_LIMIT - used;
|
|
621
884
|
const isWarning = used >= FREE_TIER_WARNING;
|
|
622
885
|
const effectiveLimit = getEffectiveLimit(ip);
|
|
623
886
|
|
|
624
|
-
if (name === 'validate_vat'
|
|
625
|
-
|
|
626
|
-
const gated = ['registered_address', 'address', 'consultation_number'];
|
|
627
|
-
gated.forEach(f => delete result[f]);
|
|
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.';
|
|
629
|
-
result._gated_fields = gated;
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
if (name === 'analyse_vat_risk') {
|
|
633
|
-
// Gate full reasoning — verdict visible, details gated
|
|
634
|
-
const gated = ['fraud_signals', 'positive_indicators', 'recommended_action', 'summary'];
|
|
635
|
-
gated.forEach(f => delete result[f]);
|
|
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.';
|
|
637
|
-
result._gated_fields = gated;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
if (name === 'compare_invoice_details') {
|
|
641
|
-
// Gate detail fields — match_status visible, discrepancies gated
|
|
642
|
-
const gated = ['discrepancies', 'name_match', 'address_match', 'recommended_action', 'summary'];
|
|
887
|
+
if (name === 'validate_vat') {
|
|
888
|
+
const gated = ['fraud_signals', 'address'];
|
|
643
889
|
gated.forEach(f => delete result[f]);
|
|
644
|
-
result._upgrade_note = 'Free tier: ' + remaining + ' of ' + effectiveLimit + ' calls remaining. Get 500 calls for $8 at ' +
|
|
890
|
+
result._upgrade_note = 'Free tier: ' + remaining + ' of ' + effectiveLimit + ' calls remaining. Get 500 calls for $8 at ' + BUNDLE_500_URL + ' -- calls never expire. Includes full registered address and detailed fraud signal breakdown.';
|
|
645
891
|
result._gated_fields = gated;
|
|
646
892
|
}
|
|
647
893
|
|
|
648
|
-
if (isWarning) result._notice = 'Warning: only ' + remaining + ' free call' + (remaining === 1 ? '' : 's') + ' left this month. Get 500 calls for $8 at ' +
|
|
894
|
+
if (isWarning) result._notice = 'Warning: only ' + remaining + ' free call' + (remaining === 1 ? '' : 's') + ' left this month. Get 500 calls for $8 at ' + BUNDLE_500_URL + ' -- calls never expire.';
|
|
649
895
|
}
|
|
650
896
|
|
|
651
897
|
response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } };
|
|
@@ -657,7 +903,57 @@ const server = http.createServer(async (req, res) => {
|
|
|
657
903
|
return;
|
|
658
904
|
}
|
|
659
905
|
|
|
660
|
-
if (req.method === 'GET' && req.url === '/') { res.writeHead(200, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ name: 'vat-validator-mcp', version: VERSION, status: 'ok', tools:
|
|
906
|
+
if (req.method === 'GET' && req.url === '/') { res.writeHead(200, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ name: 'vat-validator-mcp', version: VERSION, status: 'ok', tools: 2, free_tier: '50 calls/month, no API key required', description: 'VAT validation + AI fraud detection. EU VIES, UK HMRC, Australian ABN.', subscribe_url: METERED_SUBSCRIBE_URL, bundle_500_url: BUNDLE_500_URL, bundle_2000_url: BUNDLE_2000_URL })); return; }
|
|
907
|
+
|
|
908
|
+
if (req.url === '/subscribe' && req.method === 'GET') {
|
|
909
|
+
try {
|
|
910
|
+
const session = await stripe.checkout.sessions.create({
|
|
911
|
+
mode: 'subscription',
|
|
912
|
+
line_items: [
|
|
913
|
+
{ price: 'price_1TUkxWD6WvRe6sn3eFTaokqx' }
|
|
914
|
+
],
|
|
915
|
+
success_url: 'https://vat-validator-mcp-production.up.railway.app/subscribed',
|
|
916
|
+
cancel_url: 'https://kordagencies.com/vat-validator.html',
|
|
917
|
+
metadata: { product_name: 'metered' }
|
|
918
|
+
});
|
|
919
|
+
res.writeHead(302, { Location: session.url });
|
|
920
|
+
res.end();
|
|
921
|
+
} catch(e) {
|
|
922
|
+
res.writeHead(500, { ...cors, 'Content-Type': 'application/json' });
|
|
923
|
+
res.end(JSON.stringify({ error: 'Could not create checkout session', details: e.message }));
|
|
924
|
+
}
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
if (req.url === '/subscribed' && req.method === 'GET') {
|
|
929
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
930
|
+
res.end(`<!DOCTYPE html>
|
|
931
|
+
<html>
|
|
932
|
+
<head>
|
|
933
|
+
<meta charset="UTF-8">
|
|
934
|
+
<title>Subscription confirmed</title>
|
|
935
|
+
<style>
|
|
936
|
+
body{background:#070910;color:#00E5C3;
|
|
937
|
+
font-family:'DM Mono',monospace;padding:3rem;
|
|
938
|
+
max-width:600px;margin:0 auto}
|
|
939
|
+
h2{font-weight:400;margin-bottom:1rem}
|
|
940
|
+
p{color:#8895AA;font-size:13px;line-height:1.6;
|
|
941
|
+
margin-bottom:0.8rem}
|
|
942
|
+
a{color:#00E5C3}
|
|
943
|
+
</style>
|
|
944
|
+
</head>
|
|
945
|
+
<body>
|
|
946
|
+
<h2>Subscription confirmed.</h2>
|
|
947
|
+
<p>Your API key will arrive by email within 60 seconds.</p>
|
|
948
|
+
<p>Add it to your agent config as the
|
|
949
|
+
<span style="color:#fff">x-api-key</span> header.</p>
|
|
950
|
+
<p>Full documentation at
|
|
951
|
+
<a href="https://kordagencies.com">kordagencies.com</a></p>
|
|
952
|
+
</body>
|
|
953
|
+
</html>`);
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
|
|
661
957
|
res.writeHead(404, cors); res.end(JSON.stringify({ error: 'Not found' }));
|
|
662
958
|
});
|
|
663
959
|
|
|
@@ -675,7 +971,7 @@ function setupStdio() {
|
|
|
675
971
|
try { req = JSON.parse(line); } catch(e) { return; }
|
|
676
972
|
let response;
|
|
677
973
|
if (req.method === 'initialize') {
|
|
678
|
-
response = { jsonrpc: '2.0', id: req.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, resources: {}, prompts: {} }, serverInfo: { name: 'vat-validator-mcp', version: VERSION, description: '
|
|
974
|
+
response = { jsonrpc: '2.0', id: req.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, resources: {}, prompts: {} }, serverInfo: { name: 'vat-validator-mcp', version: VERSION, description: 'VAT Validator MCP v2. validate_vat auto-detects EU/UK/AU jurisdiction, validates against live government registries, and returns AI-powered fraud risk scoring — all in one call. No chained inputs, no prior state required.' } } };
|
|
679
975
|
} else if (req.method === 'notifications/initialized') {
|
|
680
976
|
return;
|
|
681
977
|
} else if (req.method === 'tools/list') {
|
|
@@ -702,12 +998,14 @@ function setupStdio() {
|
|
|
702
998
|
|
|
703
999
|
setupStdio();
|
|
704
1000
|
|
|
705
|
-
server.listen(PORT, () => {
|
|
1001
|
+
server.listen(PORT, async () => {
|
|
706
1002
|
loadStats();
|
|
707
|
-
|
|
1003
|
+
await loadApiKeysFromRedis('vat');
|
|
708
1004
|
console.log('VAT Validator MCP v' + VERSION + ' running on port ' + PORT);
|
|
709
1005
|
console.log('Free tier: ' + FREE_TIER_LIMIT + ' calls/IP/month, no API key required');
|
|
710
1006
|
console.log('Resend: ' + (RESEND_API_KEY ? 'configured' : 'MISSING'));
|
|
711
1007
|
console.log('Anthropic: ' + (ANTHROPIC_API_KEY ? 'configured' : 'MISSING'));
|
|
712
1008
|
console.log('ABR GUID: ' + (process.env.ABR_GUID ? 'custom GUID set' : 'using fallback demo GUID — set ABR_GUID env var'));
|
|
1009
|
+
console.log('Upstash Redis: ' + (UPSTASH_URL ? 'configured' : 'MISSING - set UPSTASH_REDIS_REST_URL'));
|
|
1010
|
+
console.log('Stripe: ' + (process.env.STRIPE_SECRET_KEY ? 'configured' : 'MISSING - set STRIPE_SECRET_KEY'));
|
|
713
1011
|
});
|