An MCP server for Enty.io — invoices, bank transactions, accounting deadlines, and contracts, exposed as tools your AI agent can call.
Enty has no public API. This server drives the same GraphQL endpoint the Enty web app uses, authenticated with your browser session token. That comes with caveats you should read before using it:
Unofficial. This project is not affiliated with or endorsed by Enty. It talks to an undocumented internal API that can change or break at any time, and using it may not be covered by Enty's terms of service. It reads your real company data — there is no sandbox. Two tools also write to it (catalog items only); nothing deletes.
| Tool | What it returns |
|---|---|
enty_get_me |
Logged-in user + active company |
enty_list_companies |
Companies on the account |
enty_list_invoices |
Invoices with status filter, totals, per-status counts |
enty_list_counterparties |
Clients/providers with tax details |
enty_list_items |
Catalog items (invoice line items) |
enty_list_item_units |
Units of measure an item may use |
enty_list_transaction_documents |
Documents already attached to a transaction |
enty_list_invoice_payment_candidates |
Transactions that could be an invoice's payment |
enty_list_bank_accounts |
Bank accounts with IBAN and connection status |
enty_list_transactions |
Transactions with account/date/direction/text filters |
enty_get_transaction |
One transaction with note and category |
enty_get_balance |
Combined balance of chosen accounts |
enty_get_transactions_statistics |
Document-matching stats for a date range |
enty_list_categories |
Transaction categorization tree |
enty_list_accounting_periods |
Accounting periods |
enty_get_period_deadlines |
Next tax/reporting deadline for a period |
enty_get_potential_tax |
Estimated tax for a period, by rate (EUR) |
enty_get_period_issues |
Open bookkeeping issues for a period |
enty_list_contracts |
Contracts with e-sign lifecycle status |
enty_list_deals |
Deals with status and counterparty |
enty_get_deals_stats |
Expected vs received deal amounts |
| Tool | What it does |
|---|---|
enty_create_item |
Create a catalog item (product/service) |
enty_update_item |
Update a catalog item's name, price, currency, unit, or description |
enty_create_deal |
Create a deal for a counterparty |
enty_create_invoice |
Create a draft invoice with line items |
enty_mark_invoice_paid |
Mark an invoice paid |
enty_link_transaction_to_invoice |
Record a bank transaction as an invoice's payment |
enty_attach_document_to_transaction |
Upload a local file and attach it to a transaction |
These write to your live company. Two risk levels are worth separating.
Catalog items and deals are reference data. Creating or editing one does not alter invoices that already reference it.
The last three touch bookkeeping. Linking a transaction to an invoice and attaching documents change records your accountant works from, and a wrong link reads as a reconciliation error. None of them can be undone here, so check ids before calling.
enty_create_invoice leaves a draft. Nothing is sent to the client and no PDF is
generated; review and issue it in the Enty web app. It takes four calls, because Enty
builds an invoice that way: create a blank draft, patch the header, add the lines,
store the totals. If a later step fails the draft survives, and the error carries its
id so you can finish or discard it by hand.
enty_link_transaction_to_invoice needs the invoice to have a generated document,
since the link is between that document and the transaction. Enty has no separate
payment record. Open a fresh invoice once in the web app if the tool reports it has
none.
enty_attach_document_to_transaction is the one path that is not GraphQL. Bytes go to
Enty's file-storage service as multipart form data, the returned file is registered as
an accounting document, and only then is it linked to the transaction.
Nothing in this server deletes. Enty's API has 233 mutations, 19 of them deletions
(deleteItemsAtOrganization, DeleteTransactions, DeleteUserCompanies, and so on).
None are exposed, and the GraphQL client refuses to send any mutation whose operation
name is not in an explicit allowlist (ALLOWED_MUTATIONS in src/enty_mcp/graphql.py),
so a blocked write never leaves the process. Deleting anything is a manual action in the
Enty web app, on purpose.
enty_update_item merges: pass only the fields you want changed. Enty's update replaces
the whole item, so the tool reads the item's current values first and sends them back
alongside your changes. Without that, updating one field would blank the rest.
The rest of the mutation surface is mapped (spec/graphql/bundle-operations/) but
unexposed. Adding a write means adding its name to the allowlist, which is the review
step.
Two options. Pick one.
Email and password (ENTY_EMAIL, ENTY_PASSWORD). The server logs in for you,
keeps the session token in memory, and logs in again by itself when the session
expires. Nothing to do when a token dies, which is what you want for a server meant
to run unattended. Enty issues no refresh token, so renewing really is a fresh login.
A session token (ENTY_AUTH_TOKEN). Log into app.enty.io,
open DevTools → Network, click any request to /api/, and copy the value of the
enty-auth request header. This expires, and without credentials the server cannot
renew it, so you repeat the steps whenever tools start failing.
Set both and the token is used first, falling back to the credentials once it expires. That skips a login on startup while still surviving expiry.
Either way the secret grants full access to your Enty account, so treat it like the
password it effectively is. Credentials and tokens are held as pydantic SecretStr,
kept out of the client's shared headers, sent only to your configured ENTY_BASE_URL,
and never written to logs. The account email is logged at INFO when a login happens,
so you can tell which account a running server is using.
Accounts with multi-factor authentication cannot use email and password. Enty answers those logins with no session id, and the server says so rather than failing obscurely. Use a token for those.
git clone https://github.com/appsome/enty-mcp-server
cd enty-mcp-server
cp .env.example .env # paste your token
uv sync
uv run enty-mcp # streamable HTTP on :8001/mcp
uv run enty-mcp --transport stdio # for stdio clientscp .env.example .env # fill in ENTY_EMAIL and ENTY_PASSWORD
docker compose up -d
# server: http://localhost:8001/mcp health: http://localhost:8001/healthThe compose file builds from source. To run a published image instead:
docker run -d -p 8001:8001 \
-e ENTY_EMAIL=you@example.com -e ENTY_PASSWORD=... \
ghcr.io/appsome/enty-mcp-server:latestImages are built for linux/amd64 and linux/arm64 and pushed to GitHub Container
Registry on every push to main, tagged latest and with the commit sha. Pushing a
v* tag adds the matching semver tags. The container exits immediately with a clear
error if neither credentials nor a token are set.
| Env var | Default | |
|---|---|---|
ENTY_EMAIL / ENTY_PASSWORD |
— | credentials; server logs in and renews itself |
ENTY_AUTH_TOKEN |
— | session token; alternative to the above |
ENTY_BASE_URL |
https://app.enty.io |
|
ENTY_LOCALE |
en |
|
ENTY_TIMEOUT_S |
30 |
|
MCP_HOST / MCP_PORT |
0.0.0.0 / 8001 |
HTTP transports |
LOG_LEVEL |
INFO |
OpenHands (docker compose): use docker-compose.override.example.yml to add the
service to your stack, then point OpenHands at http://enty-mcp:8001/mcp — see
openhands/mcp.json and openhands/config.toml.snippet.
Claude Code:
claude mcp add --transport http enty http://localhost:8001/mcpClaude Desktop (stdio):
{
"mcpServers": {
"enty": {
"command": "uv",
"args": ["run", "--directory", "/path/to/enty-mcp-server", "enty-mcp", "--transport", "stdio"],
"env": { "ENTY_EMAIL": "you@example.com", "ENTY_PASSWORD": "..." }
}
}
}Enty's GraphQL endpoint has introspection disabled, so the schema was reconstructed
from two sources: a HAR capture of the web app (56 operations with response shapes,
spec/graphql/operations/) and the app's JS bundles, which contain every GraphQL
document the frontend can send (434 operations, spec/graphql/bundle-operations/).
spec/graphql/NOTES.md documents the conventions and the remaining gaps. The
extraction scripts in scripts/ are repeatable when the app changes.
uv sync
uv run pytest # respx-mocked, no network
uv run ruff check src tests scripts
uv run pyrightTests never touch the real API. Fixture data is synthetic, shaped like recorded responses.
MIT