v4.1.0 — cut the token cost of the read surface
This release cuts what BloxForge costs an AI agent to use. Everything in it was found by driving the MCP server against a live Roblox Studio session and measuring the responses.
Measured against a live place
| before | after | |
|---|---|---|
get_file_tree from the game root |
326 KB / 2345 nodes | 5 KB / 99 nodes |
capture_screenshot (default) |
918 KB, 3130×1760 | 326 KB, 1568×882 |
structuredContent duplication |
on every tool | only the 56 declaring outputSchema |
load_toolset over /mcp |
tools/list never changed |
29 → 74 |
Fixed
get_file_tree spent 96% of its response on things nobody authors. In a place holding 24 parts, Stats (1194 nodes), StylingService (560), MemStorageService (171), CoreGui and PluginGuiService (92 each) were 2246 of 2345 nodes, and another 107 services were present but empty. One call could exhaust an agent's context to describe 99 nodes of real content. Filtering happens in the plugin, so the data never crosses the bridge. It applies only to the DataModel's own children and only when the caller named no root — get_file_tree on game.CoreGui is unchanged, include_internal: true restores the full walk, and the response reports how many services it left out. A denylist rather than an allowlist of authorable services: if Roblox ships a new noisy service the cost is a larger response, never hidden user content.
load_toolset did nothing over the Streamable HTTP transport. applyToolset — the only caller of registry.activate — lives on the stdio server. Over /mcp the tool reported 74 tools loaded, the very next tools/list still returned the 29 core ones, and client_hint blamed the host's schema-refresh step for something the server had never done. The tools stayed callable blind throughout, so nothing failed loudly.
capture_screenshot sent 5.2× the pixels it needed to. A Retina Studio window captures 3130×1760 for a 1365×768 logical viewport, and that raw RGBA crossed the bridge in full. Vision models resize past roughly 1568px anyway, so the extra pixels were transferred and then discarded. The new maxWidth defaults to 1568; maxWidth: 0 returns the native capture for reading fine text. The reported size and the simulate_mouse_input coordinate conversion always describe the image actually sent, so click coordinates stay correct at any width.
Argument errors named nothing you could act on. mass_get_property answered "Paths array and property name are required", leaving an agent to work out that those are paths and propertyName — a wasted round trip per miss. 23 messages across six files now name the parameter, each checked against the tool's declared required list. The ui_create_* message named a tool that does not exist: TextLabel.toLowerCase() produced ui_create_textlabel rather than ui_create_text_label.
load_toolset and tool_catalog_search answered malformed requests instead of rejecting them. {"toolsets":"scene"} coerced to an empty selector list and returned a success shape; tool_catalog_search with no query ranked the catalog against an empty string and returned the first eight tools as if they were matches.
Error classification missed "requires". A handler writing get_roblox_docs requires a name fell through to UNKNOWN with no recovery hint instead of INVALID_ARGUMENT.
The protocol handshake now fails closed. A Studio plugin older than protocol v3 gets a 426 with an exact instruction, not the 409 the plugin retries forever.
Changed
structuredContent is attached only to tools that declare an outputSchema. It is a byte-for-byte copy of the text block, so attaching it everywhere charged each response twice — 45% of the bytes measured over a live session — and 157 of 213 tools declare no schema, leaving a client nothing to validate the copy against. The MCP specification's compatibility guidance runs the other way: a server returning structured content should also send the serialized JSON as text, and that text is what every response already carries. The text channel is unchanged for every tool, so a client reading content sees exactly what it saw before.
Added
Asset Manifest v1 — bloxforge.assets.json, plus asset_manifest_status, asset_manifest_plan and asset_manifest_scan. An asset in a place is an opaque numeric ID; nothing recorded which local file produced it, with which import settings, or which version is published. asset_manifest_plan returns an immutable planHash covering the manifest and the current content of every file it references, so swapping a texture between preview and apply invalidates the preview.
mass_delete_objects — the one bulk CRUD verb that had no bulk form. The whole batch is a single undo step, and it is wired to the safety manager's bulk_delete kind, which had been implemented but never connected to any tool.
Undoable generated Luau. execute_luau opens a ChangeHistoryService recording when the caller declares a label, so every recipe, terrain, lighting and mutation-plan write is one undo waypoint. Verified against a running Studio: one Ctrl+Z reverts the whole call.
New build gates
check-argument-errors.mjs and check-undo-coverage.mjs run in protocol:check; check-doc-links.mjs runs in docs:check; CI enforces a test-coverage ratchet. Each gate was verified to fail when the thing it guards is broken.
npm run release:check — exit 0, 961 tests.
Full changelog: v4.0.3...v4.1.0