# Wafeq

**Category:** 💰 Finance & Fintech  
**Repository:** https://github.com/ohneben/Wafeq-MCP  
**Views:** 0  
**Installs:** 0  
**Upvotes:** 0  
**Directory Page:** https://allmcps.com/mcp/wafeq

## Description
Wafeq accounting API: all 251 endpoints as safety-categorized MCP tools, over stdio or HTTP.

## Claude Desktop Quick Installation
Heuristic fallback — verify the package name and runner against the repository README before running it. Uses `npx` (confidence: low):

```json
"mcpServers": {
  "wafeq": {
    "command": "npx",
    "args": ["-y","wafeq"]
  }
}
```

## Documentation & README

# ohneben's Wafeq MCP

[![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-ohneben-FFDD00?style=for-the-badge&logo=buymeacoffee&logoColor=black)](https://buymeacoffee.com/ohneben)

---

#### License & checks

[![CI](https://github.com/ohneben/Wafeq-MCP/actions/workflows/ci.yml/badge.svg)](https://github.com/ohneben/Wafeq-MCP/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](./LICENSE.md)

#### MCP registries

[![MCP Registry](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fregistry.modelcontextprotocol.io%2Fv0.1%2Fservers%2Fio.github.ohneben%252Fwafeq-mcp%2Fversions%2Flatest&query=%24.server.version&prefix=v&label=MCP%20Registry&color=blue&logo=modelcontextprotocol&logoColor=white)](https://registry.modelcontextprotocol.io/v0.1/servers/io.github.ohneben%2Fwafeq-mcp/versions/latest)
[![Listed on mcpservers.org](https://mcpservers.org/badge.svg)](https://mcpservers.org/servers/ohneben/wafeq-mcp)
[![Wafeq-MCP MCP server](https://glama.ai/mcp/servers/ohneben/Wafeq-MCP/badges/score.svg)](https://glama.ai/mcp/servers/ohneben/Wafeq-MCP)

Run your [Wafeq](https://www.wafeq.com/) books in plain language from AI assistants
like **Claude**, **Cursor**, and any other [MCP](https://modelcontextprotocol.io)
client.

This [Model Context Protocol](https://modelcontextprotocol.io) 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**:

| What you get | Why it matters |
| --- | --- |
| **All 251 endpoints, spec-driven** | Full coverage of invoices, bills, quotes, credit and debit notes, payments, banking, journals, payroll, projects, inventory and reports — nothing hand-picked or left behind. |
| **Nine safety categories, not four** 🟢 / 🟡 / 🟠 / 🔴 | A dozen of Wafeq's POSTs are not creates. Previews write nothing; ending an amortization early posts to the ledger with no undo; reporting an invoice to a tax authority leaves your organization permanently. Each gets its own banner instead of being lumped in with "create". |
| **Server instructions sent on connect** | The client is told how to read the safety banners and the handful of Wafeq conventions — date format, decimal separator, whole-period report ranges — up front, instead of discovering them by getting a call wrong first. |
| **Machine-readable MCP annotations** (`readOnlyHint`, `destructiveHint`) | Hosts that honor annotations (Claude included) can auto-trust the 98 read-only tools and demand confirmation before any of the 44 that delete or cannot be undone. |
| **Correct report parameters, per report** | Each of the four reports gets its own schema: balance sheet takes `date` + `period_count`; profit-and-loss and cash flow take `date_after` + `date_before`; trial balance takes `from_date` + `to_date`. Wafeq **silently ignores** misspelled query parameters, so a wrong name looks like a working call. |
| **Whole-period validation before sending** | Profit-and-loss and cash flow reject ranges that don't align to whole months or years. The server checks locally and replies with the nearest valid range instead of spending a round trip on an HTTP 400. |
| **Automatic idempotency keys** | Every one of the 146 write endpoints that supports `X-Wafeq-Idempotency-Key` gets a UUID v4 automatically, reused across retries — so a network hiccup can never duplicate an invoice. Supply your own to make a deliberate re-run safe too. |
| **File uploads that actually work** | `POST /files/` is multipart-only and `POST /files/raw/` needs a `Content-Disposition` header. Both are handled; you pass base64 content and a filename. |
| **Binary PDFs handled as bytes** | The nine PDF endpoints are base64-encoded into a small envelope with size and content type, instead of being read as text and corrupted. |
| **Automatic retries with backoff** | Transient `429` / `5xx` responses are retried with jittered exponential backoff, honoring `Retry-After` — with the same idempotency key, exactly as Wafeq's integration guide requires. |
| **Built-in rate limiting** | Self-throttles so a burst of tool calls doesn't trip a `429`. Wafeq publishes no numeric limit, so the default is deliberately conservative and configurable. |
| **Tenant verified at startup** | A Wafeq API key is organization-scoped. The server calls `GET /organization/` before serving and publishes the result on `/health`, so a mis-set key shows up as a name you can check rather than as writes against the wrong company's books. |
| **Two transports: stdio *and* Streamable HTTP** | Use it locally in Claude Desktop, or run one always-on server that any number of MCP clients reach over HTTP. |
| **Docker + docker-compose, health check, auto-restart** | `docker compose up` and it stays up, bound to localhost only. |
| **Optional bearer-token auth** on the HTTP endpoint | Put the server behind a shared secret the moment it's reachable beyond localhost. |
| **Your secrets never reach the model** | Credentials live in the server's environment and are injected on every request. The passthrough tool cannot override `Authorization` or point the credential at another host. |
| **Drop-in spec updates** | Wafeq ships a newer spec? Replace one file and rebuild — new endpoints become new tools automatically, no code changes. |

### How it compares

| Capability | **This project** | Generic OpenAPI→MCP wrapper\* |
| --- | :---: | :---: |
| All 251 Wafeq endpoints as tools | ✅ | ✅ |
| Per-tool safety category + banner | ✅ | ❌ |
| Tax-authority filing flagged as irreversible, not "create" | ✅ | ❌ |
| `readOnlyHint` / `destructiveHint` MCP annotations | ✅ | ➖ |
| Read-only fields stripped from create/update bodies | ✅ | ❌ |
| Duplicated enum prose compacted out of schemas | ✅ | ❌ |
| Correct, per-report date parameters | ✅ | ✅ |
| Whole-period range validated before sending | ✅ | ❌ |
| Automatic `X-Wafeq-Idempotency-Key`, stable across retries | ✅ | ❌ |
| Multipart + raw-binary file upload | ✅ | ➖ |
| Binary PDF responses base64-encoded, not mangled | ✅ | ➖ |
| Transaction dates recovered for journal line items | ✅ | ❌ |
| Automatic retries on `429` / `5xx` (honors `Retry-After`) | ✅ | ❌ |
| Client-side rate limiting | ✅ | ❌ |
| Organization identity verified at startup | ✅ | ❌ |
| `stdio` transport | ✅ | ✅ |
| **Streamable-HTTP transport** | ✅ | ➖ |
| **Docker + docker-compose**, health check, auto-restart | ✅ | ❌ |
| **Optional bearer-token auth** on the endpoint | ✅ | ❌ |
| License | MIT | varies |

<sub>\*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](https://github.com/ohneben/Wafeq-MCP/blob/HEAD/MIGRATION.md). "➖" = varies by tool / not guaranteed.</sub>

## 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
`$ref`s, 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](#get-your-api-credentials).
- **Docker** (Docker Desktop on macOS/Windows) for the quick start below — or
  **Node.js ≥ 20** to [run from source](#run-from-source-stdio-no-docker).

## Quick start (Docker)

**1. Add your credentials.** Copy the example config and fill it in:

```bash
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:**

```bash
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:**

```bash
curl -s http://localhost:8765/health
```

```json
{
  "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`](https://www.npmjs.com/package/mcp-remote). For the bridge, add this
under `mcpServers` in your client config and restart the app completely:

```json
{
  "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:

```bash
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](https://registry.modelcontextprotocol.io)
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](#run-the-container-over-stdio) for the equivalent
hand-written config.

```bash
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.

| Variable | Default | What it does |
| --- | --- | --- |
| `WAFEQ_API_KEY` | — | Private organization API key. Sent as `Api-Key <key>`. **One credential is required.** |
| `WAFEQ_ACCESS_TOKEN` | — | OAuth2 access token. Sent as `Bearer <token>`. Takes precedence over `WAFEQ_API_KEY`. |
| `WAFEQ_AUTH_SCHEME` | auto | Force `api-key` or `bearer`. Normally leave unset. |
| `WAFEQ_API_BASE_URL` | `https://api.wafeq.com/v1` | Wafeq API base URL. |
| `WAFEQ_OPENAPI_PATH` | bundled spec | Use a different OpenAPI document (JSON or YAML). |
| `MCP_TRANSPORT` | `stdio` | `stdio` or `http`. Docker sets `http`. |
| `PORT` | `8765` | HTTP listen port. |
| `HOST` | `0.0.0.0` | HTTP bind address. |
| `MCP_HTTP_PATH` | `/mcp` | Path the MCP endpoint is served on. |
| `MCP_AUTH_TOKEN` | — | Bearer token required on `/mcp`. **Required** unless `HOST` is a loopback address, otherwise the server refuses to start. Renamed from `MCP_SHARED_TOKEN` in 2.1.0. |
| `MCP_ALLOWED_HOSTS` | — | Comma-separated hostnames the `Host` header may carry. Empty lets the server derive it: the loopback names on a loopback bind, no check behind a reverse proxy. |
| `MCP_ALLOW_INSECURE` | `0` | Start without a token on a non-loopback bind. Only for a port that genuinely is not reachable by anyone else. |
| `MCP_BODY_LIMIT` | `25mb` | Largest accepted request body. |
| `MCP_SESSION_TTL` | `1800` | Seconds an idle session is kept before it is swept. |
| `MCP_MAX_SESSIONS` | `256` | Concurrent sessions before the least recently used one is evicted. |
| `WAFEQ_TOOL_GROUPS` | — | Comma-separated resource groups to expose, e.g. `invoices,bills,reports`. Empty = all 251. Run `npm run list-tools` for the list. |
| `WAFEQ_MAX_REQUESTS` | `20` | Client-side rate limit: requests per window. `0` disables throttling. |
| `WAFEQ_RATE_WINDOW_MS` | `10000` | Rate-limit window in milliseconds. |
| `WAFEQ_MAX_RETRIES` | `3` | Retries on `429` / `5xx` / network errors. |
| `WAFEQ_TIMEOUT_MS` | `30000` | Per-attempt request timeout. |
| `WAFEQ_ALLOW_LOCAL_FILE_UPLOAD` | `false` | Allow upload tools to read this machine's filesystem via `file_path`. See [Security](#security). |
| `WAFEQ_MAX_UPLOAD_BYTES` | `26214400` | Maximum decoded upload size (25 MiB). |

### 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:

```bash
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).

| Banner | Tools | `readOnlyHint` | `destructiveHint` | What it covers |
| --- | ---: | :---: | :---: | --- |
| 🟢 `READ-ONLY` | 85 | ✅ | ❌ | Every `GET`, plus the account-ledger convenience tool. |
| 🟢 `READ-ONLY · returns a PDF` | 9 | ✅ | ❌ | The PDF downloads: invoice, simplified invoice, credit note, debit note, bill, quote, purchase order, payment, payslip. Returned base64-encoded. |
| 🟢 `READ-ONLY · preview / simulation` | 4 | ✅ | ❌ | Amortization and revenue-recognition previews. `POST`, but documented as writing nothing. |
| 🟡 `WRITE · creates data` | 39 | ❌ | ❌ | Collection creates, both file uploads, and the two conversions (quote→invoice, purchase order→bill). Not idempotent by nature — hence the automatic idempotency key. |
| 🟡 `WRITE · updates data` | 70 | ❌ | ❌ | Every `PUT` and `PATCH`. |
| 🟠 `STATE CHANGE · moves a document in or out of the ledger` | 2 | ❌ | ❌ | Mark expense posted / draft. Reversible — each undoes the other. |
| 🔴 `IRREVERSIBLE · files the document with an external tax authority` | 3 | ❌ | ✅ | Report invoice / credit note / simplified invoice to the tax authority. Leaves your organization and **cannot be recalled**. |
| 🔴 `IRREVERSIBLE · posts the remaining balance to the ledger` | 2 | ❌ | ✅ | End amortization / revenue recognition early. No API undo — run the matching preview first. |
| 🔴 `DESTRUCTIVE · deletes` | 39 | ❌ | ✅ | Every `DELETE`, plus the `wafeq_request` passthrough (its effect can't be known in advance). |
| | **253** | **98** | **44** | |

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:

```bash
npm run list-tools
```

<details>
<summary><strong>🟢 READ-ONLY (85)</strong></summary>

| Tool | Endpoint |
|---|---|
| `wafeq_account_ledger` | _hand-written_ |
| `wafeq_accounts_list` | `GET /accounts/` |
| `wafeq_accounts_retrieve` | `GET /accounts/{id}/` |
| `wafeq_amortizations_list` | `GET /amortizations/` |
| `wafeq_amortizations_retrieve` | `GET /amortizations/{id}/` |
| `wafeq_bank_accounts_ledger_transactions_list` | `GET /bank-accounts/{bank_account_id}/ledger-transactions/` |
| `wafeq_bank_accounts_ledger_transactions_retrieve` | `GET /bank-accounts/{bank_account_id}/ledger-transactions/{id}/` |
| `wafeq_bank_accounts_list` | `GET /bank-accounts/` |
| `wafeq_bank_accounts_retrieve` | `GET /bank-accounts/{id}/` |
| `wafeq_bank_accounts_statement_transactions_list` | `GET /bank-accounts/{bank_account_id}/statement-transactions/` |
| `wafeq_bank_accounts_statement_transactions_retrieve` | `GET /bank-accounts/{bank_account_id}/statement-transactions/{id}/` |
| `wafeq_beneficiaries_list` | `GET /beneficiaries/` |
| `wafeq_beneficiaries_retrieve` | `GET /beneficiaries/{id}/` |
| `wafeq_bills_line_items_list` | `GET /bills/{bill_id}/line-items/` |
| `wafeq_bills_line_items_retrieve` | `GET /bills/{bill_id}/line-items/{id}/` |
| `wafeq_bills_list` | `GET /bills/` |
| `wafeq_bills_retrieve` | `GET /bills/{id}/` |
| `wafeq_branches_list` | `GET /branches/` |
| `wafeq_branches_retrieve` | `GET /branches/{id}/` |
| `wafeq_contacts_list` | `GET /contacts/` |
| `wafeq_contacts_retrieve` | `GET /contacts/{id}/` |
| `wafeq_cost_centers_list` | `GET /cost-centers/` |
| `wafeq_cost_centers_retrieve` | `GET /cost-centers/{id}/` |
| `wafeq_credit_notes_line_items_list` | `GET /credit-notes/{credit_note_id}/line-items/` |
| `wafeq_credit_notes_line_items_retrieve` | `GET /credit-notes/{credit_note_id}/line-items/{id}/` |
| `wafeq_credit_notes_list` | `GET /credit-notes/` |
| `wafeq_credit_notes_retrieve` | `GET /credit-notes/{id}/` |
| `wafeq_custom_fields_list` | `GET /custom-fields/` |
| `wafeq_custom_fields_retrieve` | `GET /custom-fields/{id}/` |
| `wafeq_debit_notes_line_items_list` | `GET /debit-notes/{debit_note_id}/line-items/` |
| `wafeq_debit_notes_line_items_retrieve` | `GET /debit-notes/{debit_note_id}/line-items/{id}/` |
| `wafeq_debit_notes_list` | `GET /debit-notes/` |
| `wafeq_debit_notes_retrieve` | `GET /debit-notes/{id}/` |
| `wafeq_employees_list` | `GET /employees/` |
| `wafeq_employees_retrieve` | `GET /employees/{id}/` |
| `wafeq_expenses_list` | `GET /expenses/` |
| `wafeq_expenses_retrieve` | `GET /expenses/{id}/` |
| `wafeq_files_list` | `GET /files/` |
| `wafeq_files_retrieve` | `GET /files/{id}/` |
| `wafeq_invoices_line_items_list` | `GET /invoices/{invoice_id}/line-items/` |
| `wafeq_invoices_line_items_retrieve` | `GET /invoices/{invoice_id}/line-items/{id}/` |
| `wafeq_invoices_list` | `GET /invoices/` |
| `wafeq_invoices_retrieve` | `GET /invoices/{id}/` |
| `wafeq_item_units_of_measure_list` | `GET /item-units-of-measure/` |
| `wafeq_item_units_of_measure_retrieve` | `GET /item-units-of-measure/{id}/` |
| `wafeq_items_list` | `GET /items/` |
| `wafeq_items_retrieve` | `GET /items/{id}/` |
| `wafeq_journal_line_items_list` | `GET /journal-line-items/` |
| `wafeq_journal_line_items_retrieve` | `GET /journal-line-items/{id}/` |
| `wafeq_manual_journals_list` | `GET /manual-journals/` |
| `wafeq_manual_journals_retrieve` | `GET /manual-journals/{id}/` |
| `wafeq_organization_retrieve` | `GET /organization/` |
| `wafeq_payment_requests_list` | `GET /payment_requests/` |
| `wafeq_payment_requests_retrieve` | `GET /payment_requests/{id}/` |
| `wafeq_payments_list` | `GET /payments/` |
| `wafeq_payments_retrieve` | `GET /payments/{id}/` |
| `wafeq_payslips_list` | `GET /payslips/` |
| `wafeq_payslips_pay_items_list` | `GET /payslips/{payslip_id}/pay-items/` |
| `wafeq_payslips_pay_items_retrieve` | `GET /payslips/{payslip_id}/pay-items/{id}/` |
| `wafeq_payslips_retrieve` | `GET /payslips/{id}/` |
| `wafeq_projects_list` | `GET /projects/` |
| `wafeq_projects_retrieve` | `GET /projects/{id}/` |
| `wafeq_purchase_orders_line_items_list` | `GET /purchase-orders/{purchase_order_id}/line-items/` |
| `wafeq_purchase_orders_line_items_retrieve` | `GET /purchase-orders/{purchase_order_id}/line-items/{id}/` |
| `wafeq_purchase_orders_list` | `GET /purchase-orders/` |
| `wafeq_purchase_orders_retrieve` | `GET /purchase-orders/{id}/` |
| `wafeq_quotes_line_items_list` | `GET /quotes/{quote_id}/line-items/` |
| `wafeq_quotes_line_items_retrieve` | `GET /quotes/{quote_id}/line-items/{id}/` |
| `wafeq_quotes_list` | `GET /quotes/` |
| `wafeq_quotes_retrieve` | `GET /quotes/{id}/` |
| `wafeq_reports_balance_sheet_list` | `GET /reports/balance-sheet/` |
| `wafeq_reports_cash_flow_list` | `GET /reports/cash-flow/` |
| `wafeq_reports_profit_and_loss_list` | `GET /reports/profit-and-loss/` |
| `wafeq_reports_trial_balance_list` | `GET /reports/trial-balance/` |
| `wafeq_revenue_recognitions_list` | `GET /revenue-recognitions/` |
| `wafeq_revenue_recognitions_retrieve` | `GET /revenue-recognitions/{id}/` |
| `wafeq_simplified_invoices_line_items_list` | `GET /simplified-invoices/{invoice_id}/line-items/` |
| `wafeq_simplified_invoices_line_items_retrieve` | `GET /simplified-invoices/{invoice_id}/line-items/{id}/` |
| `wafeq_simplified_invoices_list` | `GET /simplified-invoices/` |
| `wafeq_simplified_invoices_retrieve` | `GET /simplified-invoices/{id}/` |
| `wafeq_tax_rates_list` | `GET /tax-rates/` |
| `wafeq_units_of_measure_list` | `GET /units-of-measure/` |
| `wafeq_units_of_measure_retrieve` | `GET /units-of-measure/{id}/` |
| `wafeq_warehouses_list` | `GET /warehouses/` |
| `wafeq_warehouses_retrieve` | `GET /warehouses/{id}/` |
</details>

<details>
<summary><strong>🟢 READ-ONLY (PDF) (9)</strong></summary>

| Tool | Endpoint |
|---|---|
| `wafeq_bills_download_retrieve` | `GET /bills/{id}/download/` |
| `wafeq_credit_notes_download_retrieve` | `GET /credit-notes/{id}/download/` |
| `wafeq_debit_notes_download_retrieve` | `GET /debit-notes/{id}/download/` |
| `wafeq_invoices_download_retrieve` | `GET /invoices/{id}/download/` |
| `wafeq_payments_download_retrieve` | `GET /payments/{id}/download/` |
| `wafeq_payslips_download_retrieve` | `GET /payslips/{id}/download/` |
| `wafeq_purchase_orders_download_retrieve` | `GET /purchase-orders/{id}/download/` |
| `wafeq_quotes_download_retrieve` | `GET /quotes/{id}/download/` |
| `wafeq_simplified_invoices_download_retrieve` | `GET /simplified-invoices/{id}/download/` |
</details>

<details>
<summary><strong>🟢 READ-ONLY (PREVIEW) (4)</strong></summary>

| Tool | Endpoint |
|---|---|
| `wafeq_amortizations_preview_create` | `POST /amortizations/preview/` |
| `wafeq_amortizations_preview_end_early_create` | `POST /amortizations/{id}/preview-end-early/` |
| `wafeq_revenue_recognitions_preview_create` | `POST /revenue-recognitions/preview/` |
| `wafeq_revenue_recognitions_preview_end_early_create` | `POST /revenue-recognitions/{id}/preview-end-early/` |
</details>

<details>
<summary><strong>🟡 WRITE · CREATES (39)</strong></summary>

| Tool | Endpoint |
|---|---|
| `wafeq_accounts_create` | `POST /accounts/` |
| `wafeq_bank_accounts_create` | `POST /bank-accounts/` |
| `wafeq_bank_accounts_ledger_transactions_create` | `POST /bank-accounts/{bank_account_id}/ledger-transactions/` |
| `wafeq_bank_accounts_statement_transactions_create` | `POST /bank-accounts/{bank_account_id}/statement-transactions/` |
| `wafeq_beneficiaries_create` | `POST /beneficiaries/` |
| `wafeq_bills_create` | `POST /bills/` |
| `wafeq_bills_line_items_create` | `POST /bills/{bill_id}/line-items/` |
| `wafeq_branches_create` | `POST /branches/` |
| `wafeq_contacts_create` | `POST /contacts/` |
| `wafeq_cost_centers_create` | `POST /cost-centers/` |
| `wafeq_credit_notes_create` | `POST /credit-notes/` |
| `wafeq_credit_notes_line_items_create` | `POST /credit-notes/{credit_note_id}/line-items/` |
| `wafeq_custom_fields_create` | `POST /custom-fields/` |
| `wafeq_debit_notes_create` | `POST /debit-notes/` |
| `wafeq_debit_notes_line_items_create` | `POST /debit-notes/{debit_note_id}/line-items/` |
| `wafeq_employees_create` | `POST /employees/` |
| `wafeq_expenses_create` | `POST /expenses/` |
| `wafeq_invoices_create` | `POST /invoices/` |
| `wafeq_invoices_line_items_create` | `POST /invoices/{invoice_id}/line-items/` |
| `wafeq_item_units_of_measure_create` | `POST /item-units-of-measure/` |
| `wafeq_items_create` | `POST /items/` |
| `wafeq_manual_journals_create` | `POST /manual-journals/` |
| `wafeq_payment_requests_create` | `POST /payment_requests/` |
| `wafeq_payments_create` | `POST /payments/` |
| `wafeq_payslips_create` | `POST /payslips/` |
| `wafeq_payslips_pay_items_create` | `POST /payslips/{payslip_id}/pay-items/` |
| `wafeq_projects_create` | `POST /projects/` |
| `wafeq_purchase_orders_bill_create` | `POST /purchase-orders/{id}/bill/` |
| `wafeq_purchase_orders_create` | `POST /purchase-orders/` |
| `wafeq_purchase_orders_line_items_create` | `POST /purchase-orders/{purchase_order_id}/line-items/` |
| `wafeq_quotes_create` | `POST /quotes/` |
| `wafeq_quotes_invoice_create` | `POST /quotes/{id}/invoice/` |
| `wafeq_quotes_line_items_create` | `POST /quotes/{quote_id}/line-items/` |
| `wafeq_simplified_invoices_create` | `POST /simplified-invoices/` |
| `wafeq_simplified_invoices_line_items_create` | `POST /simplified-invoices/{invoice_id}/line-items/` |
| `wafeq_units_of_measure_create` | `POST /units-of-measure/` |
| `wafeq_upload_file` | `POST /files/` |
| `wafeq_upload_file_raw` | `POST /files/raw/` |
| `wafeq_warehouses_create` | `POST /warehouses/` |
</details>

<details>
<summary><strong>🟡 WRITE · UPDATES (70)</strong></summary>

| Tool | Endpoint |
|---|---|
| `wafeq_accounts_partial_update` | `PATCH /accounts/{id}/` |
| `wafeq_accounts_update` | `PUT /accounts/{id}/` |
| `wafeq_bank_accounts_ledger_transactions_partial_update` | `PATCH /bank-accounts/{bank_account_id}/ledger-transactions/{id}/` |
| `wafeq_bank_accounts_ledger_transactions_update` | `PUT /bank-accounts/{bank_account_id}/ledger-transactions/{id}/` |
| `wafeq_bank_accounts_partial_update` | `PATCH /bank-accounts/{id}/` |
| `wafeq_bank_accounts_statement_transactions_partial_update` | `PATCH /bank-accounts/{bank_account_id}/statement-transactions/{id}/` |
| `wafeq_bank_accounts_statement_transactions_update` | `PUT /bank-accounts/{bank_account_id}/statement-transactions/{id}/` |
| `wafeq_bank_accounts_update` | `PUT /bank-accounts/{id}/` |
| `wafeq_beneficiaries_partial_update` | `PATCH /beneficiaries/{id}/` |
| `wafeq_beneficiaries_update` | `PUT /beneficiaries/{id}/` |
| `wafeq_bills_line_items_partial_update` | `PATCH /bills/{bill_id}/line-items/{id}/` |
| `wafeq_bills_line_items_update` | `PUT /bills/{bill_id}/line-items/{id}/` |
| `wafeq_bills_partial_update` | `PATCH /bills/{id}/` |
| `wafeq_bills_update` | `PUT /bills/{id}/` |
| `wafeq_branches_partial_update` | `PATCH /branches/{id}/` |
| `wafeq_branches_update` | `PUT /branches/{id}/` |
| `wafeq_contacts_partial_update` | `PATCH /contacts/{id}/` |
| `wafeq_contacts_update` | `PUT /contacts/{id}/` |
| `wafeq_cost_centers_partial_update` | `PATCH /cost-centers/{id}/` |
| `wafeq_cost_centers_update` | `PUT /cost-centers/{id}/` |
| `wafeq_credit_notes_line_items_partial_update` | `PATCH /credit-notes/{credit_note_id}/line-items/{id}/` |
| `wafeq_credit_notes_line_items_update` | `PUT /credit-notes/{credit_note_id}/line-items/{id}/` |
| `wafeq_credit_notes_partial_update` | `PATCH /credit-notes/{id}/` |
| `wafeq_credit_notes_update` | `PUT /credit-notes/{id}/` |
| `wafeq_custom_fields_partial_update` | `PATCH /custom-fields/{id}/` |
| `wafeq_custom_fields_update` | `PUT /custom-fields/{id}/` |
| `wafeq_debit_notes_line_items_partial_update` | `PATCH /debit-notes/{debit_note_id}/line-items/{id}/` |
| `wafeq_debit_notes_line_items_update` | `PUT /debit-notes/{debit_note_id}/line-items/{id}/` |
| `wafeq_debit_notes_partial_update` | `PATCH /debit-notes/{id}/` |
| `wafeq_debit_notes_update` | `PUT /debit-notes/{id}/` |
| `wafeq_employees_partial_update` | `PATCH /employees/{id}/` |
| `wafeq_employees_update` | `PUT /employees/{id}/` |
| `wafeq_expenses_partial_update` | `PATCH /expenses/{id}/` |
| `wafeq_expenses_update` | `PUT /expenses/{id}/` |
| `wafeq_invoices_line_items_partial_update` | `PATCH /invoices/{invoice_id}/line-items/{id}/` |
| `wafeq_invoices_line_items_update` | `PUT /invoices/{invoice_id}/line-items/{id}/` |
| `wafeq_invoices_partial_update` | `PATCH /invoices/{id}/` |
| `wafeq_invoices_update` | `PUT /invoices/{id}/` |
| `wafeq_item_units_of_measure_partial_update` | `PATCH /item-units-of-measure/{id}/` |
| `wafeq_item_units_of_measure_update` | `PUT /item-units-of-measure/{id}/` |
| `wafeq_items_partial_update` | `PATCH /items/{id}/` |
| `wafeq_items_update` | `PUT /items/{id}/` |
| `wafeq_manual_journals_partial_update` | `PATCH /manual-journals/{id}/` |
| `wafeq_manual_journals_update` | `PUT /manual-journals/{id}/` |
| `wafeq_payment_requests_partial_update` | `PATCH /payment_requests/{id}/` |
| `wafeq_payment_requests_update` | `PUT /payment_requests/{id}/` |
| `wafeq_payments_partial_update` | `PATCH /payments/{id}/` |
| `wafeq_payments_update` | `PUT /payments/{id}/` |
| `wafeq_payslips_partial_update` | `PATCH /payslips/{id}/` |
| `wafeq_payslips_pay_items_partial_update` | `PATCH /payslips/{payslip_id}/pay-items/{id}/` |
| `wafeq_payslips_pay_items_update` | `PUT /payslips/{payslip_id}/pay-items/{id}/` |
| `wafeq_payslips_update` | `PUT /payslips/{id}/` |
| `wafeq_projects_partial_update` | `PATCH /projects/{id}/` |
| `wafeq_projects_update` | `PUT /projects/{id}/` |
| `wafeq_purchase_orders_line_items_partial_update` | `PATCH /purchase-orders/{purchase_order_id}/line-items/{id}/` |
| `wafeq_purchase_orders_line_items_update` | `PUT /purchase-orders/{purchase_order_id}/line-items/{id}/` |
| `wafeq_purchase_orders_partial_update` | `PATCH /purchase-orders/{id}/` |
| `wafeq_purchase_orders_update` | `PUT /purchase-orders/{id}/` |
| `wafeq_quotes_line_items_partial_update` | `PATCH /quotes/{quote_id}/line-items/{id}/` |
| `wafeq_quotes_line_items_update` | `PUT /quotes/{quote_id}/line-items/{id}/` |
| `wafeq_quotes_partial_update` | `PATCH /quotes/{id}/` |
| `wafeq_quotes_update` | `PUT /quotes/{id}/` |
| `wafeq_simplified_invoices_line_items_partial_update` | `PATCH /simplified-invoices/{invoice_id}/line-items/{id}/` |
| `wafeq_simplified_invoices_line_items_update` | `PUT /simplified-invoices/{invoice_id}/line-items/{id}/` |
| `wafeq_simplified_invoices_partial_update` | `PATCH /simplified-invoices/{id}/` |
| `wafeq_simplified_invoices_update` | `PUT /simplified-invoices/{id}/` |
| `wafeq_units_of_measure_partial_update` | `PATCH /units-of-measure/{id}/` |
| `wafeq_units_of_measure_update` | `PUT /units-of-measure/{id}/` |
| `wafeq_warehouses_partial_update` | `PATCH /warehouses/{id}/` |
| `wafeq_warehouses_update` | `PUT /warehouses/{id}/` |
</details>

<details>
<summary><strong>🟠 STATE CHANGE (2)</strong></summary>

| Tool | Endpoint |
|---|---|
| `wafeq_expenses_mark_as_draft_create` | `POST /expenses/{id}/mark-as-draft/` |
| `wafeq_expenses_mark_as_posted_create` | `POST /expenses/{id}/mark-as-posted/` |
</details>

<details>
<summary><strong>🔴 IRREVERSIBLE · EXTERNAL FILING (3)</strong></summary>

| Tool | Endpoint |
|---|---|
| `wafeq_credit_notes_tax_authority_report_create` | `POST /credit-notes/{id}/tax-authority/report/` |
| `wafeq_invoices_tax_authority_report_create` | `POST /invoices/{id}/tax-authority/report/` |
| `wafeq_simplified_invoices_tax_authority_report_create` | `POST /simplified-invoices/{id}/tax-authority/report/` |
</details>

<details>
<summary><strong>🔴 IRREVERSIBLE · LEDGER (2)</strong></summary>

| Tool | Endpoint |
|---|---|
| `wafeq_amortizations_end_early_create` | `POST /amortizations/{id}/end-early/` |
| `wafeq_revenue_recognitions_end_early_create` | `POST /revenue-recognitions/{id}/end-early/` |
</details>

<details>
<summary><strong>🔴 DESTRUCTIVE · DELETES (39)</strong></summary>

| Tool | Endpoint |
|---|---|
| `wafeq_accounts_destroy` | `DELETE /accounts/{id}/` |
| `wafeq_amortizations_destroy` | `DELETE /amortizations/{id}/` |
| `wafeq_bank_accounts_destroy` | `DELETE /bank-accounts/{id}/` |
| `wafeq_bank_accounts_ledger_transactions_destroy` | `DELETE /bank-accounts/{bank_account_id}/ledger-transactions/{id}/` |
| `wafeq_bank_accounts_statement_transactions_destroy` | `DELETE /bank-accounts/{bank_account_id}/statement-transactions/{id}/` |
| `wafeq_beneficiaries_destroy` | `DELETE /beneficiaries/{id}/` |
| `wafeq_bills_destroy` | `DELETE /bills/{id}/` |
| `wafeq_bills_line_items_destroy` | `DELETE /bills/{bill_id}/line-items/{id}/` |
| `wafeq_branches_destroy` | `DELETE /branches/{id}/` |
| `wafeq_contacts_destroy` | `DELETE /contacts/{id}/` |
| `wafeq_cost_centers_destroy` | `DELETE /cost-centers/{id}/` |
| `wafeq_credit_notes_destroy` | `DELETE /credit-notes/{id}/` |
| `wafeq_credit_notes_line_items_destroy` | `DELETE /credit-notes/{credit_note_id}/line-items/{id}/` |
| `wafeq_custom_fields_destroy` | `DELETE /custom-fields/{id}/` |
| `wafeq_debit_notes_destroy` | `DELETE /debit-notes/{id}/` |
| `wafeq_debit_notes_line_items_destroy` | `DELETE /debit-notes/{debit_note_id}/line-items/{id}/` |
| `wafeq_employees_destroy` | `DELETE /employees/{id}/` |
| `wafeq_expenses_destroy` | `DELETE /expenses/{id}/` |
| `wafeq_files_destroy` | `DELETE /files/{id}/` |
| `wafeq_invoices_destroy` | `DELETE /invoices/{id}/` |
| `wafeq_invoices_line_items_destroy` | `DELETE /invoices/{invoice_id}/line-items/{id}/` |
| `wafeq_item_units_of_measure_destroy` | `DELETE /item-units-of-measure/{id}/` |
| `wafeq_items_destroy` | `DELETE /items/{id}/` |
| `wafeq_manual_journals_destroy` | `DELETE /manual-journals/{id}/` |
| `wafeq_payment_requests_destroy` | `DELETE /payment_requests/{id}/` |
| `wafeq_payments_destroy` | `DELETE /payments/{id}/` |
| `wafeq_payslips_destroy` | `DELETE /payslips/{id}/` |
| `wafeq_payslips_pay_items_destroy` | `DELETE /payslips/{payslip_id}/pay-items/{id}/` |
| `wafeq_projects_destroy` | `DELETE /projects/{id}/` |
| `wafeq_purchase_orders_destroy` | `DELETE /purchase-orders/{id}/` |
| `wafeq_purchase_orders_line_items_destroy` | `DELETE /purchase-orders/{purchase_order_id}/line-items/{id}/` |
| `wafeq_quotes_destroy` | `DELETE /quotes/{id}/` |
| `wafeq_quotes_line_items_destroy` | `DELETE /quotes/{quote_id}/line-items/{id}/` |
| `wafeq_request` | _hand-written_ |
| `wafeq_revenue_recognitions_destroy` | `DELETE /revenue-recognitions/{id}/` |
| `wafeq_simplified_invoices_destroy` | `DELETE /simplified-invoices/{id}/` |
| `wafeq_simplified_invoices_line_items_destroy` | `DELETE /simplified-invoices/{invoice_id}/line-items/{id}/` |
| `wafeq_units_of_measure_destroy` | `DELETE /units-of-measure/{id}/` |
| `wafeq_warehouses_destroy` | `DELETE /warehouses/{id}/` |
</details>

## Coverage

| Area | Tools | 🟢 Read | 🟡 Write | 🔴 Irreversible | 🔴 Delete |
| --- | ---: | ---: | ---: | ---: | ---: |
| **Sales & receivables** | 69 | 25 | 31 | 3 | 10 |
| **Purchasing & payables** | 54 | 19 | 27 | 0 | 8 |
| **Banking** | 18 | 6 | 9 | 0 | 3 |
| **Ledger & reporting** | 31 | 19 | 6 | 2 | 4 |
| **Payroll** | 19 | 7 | 9 | 0 | 3 |
| **Master data & dimensions** | 54 | 18 | 27 | 0 | 9 |
| **Files & organization** | 6 | 3 | 2 | 0 | 1 |
| **Escape hatch & convenience** | 2 | 1 | 0 | 0 | 1 |
| **Total** | **253** | **98** | **111** | **5** | **39** |

<sub>"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.</sub>

### 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 carry `created_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 own
  `date_after`/`date_before` filters, 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 reports `requests_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)

```bash
npm ci
npm run build
```

Then register it with your MCP client. For **Claude Desktop**, add to
`claude_desktop_config.json`:

```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**:

```bash
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:

```json
{
  "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:

| Command | What it does |
| --- | --- |
| `npm run build` | Compile TypeScript to `dist/`. |
| `npm test` | Run the Vitest suite. |
| `npm run list-tools` | Print the categorized catalogue. Needs no credentials. |
| `npm run start:stdio` | Run over stdio. |
| `npm run start:http` | Run the Streamable HTTP server. |

## 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`](https://github.com/ohneben/Wafeq-MCP/blob/HEAD/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 `limit` and `offset`, and report the total count.
- **Reports** take report-specific date parameters — balance sheet `date` +
  `period_count`; profit-and-loss and cash flow `date_after` + `date_before`; trial
  balance `from_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 and `POST /files/raw/` needs a
  `Content-Disposition` header — 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-Key` gets
  a UUID v4 automatically, reused across retries. Pass your own to make a deliberate
  re-run safe.
- **Retries**: transient `429` / `5xx` responses are retried with jittered
  exponential backoff, honoring `Retry-After`, under the same idempotency key.
- **Rate limiting**: Wafeq publishes no numeric limit, so the client-side default
  (`WAFEQ_MAX_REQUESTS=20` per `WAFEQ_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 `/health` names 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:

1. The version is derived once, from the tag.
2. The image is built and pushed to `ghcr.io/ohneben/wafeq-mcp` — tagged with the
   version, `MAJOR.MINOR`, the short SHA, and `latest` on `main`.
3. The entry is published to the [MCP Registry](https://registry.modelcontextprotocol.io)
   with `server.json` pinned to that exact image tag. Ownership is proved by the
   `io.modelcontextprotocol.server.name` label on the image, which must match
   `server.json`'s `name` — a test enforces that it does.
4. The released number is written back into `package.json` and `server.json` on
   `main`, 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 `/health`
   without a code edit.

```bash
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 `Authorization` header, 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.example` holds placeholders only.
- **Bind to localhost, or set a token.** `docker-compose.yml` publishes on
  `127.0.0.1` only. Bound any further, the server refuses to start without
  `MCP_AUTH_TOKEN`; it is compared in constant time. A `Host` header 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=false`
  means 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 `/health`** before 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](https://github.com/ohneben/Wafeq-MCP/blob/HEAD/SECURITY.md) to report a vulnerability.

## Credits & license

MIT — see [LICENSE.md](https://github.com/ohneben/Wafeq-MCP/blob/HEAD/LICENSE.md). Built on the
[Model Context Protocol TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk).
Not affiliated with or endorsed by Wafeq.

