Metadata-Version: 2.4
Name: ipsissima-mcp
Version: 0.4.1
Summary: Turn a document into an Argdown map that cites it — its argument, the causal chain it sets out, the comparisons it draws — and check the map against the text.
Author: James Wilson
License: GPL-3.0-or-later
Project-URL: Homepage, https://jgsw.github.io/ipsissima/
Project-URL: Source, https://github.com/jgsw/ipsissima
Project-URL: Issues, https://github.com/jgsw/ipsissima/issues
Keywords: argdown,argument,reconstruction,pdf,epub,philosophy
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: Intended Audience :: Education
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Text Processing :: Markup
Classifier: Topic :: Scientific/Engineering
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: mcp>=2.3.0
Requires-Dist: pymupdf>=1.28.2
Requires-Dist: pymupdf4llm>=1.28.2
Requires-Dist: rapidocr>=3.10.0
Requires-Dist: onnxruntime>=1.31.0
Requires-Dist: beautifulsoup4>=4.15.0
Requires-Dist: lxml>=6.1.3
Requires-Dist: pyyaml>=6.0.3
Provides-Extra: zotero
Requires-Dist: pyzotero>=1.16.0; extra == "zotero"
Dynamic: license-file

# Ipsissima-MCP

Ask your assistant for an argument map of a paper, and get one that cites the text it is a
reading of. Ask for a map of a debate — the arguments for and against a conclusion, belonging
to no single text — and there is nothing to cite: the assistant writes the map directly, and
the checks still hold it to parsing and structure.

Ipsissima-MCP is an [MCP](https://modelcontextprotocol.io) server. It gathers what a map of a text
needs. It gets the document into structured Markdown with its paragraphs and page numbers intact.
It serves the method for reading the text into one Argdown file that holds several of its
structures: the argument and its reasons, the causal chain the text says happens, the questions it
asks, and the comparisons it draws, as tables. Then it checks the finished
map against its sources word for word. The judgement, reading a paper into a map, is done by the
assistant you are already talking to; this server hands it the instructions and then checks its
work.

The result opens in [Ipsissima](https://github.com/jgsw/ipsissima), which makes complex reasoning
clear through maps you can check and question. It shows the map beside the passage each claim was
drawn from, and how far each claim stands from the source's own words. It also gives you ways to
question the map: suppose a claim false and see what follows, weigh rival accounts, or study the
argument on your own.

---

## Install

### The easy way: the Claude Desktop bundle

Download the **`.mcpb` bundle** from the
[releases page](https://github.com/jgsw/ipsissima/releases) and double-click it. Claude Desktop
installs it, provisions Python and the dependencies itself, and there is nothing to configure —
no terminal, no virtual environment, no path typed into a JSON file.

**You still need [Node](https://nodejs.org) on the machine.** The Argdown parser is JavaScript
and is carried inside the bundle, so there is nothing to install *for* it, but something has to
run it. Nothing else is required: Claude ships a Node runtime for its own extensions but does not
expose it, and Python it does not ship at all — which is why the bundle asks the host to provide
one rather than carrying its own.

If the checker later reports that it cannot find Node, install it and restart Claude Desktop.
It looks in the usual places as well as on the `PATH`, because an application launched from the
Dock does not get the `PATH` your terminal has.

### The terminal's way: from PyPI

Already in a terminal, with [uv](https://docs.astral.sh/uv/) on the machine? The whole install
is one line:

```bash
claude mcp add ipsissima -- uvx ipsissima-mcp
```

`uvx` makes an environment, installs the released
[`ipsissima-mcp`](https://pypi.org/project/ipsissima-mcp/) into it and throws it away
afterwards, so there is no virtual environment to make, remember, or type the path of. Any MCP
client can run the same command — `uvx ipsissima-mcp` *is* the server. Node still has to be on
the machine, as above, and [pandoc](https://pandoc.org/installing.html) is still worth having.
The source checkout below remains the way to work on Ipsissima-MCP itself.

### The developer's way: from source

You need **Python 3.11+**, **Node** (any current version — it reads the Argdown), and ideally
[**pandoc**](https://pandoc.org/installing.html).

```bash
git clone <this repository> && cd ipsissima
python3 -m venv .venv && .venv/bin/pip install -e ipsissima-mcp
```

A virtual environment is not fussiness: a Homebrew or system Python will refuse `pip install`
outright ([PEP 668](https://peps.python.org/pep-0668/)).

**With [uv](https://docs.astral.sh/uv/) instead**, which is the same thing and faster:

```bash
uv venv && uv pip install -e ipsissima-mcp
```

**A uv-made `.venv` has no `pip` in it.** That is uv working as designed, not a broken
environment — but it means `.venv/bin/pip install …` fails with *no such file or directory*, and
the obvious reading of that message is that the virtual environment did not get made. Use
`uv pip install` for a uv venv, or `python3 -m venv` if you want `pip` inside it. The entry points
below are installed either way; `ls .venv/bin/ipsissima-mcp` is the quick way to tell whether the
package is there.

**Node is needed, `npm install` is not.** The checker uses the Argdown parser as ground truth for
whether a file is valid, and a copy of it is bundled into this package as a single file that
`node` runs — so there is nothing to install for it and nothing to keep in step. It used to reach
into `app/node_modules` instead, which meant the server could only run from a source checkout and
could not run on Windows at all, npm writing no shim of that name there. If `node` is missing the
checker says so and names the download page, rather than reporting a missing CLI.

`rapidocr` installs with it and is **not optional**. Without an OCR backend, a scanned paper
converts silently to its cover page and nothing reports it — measured at 345 words of a
1,220-word article, no error, a perfectly well-formed Markdown file. See
`eval/CONVERTER-FINDINGS.md`.

### Your assistant needs to be able to read and write files

**This is the requirement most likely to catch you out, and it is not obvious from the tool
list.** Ipsissima-MCP does not carry the whole job. Look at where the work actually happens:

| step | who does it | needs disk access? |
|---|---|---|
| `argdown_plan`, `extract_text` | the server | no — the server writes to disk itself |
| **read the extracted Markdown** | **the assistant** | **yes, read** |
| **write the `.argdown`** | **the assistant** | **yes, write** |
| `argdown_check` | the server | no — it reads the file from disk |

`extract_text` writes `source/` and returns a *report* of what it wrote rather than the text: the
reply carries a short `head` of each file and no more, because a book-length source would
otherwise arrive in the conversation twice over. The head is there to show that the conversion
worked — headings intact, page markers present — and is deliberately far too little to
reconstruct from. And no tool here writes the reconstruction — that is the model's judgement, and
the model saves it with its own file-writing tool.

So an assistant that cannot touch the filesystem gets as far as a folder full of Markdown it
cannot read, and has nowhere to put the map it then cannot write.

**And it has to be the same filesystem, which is easier to lose than it sounds.** The table asks
whether each party can reach *a* disk. What it does not ask is whether they reach the *same* one.
An assistant that talks to this server across a device bridge — a hosted session reaching your
Mac, and any arrangement like it — runs the server on your machine and the model in a sandbox
somewhere else. Every tool then works perfectly and writes to a disk the model cannot open:
`extract_text` reports a folder of Markdown that genuinely exists, at paths that resolve to
nothing where the model is standing.

Measured once, on a 321-page book. The model could not open the paths, and went back to reading
the original PDF itself — producing a competent Argdown file with no `chapter` on any claim,
which is to say a map that cites no text and that `argdown_check` cannot verify one word
of. Nothing errored and nothing said anything was wrong, which is why it is worth naming here: a
map that cannot be checked against its source is the one thing this project exists to prevent.

Connect the folder before you start — hosted clients ask for one, and every bridge has an
equivalent — and the arrangement works exactly as the table says. If you cannot, run the server
from something sitting on the same machine as your files.

**What works:**

- **Claude Code** — has its own file tools. Everything below works unmodified.
- **Anything agentic with a workspace** (Claude's Cowork, an IDE assistant like VS Code or
  Cursor) — same, provided it can reach the folder you are working in.
- **Claude Desktop** — the `.mcpb` installs and every tool runs, but on its own the assistant has
  no file access. Add a filesystem MCP server alongside this one, pointed at the folder you keep
  reconstructions in, and the whole workflow works.

**How to check yours in one line**, rather than trusting a list that will date: ask it to *write
a file called `test.md` in this folder, then read it back to me*. If it can do both, it can drive
Ipsissima-MCP end to end. If it cannot, you can still use the server for everything up to the
reconstruction — extraction, page images, repair, the Zotero lookup — and then paste the map into
a file yourself before asking for `argdown_check`. That works; it is just handwork the
other clients do for you.

### Which assistants can use this

Two questions that get confused, and they have different answers.

**The server works with any MCP client.** It is a plain MCP server — the official Python SDK,
protocol `2024-11-05`, spoken over stdin and stdout — and there is nothing Anthropic-specific in
it. Claude Code, Claude Desktop, VS Code, Cursor and anything else that talks MCP can run it.
Point the client at the `ipsissima-mcp` command, in whatever way that client is configured.

**The `.mcpb` bundle is Claude Desktop's install format, not a protocol.** No other client opens
one. Elsewhere, install from source and configure the command yourself, as below. Nothing is lost
by doing that; the bundle is a convenience, not a capability.

**What genuinely varies is prompts and resources.** MCP has three kinds of thing, and clients
support them unevenly: tools are universal, prompts and resources much less so. And the
unevenness is not the whole problem. The protocol makes prompts *user*-invoked and resources
*application*-attached, so even a client that surfaces both beautifully gives the **model** no
way to pull them mid-task — and it is the model, halfway into "make me an argument map", that
needs the method: the Assertibility Question, linked versus convergent support, what fidelity
levels mean, what to do about what an author did not say. There is very little Argdown in the
world, and a model guessing at it confidently writes files that do not parse.

So the method is served on every channel, and the tool channel is the one that cannot go
missing. The `reconstruct_argument` prompt and the `ipsissima://` resources carry it for
clients that surface them. For everything else — measured on a hosted session reaching this
server across a device bridge that forwarded tools alone — `extract_text` returns a compressed
conventions digest with the extracted text, and the `argdown_method` tool serves the
full documents as ordinary tool results, one call each. A model that can call tools has the
whole method; no client configuration and no user instruction is required. (The documents also
sit in `src/ipsissima_mcp/docs/` for anyone who wants to paste them by hand, but nothing now
depends on that.)

**A fourth thing can go missing, and it is the quietest of them.** An MCP server also ships
*instructions* — prose the client is meant to put in front of the model beside the tool list.
This server's says in its opening lines that "make an Argdown", "map the argument" and
"reconstruct this paper" are one request, and then gives the order of work. A client that drops
it leaves the model holding a list of bare tool names with no reason to connect any of them to
what was asked.

Observed, in a hosted session reaching this server across a device bridge: the instructions of
*every* connected server were dropped, the tools arrived as bare names in one alphabetised list
with no descriptions attached, and a request opening with the words "Make an argdown file" never
called a single tool of the server built to answer it. Nothing errored. If you are running
through a bridge or a hosted client and maps come back with no provenance, check this before you
look anywhere else — and the fallback is the one above: hand the model `extraction-prompt.md`
yourself.

**The model is a separate question from the client.** Ipsissima-MCP does not reconstruct
arguments; it prepares sources and checks results, and the reading itself is the model's
judgement. A weaker model behind a fully capable client will produce a weaker reconstruction, and
`argdown_check` will report the difference rather than repair it.

### Tell your assistant about it

**Claude Code**

```bash
claude mcp add ipsissima -- /absolute/path/to/ipsissima/.venv/bin/ipsissima-mcp
```

**Claude Desktop** — add to `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "ipsissima": {
      "command": "/absolute/path/to/ipsissima/.venv/bin/ipsissima-mcp"
    }
  }
}
```

Use the absolute path. The server finds the Argdown parser and its own documents relative to
where it is installed, not to where the client happens to be running.

`claude mcp add` is a command of the **Claude Code CLI**, so run it in a terminal where `claude`
is on the `PATH`. It is not available from inside every client that can talk to an MCP server, and
`command not found: claude` there means you are in the wrong window rather than that anything is
missing.

### Updating and removing it

**The bundle.** Claude Desktop's own extension settings install, update and remove it — a newer
`.mcpb` from the [releases page](https://github.com/jgsw/ipsissima/releases) replaces the one you
have. Removing it there is the whole uninstall: the dependencies live in an environment the host
made and manages, not in your Python installation, so nothing is left behind in yours.

**From source.** Update with `git pull` and re-run the install command; an editable install
(`pip install -e`) needs nothing more than the pull, which is what editable means. To remove it:

```bash
claude mcp remove ipsissima      # or delete the entry from the client's config
rm -rf .venv                     # the environment, and everything installed into it
```

Nothing is installed outside that virtual environment and the repository, and neither holds your
reconstructions — those are ordinary files wherever you chose to keep them.

**Nothing checks for updates on its own — but you can ask.** The server makes no network
request of its own accord, ever: no startup check, no timer. What it has is a
`check_for_updates` **tool**, the server's equivalent of the application's Help ▸ Check for
Updates menu item — ask your assistant *"is there a new version of Ipsissima?"* and one
request goes to the GitHub releases API, only then. It sends nothing about you or your
documents, downloads nothing, installs nothing: it names both versions and says how to update
by the route you actually installed. For bundle users it matters more than the menu item does
in the app, because the `.mcpb` format has no update feed of its own — this tool, or watching
the [releases page](https://github.com/jgsw/ipsissima/releases), is how you hear a new one
exists. It also tells you that the desktop application is a separate program on its own
schedule: a new Ipsissima-MCP does not mean a new Ipsissima.

### Check it before you rely on it

The server speaks over stdin and stdout, so "it started" and "it works" are different claims, and
a client that finds no tools will usually say nothing about why. This asks it directly:

```bash
.venv/bin/ipsissima-mcp <<'EOF'
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}
EOF
```

One line of JSON comes back naming the server and its version. **A working install answers 14
tools (12 without a Zotero library on the machine), 2 prompts and 8 resources**; if the count
is lower than that, the client is not the problem. (That sentence is held against the live
server by the test suite, so it cannot quietly drift the way it did twice: it said 9 for five
days after `check_for_updates` made it 10, and the corrected 10 then promised the Zotero tool
to machines with no Zotero — which the first public CI run caught within the hour.)

Note that `pymupdf` prints a deprecation notice on startup. It goes to stderr, where it is
harmless — the protocol is on stdout — and it is not a sign of a bad install.

---

## Using it

**It takes a path, not an attachment.** The server reads files from the disk it is running on:
`sources` is a list of paths, and anything else comes back as *no such file or directory*. A
document attached or pasted into the conversation is text in the conversation — the server never
sees it and cannot be given it. Save it to a folder first and pass the path, which is one more
instruction to the same assistant.

Within that, ask in your own words. All of these work, and all mean the same thing:

> Use Ipsissima to map the argument in `~/papers/rawls-1971.pdf`.
> Make an Ipsissima diagram of `~/papers/rawls-1971.pdf`.
> Make an Argdown from `~/papers/rawls-1971.pdf`.

**Say "Ipsissima" if nothing seems to happen.** The other phrasings are meant to work without it:
the server ships instructions saying that all of them are one request, and a client that delivers
those will route any of them here. Not every client does. When one drops them the model is left
holding bare tool names with nothing to connect them to what you asked — and the literal word
*Ipsissima* in your sentence is then the only thing that still matches, because it is in the tool
names themselves. It costs nothing, so the first example says it.

**One article.** The assistant extracts the text, reconstructs the argument, and checks it until
the checker is satisfied. You get a folder with `source/the-paper.md` and a `.argdown` beside it.
Open the folder in Ipsissima.

**Several articles, separate maps.**

> Make separate Argdown maps for each article in `~/papers/to-read`. Put them in a new folder,
> with the extracted texts and the Argdowns side by side.

**A book.**

> My manuscript is in `~/book`. Create a single Argdown map using the latest draft of each
> chapter.

**Text only, no reconstruction.**

> Just extract the markdown from these PDFs — don't make a map.

**A debate, not a document.**

> Map the arguments for and against lowering the voting age.

No source text, so no extraction and no fidelity borders — the map is the assistant's survey
of the debate, each side's best case laid out step by step, and it stands on being checkable
on screen rather than checkable against a text. The checker still holds it to parsing and
structure, and greets the genre as one: a sourceless map is not asked for provenance it
cannot have.

**An overview written first, then mapped.**

> Write me a report on the arguments about nuclear power, then map it with Ipsissima.

The report is a manuscript like any other — gold-route Markdown — so the map of it gets the
full treatment: quotations verified, departures marked, the text beside the map. Two honest
labels ride with it: the map's front matter declares `text-provenance: generated`, which
Ipsissima shows beside the title, and what the checks establish is that the map is faithful
*to the report* — not that the report is true.

### It will ask before it spends

A reconstruction is the expensive step, and *"a map of each of these articles"* and *"one map
from these chapters"* look identical from the files alone. So the server reports what is
ambiguous rather than guessing, and refuses to convert several sources until the question is
answered. If you are asked which of two drafts is current, or whether you want one map or six,
that is the server declining to spend your money on a coin toss.

### What a reconstruction costs

Measured over 25 runs of the current method on five texts (30 October 2026), not estimated.
Roughly:

```
tokens added by a run  ≈  48,000  +  2.2 × bytes of map  +  10.7 × words of source
```

That is new input plus output, after whatever your client sends before it starts. Claude Code
opened these sessions at about 75,000 tokens of its own; yours may open smaller or larger.

**In practice**, a journal article of 7,000–11,000 words runs **180,000–310,000 tokens** on top
of the client's own. A short paper of 1,500 words was about **95,000**. Of that, thinking and
writing (output) is about 90,000 on an article.

**Most of the rest is reading.** The method is read whole every time: the argument, the
mechanism, the comparisons. It is about 34,000 words, and it is why a short paper still costs a
good deal. The source is read once. The map is written once and checked.

**What the bill mostly is.** Each call re-sends the conversation from the cache, at a tenth of the
rate, so the rate-weighted total is mostly those re-reads. It follows the number of round trips
more than anything in the text. On the published multipliers an article came to 640,000–810,000
input-equivalent tokens.

Two things keep it down, and both are already applied:
- **Fewer round trips.** The instructions carry the rules for them.
- **Check replies that send only the faults while any remain**, about 1,500 tokens instead of
  14,500.

Two levers are yours:
- **Effort** is the largest. `high` came in 16% cheaper than `max` on the same paper in August,
  and made eight mechanical faults where `max` made none (`eval/effort-testing/`).
- **Map a book once, not chapter by chapter.** The method is read again for every map.

**A caveat this file would rather state than bury.** The source term rests on five texts, and two
of them carry most of the runs. Read it as "longer articles cost more", not as a price per word,
and do not extrapolate it past 11,000 words. The workings, the residuals and what would falsify
the model are in `eval/COST-2026-10-30.md`. It replaces August's model (`eval/COST-2026-08-27.md`),
which was measured another way and predates the mechanism and tables layers.
`eval/run_cost.py` measures any run of your own from its transcript.

---

## Which file to give it

**This is the single most useful thing on this page.** The same paper can arrive in three very
different states, and the difference is permanent — a manuscript converted badly stays badly
converted for every claim that ever cites it.

### 1. Markdown is gold

Already structured; nothing is recovered because nothing was lost. If you draft in Markdown,
Ipsissima reads your **live file**, so the Manuscript view is always your current draft.

### 2. Anything pandoc reads is silver

`.docx`, `.odt`, `.html`, `.epub`, `.tex`. The document's own structure survives: a heading is a
heading because the document says so, not because something guessed from the type size.

A **saved web page** is the one silver format that is mostly not the document — nav, ads,
"related stories", images embedded as base64. Ingest finds the article by text density and
never writes the rest; embedded images are blanked and a Project Gutenberg text loses its
header and licence together. Every cut is counted in the converted file's own header.

**If you are not writing in Markdown**, the best workflow is to export to Markdown yourself with
pandoc and keep that file as the manuscript:

```bash
pandoc --wrap=none -o chapter-3.md chapter-3.docx
```

No pandoc installed? The web version at **<https://pandoc.org/app/>** does the same job in a
browser. When you want Ipsissima to show a newer draft, export again and save over the same
Markdown file — the map is watching it, and the Manuscript view will update by itself.

### 3. PDF is bronze

A PDF records where ink sat on a page. Paragraphs, headings, columns, footnotes and reading order
all have to be **inferred from geometry**, and inference has a hit rate.

So: **do not give it a PDF of your own draft.** You have the `.docx`. If a folder holds
`chapter-3.pdf` beside `chapter-3.docx`, the server will say so before converting anything.

For published articles you do not have in any other form, PDFs are handled, and handled
carefully:

- Where the article has a DOI, the server checks whether a **machine-readable version exists**
  (Europe PMC and other open-access routes) and prefers it. That turns a bronze source into a
  gold one for the cost of one lookup.
- Otherwise it converts, and **assesses the difficulty first**. A clean text layer is converted
  mechanically. A damaged one is OCRed, and every route tried is scored and reported so the
  choice is visible.
- Where neither works — an old scan, a strange typeface, essentially photographs of text — it
  says so, renders the damaged passages as images, and asks the assistant to read them. You get
  a usable result either way; what changes is how much it costs.

### A scan is read again, and the readings are voted

An old scan's text layer is one OCR engine's reading of the page images, made once, and every
word it got wrong is a word a quotation will fail against -- often without looking damaged at
all. Where **tesseract** is installed (`brew install tesseract`, or your system's package),
every scan is read again, page by page at 300 dpi, and the two readings are **collated** like a textual critic's
witnesses: aligned word by word, and wherever they disagree,

1. a majority settles it, when there is a third reading to make one;
2. otherwise a word list settles it (the system's, plus every word the readings agree on
   elsewhere in the document), where one reading is a word and the other is not;
3. otherwise it is a **dispute**: the reading of whichever engine has proved more reliable on
   this document stands, and the place is logged.

A third reading is worth having when you have one: give `extract_text` a `witness` -- another
OCR text of the same work, such as archive.org's "Full text of" page saved as HTML.

**A sound layer is kept, and corrected in place.** Where a page's old layer is a reading of the
page, the converter reads it exactly as before -- its type sizes, note numbers and lines are what
the heading and footnote rules were tuned on -- and the vote's corrections are made in the
converted text, each found between its neighbours: a word misread (`siftcient`), a line cut off at
the page edge, a lost note number. Only a page with no layer, or one that is no reading of it
(letter-spaced, a database header over an image), gets a new layer written from the fresh reading,
keeping the old layer's sizes and *italics* where it had any. Either way every word keeps the
scan's coordinates, so page markers and Zotero highlights still land.

The disputes are what is left for the assistant, and they are small: `collation_disputes`
returns them with every reading and the printed lines they sit on, many to an image with the
disputed words boxed, and `settle_disputes` applies what the images say -- to the Markdown,
and to the scan's record, so a later conversion of the same PDF applies them first. Pages
missing from the scan (the printed page numbers jump) are reported. On the 1963
Gettier scan the layer's own `siftcient` and `suffiient`, two half-lines lost at the page edge,
and three lost words are put back, with 13 small disputes left (2 with a third reading); on
older JSTOR scans whose layers were cut off at the right edge of every line, whole paragraphs
come back. See `eval/COLLATION-FINDINGS.md`.

`collate: false` turns this off; a born-digital PDF has no page images and is untouched by it.
Without tesseract, a scan converts as it always did, and the conversion's notes say what
installing it would add. The OCR is kept in
your user cache folder, keyed by the file's contents, so a second conversion takes seconds; it
is never written beside a Zotero attachment.

**Age is a good predictor.** A PDF made in the last decade is usually fine. One from twenty-odd
years ago, or a scan of a photocopy, is where the difficult cases live.

**Page numbers survive.** Where the source is paginated, page breaks are kept in the Markdown as
comments, and Ipsissima's Manuscript view shows the page number beside the text.

---

## Zotero makes it better, and is not required

Everything above works with no Zotero installed. If you do have it, the server notices, reads
it **by copy** (never the live database — Zotero holds the file open), and can:

- build maps from items in your library, found by DOI, item key or title;
- use an item's **HTML snapshot** for the text and its **PDF for the page numbers** — the best
  combination there is, and the reason a snapshot is worth keeping. Each has exactly what the
  other lacks: a web page has no pages, and a PDF has no idea what a heading is. The page number
  is *read off each sheet*, not counted from one, because a paper whose front matter says it
  starts at 511 can print 514 on its first sheet;
- find snapshots you had forgotten were there, which read far better than the PDF beside them;
- look past the one record you asked about. The same work often sits on several — a reprint,
  a duplicate import, a PDF dropped in on its own — and the best copy can be on any of them.
  The lookup finds the others by title and author (or, for a record with neither, by its
  file's name), ranks every copy Markdown, then HTML/EPUB/.docx, then PDF, with an image-only
  scan last and named as one, and says when a sibling is a different printing whose wording
  may differ. The pairing above then works across records: text from one, pages from another;
- **store the finished reconstruction back into Zotero**, on your request: one `.argdown`
  attachment lands under the item it reads, carrying the converted source *inside it* (the
  argdown-bundle format — still a valid `.argdown`), so the whole reading survives beside
  its source, travels with Zotero's own sync, and opens in Ipsissima text-and-map together
  from a double-click. The exported one-file page can be stored too, on request.

Reading never writes anything. Storing writes only what you asked stored, only after you
answer Zotero's own permission dialog (which names Ipsissima, and whose grant you can revoke
in Zotero's Settings ▸ Advanced), and only to the Zotero on this machine — the server never
contacts zotero.org.

---

## What the server offers

What a map may record, and what the checker holds it to, is specified in
[`docs/PROFILE.md`](../docs/PROFILE.md), the Ipsissima profile for Argdown; its registry ships in
the package as `profile.json`, and `argdown_check` reports the profile version it validated against.
Both are CC BY 4.0, whatever the licence of the package the registry travels in.

| tool | |
|---|---|
| `argdown_plan` | reads the request; reports the sources, the routes, the cost, and what is ambiguous; lists the layers the map will hold and the way into it each feeds; advises rebuilding a text's own tables at extraction, since its comparisons are read with every argument |
| `extract_text` | documents to structured Markdown, with page markers |
| `assess_pdf` | how hard is this PDF, and does a machine-readable version exist? |
| `page_images` | crops of damaged passages, for the cases a converter cannot do |
| `repair_source` | apply corrections read off those crops |
| `collation_disputes` | where a re-read scan's readings disagreed: every reading, and the printed lines as images |
| `settle_disputes` | apply what those images say, to the text and to the scan's record |
| `add_page_numbers` | the PDF's pagination onto text taken from the snapshot |
| `argdown_check` | validity, provenance and fidelity — faults with fixes, not a report |
| `split_manuscript` | a one-file book into chapters plus a project file |
| `zotero_lookup` | only when a library is present |
| `check_for_updates` | only when asked — see *Updating and removing it* above |

**Prompts** — `reconstruct_argument` (the full instructions, read off disk every run) and
`extract_text_only`. **Resources** — the Argdown reference, the map semantics, the order views,
the viewer guide, served on demand rather than loaded into every conversation.

---

## The command line is still there

The server did not replace it, and for a one-off it is often quicker:

```bash
.venv/bin/ipsissima-ingest paper.pdf --out "samples/Author 2026 - Title"
.venv/bin/ipsissima-check reconstruction.argdown --source-root .
.venv/bin/ipsissima-split book.md --out chapters/
```

`ipsissima-check` has two modes worth knowing. Bare, it prints the full report — the apex, the
sections, the tag census, the debt. `--only-problems` prints just what is wrong, and
`--format json` prints the same as data. The short forms exist because a check-and-fix loop reads
this file every round: on the Darwin sample the full report is 687 words and 10.5 seconds, and
the short one is 221 words and 2.4.

### The pieces

| | |
|---|---|
| `server.py` | the MCP server |
| `sources.py` | what a source is, which route it takes, and what is ambiguous about a request |
| `ingest.py` | the front door: any supported document to a source folder |
| `collate.py` | a damaged scan read again by tesseract and voted against its text layer (and any other OCR text of it), the disputes logged with their geometry |
| `pdf_to_source.py` | a printed article to Markdown — ligatures, de-hyphenation, paragraphs from the printed indent, journal furniture, footnotes, page numbers |
| `structured_source.py` | the inversion: when the publisher's own HTML or EPUB is available, take the structure rather than recovering it from ink positions |
| `epub_to_source.py`, `html_to_source.py` | those two routes |
| `from_zotero.py` | acquisition, by whichever route an item's attachments allow |
| `new_reconstruction.py` | scaffold a reconstruction folder from a PDF in one command |
| `split_manuscript.py` | a one-file manuscript into per-chapter files and a project file |
| `check_argdown.py` | check a reconstruction: are the quotations verbatim, do the pinpoints hold, is anything misreported |
| `argdown_provenance.py` | where in the text each claim came from — the Python half of a pair with `app/src/argdown-positions.js` |
| `rationale_to_argdown.py` | convert a Rationale `.rtnl` map |

### One rule that is not obvious

**`check_argdown.py` and `app/src/argdown-positions.js` implement the same rule in two
languages**, because the viewer needs it in the browser and the checker needs it here.
`app/test_argdown_positions.mjs` exists solely to police the drift between them, and it is the
reason the viewer's build shells out to Python rather than reimplementing near-matching in
JavaScript.

## Tests

```bash
node app/run_all_tests.mjs        # everything, and the suite that matters
```

The MCP server's own tests drive it over a real stdio session, because the thing most likely to
break is the contract with the client rather than the logic behind it.

## Evaluation

`eval/` holds the harness and the findings from choosing a converter. `CONVERTER-FINDINGS.md` is
worth reading before replacing anything in the PDF route: `marker`, `docling` and a local
`llama.cpp` were all measured and removed — about 3.9 GB between them — because `pymupdf4llm`
with `rapidocr` beat them all on the labelled test set at a fraction of the cost.
