Skip to content

Releases: musharna/ldraw-mcp

v0.2.3

Choose a tag to compare

@musharna musharna released this 02 Aug 05:43
5140216

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, .editorconfig and CodeQL. (.mcp.json, glama.json and Dependabot were already here — this repo led on those.)
  • SECURITY.md documents 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 why DEFAULT_AZIMUTHS are 180° apart — coverage rises with separation and peaks there. The value is unchanged.
  • project.urls gained Repository/Issues/Changelog; the licence badge is now derived rather than a hardcoded literal.

v0.2.2 — blank renders fail loud

Choose a tag to compare

@musharna musharna released this 01 Aug 07:11
ad7a1eb

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.

Full detail in #18 and the CHANGELOG.

v0.2.1 — the blank-render fix

Choose a tag to compare

@musharna musharna released this 01 Aug 00:07
d0d41cd

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, and numpy (which that control imports) joins the test extra instead of being present by luck.
  • server.json's fourth version copy is now guarded. _meta.publisher-provided.version was 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

Choose a tag to compare

@musharna musharna released this 31 Jul 04:06
a9be36f

The mcp 2.x migration — the "separate work" that 0.1.2 deferred when it pinned mcp<2 to stop the broken installs.

FastMCPmcp.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

Choose a tag to compare

@musharna musharna released this 31 Jul 00:52
de68965

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

Choose a tag to compare

@musharna musharna released this 02 Jul 06:08

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-name marker in README for PyPI-ownership verification.
  • CI workflow (pytest matrix 3.10–3.13).

Run it: uvx ldraw-mcp or pip install ldraw-mcpldraw-mcp-setupclaude mcp add ldraw -- ldraw-mcp.