Open-source MCP server that lets Claude Desktop, Cursor and other MCP clients validate EU VAT numbers and look up rates directly in chat.
Stack
Libraries
Services
Community node that puts VIES VAT validation and EU rate lookups inside n8n, so an onboarding or invoicing workflow can decide on a real VAT number instead of a well-formed one.
Key Results

The people who most need a VAT number checked are usually not the people who can call an API. A finance operations lead onboarding B2B customers, an agency automating invoicing, a founder wiring together a signup flow — they build in n8n, and in n8n a VAT number was just a string.
What they could do with a regular expression is confirm a number looks like a German VAT number. What decides whether an invoice carries VAT is whether that number is actually registered — and the official EU register, VIES, speaks SOAP over an endpoint that goes down country by country. Reaching it from a no-code tool means an HTTP Request node, hand-built XML, and a parser that breaks the first time a country is offline.
So the check either doesn't happen, or it happens once by hand at the wrong end of the month.
I published a community node that makes VAT validation an ordinary step in a workflow. Two resources, four operations, and a deliberate split between what costs nothing and what costs a request:
VAT Number — Check Format runs entirely offline against the country's pattern: no key, no network call, no quota. Validate verifies the number against the live VIES register through vatnode and returns validity, the registered company name and address, the registration date, and the VIES consultation number when the account has a requester VAT configured.
VAT Rate — Get returns the standard, reduced, super-reduced and parking rates for one country plus its VAT number format; Get Many returns the same for every covered country as one n8n item each. Both are free and need no account.
The intended shape of a workflow is that order: Check Format filters typos for nothing, Validate confirms only the survivors against VIES, and an IF node downstream routes on valid while the consultation number is written to the customer record as evidence. Nobody spends quota on a mistyped number.
The node is also declared usableAsTool, so an n8n AI agent can call VAT validation directly as one of its tools — the same capability the MCP server gives Claude and Cursor, delivered inside the automation platform instead of the desktop assistant.
| Metric | Value |
|---|---|
| Release | [email protected], 5 August 2026 |
| Surface | 2 resources, 4 operations |
| Free of quota | 3 of 4 operations — format check and both rate ops |
| Source | 251 lines node + 64 lines credential, TypeScript |
| Countries | EU-27 + XI via VIES, ~45 for rate lookup |
| CI secrets | 0 — npm Trusted Publishing via OIDC |
| Provenance | Sigstore-signed, publicly auditable per release |
| License | MIT |
This is the automation-platform surface of the same VAT stack that also ships as a SaaS API, an open rate dataset, an MCP server and a WooCommerce plugin. Each one meets a different person where they already work: the developer in code, the AI assistant in chat, the shop owner in WordPress, and here the operations lead in a workflow canvas.
For the technically curious, here is how the core pieces are built.
n8n calls the credential test whenever someone saves a key, and a naive implementation validates a real VAT number — which means every save spends a request from the user's monthly quota, and a user fiddling with settings burns their free plan before running a single workflow.
The test sends the reserved XX0000001 number instead. No country uses the XX prefix, so the API rejects it on format before it ever reaches VIES: the request proves the key authenticates and stops there. A working key and a rejected key are still perfectly distinguishable — only the quota is spared.
An n8n node receives a batch of items, and in a VAT workflow the batch is a customer list where one row will have a typo. The loop isolates failures per item and honours the user's continueOnFail setting, so a bad row becomes an item with an error field rather than a dead execution:
} catch (error) {
if (this.continueOnFail()) {
returnData.push({ json: { error: (error as Error).message }, pairedItem: { item: i } });
continue;
}
// Input problems are ours, not the API's — keep them out of NodeApiError.
if (error instanceof NodeOperationError) {
throw new NodeOperationError(this.getNode(), error.message, { itemIndex: i });
}
throw new NodeApiError(this.getNode(), error as JsonObject, { itemIndex: i });
}The distinction between NodeOperationError and NodeApiError matters in the UI: the first tells the user their input is wrong, the second tells them the upstream failed. Collapsing both into one error type sends people debugging their credentials when they actually typed an empty VAT number.
Every output item also carries pairedItem, so n8n can trace each result back to the row it came from — without it the "item linking" breaks and downstream nodes lose the connection to the original customer.
An n8n parameter declared type: 'string' is not guaranteed to arrive as a string. Users fill these with expressions, and {{ $json.vatId }} on an item that has no vatId resolves to undefined. Calling .toUpperCase() on it crashes the node with a stack trace instead of a readable message:
function toStringParam(value: unknown): string {
return value === undefined || value === null ? "" : String(value);
}Coerced to an empty string, it flows into the same explicit emptiness check that produces "The VAT number is empty" — a message the person who wrote the expression can act on.
The format check needs every country's VAT pattern, and hardcoding 45 regexes into the node makes them a second source of truth that quietly drifts from the API. A build-time script generates countries.generated.ts from the same open eu-vat-rates-data package the rest of the stack uses, so a pattern is corrected in one place and reaches the node on the next release.
One quirk survives the generation on purpose: VIES writes Greece as EL, while the rate endpoint keys it as GR. Both are accepted on input and mapped where needed — a user should not have to know which EU service decided which.
AvailableNeed something similar?
I build custom solutions — from APIs to full products. Let's talk about your project.
Open-source MCP server that lets Claude Desktop, Cursor and other MCP clients validate EU VAT numbers and look up rates directly in chat.
Stack
Libraries
Services
Free, open-source EU VAT rates for all 27 member states + UK. Published as native packages for npm, PyPI, Packagist, Go, and RubyGems.
How to publish one dataset to npm, PyPI, Go Module, RubyGems, and Packagist automatically with GitHub Actions — architecture, versioning, and per-ecosystem
Use linkinator to catch broken links before production: local URL rewriting, --silent flag, GitHub Actions integration, and false positive patterns for