Skip to content

Releases: musharna/plantcv-mcp

v0.4.1

Choose a tag to compare

@musharna musharna released this 02 Aug 05:43
661c864

Patch — docs and repo hygiene only. No src/ change, so the installed code is byte-identical to 0.4.0.

  • CONTRIBUTING.md and SECURITY.md now ship in the sdist, alongside issue forms, a PR template, .editorconfig, .mcp.json, glama.json, CodeQL and Dependabot.
  • server.json is 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 plantcv and opencv-python: the plantcv==4.11.3 pin 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

Choose a tag to compare

@musharna musharna released this 01 Aug 23:19
ca6c1bd

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

Choose a tag to compare

@musharna musharna released this 31 Jul 04:52
5c94478

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:

  1. Querying the licence vocabulary endpoint (MIT → 404, mit → 200) and treating that as what the ingest accepts. It is not.
  2. 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

Choose a tag to compare

@musharna musharna released this 31 Jul 04:11
2d3ab2a

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.json states the version in three places, with nothing else making them agree with pyproject.toml
  • the README marker must match the name server.json declares — 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

Choose a tag to compare

@musharna musharna released this 31 Jul 00:52
cf86609

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 (outputSchemaoutput_schema, readOnlyHintread_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

Choose a tag to compare

@musharna musharna released this 31 Jul 00:52
878bd6b

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

Choose a tag to compare

@musharna musharna released this 31 Jul 00:52

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 to mm and mm2 — 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 both area and width as "pixels"; a unit-derived rule would leave every area wrong by exactly a factor of px_per_mm.
  • calibrate_scale_from_marker — measures a marker of known real size and returns px_per_mm. It deliberately does not wrap pcv.report_size_marker_area: against a synthetic disc of known 80 px diameter that function returns major_axis=79.1 with 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 unless include_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_marker and measure_images publish an outputSchema. segment and suggest_segmentation return image blocks and so have none by nature — a test asserts exactly that split.

Also fixed

  • fill_size=200 was 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 reports fill_erased_mask instead 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 if measure() happened to be called afterwards.

Full changelog: https://github.com/musharna/plantcv-mcp/blob/master/CHANGELOG.md