Tax MCP
The Model Context Protocol server behind the free tax tools at ai2fin.com/tools/compare β so assistants (Claude, ChatGPT, agents) can call them directly.
Stateless and no-auth. It computes over public rate data only and stores nothing. (This is separate from the authenticated app MCP at app.ai2fin.com/mcp, which works on your real financial data behind OAuth.)
| Tool | What it does |
|---|
tax_rate_lookup | A country's GST/VAT, income and company tax rates; pass region for US state or Canadian provincial sales tax |
tax_rate_as_of | The GST/VAT rate that applied on a given date β for back-dated invoices and amended returns |
compute_gst_vat | Add or remove a country's GST/VAT (or other national consumption tax, e.g. Japan's consumption tax, Malaysia's SST) on an amount; region for US/Canadian sales tax |
compare_countries | Compare headline GST/VAT, income and company tax across countries β ai2fin.com/tools/compare as a callable; pass an optional income (read in each country's own currency, not converted) to add each country's income_tax_estimate result |
income_tax_estimate | Income tax and take-home over the engine's effective-dated brackets for 57 countries, with region for US states, Canadian provinces and Scotland (Finland with municipality, church tax and the tax-card rate) |
company_tax_estimate | Company tax at the headline rate for 139 countries (28 verified against the national authority, the rest marked indicative) |
cgt_estimate | Capital-gains tax for resident individuals in 71 countries β a figure where the rule can be computed, the rule text where it cannot; FI with the deemed acquisition cost, 30%/34% and the β¬1,000 rule |
student_loan_repayment | HELP/HECS compulsory repayment on the 2025-26 marginal system, effective-dated thresholds (AU) |
superannuation_estimate | Super Guarantee, concessional cap, contributions tax and Division 293 surcharge (AU) |
tax_facts | Sourced facts and origin stories β when and why each GST/VAT began, what it replaced, and true tax stories, each linked to its source; optional topic filter |
The GST/VAT tools' answers may also include one sourced background fact (relatedFact, plus one Background: line) about that country's tax, picked deterministically per country and UTC day; the figures never depend on it.
It also offers four ready-made prompts (prompts/list): GST/VAT on an amount, take-home pay, compare countries, and the story behind a tax.
Coverage, generated from the engine: standard national GST/VAT rates for 193 countries; state and provincial sales tax for the US and Canada; income tax for 57 countries; company tax for 139; capital gains for 71. A country outside a tool's coverage gets "not covered", never a guess, and every result carries a "general information, not tax advice" note and cites its OWN source and date β an income answer cites the income-tax authority, not the GST/VAT row. Unverified figures say "not verified β confirm with" the authority.
Data = single source
data/rates.json, data/coverage.json (what each capability covers, with its citations, plus US/Canadian sales tax), src/taxEngine.generated.js (the engine's income, company and CGT calculators, bundled verbatim) and data/finland.json (Finland's scales, contribution rates and all 308 municipal and parish rates β the tool answers for the 292 on the mainland; Γ
land is out of scope) are produced by the website's gen:tax pipeline β the same source behind the calculator and comparison pages β so the MCP, the widgets and the pages never disagree. Don't hand-edit them.
data/facts.json is generated from the website's sourced facts corpus (Website Front/content/taxFacts.ts) by node scripts/sync-facts.mjs; the website's test suite fails when the two drift.
Run / deploy
cd tax-mcp
npm install
npm run dev
npm run deploy
npm run bundle
Full deploy steps (dashboard copy-paste or CLI): DEPLOY.md.
Abuse protection is via the Cloudflare dashboard (rate limiting + WAF) β not auth.
Scheduled health watch
The freshness guards on the tax-data pipeline used to be GitHub Actions, and Actions have been dead in the org since a billing lock in July 2026 β the last successful rate-watch run was 81 days old before anything noticed. So the Worker now watches the pipeline itself: a Cloudflare cron ([triggers] crons = ["17 3 * * *"] in wrangler.toml, daily at 03:17 UTC) runs scheduled() in src/index.ts, which drives the pure hops in src/watch.ts:
| Hop | Question | Fails when |
|---|
cron | Did rate-watch in embracingearth-space/ai2-tax-plugins run and succeed in the last 9 days? (GitHub API; dead-man's switch) | No success inside the window β or never |
mirror | Does the public mirror embracingearth-space/ai2fin-tax-mcp serve the dataset this Worker was built from? (same fingerprint as /health) | The fingerprints differ (row counts reported) |
live | Does https://taxmcp.ai2fin.com/health publish this build's fingerprint? | The deployed Worker is a different build |
citations | Is any verified citation in the bundled rates.json older than STALE_AFTER_DAYS (365)? | One or more are |
Every hop lands in exactly one of ok, fail or unknown. unknown ("could not check" β timeout, rate limit, DNS) is never folded into healthy: the run's status is then unknown, needsHuman is true, and the alert says so. A guard that cannot see must say so β that silence is the bug this exists to end.
Each run stores its result in the WATCH KV namespace and, when needsHuman is true or the status flipped since the last run (including the recovery), POSTs a Slack-compatible { "text": β¦ } to ALERT_WEBHOOK_URL if it is set. It always logs. scheduled() never throws.
Read it: GET /health/watch returns the last stored result (public; no secrets β hop states, row counts, fingerprint, needsHuman), or { "status": "never-run" } while KV is empty. The existing /health is unchanged.
One-time setup (the dashboard-attached build deploys the cron trigger and the KV binding from wrangler.toml, so after this nothing needs clicking):
cd tax-mcp
npx wrangler kv namespace create WATCH
npx wrangler secret put ALERT_WEBHOOK_URL
npx wrangler secret put GITHUB_TOKEN
Vars: WATCH_LIVE_URL (optional) overrides the live Worker URL the live hop fetches. To fire a run by hand: npx wrangler dev --test-scheduled then curl 'http://localhost:8787/__scheduled?cron=17+3+*+*+*'.
The watch does not fix anything: a mirror failure still means node scripts/sync-mirror.mjs --push by hand from this repo, and a cron failure means running rate-watch by hand in the engine repo until Actions are back. The laptop version of the same checks is npm run health (scripts/pipeline-health.mjs), which also compares against the checked-out tree rather than the deployed build.
Connect
Add the deployed URL as a custom MCP connector in Claude (Settings β Connectors) or ChatGPT (developer mode). A GET to the root returns server info; the MCP handshake is POST JSON-RPC (initialize β tools/list β tools/call). It pairs with the live comparison tool at ai2fin.com/tools/compare.
Quick check
curl -s https://taxmcp.ai2fin.com -X POST -H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"compute_gst_vat","arguments":{"country":"AU","amount":100,"mode":"add"}}}'