vat-validator-mcp 1.4.11 → 1.4.13
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 +54 -225
- package/glama.json +40 -14
- package/package.json +4 -1
- package/railway +0 -0
- package/server.json +2 -2
- package/smithery.yaml +1 -1
- package/src/server.js +265 -69
package/README.md
CHANGED
|
@@ -1,138 +1,43 @@
|
|
|
1
|
-
|
|
1
|
+
# VAT Validator MCP
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**AI-powered VAT fraud detection and live VAT number validation
|
|
4
|
+
for AI agents.**
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
Validates EU, UK, and AU VAT numbers against authoritative live
|
|
7
|
+
sources and uses AI pattern analysis to detect invoice fraud
|
|
8
|
+
before payment is authorised. Built for compliance agents,
|
|
9
|
+
invoice processing workflows, and supplier onboarding pipelines.
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
## What This Solves
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
VAT fraud costs EU businesses €50bn annually. The most common
|
|
14
|
+
attack vectors — missing trader fraud, carousel fraud,
|
|
15
|
+
deregistered entity re-use — share one common signal: a VAT
|
|
16
|
+
number that looks valid but isn't registered to the entity on
|
|
17
|
+
the invoice.
|
|
10
18
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"url": "https://vat-validator-mcp-production.up.railway.app"
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
Or via Smithery:
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
npx -y @smithery/cli@latest mcp add OjasKord/vat-validator-mcp
|
|
23
|
-
```
|
|
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
|
-
```
|
|
19
|
+
Claude and other LLMs cannot reliably check live VAT registration
|
|
20
|
+
status from training data. This tool calls the authoritative
|
|
21
|
+
sources directly:
|
|
64
22
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
## Why Use This
|
|
69
|
-
|
|
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.
|
|
71
|
-
|
|
72
|
-
Required for EU ViDA mandatory e-invoicing compliance from 2026.
|
|
23
|
+
- **EU VIES** — all 27 member states, real-time
|
|
24
|
+
- **UK HMRC VAT API v2** — OAuth2, authoritative
|
|
25
|
+
- **AU ABR** — Australian Business Register
|
|
73
26
|
|
|
74
27
|
## Tools
|
|
75
28
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
{ "vat_number": "DE811128135" }
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
### `validate_uk_vat`
|
|
88
|
-
UK-specific validation against HMRC live records. Returns HMRC consultation number for audit trail. Use when you need to prove compliance during a tax audit.
|
|
89
|
-
|
|
90
|
-
```json
|
|
91
|
-
{ "vat_number": "GB123456789" }
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
### `get_vat_rates`
|
|
95
|
-
Current VAT rates for all 27 EU member states, UK, and Australia. Use before generating any cross-border invoice or quote.
|
|
96
|
-
|
|
97
|
-
```json
|
|
98
|
-
{ "country_code": "DE" }
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
### `batch_validate` *(Paid only)*
|
|
102
|
-
Validate up to 10 VAT numbers in one call across any mix of EU, UK, and Australian numbers. Use for supplier onboarding batches and monthly vendor audits.
|
|
103
|
-
|
|
104
|
-
```json
|
|
105
|
-
{ "vat_numbers": ["DE811128135", "GB123456789", "FR12345678901"] }
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
### `analyse_vat_risk` *(AI-powered — NOT a database lookup)*
|
|
109
|
-
AI fraud risk assessment after validation. Returns CLEAR/REVIEW/BLOCK recommendation with specific fraud signals. Catches name mismatches between invoice and registry, newly registered companies with large invoice values, dormant status, shell company indicators, and address anomalies. Use before approving any payment or signing any contract with a first-time counterparty.
|
|
110
|
-
|
|
111
|
-
```json
|
|
112
|
-
{
|
|
113
|
-
"vat_number": "DE811128135",
|
|
114
|
-
"validation_result": { "valid": true, "company_name": null, "country": "DE" },
|
|
115
|
-
"invoice_amount": 50000,
|
|
116
|
-
"invoice_company_name": "Deutsche Test GmbH"
|
|
117
|
-
}
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
### `compare_invoice_details` *(AI-powered — NOT a database lookup)*
|
|
121
|
-
AI comparison of invoice details against official registry records. Flags discrepancies between the company name, address, and VAT number on an invoice versus registered government data. A name mismatch is one of the most common invoice fraud signals. Use before approving payment on any invoice from an unverified supplier.
|
|
122
|
-
|
|
123
|
-
```json
|
|
124
|
-
{
|
|
125
|
-
"invoice_company_name": "Deutsche Test GmbH",
|
|
126
|
-
"invoice_vat_number": "DE811128135",
|
|
127
|
-
"invoice_address": "Musterstrasse 1, Berlin",
|
|
128
|
-
"validation_result": { "valid": true, "company_name": null, "country": "DE" }
|
|
129
|
-
}
|
|
130
|
-
```
|
|
29
|
+
| Tool | Free Tier | Use When |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| validate_vat | 20/month | Before approving any EU supplier or invoice |
|
|
32
|
+
| validate_uk_vat | 20/month | Before approving any UK supplier or invoice |
|
|
33
|
+
| get_vat_rates | 20/month | Before calculating cross-border invoice totals |
|
|
34
|
+
| batch_validate | Paid | Validating a supplier list or invoice batch |
|
|
35
|
+
| analyse_vat_risk | 20/month | Before approving any high-value cross-border invoice |
|
|
36
|
+
| compare_invoice_details | 20/month | Before authorising payment on any supplier invoice |
|
|
131
37
|
|
|
132
38
|
## Add to Your Agent
|
|
133
39
|
|
|
134
|
-
|
|
135
|
-
```json
|
|
40
|
+
**Claude Code** — add to .mcp.json:
|
|
136
41
|
{
|
|
137
42
|
"mcpServers": {
|
|
138
43
|
"vat-validator": {
|
|
@@ -141,114 +46,38 @@ AI comparison of invoice details against official registry records. Flags discre
|
|
|
141
46
|
}
|
|
142
47
|
}
|
|
143
48
|
}
|
|
144
|
-
```
|
|
145
49
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
"url": "https://vat-validator-mcp-production.up.railway.app/sse",
|
|
152
|
-
"transport": "sse"
|
|
153
|
-
}
|
|
154
|
-
})
|
|
155
|
-
tools = await client.get_tools()
|
|
156
|
-
```
|
|
50
|
+
**LangChain:**
|
|
51
|
+
from langchain_mcp import MCPClient
|
|
52
|
+
client = MCPClient(
|
|
53
|
+
"https://vat-validator-mcp-production.up.railway.app/sse"
|
|
54
|
+
)
|
|
157
55
|
|
|
158
|
-
|
|
159
|
-
```python
|
|
56
|
+
**OpenAI Agents SDK:**
|
|
160
57
|
from agents.mcp import MCPServerSse
|
|
161
58
|
mcp_server = MCPServerSse(
|
|
162
|
-
params={
|
|
59
|
+
params={
|
|
60
|
+
"url": "https://vat-validator-mcp-production.up.railway.app/sse"
|
|
61
|
+
}
|
|
163
62
|
)
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
## Example Responses
|
|
167
|
-
|
|
168
|
-
**validate_vat:**
|
|
169
|
-
```json
|
|
170
|
-
{
|
|
171
|
-
"valid": true,
|
|
172
|
-
"vat_number": "DE811128135",
|
|
173
|
-
"country": "DE",
|
|
174
|
-
"company_name": null,
|
|
175
|
-
"source": "VIES",
|
|
176
|
-
"source_url": "ec.europa.eu/taxation_customs/vies",
|
|
177
|
-
"checked_at": "2026-04-09T06:17:00Z"
|
|
178
|
-
}
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
**analyse_vat_risk:**
|
|
182
|
-
```json
|
|
183
|
-
{
|
|
184
|
-
"recommendation": "REVIEW",
|
|
185
|
-
"risk_level": "MEDIUM",
|
|
186
|
-
"risk_score": 65,
|
|
187
|
-
"fraud_signals": ["Company name not available in registry despite valid VAT number", "Unable to verify invoice company name against registry data"],
|
|
188
|
-
"positive_indicators": ["VAT number validates as authentic in German registry"],
|
|
189
|
-
"recommended_action": "Request additional company documentation before processing payment.",
|
|
190
|
-
"summary": "Valid VAT number but missing registry information prevents full verification."
|
|
191
|
-
}
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
## Recommended Workflows
|
|
195
|
-
|
|
196
|
-
**Invoice processing (3 calls):**
|
|
197
|
-
1. `validate_vat` — confirm VAT number is real and active
|
|
198
|
-
2. `compare_invoice_details` — AI checks invoice name/address against registry
|
|
199
|
-
3. `analyse_vat_risk` — AI fraud risk assessment with CLEAR/REVIEW/BLOCK
|
|
200
|
-
Only proceed with payment if recommendation is CLEAR.
|
|
201
|
-
|
|
202
|
-
**Supplier onboarding (2 calls):**
|
|
203
|
-
1. `validate_vat` — confirm registration
|
|
204
|
-
2. `analyse_vat_risk` — AI fraud signal check
|
|
205
|
-
|
|
206
|
-
**Monthly vendor audit (1 call):**
|
|
207
|
-
- `batch_validate` — re-validate all active suppliers. Registrations can lapse.
|
|
208
|
-
|
|
209
|
-
## Data Sources
|
|
210
|
-
|
|
211
|
-
| Tool | Data Source | Update Frequency |
|
|
212
|
-
|---|---|---|
|
|
213
|
-
| validate_vat (EU) | EU VIES (ec.europa.eu/taxation_customs/vies) | Real-time |
|
|
214
|
-
| validate_vat (UK) | UK HMRC (api.service.hmrc.gov.uk) | Real-time |
|
|
215
|
-
| validate_vat (AU) | Australian ABR (abr.business.gov.au) | Real-time |
|
|
216
|
-
| analyse_vat_risk | Registry data + Claude AI analysis | Real-time |
|
|
217
|
-
| compare_invoice_details | Registry data + Claude AI analysis | Real-time |
|
|
218
|
-
|
|
219
|
-
Every response includes `source_url` and `checked_at` so agents can verify exactly where data came from and when.
|
|
220
|
-
|
|
221
|
-
## Supported Jurisdictions
|
|
222
|
-
|
|
223
|
-
**EU (27 member states):** 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
|
|
224
|
-
|
|
225
|
-
**UK:** GB prefix via HMRC
|
|
226
|
-
|
|
227
|
-
**Australia:** AU prefix or 11-digit ABN via ABR
|
|
228
63
|
|
|
229
64
|
## Pricing
|
|
230
65
|
|
|
231
|
-
|
|
|
66
|
+
| Tier | Calls | Price |
|
|
232
67
|
|---|---|---|
|
|
233
|
-
| Free | 20/month | No
|
|
234
|
-
|
|
|
235
|
-
|
|
|
236
|
-
|
|
237
|
-
Upgrade
|
|
238
|
-
|
|
239
|
-
##
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
## Connect
|
|
251
|
-
|
|
252
|
-
- Website: [kordagencies.com](https://kordagencies.com)
|
|
253
|
-
- Smithery: [smithery.ai/server/OjasKord/vat-validator-mcp](https://smithery.ai/server/OjasKord/vat-validator-mcp)
|
|
254
|
-
- Contact: ojas@kordagencies.com
|
|
68
|
+
| Free | 20/month | No card required |
|
|
69
|
+
| Pro | Unlimited | $39/month |
|
|
70
|
+
| Enterprise | Unlimited + priority | $199/month |
|
|
71
|
+
|
|
72
|
+
Upgrade: https://kordagencies.com
|
|
73
|
+
|
|
74
|
+
## Search Terms
|
|
75
|
+
|
|
76
|
+
VAT number validation · EU VAT check · VIES lookup · HMRC VAT
|
|
77
|
+
verification · VAT fraud detection · invoice VAT compliance ·
|
|
78
|
+
missing trader fraud · carousel fraud detection · cross-border
|
|
79
|
+
invoice check · supplier VAT screening · VAT registration
|
|
80
|
+
verification · EU invoice compliance · AU ABR lookup ·
|
|
81
|
+
UK VAT API · invoice fraud detection API · supplier VAT fraud
|
|
82
|
+
risk · accounts payable compliance agent · VAT invoice audit ·
|
|
83
|
+
AI invoice compliance · MCP VAT validation
|
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vat-validator-mcp",
|
|
3
3
|
"mcpName": "io.github.OjasKord/vat-validator-mcp",
|
|
4
|
-
"version": "1.4.
|
|
4
|
+
"version": "1.4.13",
|
|
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": {
|
|
@@ -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: "VAT
|
|
1
|
+
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."
|
|
2
2
|
startCommand:
|
|
3
3
|
type: http
|
|
4
4
|
url: https://vat-validator-mcp-production.up.railway.app
|
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.11';
|
|
9
|
-
const PRO_UPGRADE_URL = 'https://buy.stripe.com/28EeVceUB06N1ty3teebu0l';
|
|
10
|
-
const ENTERPRISE_UPGRADE_URL = 'https://buy.stripe.com/00w14m7s96vb1ty5Bmebu0m';
|
|
9
|
+
const VERSION = '1.4.13';
|
|
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; }
|
|
61
74
|
}
|
|
62
75
|
|
|
63
|
-
function
|
|
76
|
+
async function redisSet(key, value) {
|
|
64
77
|
try {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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) {}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function redisKeys(pattern) {
|
|
93
|
+
try {
|
|
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) {
|
|
@@ -352,19 +425,69 @@ function checkAccess(req) {
|
|
|
352
425
|
const apiKey = req.headers['x-api-key'];
|
|
353
426
|
if (apiKey) {
|
|
354
427
|
const record = apiKeys.get(apiKey);
|
|
355
|
-
if (!record)
|
|
356
|
-
|
|
428
|
+
if (!record) {
|
|
429
|
+
return { allowed: false, error: 'Invalid API key' };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const wasReset = checkAndResetPeriod(record);
|
|
433
|
+
if (wasReset) {
|
|
434
|
+
saveKeyToRedis(apiKey, record, REDIS_PREFIX).catch(() => {});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (record.plan === 'metered') {
|
|
438
|
+
record.calls++;
|
|
439
|
+
saveKeyToRedis(apiKey, record, REDIS_PREFIX).catch(() => {});
|
|
440
|
+
return {
|
|
441
|
+
allowed: true,
|
|
442
|
+
paid: true,
|
|
443
|
+
plan: 'metered',
|
|
444
|
+
stripeCustomerId: record.stripeCustomerId
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (record.calls >= record.limit) {
|
|
449
|
+
return {
|
|
450
|
+
allowed: false,
|
|
451
|
+
error: `Bundle exhausted. You have used all ${record.limit} calls in this bundle. Purchase another bundle or switch to pay-as-you-go.`,
|
|
452
|
+
subscribe_url: METERED_SUBSCRIBE_URL,
|
|
453
|
+
bundle_500_url: BUNDLE_500_URL,
|
|
454
|
+
bundle_2000_url: BUNDLE_2000_URL,
|
|
455
|
+
agent_action: 'PAUSE_AND_NOTIFY_USER'
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
357
459
|
record.calls++;
|
|
358
|
-
|
|
460
|
+
saveKeyToRedis(apiKey, record, REDIS_PREFIX).catch(() => {});
|
|
461
|
+
return { allowed: true, paid: true, plan: record.plan };
|
|
359
462
|
}
|
|
360
463
|
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
361
464
|
const monthKey = getMonthKey(ip);
|
|
362
465
|
const calls = freeTierUsage.get(monthKey) || 0;
|
|
363
|
-
if (calls >= FREE_TIER_LIMIT) return {
|
|
466
|
+
if (calls >= FREE_TIER_LIMIT) return {
|
|
467
|
+
allowed: false,
|
|
468
|
+
error: 'Free tier limit of 50 calls/month reached.',
|
|
469
|
+
options: {
|
|
470
|
+
pay_as_you_go: {
|
|
471
|
+
description: 'No commitment. Pay only for what you use. Billed monthly at end of period.',
|
|
472
|
+
pricing: {
|
|
473
|
+
vat_query: '$0.010 per query'
|
|
474
|
+
},
|
|
475
|
+
subscribe_url: METERED_SUBSCRIBE_URL
|
|
476
|
+
},
|
|
477
|
+
bundle: {
|
|
478
|
+
description: 'Buy a fixed call bundle. No subscription.',
|
|
479
|
+
options: [
|
|
480
|
+
{ calls: 500, price: '$8', url: BUNDLE_500_URL },
|
|
481
|
+
{ calls: 2000, price: '$28', url: BUNDLE_2000_URL }
|
|
482
|
+
]
|
|
483
|
+
}
|
|
484
|
+
},
|
|
485
|
+
agent_action: 'PAUSE_AND_NOTIFY_USER'
|
|
486
|
+
};
|
|
364
487
|
freeTierUsage.set(monthKey, calls + 1);
|
|
365
488
|
saveStats();
|
|
366
489
|
const remaining = FREE_TIER_LIMIT - calls - 1;
|
|
367
|
-
const warningMsg = remaining <
|
|
490
|
+
const warningMsg = remaining < 10 ? remaining + ' free validations remaining this month. Get 500 calls for $8 at ' + BUNDLE_500_URL + ' -- calls never expire.' : null;
|
|
368
491
|
return { allowed: true, tier: 'free', remaining, warning: warningMsg };
|
|
369
492
|
}
|
|
370
493
|
|
|
@@ -400,28 +523,47 @@ async function handleStripeWebhook(body, sig) {
|
|
|
400
523
|
const event = JSON.parse(body);
|
|
401
524
|
if (event.type === 'checkout.session.completed') {
|
|
402
525
|
const session = event.data.object;
|
|
403
|
-
const
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
526
|
+
const plan = getPlanFromProduct(session.metadata?.product_name);
|
|
527
|
+
const apiKey = generateApiKey();
|
|
528
|
+
const limit = plan === 'metered' ? null : plan === 'bundle_2000' ? 2000 : 500;
|
|
529
|
+
const record = {
|
|
530
|
+
email: session.customer_details?.email || 'unknown',
|
|
531
|
+
plan,
|
|
532
|
+
calls: 0,
|
|
533
|
+
periodStart: Date.now(),
|
|
534
|
+
limit,
|
|
535
|
+
stripeCustomerId: session.customer || null,
|
|
536
|
+
createdAt: Date.now()
|
|
537
|
+
};
|
|
538
|
+
apiKeys.set(apiKey, record);
|
|
539
|
+
await saveKeyToRedis(apiKey, record, REDIS_PREFIX);
|
|
540
|
+
await sendApiKeyEmail(record.email, apiKey, plan);
|
|
541
|
+
console.log('[vat] API key created for ' + record.email + ' (' + plan + ')');
|
|
542
|
+
return { success: true, email: record.email, plan };
|
|
543
|
+
}
|
|
544
|
+
if (event.type === 'customer.subscription.created') {
|
|
545
|
+
const sub = event.data.object;
|
|
546
|
+
const customerId = sub.customer;
|
|
547
|
+
for (const [key, record] of apiKeys.entries()) {
|
|
548
|
+
if (record.stripeCustomerId === customerId && !record.subscriptionId) {
|
|
549
|
+
record.subscriptionId = sub.id;
|
|
550
|
+
await saveKeyToRedis(key, record, REDIS_PREFIX);
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
412
553
|
}
|
|
554
|
+
return { received: true, type: event.type };
|
|
413
555
|
}
|
|
414
556
|
return { received: true, type: event.type };
|
|
415
557
|
} catch(e) { console.error('[vat] Webhook error:', e.message); return { error: e.message, status: 400 }; }
|
|
416
558
|
}
|
|
417
559
|
|
|
418
560
|
const tools = [
|
|
419
|
-
{ name: 'validate_vat', idempotent: true, description: '
|
|
420
|
-
{ name: 'validate_uk_vat', idempotent: true, description: '
|
|
421
|
-
{ name: 'get_vat_rates', idempotent: true, description: '
|
|
422
|
-
{ name: 'batch_validate', idempotent: true, description: '
|
|
423
|
-
{ name: 'analyse_vat_risk', idempotent: true, description: '
|
|
424
|
-
{ name: 'compare_invoice_details', idempotent: true, description: '
|
|
561
|
+
{ name: 'validate_vat', idempotent: true, description: 'Call this BEFORE your agent approves any EU supplier, issues a cross-border invoice, or processes an inbound EU payment. Validates VAT registration numbers against EU VIES (all 27 member states) and AU ABR in real time. Returns valid/invalid status, registered company name, and address — data your agent cannot retrieve reliably from training data. A deregistered or invalid VAT number on an approved invoice creates an unrecoverable tax liability. One call eliminates that risk. Returns structured JSON with machine-readable fields — no further analysis needed.', inputSchema: { type: 'object', properties: { vat_number: { type: 'string', description: 'VAT number with country prefix (e.g. DE123456789, GB123456789, FR12345678901)' } }, required: ['vat_number'] } },
|
|
562
|
+
{ name: 'validate_uk_vat', idempotent: true, description: 'Call this BEFORE your agent approves any UK supplier, issues a UK VAT invoice, or processes a UK inbound payment. Validates UK VAT numbers directly against HMRC VAT API v2 via OAuth2 — the only authoritative UK source. Returns valid/invalid, registered business name, and address. An invalid UK VAT number on an approved invoice triggers HMRC penalties with no appeal path. One call, authoritative source, machine-readable verdict.', inputSchema: { type: 'object', properties: { vat_number: { type: 'string', description: 'UK VAT number with or without GB prefix' } }, required: ['vat_number'] } },
|
|
563
|
+
{ name: 'get_vat_rates', idempotent: true, description: 'Call this BEFORE your agent calculates invoice totals, applies tax rates, or generates VAT-inclusive pricing for any EU country or UK. 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 structured JSON — no parsing needed.', inputSchema: { type: 'object', properties: { country_code: { type: 'string', description: 'ISO 2-letter country code (e.g. DE, FR, GB). Leave blank for all countries.' } }, required: [] } },
|
|
564
|
+
{ name: 'batch_validate', idempotent: true, description: 'Call this when your agent needs to validate multiple supplier VAT numbers in a single workflow — onboarding a supplier list, auditing an invoice batch, or screening a counterparty database. Validates against EU VIES and HMRC in one call. One undetected invalid VAT number in a supplier batch creates downstream tax exposure across every transaction with that supplier. Returns per-number verdicts in structured JSON.', inputSchema: { type: 'object', properties: { vat_numbers: { type: 'array', items: { type: 'string' }, description: 'Array of VAT numbers with country prefixes (max 10)' } }, required: ['vat_numbers'] } },
|
|
565
|
+
{ name: 'analyse_vat_risk', idempotent: true, description: 'Call this BEFORE your agent approves a high-value invoice, onboards a new EU or UK supplier, or processes any cross-border payment where VAT fraud is a material risk. AI-powered fraud risk scoring — NOT a simple database lookup. Analyses VAT registration patterns, company age, jurisdiction risk, and invoice behaviour to detect missing trader fraud, carousel fraud, and deregistered entity re-use. Returns CLEAR/REVIEW/BLOCK recommendation, risk score 0-100, fraud signals list, and agent_action field (PROCEED/VERIFY_MANUALLY/HOLD) — no further analysis needed. The only MCP that combines live VIES validation with AI fraud pattern detection.', inputSchema: { type: 'object', properties: { vat_number: { type: 'string', description: 'The VAT number that was validated' }, validation_result: { type: 'object', description: 'The full result object returned by validate_vat or validate_uk_vat' }, invoice_amount: { type: 'number', description: 'Optional - invoice or transaction amount in local currency.' }, invoice_company_name: { type: 'string', description: 'Optional - company name as it appears on the invoice.' } }, required: ['vat_number', 'validation_result'] } },
|
|
566
|
+
{ name: 'compare_invoice_details', idempotent: true, description: 'Call this BEFORE your agent finalises payment on any invoice where the supplier VAT number, company name, or address requires verification. Cross-checks invoice details against live VIES and HMRC registry data. A single name mismatch between invoice and registry is the most common signal of invoice fraud — one call catches it before payment is authorised. Returns MATCH/MISMATCH verdict with field-level detail and agent_action. Machine-ready output, no parsing needed.', inputSchema: { type: 'object', properties: { invoice_company_name: { type: 'string', description: 'Company name as it appears on the invoice' }, invoice_address: { type: 'string', description: 'Address as it appears on the invoice (optional)' }, invoice_vat_number: { type: 'string', description: 'VAT number as it appears on the invoice' }, validation_result: { type: 'object', description: 'The full result object returned by validate_vat or validate_uk_vat for this VAT number' } }, required: ['invoice_company_name', 'invoice_vat_number', 'validation_result'] } }
|
|
425
567
|
];
|
|
426
568
|
|
|
427
569
|
const sseClients = new Map();
|
|
@@ -431,7 +573,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
431
573
|
|
|
432
574
|
if (req.url === '/health' && (req.method === 'GET' || req.method === 'HEAD')) {
|
|
433
575
|
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
|
|
576
|
+
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
577
|
return;
|
|
436
578
|
}
|
|
437
579
|
|
|
@@ -480,7 +622,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
480
622
|
const { name, email, use_case } = JSON.parse(body);
|
|
481
623
|
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
624
|
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.',
|
|
625
|
+
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
626
|
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
485
627
|
const monthKey = getMonthKey(ip);
|
|
486
628
|
const currentCalls = freeTierUsage.get(monthKey) || 0;
|
|
@@ -490,9 +632,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
490
632
|
await sendEmail('ojas@kordagencies.com', 'VAT Validator -- Trial Extension: ' + name,
|
|
491
633
|
'<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
634
|
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,
|
|
635
|
+
'<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
636
|
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.',
|
|
637
|
+
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
638
|
} catch(e) { res.writeHead(400, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: e.message, agent_action: 'RETRY_IN_2_MIN' })); }
|
|
497
639
|
});
|
|
498
640
|
return;
|
|
@@ -556,15 +698,18 @@ const server = http.createServer(async (req, res) => {
|
|
|
556
698
|
} else if (request.method === 'tools/call') {
|
|
557
699
|
const access = checkAccess(req);
|
|
558
700
|
if (!access.allowed) {
|
|
559
|
-
response = { jsonrpc: '2.0', id: request.id, error: { code: -32000, message: access.
|
|
701
|
+
response = { jsonrpc: '2.0', id: request.id, error: { code: -32000, message: access.error || 'Access denied', data: access, agent_action: 'PAUSE_AND_NOTIFY_USER' } };
|
|
560
702
|
} else {
|
|
561
703
|
const { name, arguments: args } = request.params;
|
|
562
704
|
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) + '...' });
|
|
705
|
+
usageLog.push({ tool: name, tier: access.tier || access.plan || 'paid', time: new Date().toISOString(), ip: ip.slice(0, 8) + '...' });
|
|
564
706
|
if (usageLog.length > 1000) usageLog.shift();
|
|
565
707
|
toolUsageCounts[name] = (toolUsageCounts[name] || 0) + 1;
|
|
566
708
|
saveStats();
|
|
567
709
|
const result = await executeTool(name, args || {});
|
|
710
|
+
if (access.plan === 'metered' && access.stripeCustomerId) {
|
|
711
|
+
reportMeteredUsage(access.stripeCustomerId, 'vat_query').catch(() => {});
|
|
712
|
+
}
|
|
568
713
|
response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } };
|
|
569
714
|
}
|
|
570
715
|
} else {
|
|
@@ -587,16 +732,15 @@ const server = http.createServer(async (req, res) => {
|
|
|
587
732
|
const request = JSON.parse(body);
|
|
588
733
|
let response;
|
|
589
734
|
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;
|
|
735
|
+
const access = checkAccess(req);
|
|
736
|
+
if (!access.allowed) {
|
|
737
|
+
res.writeHead(429, { ...cors, 'Content-Type': 'application/json' });
|
|
738
|
+
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' } }));
|
|
739
|
+
return;
|
|
599
740
|
}
|
|
741
|
+
req._accessWarning = access.warning;
|
|
742
|
+
req._tier = access.tier;
|
|
743
|
+
req._accessResult = access;
|
|
600
744
|
}
|
|
601
745
|
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: 'Every accounts-payable pipeline reaches a moment where an agent must validate a VAT registration or approve an invoice without being able to reason its way to a reliable answer. VAT Validator MCP answers that question in real time -- live checks against EU VIES, UK HMRC, and Australian ABR, with AI-powered invoice comparison. An agent acting on stale VAT data has no defence against a tax authority. Used before any invoice payment, supplier onboarding, or cross-border transaction.' } } };
|
|
602
746
|
} else if (request.method === 'notifications/initialized') { res.writeHead(204, cors); res.end(); return;
|
|
@@ -606,46 +750,46 @@ const server = http.createServer(async (req, res) => {
|
|
|
606
750
|
} else if (request.method === 'tools/call') {
|
|
607
751
|
const { name, arguments: toolArgs } = request.params;
|
|
608
752
|
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) + '...' });
|
|
753
|
+
usageLog.push({ tool: name, tier: req._tier || req._accessResult?.plan || 'paid', time: new Date().toISOString(), ip: ip.slice(0, 8) + '...' });
|
|
610
754
|
if (usageLog.length > 1000) usageLog.shift();
|
|
611
755
|
toolUsageCounts[name] = (toolUsageCounts[name] || 0) + 1;
|
|
612
756
|
saveStats();
|
|
613
757
|
const result = await executeTool(name, toolArgs || {});
|
|
614
758
|
if (req._accessWarning) result._notice = req._accessWarning;
|
|
615
759
|
|
|
760
|
+
if (req._accessResult && req._accessResult.plan === 'metered' && req._accessResult.stripeCustomerId) {
|
|
761
|
+
reportMeteredUsage(req._accessResult.stripeCustomerId, 'vat_query').catch(() => {});
|
|
762
|
+
}
|
|
763
|
+
|
|
616
764
|
// Partial response for free tier
|
|
617
765
|
if (req._tier === 'free' && !result.error) {
|
|
618
|
-
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
619
766
|
const used = freeTierUsage.get(getMonthKey(ip)) || 0;
|
|
620
767
|
const remaining = FREE_TIER_LIMIT - used;
|
|
621
768
|
const isWarning = used >= FREE_TIER_WARNING;
|
|
622
769
|
const effectiveLimit = getEffectiveLimit(ip);
|
|
623
770
|
|
|
624
771
|
if (name === 'validate_vat' || name === 'validate_uk_vat') {
|
|
625
|
-
// Gate address on free tier — company name + valid status visible
|
|
626
772
|
const gated = ['registered_address', 'address', 'consultation_number'];
|
|
627
773
|
gated.forEach(f => delete result[f]);
|
|
628
|
-
result._upgrade_note = 'Free tier: ' + remaining + ' of ' + effectiveLimit + ' calls remaining. Get 500 calls for $8 at ' +
|
|
774
|
+
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 HMRC consultation number.';
|
|
629
775
|
result._gated_fields = gated;
|
|
630
776
|
}
|
|
631
777
|
|
|
632
778
|
if (name === 'analyse_vat_risk') {
|
|
633
|
-
// Gate full reasoning — verdict visible, details gated
|
|
634
779
|
const gated = ['fraud_signals', 'positive_indicators', 'recommended_action', 'summary'];
|
|
635
780
|
gated.forEach(f => delete result[f]);
|
|
636
|
-
result._upgrade_note = 'Free tier: ' + remaining + ' of ' + effectiveLimit + ' calls remaining. Get 500 calls for $8 at ' +
|
|
781
|
+
result._upgrade_note = 'Free tier: ' + remaining + ' of ' + effectiveLimit + ' calls remaining. Get 500 calls for $8 at ' + BUNDLE_500_URL + ' -- calls never expire. Includes full fraud signal breakdown, positive indicators, and recommended action.';
|
|
637
782
|
result._gated_fields = gated;
|
|
638
783
|
}
|
|
639
784
|
|
|
640
785
|
if (name === 'compare_invoice_details') {
|
|
641
|
-
// Gate detail fields — match_status visible, discrepancies gated
|
|
642
786
|
const gated = ['discrepancies', 'name_match', 'address_match', 'recommended_action', 'summary'];
|
|
643
787
|
gated.forEach(f => delete result[f]);
|
|
644
|
-
result._upgrade_note = 'Free tier: ' + remaining + ' of ' + effectiveLimit + ' calls remaining. Get 500 calls for $8 at ' +
|
|
788
|
+
result._upgrade_note = 'Free tier: ' + remaining + ' of ' + effectiveLimit + ' calls remaining. Get 500 calls for $8 at ' + BUNDLE_500_URL + ' -- calls never expire. Includes full discrepancy analysis and recommended action.';
|
|
645
789
|
result._gated_fields = gated;
|
|
646
790
|
}
|
|
647
791
|
|
|
648
|
-
if (isWarning) result._notice = 'Warning: only ' + remaining + ' free call' + (remaining === 1 ? '' : 's') + ' left this month. Get 500 calls for $8 at ' +
|
|
792
|
+
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
793
|
}
|
|
650
794
|
|
|
651
795
|
response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } };
|
|
@@ -657,7 +801,57 @@ const server = http.createServer(async (req, res) => {
|
|
|
657
801
|
return;
|
|
658
802
|
}
|
|
659
803
|
|
|
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: 6, free_tier: '
|
|
804
|
+
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: 6, 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; }
|
|
805
|
+
|
|
806
|
+
if (req.url === '/subscribe' && req.method === 'GET') {
|
|
807
|
+
try {
|
|
808
|
+
const session = await stripe.checkout.sessions.create({
|
|
809
|
+
mode: 'subscription',
|
|
810
|
+
line_items: [
|
|
811
|
+
{ price: 'price_1TUkxWD6WvRe6sn3eFTaokqx' }
|
|
812
|
+
],
|
|
813
|
+
success_url: 'https://vat-validator-mcp-production.up.railway.app/subscribed',
|
|
814
|
+
cancel_url: 'https://kordagencies.com/vat-validator.html',
|
|
815
|
+
metadata: { product_name: 'metered' }
|
|
816
|
+
});
|
|
817
|
+
res.writeHead(302, { Location: session.url });
|
|
818
|
+
res.end();
|
|
819
|
+
} catch(e) {
|
|
820
|
+
res.writeHead(500, { ...cors, 'Content-Type': 'application/json' });
|
|
821
|
+
res.end(JSON.stringify({ error: 'Could not create checkout session', details: e.message }));
|
|
822
|
+
}
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (req.url === '/subscribed' && req.method === 'GET') {
|
|
827
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
828
|
+
res.end(`<!DOCTYPE html>
|
|
829
|
+
<html>
|
|
830
|
+
<head>
|
|
831
|
+
<meta charset="UTF-8">
|
|
832
|
+
<title>Subscription confirmed</title>
|
|
833
|
+
<style>
|
|
834
|
+
body{background:#070910;color:#00E5C3;
|
|
835
|
+
font-family:'DM Mono',monospace;padding:3rem;
|
|
836
|
+
max-width:600px;margin:0 auto}
|
|
837
|
+
h2{font-weight:400;margin-bottom:1rem}
|
|
838
|
+
p{color:#8895AA;font-size:13px;line-height:1.6;
|
|
839
|
+
margin-bottom:0.8rem}
|
|
840
|
+
a{color:#00E5C3}
|
|
841
|
+
</style>
|
|
842
|
+
</head>
|
|
843
|
+
<body>
|
|
844
|
+
<h2>Subscription confirmed.</h2>
|
|
845
|
+
<p>Your API key will arrive by email within 60 seconds.</p>
|
|
846
|
+
<p>Add it to your agent config as the
|
|
847
|
+
<span style="color:#fff">x-api-key</span> header.</p>
|
|
848
|
+
<p>Full documentation at
|
|
849
|
+
<a href="https://kordagencies.com">kordagencies.com</a></p>
|
|
850
|
+
</body>
|
|
851
|
+
</html>`);
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
|
|
661
855
|
res.writeHead(404, cors); res.end(JSON.stringify({ error: 'Not found' }));
|
|
662
856
|
});
|
|
663
857
|
|
|
@@ -702,12 +896,14 @@ function setupStdio() {
|
|
|
702
896
|
|
|
703
897
|
setupStdio();
|
|
704
898
|
|
|
705
|
-
server.listen(PORT, () => {
|
|
899
|
+
server.listen(PORT, async () => {
|
|
706
900
|
loadStats();
|
|
707
|
-
|
|
901
|
+
await loadApiKeysFromRedis('vat');
|
|
708
902
|
console.log('VAT Validator MCP v' + VERSION + ' running on port ' + PORT);
|
|
709
903
|
console.log('Free tier: ' + FREE_TIER_LIMIT + ' calls/IP/month, no API key required');
|
|
710
904
|
console.log('Resend: ' + (RESEND_API_KEY ? 'configured' : 'MISSING'));
|
|
711
905
|
console.log('Anthropic: ' + (ANTHROPIC_API_KEY ? 'configured' : 'MISSING'));
|
|
712
906
|
console.log('ABR GUID: ' + (process.env.ABR_GUID ? 'custom GUID set' : 'using fallback demo GUID — set ABR_GUID env var'));
|
|
907
|
+
console.log('Upstash Redis: ' + (UPSTASH_URL ? 'configured' : 'MISSING - set UPSTASH_REDIS_REST_URL'));
|
|
908
|
+
console.log('Stripe: ' + (process.env.STRIPE_SECRET_KEY ? 'configured' : 'MISSING - set STRIPE_SECRET_KEY'));
|
|
713
909
|
});
|