Zeno
AI bookkeeper for Swiss small companies: books invoices, reconciles the bank, keeps VAT current.
| Endpoint | https://app.zeno-works.ch/mcp (streamable-http) |
|---|---|
| Registry version | 0.1.0 |
| Now | 51 tools · grade D (55) · pin ts_ed9f728ac88f1d874de9dfafe83b73b3 · protocol 2025-06-18 |
| First / last listed | 2026-10-26 02:22 UTC / 2026-10-26 02:22 UTC |
| Grade over time | 2026-10-26: D (55), 51 tools |
Is this your server? Own a server? ToolFit hosted watch re-checks it every 24 h ($0.25 per 30 days) or 6 h ($0.60) and sends you an HMAC-signed webhook when its tools or grade change: POST https://api.youraitools.co/toolfit/v1/watch. To opt out, see About & opt-out.
Tools: find_operations describe_operations call_operation request_api_key start_signup collect_api_key whoami list_companies get_company global_search get_kpi_report get_work_list list_autopost_decisions accept_autopost_decision upload_intake upload_document get_intake list_items get_item gl_trial_balance gl_profit_and_loss gl_balance_sheet list_gl_accounts get_gl_account_ledger list_gl_periods list_gl_open_items list_gl_journal_entries get_gl_journal_entry create_gl_journal_entry post_gl_journal_entry reverse_gl_journal_entry list_bank_accounts list_bank_entries get_bank_entry entry_candidates settle_entry reconciliation_summary list_ar_invoices get_ar_invoice create_ar_invoice issue_ar_invoice list_counterparties list_gl_vat_periods list_payroll_runs list_payroll_employees get_financial_statements customer_statement get_bank_status zeno_info zeno_overview onboarding_guide
find_operations | Search EVERY operation this server offers, including the ones not in your tool list.
The tool list you can see holds only the common operations. Several hundred
more — payroll, VAT returns, year-end close, fixed assets, expenses, cash
book, settings, integrations, onboarding — are available but unlisted, and
this is how you find them. **Search here before concluding that Zeno cannot
do something. params: query, tag, limit, offset |
describe_operations | Get the full argument schema for operations found via ``find_operations``.
Returns each operation's description and JSON-Schema arguments — everything
a listed tool would have shown you.
``include_output_schema`` is the ONLY way to obtain an operation's result
shape: ``tools/list`` publishes no output schema for any operation, listed or
not (they are 67 % of the payload, and a schema describing params: names*, include_output_schema |
call_operation | Run any operation by name — the ones in your tool list and the unlisted ones alike.
``name`` is an operation name from ``find_operations``; ``arguments`` is the
object described by ``describe_operations`` (omit it for an operation that
takes none). The result, and any error, are exactly what calling the
operation directly would give you. params: name*, arguments |
request_api_key | Ask the user to hand this session a Zeno API key. **Use this first** — every
current Zeno user already has an account.
`email` is the address on their Zeno account. **Ask them for it**; you cannot
guess it, and the request is aimed by it — Zeno will only show it to somebody
signed in as the owner of that address. Pass `client_name` too: it is the only
thing identifying you on the approval page, a params: email*, client_name |
start_signup | Create a Zeno account for somebody who does not have one yet.
Use `request_api_key` first if they might already have an account — this tool
cannot tell you, and deliberately never will.
We email `email` a verification link. The user opens it, sees that you asked
(by the name in `client_name`, shown as an unverified claim), chooses their own
password, and decides separately whether to connect you params: email*, client_name |
collect_api_key | Collect the key once the user has approved.
`status` is one of:
- `pending` — nobody has approved yet, or (with `throttled: true`) we were
rate-limited and learned nothing. **Wait `poll_interval_seconds`, then call
again** — every answer carries it. Do not ask the user to tell you when they
are done: you cannot detect it any other way, and handing the wait back to the
user is how these s params: claim_token* |
whoami | Which user this request acts as, and which company's books it is bound to (jwt or API key).
Zeno serves every tenant from one host, and an API key is opaque — nothing else
tells a connected agent whose books it is about to write. Call this FIRST when
working over MCP: ``company_scoped: true`` means this connection's data access
is confined to ``company_id`` — ``company_name`` names it (null here params: none |
list_companies | List companies (jwt, scoped). Non-admins see only their member companies. params: none |
get_company | Get one company (jwt, scoped). 403 for a non-member non-admin, 404 if absent. params: company_id* |
global_search | Full-text search across emails + items, ranked by ``ts_rank_cd``, scoped (A1).
``archive_status`` constrains item results only (the Documents → Archive page
passes ``archived`` so its search box never surfaces pending/triage-parked items). params: q*, type, source, direction, archive_status, company_id, limit, offset |
get_kpi_report | Snapshot KPIs + 13-week liquidity forecast, from the GL (CompanyMember).
Cash/liability balances aggregate the FY window of posted/reversed journal lines;
aging + forecast read ``gl_open_item``, pending salary batches and unpaid VAT
periods. Read-only. Explicit non-member ``company_id`` → 403.
``fiscal_year`` optional → latest FY with journal data; FY figures as of that
year's end once past. Agi params: company_id*, fiscal_year |
get_work_list | Everything waiting for this user in this company, most severe first. params: company_id* |
list_autopost_decisions | Recent AI-accountant autonomy decisions (CompanyMember) — the activity feed.
Shows what the autonomy policy posted (``posted``), would have posted in
``assist`` shadow mode (``shadow``), or held for human review (``held``).
``ai_no_match`` shadow rows (one per AI-consulted bank entry with no verdict)
are feed noise and hidden by default; ``include_no_match=true`` surfaces them
for audit. ``outcom params: company_id*, limit, offset, include_no_match, outcome, source_ref_id, cause |
accept_autopost_decision | One-click accept of a **held** decision (accountant) — posts the AI's proposal.
A person's accept in the app replaces the soft guardrails (confidence, amount cap,
risk flags). An agent's (MCP, API key, chat) is refused (403) unless the row is
``agent_acceptable``. Closed period, sealed year, unknown account: 409/422. The row
flips ``held → posted`` (a second accept is a 409), with ``accepted_by_k params: company_id*, decision_id*, body |
upload_intake | Upload ANY file and let the system decide what it is (the unified intake
queue — the same door as "Documents → Upload" in the web app).
``file_base64`` is the file's raw bytes, base64-encoded.
Prefer this whenever the file is not plainly an invoice, and always for:
bank statements (CAMT.053 XML, CSV, PDF or Excel exports) — these become
BankStatement + BankEntry rows, matched to the registered ba params: company_id*, filename*, file_base64* |
upload_document | Upload an INVOICE or RECEIPT (PDF, image, or .zip of them) for analysis.
``file_base64`` is the file's raw bytes, base64-encoded. ``direction`` is
``payable`` (incoming supplier invoice) or ``receivable`` (outgoing).
Creates Item(s) and queues extraction.
This door is invoices-only: it never routes a file onward by type, so a
bank statement, a journal export or a balance sheet sent here is analys params: company_id*, filename*, file_base64*, direction |
get_intake | Get one intake row by id (scoped to its company: 404/403 split). params: intake_id* |
list_items | List/filter items, scoped to the caller's member companies (A1).
Optional ``period`` (``YYYY-MM``) filters by the item's effective date
(invoice date, falling back to the Europe/Zurich-local ingestion date); a
malformed value returns 422 ``{"msg": "period must be YYYY-MM"}``.
``exclude_triage_status`` drops items carrying that triage verdict (NULL-safe)
— the Payables/Receivables pages pass ``unr params: source, direction, archive_status, triage_status, exclude_triage_status, counterparty_id, company_id, period, paid, posted, duplicate, credit_note, q, limit, offset |
get_item | Item metadata (any source). params: item_id* |
gl_trial_balance | Per-account debit/credit columns for one fiscal year, with totals.
``fiscal_year`` (year label) defaults to the latest year posted in; ``period_id``
narrows to one period of it. No all-years mode — account identity is
``(company, fiscal year, code)``, so aggregating double-counts silently (ZET-155).
Unknown year/period → 404; period outside the year → 422.
Cut by the line's stamped ``fiscal_year params: company_id*, period_id, fiscal_year |
gl_profit_and_loss | Revenue − expense → net result, for one fiscal year.
**Window** — same rules as the trial balance: ``fiscal_year`` defaults to the latest
posted year, ``period_id`` narrows to one period of it, unknown values are 404 and a
contradictory pair 422. A P&L is a **flow** statement, so no reading makes the sum
of every fiscal year a company's result — hence no all-years mode (ZET-155). Opening
batches params: company_id*, period_id, fiscal_year |
gl_balance_sheet | Assets vs liabilities + equity for one fiscal year, signed by natural side.
Window rules as the trial balance (``fiscal_year`` defaults to the latest posted
year; no all-years mode, ZET-155). With ``period_id`` this is a *movements*
aggregate over that period, not a position.
⚠️ Balances as ``assets == liabilities_and_equity``, NOT ``liabilities + equity``:
the GL has no P&L→equity closing entry params: company_id*, period_id, fiscal_year |
list_gl_accounts | List the postable chart (CompanyMember). ``fiscal_year`` filters to one year's chart
(the chart is FY-scoped, so codes repeat across years). params: company_id*, fiscal_year, limit, offset |
get_gl_account_ledger | One account's ledger (Kontoauszug) — every booking that touched it, in date
order, with Soll/Haben and the running balance after each line (CompanyMember).
No fiscal-year parameter: the chart is FY-owned, so ``account_id`` already
fixes the year. Posted **and** reversed entries are included (a reversal is a
second posted entry, so both legs must show or the balance is wrong); drafts
are not. The params: company_id*, account_id*, date_from, date_to, limit, offset |
list_gl_periods | The company's **accounting periods** — the months a journal entry may post into
(CompanyMember read).
A period belongs to one fiscal year and carries ``period_no`` (1..12 within the
year), ``date_from``/``date_to`` and a ``status``: ``open`` accepts postings,
``soft_closed`` still accepts them, and ``closed``/``year_closed`` refuse them — a
posting date inside a closed period fails until an accou params: company_id*, fiscal_year_id |
list_gl_open_items | One page of the AR/AP subledger (member read).
``direction`` filters server-side (ZCT-577) — omit it for both. The screen used to
filter the fetched page in the browser, which made the AR chip drop rows and the
total count both directions.
``as_of`` (ZCT-577) reconstructs each item's open amount from posted journal lines
dated on or before that date, instead of reading today's cached amount. Ite params: company_id*, status, direction, as_of, limit, offset |
list_gl_journal_entries | List journal entries.
``period_ids`` narrows to specific fiscal periods, and is how a caller asks for
**one fiscal year** — the year's period ids (ZET-285). Without it this route
listed, and counted, every fiscal year the company has: the Journal page cut the
year out of the fetched page *client-side* while ``total`` stayed company-wide,
so a year whose entries sat past the first page rendered as params: company_id*, status, period_ids, untagged_subledger_control, q, sort, order, limit, offset |
get_gl_journal_entry | One entry + lines, plus the subledger rows it produced/touched (Gap B detail
surfacing): its ``gl_vat_posting`` rows, its ``gl_vat_zero_posting`` turnover-only
declarations (zero-rated/exempt output — R2-13b: these have no VAT line to pair, so
without them a correctly coded 0 % line is indistinguishable from an untagged one)
and referenced ``gl_open_item`` rows. A declared base line also carries t params: company_id*, entry_id* |
create_gl_journal_entry | Create a **draft** journal entry by hand — accountant. It carries NO source document
unless you name one.
Only the pipeline paths stamp ``source_ref_type``/``source_ref_id`` (an ingested
document is ``'item'``), so by default a booking made here cannot be traced back to
what justified it. ``document_id`` is the retrofit: name an existing ``gl_document``
(create one with ``POST …/gl/documents``) a params: company_id*, period_id*, posting_date*, currency, entry_type, description, lines*, auto_reverse_date, document_id |
post_gl_journal_entry | Post a draft entry into the books — accountant. One-way.
A posted entry can no longer be replaced or discarded (both refuse with 409); the only
general correction is ``…/reverse``, which leaves both the original and the reversal
visible. The one exception is ``PUT …/guessed-account``, which re-points a
``coding_guessed`` entry's expense account in place — and it works *only* on a posted
entry. Ch params: company_id*, entry_id*, body |
reverse_gl_journal_entry | **Storno** a posted journal entry: post its mirror image (accountant role).
Nothing is deleted; the original stays, linked both ways, and the **reversal** entry
is returned. The only VAT-correct storno door — VAT postings, open items and what
the document spawned are all unwound.
``body.posting_date`` dates the reversal and decides its period; omitted, it reuses
the original's date and period.
params: company_id*, entry_id*, body |
list_bank_accounts | The company's bank accounts — the ``bank_account_id`` every statement import,
bank-entry list and reconciliation call needs (CompanyMember read).
Oldest first, no filter and no paging. Each row carries the ``iban``, ``currency``,
``label``/``bank_name`` and ``ledger_account_number`` — the chart code this account
posts through; **without one a bank movement cannot be coded**, and it is set with
`` params: company_id* |
list_bank_entries | List Bank Entries params: company_id, bank_account_id, bank_statement_id, reconciliation_status, settlement_status, match_quality, flag, credit_debit, counterparty_id, from, to, q, limit, offset |
get_bank_entry | Get Bank Entry params: entry_id* |
entry_candidates | Entry Candidates params: entry_id* |
settle_entry | Post this reconciled bank entry into the GL (clean-bank settlement).
Matched → Dr Bank / Cr AR; unmatched → Dr Bank / Cr Suspense; a previously-suspensed
entry now fully matched → Dr Suspense / Cr AR. Idempotent, and a no-op (``skipped``) when
the GL chart is not configured for native settlement. The matcher still owns
``reconciliation_status``. See doc/architecture.md §"Posting general ledger". params: entry_id* |
reconciliation_summary | Reconciliation summary banner.
Optional ``from``/``to`` scope the **per-entry bank-entry counts** (total /
matched / missing-invoice / …) to a ``booking_date`` window — mirroring the
``/bank-entries`` grid filter — so the banner agrees with a month-filtered table.
The cross-month coverage (``bank_accounts`` gaps / ``missing_periods``) stays
**global**: gap detection spans statements regardless of params: company_id*, from, to |
list_ar_invoices | List authored AR invoices, newest first.
Optional ``status`` / ``doc_type`` filter by lifecycle + document kind.
Optional ``period`` (``YYYY-MM``) filters by the invoice's issue date (its
month); a malformed value returns 422 ``{"msg": "period must be YYYY-MM"}``. params: company_id*, status, doc_type, period, limit, offset |
get_ar_invoice | One authored outgoing invoice with its lines (CompanyMember read).
Carries the header (customer, dates, currency, discounts), every line with its
computed ``net_amount``/``vat_amount``/``gross_amount``, the document totals, and
the lifecycle: ``status`` (``draft`` → ``issued`` → ``cancelled``), ``document_no``
(assigned at issue — ``null`` on a draft), ``commercial_document_id`` /
``snapshot_docu params: company_id*, invoice_id* |
create_ar_invoice | Create an outgoing (sales) invoice as a **draft** — nothing is booked yet
(accountant role).
A draft changes no ledger balance, gets no invoice number and appears in no VAT
return; it exists so it can be edited. Posting happens later, at
``POST …/ar/invoices/{invoice_id}/issue``.
Body: ``issue_date`` (required), ``counterparty_id`` (the customer — required
before it can be issued), ``currency``/ params: company_id*, doc_type, counterparty_id, issuer_profile_id, issue_date*, due_date, service_period_start, service_period_end, currency, language, payment_terms_id, invoice_discount_kind, invoice_discount_value, footer_text, legal_text, notes, credited_invoice_id, lines |
issue_ar_invoice | Issue a draft — **this is the booking step** (accountant role).
In one transaction it assigns the next invoice number, posts the journal entry
(debit the AR control account, credit revenue per line, credit output VAT per VAT
code), opens the receivable open item the customer's payment will settle, freezes
the invoice against further edits (``status`` → ``issued``) and renders the PDF+QR
snapshot. params: company_id*, invoice_id* |
list_counterparties | List a company's counterparties (filter by ``kind``/``q``, paginated).
``kind`` repeats to filter on a set (``?kind=customer&kind=both``) — a
role-shaped picker asks for every kind that plays the role. A single
``?kind=supplier`` keeps its original meaning. params: company_id*, kind, q, limit, offset |
list_gl_vat_periods | List Gl Vat Periods params: company_id* |
list_payroll_runs | The company's payroll runs — one per wage period — newest period first
(CompanyMember read; the money is aggregate, no per-employee PII).
A run walks ``draft`` → ``calculated`` → ``approved`` → ``posted`` → ``paid`` →
``locked`` (plus ``reversed``); ``run_kind`` separates a ``regular`` run from a
correction/reversal, which are always **separate runs**, never in-place edits, and
are linked by ``or params: company_id*, status_filter, year |
list_payroll_employees | Every employee on the payroll master data, with today's contract (accountant
role — this payload is personal data, so it is not a plain member read).
One row per employee, active and left alike (filter on ``status`` /
``employment_end`` yourself — no server-side filter, no paging): master data
(``employee_no``, ``display_name``, address, ``ahv_number``, ``birthdate``), the
Quellensteuer election params: company_id* |
get_financial_statements | Computed balance sheet + P&L for a fiscal year, from the journal (CompanyMember).
Aggregates every posted GL journal line whose **posting date** falls in the fiscal
year's window, classifies each account against the chart of accounts, and reports
problems (unbalanced trial balance, unclassified accounts, year-not-opened, mixed
currency). **Read-only and distinct** from the uploaded balance-sheet params: company_id*, fiscal_year, month |
customer_statement | One customer's open items + totals (the aging row's drill-down).
``as_of`` mirrors ``GET …/ar/aging`` — set, it reconstructs the items open at
that date so the statement agrees with a historical aging report. params: company_id*, counterparty_id*, as_of |
get_bank_status | Composed bank snapshot — "is my banking up to date?" (CompanyMember). "Reconciled" means
entries matched, NOT ledger = statement balance (the close check
``BANK_BALANCE_DOES_NOT_TIE_OUT`` compares that).
One call for what otherwise takes ``GET …/bank-accounts`` + one
``GET /bank-accounts/{id}/coverage`` per account + ``GET …/reconciliation/summary``
— and which those three still do not answer: pe params: company_id* |
zeno_info | What this server is, which MCP revision it speaks, and how to get a credential.
**Needs no credential.** Call it first if a tool has just refused you: the
`authentication` block names the tools that can get this session connected. params: none |
zeno_overview | What Zeno does, in one page — the platform's capabilities and its limits.
**Needs no credential.** Read this before telling a user that Zeno cannot do
something; it covers VAT, payroll, reconciliation and the AI accountant. params: none |
onboarding_guide | How to onboard and create a NEW company: what to ask the user for, which
documents to request, and the order to run the setup in.
Discovery does not depend on search: this tool is in the tier, so it is listed for
every caller, and `BASE_INSTRUCTIONS` names it. `find_operations` does reach it
("new company setup" ranks it first), but a query made only of common words is
still decided by whichever params: none |
Entries describe changes in tool definitions. They are not statements about a server, its quality or its author. The grade measures how the tool definitions fit model providers (token cost, schema compatibility, confusable names) as scored by the ToolFit engine at the time of the listing. JSON: append .json. Opt-out: how or hello@youraitools.co.