Skip to main content
Glama
ohneben

ohneben's Wafeq MCP

ohneben's Wafeq MCP

Buy Me A Coffee


License & Checks

CI License: MIT

MCP Registries

MCP Registry Listed on mcpservers.org Wafeq-MCP MCP server

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:

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

*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. If the server will be reachable beyond localhost, set MCP_SHARED_TOKEN to a long random string as well.

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_SHARED_TOKEN"
      ]
    }
  }
}

(Drop the --header line if you left MCP_SHARED_TOKEN empty.)

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.

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_SHARED_TOKEN

β€”

Bearer token required on /mcp. Empty = no auth. Set it if the port is reachable beyond localhost.

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.

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:

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:

npm run list-tools

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}/

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/

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/

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/

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}/

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/

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/

Tool

Endpoint

wafeq_amortizations_end_early_create

POST /amortizations/{id}/end-early/

wafeq_revenue_recognitions_end_early_create

POST /revenue-recognitions/{id}/end-early/

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}/

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

"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 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)

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:

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

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. If you expose the port any further, set MCP_SHARED_TOKEN first; it is compared in constant time.

  • 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 to report a vulnerability.

Credits & license

MIT β€” see LICENSE.md. Built on the Model Context Protocol TypeScript SDK, following the same architecture as ohneben's LearnWorlds MCP. Not affiliated with or endorsed by Wafeq.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ohneben/Wafeq-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server