For developers
docstore is MCP first. There is no SDK to learn and no REST surface to wrap, because the thing calling us is a model and models already speak MCP. This page is what is actually built today, and it says where the gaps are.
Streamable HTTP, stateless. /mcp binds the session to your only account automatically, which is the case almost everybody is in. /mcp/<slug> pins one account explicitly, so a bookkeeper with five clients configures five endpoints and each session's tool list is exactly that client's truth. ?account=<slug> on the bare mount does the same thing.
# Claude Code, Claude Desktop, or any other MCP client $ claude mcp add docstore https://docstore.ai/mcp # one client account per customer $ claude mcp add acme https://docstore.ai/mcp/acme
Every request carries Authorization: Bearer <token>. The token is an access token from our own sign-in, the same login the web app uses, and your client gets it by doing nothing special: the first connect returns 401, the client follows the discovery documents to the auth server, you sign in in a browser, and the client holds a token from then on.
$ curl -sS https://docstore.ai/mcp \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
A bound session also carries the tools of whichever accounting providers that account has connected, namespaced <provider>_<tool>. On a read-only connection the write tools are never registered at all, so a model cannot call what it has not been allowed to do. That is a structural gate, not a check inside a handler.
Six of them. Every one answers in markdown rather than JSON, because the caller is a language model and asking it to parse a payload it is about to read anyway is wasted work.
| Tool | What it does |
|---|---|
| search | Filter and text search across delivery events and the documents inside them. An empty query lists the feed newest first; a query ranks documents by full-text, fuzzy and semantic match together, so a half-remembered supplier name still lands and the hosting bill turns up even when the invoice says cloud compute. Filters for source, format, type, sender, date range, parent ingestion, and jsonb containment on the raw metadata. Ask for a snippet, the full text, or a signed download URL per hit. Limit caps at 200. |
| show | One record with its context: the extracted text, its sibling documents, and a five-minute signed download URL for every file with bytes in S3. The id auto-dispatches between a document and an ingestion, so a caller never has to know which kind it is holding. |
| edit | Patch the mutable fields. Summary and summary model on either kind, plus type on a document. Everything else rejects, and the summary timestamp is stamped for you. Summaries are living memory rather than a cache: an agent that learns something better about a document writes it back. |
| upload | Get a file in. Two modes, below. |
| memory | The account's persistent notes, addressed by slug: list, read, write, delete. Markdown for prose an agent reads back, HTML for a rendered artifact a person opens in the app. Every overwrite snapshots the previous version first, so a write cannot lose history. The convention is that a memory called index is read before starting anything. |
| list_accounts | The accounts this token can reach, with the pinned endpoint for each. Only interesting when a user belongs to more than one. |
Base64 in a tool call costs hundreds of thousands of context tokens per megabyte, so the default path keeps bytes out of the conversation entirely. Call upload with no data field and you get back a token valid for fifteen minutes and the curl line to use it. Several files in one request become one ingestion.
$ curl -sS -X POST https://docstore.ai/uploads/signed \ -H 'Authorization: Bearer <upload-token>' \ -F 'file=@invoice-10431879.pdf'
64MB a file, 256MB a request. The inline alternative, base64 in data, is capped hard at 5MB and is only worth it for something the agent already has in hand, such as text it just generated. Either way the file lands in the same pipeline an inbound email would: text extraction, a summary, a thumbnail.
Every account has its own local part and mail addressed to it arrives as an ingestion. The plain body, the HTML body and every attachment each become a searchable document, and the message is kept as it arrived.
acme-7f3k2p@inbox.docstore.aiMatching is domain-agnostic. We look at the envelope local part first, then the one in the To header, so pointing your own domain's MX records at us routes mail into the same account without anyone adding a per-domain record. The local part carries a random checksum, which is what stops someone who knows your account name from addressing mail into it.
Not built yet, and we would rather say so than imply otherwise.
There is no long-lived API key and no REST upload endpoint today. The design exists: POST /{slug}/api/uploads taking raw bytes or multipart, authenticated by a per-account token in the Stripe shape, mintable and revocable from a settings page, running the same ingest path as everything else. It is on the list rather than in the product.
Until it ships the two programmatic doors are the MCP upload tool and the signed-upload endpoint above, and the second of those is reachable with plain curl once a token has been minted.
Documents are kept in S3 in the form they arrived in. Which bucket is a property of your account rather than of our deployment, so the three options below are configuration, not three different products.
Your own prefix inside our bucket. Nothing of yours shares a path with anyone else's, and removing an account's documents is one recursive delete of one prefix.
Set the account's bucket and prefix to one you own and that is where the objects are written. We hold the index and the extracted text; the files themselves live in your account.
One static Go binary, a Postgres database and any S3-compatible object store. The image also carries the tools the extraction jobs shell out to, so PDFs, scans and HTML previews work the same as they do here. Talk to us before you start.
Keys are laid out per account so that a deletion request is answerable with a path rather than a query.
<bucket>/<prefix>/uploads/<ts>_<ingestion-id>/<filename>
<bucket>/<prefix>/email-attachments/<ts>_<md5>/<filename>
<bucket>/<prefix>/thumbnails/<document-id>.<ext>Extraction, summarising and thumbnailing are background jobs held in Postgres, so a document that has just arrived is not finished the moment it lands. Rather than poll, the app subscribes: the database notifies on change, a hub fans that out over a WebSocket, and the row you are looking at fills itself in as each job completes.
An account owns everything under it. An ingestion is one delivery event: an email, an upload, or a connector import. A document is the searchable atom inside an ingestion: the body text, or an attachment.
We read what's inside every file, not just store it: PDF text comes straight from the file, scans and photos go through OCR, and email bodies come along for free. Everything is indexed for full-text, fuzzy and semantic search, so you find the hosting bill even when the invoice says cloud compute. The AI search on top reads that same index and tells you which documents it used. Memories let the assistant keep notes about how your account works, revisioned so nothing is lost.
Early access is open. Tell us where your documents live now and we will set the account up.