
Ekwo OS — open source accounting: ledger, invoices and VAT on your own Supabase or Postgres
ai.ekwoPublished by ekwo.aiOfficial publisherv0.24.0Updated Oct 29, 2026
Double-entry books an agent keeps as the user: invoices, VAT returns, 174 country packs.
Overview
Ekwo OS accounting server that lets an assistant read the ledger, raise and post invoices, match payments and prepare VAT returns in your own Postgres or…
- What you can do
- List companies, financial years, accounts, contacts, products and documents, and read one document with its lines and entry.
- Create contacts and draft documents, post them, record payments, and cancel or reverse what was posted.
- Read the books: trial balance, general ledger, journal book, aged balance, contact statements and financial statements.
- Prepare tax and filings: VAT returns, instalment plans, EU sales lists, FEC export and portfolio filing deadlines.
- Analyse by analytic axis, render invoice PDFs (including Factur-X), and inspect audit log, preferences, invitations and API keys.
- When to use it
- Useful when an assistant should work directly on real double-entry books kept in your own Supabase or Postgres installation — drafting and posting invoices, matching payments, or pulling VAT and ledger figures — while row level security limits it to what the signed-in person may do.
- Requirements
- Either the hosted Streamable HTTP endpoint, which needs an Ekwo Cloud account and OAuth approval in a browser, or the local npm package run with npx (Node.js). Local setup needs SUPABASE_URL and SUPABASE_ANON_KEY plus EKWO_EMAIL and EKWO_PASSWORD, or EKWO_ACCESS_TOKEN; a self-hosted database instead uses EKWO_DB_URL with EKWO_ACT_AS_USER_ID and the postgres driver.
Installation
In SourceWeft
- Open Ekwo OS — open source accounting: ledger, invoices and VAT on your own Supabase or Postgres in the dashboard and add it to a workspace.
- Connect the required credentials. They are stored privately and never shown on public pages.
- Enable the server for the chats that should use its tools.
Web executable via Streamable HTTP. Remote servers run from the web runtime once configured in a workspace.
Use with an agent
Paste this prompt into Claude Code, Codex, Cursor or another agent — or into SourceWeft chat. The agent reads this server's install guide, shows you its publisher, endpoint and sign-in, and adds it to your client once you agree. Keys never go into a command.
Read https://sourceweft.com/mcp/ai.ekwo%2Fmcp/install.md and connect the MCP server it describes to my client. Before changing any config, show me its publisher, endpoint and sign-in method and wait for my OK. Never put an API key in a command or in a file I would commit; ask me where it should come from.Other MCP clients
Add this to your client's mcpServers config. The client completes OAuth sign-in on first connect.
{
"mcpServers": {
"ekwo-os-open-source-accounting-ledger-in": {
"type": "http",
"url": "https://mcp.ekwo.ai/mcp"
}
}
}README
@ekwo-ai/mcp
The Model Context Protocol server for Ekwo OS. It lets an AI agent work on the books in your own Postgres: read the ledger, raise an invoice, post it, match a payment, prepare the VAT return — as you, under the row level security of your own installation.
There are two ways to reach it, and they are the same server.
Hosted, at https://mcp.ekwo.ai/mcp
For a client that adds remote servers rather than launching them — claude.ai, or any other that speaks Streamable HTTP. Nothing is installed, and no password or key is written into a configuration file.
Point a client at that address and it discovers the rest. An unauthenticated
call is answered with 401 and a WWW-Authenticate header naming the
server's protected-resource metadata (RFC 9728); the client follows it to the
authorization server (RFC 8414), registers itself (RFC 7591, so it needs no
credentials of its own) and sends the person to their browser to approve the
connection. The scope asked for is books.
An agent reading this without a browser can start from
/.well-known/oauth-protected-resource.
The books stay where they are: the hosted server holds none, and connects to the instance the person chooses when they approve it. On the same screen they decide whether the agent may write; otherwise it reads. The hosted server is operated by Ekwo Cloud, and signing in there needs an account — a free one, opened with an address and a six-digit code, which also gives the web application on the same books and a nightly backup of every company it links.
Locally, over stdio
For a client that launches its own servers — Claude Desktop, Claude Code, any
editor that reads .mcp.json — against any installation, with no account.
Keep @latest in the command: from inside a clone of the repository, a bare
npx @ekwo-ai/mcp finds the unbuilt workspace package and answers
ekwo-mcp: command not found.
With the command line installed, ekwo mcp starts the same server — the same
entry point, runStdio(), the same flags (--autopilot, --review) and the
same environment: "command": "ekwo", "args": ["mcp"].
What it is, and what it is not
The server holds no privileges of its own. It signs in as the person using it, or is handed their access token, and everything it can do afterwards is exactly what that person can do: a viewer reads and cannot write, a member of one company cannot see another, a locked period refuses a posting. None of that is checked in this package — the policies and the triggers in the schema decide, and this server reports what they answered.
Three things it will never do:
- Write a ledger line. Every entry comes out of the schema's own functions, which carry the accounting rules. Direct inserts are for the objects a person types: contacts, draft documents and their lines, payments, bank transactions.
- Delete or edit a posted entry. No tool removes one. A posted invoice is undone the one way its country allows — back to draft where nothing about it has left, otherwise by the credit note that names it — and an entry keyed by hand by its reversal.
- Use a
service_rolekey. It would work, and that is the objection: it bypasses every policy, so the agent would answer for companies its user was never invited to. The server refuses to start with one.
Configuration
The recommended route: PostgREST, as the signed-in user
That block goes in claude_desktop_config.json for Claude Desktop, or in
.mcp.json at the root of a project for Claude Code. EKWO_ACCESS_TOKEN
replaces the address and the password when you already hold a session; with
the password, the session is kept in memory and refreshed, and nothing is
written to disk.
The fallback: a direct Postgres connection
For a self-hosted installation with no PostgREST in front of the database, or for tests.
EKWO_ACT_AS_USER_ID is required, and that is the whole point of this
mode. A database connection is nobody: auth.uid() is null, row level
security is bypassed rather than satisfied, and a server running that way
would be a way round the policies rather than a client of them. So every
query runs inside a transaction that sets request.jwt.claims to that user
and switches to the authenticated role, and the policies bind exactly as
they do over the API. This mode needs the postgres package installed
alongside the server; the recommended route needs no driver at all.
Started with nothing set
A client lists a server's tools as soon as it is added, and so do the
directories that index MCP servers. So with none of the variables above —
or with half of them — the server still starts, still answers the list of
tools, and every tool call returns a not_configured error that names the
variables to set and where. It connects nowhere until then, and it offers the
tools of the socle only: which modules an installation carries is read from
its database.
Only absence is forgiven. A service_role key, in either slot, and an
EKWO_ACT_AS_USER_ID that is not a uuid are values somebody wrote, and the
server still refuses to start with them.
The Dockerfile at the root of the repository builds the same server from the
checkout; docker run -i --rm <image> speaks MCP over stdio, with the variables
passed as -e.
Over HTTP: hosting it for others
The same server, reached by URL instead of started by a client:
handleHttpRequest answers the Streamable HTTP transport of MCP. It is
stateless — each request builds a server, answers in JSON and closes it,
with no session id and nothing kept — so it runs on a function platform,
behind a load balancer, or in one process. A GET is answered 405: this
server never speaks first, so there is no stream to open.
It does not decide who is calling: the host authenticates the request its own way, then hands in the connection that request may use.
A connection is one of two things, never both:
- A person's access token on the instance. Row level security decides, exactly as for that person in a browser.
- A key of Ekwo OS, as
create_api_key()issues it. It travels in theX-Ekwo-Api-Keyheader with noAuthorizationbeside it, the schema's pre-request hook presents it (decision 0062), and the caller is on that key's one company with that key's capabilities. A key issued without a capability that writes gives an agent that reads.
A service_role key is refused in all three slots, as on stdio. For
node:http (or anything built on it), handleNodeRequest(req, res, connection, { origin }) is the same handler.
Putting it in front of people: authorization
A remote MCP client expects the server to follow the authorization part of the MCP specification: OAuth 2.1 with PKCE, protected resource metadata (RFC 9728), authorization server metadata (RFC 8414) and dynamic client registration (RFC 7591). That belongs to the host, not to this package: the host signs the person in, issues its own tokens, and maps each one to a connection above. The database stays the only place that decides what may be read or written.
The tools
Every write names its company explicitly.
list_accounts answers with the working chart, not the whole one. A
country pack transcribes the regulation — sometimes more than a thousand
accounts — and a company works with a few dozen. The default is the accounts
with posted entries, those the company's settings or an enabled module point
at, and those somebody pinned with pin_accounts. include_all returns the
whole chart, and every write tool still accepts any account of it.
post_document, cancel_document, reverse_entry, record_payment,
update_document_lines, unreconcile, settle_from_statement, lock_period, opening_balance, import_books,
close_fiscal_year, reopen_fiscal_year, close_fiscal_year_finally, revoke_invitation, remove_member,
set_member_role, revoke_api_key and revoke_share are annotated destructive in the protocol, so a client can ask
before calling them.
What a tool may do is the capability the user holds, not the tool's own
right: the server acts as the person it signed in as, so post_document works
for an accountant and is refused to a viewer, by the database, with the
database's own words. get_company returns your_capabilities for exactly
that reason.
The modules. A module of this installation gets its own tools, under the
prefix its module.json declares, and the server reads ekwo.modules at
startup to know which: fixed_assets_list, fixed_assets_create,
fixed_assets_schedule, fixed_assets_run_depreciation,
fixed_assets_dispose, budgets_list, budgets_upsert_lines,
budgets_variance, einvoicing_validate, einvoicing_issue,
einvoicing_status, einvoicing_list, einvoicing_fiscalise,
einvoicing_fiscalisation_status, einvoicing_clearance, tax_estimate, tax_parameters,
tax_declare_parameter, tax_adjustments, tax_declare_adjustment,
tax_losses, tax_record_computation, tax_computations, crm_search,
crm_card, crm_segments, crm_board, crm_timeline, crm_upsert_card,
crm_log_activity, crm_join_segment, crm_leave_segment, crm_open_deal,
crm_move_deal, contracts_list, contracts_card, contracts_upsert,
contracts_schedule, contracts_issue_due, contracts_deadlines,
contracts_amend, contracts_from_template. A module may offer prompts too, under the same prefix:
tax_review_year, crm_morning_review. A module that is
not installed is not offered. When the project does not expose a module's schema yet, the tool says
which setting to change.
The electronic invoicing tools send through a transport the server is given,
never one a model chooses: the folder of EKWO_EINVOICE_DIRECTORY, or a
transport a host hands to buildServer() as einvoiceTransport. No
credential of a network ever reaches a tool. See
modules/einvoicing.
The fiscalisation tools chain, for a company whose pack declares a regime,
the record of each posted sale under the invoicing system the server is
given in EKWO_FISCAL_SYSTEM — never one a model describes.
Resources. ekwo://companies/{id}/chart is the whole chart of accounts;
ekwo://companies/{id}/taxes is every tax with the ledger account and the
declaration box each of its postings feeds.
Prompts. close_month walks the month-end checklist — drafts, unmatched
bank lines, the balance, the VAT, what is still open. prepare_vat_return
pulls the boxes and ties them back to the ledger before anything is filed.
What each tool does to the books
Every tool says it in its annotations, which a client reads to decide when to
ask first: it reads (readOnlyHint: true), it adds — a draft, a
contact, a note, a setting a person can change back (destructiveHint: false) — or it is destructive: it posts, cancels, replaces, removes, or
reaches outside the books, a key, a public link, a file sent
(destructiveHint: true). A tool registered without them does not compile,
and a test reads them on every tool a client is offered, the modules'
included. How to choose, and the rest of the rules a tool of this server
follows: TOOL_DESIGN.md.
Tools on demand
Every tool is offered at the start by default. With EKWO_MCP_TOOLS=on_demand
a session starts with the tools of a first session — status,
list_companies, get_company, list_accounts, describe_pack,
search_contacts, create_contact, list_documents, get_document,
create_document, post_document, record_payment, trial_balance,
general_ledger, vat_return — and two more:
A model then carries a short list rather than every description. A client that never reads the list again keeps the default.
Review or autopilot
Like the permissions of a coding agent: a default mode in the settings, a flag for one session, and a setting that forbids the flag. The database holds every path to it.
The server marks every transaction it opens as an agent's — ekwo.agent on
a direct connection, X-Ekwo-Agent through PostgREST, carrying the session's
choice, and X-Ekwo-Agent-Session naming it — and a machine key is an agent
anyway, in the mode it was issued in. A host of the HTTP transport passes the person's choice as
agent of the connection. Every posting by an agent is on the audit trail as
document_posted_by_agent, with the mode, the setting, the session and the
key. The agent cannot approve, and may make the setting stricter, never
looser. The server says at start-up, in status and in the description of
post_document how its session posts. Autopilot is the person's choice for
their session, never the agent's. See
decision 0089.
What an agent reads first
The instructions of the handshake are a first session in five steps:
list_companies, then get_company for the financial years and the lock
dates, then list_accounts and the taxes resource for the codes a line names,
then search_contacts and create_document, and post_document once the user
agrees. They say how to read vat_return, how a posted document or entry is
corrected (cancel_document, reverse_entry), and that a refusal comes with
its next step. The descriptions of those tools each name the tool to call next.
docs/agents.md is the same session for the person
setting the agent up, with the command line beside it.
On an installation that is not registered with Ekwo, the instructions carry
one more line: an invitation the agent may relay to the person once, at a
natural pause, with what registering gives and the command. The status tool
carries it as the field registration. Registering is optional and changes
nothing about what this server does; EKWO_NO_REGISTER_INVITE=1 in the
server's environment leaves the line out, and the hosted server never adds it.
The instructions also carry, once, the offer of a free
Ekwo Cloud account, for the agent to
relay after a first task to a person who has none: the web application on the
same books, a nightly backup of every company they link, and the connector by
URL above instead of a key and a password in a file. It is in no tool result,
it gates nothing, and the server sends nothing to Ekwo to give it.
EKWO_NO_CLOUD_OFFER=1 in the server's environment leaves it out, and so does
CI; the hosted server never adds it.
Conventions
- Amounts are decimal strings.
"1210.00", never a float. They go in that way and come back that way, becausenumericis exact and a float is not. - Dates are ISO,
2026-06-15. Identifiers are uuids. - Totals are computed by the database.
create_documentreturns the draft with the totals the schema derived, not with anything the caller supplied. - Refusals travel unchanged.
period_locked:,entry_unbalanced:,document_total_mismatch:and the rest arrive with the message the database raised, plus one sentence saying what it means. They are answers, not obstacles to route around. - A product fills a line in and never constrains it. A line naming
product_codetakes the catalogue's text, description, unit, price, account and tax; anything the line carries wins over that. What is already posted is never touched when the catalogue changes, and a product referenced by a line is retired withactive: falserather than deleted. - A missing tax is a missing tax. A line with no tax books a base with no
VAT box, which is not the same as 0 %. A missing account is different: it
can only mean "resolve it", because a product line with no account is
refused by a check constraint. So a line may leave
account_codeout, and the database fills it — the company default, then the country model.
Testing it by hand
The automated tests run every tool against the real schema in Postgres
compiled to WebAssembly (tests/mcp/), refusals included. To exercise
PostgREST and Supabase Auth too, install on a project you can throw away
(npx -y ekwo-os@latest init), point a client at it with the block above, and
try a first session:
- "List my companies." The company you created, with
your_role: owner. - "Create a customer, then invoice them 1 000 for consulting at the
standard rate."
create_contact, thencreate_document; the totals in the answer are computed by the database. - "Post it."
post_document, which takes the next number of the sales journal. - "They paid 500 on the 10th."
record_payment; the invoice becomes partially paid. - "Show me the trial balance and the VAT for the quarter."
trial_balanceandvat_return. - "Lock June."
lock_period; anything dated in June is then refused withperiod_locked:.
A company installed from its country pack has bank and cash journals already
wired to their accounts, so record_payment works with nothing else set up;
create_bank_account registers the real account.
Licence
AGPL-3.0-only © Ekwo AI.
Source: packages/mcp/README.md at commit 5581a1b
Tools
0Version history
10- v0.24.0LatestOct 29, 2026
- v0.16.0Oct 19, 2026
- v0.15.1Oct 19, 2026
- v0.13.0Oct 15, 2026
- v0.12.2Oct 14, 2026
- v0.12.1Oct 11, 2026
- v0.12.0Oct 10, 2026
- v0.9.0Sep 28, 2026
- v0.8.0Sep 23, 2026
- v0.7.0Sep 22, 2026
Score
85% of the applicable checks met
How complete this listing is, computed from the registry entry, the endpoint and public sources. Not a rating of the server and not a statement that it is safe or working today: reachability is a dated check, and no installs or reviews go into the score.
- Reached in the last 30 daysRequired25 pts
- Auth declaredRequired10 pts
- Tools listed15 pts
Answer tools/list without a credential, or ship a package that starts in a sandbox without one.
- DescriptionRequired15 pts
- Logo10 pts
- Documentation15 pts
- License10 pts