research-ledger-mcp
An MCP server that keeps the record of a preregistered study: what was hypothesised, what was fixed in advance, what was decided and when, what departed from the plan, what was run, what it cost, and what came out.
It is offline and deterministic. It never uses the network, never runs a command, and writes only inside .research/ in the project root.
Why
An agent that runs a study over several sessions forgets. It re-runs the costly step, tunes the specification after seeing the result, and reports the outcome as if it had been predicted. This server gives it a place to write things down that it cannot quietly rewrite, and derives the label "confirmatory" or "exploratory" from the timeline instead of taking anyone's word for it.
Install
Requires Node.js 20 or later.
Claude Code:
claude mcp add research-ledger --env RESEARCH_LEDGER_ROOT=/path/to/project -- node /path/to/research-ledger-mcp/server.mjs
Any MCP client (stdio):
{
"mcpServers": {
"research-ledger": {
"command": "node",
"args": ["/path/to/research-ledger-mcp/server.mjs"],
"env": { "RESEARCH_LEDGER_ROOT": "/path/to/project" }
}
}
}
| Variable | Meaning |
|---|
RESEARCH_LEDGER_ROOT | Project root. Default: the working directory of the server. Every file path given to a tool must resolve inside it. |
Tools
| Tool | What it does |
|---|
init_project | Create the ledger (once), with an optional budget |
register_hypothesis | Record a hypothesis with its test and decision rule |
freeze_protocol | Hash the protocol files and hypotheses into one root hash; refuses to freeze twice |
verify_protocol | Re-hash the frozen files; report which changed, went missing or were renamed |
amend_protocol | New linked protocol version, with the reason and whether data had been seen |
mark_data_access | Record when outcome data are first opened |
log_decision | Question, options, choice, rationale, before or after data (D-01, D-02…) |
log_deviation | Protocol said / done / why / impact on confirmatory status (V-01…) |
open_gate, close_gate | Go / no-go / conditional review, criteria recorded before evidence |
check_rerun | Is an identical run already on record, and was it expensive? |
register_run | Command, hashes of code, inputs and outputs, seed, environment (R-01…) |
record_spend, spend_report, set_budget | Cost of paid calls, exact totals, budget remaining |
record_result | Estimate, uncertainty, verdict; status derived by the ledger (X-01…) |
log_question, resolve_question | Open questions carried across sessions |
status | Everything needed to resume |
export_report | Transparency report, Markdown and JSON |
verify_ledger | Re-compute the hash chain; pinpoint the first broken record |
How a result gets its status
record_result never asks whether a result is confirmatory. It works it out:
| Situation | Status |
|---|
| Hypothesis fixed by a protocol version frozen before outcome data; files intact; no deviation | confirmatory |
Same, with deviations declared minor | confirmatory_with_deviations |
| No freeze before the result | exploratory |
| First freeze made after outcome data were accessed | exploratory |
| Hypothesis registered after the last pre-data protocol version | exploratory |
| Protocol amended after data were seen, for this hypothesis | exploratory (results recorded after the amendment) |
A deviation declared exploratory applies | exploratory |
| Frozen files differ from the manifest when the result is recorded | exploratory |
Outcome data count as accessed from the first of: a mark_data_access record with outcome_data true, an amendment declared with data_seen true, or any recorded result.
Storage
.research/
ledger.jsonl one JSON record per line, append-only
HEAD sequence number and hash of the last record
lock present only while a record is being appended
reports/ written by export_report with write: true
Each record is {seq, ts, type, data, prev, hash}. hash is the SHA-256 of the canonical JSON (sorted keys) of the other five fields; prev is the hash of the record before.
- Atomic writes. The ledger is written to a temporary file, flushed to disk, then renamed over the old one. A writer that dies leaves either the old ledger or the new one, never half a record.
- Locking. Appends are serialised by an exclusive lock file. Numbers (
D-01, R-01…) are assigned inside the lock, so concurrent writers never receive the same one. A lock older than 30 seconds is treated as abandoned.
- Corruption stops writes. If verification fails, every tool that appends refuses, and says at which record the chain broke.
What the chain proves, and what it does not
It detects a record that was edited, deleted, inserted, reordered or cut off, and reports the first affected line. The freeze manifest detects any change to a listed file.
It does not stop someone with write access from rebuilding the whole chain and HEAD from scratch: there is no secret key and no outside witness. If you need to prove to someone else what was fixed and when, copy the root hash returned by freeze_protocol (and, later, the head hash from verify_ledger) somewhere you do not control alone: a commit on a remote, a registry entry, a dated message.
Other limits to know:
- Timestamps are the local clock of the machine. They are part of each hash but are not independently attested.
- The server records what it is told.
register_run hashes the files you list at the moment you call it; it does not execute or observe the run. mark_data_access, timing and data_seen are declarations. The ledger catches contradictions with its own timeline, not what was never written.
- "Renamed" detection looks for identical content in the same directory only.
- Costs are stored in millionths of a US dollar. One currency.
- The whole ledger is rewritten on each append (that is what makes the write atomic). This suits thousands of records, not millions.
- The lock is advisory and local: it does not coordinate writers on a network file system that lacks exclusive create.
Permissions
The server reads the files you list (to hash them) and writes inside .research/. It starts no process and opens no connection. The manifest declares shell only because the client launches it with node and the test suite starts worker processes to test concurrent writers.
Tests
49 engine tests and 8 protocol tests, all offline, in temporary directories. They cover every tool, tampering (edited, deleted, inserted, swapped and truncated records), one-byte and renamed-file detection, refusal to refreeze, numbering under six concurrent writers, the expensive-run guard, exact spend arithmetic, status classification including an amendment after data, two simulated crashes during a write, and path confinement including symbolic links.
Licence
MIT