New feature: pubmed_fetch_fulltext now has a second source. When an
article isn't in PubMed Central but has a DOI, the tool resolves the DOI
via Unpaywall and fetches the legal open-access copy (PDF preferred,
HTML fallback), extracted to Markdown via Defuddle or text via unpdf.
Opt-in via UNPAYWALL_EMAIL — absent, prior behavior is preserved, but
unavailable results now carry structured reason codes.
- src/services/unpaywall/ — resolve() + fetchContent(), init/accessor
- Discriminated output: source 'pmc' (structured sections) | 'unpaywall'
(single body + contentFormat 'html-markdown' | 'pdf-text', plus
license / version / hostType provenance)
- unavailable.reason enum: no-pmc-fallback-disabled, no-doi, no-oa,
fetch-failed, parse-failed, service-error
- Bug fix (regression discovered via live field test): PMC ID Converter
omits DOI for non-PMC PMIDs, so the Unpaywall path was unreachable
for the exact case it serves. Handler now batch-fetches missing DOIs
from db=pubmed via eFetch before Unpaywall dispatch.
- Config: UNPAYWALL_EMAIL + UNPAYWALL_TIMEOUT_MS (Zod-validated),
declared in server.json env for stdio + HTTP packages.
- Deps: defuddle ^0.18.1, linkedom ^0.18.12, unpdf ^1.6.0
- Tests: 445 passed / 4 skipped (+23); new unpaywall-service.test.ts
(18 tests); db-aware mockEFetchBy helper with a named regression test
for the DOI-sourcing gap. devcheck green across all 11 checks.
- Chore: issue templates auto-assign to cyanheads.