An LLM-assisted tool that turns arXiv papers into a knowledge library inside an Obsidian vault. The name is a pun on paper (reading) notes and pepernoten, the Dutch spiced cookie.
pepernoten_cli.py parses papers into structured, interconnected notes: AI synthesis, figure extraction, topic clustering, and BibTeX generation.
See docs/index.html for the full developer guide.
- Fetches arXiv HTML/PDF, extracts figures, and calls an LLM (via Claude if found locally, or OpenRouter) to synthesise a structured markdown note
- Notes include: TL;DR, problem, methodology, results, ablation, related work, gaps/limitations, BibTeX, embedded figures
- Clusters notes into topic survey files and keeps them updated as new papers arrive, gradually building a living review per subfield. Each note gets a changelog of what it contributed to each topic
- Generates up-to-date BibTeX by searching Papers With Code, CrossRef, Semantic Scholar, and DBLP
- Optionally syncs with Scholar Inbox to parse your daily digest
- Accessible via a MCP server for integration with Claude workflows
Prerequisites
- uv for Python dependency management
- An OpenRouter API key, or a logged-in claude CLI — with no key set, Claude models route through
claude -pautomatically (vision-assisted figure selection is disabled in that mode) - Optional: a Scholar Inbox account
git clone https://github.com/ameroyer/pepernoten.git
cd pepernoten
uv sync
# Optional - If `PEPERNOTEN_MODEL` is an Anthropic model and you have `claude` insteall, pepernoten will use that. Otherwise, it will attempt to use OpenRouter for model selection.
export OPENROUTER_API_KEY=sk-or-...
# Optional - authenticate scholarinboxcli for the "inbox" command:
# 1. Open a digest email from Scholar Inbox, right-click any paper link, and copy it.
# It looks like: https://www.scholar-inbox.com/login?sha_key=YOUR_KEY&date=...&paper_id=...
# 2. Run:
uvx scholarinboxcli auth login --url "https://www.scholar-inbox.com/login?sha_key=YOUR_KEY"The vault is the project root itself — point Obsidian at /path/to/pepernoten. Notes are stored under Research/.
# From a local clone
uv run pepernoten_cli.py
# From GitHub directly (no install needed)
uvx --from git+https://github.com/ameroyer/pepernoten pepernoten| Command | What it does |
|---|---|
| parse | Paste one or more arXiv URLs or IDs. Fetches HTML/PDF, extracts figures, synthesises a structured note, updates matching topic files. |
| inbox | Fetches your Scholar Inbox digest. Tick the papers you want — only those are parsed. |
| topics | Lists all registered topics with paper counts and last-updated date. |
| list | Browse all papers in the vault; press d to delete one. |
| update_knowledge | Add or remove papers from topic files; propose and execute topic merges. |
| add_topic | Describe a topic in a sentence. Claude searches the whole vault, picks the papers that belong, and writes the survey. |
| bibtex | Generate BibTeX for a paper — checks PWC, CrossRef, Semantic Scholar, DBLP before falling back to @misc. |
| quit | Exit. |
There are two MCP servers, one with read-only features, and one allowing you to also access the parse and add_topic commands.
pepernoten-mcp(read-only) — consult your notes: read the note for a paper, read a topic survey, find a result/method/claim across the vault. Cannot modify anything; the safe choice when consultation is all you need.pepernoten-write-mcp(read + write) — everything above, plusparse(turn arXiv papers into notes) andadd_topic(write a survey from a description).
Both work against a local vault or a GitHub repo. Setup is two independent choices: how to launch the server, and which backend it uses.
The notes in Research are essentially the knowledge base used by the MCP. You can use two backends:
- Notes are stored locally (e.g. if you have a local pepernoten install)
- (Recommended) Notes are stored on a (private) GitHub repo. This will allow you to access your pepernoten notes from anywhere. You will only need to generate a Github token for this specific repo. In this setup, don't confuse the two repos involved:
ameroyer/pepernotenis the code you're reading now, only used to launch the servers (uvx --from git+...). Your notes live somewhere else (e.g.ameroyer/pepernoten_notes).
Research/ is gitignored in this repo (your notes are yours). To version it into a separate private repo, make Research/ itself a git repository — nested repos are fine, the pepernoten repo ignores the whole directory:
# one-time setup (create the private repo first, e.g. gh repo create USERNAME/pepernoten_notes --private)
cd /path/to/pepernoten/Research
git init -b main
git remote add origin git@github.com:USERNAME/pepernoten_notes.git
# each backup
cd /path/to/pepernoten/Research
git add -A && git commit -m "backup" && git push -u origin mainThe resulting repo holds the contents of Research/ at its root (notes, Topics/, index files) — exactly the default layout the MCP servers expect. Set PEPERNOTEN_GITHUB_REPO=USERNAME/pepernoten_notes and your backed-up notes are readable (or writable, with the write server) from anywhere, no further configuration.
# 1. local checkout + local vault
claude mcp add pepernoten \
--env PEPERNOTEN_VAULT=/path/to/vault \
-- uv run --directory /path/to/pepernoten pepernoten-mcp
# 2. From Github repo eg.g USERNAME/pepernoten_notes
# Go to https://github.com/settings/personal-access-tokens and create new token for this repo, and add
# permissions for "Contents" in `Read` and `Write` (or just `Read` if you only want the MCP read features).
# Copy paste the token and save it somewhere you will only see it once
# 2a. Read-only version
claude mcp add pepernoten \
--env PEPERNOTEN_GITHUB_REPO=USERNAME/pepernoten_notes \
--env PEPERNOTEN_GITHUB_TOKEN=github_pat_... \
-- uvx --from git+https://github.com/ameroyer/pepernoten pepernoten-mcp
# 2b. OR the read+write server on the same notes repo (token needs Contents read-write there)
claude mcp add pepernoten \
--env PEPERNOTEN_GITHUB_REPO=ameroyer/pepernoten_notes \
--env PEPERNOTEN_GITHUB_TOKEN=github_pat_... \
--env OPENROUTER_API_KEY=sk-or-... \ # optional OpenRouter API key
-- uvx --from git+https://github.com/ameroyer/pepernoten pepernoten-write-mcpEquivalent .mcp.json / Claude Desktop claude_desktop_config.json entry:
{
"mcpServers": {
"pepernoten": {
"command": "uvx",
"args": ["--from", "/path/to/pepernoten", "pepernoten-mcp"],
"env": { "PEPERNOTEN_VAULT": "/path/to/vault" }
}
}
}Read tools (both servers): vault_info, list_papers, read_note (by arXiv ID, filename, or partial title), search_notes (full-text + tag filter), list_tags, list_topics, read_topic.
Write tools (read+write server only; needs LLM access — OPENROUTER_API_KEY, or a logged-in claude CLI when all configured models are Claude models):
parse(arxiv_ids, verbosity=2, update_topics=True)— full pipeline per paper: fetch, figures, synthesis, note, topic updates. Already-parsed papers are skipped.add_topic(description)— an LLM selects the vault papers that fit the description and writes the survey.
Write calls take minutes per paper (arXiv rate limits + LLM calls). The server streams every pipeline step as an MCP progress notification, so clients with an idle timeout (like Claude Code) see a live heartbeat instead of assuming a hang — no timeout tuning needed. If arXiv rate limits your IP, calls fail fast with "arXiv rate limited this machine — wait a few minutes and try again" instead of stalling.
Safety model:
- The read-only server cannot modify anything — no parse, no write, no delete tools exist.
- All reads are confined to
Research/— absolute paths,.., and symlink escapes are rejected, and only.mdnotes (plus the three index JSONs, internally) are readable. - The read backend talks exclusively to
api.github.com(repo/branch names validated, 30 s timeouts, 2 MB file cap) and the token is sent only in request headers — it never appears in URLs, errors, or logs. The write server passes its token to git via environment-based config, never on the command line or in the clone's on-disk config. - The write server's only mutations are the pipeline's own outputs, committed as plain
pepernoten: …commits — review or revert them like any other commit. - Retrieved note content is wrapped in an "untrusted data" banner so a client LLM is less likely to follow instructions embedded in a note (prompt-injection hardening — the client still decides).
The variables below all describe where the notes are, never the pepernoten code, and configure both servers the same way:
| Variable | Meaning |
|---|---|
PEPERNOTEN_VAULT |
(i) Notes in a local Obsidian vault - the vault root containing Research/ (default: the pepernoten checkout itself, which doubles as a vault) |
PEPERNOTEN_GITHUB_REPO |
(ii) Notes in a GitHub repo - owner/name of the repo storing your notes, e.g. ameroyer/pepernoten_notes. Setting this switches to the GitHub backend |
PEPERNOTEN_GITHUB_TOKEN |
Fine-grained PAT scoped to just that notes repo, with Contents permission: Read-only for the read MCP server (private notes repos only), Read and write for the write server (always required, it pushes commits to the notes repo) |
PEPERNOTEN_GITHUB_BRANCH |
Branch of the notes repo to use (default: its default branch) |
PEPERNOTEN_GITHUB_ROOT |
Where the notes live inside the repo. Default: the repo root itself holds the contents of Research/ (notes, Topics/, index files). Set to Research for a repo with a Research/ directory; the write server supports only these two layouts |
The read-only server reads the notes repo over the REST API. The read+write server instead keeps a clone of it under ~/.cache/pepernoten/: write calls sync, run the pipeline, commit, and push; read calls serve the clone, refreshing it first whenever no write is in flight. Expect a pepernoten: parse … commit on the notes repo after each write call, authored as pepernoten <pepernoten@invalid> — a label, not an account (override with PEPERNOTEN_COMMIT_NAME / PEPERNOTEN_COMMIT_EMAIL if you want them attributed to you).
Edit pepernoten_prompts.yaml — no code changes needed:
paper_synthesis:
analyst_role: "You are a sharp, critical research analyst writing notes for a PhD researcher"
tones:
1: "Precision and brevity. Expert reader."
2: "Precision and depth. ML researcher, not a subfield specialist."
3: "Subfield newcomer — explain design choices."
4: "ML newcomer — define all jargon, use analogies."
topics:
init_role: "You are a senior researcher writing a living review document…"
update_role: "You are maintaining a living review document…"
discover_role: "You analyze a vault's topic structure to find merge opportunities…"Keys not present fall back to hardcoded defaults silently. Only semantics are configurable — JSON format, field specs, and retry logic are not exposed.
Tones correspond to verbosity levels, set per paper when parsing. Default levels:
| Level | Target reader | Depth |
|---|---|---|
| 1 | Expert | Minimal — terse bullets, key numbers only |
| 2 | ML researcher (default) | Standard — full sections, key comparisons |
| 3 | Subfield newcomer | Expanded — intuition for design choices |
| 4 | ML newcomer | Full — definitions, analogies, concepts section |
# Parse
uv run scripts/parse.py parse https://arxiv.org/abs/2405.12345
uv run scripts/parse.py parse_many 2405.12345 2406.67890
uv run scripts/parse.py sync # Scholar Inbox top-N
# Topics
uv run scripts/topic_manager.py list
uv run scripts/topic_manager.py create "My Topic" --tags a,b --benchmarks B1
uv run scripts/topic_manager.py add "efficient video tokenization for streaming models"
uv run scripts/topic_manager.py init <slug>
uv run scripts/topic_manager.py init_all
uv run scripts/topic_manager.py update Research/SomePaper.md
uv run scripts/topic_manager.py discover # cluster unmatched papers into new topics
uv run scripts/topic_manager.py backlink_topics # refresh ## Papers sections
# BibTeX
uv run scripts/bibtex.py generate 2405.12345 --update_note
uv run scripts/bibtex.py batch 2405.12345 2406.67890 --bib_file=refs.bibEach parsed paper produces a .md note in Research/ with:
- Frontmatter — title, authors, date, arXiv ID, tags, thumbnail, verbosity
- TL;DR callout — one-sentence summary
- Sections — Problem, Methodology, Results, Ablation (depth varies by verbosity)
- Callouts — Gaps (
[!danger]) and Limitations ([!warning]) - Related Work table — direct competitors with arXiv links and gap analysis
- BibTeX block — auto-populated after
bibtex generate
Topic files live in Research/Topics/ and are maintained automatically:
- Created from a prompt:
add_topictakes a one-line description, searches the vault, and writes the survey from whatever papers actually fit - Auto-matched: once a topic exists, new papers join it if their tags or benchmarks overlap its fingerprint
- Living surveys — Introduction, Benchmarks, Methods & Baselines table, Techniques & Tricks, Architecture Overview, Open Problems & Gaps
- Method Index — appended automatically; maps every method short name → full title → link (covers cited baselines even without an arXiv ID)
- Paper backlinks —
## Paperssection lists all vault papers as Obsidian wikilinks
pepernoten/
├── Research/
│ ├── Paper Title.md ← synthesised paper notes
│ ├── .paper_index.json ← {arxiv_id: {title, file}}
│ ├── .tag_index.json ← accumulated tag vocabulary
│ ├── images/ ← extracted figures
│ ├── Thumbnails/ ← banner images
│ └── Topics/
│ ├── my-topic.md ← living survey files
│ └── .topic_index.json ← topic metadata + fingerprints
├── pepernoten_cli.py
├── pepernoten_prompts.yaml ← user-configurable prompt semantics
├── src/ ← library modules
└── scripts/ ← fire.Fire CLI entry points
