Releases: musharna/plantcv-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. This package's description sits at 91 of the registry's 100 characters, so it had nine to spare and nothing was measuring it.- Dependabot ignores
plantcvandopencv-python: theplantcv==4.11.3pin exists because trait values shift between releases and determinism is an advertised guarantee, and opencv is bounded by that pin.
v0.4.0 — one row per plant
measure_regions() — one row per plant. measure() treats the whole frame as one region, so a tray of seedlings was merged into a single object and every size trait described the group. Regions come from auto_grid or rect_grid, and the response carries an overlay with each region outlined and numbered. An empty cell is refused by name rather than reported as area = 0.0.
Traits checked against known geometry. An 80×80 square must measure 6400 px — arithmetic as the oracle, not a stored baseline.
Audit fixes. measure() reports the PlantCV version that produced the numbers. A fresh-subprocess test drives the tool layer end to end, covering the import-time global-state failure class that bit a sibling server while 27 of its tests stayed green.
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 what the tools do.
FastMCP was renamed, not removed — it is now mcp.server.mcpserver.MCPServer, and Image moved with it.
The dependency moves to mcp>=2,<3 rather than widening to <3. This package imports mcp.server.mcpserver, which does not exist in 1.x, so a range spanning both majors could resolve to a version that cannot import the server at all. 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 — not a bare block sequence, and not a (content, structured) tuple. Tests read .content and .structured_content directly; the old res[0] if isinstance(res, tuple) else res shims were removed rather than extended, because under 2.x that shape sniff falls through and hands back the result object, moving the failure a long way from its cause.
mcp.types fields are snake_case (outputSchema → output_schema, readOnlyHint → read_only_hint). Constructing ToolAnnotations still accepts camelCase via pydantic aliases, but reading the attributes does not — so construction would have stayed green while every read broke.
Full changelog: https://github.com/musharna/plantcv-mcp/blob/master/CHANGELOG.md
v0.2.1 — the package was lying about itself
A docs-and-metadata release. No behaviour changes. It exists because two claims the package made about itself were wrong, and one of them could only be corrected by publishing.
The README told users the package was unpublished
The Install section read "Not published to PyPI. Install from the repository", so anyone following it built from a git checkout while pip install plantcv-mcp had worked since 0.2.0. That was corrected on master — but PyPI renders the description captured at release time, so the project page kept serving the false claim regardless of the fix. That is what this release is for.
plantcv_mcp.__version__ reported 0.1.0 from the published 0.2.0
It was a literal sitting beside a pyproject.toml that said 0.2.0, with nothing enforcing agreement. It now reads from installed metadata — the source packaging already enforces — so the two cannot drift again.
tests/test_version.py asserted only that the version was a non-empty string, which is exactly what a wrong version is, so it passed throughout. It now compares the reported version against what pyproject.toml declares, and was confirmed to fail on the old code before being kept.
Status is now derived rather than asserted
PyPI version and pyversions badges replace the hand-written publication claim and the static Python badge. A hand-typed status line is only true until someone forgets, which is how the Install section went stale in the first place.
Full changelog: https://github.com/musharna/plantcv-mcp/blob/master/CHANGELOG.md
v0.2.0 — real-world units, colour correction, and a mask check that is finally two-sided
The first release actually published to PyPI. 0.1.0 was tagged in the changelog but never uploaded, so everything below shipped together.
The correctness fix that matters most
object_type was hardcoded to "dark" and unreachable, so channels s and b returned the background as the plant: mask_fraction 0.961, and measure() reported area=1007829, width=1024, height=1024 — the whole frame — with no error at all. list_methods() made it worse by recommending 's'. The parameter is now exposed, and the guidance names the polarity each channel needs.
Mask validity is now two-sided. assert_not_degenerate only ever rejected masks that were too small, which left inverting a threshold — the dominant failure of any threshold operation — outside the set of outcomes this system could express as a failure. An implausible_coverage warning fires above 50% frame coverage; measured on the fixture, plant masks land at 0.031–0.046 and inverted ones at 0.959–0.967, so the boundary sits in a wide empty gap.
Added
- Real-world units.
measure(session_id, px_per_mm=…)converts spatial traits tommandmm2— without it every size is in pixels, and pixel sizes are not comparable between images shot at different distance or zoom. The conversion uses an explicit table rather than PlantCV's unit strings, which label bothareaandwidthas"pixels"; a unit-derived rule would leave every area wrong by exactly a factor ofpx_per_mm. calibrate_scale_from_marker— measures a marker of known real size and returnspx_per_mm. It deliberately does not wrappcv.report_size_marker_area: against a synthetic disc of known 80 px diameter that function returnsmajor_axis=79.1with a whole-frame ROI but 348.0 with a tight ROI around the marker, a silent 4.35× scale error under the most intuitive usage. Here the region is cropped before thresholding, removing the mechanism instead of compensating for it.- Colour-card correction.
segment(..., color_correct=true)detects a ColorChecker and corrects to a standard reference;measure()re-applies it so traits are measured on the pixels the mask was drawn on. If no card is found it raises rather than silently measuring the uncorrected image. Mean absolute error to the undistorted original falls from 8.77 to 3.43. measure_images— one recipe across up to 200 images. Batch cannot honour "no number without the picture" literally, so the overlay is replaced by explicit refusal: every image runs the same guards via shared code, and any image tripping a blocking guard returns no traits, only a reason. That is weaker than a human reading a mask, and is documented as such rather than implied.- Colour analysis (
analyses=["size", "color"]), with the three frequency histograms — 692 numbers — withheld unlessinclude_histograms=true, because that is a context-window cost rather than a feature. - Server instructions and tool metadata. Every tool publishes a title and
ToolAnnotations;measure,list_methods,calibrate_scale_from_markerandmeasure_imagespublish anoutputSchema.segmentandsuggest_segmentationreturn image blocks and so have none by nature — a test asserts exactly that split.
Also fixed
fill_size=200was hardcoded and silently erased any specimen smaller than itself; a measured 144 px object became an empty mask. Thresholding and filling are now separate steps, and this reportsfill_erased_maskinstead of presenting as a bad channel choice.measure()no longer destroys the host's PlantCV state —pcv.outputs.clear()wiped a process-global table shared with any application also using PlantCV directly.- The stale-image guard compares content, not just shape: swapping the file for a different image of identical dimensions previously passed, measuring a stale mask against new pixels.
segment()warns on an empty mask, which previously surfaced only ifmeasure()happened to be called afterwards.
Full changelog: https://github.com/musharna/plantcv-mcp/blob/master/CHANGELOG.md