preprint-fulltext 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. preprint_fulltext-0.1.0/.claude-plugin/marketplace.json +15 -0
  2. preprint_fulltext-0.1.0/.claude-plugin/plugin.json +12 -0
  3. preprint_fulltext-0.1.0/.cursor/rules/preprint-fulltext.mdc +106 -0
  4. preprint_fulltext-0.1.0/.github/copilot-instructions.md +102 -0
  5. preprint_fulltext-0.1.0/.github/workflows/live-smoke.yml +28 -0
  6. preprint_fulltext-0.1.0/.github/workflows/publish-pypi.yml +26 -0
  7. preprint_fulltext-0.1.0/.github/workflows/publish-registry.yml +41 -0
  8. preprint_fulltext-0.1.0/.github/workflows/test.yml +43 -0
  9. preprint_fulltext-0.1.0/.gitignore +33 -0
  10. preprint_fulltext-0.1.0/AGENTS.md +102 -0
  11. preprint_fulltext-0.1.0/LICENSE +40 -0
  12. preprint_fulltext-0.1.0/PKG-INFO +304 -0
  13. preprint_fulltext-0.1.0/README.md +258 -0
  14. preprint_fulltext-0.1.0/llms.txt +108 -0
  15. preprint_fulltext-0.1.0/preprint_fulltext/__init__.py +8 -0
  16. preprint_fulltext-0.1.0/preprint_fulltext/cli.py +324 -0
  17. preprint_fulltext-0.1.0/preprint_fulltext/config.py +127 -0
  18. preprint_fulltext-0.1.0/preprint_fulltext/core/__init__.py +1 -0
  19. preprint_fulltext-0.1.0/preprint_fulltext/core/assemble.py +72 -0
  20. preprint_fulltext-0.1.0/preprint_fulltext/core/cache.py +49 -0
  21. preprint_fulltext-0.1.0/preprint_fulltext/core/chunk.py +170 -0
  22. preprint_fulltext-0.1.0/preprint_fulltext/core/http.py +71 -0
  23. preprint_fulltext-0.1.0/preprint_fulltext/core/ids.py +133 -0
  24. preprint_fulltext-0.1.0/preprint_fulltext/core/jats.py +240 -0
  25. preprint_fulltext-0.1.0/preprint_fulltext/core/licenses.py +131 -0
  26. preprint_fulltext-0.1.0/preprint_fulltext/core/models.py +153 -0
  27. preprint_fulltext-0.1.0/preprint_fulltext/mcp_server.py +270 -0
  28. preprint_fulltext-0.1.0/preprint_fulltext/pipeline/__init__.py +1 -0
  29. preprint_fulltext-0.1.0/preprint_fulltext/pipeline/export.py +84 -0
  30. preprint_fulltext-0.1.0/preprint_fulltext/pipeline/ingest.py +153 -0
  31. preprint_fulltext-0.1.0/preprint_fulltext/pipeline/resolve.py +45 -0
  32. preprint_fulltext-0.1.0/preprint_fulltext/pipeline/router.py +176 -0
  33. preprint_fulltext-0.1.0/preprint_fulltext/sources/__init__.py +1 -0
  34. preprint_fulltext-0.1.0/preprint_fulltext/sources/arxiv.py +166 -0
  35. preprint_fulltext-0.1.0/preprint_fulltext/sources/arxiv_html.py +202 -0
  36. preprint_fulltext-0.1.0/preprint_fulltext/sources/base.py +47 -0
  37. preprint_fulltext-0.1.0/preprint_fulltext/sources/biorxiv_api.py +124 -0
  38. preprint_fulltext-0.1.0/preprint_fulltext/sources/biorxiv_html.py +200 -0
  39. preprint_fulltext-0.1.0/preprint_fulltext/sources/biorxiv_s3.py +249 -0
  40. preprint_fulltext-0.1.0/preprint_fulltext/sources/europepmc.py +169 -0
  41. preprint_fulltext-0.1.0/preprint_fulltext/sources/openalex.py +170 -0
  42. preprint_fulltext-0.1.0/pyproject.toml +72 -0
  43. preprint_fulltext-0.1.0/scripts/build_agent_docs.py +81 -0
  44. preprint_fulltext-0.1.0/server.json +35 -0
  45. preprint_fulltext-0.1.0/skills/preprint-fulltext/SKILL.md +113 -0
  46. preprint_fulltext-0.1.0/tests/__init__.py +0 -0
  47. preprint_fulltext-0.1.0/tests/conftest.py +45 -0
  48. preprint_fulltext-0.1.0/tests/factories.py +57 -0
  49. preprint_fulltext-0.1.0/tests/fixtures/arxiv_api.xml +18 -0
  50. preprint_fulltext-0.1.0/tests/fixtures/arxiv_latexml.html +41 -0
  51. preprint_fulltext-0.1.0/tests/fixtures/biorxiv_details.json +18 -0
  52. preprint_fulltext-0.1.0/tests/fixtures/biorxiv_page.html +44 -0
  53. preprint_fulltext-0.1.0/tests/fixtures/epmc_resolve.json +15 -0
  54. preprint_fulltext-0.1.0/tests/fixtures/epmc_search_page1.json +12 -0
  55. preprint_fulltext-0.1.0/tests/fixtures/epmc_search_page2.json +10 -0
  56. preprint_fulltext-0.1.0/tests/fixtures/jats_biorxiv_arr.xml +43 -0
  57. preprint_fulltext-0.1.0/tests/fixtures/jats_biorxiv_sample.xml +78 -0
  58. preprint_fulltext-0.1.0/tests/fixtures/openalex_search.json +24 -0
  59. preprint_fulltext-0.1.0/tests/fixtures/openalex_work.json +7 -0
  60. preprint_fulltext-0.1.0/tests/test_arxiv.py +147 -0
  61. preprint_fulltext-0.1.0/tests/test_assemble.py +59 -0
  62. preprint_fulltext-0.1.0/tests/test_biorxiv_api.py +97 -0
  63. preprint_fulltext-0.1.0/tests/test_biorxiv_html.py +108 -0
  64. preprint_fulltext-0.1.0/tests/test_biorxiv_s3.py +147 -0
  65. preprint_fulltext-0.1.0/tests/test_chunk.py +113 -0
  66. preprint_fulltext-0.1.0/tests/test_cli_discover_search.py +57 -0
  67. preprint_fulltext-0.1.0/tests/test_cli_get.py +66 -0
  68. preprint_fulltext-0.1.0/tests/test_cli_ingest.py +78 -0
  69. preprint_fulltext-0.1.0/tests/test_config.py +81 -0
  70. preprint_fulltext-0.1.0/tests/test_europepmc.py +139 -0
  71. preprint_fulltext-0.1.0/tests/test_export.py +64 -0
  72. preprint_fulltext-0.1.0/tests/test_ids.py +79 -0
  73. preprint_fulltext-0.1.0/tests/test_ingest.py +116 -0
  74. preprint_fulltext-0.1.0/tests/test_jats.py +120 -0
  75. preprint_fulltext-0.1.0/tests/test_licenses.py +60 -0
  76. preprint_fulltext-0.1.0/tests/test_live.py +137 -0
  77. preprint_fulltext-0.1.0/tests/test_mcp.py +165 -0
  78. preprint_fulltext-0.1.0/tests/test_models.py +147 -0
  79. preprint_fulltext-0.1.0/tests/test_openalex.py +104 -0
  80. preprint_fulltext-0.1.0/tests/test_router.py +154 -0
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "preprint-fulltext",
3
+ "owner": {
4
+ "name": "Min Dai",
5
+ "email": "dai@broadinstitute.org",
6
+ "url": "https://fishelllab.hms.harvard.edu"
7
+ },
8
+ "plugins": [
9
+ {
10
+ "name": "preprint-fulltext",
11
+ "source": ".",
12
+ "description": "Retrieve bioRxiv/medRxiv/arXiv preprint full text as structured sections; search/discover preprints; build embedding-ready corpora. CLI + MCP."
13
+ }
14
+ ]
15
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "preprint-fulltext",
3
+ "version": "0.1.0",
4
+ "description": "Retrieve full text of bioRxiv/medRxiv/arXiv preprints as structured sections to ground scientific reasoning and deep research, search/discover preprints, and build embedding-ready corpora (Europe PMC + openRxiv S3 + arXiv LaTeXML), via CLI and MCP.",
5
+ "author": {
6
+ "name": "Min Dai",
7
+ "email": "dai@broadinstitute.org",
8
+ "url": "https://fishelllab.hms.harvard.edu"
9
+ },
10
+ "homepage": "https://github.com/genecell/preprint-fulltext",
11
+ "license": "BSD-3-Clause"
12
+ }
@@ -0,0 +1,106 @@
1
+ ---
2
+ description: Retrieve full text of bioRxiv/medRxiv/arXiv preprints as structured sections to ground scientific reasoning and deep research (methods/results, not just abstracts), or build embedding-ready corpora. Use when a task needs a preprint's full text or sections from a DOI/arXiv id/URL; to verify a claim against a paper's Results; to extract methods or parameters; keyword/title/author search over preprints; discovery by topic/date; or a chunked JSONL/Parquet corpus for embeddings/RAG. Triggers on: bioRxiv, medRxiv, arXiv, openRxiv, preprint full text, JATS, a 10.1101/ or 10.64898/ DOI, an arXiv id (2401.10515), "get the methods/results section", "what does this paper actually do", "search preprints".
3
+ globs:
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # preprint-fulltext
8
+
9
+ A CLI + Python library + MCP server for retrieving bioRxiv/medRxiv preprint full text
10
+ and turning it into structured, embedding-ready data. One canonical model and one
11
+ shared JATS parser back every path.
12
+
13
+ ## When to use
14
+
15
+ - The user gives a **preprint DOI or bioRxiv/medRxiv URL** and wants the full text or
16
+ a specific section (methods, results, …).
17
+ - The user wants to **search** preprints by keyword, **title**, **abstract**, or
18
+ **author**, or **discover** them by topic/category/date.
19
+ - The user wants a **corpus** of preprint full text chunked for embeddings/RAG.
20
+
21
+ Do NOT use for published-journal full text in general (this is preprint-scoped), and
22
+ do not scrape the bioRxiv website in bulk — use `ingest --source s3` for corpora.
23
+
24
+ ## Why full text (for grounded reasoning)
25
+
26
+ Prefer full text over abstracts when the task needs real evidence: methods, quantities,
27
+ protocols, datasets, and results live in the body. `get_fulltext` returns section-labeled
28
+ text (`kind` = methods / results / discussion / …) with `source`, `version`, and `license`,
29
+ so you can:
30
+ - **cite precisely** — quote/attribute the exact section of a specific paper + version;
31
+ - **verify claims** — check an assertion against the paper's Results, not its abstract;
32
+ - **extract methods/parameters** — read the Methods section directly;
33
+ - **stay within-license** — each Section/Chunk carries its license and a link-back source.
34
+ This grounds reasoning in the primary source rather than a recalled (possibly stale) summary.
35
+
36
+ ## CLI
37
+
38
+ ```bash
39
+ # One preprint -> structured sections (Europe PMC -> S3 router). Accepts DOI, doi.org
40
+ # URL, or a bioRxiv/medRxiv content URL. --markdown for readable output; default JSON.
41
+ preprint-fulltext get <DOI|URL> [--markdown] [--version N] [--source europepmc|s3|html] [--html]
42
+
43
+ # Search -> one SearchHit per line (JSONL). --field scopes the query.
44
+ preprint-fulltext search "<query>" [--field fulltext|title|abstract|author] [--source europepmc|openalex] [-n N]
45
+
46
+ # Discover by topic/category/date -> SearchHit JSONL (OpenAlex by default).
47
+ preprint-fulltext discover [--query Q] [--category C] [--since YYYY-MM] [--to YYYY-MM-DD] [-n N]
48
+
49
+ # Bulk corpus -> Chunk JSONL/Parquet + <out>_manifest.jsonl (resumable, incremental).
50
+ preprint-fulltext ingest <out.jsonl> --source s3|europepmc [--server biorxiv|medrxiv|both]
51
+ [--since YYYY-MM] [--redistribution] [--format jsonl|parquet] [-n N]
52
+ ```
53
+
54
+ Notes for agents:
55
+ - **arXiv:** `get` accepts an arXiv id (`2401.10515`), `arXiv:` prefix, an arxiv.org URL, or
56
+ the `10.48550/arXiv.*` DOI, and returns full text from arXiv's LaTeXML HTML (native →
57
+ ar5iv). `search --source arxiv` / `discover --source arxiv` query the arXiv API. arXiv is
58
+ **not** available in `ingest` (single-document only).
59
+ - **Versions:** a DOI resolves to the **latest version by default** (openRxiv semantics).
60
+ Pass `--version N` for a specific one. Europe PMC only serves the latest indexed
61
+ version, so an explicit older version is fetched via the HTML source (`--source html
62
+ --version 1`) or S3. The retrieved version is reported in `preprint.version`.
63
+ - `get` with no `--source` tries Europe PMC, then S3 (if AWS creds), and only tries the
64
+ public HTML page if you pass `--html`. Europe PMC full text exists for the **CC /
65
+ open-access subset**; all-rights-reserved preprints need S3 or `--html`.
66
+ - `--field title`/`abstract` match **all terms** (not an exact phrase); `--field author`
67
+ matches an author name. For topical multi-word queries prefer `--field fulltext`.
68
+ - `ingest --source s3` is **requester-pays** (needs AWS credentials and costs money;
69
+ ~$0.09/GB). `ingest --source europepmc` is free but only covers the CC subset.
70
+ - `--redistribution` degrades non-redistributable works to metadata-only stubs.
71
+
72
+ ## Python
73
+
74
+ ```python
75
+ from preprint_fulltext.pipeline.router import Router
76
+ from preprint_fulltext.core.chunk import chunk_fulltext
77
+
78
+ result = Router().get_fulltext("10.1101/2024.01.15.575000") # -> GetResult
79
+ ft = result.fulltext # FullText | None (result.reason if None)
80
+ chunks = chunk_fulltext(ft) # list[Chunk], embedding-ready
81
+ ```
82
+
83
+ ## MCP tools
84
+
85
+ Run `preprint-fulltext-mcp` (stdio). Tools:
86
+ - `search_preprints(query, source?, limit?)` → list of hits
87
+ - `get_fulltext(doi, as_markdown?)` → structured sections (or Markdown)
88
+ - `get_metadata(doi)` → bibliographic metadata (abstract only)
89
+ - `resolve(doi_or_openalex_id)` → cross-identifier resolution
90
+
91
+ Bulk `ingest` is intentionally not an MCP tool.
92
+
93
+ ## Data model (what you get back)
94
+
95
+ - `FullText`: `preprint` + ordered `sections` (`kind` ∈ abstract|intro|methods|results|
96
+ discussion|other) + `retrieved_from`.
97
+ - `Chunk` (the corpus record): `doi, version, chunk_id, section_kind, text,
98
+ token_count, char_start, char_end, license, source`. One JSONL line per chunk.
99
+ - `SearchHit`: `title, doi?, openalex_id?, score?, source, snippet?, oa_url?, has_fulltext`.
100
+
101
+ ## Gotchas
102
+
103
+ - openRxiv DOIs use prefix **`10.64898`** since 2025-12 (legacy **`10.1101`** before).
104
+ - OpenAlex requires an API key (`OPENALEX_API_KEY`) since 2026-02-13; missing → HTTP 409.
105
+ - Set `CONTACT_EMAIL` for the Europe PMC / OpenAlex polite pools.
106
+ - Retrieved content keeps its own license; corpora are for your own TDM, not re-hosting.
@@ -0,0 +1,102 @@
1
+ <!-- GENERATED from skills/preprint-fulltext/SKILL.md by scripts/build_agent_docs.py — do not edit. -->
2
+
3
+ # preprint-fulltext
4
+
5
+ A CLI + Python library + MCP server for retrieving bioRxiv/medRxiv preprint full text
6
+ and turning it into structured, embedding-ready data. One canonical model and one
7
+ shared JATS parser back every path.
8
+
9
+ ## When to use
10
+
11
+ - The user gives a **preprint DOI or bioRxiv/medRxiv URL** and wants the full text or
12
+ a specific section (methods, results, …).
13
+ - The user wants to **search** preprints by keyword, **title**, **abstract**, or
14
+ **author**, or **discover** them by topic/category/date.
15
+ - The user wants a **corpus** of preprint full text chunked for embeddings/RAG.
16
+
17
+ Do NOT use for published-journal full text in general (this is preprint-scoped), and
18
+ do not scrape the bioRxiv website in bulk — use `ingest --source s3` for corpora.
19
+
20
+ ## Why full text (for grounded reasoning)
21
+
22
+ Prefer full text over abstracts when the task needs real evidence: methods, quantities,
23
+ protocols, datasets, and results live in the body. `get_fulltext` returns section-labeled
24
+ text (`kind` = methods / results / discussion / …) with `source`, `version`, and `license`,
25
+ so you can:
26
+ - **cite precisely** — quote/attribute the exact section of a specific paper + version;
27
+ - **verify claims** — check an assertion against the paper's Results, not its abstract;
28
+ - **extract methods/parameters** — read the Methods section directly;
29
+ - **stay within-license** — each Section/Chunk carries its license and a link-back source.
30
+ This grounds reasoning in the primary source rather than a recalled (possibly stale) summary.
31
+
32
+ ## CLI
33
+
34
+ ```bash
35
+ # One preprint -> structured sections (Europe PMC -> S3 router). Accepts DOI, doi.org
36
+ # URL, or a bioRxiv/medRxiv content URL. --markdown for readable output; default JSON.
37
+ preprint-fulltext get <DOI|URL> [--markdown] [--version N] [--source europepmc|s3|html] [--html]
38
+
39
+ # Search -> one SearchHit per line (JSONL). --field scopes the query.
40
+ preprint-fulltext search "<query>" [--field fulltext|title|abstract|author] [--source europepmc|openalex] [-n N]
41
+
42
+ # Discover by topic/category/date -> SearchHit JSONL (OpenAlex by default).
43
+ preprint-fulltext discover [--query Q] [--category C] [--since YYYY-MM] [--to YYYY-MM-DD] [-n N]
44
+
45
+ # Bulk corpus -> Chunk JSONL/Parquet + <out>_manifest.jsonl (resumable, incremental).
46
+ preprint-fulltext ingest <out.jsonl> --source s3|europepmc [--server biorxiv|medrxiv|both]
47
+ [--since YYYY-MM] [--redistribution] [--format jsonl|parquet] [-n N]
48
+ ```
49
+
50
+ Notes for agents:
51
+ - **arXiv:** `get` accepts an arXiv id (`2401.10515`), `arXiv:` prefix, an arxiv.org URL, or
52
+ the `10.48550/arXiv.*` DOI, and returns full text from arXiv's LaTeXML HTML (native →
53
+ ar5iv). `search --source arxiv` / `discover --source arxiv` query the arXiv API. arXiv is
54
+ **not** available in `ingest` (single-document only).
55
+ - **Versions:** a DOI resolves to the **latest version by default** (openRxiv semantics).
56
+ Pass `--version N` for a specific one. Europe PMC only serves the latest indexed
57
+ version, so an explicit older version is fetched via the HTML source (`--source html
58
+ --version 1`) or S3. The retrieved version is reported in `preprint.version`.
59
+ - `get` with no `--source` tries Europe PMC, then S3 (if AWS creds), and only tries the
60
+ public HTML page if you pass `--html`. Europe PMC full text exists for the **CC /
61
+ open-access subset**; all-rights-reserved preprints need S3 or `--html`.
62
+ - `--field title`/`abstract` match **all terms** (not an exact phrase); `--field author`
63
+ matches an author name. For topical multi-word queries prefer `--field fulltext`.
64
+ - `ingest --source s3` is **requester-pays** (needs AWS credentials and costs money;
65
+ ~$0.09/GB). `ingest --source europepmc` is free but only covers the CC subset.
66
+ - `--redistribution` degrades non-redistributable works to metadata-only stubs.
67
+
68
+ ## Python
69
+
70
+ ```python
71
+ from preprint_fulltext.pipeline.router import Router
72
+ from preprint_fulltext.core.chunk import chunk_fulltext
73
+
74
+ result = Router().get_fulltext("10.1101/2024.01.15.575000") # -> GetResult
75
+ ft = result.fulltext # FullText | None (result.reason if None)
76
+ chunks = chunk_fulltext(ft) # list[Chunk], embedding-ready
77
+ ```
78
+
79
+ ## MCP tools
80
+
81
+ Run `preprint-fulltext-mcp` (stdio). Tools:
82
+ - `search_preprints(query, source?, limit?)` → list of hits
83
+ - `get_fulltext(doi, as_markdown?)` → structured sections (or Markdown)
84
+ - `get_metadata(doi)` → bibliographic metadata (abstract only)
85
+ - `resolve(doi_or_openalex_id)` → cross-identifier resolution
86
+
87
+ Bulk `ingest` is intentionally not an MCP tool.
88
+
89
+ ## Data model (what you get back)
90
+
91
+ - `FullText`: `preprint` + ordered `sections` (`kind` ∈ abstract|intro|methods|results|
92
+ discussion|other) + `retrieved_from`.
93
+ - `Chunk` (the corpus record): `doi, version, chunk_id, section_kind, text,
94
+ token_count, char_start, char_end, license, source`. One JSONL line per chunk.
95
+ - `SearchHit`: `title, doi?, openalex_id?, score?, source, snippet?, oa_url?, has_fulltext`.
96
+
97
+ ## Gotchas
98
+
99
+ - openRxiv DOIs use prefix **`10.64898`** since 2025-12 (legacy **`10.1101`** before).
100
+ - OpenAlex requires an API key (`OPENALEX_API_KEY`) since 2026-02-13; missing → HTTP 409.
101
+ - Set `CONTACT_EMAIL` for the Europe PMC / OpenAlex polite pools.
102
+ - Retrieved content keeps its own license; corpora are for your own TDM, not re-hosting.
@@ -0,0 +1,28 @@
1
+ name: live-smoke
2
+ # Opt-in LIVE smoke against the real public APIs (Europe PMC, arXiv, bioRxiv/medRxiv JSON API).
3
+ # Kept OUT of the normal `test` job so CI stays offline/free by default. Run it manually
4
+ # before a release, or on the weekly schedule, to catch upstream API drift (e.g. an endpoint
5
+ # change or a new DOI prefix). Uses the CONTACT_EMAIL repo variable for the polite pools.
6
+ #
7
+ # NOTE: requester-pays S3 live tests are NOT run here (cost); they stay behind
8
+ # PREPRINT_FULLTEXT_LIVE_S3 and are exercised locally with credentials only.
9
+ on:
10
+ workflow_dispatch:
11
+ schedule:
12
+ - cron: "0 6 * * 1" # Mondays 06:00 UTC
13
+
14
+ jobs:
15
+ live:
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - uses: actions/setup-python@v5
20
+ with: { python-version: "3.11" }
21
+ - name: Install
22
+ run: python -m pip install -e ".[dev]"
23
+ - name: Live smoke (Europe PMC + arXiv + bioRxiv/medRxiv metadata; OpenAlex if key set)
24
+ env:
25
+ PREPRINT_FULLTEXT_LIVE: "1"
26
+ CONTACT_EMAIL: ${{ vars.CONTACT_EMAIL }}
27
+ OPENALEX_API_KEY: ${{ secrets.OPENALEX_API_KEY }} # optional; OpenAlex test skips if unset
28
+ run: pytest -m live -v
@@ -0,0 +1,26 @@
1
+ name: publish-pypi
2
+ # Publish preprint-fulltext to PyPI on a GitHub Release, via PyPI Trusted Publishing
3
+ # (OIDC — no API token needed). ONE-TIME SETUP: on pypi.org add a Trusted Publisher for
4
+ # project "preprint-fulltext" pointing at genecell/preprint-fulltext + this workflow, with
5
+ # the environment field left blank. (A GitHub Environment is optional — add one on both
6
+ # sides only if you want a manual-approval gate before publishing.)
7
+ on:
8
+ release:
9
+ types: [published]
10
+ workflow_dispatch:
11
+
12
+ jobs:
13
+ build-and-publish:
14
+ runs-on: ubuntu-latest
15
+ permissions:
16
+ id-token: write # OIDC identity for Trusted Publishing
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - uses: actions/setup-python@v5
20
+ with: { python-version: "3.11" }
21
+ - name: Build sdist + wheel
22
+ run: |
23
+ python -m pip install build
24
+ python -m build
25
+ - name: Publish to PyPI
26
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,41 @@
1
+ name: publish-registry
2
+ # Register preprint-fulltext in the official MCP Registry (registry.modelcontextprotocol.io)
3
+ # via GitHub OIDC — no tokens. The io.github.genecell/* namespace is authorized by OIDC from
4
+ # this repo (owner genecell). The registry verifies ownership by fetching the PyPI package's
5
+ # `mcp-name:` marker, so run this ONLY AFTER the new version is live on PyPI.
6
+ #
7
+ # HOW TO RELEASE: cut a GitHub Release (publish-pypi.yml uploads to PyPI) → confirm the version
8
+ # is live on pypi.org → then Actions tab → publish-registry → Run workflow.
9
+ on:
10
+ workflow_dispatch:
11
+
12
+ jobs:
13
+ publish-registry:
14
+ runs-on: ubuntu-latest
15
+ permissions:
16
+ id-token: write # OIDC identity for the io.github.genecell namespace
17
+ contents: read
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ - name: Sync server.json version to pyproject.toml
21
+ run: |
22
+ python - <<'PY'
23
+ import re, json, pathlib
24
+ ver = re.search(r'^version\s*=\s*"([^"]+)"', pathlib.Path("pyproject.toml").read_text(), re.M).group(1)
25
+ p = pathlib.Path("server.json"); d = json.loads(p.read_text())
26
+ d["version"] = ver
27
+ for pkg in d.get("packages", []):
28
+ pkg["version"] = ver
29
+ p.write_text(json.dumps(d, indent=2) + "\n")
30
+ print("server.json version ->", ver)
31
+ PY
32
+ - name: Install mcp-publisher (latest linux/amd64)
33
+ run: |
34
+ url=$(curl -s https://api.github.com/repos/modelcontextprotocol/registry/releases/latest \
35
+ | grep -o 'https://[^"]*mcp-publisher[^"]*linux_amd64.tar.gz' | head -1)
36
+ echo "Downloading $url"
37
+ curl -L "$url" | tar xz mcp-publisher
38
+ - name: Publish to the MCP Registry
39
+ run: |
40
+ ./mcp-publisher login github-oidc
41
+ ./mcp-publisher publish
@@ -0,0 +1,43 @@
1
+ name: test
2
+ # Offline test suite (respx mocks HTTP, moto mocks S3) + lint. No live network,
3
+ # no AWS, no cost — matches the project's "green offline by default" guarantee.
4
+ on:
5
+ push: { branches: [master, main] }
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.11", "3.12"]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: ${{ matrix.python-version }}
20
+ cache: pip
21
+ - name: Install (dev extras)
22
+ run: python -m pip install -e ".[dev]"
23
+ - name: Lint
24
+ run: ruff check preprint_fulltext/
25
+ - name: Test (offline)
26
+ run: pytest -q
27
+ - name: Build sdist + wheel
28
+ run: |
29
+ python -m pip install build
30
+ python -m build
31
+
32
+ docs-sync:
33
+ # Fail if the generated agent docs are stale vs skills/preprint-fulltext/SKILL.md.
34
+ runs-on: ubuntu-latest
35
+ steps:
36
+ - uses: actions/checkout@v4
37
+ - uses: actions/setup-python@v5
38
+ with: { python-version: "3.11" }
39
+ - name: Regenerate agent docs and check for drift
40
+ run: |
41
+ python scripts/build_agent_docs.py
42
+ git diff --exit-code -- AGENTS.md llms.txt .cursor .github/copilot-instructions.md \
43
+ || (echo "::error::agent docs are stale — run: python scripts/build_agent_docs.py" && exit 1)
@@ -0,0 +1,33 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ .venv/
9
+ venv/
10
+
11
+ # Tooling
12
+ .pytest_cache/
13
+ .ruff_cache/
14
+ .mypy_cache/
15
+ .coverage
16
+ htmlcov/
17
+
18
+ # Project
19
+ .cache/
20
+ *.jsonl
21
+ *.parquet
22
+ _manifest.jsonl
23
+ preprint-fulltext.toml
24
+
25
+ # stato — version the captured modules, but not transient history
26
+ .stato/.history/
27
+
28
+ # OS
29
+ .DS_Store
30
+
31
+ # internal dev context (lives at the project root, not in the public package)
32
+ .stato/
33
+ CLAUDE.md
@@ -0,0 +1,102 @@
1
+ <!-- GENERATED from skills/preprint-fulltext/SKILL.md by scripts/build_agent_docs.py — do not edit. -->
2
+
3
+ # preprint-fulltext
4
+
5
+ A CLI + Python library + MCP server for retrieving bioRxiv/medRxiv preprint full text
6
+ and turning it into structured, embedding-ready data. One canonical model and one
7
+ shared JATS parser back every path.
8
+
9
+ ## When to use
10
+
11
+ - The user gives a **preprint DOI or bioRxiv/medRxiv URL** and wants the full text or
12
+ a specific section (methods, results, …).
13
+ - The user wants to **search** preprints by keyword, **title**, **abstract**, or
14
+ **author**, or **discover** them by topic/category/date.
15
+ - The user wants a **corpus** of preprint full text chunked for embeddings/RAG.
16
+
17
+ Do NOT use for published-journal full text in general (this is preprint-scoped), and
18
+ do not scrape the bioRxiv website in bulk — use `ingest --source s3` for corpora.
19
+
20
+ ## Why full text (for grounded reasoning)
21
+
22
+ Prefer full text over abstracts when the task needs real evidence: methods, quantities,
23
+ protocols, datasets, and results live in the body. `get_fulltext` returns section-labeled
24
+ text (`kind` = methods / results / discussion / …) with `source`, `version`, and `license`,
25
+ so you can:
26
+ - **cite precisely** — quote/attribute the exact section of a specific paper + version;
27
+ - **verify claims** — check an assertion against the paper's Results, not its abstract;
28
+ - **extract methods/parameters** — read the Methods section directly;
29
+ - **stay within-license** — each Section/Chunk carries its license and a link-back source.
30
+ This grounds reasoning in the primary source rather than a recalled (possibly stale) summary.
31
+
32
+ ## CLI
33
+
34
+ ```bash
35
+ # One preprint -> structured sections (Europe PMC -> S3 router). Accepts DOI, doi.org
36
+ # URL, or a bioRxiv/medRxiv content URL. --markdown for readable output; default JSON.
37
+ preprint-fulltext get <DOI|URL> [--markdown] [--version N] [--source europepmc|s3|html] [--html]
38
+
39
+ # Search -> one SearchHit per line (JSONL). --field scopes the query.
40
+ preprint-fulltext search "<query>" [--field fulltext|title|abstract|author] [--source europepmc|openalex] [-n N]
41
+
42
+ # Discover by topic/category/date -> SearchHit JSONL (OpenAlex by default).
43
+ preprint-fulltext discover [--query Q] [--category C] [--since YYYY-MM] [--to YYYY-MM-DD] [-n N]
44
+
45
+ # Bulk corpus -> Chunk JSONL/Parquet + <out>_manifest.jsonl (resumable, incremental).
46
+ preprint-fulltext ingest <out.jsonl> --source s3|europepmc [--server biorxiv|medrxiv|both]
47
+ [--since YYYY-MM] [--redistribution] [--format jsonl|parquet] [-n N]
48
+ ```
49
+
50
+ Notes for agents:
51
+ - **arXiv:** `get` accepts an arXiv id (`2401.10515`), `arXiv:` prefix, an arxiv.org URL, or
52
+ the `10.48550/arXiv.*` DOI, and returns full text from arXiv's LaTeXML HTML (native →
53
+ ar5iv). `search --source arxiv` / `discover --source arxiv` query the arXiv API. arXiv is
54
+ **not** available in `ingest` (single-document only).
55
+ - **Versions:** a DOI resolves to the **latest version by default** (openRxiv semantics).
56
+ Pass `--version N` for a specific one. Europe PMC only serves the latest indexed
57
+ version, so an explicit older version is fetched via the HTML source (`--source html
58
+ --version 1`) or S3. The retrieved version is reported in `preprint.version`.
59
+ - `get` with no `--source` tries Europe PMC, then S3 (if AWS creds), and only tries the
60
+ public HTML page if you pass `--html`. Europe PMC full text exists for the **CC /
61
+ open-access subset**; all-rights-reserved preprints need S3 or `--html`.
62
+ - `--field title`/`abstract` match **all terms** (not an exact phrase); `--field author`
63
+ matches an author name. For topical multi-word queries prefer `--field fulltext`.
64
+ - `ingest --source s3` is **requester-pays** (needs AWS credentials and costs money;
65
+ ~$0.09/GB). `ingest --source europepmc` is free but only covers the CC subset.
66
+ - `--redistribution` degrades non-redistributable works to metadata-only stubs.
67
+
68
+ ## Python
69
+
70
+ ```python
71
+ from preprint_fulltext.pipeline.router import Router
72
+ from preprint_fulltext.core.chunk import chunk_fulltext
73
+
74
+ result = Router().get_fulltext("10.1101/2024.01.15.575000") # -> GetResult
75
+ ft = result.fulltext # FullText | None (result.reason if None)
76
+ chunks = chunk_fulltext(ft) # list[Chunk], embedding-ready
77
+ ```
78
+
79
+ ## MCP tools
80
+
81
+ Run `preprint-fulltext-mcp` (stdio). Tools:
82
+ - `search_preprints(query, source?, limit?)` → list of hits
83
+ - `get_fulltext(doi, as_markdown?)` → structured sections (or Markdown)
84
+ - `get_metadata(doi)` → bibliographic metadata (abstract only)
85
+ - `resolve(doi_or_openalex_id)` → cross-identifier resolution
86
+
87
+ Bulk `ingest` is intentionally not an MCP tool.
88
+
89
+ ## Data model (what you get back)
90
+
91
+ - `FullText`: `preprint` + ordered `sections` (`kind` ∈ abstract|intro|methods|results|
92
+ discussion|other) + `retrieved_from`.
93
+ - `Chunk` (the corpus record): `doi, version, chunk_id, section_kind, text,
94
+ token_count, char_start, char_end, license, source`. One JSONL line per chunk.
95
+ - `SearchHit`: `title, doi?, openalex_id?, score?, source, snippet?, oa_url?, has_fulltext`.
96
+
97
+ ## Gotchas
98
+
99
+ - openRxiv DOIs use prefix **`10.64898`** since 2025-12 (legacy **`10.1101`** before).
100
+ - OpenAlex requires an API key (`OPENALEX_API_KEY`) since 2026-02-13; missing → HTTP 409.
101
+ - Set `CONTACT_EMAIL` for the Europe PMC / OpenAlex polite pools.
102
+ - Retrieved content keeps its own license; corpora are for your own TDM, not re-hosting.
@@ -0,0 +1,40 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Min Dai and the preprint-fulltext maintainers
4
+ (Gord Fishell Lab, Harvard Medical School / Broad Institute)
5
+ All rights reserved.
6
+
7
+ Redistribution and use in source and binary forms, with or without
8
+ modification, are permitted provided that the following conditions are met:
9
+
10
+ 1. Redistributions of source code must retain the above copyright notice, this
11
+ list of conditions and the following disclaimer.
12
+
13
+ 2. Redistributions in binary form must reproduce the above copyright notice,
14
+ this list of conditions and the following disclaimer in the documentation
15
+ and/or other materials provided with the distribution.
16
+
17
+ 3. Neither the name of the copyright holder nor the names of its
18
+ contributors may be used to endorse or promote products derived from
19
+ this software without specific prior written permission.
20
+
21
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
25
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
27
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
29
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31
+
32
+ ---
33
+
34
+ NOTE ON RETRIEVED CONTENT: This BSD-3-Clause license covers the preprint-fulltext
35
+ software only. It does NOT relicense any preprint full text the software
36
+ retrieves. Preprint content remains under its author-selected license (e.g.
37
+ CC-BY, CC-BY-NC, or all-rights-reserved). Corpora built with this tool are for
38
+ the operator's own text/data mining under the openRxiv TDM terms; do not
39
+ re-host or redistribute openRxiv full text, and link back to the openRxiv-hosted
40
+ text in any tool built on it. See the openRxiv TDM terms for details.