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-Authenticate header 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:read scope. 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.