~/satyajit

Splat to Blender with Claude: two MCP servers, a six-step loop, and what it costs in image tokens

mdjsonmcp

2026-10-31 · 22 min · 3d · gaussian-splatting · mcp · blender · agents

Why read this

Notabletop 60%

Reads LichtFeld's MCP and two Blender servers, prices the loop in tokens, tests 'extract first' in toys re-run on real Blender renders. Not end to end.

  • Interactive explanations
  • Original analysis
  • Widely used

Agents & harnessesRuns on a consumer GPUGPL-3.0-or-laterPractitioner teardown

How this was scored
Is it new?
2 of 3: A real new idea, method or capability
Can I trust it?
2 of 3: Measures key facts from files, code or configs
Can I run it?
1 of 3: API-only, gated or restrictive licence
Will I understand it?
3 of 3: Mechanism carried by interactives built from real code or data
Can I act on it?
2 of 3: A concrete recipe, numbers or comparison
Will it last?
1 of 3: Relevant for months
Does it affect many?
2 of 3: A widely used model, tool or lab release
Only here?
2 of 3: A teardown or measurement few others did

Score 67 of 100, ranked 203 of 666 rated articles. Each question is answered 0–3 by hand, and a 3 is rare. How articles are scored

On 30 and 31 October 2026 the 3DGS practitioner kotohibi (@kotohibi_3d) posted two screen recordings. Each is a Blender viewport flying through a grey, untextured model of an elevated expressway and the street under it. The model in each was built by Claude, from a 3D Gaussian splat, through two MCP servers. The first post says the setup was "Claude Sonnet 5.5 + LFS MCP + Blender MCP". The second says "Opus 5.5 high + LFS MCP + Blender MCP", that fidelity went up with Opus, and that the author expects accuracy and efficiency to improve again if the deterministic parts are extracted first and handed to Claude. It could, the author adds, become a skill.

I wanted to know what is actually on the wire in a setup like that. The author has not published prompts or tool calls, so I could not reproduce it, and this machine has no GPU and no LichtFeld build (an earlier version of this article also said it had no Blender; it can now import Blender 5.0.1 as a module, headless). What I could do is read the code the two servers run, price the pictures the loop sends, watch the two videos frame by frame, and test the author's hypothesis in two small toys. A later pass installed Blender itself as a Python module (CPU only) and re-ran the second toy with real renders; that is the section "What running it showed". This article is those five things, with each claim marked measured (a number I computed), read (from source), reported (a claim by someone else), reasoned (my arithmetic or inference) or inferred (a guess about a loop nobody has shown).

What the two posts show

"LFS" is almost certainly LichtFeld Studio, the open-source 3DGS trainer and viewer whose tutorials the same author posts, and whose repository contains an MCP server (inferred from the author's other posts; read for the server). The posts do not say which Blender MCP server they use. Both posts quote the author's earlier post of a 1.5 km round-trip capture, which suggests that capture is the input, though neither says so in as many words.

I sampled twelve frames from each video and cropped the Blender outliner from one frame of each (measured by looking; nothing from the videos is on this page). Both are 3840 by 2092 at 30 frames per second, 33.8 s and 48 s long.

First test, 30 OctSecond test, 31 Oct
Model, as postedClaude Sonnet 5.5Opus 5.5, effort high
Servers, as postedLFS MCP + Blender MCPLFS MCP + Blender MCP
What the sampled frames showAn elevated expressway on piers with a curved ramp, plain box buildings, a ground that looks stepped and jagged, with dark and green patchesThe same kind of scene, with piers that have chamfered portal frames, an underside to the carriageways, smooth asphalt with dashed centre lines, edge lines and a crosswalk, street lamps, utility poles, a lattice pylon, round trees, a pale sky
What the outliner showsA collection Street_V1 holding a camera Cam_Overview and a few coarse objects: Curb, Grass, Road, Sidewalk, Street_houseCollections Road (LaneMarkings, Medians, RoadSurface, Sidewalks), Markings, Extras (RoundTower), and objects named like PortalPier_10_B and RoundBuilding_ramp_N
The author's readingSince it is Sonnet, Opus may do better; the SfM point cloud and cubemaps can also be referenced, so there is room to growFidelity rose with Opus; referencing both the 3DGS and the training images looks best; extract the deterministic parts first; maybe a skill

The second model's scene is, on the evidence of the frames, more granular and more tidily organised: more objects, more named parts, finer detail. That is a difference between two outputs and I can say it. What I cannot say from these frames is how close either is to the splat. The splat is not on screen in the frames I sampled, and the author's judgement ("fidelity went up") is the only measure of it. I also cannot separate the causes: the model changed, and the author's note about using both the splat and the training images may describe a changed prompt or a conclusion drawn afterwards. The posts do not say.

LichtFeld Studio's MCP server

I read commit 8a9f5ef (30 October 2026) of MrNeRF/LichtFeld-Studio, GPL-3.0-or-later, under src/mcp, src/app/mcp_*.cpp and the Python bridge. Nothing was run: the permission layer in this sandbox refuses to execute downloaded code, and there is no GPU.

The transport (read). The server identifies itself as lichtfeld-mcp and implements initialize, ping, tools/list, tools/call, resources/list and resources/read. Over HTTP it has one route, POST /mcp, on port 45677. A script, scripts/lichtfeld_mcp_bridge.py, bridges stdio clients to that endpoint and starts the app on demand, waiting up to 90 s; its default log path sits under ~/.codex. Tool results that carry a PNG are returned as MCP image content, which is how a capture reaches the model as a picture rather than as base64 text.

Who may call it (read). The listener binds to 127.0.0.1 unless expose_network is switched on. Every request needs a JSON content type, a local Host header and either no Origin or a local one, so a web page in your browser cannot drive the app and a DNS-rebinding trick fails the Host check. A 64-character hex bearer token lives in a file in the config directory, and the code refuses to use a token file that group or others can access. The token is required only when expose_network is on, and not even then for a local peer with a local Host and no Origin. In the default configuration any program on your machine can drive the app, with no token.

The tool surface (measured by script over the registering files, read for the schemas). I extracted 132 tool names in 20 groups: 130 concrete tools in 18 groups, plus two dynamic prefixes (editor., shader.). A few more register from other files. The big groups are scene (27), sequencer (17) and ui (15). A modelling loop touches about ten tools in six groups.

Four of them carry the loop. render.capture renders the splat either from the live viewport or, given a camera_index, from a dataset camera's pose and vertical field of view. Width and height go up to 16,384 pixels, with a nearest-neighbour resize, and the result is a PNG. Its own description adds that lens cameras are rendered as pinhole, so a fisheye capture is not shown through its real lens. camera.set_view takes an eye, a target, an up vector and an optional field of view: a free viewpoint, not only the capture cameras. camera.list returns the dataset cameras and their UIDs. dataset.get_info describes the loaded scene. scene.load_dataset loads a COLMAP directory and takes a training strategy and an iteration count, so this one app can drive the capture-to-splat step too.

Two tools matter for the author's "extract first" idea, and both are smaller than they sound. gaussians.read returns raw means, scales, rotations, opacities and spherical-harmonic coefficients, but at most 1,024 rows and 6 fields per call (the constants are MAX_MCP_GAUSSIAN_ROWS and MAX_MCP_GAUSSIAN_FIELDS). A splat of 3 million Gaussians would take 2,930 calls to read one field set (reasoned). The model cannot extract anything from the splat through that tool. The deterministic work has to happen outside the model, on an exported file (scene.export_ply and several other exporters exist), and the model receives its output. The other one is selection.by_description: it selects Gaussians from a natural-language description by sending a render to an LLM with vision, using whichever of ANTHROPIC_API_KEY or OPENAI_API_KEY is in the app's environment. The app has its own LLM client, with Anthropic and OpenAI providers. If you use that tool, your renders leave your machine for a cloud API, a different path from the MCP client talking to the app.

The Blender side

Blender has two MCP servers I could check.

The community one, which its README now calls MCP for Blender, is MIT-licensed; I read ahujasid/blender-mcp at commit 992233b (29 October 2026). It is two programs. A server speaks MCP to the client; an add-on inside Blender listens on a plain TCP socket, localhost:9876, and accepts JSON commands (read). The README lists nine tools, and the server file registers 14 with @mcp.tool (read: the other five are search_mentions, open_viewport and three viewport tools marked app-only, viewport_latest, viewport_capture and viewport_pick, which exist for the in-chat viewer and are hidden from the model): execute_blender_code, look, get_scene_info, generate_3d, search_assets, import_asset, get_addon_status, disable_telemetry and record_trajectory_feedback. The add-on's always-on handler table has 21 commands (measured by counting the table; the add-on's protocol version constant is 15), among them get_scene_info, get_viewport_screenshot, pick_viewport_object, execute_code and export_scene. The look tool is the one a compare loop wants: the README describes a view from the viewport, a camera, an auto-framed multi-angle sheet or an animation strip, drawn in solid, material, rendered, wireframe or x-ray. A code comment in the add-on explains a trap it avoids: grabbing the window is all black whenever Blender is not composited in the foreground, which is the normal case when it is driven through MCP, so the add-on renders the viewport offscreen instead and falls back to a window grab only when there is no GPU context. The response says which path produced the image. One more read detail matters for a loop: look accepts rendered shading for EEVEE and Workbench only, so a Cycles look needs a render you ask for in code.

A failed execute_code is returned as an error whose message is JSON with the exception type, the message and a full traceback with line numbers into the submitted code (read), which is what a model needs to repair a script. It also refuses to start at all in a background Blender: start() returns early with a message to use a GUI or a virtual display when bpy.app.background is true (read), so the add-on cannot serve a truly headless session without being patched.

Its safety notes are candid. execute_code is arbitrary Python in your Blender process, and "that is the product feature", by the module's own words. An opt-in safe mode, BLENDER_MCP_SAFE_MODE=1, checks each model-written script against an allowlist (no os, sys, subprocess, no eval, no operators that install extensions or run scripts) before it crosses the socket. The same note says the socket accepts a raw execute_code from any local process, so the guard covers the MCP path only: "a guard on what the model can be talked into, not a sandbox around Blender". Collecting prompts, code and screenshots for telemetry is opt-in and asked once per conversation, for clients that can ask.

The official one comes from Blender Lab and needs Blender 5.1 or newer (read on Blender's page; the repository is projects.blender.org/lab/blender_mcp, and the page names v1.0.3). It describes three components, an add-on, an LLM client and the server. It also warns that the server "will execute LLM generated code in Blender without any guards in place" and recommends a virtual machine or a system without sensitive data. The page does not list tool names, a transport or a licence.

Which of the two the author runs is not stated. The loop below needs only two things from either: run Python, and show me the viewport.

The loop between them

The posts give a setup and a verdict. The loop in between is the obvious one, and each step below is marked as in the posts or inferred.

Three details are worth a closer look. First, step 6 is the fragile one. The splat render comes from a LichtFeld camera pose in the SfM frame; the Blender view comes from a Blender camera. For the two to be comparable the pose has to cross from one program to the other with the axes, the units and the vertical field of view all matching. The PLY convention, for instance, is not Blender's: our Blender 5.3 article walks through the axes the importer applies. Second, Blender 5.3 imports splats natively, so there is a second route that skips the pose transfer: export the splat, import it into the same Blender scene, and let the model model against it in place (inferred, the posts do not say the author does this). Third, the first test's note that the SfM point cloud and cubemaps can also be referenced matches what the tools could give: points through the Python bridge or an export, cubemap-style views through free camera.set_view captures.

What the model sees, in tokens

A loop that compares pictures spends most of its budget on pictures. The vision documentation gives the cost as ceil(width / 28) x ceil(height / 28) visual tokens, after the image is scaled down, keeping its aspect ratio, to the largest size that fits the model's tier. The standard tier allows 1,568 pixels on the long edge and 1,568 tokens; the high-resolution tier, for Claude 4.7 and later models, allows 2,576 pixels and 4,784 tokens. I implemented that and checked it against the six example rows the documentation prints, in both tiers. All twelve numbers match (measured against the docs; code/tokens.py).

Applied to the videos' own frame size, 3840 by 2092, an image costs 4,692 tokens on the high-resolution tier (scaled to 2576 by 1403) and 1,566 on the standard tier (1492 by 812). A 1920 by 1080 capture costs 2,691 on the high tier and 1,560 on the standard tier. Three views per round, two images each, at 1920 by 1080 is 16,146 tokens per round, and 129,168 after eight rounds if every image stays in the conversation (reasoned: the loop's real image count is the author's, not mine).

The documentation also says that when one request carries more than 20 images, a stricter per-image size limit applies to every image in it, including images resent from earlier turns and those inside tool results. A loop at three views and two images a view crosses 20 images in the fourth round, so a long run is likely to meet that limit sooner than the context window.

What goes wrong

Each of these follows from the tool surface above rather than from a run, so treat the list as reasoned, not observed.

Scale and pose are ambiguous. The splat lives in the SfM frame, whose scale is arbitrary. Nothing in a render says how big a lamp post is. Either something known fixes the scale (a measured distance, a GPS track, an object of known size) or the model guesses from priors about doors and cars.

The splat lies in ways a mesh cannot. Floaters appear as geometry that is not there. Colour depends on the view. Parts nobody photographed are holes, and a surface seen only at a grazing angle is thin. A model that copies what it sees copies those too.

There are no semantics. A splat has no idea that something is a lamp post. Every named part in the second test's outliner, PortalPier_10_B or Medians, is the model's reading of a picture.

Code fails. The model writes Python against Blender's API, and a script that errors halfway can leave a half-built scene. Because the code runs with no guard, it can also change or delete what was there before.

Context grows. Pictures accumulate (see above), and so does the code the model has written, which it must read again to edit.

The author's idea, made concrete

The second post says: extract the deterministic parts beforehand and hand them to Claude. What is deterministic in a street scan? The up direction, the ground plane, the planes of walls and roofs, the scale from one known distance, and the oriented boxes those planes bound. Code can fit them to the SfM points in milliseconds. A model should not have to read them off pictures.

I built a toy to see whether that is even plausible. A synthetic building (12 by 8 by 6 m) stands on a ground plane, seen through a point cloud in an arbitrary frame: a random rotation, a random scale between 0.3 and 3, Gaussian noise, a share of floaters, and two camera centres 2.0 m apart that a human measured to 1%. A sequential RANSAC finds the ground, the roof and up to four walls; yaw comes from the wall normals folded mod 90 degrees; the box size from the gaps between parallel walls; the scale from the camera baseline. Twelve seeds per cell, 16 cells (measured; code/extract.py).

Up to a quarter of the cloud as floaters, every cell recovers the box within about 2 to 3% and the up axis within a fifth of a degree, and the scale error is just the baseline measurement's own 1%. At 10% floaters every one of the twelve seeds succeeds at every noise level. At 40% floaters it falls to 8, 9, 8 and 6 of 12 as the noise rises from the cleanest cloud to the noisiest. This is a toy: the cloud is generated, planes are exactly planar, and a real street has trees, cars and a bad day for SfM. It supports a narrower claim than "this works": if the points are decent, the numbers an agent would otherwise have to guess are cheap and exact.

The second question is whether starting from them helps a loop that compares pictures. I removed the language model and kept the loop. A "modeller" edits three oriented boxes (a main building, an annex and a tower) by a stochastic local search. It nudges one parameter, re-renders silhouettes from C cameras, and keeps the edit when the mean silhouette IoU against the target does not get worse. Each render is one "evaluation". It stops at IoU 0.9 or after 2,500 renders. I compared a random start with a start that has the main box nearly right (sizes within about 2%, as the extraction above gets to within 1 to 4%; the yaw within 0.5 degrees and the position within 0.3 m, which are my assumptions, since the extraction toy did not score yaw or position), with one, three and six cameras, on ten different targets each (measured; code/loop.py). The numbers below are from the numpy silhouettes; the section "What running it showed" repeats them with real Blender renders and gets the same table.

Three things show in the numbers, none of them subtle. A match in the pictures is not a match in space. With one camera, 8 of 10 targets reach silhouette IoU 0.9 from either start, and the 3D IoU of what they built is only 0.45 from a random start and 0.52 from the extracted one. The boxes agree with one view and not with the scene. More cameras make the pictures harder to satisfy (from a random start only 6 of 10 targets get there with three cameras and 5 of 10 with six) and the geometry better (3D IoU 0.66 and 0.72). A model that compares one rendered view with one photo can be fooled the same way, which is one reason to give it several poses.

The extracted start helps most where there are several cameras. With three cameras 7 of 10 targets reach 0.9 instead of 6, and the median run takes 163 renders instead of 580. With six, 9 of 10 reach it instead of 5, and the 3D IoU is 0.84 instead of 0.72. With one camera it barely matters (365 renders against 352; 3D IoU 0.52 against 0.45): the main box was right, but one view cannot place the annex and the tower in depth.

The medians hide a selection effect. At six cameras the extracted start's median is 659 renders against 578 for the random start. That is not slower: the median covers only the runs that got there, and the extracted start gets 9 targets over the line against 5, adding long runs from harder targets. Compare the counts that reached before the medians.

The limits are plain. Ten targets per cell is small, and a difference of one target (7 against 6 at three cameras) is within noise. The searcher is a random local search, which makes far more blind edits than a model would; the extracted start is the true main box with small errors, not an output of the first toy fed through; and the boxes have none of a street's mess. What the toy does show is the shape of the author's hypothesis: numbers that code can fix cheaply remove a large part of the search, but they do not replace looking from several poses.

Turning it into a skill

The author's last remark is that it could be a skill. This site's skills are Markdown files with a name and a description in the front matter and numbered instructions below. A skill for this loop would hold, at least: the order of operations (load, extract, build, compare); the exact tool names and arguments (render.capture with a camera_index, camera.set_view, execute_blender_code); the pose-transfer recipe between the two programs, which is where the fragile part lives; a naming convention so the outliner stays legible; a rule to snapshot the .blend before running code; a stopping rule (silhouette overlap, or a fixed number of rounds); and a caution about floaters and scale. I have not tested any of that against a real capture, so I make no claim that such a skill works.

Safety and privacy

Two things need saying. The model's code runs unguarded in your Blender session, by the official page's own warning and the community server's own note. Run it on a copy, in a virtual machine if the scene matters, with the .blend under version control. And LichtFeld's HTTP server accepts any local peer by default: leave expose_network off, which is the default. Related reading on this site is how a coding agent's shell is boxed (the sandbox runtime, Claude Code's sandbox from the source): this loop has no such box around the Blender process.

What running it showed

On 31 October I installed Blender as a Python module (pip install bpy gave bpy 5.0.1, Blender 5.0.1, CPU only, no display) and ran what could be run. The logs and scripts are under public/articles/gaussian-splat-to-blender-with-claude/verify/.

The toy loop with real renders (measured). I kept loop.py unchanged except that every silhouette is now a Cycles render on the CPU (one sample, emission shader, 40 by 40 pixels, a unit cube per box scaled and rotated by the same numbers, a camera with a 40 degree vertical field of view). Same ten targets, same random streams, same six cells. The 3D IoU, the number of targets that reach silhouette IoU 0.9 and the median render counts came out equal in every cell, except that two medians moved in the last digit (3D IoU 0.722 against 0.723 and 0.515 against 0.518; one median of 579 renders against 578). I spot-checked every 25th render against the numpy slab test: 3 pixels differed out of 15.0 million. In other words the numpy projection in the toy was right, so its findings hold on real renders: the extracted start reaches the target on 7 of 10 instead of 6 at three cameras, with a median of 163 renders against 580, and on 9 of 10 instead of 5 at six; one camera gives a good silhouette and a poor 3D IoU (0.45 and 0.52, against 0.84 for the extracted start at six). The run made about 235,000 Blender renders, roughly 4 to 9 ms each. This does not strengthen the toy beyond that: the search is the same random local search, and agreeing exactly with numpy shows the renderer and the slab test describe the same boxes, not that a model would behave like the search.

What a headless Blender gives a viewport tool (measured, plain bpy). In background mode the default scene has a VIEW_3D area, so the add-on's search for a viewport finds one, but gpu.types.GPUOffScreen raises SystemError: GPU functions for drawing are not available in background mode, which is the call the add-on's offscreen path makes. Workbench rendering in this box also failed (an EGL error, then an empty Render Result), while Cycles on the CPU worked. So a server like this needs either a GUI Blender with a GPU context, or a CPU Cycles render requested through execute_code.

What I could not run. I cloned ahujasid/blender-mcp at 992233b and meant to start its real add-on socket server in the headless session and drive it with the server's own client class, to time round trips and to read the screenshot and error replies. The sandbox's permission layer refused to execute that downloaded code, and I did not look for a way around it. Everything above about the add-on and the server (tool counts, the handler table, the error format, the background refusal) therefore stays read, apart from the handler count. The round-trip latency, the headless screenshot reply and the error reply are untested. The LichtFeld Studio MCP server I did not try: it is C++ linked to a CUDA renderer and has no CPU-only build I know of, so there is no slice to run on this machine.

Corrections. The first version said the machine had no Blender and presented the toy as numpy only; both are now qualified above. It also said the README's nine tools are the surface; the code registers 14.

What I could not check

I did not run LichtFeld Studio, either Blender MCP server's code, or the loop with a model in it. I did not see the author's prompts or tool calls, so every step marked inferred is a guess. I looked at 24 sampled frames and two outliner crops, not at the videos in full; I cannot tell how many iterations either took, what the model read, or how close either result is to the splat. The two toys are toys: generated points, exact planes, boxes in place of streets, a local search in place of a model. They test the information flow of the author's idea, not Claude.

What would settle it is a run with a measure. Pick one capture and fix a metric, for example silhouette IoU or Chamfer distance against a reference mesh, and count the object names. Run the same prompt with Sonnet and with Opus, then with and without extracted numbers, and publish the prompts. Then the comparison in the table above would be a result and not a reading.

Sources

MrNeRF/LichtFeld-Studio@8d44568 · snapshot 2026-10-06
tracked files
3,457
license
GPL-3.0
branch
master
tests
593 files
source
43.6 MB
commit date
2026-10-06
source by language
C++31.7 MB(1447)Python5.5 MB(324)JavaScript3.5 MB(5)CUDA2.7 MB(123)C227.8 kB(35)PowerShell41.7 kB(3)HTML37.6 kB(1)

by size of tracked source at this commit, file counts in brackets; docs, data and vendored trees excluded

local clone, 2026-10-06 at 8d44568 — branch, commit, commitDate, fileCount, hasTests, languages, license, licenseFile, shallow, testFileCount

shallow clone: counts describe the pinned tree, not the history

ahujasid/blender-mcp@992233b · snapshot 2026-10-31
tracked files
77
license
MIT
branch
main
tests
28 files
source
1.3 MB
commit date
2026-10-29
source by language
Python1.3 MB(48)HTML17.3 kB(1)Shell2.2 kB(1)PowerShell2.0 kB(1)Dockerfile0.7 kB(1)

by size of tracked source at this commit, file counts in brackets; docs, data and vendored trees excluded

local clone, 2026-10-31 at 992233b — branch, commit, commitDate, fileCount, hasTests, languages, license, licenseFile, shallow, testFileCount

shallow clone: counts describe the pinned tree, not the history

Cite this article

For attribution, please use the following reference or BibTeX:

Satyajit Ghana, "Splat to Blender with Claude: two MCP servers, a six-step loop, and what it costs in image tokens", ai.thesatyajit.com, October 2026.

bibtex
@misc{ghana2026gaussiansplattoblenderwithclaude,
  author = {Satyajit Ghana},
  title  = {Splat to Blender with Claude: two MCP servers, a six-step loop, and what it costs in image tokens},
  url    = {https://ai.thesatyajit.com/articles/gaussian-splat-to-blender-with-claude},
  year   = {2026}
}
share