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 CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "vat-validator-mcp",
3
3
  "mcpName": "io.github.OjasKord/vat-validator-mcp",
4
- "version": "1.4.13",
5
- "description": "VAT number validation for AI agents. EU VIES, UK HMRC, Australian ABN in one call.",
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 &nbsp;·&nbsp; <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
+ &nbsp;·&nbsp;
493
+ <a href="https://kordagencies.com/terms.html">Terms</a>
494
+ &nbsp;·&nbsp;
495
+ Privacy Policy
496
+ &nbsp;·&nbsp;
497
+ &copy; 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": "1.4.13",
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": "1.4.13",
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: "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."
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 (20 validations/month)."
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 to give agents a complete picture before any B2B transaction.
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
- - Validates any EU, UK, or Australian VAT number against live government registries
59
- - Auto-detects country from prefix
60
- - Free tier: first 20 calls/month, no API key needed
61
-
62
- validate_uk_vat
63
- - UK-specific HMRC validation with consultation number for audit trail
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
- - Current VAT rates for all 27 EU countries, UK, and Australia
68
- - Free tier: first 20 calls/month, no API key needed
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
- batch_validate
71
- - Validate up to 10 VAT numbers in one call
72
- - Use for supplier audits and onboarding batches
73
- - Paid API key required
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
- analyse_vat_risk (AI-POWERED)
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
- compare_invoice_details (AI-POWERED)
83
- - Call when processing an invoice to verify supplier details match registry records
84
- - AI compares invoice name/address/VAT against official registered data
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
- 20 calls/month with no API key.
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 = '1.4.13';
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.vat_number;
359
+ const { vat_number, invoice_company_name, invoice_amount } = args;
332
360
  const checkedAt = nowISO();
333
- if (!vat_number) return { error: 'vat_number is 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) };
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 { valid: null, vat_number, country: 'GB', source: 'HMRC', error: result.error, likely_cause: 'external VAT registry temporarily unavailable', agent_action: 'RETRY_IN_2_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 120000, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10), retry: true, _disclaimer: LEGAL_DISCLAIMER };
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) return { valid: true, agent_action: 'PROCEED', vat_number, country: 'GB', company_name: d.target.name || null, source: 'HMRC', source_url: 'api.service.hmrc.gov.uk', consultation_number: d.consultationNumber || null, checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
340
- return { valid: false, agent_action: 'VERIFY_MANUALLY', vat_number, country: 'GB', source: 'HMRC', source_url: 'api.service.hmrc.gov.uk', reason: d.code || 'VAT number not found', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
341
- }
342
- if (detected.type === 'eu') {
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 { valid: null, vat_number, agent_action: 'RETRY_IN_30_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 1800000, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10), country: detected.country, source: 'VIES', source_url: 'ec.europa.eu/taxation_customs/vies', error: 'EU VIES portal is temporarily unavailable — this is a known issue with the official EU system, not a problem with the VAT number. Retry in 30 minutes.', likely_cause: 'external VAT registry temporarily unavailable', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
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
- return { valid: d.isValid || false, agent_action: d.isValid ? 'PROCEED' : 'VERIFY_MANUALLY', vat_number, country: detected.country, company_name: d.traderName || null, address: d.traderAddress || null, source: 'VIES', source_url: 'ec.europa.eu/taxation_customs/vies', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
347
- }
348
- if (detected.type === 'au') {
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 { valid: null, vat_number, country: 'AU', source: 'ABR', error: result.error, likely_cause: 'external VAT registry temporarily unavailable', agent_action: 'RETRY_IN_2_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 120000, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10), _disclaimer: LEGAL_DISCLAIMER };
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
- const isValidABN = !!(d.Abn && d.AbnStatus === 'Active');
353
- return { valid: isValidABN, agent_action: isValidABN ? 'PROCEED' : 'VERIFY_MANUALLY', vat_number, country: 'AU', company_name: d.EntityName || null, abn_status: d.AbnStatus || null, source: 'ABR', source_url: 'abr.business.gov.au', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
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
- if (name === 'validate_uk_vat') {
359
- const vat_number = args.vat_number;
360
- const checkedAt = nowISO();
361
- if (!vat_number) return { error: 'vat_number is 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) };
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
- if (name === 'get_vat_rates') {
370
- const country_code = args.country_code;
371
- const checkedAt = nowISO();
372
- if (!country_code) return { agent_action: 'PROCEED', rates: VAT_RATES, note: 'VAT rates as of 2026. Verify with official tax authority before use.', source_url: 'kordagencies.com', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
373
- const code = country_code.toUpperCase();
374
- const rate = VAT_RATES[code];
375
- if (!rate) return { error: 'No VAT rate data for: ' + code + '. Supported: ' + Object.keys(VAT_RATES).join(', '), likely_cause: 'required field missing or malformed', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10), _disclaimer: LEGAL_DISCLAIMER };
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
- if (name === 'batch_validate') {
380
- const vat_numbers = args.vat_numbers;
381
- if (!vat_numbers || !Array.isArray(vat_numbers)) return { error: 'vat_numbers must be an array', 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) };
382
- if (vat_numbers.length > 10) return { error: 'Maximum 10 VAT numbers per batch. Upgrade to Enterprise at kordagencies.com for unlimited batches.', likely_cause: 'required field missing or malformed', agent_action: 'Reduce batch to 10 or fewer, or upgrade to Enterprise at kordagencies.com', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10) };
383
- const results = await Promise.all(vat_numbers.map(async (vat) => {
384
- try { return await executeTool('validate_vat', { vat_number: vat }); }
385
- catch(e) { return { vat_number: vat, valid: null, error: e.message, likely_cause: 'external VAT registry temporarily unavailable', agent_action: 'RETRY_IN_2_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 120000, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10) }; }
386
- }));
387
- return { agent_action: 'PROCEED', summary: { total: results.length, valid: results.filter(r => r.valid === true).length, invalid: results.filter(r => r.valid === false).length, error: results.filter(r => r.valid === null).length }, results, _disclaimer: LEGAL_DISCLAIMER };
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 response = await callClaude(prompt);
399
- const result = JSON.parse(response.replace(/```json|```/g, '').trim());
400
- const vatRiskAction = (result.risk_level === 'HIGH' || result.risk_level === 'CRITICAL') ? 'HOLD' : result.risk_level === 'MEDIUM' ? 'VERIFY_MANUALLY' : 'PROCEED';
401
- return Object.assign({}, result, { vat_number, agent_action: vatRiskAction, _disclaimer: LEGAL_DISCLAIMER });
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
- return { recommendation: 'REVIEW', risk_level: 'MEDIUM', risk_score: 50, vat_number, error: 'AI analysis unavailable - manual review recommended', likely_cause: 'AI analysis failed — transient Anthropic API issue', agent_action: 'RETRY_IN_2_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 120000, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10), _disclaimer: LEGAL_DISCLAIMER };
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
- if (name === 'compare_invoice_details') {
408
- const { invoice_company_name, invoice_address, invoice_vat_number, validation_result } = args;
409
- if (!invoice_company_name || !invoice_vat_number || !validation_result) return { error: 'invoice_company_name, invoice_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) };
410
- const prompt = 'You are an invoice fraud detection specialist. Compare invoice details against official registry records.\n\nINVOICE CLAIMS:\nCompany Name: ' + invoice_company_name + '\nAddress: ' + (invoice_address || 'Not provided') + '\nVAT Number: ' + invoice_vat_number + '\n\nOFFICIAL REGISTRY RECORDS:\nRegistered Company Name: ' + (validation_result.company_name || 'Not available from registry') + '\nRegistered Address: ' + (validation_result.address || validation_result.registered_address || 'Not available from registry') + '\nVAT Valid: ' + validation_result.valid + '\nCountry: ' + validation_result.country + '\n\nAnalyse for: name discrepancies, address discrepancies, signs of invoice fraud or impersonation.\n\nReturn ONLY valid JSON with no preamble: {"match_verdict":"MATCH|PARTIAL_MATCH|MISMATCH|UNVERIFIABLE","name_match":"EXACT|SIMILAR|DIFFERENT|UNVERIFIABLE","address_match":"MATCH|DIFFERENT|UNVERIFIABLE","vat_valid":true,"discrepancies":[],"fraud_risk":"LOW|MEDIUM|HIGH","recommendation":"APPROVE|REVIEW|REJECT","recommended_action":"one sentence","summary":"two sentences"}';
411
- try {
412
- const response = await callClaude(prompt);
413
- const result = JSON.parse(response.replace(/```json|```/g, '').trim());
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, likely_cause: 'required field missing or malformed', 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) };
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
- { 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'] } }
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 validation + AI fraud detection. EU VIES, UK HMRC, Australian ABN. Free tier: 20 calls/month.', 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', 'validate_uk_vat', 'get_vat_rates', 'batch_validate', 'analyse_vat_risk', 'compare_invoice_details'], circuit_breaker: false, health_endpoint: '/health', ready_endpoint: '/ready' }));
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: '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.' } } };
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: '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.' } } };
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' || name === 'validate_uk_vat') {
772
- const gated = ['registered_address', 'address', 'consultation_number'];
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 discrepancy analysis and recommended action.';
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: 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; }
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: '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.' } } };
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') {