Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,65 @@ All notable changes to PaperTrace are documented here. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
[SemVer](https://semver.org/).

## [Unreleased]

### Added — `papertrace mcp`: an MCP server, for hosts that are not a shell

The roadmap's *drive PaperTrace as a tool from any MCP-capable client*.
`papertrace mcp` serves nine tools over stdio, behind a new `[mcp]` extra
(`mcp>=2.2,<3`, the SDK's current major): `start_audit` and `audit_status`
run and follow the `papertrace run` pipeline; `audit_summary`, `list_claims`,
`get_claim`, `get_evidence`, `list_references`, `list_gaps` and `get_scout`
read any case folder, made over MCP or by the CLI. `get_evidence` returns the
red-box crops as images.

The server is held to the reports' rules rather than given its own. It
computes no verdict. Every count and verdict carries the disclosures
`disclosures.py` decided for the four report formats — serialised the
viewer's way, with a parity test extending the four-format contract to this
fifth reader — and a limited audit says so first and last. A missing or
unreadable `results.json` is refused with its reason, never answered with
empty counts; `null` in the manifest's three-state fields stays `null`; no
local PDF path is served, and no evidence image from outside `<case>/out/`. A
verdict read on its own — `get_claim`, the evidence caption — carries the
run-level caveats too. Each audit leaves `<case>/mcp_audit.json`: a case whose
last MCP audit failed or never finished is refused rather than read, because
its files may mix two runs. The job refreshes the record every 15 s, so a
second host's server can tell a live audit from an abandoned one and will not
start another in the same folder. The tools' outputs and that record are
published in
`schemas/mcp_tools.schema.json` and `schemas/mcp_audit.schema.json`.

An audit takes minutes and a host built on the TypeScript SDK gives a request
60 s, so `start_audit` returns at once and `audit_status` long-polls for at
most 50 s. The audit runs in-process — `ask.py` stays the only file in `src/`
that starts a process — on one thread at a time, because the pipeline's
console, `ask`'s model record and stdin are process-global. Inside it nobody
can be asked: a reference label whose readings disagree is withheld, as the
CLI does without a terminal, and settling one still takes a person at a
terminal. And each audit starts with `ask.forget_models()`: the CLI never
cleared the model record because it ran one command per process, and a
second audit whose judging made no calls would otherwise have named the
first one's model as its judge. Design, rejected alternatives and the
decision record:
`docs/superpowers/specs/2026-09-26-mcp-server-design.md`,
`docs/adr/0004-mcp-server.md`.

The README's *From an MCP host* names the apps that can start it — Claude
Desktop, Claude Code, Cursor, VS Code, Windsurf, Gemini CLI, Codex CLI — and
the ones that cannot: ChatGPT and Claude in a browser reach MCP servers over
the internet only. Whichever app drives the tools, the audit is judged through
`claude -p`, so Claude Code must be installed and signed in. The server
installs from GitHub with `uv tool install`, no clone needed. Claude Code has
been seen connecting to that install; the other apps' configurations follow
their documentation and have not each been run.

### Changed — `[full]` and `[dev]` carry the MCP SDK

`[full]`, the quick start's everything-install, now includes the SDK, so
`papertrace mcp` works after it. `[dev]` — what CI installs — carries it so
the MCP tests run on every Python in the matrix instead of skipping.

## [0.7.1] — 2026-09-19 (beta)

Two fixes the 0.7.0 showcase run exposed, and the showcase itself.
Expand Down
45 changes: 43 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ pytest tests/test_refs.py -q # one module
pytest tests/test_refs.py::test_paywalled_is_honest -q # one test
pytest -k coverage -q # by name
pytest tests/test_report_viewer_js.py -q # the viewer's browser logic, under node (skips without it)
pytest tests/test_mcp_server.py -q # the MCP server, via the SDK's in-memory client + one real stdio run
papertrace mcp # serve over stdio for an MCP host ([mcp] extra); nothing on stdout
ruff check src tests scripts evals
uv build # sdist + wheel (python -m build is NOT installed)
```
Expand Down Expand Up @@ -101,7 +103,9 @@ and torch with it — but never *runs* it: the ~500 MB layout models download on
first use, not on install, and every test pins `backend="pymupdf"`
(`check_claims` makes `backend` a required argument so none can forget).
`playwright` must still stay out of CI; it is the `png` extra, needed only for
`--png`, with `playwright install chromium` once.
`--png`, with `playwright install chromium` once. The MCP SDK is the opposite
case: the `[mcp]` extra, and in `[dev]` as well, so CI runs
`tests/test_mcp_server.py` instead of skipping it.

## Architecture

Expand Down Expand Up @@ -262,6 +266,8 @@ ingest → extract → refs → scout → check → highlight → report
global: `_LAST_MODEL` was overwritten by every call, so a run whose judging
made zero calls — every cited source `not_retrieved`, nothing to judge —
printed the reference-list model as the `Checker:` of verdicts it never saw.
The record assumes one command per process; `forget_models()` clears it for
a process that outlives one run, which the MCP server calls per audit.
`_ask`'s signature is frozen at `(prompt, model=None)`; every offline test
that patches the seam does so with a two-argument lambda, which is why the
site travels out of band in a context manager instead of as a third
Expand Down Expand Up @@ -326,7 +332,42 @@ ingest → extract → refs → scout → check → highlight → report
DOM-free module (parsing `annotated.md`, anchoring quotes with the claim's
`ctx_ids` block searched first, crop sets, filters, the claim map) that
`tests/test_report_viewer_js.py` runs under node, and `viewer_app.js`, which
only draws. Disclosures are never re-derived in JS.
only draws. Disclosures are never re-derived in JS. The MCP server is a fifth
reader: it serialises the same `Disclosure` objects with `_disclosure_dict`,
generically, and `tests/test_mcp_server.py`'s parity loops assert every
token reaches it.
- **`mcp_server.py`** — `papertrace mcp`, an MCP server over stdio
(`mcp>=2.2,<3`, the `[mcp]` extra) with nine tools. Seven read a case folder
and compute nothing: every verdict and count travels with its disclosures,
the scope sentence first and again as the final `limited` key; a missing
`results.json` is a `ToolError`, never empty counts; evidence is served only
from inside `<case>/out/`, and no `pdf_path` leaves. `start_audit` runs
`cli._run_pipeline` **in-process, on one daemon thread** — never a
subprocess, which `tests/test_ask.py` would refuse — and `audit_status`
long-polls for at most 50 s, inside the 60 s a TypeScript-SDK host allows a
request. A server outlives many audits, which the CLI never did, so the job
resets what a process used to reset by exiting: `cli.console` becomes the
job's log (under stdio, stdout is the protocol's wire), `sys.stdin` becomes
an empty stream (`_interactive()` is False, so a disputed label is withheld
with `chosen_by: "default"` and the person-gated escalation is unreachable —
do not offer MCP elicitation in its place: a host may be a model), and
`ask.forget_models()` runs first. Those three are process-global, hence
**one audit at a time**. The job writes `<case>/mcp_audit.json` (`running`,
refreshed every `HEARTBEAT_SECONDS`, then the outcome), and read tools refuse
a case whose last MCP audit is running, failed, or never finished: a failed
rerun leaves one run's manifest beside another's verdicts. `running` is a
measurement — fresh is another process's live audit (refused, never started
over), unrefreshed for `STALE_AFTER_SECONDS` is `interrupted` — and never
say a cause the record's age does not show. The record on disk decides, not
a server's memory: a later `results.json`/`scout.json` supersedes a failure
or a run without the scout. A verdict read alone — `get_claim`, the evidence caption — carries
the run-level disclosures as well as its own. Outputs are published in
`schemas/mcp_tools.schema.json`, each definition's keys held equal to its
`TypedDict`'s by a test. **Any new process-global state in the pipeline
must be reset per audit here.** In SDK v2 only a `ToolError`'s message reaches the model — any
other exception is a bare "Error executing tool" — so every refusal is one.
Return types are `typing_extensions.TypedDict` with `structured_output=True`:
`typing.TypedDict` silently cost every tool its output schema below 3.12.

The `case/` folder is the unit of work: one case per paper, guarded by
`_guard_case`. It is gitignored by design — manuscripts stay local.
Expand Down
Loading
Loading