ohneben's Wafeq MCP
License & checks
MCP registries
Run your Wafeq books in plain language from AI assistants like Claude, Cursor, and any other MCP client.
This Model Context Protocol server exposes the Wafeq Public API — all 251 endpoints, generated straight from the OpenAPI spec into MCP tools, plus two hand-written ones. Every tool carries a safety category (🟢 read-only / 🟡 write / 🟠 state change / 🔴 irreversible or destructive) so your assistant knows what an action does before it calls it — including the difference between saving an invoice and filing it with a tax authority, which no CRUD-shaped wrapper can tell you. It runs over stdio (Claude Desktop and other local launchers) or Streamable HTTP (hosted in Docker), and ships with retries, client-side rate limiting, request timeouts, idempotency keys, multipart upload and binary PDF handling so it holds up against a live book.
Why you'll want this
Some MCP servers just forward an API. This one is built to be safe to hand to an LLM and easy to run against real accounting data:
How it compares
*Generic OpenAPI→MCP wrappers turn any spec into MCP tools. They can reach the same endpoints, but treat every operation identically — and against Wafeq's spec specifically they inherit the read-only-required-field problem described in MIGRATION.md. "➖" = varies by tool / not guaranteed.
What you can do
Once it's connected, ask your assistant things like:
- "What was our profit and loss for the first half of this year?"
- "Show me every unpaid invoice older than 30 days, with the customer name."
- "Create a draft invoice for Acme Ltd for 3 days of consulting at €800/day."
- "Download invoice INV-2026-014 as a PDF."
- "Which account did the €7,000 transfer in January post to?"
- "Attach this receipt to expense EXP-118."
- "Reconcile the bank statement lines for March against the ledger."
- "Convert quote QUO-31 to an invoice and record the payment."
How it works
Claude / Cursor / any MCP client ──MCP──► this server ──HTTPS──► Wafeq API (your organization)
At startup the server parses the bundled OpenAPI spec into MCP tools — resolving
$refs, guarding against recursive schemas, and stripping server-assigned
(readOnly) fields out of request bodies — tags each tool with its safety category,
verifies which Wafeq organization the credentials belong to, and then injects your
credential on every outgoing request. Your key stays in the server's environment; the
model never sees or handles it.
Requirements
- A Wafeq organization with API access — either a private API key (Wafeq → Settings → Developer → API keys) or an OAuth2 access token. See Get your API credentials.
- Docker (Docker Desktop on macOS/Windows) for the quick start below — or Node.js ≥ 20 to run from source.
Quick start (Docker)
1. Add your credentials. Copy the example config and fill it in:
cp .env.example .env
Then edit .env and set WAFEQ_API_KEY. Unless HOST is a loopback address the
server also requires MCP_AUTH_TOKEN and refuses to start without one, so set it
to a long random string: MCP_AUTH_TOKEN=$(openssl rand -hex 32).
2. Start the server:
docker compose up -d --build
docker-compose.yml binds to 127.0.0.1:8765 only, so the server is reachable from
your machine but not from the network.
3. Confirm it's running — and that it's pointed at the right books:
curl -s http://localhost:8765/health
{
"status": "ok",
"server": "wafeq-mcp",
"version": "2.0.0",
"tools": 253,
"organization": {
"status": "ok",
"id": "org_...",
"name": "Your Company FZCO",
"base_currency": "EUR",
"country": "AE"
},
"auth_required": false
}
Check the name field. That is the organization your key writes to. If it isn't
the company you expected, stop and fix the key before doing anything else. /health
answers 503 and "status": "degraded" when the credentials can't be verified.
4. Point your MCP client at it: http://localhost:8765/mcp (Streamable HTTP).
Remote endpoints are added to Claude as a custom connector (Settings →
Connectors), or bridged locally with
mcp-remote. For the bridge, add this
under mcpServers in your client config and restart the app completely:
{
"mcpServers": {
"wafeq": {
"command": "npx",
"args": [
"mcp-remote",
"http://localhost:8765/mcp",
"--header", "Authorization: Bearer YOUR_MCP_AUTH_TOKEN"
]
}
}
}
(Drop the --header line only if the server runs without a token, which it
allows on a loopback bind alone.)
Prefer a ready-made image?
Every release publishes a ready-to-run image to the GitHub Container Registry, so you can skip the local build entirely:
docker run -d --name wafeq-mcp -p 127.0.0.1:8765:8765 --env-file .env \
ghcr.io/ohneben/wafeq-mcp:latest
Pin a version (:2.0.0) rather than latest if you want releases to be something
you opt into.
Install from the MCP Registry
The server is published to the MCP Registry
as io.github.ohneben/wafeq-mcp, so registry-aware clients can install it by name.
The registry entry launches the image over stdio — see
Run the container over stdio for the equivalent
hand-written config.
curl -s "https://registry.modelcontextprotocol.io/v0.1/servers/io.github.ohneben%2Fwafeq-mcp/versions/latest"
Get your API credentials
Private API key (most people): in Wafeq, go to Settings → Developer → API
keys and create a key. It is scoped to one organization. Put it in .env as
WAFEQ_API_KEY; the server sends it as Authorization: Api-Key <key>.
OAuth2 app: if you have an access token from a Wafeq OAuth2 app, put it in
.env as WAFEQ_ACCESS_TOKEN instead. The server switches to
Authorization: Bearer <token> automatically. Set WAFEQ_AUTH_SCHEME only if you
need to force one scheme while both variables are present.
Configuration
All configuration is environment variables. Everything except the credential has a working default.
Too many tools?
251 tools is a lot. The full catalogue is about 0.5 MB of JSON (~133k tokens) on
tools/list, and some hosts get slower or less accurate with that many. Two things
help.
The schemas are already compacted. Wafeq's spec renders every enum's values into
its description as well as into enum — the currency list alone is ~4 KB, inlined
at 203 places. The generator collapses those single-member allOf wrappers and drops
the duplicated bullet lists, which takes ~29% off the payload without removing a
single allowed value.
Narrow the catalogue if you still want it smaller — no code changes needed:
WAFEQ_TOOL_GROUPS=invoices,bills,contacts,payments,reports,accounts,items,tax-rates
The two hand-written tools are always available, so nothing becomes unreachable —
anything you filter out can still be called through wafeq_request.
Tool safety categories
Every tool's description opens with a banner, and every tool carries the matching MCP annotations. Counts are for the bundled spec (251 generated + 2 hand-written = 253).
The three 🔴 groups all set destructiveHint: true, so a host that honors annotations
stops and asks before any of them — not just before deletions. Filing an invoice with
a tax authority is at least as consequential as deleting one, and unlike a deletion
it reaches outside your organization.
Print the live catalogue any time, without credentials:
npm run list-tools
Coverage
"Write" includes the two 🟠 state-change tools. Areas map to Wafeq resources as follows — Sales: invoices, simplified invoices, quotes, credit notes, payments, payment requests · Purchasing: bills, purchase orders, debit notes, expenses, beneficiaries · Banking: bank accounts with their ledger and statement transactions · Ledger & reporting: accounts, manual journals, journal line items, the four reports, tax rates, amortizations, revenue recognitions · Payroll: payslips, employees · Master data: contacts, items, units of measure, warehouses, projects, cost centers, branches, custom fields.
Two hand-written tools
Everything above is generated. Two tools are written by hand:
wafeq_account_ledger(🟢) — journal line items with their real transaction date. Wafeq's/journal-line-items/rows carrycreated_ts(when the row reached Wafeq), which is regularly a different month from the transaction, and no date field at all. This tool recovers the date using the endpoint's owndate_after/date_beforefilters, which do operate on the transaction date. It probes a month at a time and only splits into day queries where rows exist, so quiet periods cost one request each; the result reportsrequests_made.wafeq_request(🔴) — the escape hatch: any method, any path, plus query, body and headers. It is the fallback for anything the bundled spec misses, not the primary interface. Categorized destructive because its effect can't be known in advance.
Run from source (stdio, no Docker)
npm ci
npm run build
Then register it with your MCP client. For Claude Desktop, add to
claude_desktop_config.json:
{
"mcpServers": {
"wafeq": {
"command": "node",
"args": ["/absolute/path/to/Wafeq MCP/dist/index.js"],
"env": {
"MCP_TRANSPORT": "stdio",
"WAFEQ_API_KEY": "your-key-here"
}
}
}
}
For Claude Code:
claude mcp add wafeq --env WAFEQ_API_KEY=your-key-here -- node /absolute/path/to/dist/index.js
Run the container over stdio
You can also let your client launch the published image directly, with no HTTP server and no local build:
{
"mcpServers": {
"wafeq": {
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "MCP_TRANSPORT=stdio",
"-e", "WAFEQ_API_KEY",
"ghcr.io/ohneben/wafeq-mcp:latest"
],
"env": {
"WAFEQ_API_KEY": "your-key-here"
}
}
}
}
MCP_TRANSPORT=stdio is required here: the image defaults to the HTTP transport.
Useful scripts:
Keeping the spec current
Tools are generated from spec/wafeq-public-api.json at startup — there is no code
generation step and no hand-written tool list. Drop in a newer OpenAPI document
(JSON or YAML), rebuild, and new endpoints become new tools. See
spec/README.md for where the bundled copy came from and what to
re-check after an update.
Observed-behaviour corrections live in src/overrides.ts, keyed by operationId and
dated, so an entry whose operation disappears simply stops applying.
Notes & conventions
- Dates are
YYYY-MM-DD. Amounts use a dot as the decimal separator. - Paging: list tools take
limitandoffset, and report the total count. - Reports take report-specific date parameters — balance sheet
date+period_count; profit-and-loss and cash flowdate_after+date_before; trial balancefrom_date+to_date. Wafeq silently ignores a misspelled query parameter, so a wrong name looks like a working call; the per-report schemas exist to make that impossible. - Whole periods: profit-and-loss and cash-flow ranges must cover whole months or years. The server checks locally and replies with the nearest valid range rather than spending a round trip on an HTTP 400.
- File uploads (
wafeq_files_*): pass base64 content plus a filename.POST /files/is multipart-only andPOST /files/raw/needs aContent-Dispositionheader — both are handled for you. - PDF downloads come back base64-encoded in a small envelope carrying the size and content type, not as mangled text.
- Idempotency: every write endpoint that supports
X-Wafeq-Idempotency-Keygets a UUID v4 automatically, reused across retries. Pass your own to make a deliberate re-run safe. - Retries: transient
429/5xxresponses are retried with jittered exponential backoff, honoringRetry-After, under the same idempotency key. - Rate limiting: Wafeq publishes no numeric limit, so the client-side default
(
WAFEQ_MAX_REQUESTS=20perWAFEQ_RATE_WINDOW_MS=10000) is deliberately conservative. Raise it if you know your allowance. - One organization per credential. A Wafeq API key is organization-scoped; every
tool call acts on that organization, and
/healthnames it.
CI & releases
Every push and pull request is built and tested on Node 20 and 22, and the tool
catalogue is generated with no credentials present — which is what catches a
duplicate or schema-illegal tool name before it ships. CI also fails if .env ever
becomes tracked.
A release is a vX.Y.Z tag and nothing else. No version number is maintained by
hand. Pushing the tag runs the whole chain:
- The version is derived once, from the tag.
- The image is built and pushed to
ghcr.io/ohneben/wafeq-mcp— tagged with the version,MAJOR.MINOR, the short SHA, andlatestonmain. - The entry is published to the MCP Registry
with
server.jsonpinned to that exact image tag. Ownership is proved by theio.modelcontextprotocol.server.namelabel on the image, which must matchserver.json'sname— a test enforces that it does. - The released number is written back into
package.jsonandserver.jsononmain, and the tag is moved onto that commit. So the repository always states the last published version, and the server reports it over MCP and on/healthwithout a code edit.
npm version 2.0.1 --no-git-tag-version # optional; CI stamps it either way
git tag v2.0.1 && git push origin v2.0.1
workflow_dispatch re-publishes a given version without cutting a new tag. Pushes to
main build a -dev.g<sha> image and stop there — they never touch the registry.
Security
- Credentials stay server-side. They are read from the environment and injected
per request. The model sees tool inputs and API responses, never the key. The
passthrough tool cannot override the
Authorizationheader, and refuses to send the credential to any host other than the configured API base. - Never commit
.env. It is git-ignored, and CI fails if it ever becomes tracked..env.exampleholds placeholders only. - Bind to localhost, or set a token.
docker-compose.ymlpublishes on127.0.0.1only. Bound any further, the server refuses to start withoutMCP_AUTH_TOKEN; it is compared in constant time. AHostheader check runs on top of that, so a web page cannot reach a loopback server by DNS rebinding. - Local file uploads are off by default.
WAFEQ_ALLOW_LOCAL_FILE_UPLOAD=falsemeans the server will not read files from its own filesystem. Turning it on lets anything that can call the server ask it to read a local path — leave it off unless you need it and trust every client. Base64 uploads work either way. - Check the organization on
/healthbefore the first write. An API key is scoped to one organization, and a wrong key fails by writing to the wrong company rather than by erroring. - The 🔴 tools mean it. Deletions are permanent, ending a schedule early has no
API undo, and a tax-authority filing cannot be recalled. Keep host confirmations on
for anything carrying
destructiveHint.
See SECURITY.md to report a vulnerability.
Credits & license
MIT — see LICENSE.md. Built on the Model Context Protocol TypeScript SDK. Not affiliated with or endorsed by Wafeq.