Using VATBuild MCP with Google Gemini

VATBuild's MCP server is directly compatible with Gemini CLI, Google AI Studio,

Vertex AI Agent Builder, and the Gemini API. This guide covers all four entry

points, a recommended agent workflow, and the OAuth alternative to long-lived API

keys.

Endpoint: POST https://vatbuild.com/mcp

Protocol: Streamable HTTP, JSON-RPC 2.0, stateless

Auth: Authorization: Bearer <api-key> — see Getting an API key


Contents

1. Getting an API key

2. Gemini CLI

3. Google AI Studio

4. Vertex AI Agent Builder

5. Gemini API — Python

6. Canonical agent workflow

7. Scopes reference

8. Rate limits

9. OAuth / PKCE alternative


Getting an API key

Option A — OAuth 2.1 with PKCE (recommended):

VATBuild supports OAuth 2.1 with PKCE. Agents and connectors that implement RFC 9728

auto-discover the auth endpoints and drive the PKCE flow — no static key required.

See OAuth / PKCE alternative for discovery URLs, the full

parameter reference, and the authorization server metadata.

Option B — Web dashboard (bearer key):

Log in at vatbuild.com, navigate to Settings → API Keys,

and create a key. Tick the scopes you need (see Scopes reference).

Keys are shown only once — copy and store them securely.

Option C — Device-code flow via MCP (new or existing account):

create_account and authenticate_account are public tools (no key required).

is returned and no account is created until the user clicks the link.** Once confirmed,

call authenticate_account to begin the device-code flow.

Both paths complete the same way: authenticate_account returns a user_code and

request_id. Display the user_code to the user, ask them to check their email and

click "Approve API access", then poll poll_authentication with the request_id

every 3–5 seconds. Once approved, the tool returns { status: "ready", key_retrieval_url }

give the user key_retrieval_url and ask them to open it in their browser. The page

shows their API key and the ready-to-paste config snippet for ~/.gemini/settings.json.

Never display the key in the chat and never pass it as a tool argument. After adding

the key the user must start a new gemini session.


Gemini CLI

The Gemini CLI reads MCP server configuration from ~/.gemini/settings.json. Add a

mcpServers block with VATBuild's Streamable HTTP endpoint and your Bearer key:


{
  "mcpServers": {
    "vatbuild": {
      "url": "https://vatbuild.com/mcp",
      "type": "http",
      "headers": {
        "Authorization": "Bearer vb_live_YOUR_API_KEY_HERE"
      }
    }
  }
}

> Gemini CLI version note: The url + type: "http" format is current as of

> Gemini CLI v0.52.0 (released after PR #13762, Nov 2025). Earlier versions used

> httpUrl instead of url + type: "http". Both keys are accepted for backward

> compatibility, but url + type: "http" is the format gemini mcp add --transport http

> now writes and is preferred for new configurations.

Save the file and start a new gemini session. The VATBuild tools are available

immediately — try:


@vatbuild check_line_item: classify "Supply and fit underfloor heating" for a new-build dwelling

The Gemini CLI sends requests server-to-server (no Origin header), which VATBuild

allows by design. No CORS or browser-origin configuration is needed.

Minimal setup (read-only, no project data)

If you only want to classify line items without creating projects or uploading

invoices, a key with no scopes is sufficient — check_line_item is public.

check_line_item is a public tool and requires no key — the Gemini CLI can call it

immediately after initialize without any credentials. A key is needed only for the

project tools (routing, invoices, reports).

Full setup (end-to-end workflow)

For the full workflow — account creation, project setup, invoice submission, line-item

routing — the key needs all five scopes. When creating the key in the dashboard, tick:


Google AI Studio

Google AI Studio supports MCP connections through the Tools panel.

1. Open aistudio.google.com and start or open a prompt.

2. In the right-hand panel, click ToolsConnect toolsAdd MCP server.

3. Enter the server URL: https://vatbuild.com/mcp

4. In the Headers section, add one header:

- Key: Authorization

- Value: Bearer vb_live_YOUR_API_KEY_HERE

5. Click Connect. AI Studio discovers the tools automatically.

Once connected, address VATBuild tools by name in your prompts. For example:


Use the set_up_project tool to create a VAT431NB project for a new-build detached
house in Bristol. Then use assess_project_eligibility to check eligibility.

> Note: AI Studio sessions are ephemeral — the MCP connection is not saved

> permanently. Re-add the server each session, or use Gemini CLI for persistent

> configuration.


Vertex AI Agent Builder

Vertex AI Agent Builder lets you register external MCP servers as tool extensions

in a Reasoning Engine or Agent configuration. Below is an example YAML agent

configuration that wires in VATBuild:


displayName: "VATBuild VAT Agent"
defaultLanguageCode: "en"
timeZone: "Europe/London"

tools:
  - displayName: "VATBuild MCP"
    mcpTool:
      serverUrl: "https://vatbuild.com/mcp"
      httpHeaders:
        - key: "Authorization"
          value: "Bearer vb_live_YOUR_API_KEY_HERE"
      # Optional: restrict to specific tools for least-privilege agents
      # toolFilter:
      #   allowedTools: ["check_line_item", "list_projects", "list_line_items"]

Apply via the gcloud CLI:


gcloud agent-builder agents create \
  --project=YOUR_PROJECT_ID \
  --location=europe-west2 \
  --display-name="VATBuild VAT Agent" \
  --config-file=vatbuild-agent.yaml

> Secret management: Store the Bearer key in

> Secret Manager and reference it with

> secretManagerVersionName instead of a literal string in httpHeaders.value. See

> the Vertex AI Agent Builder documentation for the full secret-reference syntax.


Gemini API — Python

> SDK note: The current Google Python SDK is google-genai

> (from google import genai). The older google-generativeai package is in

> maintenance mode — it still functions but new integrations should use google-genai.

> See the Google Gen AI SDK migration guide

> if upgrading an existing integration. The example below uses google-genai.

Use the google-genai SDK to call VATBuild MCP tools as function calls.

The example below classifies a single invoice line item using check_line_item.

This example calls POST /mcp directly as a stateless HTTP request — it does not

send an MCP initialize handshake. Gemini CLI and other MCP session clients can also

call check_line_item with no key; a key is needed only for the project tools (see

Minimal setup above).


# pip install google-genai
from google import genai
from google.genai import types
import json

client = genai.Client(api_key="YOUR_GEMINI_API_KEY")

# Describe the check_line_item tool as a Gemini function declaration.
check_line_item_tool = types.Tool(
    function_declarations=[
        types.FunctionDeclaration(
            name="check_line_item",
            description=(
                "Classify a single UK construction invoice line item against HMRC "
                "Notice 708 VAT rules. Returns claimRoute, reclaimAmount, and the "
                "HMRC rule that fired. Public — no API key needed."
            ),
            parameters=types.Schema(
                type=types.Type.OBJECT,
                properties={
                    "context": types.Schema(
                        type=types.Type.OBJECT,
                        description="Project-level VAT context for classification.",
                        properties={
                            "projectType":   types.Schema(type=types.Type.STRING, description="e.g. 'new build', 'conversion'"),
                            "newDwelling":   types.Schema(type=types.Type.STRING, description="'yes' or 'no'"),
                            "buildingType":  types.Schema(type=types.Type.STRING, description="e.g. 'detached house', 'barn'"),
                            "claimantRoute": types.Schema(type=types.Type.STRING, description="e.g. 'self_build_431nb', 'self_build_431c'"),
                        },
                        required=["projectType", "newDwelling", "buildingType", "claimantRoute"],
                    ),
                    "item": types.Schema(
                        type=types.Type.OBJECT,
                        description="Invoice line item to classify.",
                        properties={
                            # Required, non-nullable
                            "lineText":           types.Schema(type=types.Type.STRING, description="Invoice line description"),
                            "netAmount":          types.Schema(type=types.Type.STRING, description="Net amount, e.g. '4200.00'"),
                            "vatCharged":         types.Schema(type=types.Type.STRING, description="VAT amount charged, e.g. '840.00'"),
                            # Required but nullable — the MCP schema is strict; pass null when unknown
                            "supplyType":         types.Schema(type=types.Type.STRING, nullable=True, description="One of: materials, labour, subcontractor, supply_and_install, installation_service, professional_services. Null = auto-detect."),
                            "identifierName":     types.Schema(type=types.Type.STRING, nullable=True, description="VATBuild taxonomy category name, if known. Null = auto-detect."),
                            "vatComplexTypeHint": types.Schema(type=types.Type.STRING, nullable=True, description="Complex-VAT type hint (e.g. esm_install, fitted_furniture, disability_adaptation). Null = auto-detect."),
                            "esmStatus":          types.Schema(type=types.Type.STRING, nullable=True, description="One of: qualifying_esm, ancillary_to_esm. Null = not an ESM item."),
                            "relation":           types.Schema(type=types.Type.STRING, nullable=True, description="AI relation label. 'Likely not claimable' soft-excludes the item."),
                            "confirmedAnswer":    types.Schema(type=types.Type.STRING, nullable=True, description="Pre-supply the answer to a complex-VAT question. Null = question still open."),
                            "esmInvoiceCtx":      types.Schema(type=types.Type.BOOLEAN, description="True when the invoice contains at least one ESM install line."),
                            "saiInvoiceCtx":      types.Schema(type=types.Type.BOOLEAN, description="True when the invoice contains at least one non-ESM install line on a 431nb/431c project."),
                        },
                        required=["lineText", "netAmount", "vatCharged", "supplyType", "identifierName", "vatComplexTypeHint", "esmStatus", "relation", "confirmedAnswer", "esmInvoiceCtx", "saiInvoiceCtx"],
                    ),
                },
                required=["context", "item"],
            ),
        )
    ]
)


def call_vatbuild_mcp(tool_name: str, args: dict) -> dict:
    """Send a JSON-RPC 2.0 call to the VATBuild MCP endpoint."""
    import urllib.request
    payload = json.dumps({
        "jsonrpc": "2.0",
        "id": 1,
        "method": "tools/call",
        "params": {"name": tool_name, "arguments": args},
    }).encode()
    req = urllib.request.Request(
        "https://vatbuild.com/mcp",
        data=payload,
        headers={
            "Content-Type": "application/json",
            "Accept": "application/json, text/event-stream",
            # Add Bearer token for auth-gated tools:
            # "Authorization": "Bearer vb_live_YOUR_API_KEY_HERE",
        },
        method="POST",
    )
    with urllib.request.urlopen(req) as resp:
        return json.loads(resp.read())


response = client.models.generate_content(
    model="gemini-2.0-flash",
    contents=(
        "Classify this line item under VAT431NB rules for a new-build detached house: "
        "Supply and installation of underfloor heating, net £4,200.00, VAT charged £840.00"
    ),
    config=types.GenerateContentConfig(
        tools=[check_line_item_tool],
        automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
    ),
)

# If Gemini chose to call the tool, execute it.
for part in response.candidates[0].content.parts:
    if part.function_call:
        fn = part.function_call
        result = call_vatbuild_mcp(fn.name, dict(fn.args))
        print(json.dumps(result, indent=2))

Expected output (abbreviated):


{
  "result": {
    "content": [{
      "type": "text",
      "text": "{\"ok\":true,\"data\":{\"outcome\":{\"claimRoute\":\"supplier_correction\",\"claimantReclaimBasis\":\"0\",\"supplierInvoiceRate\":\"0\",\"reviewStatus\":\"approved\",\"noActionNote\":null,\"firedRuleId\":\"rule_3236aaa605cdb515\"},\"reclaimAmount\":\"840.00\",\"firedRuleId\":\"rule_3236aaa605cdb515\",\"rule\":{\"label\":\"New build service — supplier overcharged VAT\",\"noticeRef\":\"Notice 708 s3.3\",\"explanation\":\"Qualifying construction service should be zero-rated on a new build; supplier charged above 0% — supplier correction required.\"}}}"
    }]
  }
}

> For auth-gated tools (set_up_project, list_line_items, etc.), add the

> Authorization: Bearer vb_live_... header to call_vatbuild_mcp and declare

> those tools as additional FunctionDeclaration entries.


Canonical agent workflow

The recommended end-to-end flow for a new VATBuild user via a Gemini agent:


Step 1 — create_account → authenticate_account → poll_authentication
  create_account input:  { "firstName": "Jane", "email": "jane@example.com" }
  Output: { "status": "verification_email_sent", "verified": false, "nextStep": "check_email" }
  → No API key returned. Tell the user a confirmation email has been sent.
    The account only activates when they click the link.

  Once confirmed, call authenticate_account:
  Input:  { "email": "jane@example.com" }
  Output: { "status": "magic_link_sent", "user_code": "BCDF-7823", "request_id": "..." }
  → Display user_code to the user. Ask them to check their inbox and click
    "Approve API access" — the approval page shows the same code for verification.

  Poll poll_authentication every 3–5 s until status is "ready":
  Input:  { "request_id": "..." }
  Output: { "status": "ready", "key_retrieval_url": "https://vatbuild.com/..." }
  → Give the user key_retrieval_url and ask them to open it in their browser.
    The page shows the API key and the ready-to-paste config snippet.
    Do NOT display the key in the chat. After adding the key the user must
    start a new gemini session. Never pass the key as a tool argument.

Step 2 — set_up_project
  Input:  { "claimantRoute": "self_build_431nb", "buildingType": "detached house",
            "address": "12 Elm Lane, Bristol" }
  Output: { "projectId": "uuid", "profileConfirmed": false, "projectUrl": "..." }
  → Surface projectUrl so the user can confirm their profile in the VATBuild web app.
    Profile confirmation is required before invoices can be submitted.

Step 3 — assess_project_eligibility (optional, recommended)
  Input:  { "projectId": "uuid" }
  Output: { "eligibilityStatus": "likely_eligible", "keyFindings": [...] }
  → Surface any "warning" or "blocker" findings to the user before they proceed.

Step 4 — submit_invoice_data (once profile is confirmed in the web app)
  Input:  { "projectId": "uuid", "imageData": "<base64>", "imageMimeType": "image/jpeg",
            "supplierName": "ABC Builders Ltd", "invoiceRef": "INV-042" }
  Output: { "documentId": "uuid", "status": "extracting" }
  → VATBuild queues AI extraction in the background.
  → imageMimeType accepts "image/jpeg", "image/png", "image/webp", or "application/pdf".
  → For PDFs, pass the raw PDF bytes as base64 — do not render pages to images first.

Step 5 — Poll list_invoices until extractionStatus = "complete"
  Input:  { "projectId": "uuid" }
  → Check each item's extractionStatus. Repeat every 5–10 seconds.
  → Once complete, proceed to step 6.

  Alternatively, poll the REST job endpoint directly:
    GET https://vatbuild.com/api/v1/jobs/{jobId}
    Authorization: Bearer vb_live_...
  Requires the `jobs:read` scope. Note: the `jobId` from the underlying job queue is
  not currently returned by the `submit_invoice_data` MCP tool — use the
  `list_invoices` polling approach above when working entirely through MCP.

Step 6 — list_line_items
  Input:  { "projectId": "uuid", "status": "pending" }
  Output: array of line items; each has claimRoute and vatComplexType

Step 7a — route_line_item (for items WITHOUT a pending_complex_answer)
  Use this to persist a VAT routing decision for any line item — including
  re-classifying items or manually overriding an AI decision.
  Input:  { "projectId": "uuid", "lineItemId": "uuid",
            "item": { "lineText": "...", "netAmount": "...", "vatCharged": "...",
                      "supplyType": "labour" } }
  Output: { "data.outcome.claimRoute": "zero_at_source", "data.reclaimAmount": "0" }

Step 7b — answer_vat_complex_question (for items WITH claimRoute: "pending_complex_answer")
  Use this — not route_line_item — when an item requires a specific complex-VAT
  confirmation. It resolves the vatComplexType question and persists the decision.
  Input:  { "projectId": "uuid", "lineItemId": "uuid", "confirmedAnswer": true }
  Output: { "data.outcome.claimRoute": "supplier_correction", "data.crossItemContextRequired": false }
  → If data.crossItemContextRequired is true, call reclassify_companion_items next.

Step 8 — reclassify_companion_items (when crossItemContextRequired = true)
  Input:  { "projectId": "uuid", "documentId": "uuid" }
  → Re-routes sibling items on the same invoice using updated context.

Handling pending_complex_answer items:

When list_line_items returns an item with claimRoute: "pending_complex_answer",

read vatComplexType to know which question to ask the user. Common cases:

|---|---|

vatComplexTypeQuestion to ask
vague_description"What does this line actually represent — labour, materials, a professional fee, or something else?"
esm_install"Is this the supply and installation of an energy-saving material (insulation, solar PV, heat pump) by the same contractor?"
disability_adaptation"Is this work being carried out for a disabled or chronically sick person under the HMRC disability adaptation relief?"
fitted_furniture"Is this either (a) a fitted kitchen that was both supplied and installed by the same contractor, or (b) a basic fitted wardrobe consisting only of a wall-to-wall unit with a hanging rail or shelf for clothes and shelves — with no drawers, shoe racks, or decorative panelling?"
soft_landscaping"Is this landscaping required by a planning condition?"

Do not guess the answer. Ask the user, then call answer_vat_complex_question.


Scopes reference

When creating an API key, grant only the scopes the agent needs.

|---|---|---|

ScopeMCP tools coveredREST endpoints
*(none)*check_line_item, create_account, authenticate_accountGET /api/v1/check-line-item
projects:readcheck_account_status, list_projects, list_line_items, list_invoices, assess_project_eligibilityGET /api/v1/projects, GET /api/v1/line-items
projects:writeset_up_projectPOST /api/v1/projects
line_items:writeroute_line_item, answer_vat_complex_question, reclassify_companion_items, submit_invoice_data
jobs:read*(REST polling only — no MCP tool currently uses this scope; MCP clients should use list_invoices for extraction status)*GET /api/v1/jobs/{jobId}
invoices:writesubmit_invoice_data (upload-only)POST /api/v1/invoices, POST /api/v1/invoices/lock

Recommended scope sets:

|---|---|

Use caseScopes
Read-only analysis (classify only)*(none required)*
Read-only project accessprojects:read
Full end-to-end workflowAll five scopes

Rate limits

|---|---|

Request typeLimit
All requests per API key or IP60 per minute
Write tools (route_line_item, answer_vat_complex_question, reclassify_companion_items, submit_invoice_data)10 per minute
create_account / authenticate_account3 per hour per IP

Responses that exceed the limit return HTTP 429 with a Retry-After header.

Implement exponential back-off when polling list_invoices after submit_invoice_data.


OAuth / PKCE alternative

Instead of embedding a long-lived API key, Gemini agents can authenticate using

VATBuild's standard OAuth 2.1 authorization server with PKCE. This is preferable for

multi-tenant agents where each end-user holds their own VATBuild account.

Discovery:


GET https://vatbuild.com/.well-known/oauth-authorization-server
GET https://vatbuild.com/.well-known/oauth-protected-resource

Both endpoints are always live and return RFC 8414 / RFC 9728 metadata. The

authorization server metadata includes:


{
  "issuer": "https://vatbuild.com",
  "authorization_endpoint": "https://vatbuild.com/oauth/authorize",
  "token_endpoint": "https://vatbuild.com/oauth/token",
  "registration_endpoint": "https://vatbuild.com/oauth/register",
  "scopes_supported": ["projects:read", "projects:write", "line_items:write", "jobs:read", "invoices:write"],
  "code_challenge_methods_supported": ["S256"],
  "pkce_required": true,
  "grant_types_supported": ["authorization_code", "refresh_token"]
}

PKCE flow:

1. Dynamic registrationPOST /oauth/register with your client metadata

(redirect_uris, client_name). Receive client_id.

2. Authorization — Redirect the user to /oauth/authorize?client_id=...&scope=...&code_challenge=...&code_challenge_method=S256.

3. Token exchangePOST /oauth/token with the auth code and code_verifier.

Receive access_token and refresh_token.

4. MCP calls — Use Authorization: Bearer <access_token> on all /mcp requests.

> Prerequisite: The VATBuild deployment must have MCP_OAUTH_ENABLED=true for

> the /oauth/authorize, /oauth/token, and /oauth/register endpoints to be

> active. The discovery endpoints (/.well-known/oauth-authorization-server and

> /.well-known/oauth-protected-resource) are always live and confirmed reachable

> on production regardless of this flag.

For agents where a single service account is appropriate (rather than per-user OAuth),

the API key approach is simpler and recommended.


Troubleshooting

|---|---|---|

SymptomCauseFix
Tool calls return registration_requiredBearer key missing or wrong header nameConfirm the header is Authorization: Bearer vb_live_... (not X-Api-Key)
HTTP 429 on tool callsRate limit exceededWait for Retry-After seconds; reduce polling frequency
HTTP 403 on a specific toolAPI key lacks the required scopeRegenerate the key with the missing scope ticked
submit_invoice_data returns ASSESSMENT_NOT_CONFIRMEDProfile wizard not completedUser must visit projectUrl in the VATBuild web app and confirm their profile
check_line_item always returns pending_complex_answervatComplexTypeHint matches a confirm type and confirmedAnswer is nullPass confirmedAnswer with the user's answer, or omit vatComplexTypeHint and let the engine auto-detect
Gemini CLI doesn't list VATBuild toolsSettings file syntax errorValidate ~/.gemini/settings.json with a JSON linter; check the key is mcpServers, not mcp_servers