Skip to content

Commit 7a9406a

Browse files
committed
Merge Prepare and publish BioMCP v0.8.22 after final readiness tickets
2 parents dbf4c7b + fdd4b9e commit 7a9406a

15 files changed

Lines changed: 202 additions & 38 deletions

CHANGELOG.md

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Changelog
22

3-
## 0.8.22 — 2026-04-23
3+
## 0.8.22 — 2026-04-30
44

55
### New features
66

@@ -34,13 +34,23 @@
3434
- Added opt-in `clinical_features` disease output backed by MedlinePlus
3535
clinical summaries, reviewed HPO phenotype mapping, source-native evidence
3636
URLs, and unsupported-disease empty states via
37-
`get disease <name> clinical_features`. (252, 253)
37+
`get disease <name> clinical_features`. (252, 253, 254)
3838
- Added offline routing via `biomcp suggest <question>` so worked-example
3939
prompts can resolve to structured `_meta.workflow` guidance without requiring
4040
a live tool call. (279)
4141
- Added workflow-ladder sidecars and schema-backed `_meta.ladder[]` payloads
4242
so routed workflows can carry explicit ladder steps across the CLI/MCP
4343
contract. (282)
44+
- Added richer CLI help examples for batch/variant-limit usage and preserved
45+
the parser-level skill uninstall contract through current help and MCP guard
46+
surfaces. (217, 224)
47+
- Added opt-in article fulltext source-boundary coverage and PDF fallback so
48+
article retrieval can truthfully report source limits while recovering PDF
49+
text when structured fulltext is unavailable. (255, 256)
50+
- Added DDInter-backed drug-drug interaction workflows with
51+
`biomcp drug interactions <name>`, `get drug <name> interactions`, local
52+
bundle health, source-scoped empty wording, and `biomcp ddinter sync`.
53+
(303)
4454

4555
### Docs
4656

@@ -57,27 +67,51 @@
5767
- Aligned the public landing-copy contract with the shipped `suggest` and
5868
workflow landing bullets so `make test-contracts` accepts the current README
5969
and docs homepage feature counts. (286)
70+
- Refreshed source, licensing, versioning, CLI decomposition, diagnostic,
71+
clinical-features, API-key, quality-bar, and staging-demo docs to match the
72+
shipped v0.8.22 surface and current local-runtime key expectations. (222,
73+
272, 292, 304, 306, 314, 318, 329, 332, 341, 358, 362)
6074

6175
### Fixes
6276

6377
- Changed custom CLI validation failures (`BioMcpError::InvalidArgument`) to exit
6478
`2`, matching clap parser failures and separating bad usage from runtime
65-
failures.
79+
failures. (353)
6680
- Fixed `suggest` so resistance-to-drug mechanism questions anchor starter
6781
commands on the drug instead of filler text. (291)
6882
- Patched `rustls-webpki` to the safe release line and made `make check` enforce
69-
the cargo-deny advisory gate alongside the existing license gate.
83+
the cargo-deny advisory gate alongside the existing license gate. (290)
84+
- Fixed cargo-install compatibility, deterministic EMA freshness checks, OLS4
85+
smoke routing, WHO drug JSON envelopes, and WHO API cache-limit wording so
86+
install, health, and explicit WHO/API drug searches stay stable. (218, 220,
87+
226, 232)
7088
- Added compact diagnostic rows and capped disease diagnostic pivots so
7189
gene/disease diagnostic follow-ups stay scannable and bounded. (266, 267)
7290
- Replaced the stale GTR sample with a live-valid GTR example and added
7391
zero-result recovery for the local diagnostic surface. (268, 269)
7492
- Tightened entity-aware article follow-ups with a same-session loop-breaker
7593
for overlapping suggestions and PubMed ESearch cleanup for bounded
7694
question-format filler words. (277, 278, 283)
95+
- Improved sparse drug research-code recovery, relational `discover` filtering,
96+
trial help example/flag consistency, and CTGov intervention alias preservation
97+
so generated follow-up commands stay useful and shell-safe for degraded or
98+
ambiguous inputs. (302, 310, 313, 338, 339, 340, 342, 351, 357)
7799
- Repaired the targeted `SPEC_SMOKE_ARGS` lane so it stores stable smoke
78100
section IDs and resolves them to current mustmatch pytest item IDs at runtime;
79101
the quality ratchet now checks collectability before stale line-qualified
80102
selectors can reach `make spec-smoke`. (288)
103+
- Hardened update and machine-readable CLI contracts: `biomcp update` fails
104+
closed when checksum sidecars are missing, `biomcp --json version` documents
105+
its plain-text exception, `biomcp --json list` remains parseable, and
106+
short-literal update ratchets were replaced with structural behavior checks.
107+
(331, 333, 352, 355)
108+
- Stabilized MCP stdio no-input guidance, cBioPortal study download idle/stall
109+
handling, disease clinical-feature specs, OLS4 disease/discover fallback IDs,
110+
WikiPathways parallel tests, and protein ComplexPortal specs with regression
111+
coverage. (326, 336, 345, 346, 350, 354, 358)
112+
- Surfaced Semantic Scholar authentication/degradation status in article search
113+
and honored `Retry-After` during authenticated retries so throttled searches
114+
disclose source health and back off correctly. (364, 365, 366)
81115

82116
### Internal
83117

@@ -89,8 +123,23 @@
89123
- Reworked `SPEC_SMOKE_ARGS` handling around stable smoke inventory and current
90124
mustmatch pytest item IDs, and cleaned up `.march` artifact handling for
91125
repo submission paths. (270, 271)
92-
- Recorded the neural-reranking spike as a no-go for `0.8.22`: no runtime wiring
93-
shipped, and the spike remains deferred from the release surface. (284)
126+
- Migrated the release gate to the spec-v2 corpus, made `spec-pr` self-contained
127+
for March kickoff, restored release-gate reliability, and kept stable smoke,
128+
canary, cache-warm, and test-contract lanes aligned with the current shipped
129+
docs/spec surface. (294, 297, 298, 299, 300, 301, 307, 308, 344)
130+
- Decomposed oversized CLI modules, absorbed residual line-cap allowlists,
131+
clarified the benchmark harness as internal, and added runtime-wiring/line-cap
132+
ratchets so structural cleanup cannot reintroduce public-surface drift. (309,
133+
319, 320, 321, 322, 323, 324, 325, 334, 335, 343, 347)
134+
- Strengthened release readiness with gene-all warm-budget coverage, local gate
135+
uv-build fixes, leaked-artifact cleanup, binary asset attributes, CI wrapper
136+
stale-binary warnings, Python 3.12/Rust 1.95 CI compatibility, self-contained
137+
local-data spec fixtures, and final v0.8.22 readiness audits before publish.
138+
(247, 258, 262, 263, 285, 289, 315, 316, 317, 327, 328, 330, 348, 363, 367)
139+
- Recorded the diagnostic-entity, HPO clinical-feature, Obsidian vault,
140+
biomedical news, post-v0.8.21 shipped-surface, and neural-reranking reviews
141+
as architecture or deferred work unless runtime wiring shipped in the bullets
142+
above. (230, 243, 244, 245, 246, 284)
94143

95144
## 0.8.21 — 2026-04-16
96145

CITATION.cff

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ abstract: >-
77
databases with one command grammar, compact markdown output, and local study
88
analytics.
99
version: 0.8.22
10-
date-released: 2026-04-23
10+
date-released: 2026-04-30
1111
license: MIT
1212
repository-code: https://github.com/genomoncology/biomcp
1313
url: https://biomcp.org

Makefile

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,22 @@ spec:
4242
# Keep upstream-heavy canaries in their serialized spec partition.
4343
cargo build --release --locked
4444
$(MAKE) sync-python-dev
45-
PATH="$(CURDIR)/target/release:$(PATH)" BIOMCP_BIN="$(CURDIR)/target/release/biomcp" \
45+
bash spec/fixtures/setup-study-spec-fixture.sh "$(CURDIR)"
46+
bash spec/fixtures/setup-ddinter-spec-fixture.sh "$(CURDIR)"
47+
. "$(CURDIR)/.cache/spec-study-env"; . "$(CURDIR)/.cache/spec-ddinter-env"; PATH="$(CURDIR)/target/release:$(PATH)" BIOMCP_BIN="$(CURDIR)/target/release/biomcp" \
4648
uv run --no-sync sh -c 'PATH="$(CURDIR)/target/release:$$PATH" BIOMCP_BIN="$(CURDIR)/target/release/biomcp" pytest spec/entity/ spec/surface/ --mustmatch-lang bash --mustmatch-timeout 120 -v $(SPEC_XDIST_ARGS) --deselect spec/entity/protein.md --deselect spec/entity/disease.md --deselect spec/surface/discover.md'
47-
PATH="$(CURDIR)/target/release:$(PATH)" BIOMCP_BIN="$(CURDIR)/target/release/biomcp" \
49+
. "$(CURDIR)/.cache/spec-study-env"; . "$(CURDIR)/.cache/spec-ddinter-env"; PATH="$(CURDIR)/target/release:$(PATH)" BIOMCP_BIN="$(CURDIR)/target/release/biomcp" \
4850
uv run --no-sync sh -c 'PATH="$(CURDIR)/target/release:$$PATH" BIOMCP_BIN="$(CURDIR)/target/release/biomcp" pytest spec/entity/protein.md spec/entity/disease.md spec/surface/discover.md --mustmatch-lang bash --mustmatch-timeout 120 -v'
4951

5052
spec-pr:
5153
# Keep upstream-heavy canaries in their serialized spec partition.
5254
cargo build --release --locked
5355
$(MAKE) sync-python-dev
54-
PATH="$(CURDIR)/target/release:$(PATH)" BIOMCP_BIN="$(CURDIR)/target/release/biomcp" \
56+
bash spec/fixtures/setup-study-spec-fixture.sh "$(CURDIR)"
57+
bash spec/fixtures/setup-ddinter-spec-fixture.sh "$(CURDIR)"
58+
. "$(CURDIR)/.cache/spec-study-env"; . "$(CURDIR)/.cache/spec-ddinter-env"; PATH="$(CURDIR)/target/release:$(PATH)" BIOMCP_BIN="$(CURDIR)/target/release/biomcp" \
5559
uv run --no-sync sh -c 'PATH="$(CURDIR)/target/release:$$PATH" BIOMCP_BIN="$(CURDIR)/target/release/biomcp" pytest spec/entity/ spec/surface/ --mustmatch-lang bash --mustmatch-timeout 180 -v $(SPEC_XDIST_ARGS) --deselect spec/entity/protein.md --deselect spec/entity/disease.md --deselect spec/surface/discover.md'
56-
PATH="$(CURDIR)/target/release:$(PATH)" BIOMCP_BIN="$(CURDIR)/target/release/biomcp" \
60+
. "$(CURDIR)/.cache/spec-study-env"; . "$(CURDIR)/.cache/spec-ddinter-env"; PATH="$(CURDIR)/target/release:$(PATH)" BIOMCP_BIN="$(CURDIR)/target/release/biomcp" \
5761
uv run --no-sync sh -c 'PATH="$(CURDIR)/target/release:$$PATH" BIOMCP_BIN="$(CURDIR)/target/release/biomcp" pytest spec/entity/protein.md spec/entity/disease.md spec/surface/discover.md --mustmatch-lang bash --mustmatch-timeout 180 -v'
5862

5963
validate-skills:
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
repo_root="${1:-$PWD}"
5+
cache_dir="$repo_root/.cache"
6+
ddinter_root="$cache_dir/spec-ddinter"
7+
8+
rm -rf "$ddinter_root"
9+
mkdir -p "$ddinter_root" "$cache_dir"
10+
11+
header='DDInterID_A,Drug_A,DDInterID_B,Drug_B,Level'
12+
cat >"$ddinter_root/ddinter_downloads_code_A.csv" <<'EOF'
13+
DDInterID_A,Drug_A,DDInterID_B,Drug_B,Level
14+
DDInterW,Warfarin,DDInterAMX,Amoxicillin,Major
15+
DDInterW,Warfarin,DDInterATO,Atorvastatin,Moderate
16+
DDInterW,Warfarin,DDInterCLO,Clopidogrel,Major
17+
DDInterI,Imatinib,DDInterCYP,CYP3A4 inhibitor,Major
18+
EOF
19+
20+
for code in B D H L P R V; do
21+
printf '%s\n' "$header" >"$ddinter_root/ddinter_downloads_code_${code}.csv"
22+
done
23+
24+
printf 'export BIOMCP_DDINTER_DIR=%q\n' "$ddinter_root" >"$cache_dir/spec-ddinter-env"

src/entities/article/batch.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ pub(super) fn merge_semantic_scholar_compact_rows(
103103
item_positions: &[usize],
104104
rows: Vec<Option<SemanticScholarPaper>>,
105105
) {
106-
for (idx, paper) in item_positions.iter().zip(rows.into_iter()) {
106+
for (idx, paper) in item_positions.iter().zip(rows) {
107107
let Some(paper) = paper else {
108108
continue;
109109
};

src/entities/article/candidates.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ fn cap_article_candidates_by_source(
256256
let effective_cap = match cap_mode {
257257
ArticleSourceCapMode::Disabled => return candidates,
258258
ArticleSourceCapMode::Default(_) if source_count < 3 => return candidates,
259-
ArticleSourceCapMode::Default(cap) | ArticleSourceCapMode::Explicit(cap)
259+
ArticleSourceCapMode::Default(_cap) | ArticleSourceCapMode::Explicit(_cap)
260260
if source_count < 2 =>
261261
{
262262
return candidates;

src/entities/article/enrichment.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ pub(super) async fn enrich_article_search_rows_with_semantic_scholar(
138138
let chunk_end = chunk_start + chunk.len();
139139
match client.paper_batch_search_enrichment(chunk).await {
140140
Ok(papers) => {
141-
for (lookup_id, paper) in chunk.iter().zip(papers.into_iter()) {
141+
for (lookup_id, paper) in chunk.iter().zip(papers) {
142142
let Some(paper) = paper else {
143143
continue;
144144
};

src/entities/drug/interactions.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,8 +335,17 @@ fn normalized_bucket_from_drug_name(drug_name: &str) -> Option<&'static str> {
335335
|| lowered == "voriconazole"
336336
{
337337
Some("anti-infectives")
338+
} else if lowered.contains("platelet")
339+
|| matches!(
340+
lowered.as_str(),
341+
"aspirin" | "clopidogrel" | "prasugrel" | "ticagrelor"
342+
)
343+
{
344+
Some("antiplatelets")
338345
} else if lowered.contains("statin") {
339346
Some("statins")
347+
} else if lowered.contains("cyp3a4") {
348+
Some("CYP3A4")
340349
} else {
341350
None
342351
}

src/sources/cbioportal.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ impl CBioPortalClient {
267267
sample_count: sample_count as i32,
268268
})
269269
.collect::<Vec<_>>();
270-
dist.sort_by(|a, b| b.sample_count.cmp(&a.sample_count));
270+
dist.sort_by_key(|row| std::cmp::Reverse(row.sample_count));
271271
dist.truncate(5);
272272

273273
Ok(CBioMutationSummary {

src/sources/cbioportal_study.rs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1424,15 +1424,11 @@ fn parse_meta_study(path: &Path, fallback_study_id: &str) -> Result<StudyMeta, B
14241424
let value = raw_value.trim();
14251425

14261426
match key.as_str() {
1427-
"cancer_study_identifier" => {
1428-
if !value.is_empty() {
1429-
study_id = value.to_string();
1430-
}
1427+
"cancer_study_identifier" if !value.is_empty() => {
1428+
study_id = value.to_string();
14311429
}
1432-
"name" => {
1433-
if !value.is_empty() {
1434-
name = value.to_string();
1435-
}
1430+
"name" if !value.is_empty() => {
1431+
name = value.to_string();
14361432
}
14371433
"short_name" => {
14381434
short_name = non_empty(value);

0 commit comments

Comments
 (0)