MCP Server for SketchUp
Connects an AI client to a live SketchUp model over a two-process bridge (Python MCP server + Ruby SketchUp extension), enabling prompt-driven 3D modeling. Exposes tools for geometry creation (cube/cylinder/cone/sphere) and transformation in mm, material assignment (named colors and hex), boolean operations (union/difference/intersection), edge chamfer and fillet, joinery creation (mortise & tenon, dovetail, finger joints), scene export (skp/obj/dae/stl/png/jpg), model introspection (component listing, searching, layers, selection, version), viewport screenshots, atomic undo, and an eval_ruby escape hatch for arbitrary Ruby execution against the SketchUp Ruby API.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@MCP Server for SketchUp create a 1.2 × 0.8 m oak dining table and export it as stl"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
MCP Server for SketchUp
Connect Claude (or any MCP-aware AI client) to SketchUp for prompt-driven 3D modeling.
Two-process bridge:
Python MCP server (
sketchup-mcp2on PyPI) — exposes typed tools to the LLM via the Model Context Protocol.Ruby SketchUp extension — runs a TCP server inside SketchUp and executes commands against the live model.
Quickstart
1. Install the SketchUp extension
Either grab the latest .rbz from the Releases page or build it from source:
gem install --user-install rubyzip
(cd mcp_for_sketchup && ruby package.rb)
# → mcp_for_sketchup/mcp_for_sketchup_v<version>.rbzIn SketchUp: Window → Extension Manager → Install Extension, pick the .rbz, restart SketchUp. The plugin ships with eval_ruby — arbitrary Ruby execution inside SketchUp — enabled by default; uncheck Enable Ruby evaluation in Plugins → MCP Server → Settings... to close the gate.
2. Start the server inside SketchUp
Plugins → MCP Server → Start Server — by default listens on 127.0.0.1:9876.
3. Configure your MCP client
For Claude Code / Claude Desktop, add to .mcp.json (or claude_desktop_config.json):
{
"mcpServers": {
"sketchup": {
"command": "uvx",
"args": ["sketchup-mcp2"],
"env": {
"SKETCHUP_MCP_HOST": "127.0.0.1",
"SKETCHUP_MCP_PORT": "9876",
"SKETCHUP_MCP_TIMEOUT": "60",
"SKETCHUP_MCP_LOG_LEVEL": "INFO"
}
}
}
}uvx will pull sketchup-mcp2 from PyPI automatically — install uv if you don't have it.
That's it. Ask Claude things like "create a 1.2 × 0.8 m oak dining table" and watch it happen.
Related MCP server: blender-ai-mcp
Features
Tool catalogue
Category | Tools |
Geometry |
|
Materials |
|
Booleans |
|
Edge ops |
|
Joinery |
|
Export |
|
Introspection |
|
View |
|
Lifecycle |
|
Escape hatch |
|
All dimensions in millimeters; angles in degrees. Every entity-returning handler also responds with bbox_mm so the LLM can re-locate entities by bounding box if their IDs go stale after destructive ops.
Capabilities
Multi-client support — N concurrent MCP clients can be connected at once (e.g. Claude Desktop + a smoke-test script + your own Python notebook). Operations are still serialised on the SketchUp UI thread; frames are dispatched in a single global FIFO ordered by decode arrival.
One-time version handshake — every TCP connection begins with a JSON-RPC
hellocarryingclient_version; the server validates against its supported range and replies withserver_version+client_id. Incompatible pairs surface immediately asIncompatibleVersionErrorand the socket is closed.Atomic undo — every mutating handler wraps the edit in
model.start_operation/commit_operation, so a singleEdit → Undorolls back the whole call.MCP prompt
sketchup_modeling_strategy— surfaced in MCP-aware clients' slash menu; teaches the model project conventions (mm units, typed-tools-vs-eval_ruby, pitfalls like reversedGroup#subtract).Settings dialog —
Plugins → MCP Server → Settings...for host / port / log level. Log level applies immediately; host/port changes prompt for a restart.
Configuration
Python side (env vars in .mcp.json)
Variable | Default | Description |
|
| Where to connect to the SketchUp extension |
|
| TCP port |
|
| Per-tool-call timeout (seconds) |
|
|
|
Ruby side (Settings dialog inside SketchUp)
Open Plugins → MCP Server → Settings... to change Host, Port, Log Level, the Ruby evaluation gate, and log-to-file options. Values persist in SketchUp's preferences under section MCPforSketchUp. No environment variables are read on the Ruby side.
The Ruby side logs at WARN by default, so it stays quiet in SketchUp's shared Ruby console; any line it does print is prefixed [MCPforSU] with a UTC timestamp. Enable Log to file to mirror every line to a UTF-8 log file in addition to the console (Plugins → MCP Server → Show Log opens it). The file is written append-only — there is no automatic rotation or size cap, so rotate or clean it up yourself for long-lived sessions.
eval_ruby — the arbitrary-Ruby escape hatch — is enabled by default. Uncheck Enable Ruby evaluation in the Settings dialog to close the gate; the setting persists across SketchUp restarts, and re-enabling it pops a blocking confirmation spelling out the risk (arbitrary Ruby ⇒ full filesystem / network / shell access). With the gate closed, a client's eval_ruby call comes back as a plain message telling the user how to re-open it.
Per-call review. Open gate or not, the exact Ruby a client sends stays visible in your MCP client — Claude Desktop and Claude Code display every tool call's arguments and let you approve or deny each one before it runs, so you can review each snippet case by case. (That per-call prompt is skipped only if you opt out of approvals, e.g. Claude Code's --dangerously-skip-permissions.)
⚠ Security warning: binding the host to
0.0.0.0exposes the MCP server — includingeval_ruby, which runs arbitrary Ruby inside SketchUp — to the entire local network with no authentication. Use only on trusted networks (host → VM, isolated lab). For multi-machine setups consider a loopback SSH tunnel instead.
Examples
Things you can ask Claude:
"Create a simple dining table — 1.2 × 0.8 m, 760 mm tall, oak finish"
"Highlight every component smaller than 100 mm in any dimension"
"Make the selected component red, then move it 100 mm up"
"Export the scene as STL for 3D printing"
"Build a small arts-and-crafts cabinet using
eval_rubywith dovetails"
For richer Ruby recipes that drive the SketchUp API directly — framed walls, gable/hip roofs, joist arrays, follow_me extrusions, world-space transforms, common pitfalls — see docs/sketchup-ruby-cookbook.md.
Working examples and load tests live in examples/:
smoke_check.py— 25-step end-to-end verification of every tool category.smoke_multi_client.py— concurrent multi-client load test.
Architecture
Claude (MCP client)
↕ MCP (stdio)
Python MCP server (FastMCP) src/sketchup_mcp/
↕ TCP — JSON-RPC 2.0, 4-byte big-endian length-prefix framing, 64 MiB cap
Ruby SketchUp extension (server) mcp_for_sketchup/mcp_for_sketchup/
↕ SketchUp Ruby API
Live SketchUp modelThe Ruby side runs entirely on the SketchUp UI thread via UI.start_timer callbacks (SketchUp's Ruby is single-threaded — no native threads allowed). The Python side holds one persistent TCP socket per process and serialises tool-calls with an asyncio.Lock.
Source layout:
Python:
src/sketchup_mcp/{tools,connection,config,compat,errors,prompts}.pyRuby:
mcp_for_sketchup/mcp_for_sketchup/{core,handlers,helpers,ui}/
See CLAUDE.md for the project's working notes and non-obvious constraints (unit conversions, reversed boolean semantics, framing details, etc.).
Development
Python package (editable install)
uv pip install -e .
python -m sketchup_mcp # direct
uvx sketchup-mcp2 # production-style (from PyPI)Tests
ruby test/run_all.rb # Ruby unit tests (minitest; stdlib + rubyzip for the package test)
uv run pytest tests/ -q # Python unit testsLive smoke (requires SketchUp running with the extension started)
uv run python examples/smoke_check.py # 25-step end-to-end
uv run python examples/smoke_multi_client.py # concurrent multi-clientFor a split-host setup (e.g. Linux dev box + Windows SketchUp), prefix with SKETCHUP_MCP_HOST=<sketchup-host>.
Troubleshooting
SketchUp not running or extension not started: ...
The Python MCP server connected to the configured host/port but found nothing listening. Either:
SketchUp isn't running, or
The extension is installed but not started — open
Plugins → MCP Server → Start Server.
The Python server stays alive after this error; the next tool-call retries the connect.
IncompatibleVersionError
Your installed sketchup-mcp2 Python package and the .rbz extension are outside the supported version range. Rebuild the .rbz from the same commit as the Python package, or pip install -U sketchup-mcp2. The current supported range lives in src/sketchup_mcp/compat.py and mcp_for_sketchup/mcp_for_sketchup/core/compat.rb.
Tool-call timeouts on long operations
Bump SKETCHUP_MCP_TIMEOUT in your .mcp.json env block. Default is 60 seconds.
SketchUp UI freezes during big requests
Frame-decoding is capped at 50 reads × 64 KiB per client per tick (~3.2 MB) to keep the UI responsive, but a very large eval_ruby body or a runaway loop inside it will still freeze SketchUp until it returns. Break the work into smaller calls if you can.
MCP client reports connection timed out after 30000ms at startup
If the client can't connect but SketchUp itself is reachable (e.g. telnet <host> 9876 succeeds), the bottleneck is the Python server's own startup, not the link to SketchUp.
The usual culprit is running the server from a source checkout whose virtual environment lives on a slow filesystem — VMware Shared Folders (vmhgfs-fuse), VirtualBox shared folders, NFS/CIFS network drives, or WSL's /mnt/.... Python touches hundreds of small files at startup, and importing the FastMCP dependency stack from such a filesystem can take 30 s+ — past the client's init timeout. (A quick check: time uv run python -c "import sketchup_mcp.app" — if that takes tens of seconds, the filesystem is the problem.)
Keep the virtualenv on a local disk. With uv, point it there via UV_PROJECT_ENVIRONMENT in the server's .mcp.json env block — the project source can stay on the shared folder (it's small, and editable installs pick up changes live); only the dependency-heavy venv needs to be local:
"sketchup": {
"command": "uv",
"args": ["run", "--directory", "/path/to/sketchup-mcp2", "python", "-m", "sketchup_mcp"],
"env": {
"UV_PROJECT_ENVIRONMENT": "/home/you/.venvs/sketchup-mcp2",
"SKETCHUP_MCP_HOST": "127.0.0.1"
}
}The uvx sketchup-mcp2 setup shown earlier isn't affected — uvx already keeps its environment under uv's local cache.
Why is the venv on a shared folder at all? This bridge is typically run in an isolated VM setup — both Claude Code launched with
--dangerously-skip-permissionsandeval_ruby(arbitrary Ruby, full filesystem/shell access) enabled are risky enough to want a disposable VM. Typical layout: a Linux VM (Claude Code + MCP server) ↔ a Windows VM (SketchUp) over the LAN, project on a host-shared folder — which is exactly why theUV_PROJECT_ENVIRONMENTnote above matters.
License
MIT — see LICENSE.
Credits and attribution
Originally forked from mhyrr/sketchup-mcp. The fork diverged at v0.0.1 with a new wire protocol (4-byte length-prefix framing, JSON-RPC 2.0 envelopes), modular handler architecture, expanded introspection / joinery / edge-op tools, multi-client server with one-time
hellohandshake, MCP prompt, viewport screenshot, settings dialog, and full unit-test coverage on both Ruby and Python sides.Published to PyPI as
sketchup-mcp2; the upstream package issketchup-mcp.Bridge-pattern inspiration from ahujasid/blender-mcp.
Contributing
Pull requests welcome. Before opening one, please run both test suites (ruby test/run_all.rb and uv run pytest tests/) and — if you've touched anything in the IO path — the live smokes against a running SketchUp.
Available Tools
22 toolsboolean_operationA
Perform a boolean operation (union / difference / intersection) on two solids.
difference = target minus tool. Operating on an instance of a shared definition consumes only that instance — the result is a new group, sibling instances are untouched. Unreliable on non-manifold geometry.
Returns: JSON {id, name, type, bbox_mm{min,max}|null}. Read bbox_mm to verify the result; it is null for empty geometry (e.g. a difference that consumed the whole body).
Inputs: tool_id (required), target_id (required), operation, delete_originals. Returns: result
chamfer_edgeA
Chamfer (bevel) edges of a group/component by distance mm.
By default ALL edges are chamfered. Unreliable on non-manifold geometry.
Returns: JSON {id, name, type, bbox_mm|null, edges_chamfered, stats{attempted, skipped_no_match, subtract_failed, succeeded}} — check stats.subtract_failed == 0 (failed cuts) and stats.skipped_no_match == 0 (edges consumed by earlier cuts).
Inputs: id (required), distance. Returns: result
create_componentA
Create a primitive (cube / cylinder / cone / sphere) in SketchUp.
All linear values are millimeters (mm). Minimum size per dimension: 0.1 mm for cube (thin stock like veneer is fine), 1.0 mm for sphere / cylinder / cone (tessellated types degenerate earlier). position is the bounding-box MIN corner (not the center); the same anchor is used by transform_component.position. Per-type dimensions: cube uses [x, y, z]; cylinder and cone use [0]=diameter, [2]=height ([1] is ignored); sphere uses [0]=diameter only. New geometry is wrapped in a SketchUp Group.
Returns: JSON {id, name, type, bbox_mm{min,max}|null}. Read bbox_mm to verify the result before the next step.
Inputs: name, type, position, dimensions. Returns: result
create_dovetailA
Create a dovetail joint between two boards.
All dimensions in millimeters; angle is in degrees, valid range (0, 60]. Offsets shift the joint from the board face's center. Defaults are sized for ~100 mm boards. The two boards must already touch/overlap along the joint axis.
Returns: JSON {tail: {id, name, type, bbox_mm|null}, pin: {...}, boolean_cuts: {attempted, failed}} — non-zero failed means some cuts did not apply (likely non-manifold geometry); verify via bbox_mm.
Inputs: pin_id (required), tail_id (required), angle, depth, +6 more. Returns: result
create_finger_jointA
Create a finger joint (box joint) between two boards.
All dimensions in millimeters; offsets shift the joint from the board face's center. Defaults are sized for ~100 mm boards. The two boards must already touch/overlap along the joint axis.
Returns: JSON {board1: {id, name, type, bbox_mm|null}, board2: {...}, boolean_cuts: {attempted, failed}} — non-zero failed means some cuts did not apply (likely non-manifold geometry); verify via bbox_mm.
Inputs: board1_id (required), board2_id (required), depth, width, +5 more. Returns: result
create_layerA
Create a new layer (tag) with the given name.
Returns: JSON {id, name, visible}.
Inputs: name (required). Returns: result
create_mortise_tenonA
Create a mortise-and-tenon joint between two boards.
All dimensions in millimeters; offsets shift the joint from the board face's center. Defaults are sized for ~100 mm boards. The two boards must already touch/overlap along the joint axis.
Returns: JSON {mortise: {id, name, type, bbox_mm|null}, tenon: {...}, boolean_cuts: {attempted, failed}} — non-zero failed means some cuts did not apply (likely non-manifold geometry); verify via bbox_mm.
Inputs: tenon_id (required), mortise_id (required), depth, width, +4 more. Returns: result
delete_componentB
Delete a group or component by entity ID.
Returns: JSON {ok: true}.
Inputs: id (required). Returns: result
eval_rubyA
Evaluate arbitrary Ruby code in SketchUp.
Enabled by default; the user can close the gate in the SketchUp
extension's Settings. When closed, the SketchUp side returns JSON-RPC
code -32010 with a user-facing message explaining how to re-enable it.
This wrapper surfaces that message as a plain string so the LLM can
repeat it to the user verbatim — without the [code] prefix that
format_error would otherwise add.
Returns the .to_s of the LAST evaluated expression; stdout (puts) is NOT
captured. End scripts with an explicit expression — e.g. a final
result.to_json — to get structured data back. Errors return
"[code] message" with the Ruby exception class and message.
Inputs: code (required). Returns: result
export_sceneA
Export the current scene to a temp file on the SketchUp host.
Formats: skp (native), obj / dae / stl (geometry), png / jpg (viewport render, default 1920×1080). The file is written on the machine running SketchUp — on a split-host setup the path is not directly readable here.
Returns: JSON {path, format} plus a "warning" field when exporting skp from a never-saved model (SketchUp binds the live document to the export path — relay the warning to the user).
Inputs: format. Returns: result
fillet_edgeA
Round (fillet) edges of a group/component by radius mm with
segments arc segments.
By default ALL edges are filleted. Unreliable on non-manifold geometry.
Returns: JSON {id, name, type, bbox_mm|null, edges_filleted, stats{attempted, skipped_no_match, subtract_failed, succeeded}} — check stats.subtract_failed == 0 (failed cuts) and stats.skipped_no_match == 0 (edges consumed by earlier cuts).
Inputs: id (required), radius, segments. Returns: result
find_componentsA
Find components matching name substring, layer, and/or type.
Name matching is case-insensitive substring; layer must match exactly. Searches recursively (bounded by max_depth). With no filters it returns all components up to max_depth (paginated) — same traversal as list_components.
Returns: JSON {components[], total, offset, truncated} — if truncated, request the next page with offset += limit.
Inputs: name, type, layer, limit, +3 more. Returns: result
get_component_infoA
Detailed info for a single group or component instance by entity ID.
Returns: JSON {id, name, type, layer, depth, bbox_mm|null}.
Inputs: id (required). Returns: result
get_model_infoA
Get current SketchUp model info: file path, title, units, bounding box, entity count, layer list.
Returns: JSON {path, title, units: "mm", bounding_box_mm|null, entity_count, layers[]}.
Inputs: no named parameters. Returns: result
get_selectionA
Get the entities currently selected in the SketchUp UI.
Returns: JSON {entities: [...]} — groups/components are {id, name, type, layer, depth, bbox_mm|null}; other selected entities (edges, faces, ...) are {id, type} only.
Inputs: no named parameters. Returns: result
get_versionA
Return the server version and Python↔Ruby compatibility verdict.
Useful as a runtime sanity probe — always returns a payload, even when the connection or other tools surface errors. The result is a JSON string with fields: python_version, ruby_version, min_compatible_ruby, max_compatible_ruby, ruby_min_compatible_python, ruby_max_compatible_python, compatible (bool), error (string | null).
Inputs: no named parameters. Returns: result
get_viewport_screenshotA
Capture the current SketchUp viewport; returns the PNG image plus a JSON text block {width, height, preset_used, style_used}.
Useful for letting Claude visually verify the scene between steps.
Parameters
max_size: largest side of the returned PNG (64..4096). Aspect ratio is taken from the current viewport; the smaller side is scaled proportionally.
view_preset: switch the camera to a standard view before snapping.
currentleaves the camera alone.zoom_extents: call view.zoom_extents before snapping.
style: temporarily flip a small set of rendering_options keys.
defaultleaves them alone.restore_view: when true (default), camera and rendering_options are snapshotted before mutation and restored after the snapshot, so the user's viewport is unchanged.
If the connection drops mid-response the call is retried automatically; the viewport may briefly flicker in that rare case.
Inputs: style, max_size, view_preset, restore_view, +1 more.
list_componentsA
List groups and component instances in the model (paginated).
Each component is {id, name, type, layer, depth, bbox_mm} (detailed) or {id, name, type, layer, depth} (concise); bounds are world-coordinate mm. Set recursive=true to descend into nested components (bounded by max_depth, default 3).
Returns: JSON {components[], total, offset, truncated} — if truncated, request the next page with offset += limit.
Inputs: limit, offset, max_depth, recursive, +1 more. Returns: result
list_layersA
List all model layers (tags).
Returns: JSON {layers: [{name, visible, color, id}]}.
Inputs: no named parameters. Returns: result
set_materialA
Assign a material (color) to a group or component.
material accepts a named color — red, green, blue, yellow, cyan, turquoise, magenta, purple, white, black, brown, wood, orange, gray, grey — or a 6-digit hex string like "#a05030" (#rrggbb). Anything else fails with error -32602. Named colors are case-insensitive. Painting affects only this instance (it is made unique first). That applies to groups/components; painting a raw face/edge id (obtainable via get_selection) colors the shared definition — all instances show it.
Returns: JSON {id, name, type, bbox_mm{min,max}|null}.
Inputs: id (required), material (required). Returns: result
transform_componentA
Move, rotate and/or scale a group or component (mm / degrees).
position: ABSOLUTE target for the entity's bounding-box MIN corner, in mm — the same anchor create_component uses. Applied LAST (after rotation/scale), so the final bbox-min lands exactly at [x, y, z] even in combined calls. It is NOT a relative offset: repeating the same position is idempotent.
rotation: RELATIVE rotation in degrees around the bbox center, applied sequentially about world X, then Y, then Z.
scale: RELATIVE scale factors about the bbox center.
These validations (3-element lists, non-zero scale) apply only to this typed tool — raw Ruby driven through eval_ruby bypasses them.
Returns: JSON {id, name, type, bbox_mm{min,max}|null}. Read bbox_mm to verify the result; it is null for empty geometry.
Inputs: id (required), scale, position, rotation. Returns: result
undoA
Undo the last atomic operation in SketchUp. One MCP tool-call = one undo step.
Returns: JSON {ok: true}.
Inputs: no named parameters. Returns: result
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 22 tools
Most tools target clearly distinct operations (joints, booleans, edge chamfer/fillet, transforms), and descriptions clarify boundaries well. The main soft spots are the component trio (list_components/find_components/get_component_info) where find with no filters duplicates list, and the somewhat misleading create_component which makes primitives rather than SketchUp components.
Nearly all tools use a consistent snake_case verb_noun pattern (create_component, list_layers, get_model_info, chamfer_edge, eval_ruby). Only minor deviations like boolean_operation (noun-only, no verb) break the pattern.
22 tools is on the heavy side but appropriate for a rich 3D modeling domain spanning primitives, joints, booleans, edge operations, layers, export, and introspection. Each tool covers a distinct capability with little redundancy, though it sits near the upper bound of comfortable scoping.
Strong lifecycle coverage: create/delete/transform, materials, booleans, joinery, layers, export, screenshots, and model introspection. Gaps exist (no rename/re-layer component, no duplicate/mirror/array, no delete_layer or save), but eval_ruby provides a general escape hatch for uncovered operations.
Maintenance
Related MCP Connectors
Connect AI clients to your Dactic work context through a hosted MCP service.
MCP server that lets AI assistants use all OneSchema features exposed via the public API.
Talk to your public-facing AI from any MCP client — Claude, ChatGPT, Cursor, Cline, Windsurf.
Use AI models for chat, image, and video generation from Claude Code and other MCP hosts.
Related MCP Servers
- AlicenseBqualityDmaintenanceConnects Sketchup to Claude AI through the Model Context Protocol, allowing Claude to directly interact with and control Sketchup for prompt-assisted 3D modeling and scene manipulation.10545MIT
- AlicenseNot gradedqualityDmaintenanceConnects MCP-compatible clients to a live Blender scene for AI-assisted 3D workflows, enabling inspection and controlled operations on objects, materials, cameras, lights, render settings, animation, UVs, Geometry Nodes, imports, exports, and Python execution.1MIT
- AlicenseAqualityDmaintenanceControls SketchUp via AI assistants, enabling 3D model creation programmatically through the Model Context Protocol.2115MIT
- AlicenseBqualityDmaintenanceConnects SketchUp to Claude AI through MCP, enabling natural language controlled 3D modeling, component manipulation, and scene creation.10MIT