vat-validator-mcp 1.4.13 → 2.0.2
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/package.json +2 -2
- package/privacy.html +501 -0
- package/server.json +2 -2
- package/smithery.yaml +25 -55
- package/src/server.js +231 -102
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.2",
|
|
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"
|
package/privacy.html
ADDED
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Privacy Policy — Kord Agencies</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400&display=swap" rel="stylesheet">
|
|
10
|
+
<style>
|
|
11
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
12
|
+
|
|
13
|
+
:root {
|
|
14
|
+
--bg: #070910;
|
|
15
|
+
--bg-2: #0C1018;
|
|
16
|
+
--bg-3: #111722;
|
|
17
|
+
--teal: #00E5C3;
|
|
18
|
+
--amber: #F0A030;
|
|
19
|
+
--text: #E2E8F2;
|
|
20
|
+
--text-2: #8895AA;
|
|
21
|
+
--text-3: #505A6A;
|
|
22
|
+
--border: rgba(255,255,255,0.06);
|
|
23
|
+
--border-2: rgba(255,255,255,0.12);
|
|
24
|
+
--teal-dim: rgba(0,229,195,0.08);
|
|
25
|
+
--teal-border: rgba(0,229,195,0.25);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
html { font-size: 14px; }
|
|
29
|
+
|
|
30
|
+
body {
|
|
31
|
+
background: var(--bg);
|
|
32
|
+
color: var(--text);
|
|
33
|
+
font-family: 'DM Mono', monospace;
|
|
34
|
+
line-height: 1.7;
|
|
35
|
+
min-height: 100vh;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* ── Nav ── */
|
|
39
|
+
nav {
|
|
40
|
+
height: 44px;
|
|
41
|
+
border-bottom: 1px solid var(--border);
|
|
42
|
+
display: flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
padding: 0 1.5rem;
|
|
45
|
+
position: sticky;
|
|
46
|
+
top: 0;
|
|
47
|
+
background: var(--bg);
|
|
48
|
+
z-index: 10;
|
|
49
|
+
}
|
|
50
|
+
nav a {
|
|
51
|
+
color: var(--teal);
|
|
52
|
+
text-decoration: none;
|
|
53
|
+
font-size: 11px;
|
|
54
|
+
letter-spacing: 0.1em;
|
|
55
|
+
text-transform: uppercase;
|
|
56
|
+
}
|
|
57
|
+
nav .sep {
|
|
58
|
+
color: var(--text-3);
|
|
59
|
+
margin: 0 0.6rem;
|
|
60
|
+
font-size: 11px;
|
|
61
|
+
}
|
|
62
|
+
nav .current {
|
|
63
|
+
color: var(--text-2);
|
|
64
|
+
font-size: 11px;
|
|
65
|
+
letter-spacing: 0.05em;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* ── Layout ── */
|
|
69
|
+
.wrap {
|
|
70
|
+
max-width: 780px;
|
|
71
|
+
margin: 0 auto;
|
|
72
|
+
padding: 3rem 1.5rem 5rem;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* ── Header ── */
|
|
76
|
+
.page-header {
|
|
77
|
+
border-bottom: 1px solid var(--border);
|
|
78
|
+
padding-bottom: 2rem;
|
|
79
|
+
margin-bottom: 2.5rem;
|
|
80
|
+
}
|
|
81
|
+
.label {
|
|
82
|
+
font-size: 10px;
|
|
83
|
+
letter-spacing: 0.15em;
|
|
84
|
+
text-transform: uppercase;
|
|
85
|
+
color: var(--teal);
|
|
86
|
+
margin-bottom: 0.75rem;
|
|
87
|
+
}
|
|
88
|
+
h1 {
|
|
89
|
+
font-size: 1.4rem;
|
|
90
|
+
font-weight: 400;
|
|
91
|
+
color: var(--text);
|
|
92
|
+
letter-spacing: -0.01em;
|
|
93
|
+
margin-bottom: 0.75rem;
|
|
94
|
+
}
|
|
95
|
+
.meta {
|
|
96
|
+
font-size: 11px;
|
|
97
|
+
color: var(--text-3);
|
|
98
|
+
letter-spacing: 0.05em;
|
|
99
|
+
}
|
|
100
|
+
.meta span { color: var(--text-2); }
|
|
101
|
+
|
|
102
|
+
/* ── Sections ── */
|
|
103
|
+
section {
|
|
104
|
+
margin-bottom: 2.5rem;
|
|
105
|
+
padding-bottom: 2.5rem;
|
|
106
|
+
border-bottom: 1px solid var(--border);
|
|
107
|
+
}
|
|
108
|
+
section:last-of-type {
|
|
109
|
+
border-bottom: none;
|
|
110
|
+
}
|
|
111
|
+
h2 {
|
|
112
|
+
font-size: 11px;
|
|
113
|
+
font-weight: 500;
|
|
114
|
+
letter-spacing: 0.12em;
|
|
115
|
+
text-transform: uppercase;
|
|
116
|
+
color: var(--teal);
|
|
117
|
+
margin-bottom: 1rem;
|
|
118
|
+
}
|
|
119
|
+
p {
|
|
120
|
+
font-size: 13px;
|
|
121
|
+
color: var(--text-2);
|
|
122
|
+
margin-bottom: 0.9rem;
|
|
123
|
+
line-height: 1.75;
|
|
124
|
+
}
|
|
125
|
+
p:last-child { margin-bottom: 0; }
|
|
126
|
+
|
|
127
|
+
/* ── Callout box ── */
|
|
128
|
+
.callout {
|
|
129
|
+
background: var(--teal-dim);
|
|
130
|
+
border: 1px solid var(--teal-border);
|
|
131
|
+
border-radius: 4px;
|
|
132
|
+
padding: 1rem 1.25rem;
|
|
133
|
+
margin: 1.25rem 0;
|
|
134
|
+
font-size: 12px;
|
|
135
|
+
color: var(--text);
|
|
136
|
+
line-height: 1.7;
|
|
137
|
+
}
|
|
138
|
+
.callout strong {
|
|
139
|
+
color: var(--teal);
|
|
140
|
+
font-weight: 500;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* ── Data table ── */
|
|
144
|
+
.data-table {
|
|
145
|
+
width: 100%;
|
|
146
|
+
border-collapse: collapse;
|
|
147
|
+
font-size: 12px;
|
|
148
|
+
margin: 1rem 0;
|
|
149
|
+
}
|
|
150
|
+
.data-table th {
|
|
151
|
+
text-align: left;
|
|
152
|
+
font-size: 10px;
|
|
153
|
+
letter-spacing: 0.1em;
|
|
154
|
+
text-transform: uppercase;
|
|
155
|
+
color: var(--text-3);
|
|
156
|
+
padding: 0.5rem 0.75rem;
|
|
157
|
+
border-bottom: 1px solid var(--border-2);
|
|
158
|
+
font-weight: 400;
|
|
159
|
+
}
|
|
160
|
+
.data-table td {
|
|
161
|
+
padding: 0.65rem 0.75rem;
|
|
162
|
+
border-bottom: 1px solid var(--border);
|
|
163
|
+
color: var(--text-2);
|
|
164
|
+
vertical-align: top;
|
|
165
|
+
}
|
|
166
|
+
.data-table tr:last-child td { border-bottom: none; }
|
|
167
|
+
.data-table td:first-child { color: var(--text); }
|
|
168
|
+
.tag {
|
|
169
|
+
display: inline-block;
|
|
170
|
+
font-size: 9px;
|
|
171
|
+
letter-spacing: 0.1em;
|
|
172
|
+
text-transform: uppercase;
|
|
173
|
+
padding: 2px 6px;
|
|
174
|
+
border-radius: 3px;
|
|
175
|
+
font-weight: 500;
|
|
176
|
+
}
|
|
177
|
+
.tag-no { background: rgba(248,113,113,0.1); color: #F87171; border: 1px solid rgba(248,113,113,0.2); }
|
|
178
|
+
.tag-yes { background: rgba(0,229,195,0.1); color: var(--teal); border: 1px solid var(--teal-border); }
|
|
179
|
+
.tag-ltd { background: rgba(240,160,48,0.1); color: var(--amber); border: 1px solid rgba(240,160,48,0.2); }
|
|
180
|
+
|
|
181
|
+
/* ── Inline list ── */
|
|
182
|
+
ul.plain {
|
|
183
|
+
list-style: none;
|
|
184
|
+
margin: 0.5rem 0 0.9rem;
|
|
185
|
+
padding: 0;
|
|
186
|
+
}
|
|
187
|
+
ul.plain li {
|
|
188
|
+
font-size: 13px;
|
|
189
|
+
color: var(--text-2);
|
|
190
|
+
padding: 0.3rem 0;
|
|
191
|
+
padding-left: 1rem;
|
|
192
|
+
position: relative;
|
|
193
|
+
}
|
|
194
|
+
ul.plain li::before {
|
|
195
|
+
content: '—';
|
|
196
|
+
position: absolute;
|
|
197
|
+
left: 0;
|
|
198
|
+
color: var(--text-3);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/* ── Contact block ── */
|
|
202
|
+
.contact-block {
|
|
203
|
+
background: var(--bg-2);
|
|
204
|
+
border: 1px solid var(--border-2);
|
|
205
|
+
border-radius: 4px;
|
|
206
|
+
padding: 1.25rem;
|
|
207
|
+
font-size: 12px;
|
|
208
|
+
color: var(--text-2);
|
|
209
|
+
line-height: 1.9;
|
|
210
|
+
}
|
|
211
|
+
.contact-block a {
|
|
212
|
+
color: var(--teal);
|
|
213
|
+
text-decoration: none;
|
|
214
|
+
}
|
|
215
|
+
.contact-block a:hover { text-decoration: underline; }
|
|
216
|
+
.contact-block .field {
|
|
217
|
+
display: flex;
|
|
218
|
+
gap: 1rem;
|
|
219
|
+
}
|
|
220
|
+
.contact-block .field-label {
|
|
221
|
+
color: var(--text-3);
|
|
222
|
+
font-size: 10px;
|
|
223
|
+
letter-spacing: 0.1em;
|
|
224
|
+
text-transform: uppercase;
|
|
225
|
+
min-width: 80px;
|
|
226
|
+
padding-top: 1px;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* ── Footer ── */
|
|
230
|
+
footer {
|
|
231
|
+
border-top: 1px solid var(--border);
|
|
232
|
+
padding: 1.5rem;
|
|
233
|
+
text-align: center;
|
|
234
|
+
font-size: 11px;
|
|
235
|
+
color: var(--text-3);
|
|
236
|
+
letter-spacing: 0.05em;
|
|
237
|
+
}
|
|
238
|
+
footer a { color: var(--text-3); text-decoration: none; }
|
|
239
|
+
footer a:hover { color: var(--text-2); }
|
|
240
|
+
</style>
|
|
241
|
+
</head>
|
|
242
|
+
<body>
|
|
243
|
+
|
|
244
|
+
<nav>
|
|
245
|
+
<a href="https://kordagencies.com">kordagencies.com</a>
|
|
246
|
+
<span class="sep">/</span>
|
|
247
|
+
<span class="current">Privacy Policy</span>
|
|
248
|
+
</nav>
|
|
249
|
+
|
|
250
|
+
<div class="wrap">
|
|
251
|
+
|
|
252
|
+
<div class="page-header">
|
|
253
|
+
<div class="label">Legal</div>
|
|
254
|
+
<h1>Privacy Policy</h1>
|
|
255
|
+
<div class="meta">
|
|
256
|
+
Kord Agencies · <span>Last updated May 2026</span>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<!-- 1 -->
|
|
261
|
+
<section>
|
|
262
|
+
<h2>Who we are</h2>
|
|
263
|
+
<p>
|
|
264
|
+
This policy applies to VAT Validator MCP and all services operated by
|
|
265
|
+
<strong style="color:var(--text)">Kord Agencies</strong>, a company incorporated in Singapore.
|
|
266
|
+
</p>
|
|
267
|
+
<div class="contact-block">
|
|
268
|
+
<div class="field"><span class="field-label">Company</span> Kord Agencies, Singapore</div>
|
|
269
|
+
<div class="field"><span class="field-label">Contact</span> <a href="mailto:ojas@kordagencies.com">ojas@kordagencies.com</a></div>
|
|
270
|
+
<div class="field"><span class="field-label">Service</span> VAT Validator MCP — <a href="https://kordagencies.com">kordagencies.com</a></div>
|
|
271
|
+
</div>
|
|
272
|
+
</section>
|
|
273
|
+
|
|
274
|
+
<!-- 2 -->
|
|
275
|
+
<section>
|
|
276
|
+
<h2>What we collect</h2>
|
|
277
|
+
|
|
278
|
+
<div class="callout">
|
|
279
|
+
<strong>VAT numbers you submit are never logged or stored.</strong> Query content — the VAT numbers, company names, and invoice amounts passed to our tools — is transmitted to the relevant government API and immediately discarded. It does not touch our database.
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
<table class="data-table">
|
|
283
|
+
<thead>
|
|
284
|
+
<tr>
|
|
285
|
+
<th>Data</th>
|
|
286
|
+
<th>Why</th>
|
|
287
|
+
<th>Stored</th>
|
|
288
|
+
</tr>
|
|
289
|
+
</thead>
|
|
290
|
+
<tbody>
|
|
291
|
+
<tr>
|
|
292
|
+
<td>Email address</td>
|
|
293
|
+
<td>Paid subscribers only — to deliver your API key</td>
|
|
294
|
+
<td><span class="tag tag-yes">Yes</span></td>
|
|
295
|
+
</tr>
|
|
296
|
+
<tr>
|
|
297
|
+
<td>Payment information</td>
|
|
298
|
+
<td>Processed by Stripe — we never see raw card data</td>
|
|
299
|
+
<td><span class="tag tag-no">Stripe only</span></td>
|
|
300
|
+
</tr>
|
|
301
|
+
<tr>
|
|
302
|
+
<td>API key</td>
|
|
303
|
+
<td>Authentication and usage tracking for paid plans</td>
|
|
304
|
+
<td><span class="tag tag-yes">Yes</span></td>
|
|
305
|
+
</tr>
|
|
306
|
+
<tr>
|
|
307
|
+
<td>IP address</td>
|
|
308
|
+
<td>Free tier rate limiting (50 calls/month/IP). Stored as truncated hash, not full address.</td>
|
|
309
|
+
<td><span class="tag tag-ltd">Truncated</span></td>
|
|
310
|
+
</tr>
|
|
311
|
+
<tr>
|
|
312
|
+
<td>Tool name + timestamp</td>
|
|
313
|
+
<td>Aggregate usage statistics (e.g. "validate_vat called at 14:22"). No query content.</td>
|
|
314
|
+
<td><span class="tag tag-ltd">Aggregate</span></td>
|
|
315
|
+
</tr>
|
|
316
|
+
<tr>
|
|
317
|
+
<td>VAT numbers / query content</td>
|
|
318
|
+
<td>Not applicable — not collected</td>
|
|
319
|
+
<td><span class="tag tag-no">Never</span></td>
|
|
320
|
+
</tr>
|
|
321
|
+
</tbody>
|
|
322
|
+
</table>
|
|
323
|
+
</section>
|
|
324
|
+
|
|
325
|
+
<!-- 3 -->
|
|
326
|
+
<section>
|
|
327
|
+
<h2>Where data is stored</h2>
|
|
328
|
+
<p>
|
|
329
|
+
Subscriber API keys and rate-limiting counters are stored in
|
|
330
|
+
<strong style="color:var(--text)">Upstash Redis</strong> (hosted in the United States).
|
|
331
|
+
The application itself runs on <strong style="color:var(--text)">Railway</strong>
|
|
332
|
+
(hosted in the United States). Both providers maintain industry-standard
|
|
333
|
+
encryption at rest and in transit.
|
|
334
|
+
</p>
|
|
335
|
+
<p>
|
|
336
|
+
We do not operate our own database servers. No data is stored in Singapore.
|
|
337
|
+
</p>
|
|
338
|
+
</section>
|
|
339
|
+
|
|
340
|
+
<!-- 4 -->
|
|
341
|
+
<section>
|
|
342
|
+
<h2>Third-party services</h2>
|
|
343
|
+
<p>
|
|
344
|
+
When you call our tools, your VAT number is forwarded to the relevant official
|
|
345
|
+
government registry to perform the lookup. These are read-only API calls — no
|
|
346
|
+
personal data about you is sent to them.
|
|
347
|
+
</p>
|
|
348
|
+
<table class="data-table">
|
|
349
|
+
<thead>
|
|
350
|
+
<tr>
|
|
351
|
+
<th>Third party</th>
|
|
352
|
+
<th>Purpose</th>
|
|
353
|
+
<th>Data sent</th>
|
|
354
|
+
</tr>
|
|
355
|
+
</thead>
|
|
356
|
+
<tbody>
|
|
357
|
+
<tr>
|
|
358
|
+
<td>Stripe</td>
|
|
359
|
+
<td>Payment processing and subscription management</td>
|
|
360
|
+
<td>Name, email, card details (handled entirely by Stripe)</td>
|
|
361
|
+
</tr>
|
|
362
|
+
<tr>
|
|
363
|
+
<td>UK HMRC VAT API v2</td>
|
|
364
|
+
<td>UK VAT number validation</td>
|
|
365
|
+
<td>VAT number only</td>
|
|
366
|
+
</tr>
|
|
367
|
+
<tr>
|
|
368
|
+
<td>EU VIES (ec.europa.eu)</td>
|
|
369
|
+
<td>EU VAT number validation (all 27 member states)</td>
|
|
370
|
+
<td>VAT number only</td>
|
|
371
|
+
</tr>
|
|
372
|
+
<tr>
|
|
373
|
+
<td>Australian ABR (abr.business.gov.au)</td>
|
|
374
|
+
<td>Australian ABN validation</td>
|
|
375
|
+
<td>ABN only</td>
|
|
376
|
+
</tr>
|
|
377
|
+
<tr>
|
|
378
|
+
<td>Anthropic (Claude API)</td>
|
|
379
|
+
<td>AI-powered fraud risk analysis</td>
|
|
380
|
+
<td>Validation result metadata — no raw VAT numbers or personal data</td>
|
|
381
|
+
</tr>
|
|
382
|
+
<tr>
|
|
383
|
+
<td>Upstash Redis</td>
|
|
384
|
+
<td>API key storage and rate limiting</td>
|
|
385
|
+
<td>API keys and truncated IP counters</td>
|
|
386
|
+
</tr>
|
|
387
|
+
<tr>
|
|
388
|
+
<td>Railway</td>
|
|
389
|
+
<td>Application hosting</td>
|
|
390
|
+
<td>Application logs (no query content)</td>
|
|
391
|
+
</tr>
|
|
392
|
+
</tbody>
|
|
393
|
+
</table>
|
|
394
|
+
</section>
|
|
395
|
+
|
|
396
|
+
<!-- 5 -->
|
|
397
|
+
<section>
|
|
398
|
+
<h2>Legal basis and UK GDPR</h2>
|
|
399
|
+
<p>
|
|
400
|
+
Where we process personal data relating to individuals in the UK or EEA, we do so
|
|
401
|
+
under the following legal bases:
|
|
402
|
+
</p>
|
|
403
|
+
<ul class="plain">
|
|
404
|
+
<li><strong style="color:var(--text)">Contract performance</strong> — processing your email and API key to deliver the service you subscribed to.</li>
|
|
405
|
+
<li><strong style="color:var(--text)">Legitimate interests</strong> — rate limiting by truncated IP address to prevent abuse of the free tier.</li>
|
|
406
|
+
<li><strong style="color:var(--text)">Legal obligation</strong> — retaining transaction records as required for tax compliance.</li>
|
|
407
|
+
</ul>
|
|
408
|
+
<p>
|
|
409
|
+
We comply with the UK General Data Protection Regulation (UK GDPR) and the
|
|
410
|
+
Data Protection Act 2018. For EEA residents, we comply with EU GDPR (Regulation
|
|
411
|
+
2016/679). Transfers of personal data to the United States (Upstash, Railway,
|
|
412
|
+
Anthropic) are made under Standard Contractual Clauses or equivalent adequacy
|
|
413
|
+
mechanisms.
|
|
414
|
+
</p>
|
|
415
|
+
</section>
|
|
416
|
+
|
|
417
|
+
<!-- 6 -->
|
|
418
|
+
<section>
|
|
419
|
+
<h2>Data retention</h2>
|
|
420
|
+
<ul class="plain">
|
|
421
|
+
<li>API keys and associated email addresses are retained for the lifetime of the subscription plus 90 days, then deleted.</li>
|
|
422
|
+
<li>Truncated IP rate-limit counters reset automatically each calendar month.</li>
|
|
423
|
+
<li>Aggregate tool usage statistics (tool name + timestamp, no content) are retained for up to 12 months for service improvement.</li>
|
|
424
|
+
<li>Stripe retains payment records in accordance with their own privacy policy and applicable financial regulations.</li>
|
|
425
|
+
</ul>
|
|
426
|
+
</section>
|
|
427
|
+
|
|
428
|
+
<!-- 7 -->
|
|
429
|
+
<section>
|
|
430
|
+
<h2>Your rights</h2>
|
|
431
|
+
<p>
|
|
432
|
+
Under UK GDPR and EU GDPR, you have the right to access, rectify, or erase
|
|
433
|
+
personal data we hold about you. You also have the right to restrict or object
|
|
434
|
+
to processing, and to data portability where applicable.
|
|
435
|
+
</p>
|
|
436
|
+
<p>
|
|
437
|
+
To exercise any of these rights — including requesting deletion of your API key
|
|
438
|
+
and associated email — contact us at:
|
|
439
|
+
</p>
|
|
440
|
+
<div class="contact-block" style="margin-top:0.75rem">
|
|
441
|
+
<div class="field">
|
|
442
|
+
<span class="field-label">Email</span>
|
|
443
|
+
<a href="mailto:ojas@kordagencies.com">ojas@kordagencies.com</a>
|
|
444
|
+
</div>
|
|
445
|
+
<div class="field">
|
|
446
|
+
<span class="field-label">Response</span>
|
|
447
|
+
Within 30 days of receipt
|
|
448
|
+
</div>
|
|
449
|
+
</div>
|
|
450
|
+
<p style="margin-top:1rem">
|
|
451
|
+
If you are located in the UK, you have the right to lodge a complaint with the
|
|
452
|
+
Information Commissioner's Office (ICO) at
|
|
453
|
+
<a href="https://ico.org.uk" style="color:var(--teal)">ico.org.uk</a>.
|
|
454
|
+
If you are in the EEA, you may contact your local supervisory authority.
|
|
455
|
+
</p>
|
|
456
|
+
</section>
|
|
457
|
+
|
|
458
|
+
<!-- 8 -->
|
|
459
|
+
<section>
|
|
460
|
+
<h2>Cookies and tracking</h2>
|
|
461
|
+
<p>
|
|
462
|
+
The VAT Validator MCP API does not use cookies, browser tracking, analytics
|
|
463
|
+
scripts, or fingerprinting of any kind. This privacy policy page itself does not
|
|
464
|
+
set any cookies.
|
|
465
|
+
</p>
|
|
466
|
+
</section>
|
|
467
|
+
|
|
468
|
+
<!-- 9 -->
|
|
469
|
+
<section>
|
|
470
|
+
<h2>Changes to this policy</h2>
|
|
471
|
+
<p>
|
|
472
|
+
We may update this policy from time to time. The date at the top of this page
|
|
473
|
+
reflects when it was last revised. Material changes will be communicated to
|
|
474
|
+
active subscribers by email.
|
|
475
|
+
</p>
|
|
476
|
+
</section>
|
|
477
|
+
|
|
478
|
+
<!-- 10 -->
|
|
479
|
+
<section>
|
|
480
|
+
<h2>Contact</h2>
|
|
481
|
+
<div class="contact-block">
|
|
482
|
+
<div class="field"><span class="field-label">Company</span> Kord Agencies, Singapore</div>
|
|
483
|
+
<div class="field"><span class="field-label">Email</span> <a href="mailto:ojas@kordagencies.com">ojas@kordagencies.com</a></div>
|
|
484
|
+
<div class="field"><span class="field-label">Website</span> <a href="https://kordagencies.com">kordagencies.com</a></div>
|
|
485
|
+
</div>
|
|
486
|
+
</section>
|
|
487
|
+
|
|
488
|
+
</div><!-- /wrap -->
|
|
489
|
+
|
|
490
|
+
<footer>
|
|
491
|
+
<a href="https://kordagencies.com">kordagencies.com</a>
|
|
492
|
+
·
|
|
493
|
+
<a href="https://kordagencies.com/terms.html">Terms</a>
|
|
494
|
+
·
|
|
495
|
+
Privacy Policy
|
|
496
|
+
·
|
|
497
|
+
© 2026 Kord Agencies
|
|
498
|
+
</footer>
|
|
499
|
+
|
|
500
|
+
</body>
|
|
501
|
+
</html>
|
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": "
|
|
6
|
+
"version": "2.0.1",
|
|
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": "
|
|
16
|
+
"version": "2.0.1",
|
|
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,11 +2,22 @@ const http = require('http');
|
|
|
2
2
|
const https = require('https');
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
5
6
|
const Stripe = require('stripe');
|
|
6
7
|
const stripe = Stripe(process.env.STRIPE_SECRET_KEY);
|
|
7
8
|
|
|
8
9
|
const PERSIST_FILE = '/tmp/vat_stats.json';
|
|
9
|
-
const VERSION = '
|
|
10
|
+
const VERSION = '2.0.2';
|
|
11
|
+
|
|
12
|
+
// Persistent device ID for HMRC fraud prevention headers (BATCH_PROCESS_DIRECT)
|
|
13
|
+
const DEVICE_ID_FILE = path.join(__dirname, '..', 'device-id.txt');
|
|
14
|
+
let DEVICE_ID;
|
|
15
|
+
try {
|
|
16
|
+
DEVICE_ID = fs.readFileSync(DEVICE_ID_FILE, 'utf8').trim();
|
|
17
|
+
} catch(e) {
|
|
18
|
+
DEVICE_ID = crypto.randomUUID();
|
|
19
|
+
try { fs.writeFileSync(DEVICE_ID_FILE, DEVICE_ID); } catch(we) {}
|
|
20
|
+
}
|
|
10
21
|
const RESEND_API_KEY = process.env.RESEND_API_KEY || '';
|
|
11
22
|
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || '';
|
|
12
23
|
const PORT = process.env.PORT || 3000;
|
|
@@ -200,6 +211,22 @@ async function validateVIES(countryCode, vatNumber) {
|
|
|
200
211
|
});
|
|
201
212
|
}
|
|
202
213
|
|
|
214
|
+
function getFraudPreventionHeaders() {
|
|
215
|
+
return {
|
|
216
|
+
'Gov-Client-Connection-Method': 'BATCH_PROCESS_DIRECT',
|
|
217
|
+
'Gov-Client-Device-ID': DEVICE_ID,
|
|
218
|
+
'Gov-Client-Local-IPs': '127.0.0.1',
|
|
219
|
+
'Gov-Client-Local-IPs-Timestamp': new Date().toISOString().replace(/(\.\d{3})\d*Z/, '$1Z'),
|
|
220
|
+
'Gov-Client-MAC-Addresses': 'not-applicable',
|
|
221
|
+
'Gov-Client-Timezone': 'UTC+00:00',
|
|
222
|
+
'Gov-Client-User-Agent': 'os-family=Linux&os-version=Server&device-manufacturer=Railway&device-model=Cloud',
|
|
223
|
+
'Gov-Client-User-IDs': 'os=railway-service',
|
|
224
|
+
'Gov-Vendor-License-IDs': 'vat-validator-mcp=not-applicable',
|
|
225
|
+
'Gov-Vendor-Product-Name': 'VAT%20Validator%20MCP',
|
|
226
|
+
'Gov-Vendor-Version': 'vat-validator-mcp=2.0.2'
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
203
230
|
// HMRC OAuth 2.0 token cache
|
|
204
231
|
let hmrcToken = null;
|
|
205
232
|
let hmrcTokenExpiry = 0;
|
|
@@ -223,7 +250,7 @@ async function getHMRCToken() {
|
|
|
223
250
|
hostname,
|
|
224
251
|
path: '/oauth/token',
|
|
225
252
|
method: 'POST',
|
|
226
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': Buffer.byteLength(body) }
|
|
253
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': Buffer.byteLength(body), ...getFraudPreventionHeaders() }
|
|
227
254
|
}, res => {
|
|
228
255
|
let d = ''; res.on('data', c => d += c);
|
|
229
256
|
res.on('end', () => {
|
|
@@ -259,7 +286,7 @@ async function validateHMRC(vatNumber) {
|
|
|
259
286
|
hostname,
|
|
260
287
|
path: '/organisations/vat/check-vat-number/lookup/' + clean,
|
|
261
288
|
method: 'GET',
|
|
262
|
-
headers: { 'Accept': 'application/vnd.hmrc.2.0+json', 'Authorization': 'Bearer ' + token }
|
|
289
|
+
headers: { 'Accept': 'application/vnd.hmrc.2.0+json', 'Authorization': 'Bearer ' + token, ...getFraudPreventionHeaders() }
|
|
263
290
|
}, res => {
|
|
264
291
|
let d = ''; res.on('data', c => d += c);
|
|
265
292
|
res.on('end', () => {
|
|
@@ -298,6 +325,7 @@ async function validateABN(abn) {
|
|
|
298
325
|
function detectCountry(vatNumber) {
|
|
299
326
|
const clean = vatNumber.trim().toUpperCase().replace(/\s/g, '');
|
|
300
327
|
if (clean.startsWith('GB')) return { country: 'GB', type: 'uk', number: clean.slice(2) };
|
|
328
|
+
if (clean.startsWith('ABN')) return { country: 'AU', type: 'au', number: clean.slice(3) };
|
|
301
329
|
if (clean.startsWith('AU') || /^\d{11}$/.test(clean)) return { country: 'AU', type: 'au', number: clean };
|
|
302
330
|
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'];
|
|
303
331
|
for (const code of euCodes) {
|
|
@@ -328,97 +356,193 @@ const VAT_RATES = {
|
|
|
328
356
|
|
|
329
357
|
async function executeTool(name, args) {
|
|
330
358
|
if (name === 'validate_vat') {
|
|
331
|
-
const vat_number = args
|
|
359
|
+
const { vat_number, invoice_company_name, invoice_amount } = args;
|
|
332
360
|
const checkedAt = nowISO();
|
|
333
|
-
|
|
361
|
+
|
|
362
|
+
if (!vat_number) return {
|
|
363
|
+
error: 'vat_number is required',
|
|
364
|
+
agent_action: 'PROVIDE_REQUIRED_FIELD',
|
|
365
|
+
category: 'invalid_input',
|
|
366
|
+
retryable: false,
|
|
367
|
+
retry_after_ms: null,
|
|
368
|
+
fallback_tool: null,
|
|
369
|
+
trace_id: Math.random().toString(36).slice(2, 10)
|
|
370
|
+
};
|
|
371
|
+
|
|
334
372
|
const detected = detectCountry(vat_number);
|
|
373
|
+
let valid = false;
|
|
374
|
+
let company_name = null;
|
|
375
|
+
let address = null;
|
|
376
|
+
let jurisdiction = '';
|
|
377
|
+
let sourceUrl = '';
|
|
378
|
+
|
|
335
379
|
if (detected.type === 'uk') {
|
|
380
|
+
jurisdiction = 'UK';
|
|
381
|
+
sourceUrl = 'api.service.hmrc.gov.uk';
|
|
336
382
|
const result = await validateHMRC(detected.number);
|
|
337
|
-
if (result.error) return {
|
|
383
|
+
if (result.error) return {
|
|
384
|
+
error: result.error,
|
|
385
|
+
vat_number,
|
|
386
|
+
jurisdiction,
|
|
387
|
+
agent_action: 'RETRY_IN_2_MIN',
|
|
388
|
+
category: 'upstream_unavailable',
|
|
389
|
+
retryable: true,
|
|
390
|
+
retry_after_ms: 120000,
|
|
391
|
+
fallback_tool: null,
|
|
392
|
+
trace_id: Math.random().toString(36).slice(2, 10),
|
|
393
|
+
source_url: sourceUrl,
|
|
394
|
+
checked_at: checkedAt,
|
|
395
|
+
_disclaimer: LEGAL_DISCLAIMER
|
|
396
|
+
};
|
|
338
397
|
const d = result.data;
|
|
339
|
-
if (result.status === 200 && d.target)
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
398
|
+
if (result.status === 200 && d.target) {
|
|
399
|
+
valid = true;
|
|
400
|
+
company_name = d.target.name || null;
|
|
401
|
+
address = d.target.address ? Object.values(d.target.address).filter(Boolean).join(', ') : null;
|
|
402
|
+
}
|
|
403
|
+
} else if (detected.type === 'eu') {
|
|
404
|
+
jurisdiction = 'EU';
|
|
405
|
+
sourceUrl = 'ec.europa.eu/taxation_customs/vies';
|
|
343
406
|
const result = await validateVIES(detected.country, detected.number);
|
|
344
|
-
if (result.error) return {
|
|
407
|
+
if (result.error) return {
|
|
408
|
+
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.',
|
|
409
|
+
vat_number,
|
|
410
|
+
jurisdiction,
|
|
411
|
+
agent_action: 'RETRY_IN_30_MIN',
|
|
412
|
+
category: 'upstream_unavailable',
|
|
413
|
+
retryable: true,
|
|
414
|
+
retry_after_ms: 1800000,
|
|
415
|
+
fallback_tool: null,
|
|
416
|
+
trace_id: Math.random().toString(36).slice(2, 10),
|
|
417
|
+
source_url: sourceUrl,
|
|
418
|
+
checked_at: checkedAt,
|
|
419
|
+
_disclaimer: LEGAL_DISCLAIMER
|
|
420
|
+
};
|
|
345
421
|
const d = result.data;
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
422
|
+
valid = d.isValid || false;
|
|
423
|
+
company_name = d.traderName || null;
|
|
424
|
+
address = d.traderAddress || null;
|
|
425
|
+
} else if (detected.type === 'au') {
|
|
426
|
+
jurisdiction = 'AU';
|
|
427
|
+
sourceUrl = 'abr.business.gov.au';
|
|
349
428
|
const result = await validateABN(detected.number);
|
|
350
|
-
if (result.error) return {
|
|
429
|
+
if (result.error) return {
|
|
430
|
+
error: result.error,
|
|
431
|
+
vat_number,
|
|
432
|
+
jurisdiction,
|
|
433
|
+
agent_action: 'RETRY_IN_2_MIN',
|
|
434
|
+
category: 'upstream_unavailable',
|
|
435
|
+
retryable: true,
|
|
436
|
+
retry_after_ms: 120000,
|
|
437
|
+
fallback_tool: null,
|
|
438
|
+
trace_id: Math.random().toString(36).slice(2, 10),
|
|
439
|
+
source_url: sourceUrl,
|
|
440
|
+
checked_at: checkedAt,
|
|
441
|
+
_disclaimer: LEGAL_DISCLAIMER
|
|
442
|
+
};
|
|
351
443
|
const d = result.data;
|
|
352
|
-
|
|
353
|
-
|
|
444
|
+
valid = !!(d.Abn && d.AbnStatus === 'Active');
|
|
445
|
+
company_name = d.EntityName || null;
|
|
446
|
+
} else {
|
|
447
|
+
return {
|
|
448
|
+
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).',
|
|
449
|
+
vat_number,
|
|
450
|
+
agent_action: 'PROVIDE_COUNTRY_PREFIX',
|
|
451
|
+
category: 'invalid_input',
|
|
452
|
+
retryable: false,
|
|
453
|
+
retry_after_ms: null,
|
|
454
|
+
fallback_tool: null,
|
|
455
|
+
trace_id: Math.random().toString(36).slice(2, 10),
|
|
456
|
+
_disclaimer: LEGAL_DISCLAIMER
|
|
457
|
+
};
|
|
354
458
|
}
|
|
355
|
-
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 };
|
|
356
|
-
}
|
|
357
459
|
|
|
358
|
-
|
|
359
|
-
const
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
const result = await validateHMRC(vat_number);
|
|
363
|
-
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 };
|
|
364
|
-
const d = result.data;
|
|
365
|
-
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 };
|
|
366
|
-
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 };
|
|
367
|
-
}
|
|
460
|
+
// AI fraud risk analysis — runs internally, result is always returned in one call
|
|
461
|
+
const nameSection = invoice_company_name ? `Invoice Company Name: ${invoice_company_name}\n` : '';
|
|
462
|
+
const amountSection = invoice_amount != null ? `Invoice Amount: ${invoice_amount}\n` : '';
|
|
463
|
+
const prompt = `You are a B2B fraud detection specialist. Analyze this VAT validation result for fraud risk.
|
|
368
464
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
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 });
|
|
377
|
-
}
|
|
465
|
+
VAT Number: ${vat_number}
|
|
466
|
+
Jurisdiction: ${jurisdiction}
|
|
467
|
+
Valid/Active: ${valid}
|
|
468
|
+
Registered Company Name: ${company_name || 'Not available from registry'}
|
|
469
|
+
Registered Address: ${address || 'Not available from registry'}
|
|
470
|
+
${nameSection}${amountSection}
|
|
471
|
+
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.
|
|
378
472
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
473
|
+
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.
|
|
474
|
+
|
|
475
|
+
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.
|
|
476
|
+
|
|
477
|
+
Return ONLY valid JSON with no preamble or markdown:
|
|
478
|
+
{"fraud_risk_score":0,"fraud_risk_level":"LOW","fraud_signals":[],"name_match":"NOT_CHECKED","recommendation":"CLEAR","summary":"one sentence plain English"}`;
|
|
479
|
+
|
|
480
|
+
let fraudRiskScore = 50;
|
|
481
|
+
let fraudRiskLevel = 'MEDIUM';
|
|
482
|
+
let fraudSignals = [];
|
|
483
|
+
let nameMatch = 'NOT_CHECKED';
|
|
484
|
+
let recommendation = 'REVIEW';
|
|
485
|
+
let summary = 'Manual review recommended — AI analysis unavailable.';
|
|
389
486
|
|
|
390
|
-
if (name === 'analyse_vat_risk') {
|
|
391
|
-
const vat_number = args.vat_number;
|
|
392
|
-
const validation_result = args.validation_result;
|
|
393
|
-
const invoice_amount = args.invoice_amount;
|
|
394
|
-
const invoice_company_name = args.invoice_company_name;
|
|
395
|
-
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) };
|
|
396
|
-
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"}';
|
|
397
487
|
try {
|
|
398
|
-
const
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
488
|
+
const aiResponse = await callClaude(prompt);
|
|
489
|
+
const parsed = JSON.parse(aiResponse.replace(/```json|```/g, '').trim());
|
|
490
|
+
fraudRiskScore = typeof parsed.fraud_risk_score === 'number' ? parsed.fraud_risk_score : fraudRiskScore;
|
|
491
|
+
fraudRiskLevel = parsed.fraud_risk_level || fraudRiskLevel;
|
|
492
|
+
fraudSignals = Array.isArray(parsed.fraud_signals) ? parsed.fraud_signals : [];
|
|
493
|
+
nameMatch = parsed.name_match || nameMatch;
|
|
494
|
+
recommendation = parsed.recommendation || recommendation;
|
|
495
|
+
summary = parsed.summary || summary;
|
|
402
496
|
} catch(e) {
|
|
403
|
-
|
|
497
|
+
if (!valid) {
|
|
498
|
+
fraudRiskLevel = 'HIGH';
|
|
499
|
+
fraudRiskScore = 75;
|
|
500
|
+
fraudSignals = ['VAT number invalid or deregistered'];
|
|
501
|
+
recommendation = 'BLOCK';
|
|
502
|
+
summary = 'VAT number is invalid or deregistered.';
|
|
503
|
+
}
|
|
404
504
|
}
|
|
405
|
-
}
|
|
406
505
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
const agentAction = result.match_verdict === 'MATCH' ? 'PROCEED' : 'INVESTIGATE';
|
|
415
|
-
return Object.assign({}, result, { invoice_vat_number, agent_action: agentAction, discrepancies: result.discrepancies || [], _disclaimer: LEGAL_DISCLAIMER });
|
|
416
|
-
} catch(e) {
|
|
417
|
-
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 };
|
|
506
|
+
let agentAction;
|
|
507
|
+
if (!valid || fraudRiskLevel === 'CRITICAL' || nameMatch === 'MISMATCH') {
|
|
508
|
+
agentAction = 'HOLD';
|
|
509
|
+
} else if (fraudRiskLevel === 'HIGH' || fraudRiskLevel === 'MEDIUM') {
|
|
510
|
+
agentAction = 'VERIFY_MANUALLY';
|
|
511
|
+
} else {
|
|
512
|
+
agentAction = 'PROCEED';
|
|
418
513
|
}
|
|
514
|
+
|
|
515
|
+
return {
|
|
516
|
+
agent_action: agentAction,
|
|
517
|
+
valid,
|
|
518
|
+
vat_number,
|
|
519
|
+
jurisdiction,
|
|
520
|
+
company_name,
|
|
521
|
+
address,
|
|
522
|
+
fraud_risk_score: fraudRiskScore,
|
|
523
|
+
fraud_risk_level: fraudRiskLevel,
|
|
524
|
+
fraud_signals: fraudSignals,
|
|
525
|
+
name_match: nameMatch,
|
|
526
|
+
recommendation,
|
|
527
|
+
summary,
|
|
528
|
+
source_url: sourceUrl,
|
|
529
|
+
checked_at: checkedAt,
|
|
530
|
+
_disclaimer: LEGAL_DISCLAIMER,
|
|
531
|
+
ai_notice: 'AI-powered fraud analysis — NOT a simple database lookup'
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (name === 'get_vat_rates') {
|
|
536
|
+
const country_code = args.country_code;
|
|
537
|
+
const checkedAt = nowISO();
|
|
538
|
+
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 };
|
|
539
|
+
const code = country_code.toUpperCase();
|
|
540
|
+
const rate = VAT_RATES[code];
|
|
541
|
+
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 };
|
|
542
|
+
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 });
|
|
419
543
|
}
|
|
420
544
|
|
|
421
|
-
return { error: 'Unknown tool: ' + name,
|
|
545
|
+
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) };
|
|
422
546
|
}
|
|
423
547
|
|
|
424
548
|
function checkAccess(req) {
|
|
@@ -558,12 +682,32 @@ async function handleStripeWebhook(body, sig) {
|
|
|
558
682
|
}
|
|
559
683
|
|
|
560
684
|
const tools = [
|
|
561
|
-
{
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
685
|
+
{
|
|
686
|
+
name: 'validate_vat',
|
|
687
|
+
idempotent: true,
|
|
688
|
+
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.',
|
|
689
|
+
inputSchema: {
|
|
690
|
+
type: 'object',
|
|
691
|
+
properties: {
|
|
692
|
+
vat_number: { type: 'string', description: 'VAT number with country prefix. EU: DE123456789. UK: GB123456789. AU: ABN12345678901.' },
|
|
693
|
+
invoice_company_name: { type: 'string', description: 'Company name as it appears on the invoice — if provided, cross-checks against registry and flags mismatches.' },
|
|
694
|
+
invoice_amount: { type: 'number', description: 'Invoice amount in local currency — used in fraud risk weighting.' }
|
|
695
|
+
},
|
|
696
|
+
required: ['vat_number']
|
|
697
|
+
}
|
|
698
|
+
},
|
|
699
|
+
{
|
|
700
|
+
name: 'get_vat_rates',
|
|
701
|
+
idempotent: true,
|
|
702
|
+
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.',
|
|
703
|
+
inputSchema: {
|
|
704
|
+
type: 'object',
|
|
705
|
+
properties: {
|
|
706
|
+
country_code: { type: 'string', description: 'ISO 2-letter code e.g. DE, FR, GB. Omit for all countries.' }
|
|
707
|
+
},
|
|
708
|
+
required: []
|
|
709
|
+
}
|
|
710
|
+
}
|
|
567
711
|
];
|
|
568
712
|
|
|
569
713
|
const sseClients = new Map();
|
|
@@ -655,7 +799,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
655
799
|
|
|
656
800
|
if (req.url === '/.well-known/mcp/server-card.json' && req.method === 'GET') {
|
|
657
801
|
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
658
|
-
res.end(JSON.stringify({ name: 'vat-validator-mcp', title: 'VAT Validator MCP', version: VERSION, description: 'VAT
|
|
802
|
+
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' }));
|
|
659
803
|
return;
|
|
660
804
|
}
|
|
661
805
|
|
|
@@ -686,7 +830,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
686
830
|
const request = JSON.parse(body);
|
|
687
831
|
let response;
|
|
688
832
|
if (request.method === 'initialize') {
|
|
689
|
-
response = { jsonrpc: '2.0', id: request.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, resources: {}, prompts: {} }, serverInfo: { name: 'vat-validator-mcp', version: VERSION, description: '
|
|
833
|
+
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.' } } };
|
|
690
834
|
} else if (request.method === 'notifications/initialized') {
|
|
691
835
|
res.writeHead(204, cors); res.end(); return;
|
|
692
836
|
} else if (request.method === 'tools/list') {
|
|
@@ -742,7 +886,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
742
886
|
req._tier = access.tier;
|
|
743
887
|
req._accessResult = access;
|
|
744
888
|
}
|
|
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: '
|
|
889
|
+
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.' } } };
|
|
746
890
|
} else if (request.method === 'notifications/initialized') { res.writeHead(204, cors); res.end(); return;
|
|
747
891
|
} else if (request.method === 'tools/list') { response = { jsonrpc: '2.0', id: request.id, result: { tools } };
|
|
748
892
|
} else if (request.method === 'resources/list') { response = { jsonrpc: '2.0', id: request.id, result: { resources: [] } };
|
|
@@ -761,31 +905,16 @@ const server = http.createServer(async (req, res) => {
|
|
|
761
905
|
reportMeteredUsage(req._accessResult.stripeCustomerId, 'vat_query').catch(() => {});
|
|
762
906
|
}
|
|
763
907
|
|
|
764
|
-
// Partial response for free tier
|
|
765
908
|
if (req._tier === 'free' && !result.error) {
|
|
766
909
|
const used = freeTierUsage.get(getMonthKey(ip)) || 0;
|
|
767
910
|
const remaining = FREE_TIER_LIMIT - used;
|
|
768
911
|
const isWarning = used >= FREE_TIER_WARNING;
|
|
769
912
|
const effectiveLimit = getEffectiveLimit(ip);
|
|
770
913
|
|
|
771
|
-
if (name === 'validate_vat'
|
|
772
|
-
const gated = ['
|
|
773
|
-
gated.forEach(f => delete result[f]);
|
|
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.';
|
|
775
|
-
result._gated_fields = gated;
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
if (name === 'analyse_vat_risk') {
|
|
779
|
-
const gated = ['fraud_signals', 'positive_indicators', 'recommended_action', 'summary'];
|
|
780
|
-
gated.forEach(f => delete result[f]);
|
|
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.';
|
|
782
|
-
result._gated_fields = gated;
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
if (name === 'compare_invoice_details') {
|
|
786
|
-
const gated = ['discrepancies', 'name_match', 'address_match', 'recommended_action', 'summary'];
|
|
914
|
+
if (name === 'validate_vat') {
|
|
915
|
+
const gated = ['fraud_signals', 'address'];
|
|
787
916
|
gated.forEach(f => delete result[f]);
|
|
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
|
|
917
|
+
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.';
|
|
789
918
|
result._gated_fields = gated;
|
|
790
919
|
}
|
|
791
920
|
|
|
@@ -801,7 +930,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
801
930
|
return;
|
|
802
931
|
}
|
|
803
932
|
|
|
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:
|
|
933
|
+
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; }
|
|
805
934
|
|
|
806
935
|
if (req.url === '/subscribe' && req.method === 'GET') {
|
|
807
936
|
try {
|
|
@@ -869,7 +998,7 @@ function setupStdio() {
|
|
|
869
998
|
try { req = JSON.parse(line); } catch(e) { return; }
|
|
870
999
|
let response;
|
|
871
1000
|
if (req.method === 'initialize') {
|
|
872
|
-
response = { jsonrpc: '2.0', id: req.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, resources: {}, prompts: {} }, serverInfo: { name: 'vat-validator-mcp', version: VERSION, description: '
|
|
1001
|
+
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.' } } };
|
|
873
1002
|
} else if (req.method === 'notifications/initialized') {
|
|
874
1003
|
return;
|
|
875
1004
|
} else if (req.method === 'tools/list') {
|