Skip to main content
Glama

Validate UK VAT Number (HMRC)

vat_validate
Read-onlyIdempotent

Validate a UK VAT number against the HMRC register to retrieve the registered trading name and address for due diligence checks.

Instructions

Validate a UK VAT number against the HMRC register.

Returns the trading name and address as registered with HMRC for VAT purposes. The VAT-registered trading address often differs from the Companies House registered address — that discrepancy is a due diligence signal worth noting.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
vat_numberYesUK VAT registration number. Accepts: 'GB123456789', '123456789', 'GB 123 456 789'. GB prefix and spaces normalised automatically.

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
validYesTrue if HMRC confirmed the VAT number is currently registered. False means HMRC returned 404 (not registered / deregistered).
vat_numberYesCanonical VAT number in 'GB<9 digits>' format.
trading_nameNoTrading name registered with HMRC for VAT. Compare with the Companies House name — discrepancies are a due diligence signal.
registered_addressNoVAT-registered trading address. May differ from the Companies House registered office address.
consultation_numberNoHMRC consultation reference number for this lookup.

Implementation Reference

  • The vat_validate async function that executes the tool logic: normalises the VAT number, obtains a Bearer token from HMRC, calls the HMRC VAT lookup API v2, and returns a VATValidationResult with trading name, address, and consultation number.
    async def vat_validate(
        vat_number: Annotated[str, Field(description="UK VAT registration number. Accepts: 'GB123456789', '123456789', 'GB 123 456 789'. GB prefix and spaces normalised automatically.", min_length=9, max_length=15)],
    ) -> VATValidationResult:
        """Validate a UK VAT number against the HMRC register.
    
        Returns the trading name and address as registered with HMRC for VAT
        purposes. The VAT-registered trading address often differs from the
        Companies House registered address — that discrepancy is a due
        diligence signal worth noting.
        """
        # Normalise VAT number: strip GB prefix, spaces, hyphens
        clean_vat = vat_number.upper().replace("GB", "").replace(" ", "").replace("-", "")
        if not clean_vat.isdigit() or len(clean_vat) != 9:
            raise ValueError(
                f"Invalid VAT number format: '{vat_number}'. "
                "Must be 9 digits after removing 'GB' prefix and spaces."
            )
        vat_number = clean_vat
    
        token = await _get_bearer_token()
        base = _hmrc_base()
        url = f"{base}{HMRC_VAT_LOOKUP_PATH}/{vat_number}"
    
        async with httpx.AsyncClient(timeout=10.0) as client:
            resp = await client.get(
                url,
                headers={
                    "Accept": "application/json",
                    "Authorization": f"Bearer {token}",
                },
            )
    
            if resp.status_code == 404:
                return VATValidationResult(
                    valid=False,
                    vat_number=f"GB{vat_number}",
                    trading_name=None,
                    registered_address=None,
                    consultation_number=None,
                )
    
            resp.raise_for_status()
            data = resp.json()
    
        target = data.get("target", {})
        name = target.get("name")
        confirmed_vat = target.get("vatNumber", vat_number)
        address = _format_address(target.get("address", {})) or None
        consultation_number = data.get("consultationNumber")
    
        return VATValidationResult(
            valid=True,
            vat_number=f"GB{confirmed_vat}",
            trading_name=name,
            registered_address=address,
            consultation_number=consultation_number,
        )
  • hmrc_vat.py:112-123 (registration)
    The register_tools function that registers the vat_validate tool with FastMCP using @mcp.tool(name='vat_validate', ...) decorator.
    def register_tools(mcp: FastMCP) -> None:
    
        @mcp.tool(
            name="vat_validate",
            annotations={
                "title": "Validate UK VAT Number (HMRC)",
                "readOnlyHint": True,
                "destructiveHint": False,
                "idempotentHint": True,
                "openWorldHint": True,
            },
        )
  • The VATValidationResult Pydantic model that defines the output schema: valid (bool), vat_number (str), trading_name (str|None), registered_address (str|None), consultation_number (str|None).
    class VATValidationResult(BaseModel):
        """HMRC VAT validation result."""
    
        model_config = BASE_CFG
    
        valid: bool = Field(
            ...,
            description=(
                "True if HMRC confirmed the VAT number is currently registered. "
                "False means HMRC returned 404 (not registered / deregistered)."
            ),
        )
        vat_number: str = Field(
            ...,
            description="Canonical VAT number in 'GB<9 digits>' format.",
        )
        trading_name: str | None = Field(
            None,
            description=(
                "Trading name registered with HMRC for VAT. Compare with the "
                "Companies House name — discrepancies are a due diligence signal."
            ),
        )
        registered_address: str | None = Field(
            None,
            description=(
                "VAT-registered trading address. May differ from the Companies "
                "House registered office address."
            ),
        )
        consultation_number: str | None = Field(
            None,
            description="HMRC consultation reference number for this lookup.",
        )
  • Helper function _get_bearer_token() obtains (or returns cached) HMRC OAuth2 Bearer token using client_credentials grant. Also includes _hmrc_env(), _hmrc_base() helpers and _format_address() utility.
    async def _get_bearer_token() -> str:
        """Obtain (or return cached) HMRC application-restricted Bearer token."""
        client_id = os.environ.get("HMRC_CLIENT_ID")
        client_secret = os.environ.get("HMRC_CLIENT_SECRET")
        if not client_id or not client_secret:
            raise ValueError(
                "HMRC VAT validation requires application credentials. "
                "Set HMRC_CLIENT_ID and HMRC_CLIENT_SECRET environment variables. "
                "Register your application at https://developer.service.hmrc.gov.uk"
            )
    
        # Return cached token if still valid (with 60s buffer)
        now = time.time()
        if _token_cache["token"] and now < _token_cache["expires_at"] - 60:
            return _token_cache["token"]
    
        token_url = f"{_hmrc_base()}/oauth/token"
        async with httpx.AsyncClient(timeout=10.0) as client:
            resp = await client.post(
                token_url,
                data={
                    "grant_type": "client_credentials",
                    "client_id": client_id,
                    "client_secret": client_secret,
                },
                headers={"Content-Type": "application/x-www-form-urlencoded"},
            )
            resp.raise_for_status()
            data = resp.json()
    
        token = data["access_token"]
        expires_in = int(data.get("expires_in", 14400))
        _token_cache["token"] = token
        _token_cache["expires_at"] = now + expires_in
        return token
    
    
    def _format_address(addr: dict[str, Any]) -> str:
        lines = [
            addr.get("line1", ""),
            addr.get("line2", ""),
            addr.get("line3", ""),
            addr.get("line4", ""),
            addr.get("postCode", ""),
            addr.get("countryCode", ""),
        ]
        return ", ".join(p for p in lines if p)
  • server.py:154-164 (registration)
    Main server registration: imports hmrc_vat module and calls hmrc_vat.register_tools(mcp) to register the vat_validate tool.
    import companies_house, charity, disqualified, land_registry, gazette, hmrc_vat, search_fetch
    import prompts as prompts_module
    from fastmcp.server.transforms import PromptsAsTools
    
    companies_house.register_tools(mcp)
    charity.register_tools(mcp)
    disqualified.register_tools(mcp)
    land_registry.register_tools(mcp)
    gazette.register_tools(mcp)
    hmrc_vat.register_tools(mcp)
    search_fetch.register_tools(mcp)
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already indicate read-only (readOnlyHint=true, destructiveHint=false, idempotentHint=true). The description adds behavioral context: returns trading name and address, and notes discrepancy signal, which is useful beyond annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three sentences, front-loaded with purpose, no unnecessary words. Efficiently structured with a clear first sentence describing the core function.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Despite low complexity, the description covers purpose, input formats, and return information. With output schema available, explanation of return values is sufficient.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% with one parameter. Description adds value by listing accepted formats and noting automatic normalization, which agents may not infer from schema alone.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states 'Validate a UK VAT number against the HMRC register', specifying both the action and resource. It distinguishes from sibling tools like company_profile by focusing on VAT validation.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implicitly tells when to use (for VAT validation) but does not explicitly state when not to use or provide alternatives. It is clear enough for a dedicated tool.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/paulieb89/uk-due-diligence-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server