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 — Register via MCP (zero-click):

Call create_account with no auth header. The tool returns a vb_live_... API key

immediately and creates a free VATBuild account. Copy the key and add it to your

Gemini config.

Option B — Web dashboard:

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 — Existing account, forgotten key:

Call authenticate_account with your email address. A fresh key is returned and a

sign-in link is sent to your inbox.


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": {
      "httpUrl": "https://vatbuild.com/mcp",
      "headers": {
        "Authorization": "Bearer vb_live_YOUR_API_KEY_HERE"
      }
    }
  }
}

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 (or the check_line_item tool with no key at all) is

sufficient — check_line_item is public.

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: Google's active Python SDK is now google-genai

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

> maintenance mode but continues to function. For new integrations, prefer

> google-genai and its genai.Client + tool_config pattern — see the

> Google Gen AI SDK migration guide.

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

The example below classifies a single invoice line item using check_line_item

(no API key needed — the tool is public):


import google.generativeai as genai
import json

genai.configure(api_key="YOUR_GEMINI_API_KEY")

# Describe the check_line_item tool as a Gemini function declaration.
check_line_item_tool = genai.protos.Tool(
    function_declarations=[
        genai.protos.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=genai.protos.Schema(
                type=genai.protos.Type.OBJECT,
                properties={
                    "context": genai.protos.Schema(
                        type=genai.protos.Type.OBJECT,
                        description="Project-level VAT context for classification.",
                        properties={
                            "projectType":   genai.protos.Schema(type=genai.protos.Type.STRING, description="e.g. 'new build', 'conversion'"),
                            "newDwelling":   genai.protos.Schema(type=genai.protos.Type.STRING, description="'yes' or 'no'"),
                            "buildingType":  genai.protos.Schema(type=genai.protos.Type.STRING, description="e.g. 'detached house', 'barn'"),
                            "claimantRoute": genai.protos.Schema(type=genai.protos.Type.STRING, description="e.g. 'self_build_431nb', 'self_build_431c'"),
                        },
                        required=["projectType", "newDwelling", "buildingType", "claimantRoute"],
                    ),
                    "item": genai.protos.Schema(
                        type=genai.protos.Type.OBJECT,
                        description="Invoice line item to classify.",
                        properties={
                            # Required fields
                            "lineText":          genai.protos.Schema(type=genai.protos.Type.STRING, description="Invoice line description"),
                            "netAmount":         genai.protos.Schema(type=genai.protos.Type.STRING, description="Net amount, e.g. '4200.00'"),
                            "vatCharged":        genai.protos.Schema(type=genai.protos.Type.STRING, description="VAT amount charged, e.g. '840.00'"),
                            # Optional / nullable fields — pass null when unknown
                            "supplyType":        genai.protos.Schema(type=genai.protos.Type.STRING, description="One of: materials, labour, subcontractor, supply_and_install, installation_service, professional_services. Null = auto-detect."),
                            "identifierName":    genai.protos.Schema(type=genai.protos.Type.STRING, description="VATBuild taxonomy category name, if known. Null = auto-detect."),
                            "vatComplexTypeHint": genai.protos.Schema(type=genai.protos.Type.STRING, description="Complex-VAT type hint (e.g. esm_install, fitted_furniture, disability_adaptation). Null = auto-detect."),
                            "esmStatus":         genai.protos.Schema(type=genai.protos.Type.STRING, description="One of: qualifying_esm, ancillary_to_esm. Null = not an ESM item."),
                            "relation":          genai.protos.Schema(type=genai.protos.Type.STRING, description="AI relation label. 'Likely not claimable' soft-excludes the item."),
                            "confirmedAnswer":   genai.protos.Schema(type=genai.protos.Type.STRING, description="Pre-supply the answer to a complex-VAT question. Null = question still open."),
                            "esmInvoiceCtx":     genai.protos.Schema(type=genai.protos.Type.BOOLEAN, description="True when the invoice contains at least one ESM install line."),
                            "saiInvoiceCtx":     genai.protos.Schema(type=genai.protos.Type.BOOLEAN, description="True when the invoice contains at least one non-ESM install line on a 431nb/431c project."),
                        },
                        required=["lineText", "netAmount", "vatCharged"],
                    ),
                },
                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())


model = genai.GenerativeModel(
    model_name="gemini-2.0-flash",
    tools=[check_line_item_tool],
)

chat = model.start_chat(enable_automatic_function_calling=False)

response = chat.send_message(
    "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"
)

# If Gemini chose to call the tool, execute it.
for part in response.parts:
    if 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": "{\"outcome\":\"supplier_correction\",\"reclaimAmount\":\"840.00\",\"rule\":{\"label\":\"Overcharged VAT — New Build Services\",\"noticeRef\":\"Notice 708 s3\",\"explanation\":\"Contractor services on a new-build dwelling should be zero-rated (0%). VAT was charged at 20%. Request a corrected invoice.\"}}"
    }]
  }
}

> 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
  Input:  { "firstName": "Jane", "email": "jane@example.com" }
  Output: { "apiKey": "vb_live_...", "nextStep": "set_up_project" }
  → Store apiKey; use it as the Bearer token for all subsequent calls.

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:
    GET https://vatbuild.com/api/v1/jobs/{jobId}
    Authorization: Bearer vb_live_...
  The jobId is returned by submit_invoice_data's underlying job queue and is not
  currently surfaced by the MCP tool directly — use list_invoices polling instead.

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: { "claimRoute": "zero_at_source", "reclaimAmount": "0.00" }

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: { "claimRoute": "supplier_correction", "crossItemContextRequired": false }
  → If 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"Was this fitted kitchen both supplied and installed by the same contractor?"
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:readGET /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/...) are always live regardless of

> this flag. Contact the VATBuild operator if the OAuth endpoints return 404.

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