Dutch Tax Income Calculator MCP
README.md
# dutch-tax-income-calculator-mcp
A remote [MCP](https://modelcontextprotocol.io) server, deployed on Cloudflare Workers, that exposes the
[`dutch-tax-income-calculator`](https://www.npmjs.com/package/dutch-tax-income-calculator) npm package as MCP
tools and a resource. This server never reimplements Dutch tax logic or copies tax tables — every calculation is
delegated directly to the package.
> **Indicative only — not tax advice.** Every tool result includes this disclaimer. Confirm with a qualified
> Dutch tax advisor or the Belastingdienst before acting on any number this server returns.
## What it exposes
### Tools
- **`calculate_net_salary`** — gross → net, wraps the package's `SalaryPaycheck` class.
- **`calculate_gross_from_net`** — net → gross, wraps the package's `netToGross` solver. Because every internal
amount is rounded to 2 decimals, more than one gross can round to the same net; when that happens the tool
returns the package's own plateau bounds (`grossLow` / `grossHigh`) verbatim instead of guessing. If no gross
produces the target net, the package's "no solution" error message is returned verbatim, unmodified.
- **`compare_scenarios`** — runs 2 to 5 `calculate_net_salary`-shaped scenarios side by side and returns a
markdown comparison table alongside the per-scenario results.
Every tool result returns: `normalizedInput`, `result`, `breakdown[]` (payroll tax, social security, general
credit, labour credit), `assumptions[]`, and a `permalink` to the equivalent scenario on
[thetax.nl](https://thetax.nl/).
### Resource
- **`tax://brackets/{year}`** — reads `data.json` from the package directly: payroll tax, social security,
general credit and labour credit brackets, plus 30% ruling thresholds, for the requested year.
`year` is validated against the years actually present in the package's `data.json` on every tool call and
resource read. An unsupported year always returns a clear, explicit error — never a silent fallback to the
nearest year.
## Stack
- [MCP TypeScript SDK v2](https://ts.sdk.modelcontextprotocol.io/v2/) (`@modelcontextprotocol/server`)
- [`createMcpHandler`](https://developers.cloudflare.com/agents/model-context-protocol/mcp-handler-api/) from
`agents/mcp/server` (Cloudflare Agents SDK) as the entire Worker `fetch` handler — stateless, no Durable
Objects, no `McpAgent`. `createMcpHandler` is given a **factory function** (`createServer`) that builds a
fresh `McpServer` per request; no server or transport instance is ever held in module scope.
- [Zod v4](https://zod.dev/) for input validation
- [Wrangler](https://developers.cloudflare.com/workers/wrangler/) + [Vitest](https://vitest.dev/)
(`@cloudflare/vitest-pool-workers`)
No Hono, no Express — `src/index.ts` is the whole HTTP layer.
## Rate limiting & privacy
- **60 requests/minute per client IP**, enforced via a Cloudflare [Rate Limiting
binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/) (`wrangler.jsonc`), on
every request. Exceeding it returns `429` with a `Retry-After` header.
- **Zero logging of input values.** The handler never logs request bodies, tool arguments, or calculated
amounts. The client IP is used only as an ephemeral rate-limit counter key, never stored or logged.
- Full policy at [`thetax.nl/privacy`](https://thetax.nl/privacy).
## Development
```bash
npm install
npm run dev # wrangler dev, http://localhost:8787/mcp
npm test # vitest (unit tests + Worker-level MCP/HTTP integration tests)
npm run typecheck
npm run deploy # wrangler deploy
```
## Connecting a client
The server speaks the MCP Streamable HTTP transport at `/mcp`.
### Claude Desktop
Claude Desktop connects to remote MCP servers through the [`mcp-remote`](https://www.npmjs.com/package/mcp-remote)
bridge. Add this to `claude_desktop_config.json`:
```json
{
"mcpServers": {
"dutch-tax-income-calculator": {
"command": "npx",
"args": [
"mcp-remote",
"https://thetax.nl/mcp",
"--transport",
"http-only"
]
}
}
}
```
### `mcp.json` (VS Code, Cursor, and other clients supporting the standard config format)
```json
{
"servers": {
"dutch-tax-income-calculator": {
"type": "http",
"url": "https://thetax.nl/mcp"
}
}
}
```
### Claude Code
A real CLI command — run it directly in your terminal:
```bash
claude mcp add --transport http dutch-tax-income-calculator https://thetax.nl/mcp
```
### Codex CLI
Codex's CLI only has a one-liner for local (stdio) servers; a remote HTTP server like this one needs a
`config.toml` edit instead. Since Codex is itself an agent with file access, the easiest path is to paste this
prompt into Codex chat and let it make the edit:
> Add a remote MCP server to my Codex CLI config. Edit `~/.codex/config.toml` (or `./.codex/config.toml` for
> this project only) and add:
>
> ```toml
> [mcp_servers.dutch-tax-income-calculator]
> url = "https://thetax.nl/mcp"
> ```
>
> No authentication is needed, so no `bearer_token_env_var` or `http_headers` are required. Then confirm the
> entry was added correctly.
Deployed at `thetax.nl/mcp` (path-scoped routes on the existing `thetax.nl` zone, not a custom domain — the
rest of the site is unaffected). A human-readable documentation page with these same instructions, plus the
full input/output schema for every tool, is served at [`thetax.nl/mcp/docs`](https://thetax.nl/mcp/docs). If you
deploy your own copy, replace the URL with the workers.dev subdomain `npm run deploy` assigns (or your own
domain/route).
## Project layout
```
src/
index.ts Worker fetch handler: per-IP rate limit + createMcpHandler(createServer)
server.ts createServer() factory — registers the 3 tools and the tax://brackets/{year} resource
docs-page.ts Renders the /mcp/docs page (setup instructions + full tool schemas)
privacy-page.ts Renders the /privacy policy
terms-page.ts Renders the /terms of service
icon.ts Base64-embedded PNG served at /mcp/icon.png
env.ts Env (RATE_LIMITER binding, OPENAI_APPS_CHALLENGE_TOKEN) type
tax/
schemas.ts Zod v4 input schemas
years.ts Year validation against the package's constants.years
paycheck.ts Thin wrappers around SalaryPaycheck / netToGross — no tax logic here
breakdown.ts breakdown[] / assumptions[] builders
permalink.ts thetax.nl permalink builder
compare.ts Comparison table builder for compare_scenarios
test/ Vitest unit tests + Worker-level MCP/HTTP integration tests
```
### Routes
| Path | Purpose |
|---|---|
| `/mcp` | MCP Streamable HTTP endpoint |
| `/mcp/docs` | Documentation: setup instructions + full tool schemas |
| `/privacy` | Privacy policy |
| `/terms` | Terms of service |
| `/mcp/icon.png` | Connector icon |
| `/.well-known/openai-apps-challenge` | OpenAI App Directory domain-verification token (404 until `OPENAI_APPS_CHALLENGE_TOKEN` is set) |
`/docs`, `/mcp/setup(.html)`, `/mcp/privacy.html`, and `/mcp/terms.html` redirect to the routes above for
continuity.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues