Skip to main content
Glama
Invoice Ninja MCP Server

Invoice Ninja MCP Server

by TETRA-2023
README.md
# Invoice Ninja MCP Server

A **self-hosted MCP server for Invoice Ninja v5** — built to talk only to *your*
instance, with no third-party SaaS bridge in the data path. Runs locally over
stdio (Claude Desktop / Claude Code) or remotely over streamable HTTP on your own
box (e.g. Exoscale Zürich).

## Why this exists

The existing options all have a catch:

| Option | Problem for a sovereignty-first setup |
|---|---|
| Zapier / viaSocket / Pipedream MCP | Hosted on US infra, per-call task billing — your invoice data transits a third party |
| `a-wiseguy/invoiceninja-mcp` (Python) | Read-only; writes never implemented; pinned to one IN build |
| `invoice-ninja-mcp-server` (npm) | Single unverified v1.0.0 release |
| Official IN MCP | Requested (issue #11843) but does not exist yet |

This server is standalone (no modification to Invoice Ninja), covers **read + write**,
gates all mutations behind an explicit flag, and deploys anywhere Node runs.

The story behind it — why a chat-driven invoicing workflow, what the write-gating looks
like in daily use, and Invoice Ninja REST API examples in curl and Python — is in the
blog post [Automate invoicing with MCP: my Invoice Ninja server](https://halvic.ch/en/blog/automate-your-invoicing-with-mcp)
(German: [Buchhaltung automatisieren mit MCP](https://halvic.ch/de/blog/buchhaltung-automatisieren-mit-mcp)).

## Tools

**Read (always on):**
`in_ping`, `in_list_clients`, `in_get_client`, `in_list_invoices`, `in_get_invoice`,
`in_list_quotes`, `in_list_payments`, `in_list_products`, `in_list_expenses`,
`in_outstanding_summary` (aggregated AR / overdue totals + top debtors),
`in_list_tasks`, `in_get_task`, `in_list_projects`.

**Write (only when `INVOICE_NINJA_ALLOW_WRITES=true`):**
`in_create_client`, `in_update_client`, `in_create_invoice`, `in_invoice_action`
(mark_sent / mark_paid / archive / restore / delete / cancel / email),
`in_create_payment`, `in_create_quote`, `in_create_product`, `in_create_expense`,
`in_create_task`, `in_start_task`, `in_stop_task`, `in_log_time`, `in_task_action`
(archive / restore / delete / **invoice**), `in_create_project`.

List results are projected to key fields to keep model context lean; use the
`in_get_*` tools for full records.

**Safe mode and a deny list.** `INVOICE_NINJA_SAFE_MODE=true` keeps writes on but leaves out
`in_invoice_action`, `in_quote_action` and `in_create_payment`, and narrows `in_task_action` to
`archive` and `restore`: the model prepares drafts, clients, expenses, tasks and time entries, and
people send, settle, cancel and delete in Invoice Ninja. `INVOICE_NINJA_DISABLED_TOOLS` names
further tools to leave out, comma-separated. `in_ping` reports `safe_mode` next to `writes_enabled`,
and the token's company (`company: {id, name}`) where the instance tells.

## Time tracking

Invoice Ninja tracks work time through **Tasks** (a task is a time entry billed at
an hourly rate), optionally grouped under **Projects**. This server exposes the
full loop:

- **Live timer:** `in_start_task` appends an open entry; `in_stop_task` closes it.
  A task with an open entry is *running*.
- **After the fact:** `in_log_time` appends a completed session — pass `start` +
  `end`, or `start` + `duration_minutes`.
- **Seed on creation:** `in_create_task` can take `time_entries` up front.
- **Bill it:** `in_task_action` with `action: "invoice"` turns tracked time into
  an invoice line item.

Read tools decode the raw `time_log` for you and add `tracked_seconds`,
`tracked_hms` (e.g. "1h 30m") and a `running` flag. Internally, `time_log` is a
JSON string of `[start_epoch, end_epoch]` pairs with `end = 0` meaning still
running — the server handles that encoding, you work in plain timestamps.

Note: starting/stopping/logging time are **writes**, so they require
`INVOICE_NINJA_ALLOW_WRITES=true`.

## Setup

```bash
npm install
cp .env.example .env     # fill in INVOICE_NINJA_URL + INVOICE_NINJA_TOKEN
npm run build
```

Get the token in Invoice Ninja under **Settings → Account Management →
Integrations → API Tokens**. `INVOICE_NINJA_URL` is the bare instance URL — do
**not** append `/api/v1` (it's added for you).

### Claude Desktop (stdio)

`claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "invoiceninja": {
      "command": "node",
      "args": ["/abs/path/to/invoiceninja-mcp/dist/index.js"],
      "env": {
        "INVOICE_NINJA_URL": "https://invoicing.example.ch",
        "INVOICE_NINJA_TOKEN": "your-company-token",
        "INVOICE_NINJA_ALLOW_WRITES": "false"
      }
    }
  }
}
```

### Claude Code

```bash
claude mcp add invoiceninja -- node /abs/path/to/invoiceninja-mcp/dist/index.js
```

(set the env vars in the shell or via `--env` flags)

### Container

```bash
docker build -t invoiceninja-mcp .
docker run --rm -p 8787:8787 -e HOST=0.0.0.0 -e INVOICE_NINJA_URL=https://invoicing.example.ch \
  -e INVOICE_NINJA_TOKEN=... -e INVOICE_NINJA_ALLOW_WRITES=true -e INVOICE_NINJA_SAFE_MODE=true \
  -e MCP_BEARER_TOKEN=... invoiceninja-mcp
```

The image runs `node dist/http.js` as a non-root user (UID 10001), writes nothing to disk (run it with
a read-only root filesystem) and answers `/healthz`. CI publishes `ghcr.io/tetra-2023/invoiceninja-mcp`
with the tags `main` and `sha-<7>` from every merge to `main`, after a container smoke test; pin the digest.

### Remote (streamable HTTP, self-hosted)

```bash
MCP_BEARER_TOKEN="$(openssl rand -hex 32)" npm run start:http
```

Listens on `127.0.0.1:8787/mcp` with `/healthz` for probes. Terminate TLS at your
proxy. Caddy example:

```
invoicing-mcp.example.ch {
    reverse_proxy 127.0.0.1:8787
}
```

Always set `MCP_BEARER_TOKEN` for remote exposure — clients send
`Authorization: Bearer <token>`.

## Safety model

- Writes are **off by default**. Read-only is the safe baseline.
- The Invoice Ninja address and token come **only from the environment**; request headers never
  change the configuration.
- No tool reads the server's filesystem (there is no document-upload tool).
- Over HTTP, the server refuses to start without `MCP_BEARER_TOKEN` unless it listens on the loopback;
  the bearer is compared in constant time.
- Write tools carry `destructiveHint` annotations so the host can prompt before
  executing lifecycle actions.
- Reminder: **updating a client requires resending the full `contacts` array**
  (with each contact `id`) — Invoice Ninja replaces contacts wholesale. The
  `in_update_client` description states this; honour it or you'll drop contacts.

## Configuration reference

| Env var | Default | Purpose |
|---|---|---|
| `INVOICE_NINJA_URL` | — (required) | Instance base URL, no `/api/v1` (an internal `http://` address is fine) |
| `INVOICE_NINJA_TOKEN` | — (required) | Company API token |
| `INVOICE_NINJA_SECRET` | — | Optional `X-Api-Secret` |
| `INVOICE_NINJA_ALLOW_WRITES` | `false` | Enable mutating tools |
| `INVOICE_NINJA_SAFE_MODE` | `false` | With writes on, leave out sending, settling, cancelling, deleting and payments |
| `INVOICE_NINJA_DISABLED_TOOLS` | — | Tool names never registered, comma-separated |
| `INVOICE_NINJA_MAX_PER_PAGE` | `100` | Cap on rows per list call |
| `INVOICE_NINJA_TIMEOUT_MS` | `30000` | Per-request timeout |
| `PORT` / `HOST` | `8787` / `127.0.0.1` | HTTP transport bind |
| `MCP_BEARER_TOKEN` | — | Bearer auth for HTTP transport; required unless `HOST` is a loopback address |

## Alternative: bake it into Laravel

Since Invoice Ninja is itself Laravel and `laravel/mcp` now exists, you could
instead expose these tools from a Laravel control plane and reuse Invoice Ninja's
own models/policies. That couples you to the IN codebase and its release cadence.
This standalone server stays decoupled and works against hosted or self-hosted
instances alike — generally the better choice unless you're already extending IN
in-process.

## Roadmap

- Recurring invoices + subscriptions
- Per-project time & budget reporting (hours logged vs budgeted_hours)
- Tax-period reporting tool (quarterly VAT)
- `outputSchema` on tools for structured-content clients

## Origin & Acknowledgments

This repository began as a copy of
[halviclabs/invoiceninja-mcp](https://github.com/halviclabs/invoiceninja-mcp) at commit
**6e51f1d** (v0.3.0, 2026-11-04), MIT License, Copyright (c) 2026 Halvic Labs. All credit for the
Invoice Ninja API mapping, the tools, the safe mode and the stateless HTTP transport belongs to that
project.

This fork is **disconnected from upstream**. It is deployed by TETRA as a single-instance server behind
an MCP gateway and differs from upstream on purpose:

- configuration from the environment only (the per-request `X-Invoice-Ninja-*` credential headers and
  `INVOICE_NINJA_REQUIRE_REQUEST_CREDENTIALS` are removed);
- no `in_upload_document` tool;
- bearer required off the loopback, compared in constant time;
- image published to `ghcr.io/tetra-2023/invoiceninja-mcp` by this repo's CI.

Upstream changes are picked up manually when needed.

## License

MIT (see [`LICENSE`](LICENSE)).

## Original author

Built by [Halvic Labs](https://halvic.ch) — a Swiss AI-engineering workshop.
More on building MCP servers: [Write your first MCP server](https://halvic.ch/en/blog/write-your-first-mcp-server).

MIT licensed.