[{"slug":"ZV-2026-1949","server_name":"3dcms.info","severity":"breaking","title":"3dcms.info: Type of confirmReplace on publish_scene changed boolean → boolean|string.","summary":"[safe] Description of get_scene_screenshot changed (5% word delta). [safe] Description of list_assets changed (21% word delta). [risky] Optional field scope was added to list_assets; may shift model behaviour. [breaking] Type of confirmReplace on publish_scene changed boolean → boolean|string. [risky] Description of upload_asset changed (29% word delta). [risky] Optional field assetId was added to upload_asset; may shift model behaviour.","changes":[{"kind":"description_changed","tool":"get_scene_screenshot","after":"Render the scene server-side (real WebGL, headless) and return screenshot image(s) — your EYES on the scene. Use it after apply_ops batches and after probe_framing, and judge each image as a photographer and a web designer would: one clear subject, on a third (for a page's own camera, the whole card at a readable angle); the horizon off the middle; the eye at a standing height; text legible and on even margins; a way out in view; the colours serving the brief (one accent, on the primary action); nothing floating, clipped or overlapping. Beside each image comes `composition`, read from its own pixels: tonalRange (p5 to p95 of lightness; under 0.35 is flat), clipping (share blown to white, crushed to black), key (low, mid, high), a 3x3 thirds grid of lightness and detail with the busiest and brightest cells, the hues on screen by share, and findings in sentences. Fix with apply_ops. Discipline: at most 2 screenshot→fix rounds per user request, then report what you built. views (max 4, default = the visitor's entry view): pass \"page:<pageIdOrSlug>\" to frame one page/neighborhood, or {position:[x,y,z], target:[x,y,z], fov?} for an exact camera. Rendering is real but slow — ~10s for simple scenes, and the server gives up after 45 s per view by default; a 400-node scene with many textures needs more, so pass timeoutSeconds (up to 180) and a smaller maxDim (a 512-wide shot is a fine check of placement and readability and renders faster). A timeout comes back as an error naming it, never as an empty image; repeat shots of an unchanged scene at the same size are cached, instant and free; only a fresh render counts against the daily budget (a failed one does not), and each reply says shotsLeft. Rate-limited: prefer ONE well-chosen view; only multi-view when areas are far apart (four views at 180 s can hold the renderer for twelve minutes). If it returns a busy/unavailable error, keep building and retry once later — do not loop on it.","before":"Render the scene server-side (real WebGL, headless) and return screenshot image(s) — your EYES on the scene. Use it after apply_ops batches and after probe_framing, and judge each image as a photographer and a web designer would: one clear subject, on a third (for a page's own camera, the whole card at a readable angle); the horizon off the middle; the eye at a standing height; text legible and on even margins; a way out in view; the colours serving the brief (one accent, on the primary action); nothing floating, clipped or overlapping. Beside each image comes `composition`, read from its own pixels: tonalRange (p5 to p95 of lightness; under 0.35 is flat), clipping (share blown to white, crushed to black), key (low, mid, high), a 3x3 thirds grid of lightness and detail with the busiest and brightest cells, the hues on screen by share, and findings in sentences. Fix with apply_ops. Discipline: at most 2 screenshot→fix rounds per user request, then report what you built. views (max 4, default = the visitor's entry view): pass \"page:<pageIdOrSlug>\" to frame one page/neighborhood, or {position:[x,y,z], target:[x,y,z], fov?} for an exact camera. Rendering is real but slow — ~10s for simple scenes, and the server gives up after 45 s per view by default; a 400-node scene with many textures needs more, so pass timeoutSeconds (up to 180) and a smaller maxDim (a 512-wide shot is a fine check of placement and readability and renders faster). A timeout comes back as an error naming it, never as an empty image; repeat shots of an unchanged scene at the same size are cached and instant. Rate-limited: prefer ONE well-chosen view; only multi-view when areas are far apart (four views at 180 s can hold the renderer for twelve minutes). If it returns a busy/unavailable error, keep building and retry once later — do not loop on it.","detail":"Description of `get_scene_screenshot` changed (5% word delta).","severity":"safe","descriptionDelta":0.04932735426008972},{"kind":"description_changed","tool":"list_assets","after":"Search the images, models and sounds in the account AND the shared public library (frames, props, texture sets; `scope` mine|shared|all, default all). A row with shared:true is copied in first with upload_asset {assetId}: place the url that returns, never the shared one. This is how you TEXTURE something with what the user already owns: search for the look you want, take the `url` of the result, and bind it on an apply_ops add or assignMaterial as `materialMaps: { baseColor: \"<url>\" }`, plus `normal` and the set's packed `ormh` (or `orm`), or separate `roughness` / `metalness` maps. PBR sets are stored as SEPARATE images that share a setId; the base color member is often unsuffixed (mapRole null) while its siblings end in the map name (\"... Normal\", \"... Ormh\"). Search the set name, then pick one url per slot by mapRole; a base colour alone already reads as a texture. ALWAYS search rather than listing: an account can hold thousands of images. `q` is a set of terms that must ALL appear in the label or keywords, so \"weathered wood\" narrows instead of returning everything wooden. Returns id, kind, label, url, sizeBytes, keywords, mapRole and setId. mapRole names the slot an image is (baseColor | normal | roughness | metalness | height | emissive | ao | orm | ormh, or null when the label does not say); setId is shared by every image of one PBR set, so bind a whole set by matching setId and placing each url by its mapRole. orm/ormh are channel-packed (R=AO, G=roughness, B=metalness, A=height): bind one on materialMaps.ormh (or .orm), never as roughness; the channels are split for you. An `emissive` member goes on materialMaps.emissive. `kind` is \"image\" for textures and \"glb\" for models (a glb url goes on a geometry node as data: {primitive:\"imported\", assetUrl}, never as the top-level primitive), \"audio\" for sounds (data.audioClip.src).","before":"Search the images and 3D models already in the account. This is how you TEXTURE something with what the user already owns: search for the look you want, take the `url` of the result, and bind it on an apply_ops add or assignMaterial as `materialMaps: { baseColor: \"<url>\" }`, plus `normal`, and the set's packed `ormh` (or `orm`) file on materialMaps.ormh, or separate `roughness` / `metalness` maps when the set has those instead. PBR sets are stored as SEPARATE images that share a setId; the base color member is often unsuffixed (mapRole null) while its siblings end in the map name (\"... Normal\", \"... Ormh\"). Search the set name, then pick one url per slot by mapRole; a base colour alone already reads as a texture. ALWAYS search rather than listing: an account can hold thousands of images. `q` is a set of terms that must ALL appear in the label or keywords, so \"weathered wood\" narrows instead of returning everything wooden. Returns id, kind, label, url, sizeBytes, keywords, mapRole and setId. mapRole names the slot an image is (baseColor | normal | roughness | metalness | height | emissive | ao | orm | ormh, or null when the label does not say); setId is shared by every image of one PBR set, so bind a whole set by matching setId and placing each url by its mapRole. orm/ormh are channel-packed files (R=AO, G=roughness, B=metalness, A=height): bind one on materialMaps.ormh (or .orm), never as roughness, and the channels are split out for you, so a library set is baseColor + normal + ormh. An `emissive` member goes on materialMaps.emissive. `kind` is \"image\" for textures and \"glb\" for models (a glb url goes on a geometry node as data: {primitive:\"imported\", assetUrl}, never as the top-level primitive; the low-poly picture frames face +X, rotate [0, 1.5708, 0] to hang one on a wall facing +Z).","detail":"Description of `list_assets` changed (21% word delta).","severity":"safe","descriptionDelta":0.2068965517241379},{"kind":"input_property_added","path":"inputSchema.properties.scope","tool":"list_assets","after":{"enum":["mine","shared","all"],"type":"string","description":"\"mine\" = this account only, \"shared\" = the public library only, \"all\" (default) = both, yours first."},"detail":"Optional field `scope` was added to `list_assets`; may shift model behaviour.","severity":"risky"},{"kind":"input_type_changed","path":"inputSchema.properties.confirmReplace","tool":"publish_scene","after":"boolean|string","before":"boolean","detail":"Type of `confirmReplace` on `publish_scene` changed boolean → boolean|string.","severity":"breaking"},{"kind":"description_changed","tool":"upload_asset","after":"Put a file into the account's asset library and get back its /assets url to use in apply_ops (a glb on a geometry node as data {primitive:\"imported\", assetUrl}, an image in materialMaps or a webImage src, an mp3 in an audioClip: data.audioClip = {src, volume 0-1, loop, mode, muted} on any node; get_scene reports it as `audio`). A `url` may be a picture (JPEG, PNG, WebP, GIF, AVIF, up to 12 MB), a binary glTF 2.0 model (.glb, up to 36 MB) or a sound (MP3, OGG, WAV, M4A, up to 12 MB); the type is read from the bytes. `assetId` instead copies a SHARED library asset (a list_assets row marked shared:true) into this account and returns the url to place; placing the copy keeps the page whole if the sharer removes theirs, and the same asset copied twice returns the first copy. Give ONE source: `url` (a public http(s) link, fetched by the server; use this for anything larger), `dataBase64` with `filename` (the file itself, up to about 3 MB on this hosted connector; bigger files go by `url` or the editor's upload), or (`path` is only available when the MCP runs on your own computer). A model gets its phone-sized copy made automatically, the same as an editor upload. Label it and give keywords so list_assets finds it again.","before":"Put a file into the account's asset library and get back its /assets url to use in apply_ops (a glb on a geometry node as data {primitive:\"imported\", assetUrl}, an image in materialMaps or a webImage src, an mp3 in an audioClip: data.audioClip = {src, volume 0-1, loop, mode, muted} on any node; get_scene reports it as `audio`). Give ONE source: `url` (a public http(s) link, fetched by the server; use this for anything larger), `dataBase64` with `filename` (the file itself, up to about 3 MB on this hosted connector; bigger files go by `url` or the editor's upload), or (`path` is only available when the MCP runs on your own computer). A model gets its phone-sized copy made automatically, the same as an editor upload. Label it and give keywords so list_assets finds it again.","detail":"Description of `upload_asset` changed (29% word delta).","severity":"risky","descriptionDelta":0.2928571428571428},{"kind":"input_property_added","path":"inputSchema.properties.assetId","tool":"upload_asset","after":{"type":"string","description":"Copy this shared library asset (a list_assets id with shared:true) into your library instead."},"detail":"Optional field `assetId` was added to `upload_asset`; may shift model behaviour.","severity":"risky"}],"published_at":"2026-10-08T02:40:14.244Z"}]