# mcp - the reading as a tool an assistant can call Source: https://truecopy.dev/docs/mcp/ Module: truecopy/mcp An assistant handed somebody's statement has a tool call and nothing else. It offers two tools over stdio. One reads the table, the other checks the values against the rows they were cited from, and the file never leaves the machine. An agent with a terminal already had [`npx truecopy`](https://truecopy.dev/docs/cli/). One that can import a module had the library. **Most assistants are neither**: a host that speaks MCP reaches a tool call and nothing else, so the reading was unreachable from the place it is most needed - the model that has just been handed somebody's bank statement and has no way to check what it read off it. ```json { "mcpServers": { "truecopy": { "command": "npx", "args": ["-y", "-p", "truecopy", "-p", "pdfjs-dist", "truecopy-mcp"] } } } ``` `pdfjs-dist` is named on the command line because it is an **optional** peer dependency: without it the server still reads CSV, TSV and pasted tables, and answers `no-engine` on a PDF rather than pretending. ## Two tools **`read_table`** turns a PDF, a CSV, a TSV or a saved paste into rows, and says what the reading could not vouch for. It hands back the cut into columns, what each column was taken to hold, the rows **numbered**, and the doubts. At most 2000 rows a call, 200 by default - and what it leaves is `counted`, never silently dropped. **`check_citations`** takes the records the model claims to have read, each citing the row numbers it came from, and says which values those rows do not print. A JSON number is checked as a figure, with the document's own [decimal mark](https://truecopy.dev/docs/notation/); a JSON string as its words in order. Each value is looked up in the cited rows and **nowhere else**, so nothing can be produced - only pointed at. It is [`cite`](https://truecopy.dev/docs/cite/), reached from a host instead of from code. Cite the numbers `read_table` returned, for the same file and the same `maximumPages`: a different page bound is a different reading, and its rows are numbered differently. ### A description is a prompt, not documentation The sentence a host pastes in front of a model decides which tool it reaches for, so neither description stops at what its tool does. Both name what the tool does **not** prove - `read_table` says outright that an empty list of doubts is not a promise that the reading is right. A description that said "reads tables from PDFs" and stopped would have taught the opposite of this library. ## Over stdio, and not over HTTP That is the design, not a limitation. A statement, a payslip and a career record are the worst documents there are to upload, and **there is nothing a server could compute on them that this does not compute where the file already sits**: the reading is deterministic and makes no network call. An endpoint would add a copy of somebody's document to the world and buy nothing with it. `TRUECOPY_MCP_ROOT` confines what may be opened, when the host is not to be trusted with a path. It is applied to the **resolved** path, so a link inside the root cannot walk back out of it. ## A server of your own `truecopy/mcp` is the deciding half, and it touches no filesystem, no stream and no clock. ```ts import { TOOLS, respond } from 'truecopy/mcp'; const answer = await respond(message, { open: (path) => openTheFile(path), version: '2.0.4' }); ``` | | | | ----------------- | --------------------------------------------------------------------------------------------- | | `TOOLS` | the descriptors, with their input and output schemas, and the sentences above | | `respond` | one JSON-RPC message in, one `McpResponse` out - `null` for a notification | | `McpCapabilities` | what the module needs from the world: `open`, a path to a `File`, and the `version` to report | | `McpResponse` | `jsonrpc`, `id`, and one of `result` or `error` | `open` is **injected rather than imported**, which is what keeps the promise above: the confinement of what may be opened belongs to whoever launches the server, not to the protocol. `version` is passed in for the same reason - a browser has no `package.json` to read. `null` for a notification is not an implementation detail: by the JSON-RPC rules a message with no `id` must not be answered, and a client that receives an answer to `notifications/initialized` has to work out what it belongs to. `bin/truecopy-mcp.mjs` is the other half - the one that talks to the world - and it is thin enough to read in one sitting. Same split as [the parsers](https://truecopy.dev/docs/open/), for the same reason: what decides is tested, and what touches the world is small. ## Two generations of the protocol, both answered Up to `2025-11-25` a client opens with `initialize`. From `2026-07-28` the protocol is stateless, that method is gone from the schema, and a client either probes `server/discover` or simply calls. Answering only the newer one would go silent against every client shipped before July 2026, and none of the three costs anything to keep. Map of this site for a model: https://truecopy.dev/llms.txt Every page in one file: https://truecopy.dev/llms-full.txt