Skip to main content
Glama
AIWerk

@aiwerk/mcp-server-bexio

by AIWerk
README.md
# @aiwerk/mcp-server-bexio

MCP server for the [bexio](https://www.bexio.com) API, the Swiss business software for
invoicing, accounting, CRM, projects and payroll.

310 tools covering the complete public API surface across all three API versions,
generated from bexio's official OpenAPI 3.0.2 specification.

```
Contacts        Quotes        Invoices       Bills          Projects
Orders          Deliveries    Payments       Expenses       Timesheets
Items           Reminders     Banking        Payroll        Files
Accounting      Taxes         Currencies     Users          Notes
```

## Why generated

Every endpoint, HTTP verb, parameter and field name comes from the official
specification rather than from prose documentation. Hand written API clients drift:
they call routes that do not exist, use the wrong verb, target the wrong API version,
or advertise fields the server rejects. None of that can be introduced here, because
none of it is written by hand.

What a specification cannot tell you are the business rules, so the write paths are
also exercised against a live account. See [Testing](#testing).

## Install

```bash
npm install -g @aiwerk/mcp-server-bexio
```

Requires Node.js 18 or newer.

## Authentication

There are two ways in, and which one is appropriate depends on whose account it is.

### OAuth 2.0, for an account you do not own

bexio runs its identity layer on Keycloak with PKCE and refresh tokens. Signing in
grants only the permissions the integration asks for, and the authorisation does not
expire on a fixed schedule.

This is the **only appropriate route for a client's account**. It is available through
the [AIWerk hosted service](https://aiwerkmcp.com), which owns the authorisation flow
and the token lifecycle and passes the access token to the server in
`BEXIO_API_TOKEN`. Running the server standalone with your own OAuth client is
possible, but you have to refresh the token yourself.

### Personal access token, for your own account

Create one at [developer.bexio.com/pat](https://developer.bexio.com/pat).

```bash
export BEXIO_API_TOKEN="your-token"
```

Two things to know:

- It is valid for **60 days** and cannot be renewed, only replaced.
- It carries **every scope**, so it grants full access to the company data. bexio
  documents personal access tokens as strictly personal and not to be shared, so do
  not ask a client for theirs.

The server accepts either kind of token in the same variable, since both are sent as
a bearer credential.

## Usage

### Claude Code

```bash
claude mcp add bexio --env BEXIO_API_TOKEN=your-token -- npx -y @aiwerk/mcp-server-bexio
```

### Claude Desktop

```json
{
  "mcpServers": {
    "bexio": {
      "command": "npx",
      "args": ["-y", "@aiwerk/mcp-server-bexio"],
      "env": { "BEXIO_API_TOKEN": "your-token" }
    }
  }
}
```

### AIWerk hosted service

Install it from the catalogue at [aiwerkmcp.com](https://aiwerkmcp.com) and add your
token in the interface. No local setup required.

## Safety features

Accounting data is not a good place to find out that a tool did something unexpected,
so three guards ship by default.

### Dry run

```bash
export BEXIO_DRY_RUN=1
```

Every write is stopped inside the process and returns a description of the request
that would have been sent. Reads still work normally. Useful for letting an agent plan
a change before you allow it to happen.

### Pre write snapshots

Before modifying or deleting an existing record, the server fetches its current state
and writes it to `~/.aiwerk/bexio-snapshots/`. The tool result carries the file path in
`_snapshot`, so the previous state is always recoverable.

Several bexio edit endpoints replace the whole record, which means an omitted field
becomes empty. The snapshot is what makes that reversible.

If the snapshot cannot be taken, the write is refused. Set
`BEXIO_SNAPSHOT_FAIL_OPEN=1` to downgrade that to a warning, or `BEXIO_NO_SNAPSHOT=1`
to switch snapshots off entirely.

### Rate limit handling

bexio applies a per minute limit per company, and the limit is **not the same for every
endpoint**. Measured against a live account, the items endpoint allows 400 requests per
minute while contacts, accounts and currencies allow 1000.

The server therefore tracks the remaining allowance separately for each endpoint group,
waits out short windows, retries on 429 with the reset hint, and fails with a clear
message rather than hanging when the wait would be long.

## Configuration

| Variable | Default | Purpose |
|---|---|---|
| `BEXIO_API_TOKEN` | required | Personal access token |
| `BEXIO_API_BASE_URL` | `https://api.bexio.com` | Override the host, applies to all API versions |
| `BEXIO_API_TIMEOUT_MS` | `30000` | Per request timeout |
| `BEXIO_DRY_RUN` | off | `1` blocks all writes |
| `BEXIO_NO_SNAPSHOT` | off | `1` disables pre write snapshots |
| `BEXIO_SNAPSHOT_FAIL_OPEN` | off | `1` allows a write when the snapshot fails |
| `BEXIO_SNAPSHOT_DIR` | `~/.aiwerk/bexio-snapshots` | Where snapshots are written |
| `BEXIO_MAX_RATE_LIMIT_WAIT_MS` | `10000` | Longest wait before failing on a rate limit |
| `BEXIO_ENABLED_TAGS` | all | Comma separated domain filter, for example `Contacts,Invoices` |

### Narrowing the tool set

All 310 tools are registered by default. A client that prefers a smaller surface can
restrict the server to specific domains:

```bash
export BEXIO_ENABLED_TAGS="Contacts,Invoices,Quotes,Items"
```

Unknown domain names are reported on startup rather than silently ignored.

## Tool naming

Tools follow a predictable shape, so an agent that knows one name can guess the rest:

```
list_contacts      get_contact      create_contact
search_contacts    update_contact   delete_contact
```

Collection verbs (`list_`, `search_`) take a plural noun, single record verbs take a
singular one. Document actions keep their own verb: `issue_invoice`, `cancel_invoice`,
`send_invoice`, `mark_as_sent_invoice`, `revert_issue_quote`.

## A few bexio specifics worth knowing

- **Three API versions coexist.** Contacts, sales documents, items and projects live on
  2.0, files and expenses on 3.0, bills and banking on 4.0. The server handles this
  transparently, but it explains why paths look inconsistent in error messages.
- **Document positions require a tax id.** The specification does not mark `tax_id` as
  required, yet bexio rejects a position without one. Fetch a valid id with `list_taxes`.
- **A 403 does not always mean permissions.** bexio also answers 403 when the record's
  state forbids the operation, for example deleting an invoice that has been issued.
- **A contact's `address` is readable but not writable.** Reading a contact returns a
  combined `address` ("Alte Jonastrasse 24") next to the split fields, but a write only
  accepts `street_name`, `house_number`, `postcode` and `city`. Reading a contact and
  sending it straight back is rejected with 422.
- **`contact_type_id`** is `1` for a company and `2` for a person.

## Testing

```bash
npm test          # unit tests
npm run smoke     # read only, against a live account
```

The write paths are covered by a separate script that creates and deletes real records,
so it refuses to run without an explicit confirmation:

```bash
BEXIO_WRITE_SMOKE=yes node scripts/write-smoke.mjs
```

Point it at a throwaway trial account, never at production data. It exercises the full
lifecycle of contacts, items, invoices and quotes, then removes what it can.

It cannot remove everything. Issuing an invoice consumes a number from the account's
document sequence, and bexio then refuses to delete that document, which is the correct
behaviour for an accounting system. Every run therefore leaves two cancelled invoices
behind. On a real ledger that trace is permanent.

## Development

The tool layer is generated and must not be edited by hand:

```bash
npm run gen-naming   # specification  ->  tool names
npm run gen-tools    # specification  ->  zod schemas and call sites
npm run build
```

## Licence

MIT, see [LICENSE](LICENSE).

Built by [AIWerk](https://aiwerkmcp.com). Not affiliated with bexio AG.

TDQS

C2.1/5.0

Scored across 310 tools

Disambiguation2/5

Multiple tools have overlapping purposes or unclear boundaries, such as the various position types (item_position, discount_position, pagebreak_position, etc.) and the duplicate paystub PDF tools (get_paystub_pdf vs get_pdf_for_employee_in_month). The 'expens' vs 'expense' naming further adds confusion.

Naming Consistency2/5

While many tools follow a verb_noun pattern, there are significant deviations like create_expens, create_file_file, upload_manual_compound_entry_file_file, list_mes, and inconsistent use of 'list' vs 'search'. This mixed convention hurts predictability.

Tool Count1/5

With 310 tools, the server is far too large for a coherent MCP interface. Even for a broad ERP system, this is excessive and likely overwhelms agents, making selection difficult.

Completeness3/5

The tool set covers a wide range of business domains (contacts, invoices, orders, projects, tasks, etc.), but there are gaps (e.g., no update for company_profile, no delete for business_activity) and redundancies that indicate incomplete or uneven coverage.

Maintenance

ActivitySlowing
ResponsivenessNo issues