endnotex9-toolkit
A local-first Model Context Protocol server for EndNote X9. It lets Codex, Claude, and other MCP clients search a library, inspect existing groups, build an auditable classification, format citations, and read attached PDFs without uploading the library to a third-party service.
[!NOTE] Renamed from
endnotex9-mcpin 0.2.0. The old command still starts the same CLI, and a configuration written by an earliersetupis still found where it was saved.
[!IMPORTANT] Direct library parsing is tested against EndNote X9. The format is undocumented; run
endnotex9-toolkit crosscheckbefore trusting a later EndNote version. Almost every MCP tool is read-only. Two tools —update_reference_fieldsandmark_references— can write to the library, and only to a narrow, pre-verified set of columns: bibliographic fields (volume, issue, pages, article number, number of volumes), journal, notes, title, abstract, DOI, the author list, the keyword list, or the unused Custom 2–7 fields.yearcannot be changed at all — it is a MyISAM index key. Both default to a no-op preview (dry_run=True) and back up the whole library before any real write. See Safety model.
Features
- Direct, read-only access to X9
.enland<Library>.Data/rdbtables; no MySQL server required. - XML export fallback and field-by-field cross-checking.
- CJK-aware SQLite FTS5 search with multi-query merging for bilingual libraries.
- EndNote group and smart-group discovery.
- Reproducible, review-gated classification with taxonomy, rules, evidence, conflict handling,
holdout audits, and a read-only
Custom 1writeback preview. - Two narrow, backup-first write tools: correcting text fields (volume, issue, pages, article number, number of volumes, journal, notes, title, abstract, DOI, the author list, the keyword list), and tagging records in Custom 2–7 so the user can bulk-act on them with an EndNote Smart Group.
- Incremental PDF text extraction, full-text search, and page-range reading by title or EndNote record number.
- Consistent record-number, title, and DOI selectors with ambiguity reporting instead of first-match guessing.
- APA 7th and GB/T 7714-2015 citations plus BibTeX export.
- Incremental background re-indexing when the source library changes.
Safety model
The normal read path never writes to .enl, .MYD, .MYI, or attachment files. Classification
state and extracted PDF text live in a separate local SQLite index. prepare_classification_writeback
only produces a fingerprinted preview; there is still no commit path for the Custom 1 topic field.
Two tools do write to the library: update_reference_fields (volume, issue, pages, article
number, number of volumes, journal, notes, title, abstract, DOI, author list, keyword list; year
cannot be changed — see below) and mark_references (a
marker string into Custom 2–7, meant to drive an EndNote Smart Group rather than delete anything
directly — this server never deletes a record). Both share the same guardrails:
- The column allowlist is mechanical, not just policy:
writeback.WRITABLE_COLUMNSonly accepts columns verified to sit outside every EndNote.MYIkey and sort cache (csort/misc.DbSortedRecList).author,secondary_title(journal) andkeywordsare the one case that looked risky on paper — they back three of EndNote's own term lists — soterms.MYDwas read byte for byte against a real library rather than assumed safe: all 4818 rows fit one fixed shape (a row id, alist_idnaming which term list, a length-prefixed string) with no field anywhere linking a term back to therefsrecords that use it. That makesterms/jtermsa denormalized autocomplete cache, not a structurerefsrows are validated against — writing straight torefsjust leaves that cache stale (old values listed, new ones missing) until the user reopens EndNote's Tools/References → Define Term Lists → Update List, which is a UI-only gap, not a corruption risk. See the module docstring inwriteback.pyfor the full byte-level evidence, including empirical confirmation that a stalemisc.DbSortedRecListsort cache does not break a real library either.titlegot its own pass for the same reasonyeardidn't: a byte-level read of the realrefs.MYI(19 x 1024-byte pages) located both key roots directly — page 1 (id) and page 13 (yearIndex, confirmed by finding the literal text"2017"inside it) — and every page in the file is accounted for between the two trees, with none left over for atitleindex to hide in. yearcannot be written at all: it is one of the library's only two MyISAM keys, and changing an indexed value means removing and reinserting a B-tree key, not just editing bytes in place. A 2026-08 spike against the realrefs.MYI(read-only) found the key roots and page layout but stopped short of the page-internal format and the insert/delete/rebalance algorithm — treatyear, like record deletion, as a separate future project, not part of this tool.- Both tools default to
dry_run=True: the first call always returns a diff and writes nothing. A second, explicitdry_run=Falsecall is required to commit. - A commit only ever touches fields that actually change, and always backs up the entire library
(
.enlplus the wholerdbdirectory) to a fresh timestamped folder first — seebackup_pathin the result. - Every write re-reads the rewritten file block by block and verifies the target columns hold exactly the intended value and every other record is byte-identical to before, raising before anything is copied over the library on any mismatch.
- The
.enlmirror is rebuilt and checked beforerefs.MYD/refs.MYIare touched, so a repack that was going to fail costs nothing instead of leaving therdbtables updated and the mirror stale. Every member of the archive is carried across and onlyrefs.MYDis substituted: the libraries inspected so far hold justrefs.MYDandrefs.frm, but silently dropping an unanticipated member would be unrecoverable. - Close EndNote before a real commit. Both tools write
refs.MYD/refs.MYI/.enldirectly, outside EndNote's own process; writing while EndNote has the library open risks a sharing conflict or, worse, EndNote's stale in-memory copy silently overwriting the change later. A best-effort lock check (writeback._ensure_unlocked) refuses the write if the files look open elsewhere, but it is not a guarantee — a process that opened with share-read/share-write access would not be caught. Close EndNote regardless of what the check says.
The rest of writeback.py's low-level API (arbitrary-column set_column/set_fields) remains
unexposed to MCP clients; only the two narrow, field-scoped wrappers in commit.py are tools. See
SECURITY.md before extending writeback further, and test any change against a
disposable copy of a real library before trusting it against a live one.
Requirements
- Python 3.10 or newer
- An EndNote X9
.enllibrary, an EndNote XML export, or both - Optional: PyMuPDF for PDF text extraction
The .enl library is the preferred source. An XML export is useful as a fallback, as a format
cross-check, and for learning EndNote reference-type names.
Install as a plugin (Claude Code or Codex)
The repository is also an agent plugin: the MCP server plus five skills that drive it. This is the
shortest path for either agent, and it needs uv on the PATH — the
plugin launches the server with uv run --directory <plugin>, which builds its own environment on
first use, so there is nothing to pip install yourself.
Claude Code:
/plugin marketplace add HymanLabX/endnotex9-toolkit
/plugin install endnotex9@endnotex9
Codex:
codex plugin marketplace add HymanLabX/endnotex9-toolkit
codex plugin add endnotex9@endnotex9
Both read the same tree. Claude Code takes its manifest from .claude-plugin/plugin.json; Codex
prefers .codex-plugin/plugin.json and falls back to the Claude one, and both expand the same
${CLAUDE_PLUGIN_ROOT} in .mcp.json, so one .mcp.json serves both. The two
manifests are kept in step by tests/test_plugin_manifests.py.
One Codex-specific caveat: the sandbox. Codex's default workspace-write policy confines
writes to the working directory and blocks network access, and your EndNote library almost
certainly lives outside that directory. Reading works, but lit-cleaner's CrossRef lookups need
the network, and any --commit — like every update_reference_fields/mark_references write —
needs the library's own directory to be writable. Grant those explicitly rather than reaching for
danger-full-access:
# ~/.codex/config.toml
[sandbox_workspace_write]
network_access = true
writable_roots = ["D:\\Research"] # the folder holding the .enl and its .Data directory
This changes nothing about the server's own guarantees: writes still default to a dry run, still back up the whole library first, and still refuse to run while EndNote holds the library open.
Then run the one-time setup so the server knows which library to read:
uv run --directory <plugin-dir> --extra pdf endnotex9-toolkit setup
--extra pdf matches the environment the plugin starts the server in, so setup also finishes
building it. Building that environment is what makes a first launch slow: on a machine with an
empty uv cache it can take longer than 30 seconds, which is as long as Claude Code waits for an
MCP server to connect. If the endnotex9 server shows as failed right after installing, run
setup, then reconnect it (/mcp in Claude Code) or start a new session. Before setup has run the
server still starts; every tool answers with the setup command instead of data.
/plugin and codex plugin list both report the installed directory; on a source checkout, use
the checkout root instead and point the marketplace at . (/plugin marketplace add .,
codex plugin marketplace add .).
The plugin ships five skills. Claude Code exposes them as slash commands; Codex lists them as
endnotex9:<name>:
| Skill | What it does |
|---|---|
/lit-cleaner |
Fill in or correct metadata, merge duplicates, tidy orphaned PDFs and old backups. |
/lit-downloader |
Download, verify, and natively attach full-text PDFs to selected records; or build a new collection from a Web of Science search into an EndNote import file with the PDFs already inside the library (Windows). |
/lit-classifier |
Profile, propose a taxonomy, write bounded rules, audit, and preview the Custom 1 writeback. |
/lit-deep-reader |
Close-read one paper from its attached PDF at triage/standard/deep depth, verify its claims against the full text, and optionally save a note into its Notes field. |
/lit-review-writer |
Write a review over one slice of the library — a group, a research direction, a lab, or one researcher — with every claim carrying the record it came from, and a coverage ledger beside it. |
lit-review-writer reviews a closed corpus: it freezes the resolved records into a corpus.json,
reads them at an agreed depth, and its cite_audit.py refuses any draft citing a record outside
that file. What it can see is the collection, not the field, and it says so in the review it
writes.
lit-deep-reader writes nothing by itself: its notes_block.py merges the reading note into
whatever the Notes field already holds and prints the update_reference_fields payload, so the
write still goes through that tool's dry-run-then-confirm gate.
lit-downloader is Windows-only and runs from the lit-downloader extra (Playwright, PyMuPDF,
pywinauto); its own SKILL.md gives the uv run --extra lit-downloader … form. It drives the
Microsoft Edge already installed on the machine (channel="msedge"), so Playwright has no browser
to download.
Its new-collection mode (scripts/collection/) takes a search from export to import: merge and
tier the exports by topic rules, trim them, fetch open-access copies, open the remaining
publisher links in the user's own Chrome one at a time (ScienceDirect at most 100 a day, every
site stopped at its first block page), and write an RIS file whose internal-pdf:// links
EndNote keeps on import. The library's database is never written, but the PDFs are copied into
its <Library>.Data/PDF folder ahead of the import, which the user runs.
Install as a plain package
For Claude Desktop and every other MCP client — and for Codex if you would rather register the server by hand than install the plugin — install the package directly.
Clone the repository, create a virtual environment, and install the package:
python -m venv .venv
python -m pip install -e ".[pdf]"
Activate the environment before running commands, or use the executable inside .venv by its
absolute path. Omit [pdf] if PDF extraction is not needed.
Run the interactive setup once:
endnotex9-toolkit setup
endnotex9-toolkit index-pdfs
endnotex9-toolkit status
For scripts or managed installations, pass paths directly; supplying path flags makes setup non-interactive:
endnotex9-toolkit setup --library "/path/to/Library.enl"
endnotex9-toolkit setup --xml "/path/to/export.xml" --pdf-dir "/path/to/Library.Data/PDF"
endnotex9-toolkit doctor --json
Use --skip-index to write configuration only, --db-path for a custom index location, and
--no-watch to disable background refresh. Non-interactive setup rejects missing source paths
instead of saving a configuration that cannot run.
setup stores paths and the local SQLite index in the operating system's application-config
directory. It does not copy the EndNote library into this repository.
On Windows, the library setup writes is a fallback, not a pin. Every load_config call
(every CLI command, every lit-cleaner script run, and the MCP server's own startup) checks the
registry for whichever library EndNote for Windows currently has open (Recent Libraries\File1,
confirmed by a file-lock check — a merely recently used but now-closed library never counts) and
prefers it over the stored path whenever they differ. Point setup at a library once; afterward,
whichever .enl you actually have open in EndNote takes over automatically, and the stored path
is only used when nothing is currently open (EndNote isn't running, or you're on a non-Windows
platform, or the library is only ever an XML export). endnotex9-toolkit status marks the line
(auto-detected: currently open in EndNote) when this override is active.
Connect an MCP client
Codex
Installing the plugin already registers the server; codex mcp list will show it. To register it
by hand instead:
codex mcp add endnotex9 -- endnotex9-toolkit serve
codex mcp get endnotex9 --json
If endnotex9-toolkit is not on the PATH used by Codex, replace it with the absolute path to the
virtual environment executable. A TOML example is available at
examples/codex-config.toml.
Claude Desktop or Claude Code
Add the following server entry to the relevant MCP configuration:
{
"mcpServers": {
"endnotex9": {
"command": "endnotex9-toolkit",
"args": ["serve"]
}
}
}
See examples/claude-mcp.json. Use an absolute executable path when
the client does not inherit the virtual environment's PATH.
Other clients
Use the stdio transport and launch:
endnotex9-toolkit serve
Example requests
Search my library for elastocaloric cooling in both English and Chinese.
Read pages 1-3 of "Achieving excellent superelasticity and extraordinary
elastocaloric effect in a directionally solidified Co-V-Ga alloy".
Profile the library, propose a topic taxonomy, and wait for my approval before
evaluating classification rules.
Format these references in GB/T 7714-2015 and export their BibTeX entries.
MCP tools
Search and browse
| Tool | Purpose |
|---|---|
search_references |
Search titles, authors, abstracts, and keywords with filters and bilingual query merging. |
list_references |
Browse by author, journal, keyword, year, type, language, label, group, or topic. |
find_related |
Find a term-overlap neighbourhood selected by record number, title, or DOI. |
list_groups |
List EndNote groups and smart groups. |
list_topics |
Count topic labels stored in the index. |
library_stats |
Report corpus size, coverage, years, journals, authors, keywords, groups, and PDF status. |
get_reference_details |
Return complete metadata selected by record number, title, or DOI. |
PDF full text
| Tool | Purpose |
|---|---|
index_pdfs |
Incrementally extract text from selected or all attachments. |
search_fulltext |
Search extracted page text and return matching pages and excerpts. |
read_pdf_pages |
Read a page range by title, DOI, or rec_number. Ambiguous selectors return candidates. |
PyMuPDF is optional because it is AGPL-3.0/commercially licensed while this project is MIT. Scanned PDFs without a text layer currently require external OCR.
Citation and export
| Tool | Purpose |
|---|---|
get_citation |
Format one APA 7th or GB/T 7714-2015 citation selected by title, DOI, or record number. |
get_bibliography |
Format an ordered list of mixed title, DOI, and record-number selectors. |
get_bibtex |
Export mixed selectors as BibTeX while preserving EndNote labels as citation keys. |
Classification
The classification API deliberately uses explicit review gates:
profile_libraryandsample_referencescreate_classification_sessionsave_classification_taxonomyandapprove_classification_taxonomysave_classification_rulesandfreeze_classification_rulesevaluate_classification_rulesandlist_unresolved_classificationscreate_classification_audit,submit_classification_audit, andget_classification_reportprepare_classification_writebackfor a read-only, fingerprinted diff
Taxonomies and rules are versioned and bound to a library fingerprint. Rule predicates are declarative and bounded; arbitrary Python, SQL, and regular-expression execution is rejected.
Writing back
| Tool | Purpose |
|---|---|
update_reference_fields |
Fill in or correct volume, issue, pages, article number, number of volumes, journal, notes, title, abstract, DOI, the author list, or the keyword list. Defaults to a dry_run diff preview; dry_run=False backs up the library and commits. |
mark_references |
Tag records in Custom 2–7 with a marker (e.g. for an EndNote Smart Group to bulk-delete against). Same dry_run/backup pattern; never deletes anything itself. |
See Safety model for exactly what these tools can and cannot touch.
Maintenance
| Tool | Purpose |
|---|---|
rebuild_index |
Refresh metadata and groups, incrementally by default. |
index_pdfs |
Refresh extracted PDF text, incrementally by default. |
The CLI also provides index, index-pdfs, crosscheck, status, and read-only doctor
commands.
How the data path works
EndNote X9 .enl / <Library>.Data/rdb ── read-only parser ──┐
├─ local SQLite + FTS5 ─ MCP stdio
Optional XML export ── fallback / cross-check ─────────────┤
<Library>.Data/PDF ── optional extraction ─────────────────┘
EndNote X9 stores the working tables in MyISAM-style files. The parser walks those files directly
and rejects torn or malformed reads instead of silently accepting partial data. Existing groups
are decoded from the library's misc table; they are not present in the XML export.
Privacy
Do not commit a real library, XML export, .Data directory, extracted index, or PDF attachment.
The repository's .gitignore excludes common EndNote data and local MCP configuration. Tests use
synthetic fixtures created in temporary directories.
Attachment paths are resolved only inside the configured PDF directory, including after URL
decoding and symlink resolution. XML imports reject DTD/entity declarations and disable network
and entity resolution in lxml.
Development
python -m pip install -e ".[pdf,dev]"
ruff check src tests
pytest
python -m build
See the architecture guide for module boundaries and CONTRIBUTING.md for contribution and independent-implementation rules.
Limitations and roadmap
- EndNote versions newer than X9 require an explicit
crosscheckbefore they are trusted. - OCR for scanned PDFs is not included.
- Semantic/embedding search is not included.
- EndNote groups and smart groups are read-only;
mark_referenceswrites a Custom-field marker for the user to build a Smart Group from, but does not create or edit one itself. - Writing is limited to
update_reference_fields's text/list-field allowlist (volume, issue, pages, article number, number of volumes, journal, notes, title, abstract, DOI, author, keywords) and Custom 2–7 viamark_references; there is still no commit path forCustom 1, no field beyond those allowlists (yearstays read-only: it is a MyISAM index key, and changing it needs real B-tree writes), and no record-deletion capability at all. - Writing
author/journal/keywordsdoes not update EndNote's Author/Journals/Keywords term lists; run Tools/References → Define Term Lists → Update List in EndNote afterward if stale autocomplete entries matter to you. - Citation styles are currently limited to APA 7th and GB/T 7714-2015.
License and trademarks
Released under the MIT License.
EndNote is a trademark of Clarivate. This independent project is not affiliated with or endorsed by Clarivate. MCP client names are used only to describe interoperability.