MCP server
Connect a model to your statements. Four read-only tools over the same endpoints as the REST API. Log in to connect, or use an API key.
https://www.extractbankstatements.com/api/mcp
MCP is how a model reaches a service directly, rather than being handed a file and asked to read it. Connect this one and you can ask questions of your statements in the place you are already working: what you spent somewhere last month, whether a statement reconciled, which rows need looking at.
It is the same product as the REST API, calling the same code. A number that comes back through a tool is the number the API returns.
Included in Pro and Business, like the API.
Connect it#
Two ways in, and the first is the one you want.
Log in#
Give a client that speaks OAuth the URL above and nothing else. It will send you here to sign in and approve it. There is nothing to copy, nothing to paste into a config file, and nothing sitting in one afterwards.
https://www.extractbankstatements.com/api/mcp
The client registers itself, asks for the statements:read scope, and gets a
token bound to this endpoint alone. If you connect a second time it remembers
that you already agreed.
In Claude Code:
claude mcp add --transport http extractbankstatements \
https://www.extractbankstatements.com/api/mcp
An API key#
For a client that does not speak OAuth, and for a script driving MCP directly. The same key the REST API takes, from your API keys page, as a bearer token.
claude mcp add --transport http extractbankstatements \
https://www.extractbankstatements.com/api/mcp \
--header "Authorization: Bearer sk_live_..."
Or in a JSON config:
{
"mcpServers": {
"extractbankstatements": {
"type": "http",
"url": "https://www.extractbankstatements.com/api/mcp",
"headers": {
"Authorization": "Bearer sk_live_..."
}
}
}
}
The transport is Streamable HTTP and the server is stateless, so there is no session to keep alive and nothing to run locally.
The tools#
| Tool | What it does |
|---|---|
list_documents |
Your documents, newest first, each with its verification. Takes an optional limit, up to 100. |
get_document |
One document: period, currency, opening and closing balance, and its verification. |
get_transactions |
Every transaction on a document, in order, each with its own verification, and the document it came from. currency narrows them to one of the accounts a statement holds. |
export_document |
The document as a file, for when you want the rows rather than an answer about them. format is csv or ofx, and currency picks one account. |
Every tool takes the document id that list_documents returns.
export_document is the one with a second argument. csv is the thirteen
columns a person reads in a spreadsheet. ofx is a bank statement in the
format Xero, QuickBooks, Sage, FreeAgent, Wave, MYOB and GnuCash import, so an
assistant can hand somebody a file that goes straight into their books rather
than a table they have to map. It is refused, with the reason, for anything
that is not a bank statement;
exporting to an accounting package says why. There is no
xlsx here because a workbook is not text.
A statement holding several currency accounts is refused only until you say
which one: an OFX statement describes one account in one currency, so call
export_document again with currency set to each of the codes the refusal
names, and you get one file per account. The same argument works on
get_transactions, for when the question is about the euro rows alone.
What a model should do with the answers#
The server tells the model this when it connects, and it is worth knowing yourself, because it is the difference between this product and a converter.
Gate on verification, not on status. A document can finish processing
without being provable. verified means every figure was checked against the
document's own arithmetic and agreed. contradicted means it was checked and
disagreed. not_verifiable means the document holds no arithmetic that could
check it, which is ordinary for an invoice or a receipt.
Every one of the three still returns the rows. None of them is a refusal.
A model that tells its user a document could not be processed because it came
back not_verifiable or contradicted has withheld an answer it was given.
A row with needs_review: true was contradicted, not merely unproved.
review_reason says what we found. It should be surfaced, not quietly summed
over.
Say whose reading is in doubt. verification_summary distinguishes a
document that disagrees with itself from a figure we probably misread, and on
a contradicted document it is usually the second. Report it as written rather
than translating it into "this document is invalid".
A not_verifiable figure should be reported as read and not confirmed, and
NOT as a problem with the document. Nothing found a problem. A model that
describes an ordinary invoice as failing a check has told its user something
untrue about their paperwork, which is a worse failure than saying nothing.
Check kind before summing. A statement's amounts are signed by direction,
so they add up to a net movement. An invoice's are as printed, because the
document does not say which way round it is, so summing a mixed list of the two
produces a number that means nothing.
Amounts and balances are strings. "-25.63", not -25.63. They are exact
decimal values, and 0.1 does not survive a binary float. A model that turns
them into numbers to add them up has reintroduced the error this product
exists to catch.
A null tax is not a zero. tax_total: null means the invoice stated no tax
or none could be read. A stated zero is "0.00", and tax_treatment says why
it is zero where the document says: reverse_charge or exempt. Reporting the
first as "no VAT was charged" is a claim about somebody's tax position that the
document did not make.
A tax figure is never computed. tax_amount on a line is what the line
printed, and it is null where the line printed a rate and no tax. If a user
wants the missing figure, work it out and say that is what you did, rather than
reporting it as though the supplier had written it down.
Authentication#
Either an OAuth access token, obtained by the flow above, or an API key as
Authorization: Bearer sk_live_…. Both resolve to the same account and reach
the same tools.
An OAuth token is narrower than a key, which is the argument for preferring
it. It carries the statements:read scope and an audience naming this
endpoint, so it is useless anywhere else, and you can withdraw a client's
access without touching anything else you have connected. A key is one
credential for the whole API.
Two failures are worth telling apart:
- 401 means we do not know who you are: no credential, or one that is
revoked or wrong. The response carries a
WWW-Authenticateheader naming where to authenticate, which is how an OAuth client starts the flow without being told the address. - 403 means we know who you are and something about the request is not
allowed: usually a plan without the API, sometimes a token missing the
statements:readscope. Retrying with the same credential will not help. A plan is fixed on the billing page; a scope is fixed by reconnecting.
Reading, not writing#
There is no upload tool, on purpose.
A statement is a multi-megabyte PDF on your disk, and MCP has no good way to move one yet. Base64 in a tool argument fills the context window with a file the model cannot read. A local path means nothing to a server running in Germany. A presigned URL needs the client to upload a file it was never told to open.
The file uploads working group is drafting the missing piece: a declarative file input, so a host can show you a normal file picker and send what you choose. When that ships, uploading arrives here.
Until then: upload in the app, which takes a moment and happens once per statement, then read from wherever you are working.
Discovery#
The server describes itself, so a client that supports discovery does not need any of the above typed in.
GET /.well-known/mcp/server-card.json
The OAuth documents are at
/.well-known/oauth-authorization-server
and
/.well-known/oauth-protected-resource/api/mcp,
which is where the WWW-Authenticate header on a 401 points.
Every page also advertises the server in a Link header, alongside the
API catalog
and the OpenAPI description.