Skip to main content
Glama
README.md
# 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).

TDQS

A4.4/5.0

Scored across 2 tools

Disambiguation4/5

check_citations (bulk scan of text) and check_citation (single DOI) have overlapping purposes, and the near-identical singular/plural names invite misselection. However, each description explicitly cross-references the other with 'use X instead' guidance, so an agent can reliably choose the right one.

Naming Consistency4/5

Both tools use consistent snake_case with a clear verb_noun shape, and the singular/plural convention neatly signals single vs. bulk behavior. The only downside is that the two names differ by a single character, which is a mild collision risk.

Tool Count4/5

Two tools is on the thin side, but the domain (checking retraction status of DOIs) is genuinely narrow and the single/bulk split is the one meaningful axis of variation. Nothing feels gratuitous, though a third tool would be hard to justify.

Completeness4/5

The surface covers the core lifecycle: check one DOI, check many DOIs in a block of text, with defined verdicts and error handling. A minor gap is the absence of any way to resolve a citation lacking a DOI (e.g. by title/author) into a checkable identifier.

Maintenance

ActivityMaintained
ResponsivenessNo issues