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
2. Gemini CLI
8. Rate limits
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).
create_account — VATBuild sends a confirmation email. **No API keyis returned and no account is created until the user clicks the link.** Once confirmed,
call authenticate_account to begin the device-code flow.
authenticate_account directly with your email address.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.
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.
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).
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:
projects:readprojects:writeline_items:writejobs:readinvoices:writeGoogle 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 Tools → Connect tools → Add 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 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.
> 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.
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:
vatComplexType | Question 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.
When creating an API key, grant only the scopes the agent needs.
| Scope | MCP tools covered | REST endpoints |
|---|---|---|
| *(none)* | check_line_item, create_account, authenticate_account | GET /api/v1/check-line-item |
projects:read | check_account_status, list_projects, list_line_items, list_invoices, assess_project_eligibility | GET /api/v1/projects, GET /api/v1/line-items |
projects:write | set_up_project | POST /api/v1/projects |
line_items:write | route_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:write | submit_invoice_data (upload-only) | POST /api/v1/invoices, POST /api/v1/invoices/lock |
Recommended scope sets:
| Use case | Scopes |
|---|---|
| Read-only analysis (classify only) | *(none required)* |
| Read-only project access | projects:read |
| Full end-to-end workflow | All five scopes |
| Request type | Limit |
|---|---|
| All requests per API key or IP | 60 per minute |
Write tools (route_line_item, answer_vat_complex_question, reclassify_companion_items, submit_invoice_data) | 10 per minute |
create_account / authenticate_account | 3 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.
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 registration — POST /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 exchange — POST /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.
| Symptom | Cause | Fix |
|---|---|---|
Tool calls return registration_required | Bearer key missing or wrong header name | Confirm the header is Authorization: Bearer vb_live_... (not X-Api-Key) |
| HTTP 429 on tool calls | Rate limit exceeded | Wait for Retry-After seconds; reduce polling frequency |
| HTTP 403 on a specific tool | API key lacks the required scope | Regenerate the key with the missing scope ticked |
submit_invoice_data returns ASSESSMENT_NOT_CONFIRMED | Profile wizard not completed | User must visit projectUrl in the VATBuild web app and confirm their profile |
check_line_item always returns pending_complex_answer | vatComplexTypeHint matches a confirm type and confirmedAnswer is null | Pass confirmedAnswer with the user's answer, or omit vatComplexTypeHint and let the engine auto-detect |
| Gemini CLI doesn't list VATBuild tools | Settings file syntax error | Validate ~/.gemini/settings.json with a JSON linter; check the key is mcpServers, not mcp_servers |