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 — 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.
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.
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.
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: 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.
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:
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 | "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.
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 | — | 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/...) 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.
| 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 |