# citeguard [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.22115109.svg)](https://doi.org/10.5281/zenodo.22115109) [![PyPI](https://img.shields.io/pypi/v/citeguard-cli?label=PyPI)](https://pypi.org/project/citeguard-cli/) [![npm](https://img.shields.io/npm/v/citeguard-mcp-server?label=npm)](https://www.npmjs.com/package/citeguard-mcp-server) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) Retracted papers get cited for years after retraction. Andrew Wakefield's fraudulent 1998 paper linking the MMR vaccine to autism — retracted in 2010 — has been cited well over a thousand times *since* its retraction, by researchers who had no easy way to know. A 2026 JMIR study found that freely available AI tools "cannot reliably flag retracted literature," right as AI-assisted research and writing has exploded. citeguard closes that gap: check a citation against [Crossref](https://www.crossref.org/) — free, no API key — before it goes in a paper, a summary, or a review. Three surfaces, one verified detection algorithm: | Surface | For | Location | |---|---|---| | **Python library + CLI** | academic writers, scripts | [`src/citeguard/`](src/citeguard/) | | **MCP server** | AI agents doing research/writing | [`mcp-server/`](mcp-server/) | | **GitHub Action** | CI on a lab's or journal's repo | [`action.yml`](action.yml) | ## Why this is built the way it is The detection logic isn't guessed at from documentation — it was built by querying Crossref's real API for known cases and reading the actual response shapes, then writing tests against the saved real responses (committed in `tests/fixtures/`, shared by both the Python and TypeScript implementations). Two real papers anchor the ground truth: - **[Wakefield et al., 1998, *The Lancet*](https://doi.org/10.1016/S0140-6736(97)11096-0)** (the MMR-autism paper): the publisher's own Crossref metadata has *no* structured retraction data — an `update-to` check alone would silently miss it. What actually carries the signal is a separate `updated-by` field, where Crossref has backfilled the retraction (and an earlier 2004 correction) from the **Retraction Watch database** itself. Missing this field would have meant missing the single most famous retracted paper in medicine. - **[Mehra et al., 2020, *The Lancet*](https://doi.org/10.1016/S0140-6736(20)31180-6)** (the Surgisphere-linked COVID/hydroxychloroquine paper): here the *publisher* did attach structured data directly, via `update-to`. A different field, a different provenance, same underlying fact. - **[Watson & Crick, 1953, *Nature*](https://doi.org/10.1038/171737a0)** serves as the clean control in every test suite — a definitely-real, definitely-not-retracted paper that must never be flagged. So the checker looks at three independent signals, in order: `update-to` (publisher-asserted), `updated-by` (often Retraction-Watch-sourced, catching what publishers miss), and a title-prefix fallback ("RETRACTED:", "WITHDRAWN:", etc.) for older or unlinked cases with no structured metadata on either field. Each signal is mapped to the right severity — an "Expression of Concern:" title is not the same thing as a retraction, and earlier versions of this code collapsing that distinction was itself a bug caught by testing against real titles, not just synthetic ones. See [`src/citeguard/analyze.py`](src/citeguard/analyze.py) for the fully-commented implementation. ## Quick start **CLI:** ```bash pip install citeguard-cli citeguard doi 10.1016/S0140-6736(97)11096-0 citeguard file references.bib --fail-on retracted # for CI, see below ``` The distribution is named `citeguard-cli` on PyPI because the plain `citeguard` name there belongs to an unrelated project. The command and the import are still `citeguard`. **MCP server** (add to your client's config, e.g. `.mcp.json`): ```json { "mcpServers": { "citeguard": { "command": "npx", "args": ["-y", "citeguard-mcp-server"] } } } ``` Published on npm as [`citeguard-mcp-server`](https://www.npmjs.com/package/citeguard-mcp-server) — nothing to clone or build. To run it from a local checkout instead, point `command` at `node` and `args` at `mcp-server/dist/index.js`. **GitHub Action** — on the [GitHub Marketplace](https://github.com/marketplace/actions/citeguard-retracted-citation-checker). Add to any repository that holds a manuscript and its bibliography: ```yaml - uses: wedo911/citeguard@v0.1.2 with: path: references.bib fail-on: concern # never | retracted | concern | corrected ``` ## What this is *not* - **Not proof a paper's content is correct.** It only checks retraction status, not whether a non-retracted paper's findings hold up. - **Not exhaustive.** The title-prefix heuristic only catches the publisher conventions it's been tested against; a clean result means "no known signal found," not "guaranteed never retracted." - **Not a bulk-scraping tool.** It's built for the size of a real bibliography (tens of citations), with a small fixed delay between Crossref requests and an optional persistent cache ([`src/citeguard/cache.py`](src/citeguard/cache.py)) — good API citizenship for a free public service, not a tool for scanning millions of DOIs. ## Running the tests ```bash # Python (55 tests, including against the real fixtures above) pip install -e ".[dev]" && pytest -v # MCP server (17 tests against the same real fixtures) cd mcp-server && npm install && npm run build && npm test ``` Both suites are network-free and deterministic — they run against the committed real API responses, not live calls, so they're fast and don't depend on Crossref being reachable. Live end-to-end behavior (the actual CLI, the actual MCP tool, hitting the real API) was separately verified by hand during development; the GitHub Action additionally has its own CI job (`action-smoke-test`) that runs the real composite action against a known-retracted and a known-clean bibliography on every push, so the Action itself — not just the underlying library — is continuously verified against the live API. ## Contributing New signal types, additional publisher title conventions, and false-positive reports are all welcome. If you add a case, prefer adding it as a real, cited Crossref fixture over a synthetic one where possible — that's what caught the two real bugs this project's own test suite found during development (a URL-encoding bug in the DOI request path, and a BibTeX parser that could swallow an adjacent entry when parsing a malformed `@comment` block). ## Citing citeguard Archived on Zenodo with a DOI, so it can be cited in a paper: > Bajaman, W. (2026). *citeguard: retraction, correction, and expression-of-concern > checking for citations.* Zenodo. https://doi.org/10.5281/zenodo.22115109 That DOI always resolves to the latest archived version. GitHub's **Cite this repository** button generates BibTeX and APA from [`CITATION.cff`](CITATION.cff). ## License MIT — see [LICENSE](LICENSE).