# Ekwo OS (npm · @ekwo-ai/mcp)

Open source accounting data infrastructure on your own Postgres, acting as the user under RLS.

- Trust score: 72/100 (medium)
- Registry status: active
- Liveness: live
- Owner verified: no
- Last scored: 2026-09-25

## Components

- npm · `@ekwo-ai/mcp`: 72/100 (this document), [markdown](https://verifymcp.io/servers/ai-ekwo-mcp/ekwo-ai-mcp.md), [page](https://verifymcp.io/servers/ai-ekwo-mcp/ekwo-ai-mcp)

## Channel facts

- Registry: `npm`
- Package: `@ekwo-ai/mcp`
- Version: `0.8.0`
- Transport: `stdio`

## Trust breakdown

How this component scores in each security and reliability category. Every signal is checked automatically from public evidence about the published package, including repeated runs of it in an isolated sandbox, and we only credit what we can confirm. Scores are 0–100 per category. Scoring method: https://verifymcp.io/docs/scoring (what has changed: https://verifymcp.io/docs/scoring/changelog)

Scored 2026-09-25.

- **Supply Chain Security**: 99/100
  - No malware found by supply-chain analysis.
  - No known CVEs affecting this package version or its production dependencies.
  - No install/post-install scripts declared.
  - 31 of 112 dependencies flagged as unhealthy.
- **Provenance & Transparency**: 48/100
  - Source repository is publicly reachable at the declared URL.
  - Provenance check failed: no build-provenance attestation is published.
  - Clear OSI-approved license (AGPL-3.0-only).
  - Actively maintained (last published 2 days ago).
  - Publishes a security disclosure policy (SECURITY.md).
- **Schema Quality & AI Usability**: 77/100
  - 100% of prompts and resources have a non-trivial description (not blank, and not just the item's name).
  - AI-judged instruction clarity (excellent).
  - Context-footprint check failed: tool/resource definitions use about 11217 tokens (~203/item across 55 items; 55 tools + 0 resources), over budget; trim descriptions and params.
  - Usage-examples check failed: none of the tools include examples.
- **Stability & Change Management**: 10/100
  - Stability observed for 3 of 30 days with no destabilising changes; credit accrues until the full window elapses.
- **Tool Coverage**: 88/100
  - 100% of tools have a non-trivial description (not blank, and not just the tool's name).
  - 64% of tool parameters carry a description.
- **Tool Safety**: 100/100
  - No prompt-injection markers were found in the server instructions, tool names or descriptions we captured.
  - All 4 tool(s) whose name or description implies an irreversible operation declare an MCP destructiveHint annotation.
  - An AI judge read all 56 captured unit(s) of tool text and found none that tries to manipulate the model reading it.
- **Capabilities**: 100/100
  - Implements a supported MCP spec version (2025-11-25); the latest is 2026-07-28.

## Install

### How do I install the Ekwo OS MCP server?

Ekwo OS runs locally as an npm package, launched with npx -y @ekwo-ai/mcp. Ready-made configuration for Claude, Cursor, VS Code, Codex and 5 more is on this page, copied from each client's own documentation.

### Claude

```bash
claude mcp add ai-ekwo-mcp -- npx -y @ekwo-ai/mcp
```

### Cursor

```json
{
  "mcpServers": {
    "ai-ekwo-mcp": {
      "command": "npx",
      "args": [
        "-y",
        "@ekwo-ai/mcp"
      ]
    }
  }
}
```

### VS Code

```json
{
  "servers": {
    "ai-ekwo-mcp": {
      "command": "npx",
      "args": [
        "-y",
        "@ekwo-ai/mcp"
      ]
    }
  }
}
```

### Codex

```bash
codex mcp add ai-ekwo-mcp -- npx -y @ekwo-ai/mcp
```

### opencode

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "ai-ekwo-mcp": {
      "type": "local",
      "command": [
        "npx",
        "-y",
        "@ekwo-ai/mcp"
      ],
      "enabled": true
    }
  }
}
```

### OpenClaw

```bash
openclaw mcp add ai-ekwo-mcp --command npx --arg -y --arg @ekwo-ai/mcp
```

### Hermes

```yaml
mcp_servers:
  ai-ekwo-mcp:
    command: "npx"
    args: ["-y", "@ekwo-ai/mcp"]
```

### Netclaw

```json
{
  "McpServers": {
    "ai-ekwo-mcp": {
      "Transport": "stdio",
      "Command": "npx",
      "Arguments": [
        "-y",
        "@ekwo-ai/mcp"
      ]
    }
  }
}
```

### Vellum

```bash
assistant mcp add ai-ekwo-mcp -t stdio -c npx -a -y @ekwo-ai/mcp
```

### Other

```json
{
  "mcpServers": {
    "ai-ekwo-mcp": {
      "command": "npx",
      "args": [
        "-y",
        "@ekwo-ai/mcp"
      ]
    }
  }
}
```

## Changelog

Every change recorded for this component, newest first. Days that predate change tracking, or that we cannot explain, say so: "we were watching and nothing happened" and "we were not watching" are different claims.

### 2026-09-25 (score 72, +1)

- [functional] We updated how we score, so this day's move reflects our rubric, not a change to the server

### 2026-09-23 (score 71, +12)

- [security improvement] Known CVEs: unverified → pass
- [functional improvement] Dependency health: unverified → 0.88
- [functional improvement] Stability: unverified → 0.03
- [functional] Package version: 0.7.0 → 0.8.0

### 2026-09-22 (score 59)

First indexed and scored.

## MCP tools (55)

### `list_companies` (~68 tokens)

List companies

The companies of this installation that you are a member of, with your role on each. Start here: every other tool needs a company_id. Returns id, name, country, currency and your role. It does not list companies you were never invited to — those are invisible, not hidden.

### `get_company` (~90 tokens)

Company settings

Everything needed before booking in a company: its financial years and whether they are closed, its lock dates, its journals, and the accounts that play the receivable, payable, suspense and retained-earnings roles. Read this before creating a document if you do not already know the journals and the lock dates.

Input parameters:

- `company_id` (string, required): The company to work in. Ask list_companies if unsure.

### `list_accounts` (~352 tokens)

Chart of accounts

The accounts a company actually works with, filtered by code prefix, by account type, or by a search on the name. Use it to find the income or expense account a document line should carry. By default it returns the working chart — the accounts that carry entries, that the company's settings or an enabled module point at, or that somebody pinned — because a country pack transcribes the whole regulation and a company uses a few dozen of its several hundred accounts; `in_use_from` and `in_use_to` narrow the movements to a period, `include_all` returns the whole chart, and `include_deprecated` returns it with the retired accounts too. This is a reading and not a restriction: any account of the chart that is not deprecated may still be booked on.

Input parameters:

- `account_type` (string): One of the eighteen account types, e.g. asset_receivable.
- `code_prefix` (string): Only accounts whose code starts with this, e.g. "70".
- `company_id` (string, required): The company to work in. Ask list_companies if unsure.
- `in_use_from` (string): Narrows the movements to entries on or after this date. References and pinned accounts are not dated and stay in.
- `in_use_to` (string): The other end of that period.
- `include_all` (boolean): Return the whole chart instead of the accounts in use. Use it when a search over the working chart found nothing.
- `include_deprecated` (boolean): Include deprecated accounts. A deprecated account is never in use, so this returns the whole chart.
- `limit` (integer)
- `search` (string): Case-insensitive match on the account name.

### `search_contacts` (~128 tokens)

Find a contact

Customers, suppliers and other third parties of a company, by name, by type or by VAT number. Use it before creating a document: a document is booked against a contact, and creating a second contact for a customer who already exists splits their account.

Input parameters:

- `company_id` (string, required): The company to work in. Ask list_companies if unsure.
- `contact_type` (string)
- `limit` (integer)
- `query` (string): Case-insensitive match on the contact name.
- `vat_number` (string): Exact match, e.g. BE0123456749.

### `list_documents` (~180 tokens)

List documents

Invoices, credit notes and quotes of a company, filtered by type, state, settlement state, contact or date range. `state` is the document (draft, posted, cancelled) and `payment_state` is the settlement (not_paid, partially_paid, paid) — two different questions. Returns headers and totals, not lines; use get_document for those.

Input parameters:

- `company_id` (string, required): The company to work in. Ask list_companies if unsure.
- `contact_id` (string)
- `doc_type` (string)
- `from` (string): Earliest document date.
- `limit` (integer)
- `payment_state` (string)
- `state` (string)
- `to` (string): Latest document date.
- `unpaid` (boolean): True: posted and still owed — not paid, or partially.

### `get_document` (~91 tokens)

Read a document

One document with its lines, the account and tax of each line, the legal mentions its country requires on it, that country's payment and e-invoicing rules, and — when it has been posted — the ledger entry it produced with every ledger line. Use it to check what a document will book, what it did book, and what has to be printed on it.

Input parameters:

- `document_id` (string, required)

### `get_preferences` (~120 tokens)

What this user prefers

The signed-in user’s own preferences — which company an interface opens on, the language they read labels in, their timezone and how they like a date and a number written — and the language chain to read labels with: theirs, then the company’s, then the country pack’s. Every one of them may be null, and null means "take the next answer in the chain" rather than a default this installation picked.

Input parameters:

- `company_id` (string): Resolve the language chain against this company. Left out, only what the user themselves chose is returned.

### `list_api_keys` (~101 tokens)

Machine keys of a company

The keys a company has issued to machines, what each one may do, when it was last used and whether it is still live. Only somebody who manages members sees them, and no secret is in here: a key is shown once, when it is issued.

Input parameters:

- `company_id` (string, required): The company to work in. Ask list_companies if unsure.
- `include_withdrawn` (boolean): Also the ones withdrawn or expired. Default false.

### `list_invitations` (~114 tokens)

Invitations into a company

The people invited into a company and not yet on its books, with the preset and the capabilities each was invited with, and whether the invitation is still pending, expired, accepted or withdrawn. Only somebody who manages members sees them. The token is never in here: it is shown once, when the invitation is issued.

Input parameters:

- `company_id` (string, required): The company to work in. Ask list_companies if unsure.
- `include_settled` (boolean): Also the ones already accepted or withdrawn. Default false.

### `list_shares` (~132 tokens)

Public links onto documents

The links a company has published onto its own sales documents, with how many times each was opened and when it was last opened. No token is in here: a link is shown once, when it is made. Use it to answer "did the customer open the invoice?" and to find the link to withdraw.

Input parameters:

- `company_id` (string, required): The company to work in. Ask list_companies if unsure.
- `document_id` (string): Only the links onto this document. Left out, the whole company.
- `include_withdrawn` (boolean): Also the ones withdrawn or expired. Default false.

### `list_bank_transactions` (~124 tokens)

Bank transactions

Statement lines of a company, pending by default — the ones still waiting to be dealt with. `amount` is signed: positive is money in. A statement line is not a ledger entry; recording a payment is what books it.

Input parameters:

- `bank_account_id` (string)
- `company_id` (string, required): The company to work in. Ask list_companies if unsure.
- `from` (string)
- `limit` (integer)
- `state` (string): Defaults to pending: what still has to be dealt with.
- `to` (string)

### `trial_balance` (~86 tokens)

Trial balance

Opening balance, movements of the period and closing balance for every account, from posted entries only. The debit and credit totals are equal on a healthy ledger; if they are not, say so rather than explaining it away.

Input parameters:

- `company_id` (string, required): The company to work in. Ask list_companies if unsure.
- `from` (string, required)
- `to` (string, required)

### `general_ledger` (~137 tokens)

General ledger

Every posted line of an account over a period, with the balance carried forward from before it and a running balance. Give account_code for one account; leave it out for all of them, which on a real company is a lot of lines.

Input parameters:

- `account_code` (string): The account to detail, by its code, e.g. "400000".
- `account_ids` (array): The accounts to detail, by id. Leave both out for every account.
- `company_id` (string, required): The company to work in. Ask list_companies if unsure.
- `from` (string, required)
- `to` (string, required)

### `aged_balance` (~103 tokens)

Aged balance

What customers still owe (or what is still owed to suppliers), bucketed by how overdue it is. It reads unmatched ledger lines, not invoices, so it ties back to the balance sheet: an invoice counts as settled only once its payment has been matched.

Input parameters:

- `at` (string): The day to age at. Defaults to today.
- `company_id` (string, required): The company to work in. Ask list_companies if unsure.
- `group` (string)

### `vat_return` (~169 tokens)

VAT return

The boxes of the VAT return for a period, with their names: the base and tax boxes are summed from what the postings wrote on the ledger lines, the totals are derived from them by the declaration form of the country pack. No country rule lives in this tool, nor in the function behind it. Name a report_code only where a country files several declarations. It prepares figures; it files nothing.

Input parameters:

- `company_id` (string, required): The company to work in. Ask list_companies if unsure.
- `from` (string, required)
- `report_code` (string): Which declaration form, when the country files more than one (BE-VAT-PERIODIC, FR-CA3). Leave it out and the periodic return of the company country is used.
- `to` (string, required)

### `ec_sales_list` (~268 tokens)

Recapitulative statement of intra-Community supplies

Who, in another Member State, was supplied without VAT over a period, and for how much: one line per customer VAT number and per nature — goods, services — read from the treatment of the tax on each sale line, with credit notes deducted. The totals tie back to the intra-Community boxes of vat_return for the same period. A line that carries an issue cannot be filed as it stands, most often because the customer has no VAT number recorded: say so rather than leaving it out of the answer. Name a report_code to have the period checked against the cadence this company files that statement on — it has one of its own, and it is rarely the cadence of the return. No country rule lives in this tool. It prepares a statement; it files nothing.

Input parameters:

- `company_id` (string, required): The company to work in. Ask list_companies if unsure.
- `from` (string, required)
- `report_code` (string): Which recapitulative statement form, when the installation carries one and you are filing it. Name it and the period is checked against the cadence this company files that statement on, which is not…
- `to` (string, required)

### `portfolio_upcoming_filings` (~201 tokens)

Returns falling due, across your companies

What falls due between two dates in every company you may read the declarations of — the question a firm asks of all the books it keeps, and the one a person asks of the single company they run. "Portfolio" is meant as an accounting firm means it — its client portfolio — and is nothing but that: the companies you hold filings.read on, worked out at each call, never a list anybody maintains. Every one of them is in the answer, including a company whose country pack names no deadline, which is listed without a date and says so; a company left out would be indistinguishable from one that was never looked at. No country rule lives in this tool. It reads a calendar; it prepares and files nothing.

Input parameters:

- `from` (string, required): First day a return may fall due on. Go back a few days to catch what is already late.
- `to` (string, required): Last day a return may fall due on.

### `portfolio_filings_touched_since` (~157 tokens)

Declared periods that moved, across your companies

Which declarations, across your portfolio — every company you may read the declarations of, which for an accounting firm is its clients — had entries posted into their period after they were filed — with the company named, how many entries, and how many of the filed figures now disagree. A period stays open after its return has gone, on purpose; this is what keeps a late entry from going unnoticed. Companies with nothing to report are named too, with how many filed declarations were examined. It reads; whether a corrective is owed is for whoever keeps the books.

Input parameters:

- `from` (string): Only declarations whose period ends on or after this day.
- `to` (string): Only declarations whose period starts on or before this day.

### `list_statements` (~92 tokens)

List financial statements

The schemes this company can be presented on: those of its country and of its chart of accounts, plus the generic framework by account type that fits any chart. Ask this before financial_statement rather than guessing a code.

Input parameters:

- `at` (string): The day to read the schemes in force at. Defaults to today.
- `company_id` (string, required): The company to work in. Ask list_companies if unsure.

### `financial_statement` (~181 tokens)

Financial statement

A balance sheet or an income statement for a period, on the scheme the country pack declares — the Belgian abbreviated model, the French liasse — or on the generic framework by account type. Each line is summed from the accounts its rules catch and the totals are derived from the lines; no country rule lives in this tool. It also returns the accounts no line catches: if that list is not empty, say so instead of presenting a statement that does not tie out. It prepares figures; it files nothing.

Input parameters:

- `company_id` (string, required): The company to work in. Ask list_companies if unsure.
- `from` (string, required)
- `statement_code` (string, required): Which scheme, from list_statements (BE-BNB-ABBR-BS, FR-2050, IFRS-SME-BS).
- `to` (string, required)

### `generate_fec` (~118 tokens)

Generate the French FEC

The Fichier des Écritures Comptables for a period: the eighteen columns of the arrêté du 29 juillet 2013, as text, with the checks a tax inspector applies first (mandatory fields, one side per line, entries balancing) and the filename the format wants. The file comes back in the answer; writing it to disk is the caller's business.

Input parameters:

- `company_id` (string, required): The company to work in. Ask list_companies if unsure.
- `from` (string, required)
- `to` (string, required)

### `describe_pack` (~194 tokens)

Where a country pack comes from

The country packs this installation holds, with their version, how much anyone has read them, and the register of texts each was built from — the title, the official publisher, an absolute link and the day somebody opened it, per text. Use it whenever you are asked where a rate, a chart of accounts or a box of the declaration comes from, and before repeating a figure a pack gave you: a community pack has not been read by an accountant, and the status says so. The article behind a single rule is on that rule itself — legal_reference on a tax or on a box — and the register is where that article can be read. It carries no copy of the law: a link that has gone quiet means the register is stale, never that the rule is wrong.

Input parameters:

- `country` (string): ISO 3166-1 alpha-2, upper case. Left out, every pack this installation holds.

### `status` (~61 tokens)

Installation status

The schema version, the instance this is, how this server is connected and as whom, and the companies you can see. Use it first when something does not add up — an empty company list usually means the user was never invited rather than that the books are empty.

### `read_audit_log` (~400 tokens)

Audit trail

Who changed what, and when. Every change to the configuration and reference data of a company — the chart of accounts, the journals, the taxes and the accounts they post to, the bank accounts, the contacts, the products, the financial years, the members and their roles, the country pack version — and every act that changes a state: a document posted or cancelled, an entry posted or reversed, a payment booked, matched or unmatched, a financial year closed or reopened. Filter by table, by natural key, by user, by act, or by date range. The ledger itself is not in here: a posted entry is immutable and is corrected by a reversal, so what is recorded is the act of posting and never the lines. Read-only, and there is no tool that writes it: the trail is append-only and even the operator cannot edit a row.

Input parameters:

- `action` (string): One act: document_posted, document_cancelled, entry_posted, entry_reversed, payment_posted, payment_reconciled, payment_unreconciled, fiscal_year_closed, fiscal_year_reopened, pack_upgraded.
- `actor_id` (string): Only what this user changed. Their auth.users id.
- `company_id` (string, required): The company to work in. Ask list_companies if unsure.
- `from` (string): Earliest date, inclusive.
- `limit` (integer)
- `operation` (string)
- `record_key` (string): The natural key of one row — an account code, a tax code, an invoice number.
- `table` (string): One table of the schema: accounts, journals, taxes, tax_postings, contacts, products, bank_accounts, companies, fiscal_years, company_members, company_packs, api_keys, documents, payments, entries, r…
- `to` (string): Latest date, inclusive — the whole of that day.

### `search_products` (~155 tokens)

Find a product

The catalogue of a company: what it sells and buys, with the code, the unit, the price, the account each books to and the tax each carries. Search it before writing a line by hand — a product fills in the text, the price, the account and the tax, and keeps two invoices for the same thing consistent. Retired products are left out unless you ask for them.

Input parameters:

- `company_id` (string, required): The company to work in. Ask list_companies if unsure.
- `include_inactive` (boolean): Default false: a retired product is hidden.
- `kind` (string)
- `limit` (integer)
- `query` (string): Matches the code, the name or the description.

### `create_product` (~394 tokens)

Add a product

Adds an item to the catalogue: a code unique in the company, a name, a unit, a price, and optionally the account and the tax a sale or a purchase of it carries. It changes nothing already booked — a product is what a new line is filled in from, never a rule applied to the past. Search first: a second code for the same thing is how a price list stops being one.

Input parameters:

- `code` (string, required): Your own reference for the item. Unique in the company, and what EN 16931 calls the seller's item identifier (BT-155).
- `company_id` (string, required): The company to work in. Ask list_companies if unsure.
- `currency_code` (string)
- `description` (string): The longer text under the name (BT-154).
- `kind` (string): Defaults to service. Goods and services are not taxed alike and do not feed the same declaration boxes.
- `name` (string, required): What appears on the invoice line (BT-153).
- `purchase_account_code` (string): Expense account a purchase of this books to.
- `purchase_account_id` (string)
- `purchase_price` (string|number)
- `purchase_tax_code` (string)
- `purchase_tax_id` (string)
- `sale_account_code` (string): Income account a sale of this books to.
- `sale_account_id` (string)
- `sale_price` (string|number): Net unit price on a sale. A line may still carry another.
- `sale_tax_code` (string): Tax applied when it is sold, by code.
- `sale_tax_id` (string)
- `unit_code` (string): UN/ECE recommendation 20: C62 a piece, HUR an hour, DAY a day, MON a month, KGM, LTR, MTR, KWH. Defaults to C62.

### `update_product` (~301 tokens)

Change a product

Changes a catalogue row — its name, price, unit, account, tax — or retires it with active: false. Documents already written keep the text, the price and the account they were invoiced with; an invoice is a statement about the day it was raised, and this tool cannot rewrite one. Retiring is the way to withdraw something: deleting is refused while any line still points at it.

Input parameters:

- `active` (boolean): False retires it: searches stop offering it and the lines that already carry it are untouched.
- `code` (string): A new reference for it.
- `company_id` (string): Needed with product_code, to say which company the code belongs to.
- `description` (string|null)
- `kind` (string)
- `name` (string)
- `product_code` (string): Instead of product_id, with company_id.
- `product_id` (string)
- `purchase_account_code` (string)
- `purchase_price` (string|number|null)
- `purchase_tax_code` (string)
- `sale_account_code` (string)
- `sale_price` (string|number|null)
- `sale_tax_code` (string)
- `unit_code` (string): UN/ECE recommendation 20: C62 a piece, HUR an hour, DAY a day, MON a month, KGM, LTR, MTR, KWH. Defaults to C62.

### `create_contact` (~225 tokens)

Create a contact

Adds a customer, supplier or other third party to a company. Search first: a duplicate contact splits a customer account in two and the aged balance stops making sense. payment_terms_days drives the due date a posted invoice gets when none is given.

Input parameters:

- `address_line1` (string)
- `auxiliary_code` (string): Sub-ledger code; the French FEC reports it.
- `city` (string)
- `client_ref` (string): Your own reference for this creation. Calling again with the same one returns what was created the first time (`replayed: true`) instead of creating a second — use it whenever a call might be repeate…
- `company_id` (string, required): The company to work in. Ask list_companies if unsure.
- `contact_type` (string, required)
- `country` (string): ISO country code, e.g. BE.
- `email` (string)
- `name` (string, required)
- `payment_terms_days` (integer)
- `postal_code` (string)
- `vat_number` (string)

### `create_document` (~326 tokens)

Create a draft invoice

Creates a draft invoice, credit note or quote with its lines, and returns it with the totals the database computed — never with totals you supplied. A line may name a product_code, which fills in its text, price, unit, account and tax; anything the line carries wins over that. With no product and no account_code, the account falls back to the company default and then to its country model. A tax is different: a line with none is booked as a base with no VAT box, which is not the same as 0 %. Nothing is in the ledger yet; post_document is what books it.

Input parameters:

- `accounting_date` (string): The date the entry is booked on, when it differs from the document date.
- `client_ref` (string): Your own reference for this creation. Calling again with the same one returns what was created the first time (`replayed: true`) instead of creating a second — use it whenever a call might be repeate…
- `company_id` (string, required): The company to work in. Ask list_companies if unsure.
- `contact_id` (string, required)
- `currency_code` (string)
- `doc_type` (string, required)
- `document_date` (string, required)
- `due_date` (string)
- `journal_id` (string)
- `lines` (array, required)
- `number` (string): Your own number. Left out, posting takes the entry number.
- `payment_reference` (string)
- `supplier_reference` (string): The supplier's own invoice number, on a purchase.

### `update_document_lines` (~93 tokens)

Replace the lines of a draft

Replaces every line of a draft document with the set you give, and returns the document with its recomputed totals. Lines take product_code the same way create_document does. Drafts only: a posted document is never edited; cancel_document undoes it with a credit note.

Input parameters:

- `document_id` (string, required)
- `lines` (array, required): The complete new set of lines; what is there now is replaced.

### `post_document` (~156 tokens)

Post a document to the ledger

Books a draft document: base lines, VAT lines from the tax configuration, and the customer or supplier counterpart, numbered and posted. A posted entry is never edited; a mistake is undone by cancel_document, which puts the document back to draft only where its country allows it and nothing has left, and issues a credit note otherwise. Ask the user before calling it. It refuses a locked period, a tax that is not in force, and a header total that disagrees with the lines.

Input parameters:

- `document_id` (string, required)
- `dry_run` (boolean): True: the database posts for real and takes it back, and the entry it would have written is returned. Nothing is written; a refusal is the one posting would give.

### `cancel_document` (~411 tokens)

Undo a posted invoice

Undoes a posted sale or purchase invoice in one call, the one way its country and its facts allow, and says which in undone_by. 'draft': the country's posted_edit_policy lets a posted document go back to draft and nothing about this one has left — never sent nor on Peppol, not paid, not declared, its period open and, where numbering is gapless, its number the last one drawn; its entry is taken away, its number given back, and it is a draft to correct and post again. 'credit_note' otherwise, with the reason in why: its credit note — the same lines, taxes, accounts, contact, currency and rate — is issued naming it, posted and matched against it, and the invoice is cancelled, payment state reversed; the invoice keeps its number and its entry. Tell the user which it was. A date, or credit_note: true, asks for the credit note outright. Dated on the invoice's booking day unless you give a date; refused by name when that period is locked or closed (reversal_date_needed), and then you ask the user for a date rather than choosing one. Refused for a draft, a credit note, an invoice already cancelled or credited, and one that is paid in part or in full — the payment is unmatched first with unreconcile, and only when the user says so. Ask the user before calling it.

Input parameters:

- `credit_note` (boolean): True: undo it with a credit note even where it could go back to draft — the user wants the correction on the record.
- `date` (string): The day a credit note is issued and booked on. Given, it asks for the credit note. Left out: the invoice's own booking day, and refused by name (reversal_date_needed) when that period is no longer op…
- `document_id` (string, required): The posted sale or purchase invoice to undo.

### `reverse_entry` (~265 tokens)

Reverse a posted entry

Undoes a posted entry in one call: writes its mirror in the same journal — every line on the other side — naming it, posts it under the next number, and matches the two. The original keeps its number and its lines. Dated on the original's day unless you give a date; refused by name when that period is locked or closed (reversal_date_needed), and then you ask the user for a date rather than choosing one. Refused for an entry a document wrote (use cancel_document), the entries of a close (reopen_fiscal_year), an opening, the entry of a payment, a bank transaction, a module or a matching, an entry already reversed or that is itself a reversal, and a matched entry — unmatch it first with unreconcile, and only when the user says so. Ask the user before calling it.

Input parameters:

- `date` (string): The day the reversal is booked on. Left out: the original entry's day, and refused by name (reversal_date_needed) when that period is no longer open — then ask the user which open date to use; never…
- `entry_id` (string, required): The posted entry to undo. An entry a document wrote is undone with the document, by cancel_document.

### `record_payment` (~477 tokens)

Record a payment

Records money in or out, books it (bank against the customer or supplier account) and, unless you say otherwise, matches it against that contact's oldest open invoices up to the amount paid. Matching is what makes an invoice count as paid. The entry it produces cannot be unposted, though the matching can be undone with unreconcile.

Input parameters:

- `amount` (string|number, required): A positive decimal string; the direction carries the sign.
- `bank_account_id` (string): Which bank account the money moved on. list_bank_accounts says what exists. Given alone, it also names the journal; left out, the default account of the journal is used.
- `client_ref` (string): Your own reference for this creation. Calling again with the same one returns what was created the first time (`replayed: true`) instead of creating a second — use it whenever a call might be repeate…
- `company_id` (string, required): The company to work in. Ask list_companies if unsure.
- `contact_id` (string): Who paid or was paid. Needed for the payment to be matched against their invoices.
- `currency_code` (string): Left out: the currency the company keeps its books in.
- `direction` (string): inbound: a customer paid you. outbound: you paid a supplier. May be left out when document_id is given.
- `document_id` (string): The posted document this money pays. It names the contact and the direction, read off what is still open on it, and the matching is offered to that document alone rather than to the oldest open items.
- `exchange_rate` (string|number): Units of the payment currency for one unit of the company currency, as a rate table states it. Only needed when the payment is in another currency; the realised difference against the invoice is book…
- `journal_code` (string): The bank or cash journal, by code, e.g. BNK. Not needed when bank_account_id is given: the account knows its journal.
- `journal_id` (string)
- `match_open_items` (boolean): Default true: match the payment against the oldest open invoices of that contact, up to the amount paid.
- `memo` (string)
- `payment_date` (string, required)
- `reference` (string)

### `reconcile` (~130 tokens)

Match two ledger lines

Matches a debit line against a credit line on the same reconcilable account, for an amount, defaulting to the smaller of the two open amounts. Use it to settle an invoice against a payment by hand. Matching changes no account and stays possible after a period is locked.

Input parameters:

- `amount` (string|number): Left out: the smaller of the two open amounts.
- `line_a` (string, required): A ledger line to match. Either side; the schema works out which is the debit.
- `line_b` (string, required): The line it settles. Both must be on the same reconcilable account.

### `unreconcile` (~68 tokens)

Undo a matching

Removes one matching, putting the residual back on both lines. The entries themselves are untouched. It is what cancel_document and reverse_entry ask for first when what they would undo is matched.

Input parameters:

- `reconciliation_id` (string, required): The matching to undo, as returned by reconcile or read from the ledger line.

### `create_bank_account` (~286 tokens)

Add a bank account

Registers a bank account of the company from its IBAN, and wires it to the bank journal and to the ledger account behind it — both of which the country template has already chosen, so neither has to be given. Running it twice with the same IBAN returns the one that exists rather than creating a second. Do this once per account: until a company has one, an invoice carries no IBAN and record_payment can only book on the journal's default account.

Input parameters:

- `account_code` (string): The ledger account behind it. Left out, the journal's default account — 550000 in Belgium, 512000 in France.
- `account_id` (string)
- `bank_name` (string)
- `bic` (string)
- `company_id` (string, required): The company to work in. Ask list_companies if unsure.
- `currency_code` (string): Defaults to the company's own currency.
- `iban` (string, required): The IBAN. Spaces are removed and the value is upper-cased; it is the natural key of a bank account in a company.
- `journal_code` (string): The financial journal it books through. Left out, the bank journal of the company.
- `journal_id` (string)
- `label` (string): What it is called in the books. Defaults to the bank name, then to the IBAN.

### `list_bank_accounts` (~87 tokens)

Bank accounts

The bank accounts of a company, with their IBAN, the journal they book through and the ledger account behind each. Read it before recording a payment on a particular account, and to find out whether the company has one at all.

Input parameters:

- `company_id` (string, required): The company to work in. Ask list_companies if unsure.
- `include_inactive` (boolean): Default false.

### `create_bank_transaction` (~189 tokens)

Add a bank transaction

Records one statement line by hand, for an installation without a bank feed. `amount` is signed: positive is money in. It books nothing — the line waits as pending until a payment is recorded.

Input parameters:

- `amount` (string|number, required): Signed: positive is money in, negative is money out.
- `bank_account_id` (string, required)
- `company_id` (string, required): The company to work in. Ask list_companies if unsure.
- `contact_id` (string)
- `counterpart_iban` (string)
- `counterpart_name` (string)
- `description` (string)
- `reference` (string)
- `statement_id` (string)
- `structured_reference` (string): A structured communication, e.g. +++000/0000/00000+++ or RF…
- `transaction_date` (string, required)
- `value_date` (string)

### `import_bank_statement` (~481 tokens)

Import a bank statement

Reads a bank statement file and writes its statements and lines. `format` says what the file is — `camt.053` (the ISO 20022 XML statement), `coda` or `cfonb120`, which are the ones this server reads today — and `content` is the file as text. It is never guessed from the content. It books nothing: every line waits as pending, to be settled afterwards. Safe to repeat: the same file imported twice creates nothing the second time, and a statement overlapping an earlier one imports only the new lines. Refused by name, with nothing written: an account the company does not have (create it first with create_bank_account — an import never creates one), a statement whose opening balance plus lines is not its closing balance, a line in another currency. Signalled in `warnings`, not refused: an opening balance that is not the previous closing one, which means a statement is missing.

Input parameters:

- `bank_account_id` (string): Only for a file of one statement whose account the company identifies otherwise than the file does. Left out, the account is found by the identifier the statement carries, and an unknown one is refus…
- `company_id` (string, required): The company to work in. Ask list_companies if unsure.
- `content` (string, required): The file itself, as text (UTF-8).
- `file_name` (string): The name the file had, kept on the statement.
- `format` (string, required): What the file is. Required, and never guessed from the content: `camt.053` is the ISO 20022 XML statement, `coda` the coded statement of account of 128-character records, `cfonb120` the statement of…
- `iban_country` (string): Only for `cfonb120`, which identifies an account by a bank code, a branch code and a number, and names no country. With the two letters of the country the account is held in, the statement is matched…
- `storage_path` (string): Where the caller stored the file, if it did. With it the file is recorded in attachments, on the statement; this server stores no bytes.

### `create_company` (~260 tokens)

Create a company

Creates a company on a country pack: its chart of accounts, its journals and its taxes are copied in, its first financial year is opened on the month that country opens one on, and you become its first member. Creating a company is an instance-level act — it needs an instance administrator, and being one is not the same as being on anybody’s books. Ask the user for the country rather than guessing: the wrong answer is a whole chart of accounts.

Input parameters:

- `chart_code` (string): Which chart, where the country offers several.
- `country` (string, required): Which country pack: its chart of accounts, its taxes and its declaration form. list_companies shows what the installation already uses.
- `currency_code` (string): Left out, the country pack's.
- `fiscal_year` (integer): Calendar year the first financial year opens in. Default this year.
- `fiscal_year_start` (string): First day of it. Left out, the month the country pack opens a year on — and a pack that names none is a refusal, not a January.
- `language` (string): Language the books are kept in — it decides the labels of the chart. Left out, the country pack's.
- `name` (string, required)

### `update_company_profile` (~385 tokens)

Change the company itself

Changes what a company says about itself on its documents: the names it goes by, its address and identifiers, its logo, its stated capital, its activity code and the bank account customers are asked to pay into. Only the fields you name change. It touches nothing in the ledger, and it needs company.write — the owner preset, not the accountant one.

Input parameters:

- `activity_code` (string|null): NACE, APE, SIC — the code itself.
- `activity_scheme` (string|null): Which register the code belongs to.
- `address_line1` (string|null)
- `address_line2` (string|null)
- `city` (string|null)
- `company_id` (string, required): The company to work in. Ask list_companies if unsure.
- `default_bank_account_id`: Fills the payee IBAN of a sales document that names none. list_bank_accounts says what exists.
- `document_template` (string|null): A code the renderer interprets.
- `email` (string|null)
- `legal_form` (string|null)
- `legal_name` (string|null)
- `logo_url` (string|null): Where the logo is. A URL or a storage path: the core keeps no file.
- `name` (string)
- `phone` (string|null)
- `postal_code` (string|null)
- `registration_number` (string|null): The number of the commercial register of its country. There is no second column for it.
- `share_capital` (string|null): A decimal string. Several legal forms must state it on every document.
- `share_capital_currency`: Left out, the company's own currency.
- `trade_name` (string|null): The name it trades under, if not the statutory one.
- `vat_number` (string|null)
- `website` (string|null)

### `set_preferences` (~168 tokens)

Change what this user prefers

Writes the signed-in user’s own preferences and nobody else’s. Only the fields you name change: leave one out and it is untouched, pass null and it is cleared, which puts that question back to the company and then to the country pack. It changes nothing in the books.

Input parameters:

- `date_display_format` (string|null): How this person likes a date written.
- `language` (string|null): Two letters, optionally a region. Labels are read in it first.
- `number_display_format` (string|null): How this person likes a number written. Not the pattern a document number is built from, which belongs to the country.
- `preferred_company_id`: The company to open on.
- `theme` (string|null)
- `timezone` (string|null): An IANA name.

### `invite_member` (~241 tokens)

Invite somebody into a company

Invites an address into a company with a preset — viewer reads, client reads and hands pieces over, accountant keeps the books, owner also administers — and any capability granted on top of it. It returns a token once and stores only its hash, so hand the token to the person you invited: they accept it themselves, signed in with that address. It does not send an e-mail, and it does not create an account. Inviting the same address again withdraws the invitation that was pending.

Input parameters:

- `capabilities` (array): Capability codes granted on top of the preset, e.g. members.manage. get_company lists what this installation knows.
- `company_id` (string, required): The company to work in. Ask list_companies if unsure.
- `email` (string, required): The address the invitation is for. Matched when it is accepted.
- `role` (string): The preset. Default viewer, which reads and changes nothing. client is the person whose company it is, invited by whoever keeps their books: it reads the same, and may also hand a file over (document…
- `valid_for_days` (integer): Default 14.

### `create_api_key` (~216 tokens)

Issue a key to a machine

Issues a key so a script — a nightly import, a till, a bank feed — can work in one company without a person signing in. It carries an explicit list of capabilities and nothing else, it can never reach another company, and it cannot hold a capability you do not hold yourself. The secret comes back once and is stored only as a hash: show it to the user and say it cannot be read back. Prefer this to sharing anybody’s password, and never suggest a service key.

Input parameters:

- `capabilities` (array, required): Exactly what the machine may do, e.g. ["bank.write"]. You cannot put a capability on a key that you do not hold yourself.
- `company_id` (string, required): The company to work in. Ask list_companies if unsure.
- `expires_at` (string|null): When it stops working, as a timestamp. Left out, it does not expire on its own.
- `name` (string, required): What this key is for, in the words an operator will read a year from now.

### `revoke_api_key` (~68 tokens)

Withdraw a machine key

Stops a key working, now and for good. There is no un-withdraw: a secret that has been out of the building is issued again rather than brought back. Whatever the machine was doing with it stops, so say so before calling it.

Input parameters:

- `api_key_id` (string, required)

### `revoke_invitation` (~64 tokens)

Withdraw an invitation

Withdraws an invitation that has not been accepted, so its token stops working. An invitation that has already become a membership is refused: a member is removed from the company, which is a different act and not one this server does.

Input parameters:

- `invitation_id` (string, required)

### `share_document` (~186 tokens)

Publish a document behind a link

Publishes a posted sales invoice, credit note or quote behind a link the customer opens without an account and without a password: the token in the url is the whole secret. It returns that token once and stores only its hash, so give the url to the customer and say it cannot be read back. The page shows the document as it was sent and what is still owed on it today, so the link stays worth opening after a payment. A purchase document is refused — it is a third party’s own document — and so is a draft. A link is never edited: to change when it expires, withdraw it and make another.

Input parameters:

- `document_id` (string, required): The sales document to publish. A purchase document and a draft are refused.
- `expires_at` (string|null): When the link stops answering, as a timestamp. Left out, it answers until it is withdrawn.

### `revoke_share` (~73 tokens)

Withdraw a published link

Stops a link working, now and for good. Whoever holds it sees exactly what they would see for a link that never existed. There is no un-withdraw: a secret that has been out of the building is issued again rather than brought back, so say so before calling it.

Input parameters:

- `share_id` (string, required)

### `pin_accounts` (~133 tokens)

Pin accounts to the working chart

Adds accounts to the working chart of a company, so list_accounts offers them whether or not anything has been booked on them. Pass pinned: false to take one back out. Pinning is display and nothing else: an unpinned account may still be booked on, and a pinned one is still hidden once it is deprecated.

Input parameters:

- `account_codes` (array, required): Codes in this company's chart. list_accounts with include_all says what exists.
- `company_id` (string, required): The company to work in. Ask list_companies if unsure.
- `pinned` (boolean): True pins, false unpins. Defaults to true.

### `lock_period` (~135 tokens)

Lock a period

Moves the accounting lock date, the VAT lock date, or both. After this, nothing can be booked, changed or deleted on or before that date — including corrections you may want later. Only an owner of the company may do it. Always ask the user first, and never lock a period you have just posted into without being told to.

Input parameters:

- `company_id` (string, required): The company to work in. Ask list_companies if unsure.
- `lock_date`: Nothing may be booked on or before this date. null lifts the lock.
- `tax_lock_date`: Additionally freezes anything carrying a VAT box. null lifts it.

### `import_books` (~985 tokens)

Import books from another system

Takes over the books a user kept elsewhere — a FEC, an export of journal items, a journal report, a trial balance, or the export of odoo or xero by name — whole or not at all. Each file is read by the reader of its source; every account of the old chart is matched to one of this company's chart and every old journal to a journal of the company. The codes give a candidate (the same code, the same digits without the padding zeros, or the longest beginning of three digits or more), and what the files say of the old account — its type, its name, the side of its balance — is held against what the chart says the candidate is, never a country: only the same code, not contradicted, is `exact`; anything else is `suggested` with its reason, or `none`. Call it with dry_run first: it returns the correspondence proposed, the suggestions under mapping.suggested, what is unanswered, and — when everything is answered — what the database would write, rehearsed and rolled back. Show the user every suggested line and its reason first; nothing is posted while one is unconfirmed. Settle each by writing its code under mapping.accounts, or, once the user has read and approved them all, pass accept_suggestions. Every entry is posted through post_entry(); a trial balance becomes the opening entry. Imported lines carry no tax and feed no VAT box. The same files twice are refused. The files travel as text, up to 256 KiB in all: a larger export is imported from the terminal with `ekwo import`, which reads it from the disk. Ask the user before the real call.

Input parameters:

- `accept_suggestions` (boolean): Take every account the proposal only suggested as the answer. Only after the user has read each suggested line and its reason, and said yes to all of them; left out, a suggestion stops the import.
- `allow_result_accounts` (boolean): Let an opening balance carry income and expense accounts: books taken over in the middle of a year.
- `company_id` (string, required): The company to work in. Ask list_companies if unsure.
- `date_order` (string): For `journal-report`: the order of a date written only in digits, which the file does not say.
- `dry_run` (boolean): Show what would be written — the correspondence proposed, the entries, the numbers — and write nothing. Always first, and the user reads it before the real call.
- `files` (array, required): The files of one source, in the order given. `journal-items` and `journal-report` take the chart of accounts and the parties beside the lines, each recognised by its header; the others take one file.
- `keep_numbers` (boolean): Post each entry under the number it had. Where the country forbids a hole in the sequence it takes the entries.import capability; left out, the journal draws the numbers and the old one is kept as th…
- `mapping` (object): The correspondence, as a previous call with dry_run returned it and the user completed it. What it answers wins over the proposal.
- `open_years` (boolean): Open the fiscal years the entries fall in where the company has none, on the length and first day of its own. Off, such an entry is refused by name.
- `opening_date` (string): For a trial balance: the first day of the fiscal year it opens. The file does not say it.
- `source` (string, required): What the files are, never guessed from their content: `trial-balance` — a trial balance as CSV — account, debit, credit, or a signed balance — which becomes the opening entry of a year; `fec` — a fic…

### `opening_balance` (~204 tokens)

Import an opening balance

Turns the trial balance of whatever kept the books before into the opening entry of a fiscal year, on the opening journal, dated on its first day. Lines are {account_code, debit, credit}; total debit must equal total credit. Balance-sheet accounts only, unless allow_result_accounts is set, which is for taking books over in the middle of a year. A year holds one opening: a second call is refused rather than added to.

Input parameters:

- `allow_result_accounts` (boolean): Default false: an opening balance is made of the balance sheet. Pass true only when taking books over in the middle of a year that has already run.
- `company_id` (string, required): The company to work in. Ask list_companies if unsure.
- `fiscal_year_id` (string, required): The year the balance opens. The entry is dated on its first day.
- `lines` (array, required): The trial balance of the previous system, one entry per row. Total debit must equal total credit.

### `close_fiscal_year` (~138 tokens)

Close a fiscal year

Closes a year: the result of the year leaves the income statement the way the country model says — straight to retained earnings, into a current-year result account, or through the appropriation accounts — every income and expense account goes back to zero, and the year stops accepting entries. Refused while the year holds a draft entry, while an earlier year with entries is open, or when a later year is already closed. It never writes what a general meeting decides to do with the result. Always ask the user before calling it.

Input parameters:

- `fiscal_year_id` (string, required): The year to close. Every entry in it must be posted.

### `reopen_fiscal_year` (~86 tokens)

Re-open a closed fiscal year

Undoes a close that was run too early: the entries it wrote are reversed, never deleted, and the year accepts entries again. Refused once a later year is closed or holds entries of its own, because re-opening changes a result those years stand on. Always ask the user before calling it.

Input parameters:

- `fiscal_year_id` (string, required): The closed year to open again.

## Diagnostics

Captured diagnostic sections: Provenance, Dependencies. The full working is on the page: https://verifymcp.io/servers/ai-ekwo-mcp/ekwo-ai-mcp#diagnostics

## Score history

- 2026-09-25: 72
- 2026-09-24: 71
- 2026-09-23: 71
- 2026-09-22: 59

## Common questions

### What is the Ekwo OS MCP server?

Ekwo OS is an MCP server listed in the public MCP registry as ai.ekwo/mcp. Open source accounting data infrastructure on your own Postgres, acting as the user under RLS. This page covers its npm package (@ekwo-ai/mcp).

### Is the Ekwo OS MCP server safe to use?

Ekwo OS scores 72 out of 100 on VerifyMCP. We found no known CVEs affecting it as of 25 September 2026. It declares no install or post-install scripts. That is a record of what we were able to check automatically, not an endorsement. The category breakdown on this page shows every signal behind the number, including the ones we could not confirm.

### What tools does the Ekwo OS MCP server expose?

Ekwo OS exposes 55 tools: list_companies, get_company, list_accounts, search_contacts, list_documents, and 50 more. Their descriptions and schemas cost roughly 11,053 tokens of context every time the server is loaded.

### Is the Ekwo OS MCP server still maintained?

Ekwo OS is still listed as active in the MCP registry. We last reached this channel on 25 September 2026. Those dates come from our own scans of the registry and the channel itself, not from anything the publisher announced.

### What licence is the Ekwo OS MCP server under?

Ekwo OS declares the AGPL-3.0-only licence, which is OSI-approved. That covers the source only, and says nothing about the cost of any service it calls.

## Links

- npm package: https://www.npmjs.com/package/@ekwo-ai/mcp
- Socket report: https://socket.dev/npm/package/@ekwo-ai/mcp
- Repository: https://github.com/Ekwo-ai/ekwo-os
- Website: https://ekwo.ai/
- Changelog RSS feed: https://verifymcp.io/servers/ai-ekwo-mcp/ekwo-ai-mcp.xml
- Changelog JSON feed: https://verifymcp.io/servers/ai-ekwo-mcp/ekwo-ai-mcp.json
- HTML version of this page: https://verifymcp.io/servers/ai-ekwo-mcp/ekwo-ai-mcp
