Releases: musharna/ldraw-mcp
Release list
v0.2.3
Patch — docs and comments. Behaviour is identical to 0.2.2.
- Brings this repo level with the sibling MCP servers on community health:
CONTRIBUTING.md,SECURITY.md, issue forms, a PR template,.editorconfigand CodeQL. (.mcp.json,glama.jsonand Dependabot were already here — this repo led on those.) SECURITY.mddocuments the trust boundary specific to this server: it launches Blender as a subprocess, with the command built as an argv list run without a shell, a bundled render script,--factory-startup, and the binary chosen by the operator rather than the caller.- The one
src/change is a measured comment explaining whyDEFAULT_AZIMUTHSare 180° apart — coverage rises with separation and peaks there. The value is unchanged. project.urlsgainedRepository/Issues/Changelog; the licence badge is now derived rather than a hardcoded literal.
v0.2.2 — blank renders fail loud
A blank render is now an error instead of a success.
0.2.1 renders correctly, so nobody is broken today. What this release adds is that the next scene-setup defect fails loudly rather than returning a well-formed PNG of nothing — which is exactly how #14 reached a user.
Why this, and not just the clip fix
Fixing the clip planes in 0.2.1 removed one cause of an empty scene. It did nothing to make an empty scene detectable. The render path had no output postcondition at all: the import succeeded, Blender exited 0, a well-formed PNG landed on disk, and a frame containing nothing was indistinguishable from a good one at every layer. Hidden geometry, a misaimed camera, an import that yields nothing, or dead lights would each have reached a user the same silent way.
What changed
render_ldraw rejects any view whose largest per-channel value range is at or below BLANK_FRAME_MAX_SPREAD (24).
- Per view, before stitching. One blank view beside one good one stitches into an image full of contrast, so the same check applied to the combined result would miss half the failure.
- Extrema, not a count of differing pixels. Extrema does not depend on how much of the frame the model fills, so a small part rendered at high resolution cannot read as blank.
- No new dependency — Pillow was already required.
Measured separation is ~34x with nothing in between: a blank frame scores 6 (cycles sampling noise only), the sparsest legitimate render measured — model covering just 1% of the frame — scores 207.
Behaviour change
render_ldraw can now raise LDrawRenderError where it previously returned a path to a blank image. A caller that assumed it always got a file back has a new error path to handle. Patch level regardless, because a blank image was never a correct result.
Verification
The guard was seen to fail before being trusted: deleting it makes both rejection tests fail with DID NOT RAISE while the controls stay green, and moving the threshold to 0 is caught by the real blank fixture. Reverting the 0.2.1 clip fix and rendering through real Blender raises 2 of 2 view(s) rendered blank.
v0.2.1 — the blank-render fix
0.2.0 rendered a uniform grey frame with no model in it for small models — anything spanning less than ~4.5 cm, roughly 11 studs. This release fixes that. (This sentence originally read “for every input”; see the correction at the bottom.)
What was wrong
The camera distance was derived from the scene bounds; the camera clip planes were not. ImportLDraw imports at real-world metre scale — two 2×4 bricks genuinely span 3.2 cm — so distance = span * 2.2 placed the camera 7 cm from the model, inside Blender's default clip_start of 0.1 m. Every mesh was clipped away before shading, and the frame rendered as the bare white world, which AgX maps to a plausible grey.
Nothing failed along the way. The import succeeded (472 vertices per brick, nothing hidden from rendering), Blender exited 0, and the PNG was well-formed. The fault was a parameter that was never set, so no line of code reads wrong on inspection — only rendering and measuring the pixels found it.
The fix
Both clip planes are now derived from the same distance the camera position already uses. That holds at any model scale, rather than special-casing small models with a fixed tiny near plane that would quietly cost depth precision on large ones.
Measured on an otherwise identical scene:
| max pixel deviation | pixels differing >40 | |
|---|---|---|
clip_start = 0.1 (before) |
6 | 0 / 40000 |
| derived from span | 195 | 13010 / 40000 |
Verified from the built wheel in a clean environment, not just the source tree: max deviation 412, 105023 of 320000 pixels.
Also
- The blank-frame eval's control had never run in CI. It sat under a module-level
skipif(not is_available())and CI runners have no Blender, so the one check proving the detector can tell a blank frame from a drawn one was skipped exactly where it mattered — while its docstring claimed it ran without a renderer. The skip now applies only to the test that drives Blender, andnumpy(which that control imports) joins thetestextra instead of being present by luck. server.json's fourth version copy is now guarded._meta.publisher-provided.versionwas not covered by the version-sync test that exists precisely because unchecked version sources drift.
Fixes #14.
Correction (2026-08-01)
This note originally said the bug affected "every input". Measured, that is false: the
failure is size-dependent. A model is clipped only when span * 2.2 falls below
Blender's default 0.1 m near plane — about 4.5 cm, roughly 11 studs across.
Measured on 0.2.0's own code:
| model | span | camera distance | result |
|---|---|---|---|
| single 2×4 brick | 32 mm | 0.070 m | blank — 0 pixels differ from background |
| two bricks, 56 mm apart | 56 mm | 0.123 m | renders correctly |
| two bricks, 192 mm apart | 192 mm | 0.422 m | renders correctly |
So small models — the single part, the demo, the test case — were broken, while larger
builds were always fine.
The same correction applies to 0.1.0, 0.1.1 and 0.1.2: none of them set the clip planes
either (verified against the published 0.1.0 sdist, which has no git tag), so every release
before 0.2.1 carries the defect under that size threshold.
Separately, main now rejects a blank render instead of returning it as a success — the
render path had no output postcondition, which is why this shipped invisibly in the first
place (#18).
v0.2.0 — mcp 2.x, and the one removal that needed new code
The mcp 2.x migration — the "separate work" that 0.1.2 deferred when it pinned mcp<2 to stop the broken installs.
FastMCP → mcp.server.mcpserver.MCPServer, with Image moving alongside it. 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 can resolve to a version that cannot import the server. That is precisely how dependabot's mcp<3 proposal (#7) broke the build, and why it was closed rather than merged. mcp 2.x requires Python >=3.10, exactly this package's floor, so the support matrix is unchanged.
The one removal that needed new code
create_connected_server_and_client_session is genuinely gone from mcp.shared.memory — the single change in this migration that was a removal rather than a rename, and the reason #7's CI failed for a second reason beyond the server break.
The replacement is mcp.client.Client, which accepts an MCPServer directly. It is better than what it replaces on two counts: no reaching through the private _mcp_server attribute, and the context manager initialises the session so the explicit initialize() call disappears. It remains a real client over in-memory transport rather than a direct call into the tool functions, which is the point of that test.
Version drift
__version__ now reads from installed metadata instead of being a fourth hand-maintained copy. test_packaging.py already guarded all four sources — it exists because this repo once shipped __version__ 0.1.0 while everything else said 0.1.1 — so this removes a source of drift rather than replacing the check that catches it. server.json and CITATION.cff cannot be derived and are still asserted.
Full changelog: https://github.com/musharna/ldraw-mcp/blob/main/CHANGELOG.md
v0.1.2 — pin mcp<2 so installs work again
Fixes broken installs against the mcp 2.0.0 SDK.
The dependency was an unbounded "mcp". The MCP spec revision 2026-07-28 shipped alongside mcp 2.0.0 on the same day, so a clean pip install ldraw-mcp resolved the new major — in which mcp.server.fastmcp no longer exists — and the server failed at import with:
ModuleNotFoundError: No module named 'mcp.server.fastmcp'
Now pinned mcp<2.
Migrating to the 2.x API is separate work, done in 0.2.0. The new spec's deprecations carry a twelve-month minimum window, and this server is stdio, which the stateless-transport changes do not touch.
Full changelog: https://github.com/musharna/ldraw-mcp/blob/main/CHANGELOG.md
v0.1.1
Distribution release — published to the official MCP registry (io.github.musharna/ldraw-mcp), PyPI (pip install ldraw-mcp), and MCP directories.
Changes
- Publish to the official MCP registry (
server.json+ OIDC workflow) and directory metadata (Glama, Cursor). mcp-namemarker in README for PyPI-ownership verification.- CI workflow (pytest matrix 3.10–3.13).
Run it: uvx ldraw-mcp or pip install ldraw-mcp → ldraw-mcp-setup → claude mcp add ldraw -- ldraw-mcp.