Releases: musharna/breedsim-mcp
Release list
v0.4.1
Patch — docs and repo hygiene only. No src/ change, so the installed code is byte-identical to 0.4.0.
CONTRIBUTING.mdandSECURITY.mdnow ship in the sdist, alongside issue forms, a PR template,.editorconfig,.mcp.json,glama.json, CodeQL and Dependabot.server.jsonis validated against the MCP registry's own published schema on every test run, so a registry constraint fails locally instead of after a tag is cut — which is exactly how 0.4.0 shipped before the registry refused it with a 422.- Dependabot's
uvecosystem now ignoresrpy2: its<3.6bound is runtime compatibility, and a proposal to widen it failed all three test jobs insideopenrlib.rlib.R_getVar(...)with "The embedded R is not initialized." against R 4.3.
Correction (added after release). This note originally said rpy2 ≥3.6 "needs R ≥4.4, and Debian/Ubuntu ship 4.3". Both were wrong to assert: upstream's own 3.6.x docs state R >= 4.5, and the distro claim is unverified since CRAN publishes R 4.5 packages for current Ubuntu. The pin itself is unchanged and correct — the failing
R_getVarlog is the evidence. Fixed in #30.
v0.4.0 — multi-trait index selection
Multi-trait architecture and index selection. found_population takes h2 as a list plus trait_correlation; run_program takes index_weights and selects on the weighted index. A multi-trait session without index_weights is refused — AlphaSimR's selectInd would silently select on trait 1 alone while the response still looked multi-trait.
An external oracle for the simulation. The realised response is now asserted against R = h²S. Every prior test compared this server against itself.
Audit fixes. Selecting everyone (n_select == n_ind) is named as such and no longer draws a replicates_too_few advisory — that guard could not tell "underpowered" from "there is no effect to power against". Results carry recipe.engine and recipe.gain_scale. server.json now leads with the R + libtirpc-dev requirement, because rpy2 ships no Linux wheels and clients saw only a raw linker error.
Full detail in CHANGELOG.md.
v0.3.2 — Zenodo archival, licence ID corrected
Archived to Zenodo. This release exists so the tag carries the citation metadata a DOI is minted from.
Why a release was required
The Zenodo↔GitHub integration mints its DOI from the tag's tarball. The previous tag predated .zenodo.json and CITATION.cff entirely — both were added after it was cut — so re-publishing the existing release could never have archived correct metadata, and Zenodo returns 409 on it regardless, having created a deposit on the first attempt that never became public.
Correction — these notes originally claimed a licence bug that does not exist
This release was first described as fixing a defect in which SPDX casing in .zenodo.json "silently dropped the licence from the published record". That was wrong, and the claim is retracted here rather than quietly removed.
Zenodo normalises the licence identifier on ingest. The sibling ldraw-mcp archived with "MIT" still in place and its record reads license: mit-license. Every record carries its licence correctly; the licence was never dropped.
The apparent evidence was two measurement errors, both the same mistake — probing a proxy instead of the artifact:
- Querying the licence vocabulary endpoint (
MIT→ 404,mit→ 200) and treating that as what the ingest accepts. It is not. - Reading the RDM-era field names (
rights,subjects,creators[].person_or_org) against an API endpoint that returns the legacy shape (metadata.license,metadata.keywords,creators[].orcid). Every field reported as absent was present throughout.
The lowercase identifier is kept because it is the canonical spelling — it simply fixed nothing.
Verified before publishing
Version agreement across pyproject.toml, server.json (three fields) and CITATION.cff; cffconvert --validate against the CFF 1.2.0 schema; mcp-publisher validate; ruff check and format; registry metadata tests; twine check; the mcp-name marker confirmed inside the built wheel's METADATA; and both wheel and sdist confirmed present on the /simple/ index after upload.
No functional change — tools, guards and dependency pins are untouched.
v0.3.1 — published to the official MCP registry
Published to the official MCP registry via server.json and a GitHub Actions OIDC workflow, so this server is discoverable from MCP clients and directories rather than only from PyPI.
Why this needed a release
The registry proves PyPI ownership by finding an mcp-name: <server-name> marker in the package README as published to PyPI — and PyPI captures long_description at release time. A marker sitting on the default branch verifies nothing, so registering required a version that carries it.
Guarding it
tests/test_registry_metadata.py asserts what would otherwise only fail during a release, after the version is spent:
server.jsonstates the version in three places, with nothing else making them agree withpyproject.toml- the README marker must match the name
server.jsondeclares — that exact string is what the registry greps for - the declared transport is
stdio, which is what this server implements
All three were confirmed red against mutants before being kept.
Running the real mcp-publisher validate also caught a constraint no schema read surfaced: description is capped at 100 characters. That would otherwise have failed the publish after the version was already on PyPI and unreclaimable, so the workflow now validates before authenticating.
No functional change.
v0.3.0 — mcp 2.x
Migration to the mcp 2.x SDK. No change to the simulation, the tools, or the replicate floor.
FastMCP was renamed, not removed — it is now mcp.server.mcpserver.MCPServer, the same class with the same kwargs. The dependency moves to mcp>=2,<3 rather than widening to <3: this package imports mcp.server.mcpserver, absent in 1.x, so a range spanning both majors could resolve to a version that cannot import the server. mcp 2.x requires Python >=3.10, below this package's >=3.11 floor, so the support matrix is unchanged.
The one real API change behind the renames
call_tool now returns a CallToolResult instead of a (content, structured) tuple. Tests read .structured_content directly; the old out[1] if isinstance(out, tuple) else out shim is gone rather than extended, since a shape sniff would silently hand back the result object if the field were renamed again, surfacing the failure far from its cause.
mcp.types fields are snake_case (outputSchema → output_schema, readOnlyHint → read_only_hint). The camelCase spellings survive as pydantic aliases, so constructing ToolAnnotations still works either way — but that rescue does not extend to attribute access, which is what the tests do.
A test that could pass for the wrong reason
test_single_replicate_is_refused_through_the_tool_layer used pytest.raises(Exception). During this migration it caught an unrelated TypeError from a break three lines above it, and only the match="replicates" kept it from passing for the wrong reason. It now expects ToolError, so it can distinguish the refusal under test from any other failure on the way to it.
Full changelog: https://github.com/musharna/breedsim-mcp/blob/master/CHANGELOG.md
v0.2.0 — genomic selection
Genomic selection, gated on measured founder linkage disequilibrium.
Added
selection_method="genomic" on run_program and compare_programs — RRBLUP fitted to marker genotypes each cycle, selection on the estimated breeding value. found_population gains n_snp_per_chr for the SNP chip, which is a founding decision because the markers must exist before anything can be predicted from them.
Setting the two arms of compare_programs to different methods makes "is genotyping worth it on these founders?" a single paired call — a contrast that needs pairing more than most, since the gap between methods is routinely smaller than the sd 0.247 of seed-to-seed noise.
The trap this release exists to surface
Markers predict a trait only through linkage disequilibrium with the causal loci. Measured on AlphaSimR 2.1.0:
| generator | adjacent |r| | distant pairs | ratio | out-of-sample accuracy |
|---|---|---|---|---|
quickHaplo |
0.0444 | 0.0462 | 0.96 | 0.097 |
runMacs |
0.1979 | 0.0495 | 4.00 | 0.351 |
quickHaplo samples haplotypes with no coalescent history, so adjacent markers are indistinguishable from unlinked ones and there is nothing to learn from. It is also the default generator and the only reproducible one — so on this engine, reproducibility and genomic realism cannot currently be had at the same time. Every genotyped founding now returns a measured linkage_disequilibrium block, with a no_linkage_disequilibrium warning when the markers are uninformative.
Why the guard measures LD rather than accuracy
At 20 replicates, a population with an LD ratio of 1.00 reached prediction accuracy 0.208 by cycle three, beating a genuinely linked population at the same selection intensity. Out-of-sample accuracy in a closed population conflates linkage with plain relatedness — as descendants of a few parents fill it, markers predict by tracking pedigree. An accuracy threshold would pass exactly the case it claimed to catch.
Accuracy is reported per cycle, measured out-of-sample on progeny the model never saw. Scoring it in-sample read 0.448 where the truth was 0.097.
Verification
57 tests against real AlphaSimR on Python 3.11/3.12/3.13, and 20 mutation checks all confirmed red (docs/MUTATION-CHECKS.md).
Full changelog: https://github.com/musharna/breedsim-mcp/blob/master/CHANGELOG.md
v0.1.1 — eviction actually frees now
uv add breedsim-mcp → 0.1.1
A patch release for two defects found auditing 0.1.0 shortly after publishing it.
Session eviction was freeing nothing
SessionStore._free_r_state intersected its target names against a bare
ls(envir=.GlobalEnv). R's ls() omits dot-prefixed names unless all.names=TRUE,
and the session prefix starts with a dot by design — so the intersection was always empty
and the rm() never removed anything.
Measured: after evicting a session, all five of its R objects survived. Because the server
is deliberately long-lived, every founded-then-evicted session leaked a whole population
for the life of the process.
Cleanup now matches on the prefix rather than an enumerated list of names, which also
fixes a second defect underneath: the old two-name list would still have missed _p0,
_pop and _pop_sel — the populations, i.e. the large objects — even with
all.names=TRUE.
Eviction had no test. It has one now, and it asserts against ls(all.names=TRUE)
deliberately: a test written with the same bare ls() as the code would have seen zero
survivors and passed against the broken version. A test that reuses the failing
component's own assumption cannot detect that assumption being wrong.
Every size parameter now has a ceiling
0.1.0 validated floors only. R is a single interpreter and the tools are synchronous, so
one oversized call blocks every other call until it finishes — and the caller is a language
model, which will ask for replicates=10000 because the number sounds thorough. The caps
are published through list_methods() under limits, so a caller can size a request
rather than discover the bound by being refused.
Also
session.py claimed the genotype matrix never crosses the boundary. Nothing is returned to
the caller, but _founder_hash pulls the whole haplotype matrix into Python once per
found_population — 200,000 values at default sizes. Reworded to what is actually true.
42 tests against real AlphaSimR on Python 3.11/3.12/3.13, and 17 mutation checks. Both
fixes were mutation-checked against the code that actually shipped in 0.1.0, and the built
wheel was installed into a clean environment and confirmed to contain them before release.
Full changelog: v0.1.0...v0.1.1
v0.1.0 — breeding-scheme simulation over MCP
On PyPI: breedsim-mcp 0.1.0 — uv add breedsim-mcp
Breeding-scheme simulation driven by AlphaSimR 2.1.0 through rpy2, with one structural rule: a single stochastic run is not a result, and this API will not return one.
Five seeds of an identical three-cycle programme gave genetic gains of [1.151, 1.841, 1.424, 1.429, 1.473] — sd 0.247, the same order as the differences people try to compare. So there is no replicates=1, no raw=True, and no value field anywhere.
Five tools
| tool | returns |
|---|---|
list_methods() |
engine versions, generators, replicate floor, determinism status |
found_population(...) |
session_id + founder provenance + reproducible |
run_program(...) |
per-cycle distributions — mean, sd, 95% CI |
compare_programs(...) |
the paired difference between two programmes, with a CI |
describe_session(...) |
provenance, trait architecture, cycles run |
compare_programs
Comparing two schemes is what the replicate floor was always for. The two arms run on shared founders with common random numbers — replicate i of each starts from identical founders under the same seed — and are differenced within the pair, so the shared luck cancels.
favours is null whenever the difference interval contains zero: that is the answer "not distinguishable at this replicate count", not a missing value.
Two overlapping confidence intervals do not imply no difference. Measured at 12-of-100 vs 18-of-100: the arms span 1.900–2.192 and 1.552–1.909 (overlapping), while the paired difference is [+0.100, +0.531]. That is what the overlap_but_different warning is for, and it is the easiest way to get a breeding comparison wrong.
Reproducibility is reported, not assumed
Two independent things break it, both measured: runMacs founders (its coalescent RNG is seeded once per R session and advances across calls) and OpenMP running multi-threaded (2.397 vs 2.125 from one seed). Every response carries reproducible, and says why when false.
Testing
38 tests against real AlphaSimR — no mocked R — on Python 3.11, 3.12 and 3.13, plus 15 mutation checks all confirmed red (docs/MUTATION-CHECKS.md).
Two of those mutants earned their keep: breaking the pairing made two identical programmes differ by 0.169 and 0.271 — quantifying what common random numbers remove — and a verdict reading the sign of the mean instead of the interval exposed a coverage gap, because a test whose subject is always zero cannot discriminate a rule about signs.
Install
R and AlphaSimR are required and are not light — see the README's install section.
sudo apt-get install -y r-base r-base-dev libtirpc-dev
R -e 'install.packages("AlphaSimR", repos="https://cloud.r-project.org")'
uv add breedsim-mcpLicensed GPL-3.0-or-later because it imports rpy2 (GPLv2+). AlphaSimR itself is MIT. Unofficial and not affiliated with the AlphaSimR authors.