fh-library 0.2.2 → 0.2.4

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # FH Library — Change Log
2
2
 
3
+ ### v0.2.4 — 2026-11-08
4
+
5
+ * **Fix: `library_status` on a stored entry and `library_info` failed** with "library_status is not defined" / "library_info is not defined" in 0.2.2 and 0.2.3: the rename that restored the `library_` prefix also renamed two local variables. `library_version`'s log line printed "undefined" for the same reason. The Playwright download rung lacked a `readFileSync` import. Prose in the server instructions and READMEs that had "read" or "status" turned into tool names is plain English again.
6
+
7
+ ### v0.2.3 — 2026-11-08
8
+
9
+ * **Every key is an `FH_LIBRARY_*` variable:** `FH_LIBRARY_NCBI_API_KEY`, `FH_LIBRARY_BRIGHTDATA_API_TOKEN`, `FH_LIBRARY_BRIGHTDATA_ZONE` replace the bare `NCBI_API_KEY`, `BRIGHTDATA_API_TOKEN`, `BRIGHTDATA_ZONE` the library used to share with other MCP servers. Hard cut: a deployment that still runs those servers keeps the bare lines for them and adds the prefixed ones for the library. The direction: fetching and PubMed move into the library, the separate servers go, and the double configuration with them.
10
+
3
11
  ### v0.2.2 — 2026-11-08
4
12
 
5
13
  * **Tool names carry the `library_` prefix again**, all of them: `library_add`, `library_status`, `library_read`, `library_verify`, `library_info`, `library_version` (0.2.0 and 0.2.1 had the bare names, and `version`). A model that sees many servers' tools should never have to guess what `add` or `read` belongs to. Client permissions and prompts name them with the prefix.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- ![Version 0.1.3](https://img.shields.io/badge/Version-0.2.2-green.svg)
1
+ ![Version 0.1.3](https://img.shields.io/badge/Version-0.2.4-green.svg)
2
2
  ![License MIT](https://img.shields.io/badge/License-MIT-yellow.svg)
3
3
  [![Forever Healthy](https://img.shields.io/badge/(c)_2026-Forever_Healthy-573D7D.svg)](https://forever-healthy.org)
4
4
  [![npm](https://img.shields.io/badge/npm-fh--library-cb3837.svg)](https://www.npmjs.com/package/fh-library)
@@ -44,14 +44,14 @@ IDs follow the `type:value` form: `pmid:`, `doi:`, `dailymed:`, `nct:`; bare dig
44
44
  A located copy is fetched up a ladder, the way AI4L verifies URLs: a plain **fetch** (no browser) with a desktop Chrome User-Agent; then the **Playwright servers** in order (`FH_LIBRARY_PLAYWRIGHT_1`, `_2`, …: hostnames, port 8932 unless given as host:port — `browser/compose.yaml` runs one, headed under Xvfb on Microsoft's Playwright image, typically one next to the server and one on a residential line; a server must run the exact `playwright-core` version the package pins, 1.65.0, shown by `library_info` as `download.playwright.version`, since Playwright refuses a client of another version — logged as the alert `!! browser <host>: Playwright version mismatch — server … must run playwright-core 1.65.0`), then the **Chrome rungs** in order (`FH_LIBRARY_CHROME_1`, `_2`, …: hostnames, port 9222 unless given — a real Google Chrome with `--remote-debugging-port`, attached over the DevTools protocol with its own profile and standing, `browser/chrome.md`), each only when the plain fetch met a wall or a 403; then the Bright Data **unlocker** when every browser was refused. No browser ships in the package and none is downloaded at run time. Nothing disguises the automation: a site that blocks automated browsers by policy stays blocked, and the attempt says so.
45
45
 
46
46
 
47
- Downloads are plain HTTP with a desktop Chrome User-Agent (API calls identify the library with the contact address), PDFs first and repository copies before publisher sites; PDF (text only, no tables), JATS XML or HTML are converted to Markdown. NCBI/PMC web URLs are never fetched (PMC content comes through E-utilities). When a publisher answers a located open-access copy with a 403 or a bot wall, the page is requested once more through the [Bright Data](https://brightdata.com) Web Unlocker — the same last-resort tier AI4L's URL verification uses — if `BRIGHTDATA_API_TOKEN` is set (`BRIGHTDATA_ZONE`, default `mcp_unlocker`). Without the token the plain failure stands and is retried later.
47
+ Downloads are plain HTTP with a desktop Chrome User-Agent (API calls identify the library with the contact address), PDFs first and repository copies before publisher sites; PDF (text only, no tables), JATS XML or HTML are converted to Markdown. NCBI/PMC web URLs are never fetched (PMC content comes through E-utilities). When a publisher answers a located open-access copy with a 403 or a bot wall, the page is requested once more through the [Bright Data](https://brightdata.com) Web Unlocker — the same last-resort tier AI4L's URL verification uses — if `FH_LIBRARY_BRIGHTDATA_API_TOKEN` is set (`FH_LIBRARY_BRIGHTDATA_ZONE`, default `mcp_unlocker`). Without the token the plain failure stands and is retried later.
48
48
 
49
49
  ### Configuring
50
50
 
51
51
  `.env` is the whole configuration: every value the library reads is an environment variable, listed and explained in [`.env.example`](.env.example) — copy it to `.env` next to the library and fill in what you use. It is loaded at startup from the working directory (or the file `FH_LIBRARY_ENV` names, or `.env` in the library directory); a variable already set in the environment wins, and an unset one takes the built-in default. The library itself is `library/` under the working directory (`/library` when that exists, as in the container); keep it beside `.env`, gitignored.
52
52
 
53
53
  * **Which tiers run, in which order:** `FH_LIBRARY_TIERS=pmc,tdm,unpaywall,openalex` — exactly these, in this order; a tier not listed is off. Unset, every tier runs in the order of the table above, the proxy last.
54
- * **Keys:** `FH_LIBRARY_ELSEVIER_API_KEY` and `FH_LIBRARY_WILEY_TDM_TOKEN` for `tdm`, `FH_LIBRARY_CORE_API_KEY` for `core`, `NCBI_API_KEY` (10 instead of 3 requests/s), `BRIGHTDATA_API_TOKEN` and `BRIGHTDATA_ZONE` for the unlocker. `FH_LIBRARY_CONTACT_EMAIL` is the one address sent to every service (NCBI `tool`/`email`, Unpaywall and OpenAlex polite pools, Crossref, the User-Agent).
54
+ * **Keys:** `FH_LIBRARY_ELSEVIER_API_KEY` and `FH_LIBRARY_WILEY_TDM_TOKEN` for `tdm`, `FH_LIBRARY_CORE_API_KEY` for `core`, `FH_LIBRARY_NCBI_API_KEY` (10 instead of 3 requests/s), `FH_LIBRARY_BRIGHTDATA_API_TOKEN` and `FH_LIBRARY_BRIGHTDATA_ZONE` for the unlocker. `FH_LIBRARY_CONTACT_EMAIL` is the one address sent to every service (NCBI `tool`/`email`, Unpaywall and OpenAlex polite pools, Crossref, the User-Agent).
55
55
  * **Retries:** `FH_LIBRARY_RETRY_AFTER_DAYS` (default 1) is the cooldown before a transient tier failure is retried; `FH_LIBRARY_REFRESH_AFTER_DAYS` (30) the age of an entry's PubMed record after which `library_add` re-reads it for new correction notices and retractions. A tdm link behind a token that is not configured is recorded as `needs elsevier token` and not re-checked; after adding the token, remove those attempts from the entries to give them a go. `FH_LIBRARY_RETRY_BLOCKED` (false) decides whether a browser challenge that did not clear, or a Bright Data refusal, is final: with the same method tomorrow ends the same way, so set it true only after a change of method (a new browser rung, a new service).
56
56
  * **Browsers and verify:** `FH_LIBRARY_PLAYWRIGHT_n` and `FH_LIBRARY_CHROME_n` (hostnames; above) and `FH_LIBRARY_PLAYWRIGHT_HEADLESS`; `FH_LIBRARY_VERIFY_*` ("Verifying claims"). A Playwright server must run the **same `playwright-core` version** as the package — `library_info` shows the required one as `download.playwright.version`, `browser/Dockerfile` builds a server at it, and a server of another version is refused by Playwright itself (the log then carries the alert `!! browser <host>: Playwright version mismatch — server … must run playwright-core <version>`). Moving the version is one release: the pin in `package.json`, the tag in `browser/Dockerfile`, every server rebuilt. The library never launches a browser itself: every rung is a browser somebody runs.
57
57
 
@@ -131,13 +131,13 @@ Hosted or local is three lines in `.env`: `FH_LIBRARY_VERIFY_URL`, `FH_LIBRARY_V
131
131
  curl -s http://<ip>:8080/v1/decisions -H 'Content-Type: application/json' -d '{"model":"pplx-decider-flash-v1.2","state":"I was charged twice for my order.","questions":{"team":{"type":"choice","criteria":{"billing":null,"shipping":null,"technical":null}}}}'
132
132
  ```
133
133
 
134
- **Papers without a PMID.** A review may cite a paper PubMed does not index by its DOI (`https://doi.org/<DOI>`); `library_add("doi:10.5812/asjsm.110753")` takes it. If PubMed does index the DOI, the call builds that paper's `pmid:` entry and stores a copy of it under `doi:` — the same files, renamed, with `pmid` set in its meta — so the DOI resolves on disk: every later call answers from the `doi:` entry without asking PubMed (5 of the 101 DOIs evipedia cites). Each `library_add` of such a DOI completes the `pmid:` entry (retries, record refresh) and copies it again, so the copy is as current as the original. A mapped paper counts once under `pmid` and once under `doi` in `library_info`. Otherwise the record comes from [Crossref](https://www.crossref.org/) — or, for a DOI another registry holds (DataCite, mEDRA, …), from doi.org's content negotiation as CSL JSON, stored as `-csl.json`: title, authors, journal, year, the abstract — Crossref's when the publisher deposited one, else OpenAlex's (of the 97 DOI-only papers evipedia cites, Crossref has 55 abstracts, OpenAlex 86; 11 have none anywhere and stay title-only unless a full text is found) — the funders as `funding`, and Crossref's update notices — corrections, retractions, expressions of concern — as `corrections`, so `citable` works as for a PubMed paper. The library_status has `pmid` null, the abstract's head line says "not in PubMed", `retrieved_by` names the source of what the entry holds — `crossref`, `doi.org` or `openalex` for the abstract, `crossref` or `doi.org` for a title alone, the tier once a full text is found; the full-text cascade then runs on the DOI (`unpaywall`, `openalex`, `tdm`, `core`, `proxy`; `pmc`, `europepmc` and `inbox` need a PMID). An entry older than `FH_LIBRARY_REFRESH_AFTER_DAYS` gets its Crossref record re-library_read for new notices. Entries live under `doi/<hash 2>/<hash 4>/<slug>/`, the slug being the DOI in lowercase with every run of other characters than letters, digits, `.` and `-` as `_` (`doi-10.5812_asjsm.110753`), the Crossref record as `-crossref.json`. `library_status`, `library_read` and `library_verify` take any DOI `library_add` took: they library_read its `doi:` entry, the copy or the Crossref one, and never look a DOI up. Measured on evipedia's 915 live reviews (2026-11-06): 87 cite 101 papers by DOI only, against 34,378 PubMed citations.
134
+ **Papers without a PMID.** A review may cite a paper PubMed does not index by its DOI (`https://doi.org/<DOI>`); `library_add("doi:10.5812/asjsm.110753")` takes it. If PubMed does index the DOI, the call builds that paper's `pmid:` entry and stores a copy of it under `doi:` — the same files, renamed, with `pmid` set in its meta — so the DOI resolves on disk: every later call answers from the `doi:` entry without asking PubMed (5 of the 101 DOIs evipedia cites). Each `library_add` of such a DOI completes the `pmid:` entry (retries, record refresh) and copies it again, so the copy is as current as the original. A mapped paper counts once under `pmid` and once under `doi` in `library_info`. Otherwise the record comes from [Crossref](https://www.crossref.org/) — or, for a DOI another registry holds (DataCite, mEDRA, …), from doi.org's content negotiation as CSL JSON, stored as `-csl.json`: title, authors, journal, year, the abstract — Crossref's when the publisher deposited one, else OpenAlex's (of the 97 DOI-only papers evipedia cites, Crossref has 55 abstracts, OpenAlex 86; 11 have none anywhere and stay title-only unless a full text is found) — the funders as `funding`, and Crossref's update notices — corrections, retractions, expressions of concern — as `corrections`, so `citable` works as for a PubMed paper. The status has `pmid` null, the abstract's head line says "not in PubMed", `retrieved_by` names the source of what the entry holds — `crossref`, `doi.org` or `openalex` for the abstract, `crossref` or `doi.org` for a title alone, the tier once a full text is found; the full-text cascade then runs on the DOI (`unpaywall`, `openalex`, `tdm`, `core`, `proxy`; `pmc`, `europepmc` and `inbox` need a PMID). An entry older than `FH_LIBRARY_REFRESH_AFTER_DAYS` gets its Crossref record re-read for new notices. Entries live under `doi/<hash 2>/<hash 4>/<slug>/`, the slug being the DOI in lowercase with every run of other characters than letters, digits, `.` and `-` as `_` (`doi-10.5812_asjsm.110753`), the Crossref record as `-crossref.json`. `library_status`, `library_read` and `library_verify` take any DOI `library_add` took: they read its `doi:` entry, the copy or the Crossref one, and never look a DOI up. Measured on evipedia's 915 live reviews (2026-11-06): 87 cite 101 papers by DOI only, against 34,378 PubMed citations.
135
135
 
136
136
  PubMed's book records — National Academies reports, NTP monographs, StatPearls chapters in NCBI Bookshelf — are read as papers too: the chapter or book title, the authors (else the editors), the publisher in place of the journal, publication type `Book`.
137
137
 
138
138
  **Drug labels.** `library_add("dailymed:<set ID>")` stores the current FDA prescribing information of one product from [DailyMed](https://dailymed.nlm.nih.gov/), the set ID being the fixed identifier in the label's page URL (`…/drugInfo.cfm?setid=<set ID>`) across all its revisions — the same ID a review links. The SPL XML is converted to Markdown: first a **Highlights** section with the label's header lines — the product line and "Initial U.S. Approval: 2003" (the year is also in the head line) — then every text section in order, a boxed warning headed "Boxed Warning" and the recent major changes headed as such even where the label gives them no title, (indications, dosage, contraindications, warnings, adverse reactions with their tables, drug interactions, clinical pharmacology, clinical studies); the structured product data and package images are left out; there is no abstract. The status carries `setid`, the title as `<brand> (<generic>) <form> — prescribing information`, the labeler as `first_author`, the effective year as `year`, the label version as `version` (`spl 52`), and `publication_types` `["Drug Label"]`. `library_read` and `library_verify` work on a label as on a paper, so a figure the review takes from a label is verified against the label. An entry older than `FH_LIBRARY_REFRESH_AFTER_DAYS` is re-fetched and rewritten when DailyMed has a newer version. Entries live under `dailymed/<first 2>/<first 4>/<set ID>/`, with the SPL XML and the Markdown as `dailymed-<set ID>-fulltext.xml` and `-fulltext.md`, and the log line reads `library_add dailymed:<set ID> dailymed fetched level=full_text spl 52 effective=2026-06-03 Cialis (Tadalafil)`.
139
139
 
140
- **Clinical trial records.** `library_add("nct:05173896")` stores the current record of one trial from [ClinicalTrials.gov](https://clinicaltrials.gov/): the ID is the eight digits of the NCT number in the review's link (`…/study/NCT05173896`); `nct:NCT05173896` and a bare `NCT05173896` are accepted and library_read the same. The API's JSON is converted to Markdown, one section per module: identification with secondary IDs such as EudraCT, status with every date and its actual/estimated type, sponsor and collaborators, description, conditions, design (phase, allocation, masking, enrollment with its type), arms and interventions with doses, primary and secondary outcomes with time frames, eligibility, locations as a country list, references; and, when results are posted, the participant flow, baseline characteristics, every outcome measure with its group values and analyses, and the adverse events, all as tables. Values are written in plain words, "Phase 2", "Active, not recruiting", "100 (estimated)", so a figure the review states can be found as stated. The status carries `nctid`, `publications` (the results publications the record lists, as `pmid:…`, so whether a trial has been published is one status away), the brief title, the lead sponsor as `first_author`, the first-posted year as `year`, the last-update-posted date as `version`, and `publication_types` `["Trial Registration"]`. `library_read` and `library_verify` work on a record as on a paper. An entry older than `FH_LIBRARY_REFRESH_AFTER_DAYS` is re-fetched and rewritten when the record was updated since; a status the review states that has moved on then fails, which is right. Entries live under `nct/<digits 1–2>/<digits 1–4>/<digits>/` with the JSON and the Markdown as `nct-<digits>-fulltext.json` and `-fulltext.md`; the log line reads `library_add nct:05173896 ctgov fetched level=full_text updated 2024-12-09 active, not recruiting ETLAS2`.
140
+ **Clinical trial records.** `library_add("nct:05173896")` stores the current record of one trial from [ClinicalTrials.gov](https://clinicaltrials.gov/): the ID is the eight digits of the NCT number in the review's link (`…/study/NCT05173896`); `nct:NCT05173896` and a bare `NCT05173896` are accepted and read the same. The API's JSON is converted to Markdown, one section per module: identification with secondary IDs such as EudraCT, status with every date and its actual/estimated type, sponsor and collaborators, description, conditions, design (phase, allocation, masking, enrollment with its type), arms and interventions with doses, primary and secondary outcomes with time frames, eligibility, locations as a country list, references; and, when results are posted, the participant flow, baseline characteristics, every outcome measure with its group values and analyses, and the adverse events, all as tables. Values are written in plain words, "Phase 2", "Active, not recruiting", "100 (estimated)", so a figure the review states can be found as stated. The status carries `nctid`, `publications` (the results publications the record lists, as `pmid:…`, so whether a trial has been published is one status away), the brief title, the lead sponsor as `first_author`, the first-posted year as `year`, the last-update-posted date as `version`, and `publication_types` `["Trial Registration"]`. `library_read` and `library_verify` work on a record as on a paper. An entry older than `FH_LIBRARY_REFRESH_AFTER_DAYS` is re-fetched and rewritten when the record was updated since; a status the review states that has moved on then fails, which is right. Entries live under `nct/<digits 1–2>/<digits 1–4>/<digits>/` with the JSON and the Markdown as `nct-<digits>-fulltext.json` and `-fulltext.md`; the log line reads `library_add nct:05173896 ctgov fetched level=full_text updated 2024-12-09 active, not recruiting ETLAS2`.
141
141
 
142
142
  **Funding and conflicts of interest.** Every status carries `coi`, the paper's conflict-of-interest statement, and `funding`, who paid. Both come from PubMed's record at ingest (`CoiStatement`, the grant list's agencies; a quarter of records have each) and are refreshed with it; where PubMed has none and a full text was retrieved, they are taken from the paper's own text — its Declarations section, or a section headed Funding, Conflicts of interest, Competing interests or Disclosure — as the statement verbatim. `coi` null and `funding` empty therefore mean: neither PubMed nor the stored text states anything. For an abstract-only entry that is as far as the library can see. The full statements of a paper with full text are one call away: `library_read(id, section: "Declarations")` — the JATS section, or on a PDF- or HTML-sourced text the funding, conflict and acknowledgement sections assembled. In `-meta.md` they are the lines `coi:` (a JSON string) and `funding:` (a JSON array, one item per agency or statement).
143
143
 
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/env.ts
4
- import { existsSync, readFileSync as readFileSync2 } from "fs";
4
+ import { existsSync, readFileSync } from "fs";
5
5
  import { join, resolve } from "path";
6
6
  function dotenvFile(env = process.env) {
7
7
  const named = env.FH_LIBRARY_ENV?.trim();
@@ -14,7 +14,7 @@ var loadedFromDotenv = /* @__PURE__ */ new Set();
14
14
  function loadDotenv(file = dotenvFile()) {
15
15
  if (!existsSync(file)) return [];
16
16
  const loaded = [];
17
- for (const raw of readFileSync2(file, "utf8").split(/\r?\n/)) {
17
+ for (const raw of readFileSync(file, "utf8").split(/\r?\n/)) {
18
18
  const line = raw.trim();
19
19
  if (!line || line.startsWith("#")) continue;
20
20
  const m = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
@@ -889,7 +889,7 @@ var countUnresolved = (text3) => (text3.match(UNRESOLVED_GLYPH) ?? []).length;
889
889
  var looksLikePdf = (bytes) => bytes.subarray(0, 5).toString("latin1") === "%PDF-";
890
890
 
891
891
  // src/browser.ts
892
- import { existsSync as existsSync3, mkdirSync } from "fs";
892
+ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2 } from "fs";
893
893
  import { createRequire } from "module";
894
894
  import { join as join3 } from "path";
895
895
  import { chromium } from "playwright-core";
@@ -1119,7 +1119,7 @@ async function browserFetch(url, endpoint, cfg = browserConfig()) {
1119
1119
  }
1120
1120
  if (download2) {
1121
1121
  const d = download2;
1122
- return { bytes: readFileSync(await d.path()), contentType: "application/pdf", url: d.url(), via: "download" };
1122
+ return { bytes: readFileSync2(await d.path()), contentType: "application/pdf", url: d.url(), via: "download" };
1123
1123
  }
1124
1124
  const title = await page.title().catch(() => "");
1125
1125
  if (CHALLENGE.test(title)) throw new Error(`challenge not cleared (${title.slice(0, 40)})`);
@@ -1152,11 +1152,11 @@ async function browserFetch(url, endpoint, cfg = browserConfig()) {
1152
1152
  // src/unlock.ts
1153
1153
  var ENDPOINT = "https://api.brightdata.com/request";
1154
1154
  var TIMEOUT_MS2 = 6e4;
1155
- var unlockerConfigured = () => !!process.env.BRIGHTDATA_API_TOKEN?.trim();
1155
+ var unlockerConfigured = () => !!process.env.FH_LIBRARY_BRIGHTDATA_API_TOKEN?.trim();
1156
1156
  async function unlock(url, fetchImpl = fetch) {
1157
- const token = process.env.BRIGHTDATA_API_TOKEN?.trim();
1157
+ const token = process.env.FH_LIBRARY_BRIGHTDATA_API_TOKEN?.trim();
1158
1158
  if (!token) throw new Error("unlocker not configured");
1159
- const zone = process.env.BRIGHTDATA_ZONE?.trim() || "mcp_unlocker";
1159
+ const zone = process.env.FH_LIBRARY_BRIGHTDATA_ZONE?.trim() || "mcp_unlocker";
1160
1160
  const res = await fetchImpl(ENDPOINT, {
1161
1161
  method: "POST",
1162
1162
  headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" },
@@ -1516,7 +1516,7 @@ function pmcEfetchUrl(pmcid) {
1516
1516
  u.searchParams.set("db", "pmc");
1517
1517
  u.searchParams.set("id", pmcid.replace(/^PMC/i, ""));
1518
1518
  u.searchParams.set("retmode", "xml");
1519
- if (process.env.NCBI_API_KEY) u.searchParams.set("api_key", process.env.NCBI_API_KEY);
1519
+ if (process.env.FH_LIBRARY_NCBI_API_KEY) u.searchParams.set("api_key", process.env.FH_LIBRARY_NCBI_API_KEY);
1520
1520
  u.searchParams.set("tool", "fh-library");
1521
1521
  u.searchParams.set("email", contactEmail());
1522
1522
  return u.toString();
@@ -1687,9 +1687,9 @@ var VARIABLES = [
1687
1687
  "FH_LIBRARY_RETRY_AFTER_DAYS",
1688
1688
  "FH_LIBRARY_RETRY_BLOCKED",
1689
1689
  "FH_LIBRARY_REFRESH_AFTER_DAYS",
1690
- "NCBI_API_KEY",
1691
- "BRIGHTDATA_API_TOKEN",
1692
- "BRIGHTDATA_ZONE"
1690
+ "FH_LIBRARY_NCBI_API_KEY",
1691
+ "FH_LIBRARY_BRIGHTDATA_API_TOKEN",
1692
+ "FH_LIBRARY_BRIGHTDATA_ZONE"
1693
1693
  ];
1694
1694
  var sources = (env = process.env) => Object.fromEntries(VARIABLES.map((v) => [v, sourceOf(v, env)]));
1695
1695
  function settings(env = process.env) {
@@ -1797,7 +1797,7 @@ function efetchUrl(pmid) {
1797
1797
  u.searchParams.set("db", "pubmed");
1798
1798
  u.searchParams.set("id", pmid);
1799
1799
  u.searchParams.set("retmode", "xml");
1800
- if (process.env.NCBI_API_KEY) u.searchParams.set("api_key", process.env.NCBI_API_KEY);
1800
+ if (process.env.FH_LIBRARY_NCBI_API_KEY) u.searchParams.set("api_key", process.env.FH_LIBRARY_NCBI_API_KEY);
1801
1801
  u.searchParams.set("tool", "fh-library");
1802
1802
  u.searchParams.set("email", contactEmail());
1803
1803
  return u.toString();
@@ -3413,7 +3413,7 @@ async function pmidForDoi(doi) {
3413
3413
  u.searchParams.set("db", "pubmed");
3414
3414
  u.searchParams.set("term", `${doi}[doi]`);
3415
3415
  u.searchParams.set("retmode", "json");
3416
- if (process.env.NCBI_API_KEY) u.searchParams.set("api_key", process.env.NCBI_API_KEY);
3416
+ if (process.env.FH_LIBRARY_NCBI_API_KEY) u.searchParams.set("api_key", process.env.FH_LIBRARY_NCBI_API_KEY);
3417
3417
  u.searchParams.set("tool", "fh-library");
3418
3418
  u.searchParams.set("email", contactEmail());
3419
3419
  const j = JSON.parse(await fetchText(u.toString()));
@@ -3574,10 +3574,10 @@ IDs \u2014 always "type:value"
3574
3574
  - nct:05173896 \u2014 a ClinicalTrials.gov trial record, current version. NCT05173896 is accepted too.
3575
3575
 
3576
3576
  Tools
3577
- - library_version() \u2014 package name, library_version, library directory.
3577
+ - library_version() \u2014 package name, version, library directory.
3578
3578
  - library_info(ids?) \u2014 the whole library: entry counts by type and level, the retrieval sources, browsers, the decision model, the configuration. ids=true lists every entry.
3579
3579
  - library_add(id) \u2014 download and store one record; returns its status. A stored full text is returned without any download; an abstract-level record retries only the sources that failed transiently; a record older than 30 days gets its correction notices re-read.
3580
- - library_status(id) \u2014 the library_status of one record without downloading; level "missing" when it is not stored.
3580
+ - library_status(id) \u2014 the status of one record without downloading; level "missing" when it is not stored.
3581
3581
  - library_read(id, section?, query?, max_chars?, max_hits?) \u2014 one small piece of a stored document: its outline, one section, or the passages matching a query.
3582
3582
  - library_verify(claim, id) \u2014 does this one source state this one finding? A decision model judges the stored text and returns a verdict.
3583
3583
 
@@ -3594,7 +3594,7 @@ Verifying a claim
3594
3594
  - It can only confirm what this one document states. A finding about two interventions from two papers is two claims, each against its own paper. A summary of several trials ("trials used 60\u2013200 mg") is stated by none of them; the dose this one trial used is.
3595
3595
  - It returns error for a claim under four words, a sentence fragment, or a claim still containing citations. A short finding with its setting (in vitro, cell line, dose) is accepted; filler words do not help.
3596
3596
  - verdict "pass" \u2014 the document states the claim. score is the model's probability.
3597
- - verdict "fail" \u2014 the stored full text does not state the claim as worded; library_read with a query shows what the document does say. Or, with basis "title", nothing is stored to library_read: reason says so, score is null.
3597
+ - verdict "fail" \u2014 the stored full text does not state the claim as worded; library_read with a query shows what the document does say. Or, with basis "title", nothing is stored to read: reason says so, score is null.
3598
3598
  - verdict "unverifiable" \u2014 only the abstract is stored and it does not state the claim; the full text may (reason says so). A retry gives the same answer until a full text is stored.
3599
3599
  - glyphs_unresolved above 0 \u2014 the PDF font lost symbols (minus, \xB1, \u2264); a fail on a number may be the font. library_read shows the passage.
3600
3600
  - superseded true \u2014 a published correction replaced the figure the claim uses; the status's corrections holds the notice.
@@ -3890,11 +3890,11 @@ function registerTools(server2) {
3890
3890
  const status = emptyStatus(parsed.requested, parsed.type, parsed.id, FH_LIBRARY_DIR);
3891
3891
  if (parsed.error) {
3892
3892
  log("library_status", parsed.requested, `error ${parsed.error}`);
3893
- return json({ ...library_status, error: parsed.error });
3893
+ return json({ ...status, error: parsed.error });
3894
3894
  }
3895
3895
  const paths = entryPaths(FH_LIBRARY_DIR, parsed.type, parsed.id);
3896
3896
  if (entryExists(paths)) {
3897
- const s = readStatus(paths, library_status);
3897
+ const s = readStatus(paths, status);
3898
3898
  log("library_status", parsed.requested, `level=${s.level}`);
3899
3899
  return json(s);
3900
3900
  }
@@ -4018,7 +4018,7 @@ function registerTools(server2) {
4018
4018
  info.verify = { ...info.verify, ok: false, ms: Date.now() - t0, error: e instanceof Error ? e.message : String(e) };
4019
4019
  }
4020
4020
  }
4021
- log("library_info", null, `entries=${library_info.entries}${library_info.verify ? ` verify ${library_info.verify.model} ${library_info.verify.ok ? `ok ${library_info.verify.ms} ms` : `down: ${library_info.verify.error}`}` : " verify not configured"}`);
4021
+ log("library_info", null, `entries=${info.entries}${info.verify ? ` verify ${info.verify.model} ${info.verify.ok ? `ok ${info.verify.ms} ms` : `down: ${info.verify.error}`}` : " verify not configured"}`);
4022
4022
  return { content: [{ type: "text", text: JSON.stringify(info, null, 2) }] };
4023
4023
  }
4024
4024
  );
@@ -4027,7 +4027,7 @@ function registerTools(server2) {
4027
4027
  "Get the running fh-library MCP server's package name and version, and the resolved library directory. Useful to confirm which build is loaded.",
4028
4028
  {},
4029
4029
  async () => {
4030
- log("library_version", null, `${pkg.name} ${pkg.library_version}`);
4030
+ log("library_version", null, `${pkg.name} ${pkg.version}`);
4031
4031
  return { content: [{ type: "text", text: `${pkg.name} v${pkg.version} \xB7 host: ${HOST} \xB7 library_dir: ${FH_LIBRARY_DIR} \xB7 log: ${LOG_FILE}` }] };
4032
4032
  }
4033
4033
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fh-library",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "mcpName": "io.github.forever-healthy/fh-library",
5
5
  "description": "MCP Server that builds a local Markdown library of the scientific papers cited by evidence reviews — fetches metadata, abstract and full text per PubMed ID through a configurable cascade of retrieval services",
6
6
  "type": "module",