makeit4me 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "makeit4me",
3
+ "version": "1.0.0",
4
+ "description": "MCP server and skills that give AI agents full control over SketchUp Pro",
5
+ "author": {
6
+ "name": "Lookahead Labs LLC",
7
+ "email": "support@makeit4me.net"
8
+ },
9
+ "homepage": "https://www.makeit4me.net",
10
+ "license": "proprietary",
11
+ "keywords": ["sketchup", "3d-modeling", "mcp", "cad"],
12
+ "skills": "./skills/",
13
+ "mcpServers": "./.mcp.json"
14
+ }
package/.mcp.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "makeit4me": {
4
+ "command": "node",
5
+ "args": ["${CLAUDE_PLUGIN_ROOT}/index.js"]
6
+ }
7
+ }
8
+ }
package/index.js ADDED
@@ -0,0 +1,223 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { z } from "zod";
6
+ import http from "node:http";
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import os from "node:os";
10
+
11
+ const PORT = 9876;
12
+ const BASE = `http://127.0.0.1:${PORT}`;
13
+
14
+ // ── HTTP helper ──────────────────────────────────────────────────────────────
15
+
16
+ function request(method, endpoint, body) {
17
+ return new Promise((resolve, reject) => {
18
+ const url = new URL(endpoint, BASE);
19
+ const payload = body ? JSON.stringify(body) : undefined;
20
+
21
+ const req = http.request(
22
+ {
23
+ hostname: url.hostname,
24
+ port: url.port,
25
+ path: url.pathname,
26
+ method,
27
+ headers: {
28
+ ...(payload
29
+ ? {
30
+ "Content-Type": "application/json",
31
+ "Content-Length": Buffer.byteLength(payload),
32
+ }
33
+ : {}),
34
+ },
35
+ },
36
+ (res) => {
37
+ const chunks = [];
38
+ res.on("data", (c) => chunks.push(c));
39
+ res.on("end", () => {
40
+ const text = Buffer.concat(chunks).toString();
41
+ try {
42
+ resolve(JSON.parse(text));
43
+ } catch {
44
+ resolve({ ok: false, error: `Non-JSON response: ${text.slice(0, 200)}` });
45
+ }
46
+ });
47
+ }
48
+ );
49
+
50
+ req.on("error", (err) => {
51
+ resolve({
52
+ ok: false,
53
+ error: `Connection failed: ${err.message}. Is SketchUp running with MakeIt4Me?`,
54
+ });
55
+ });
56
+
57
+ if (payload) req.write(payload);
58
+ req.end();
59
+ });
60
+ }
61
+
62
+ // ── MCP Server ───────────────────────────────────────────────────────────────
63
+
64
+ const server = new McpServer({
65
+ name: "makeit4me",
66
+ version: "1.0.0",
67
+ });
68
+
69
+ // ── Tools ────────────────────────────────────────────────────────────────────
70
+
71
+ server.registerTool("sketchup_status", {
72
+ title: "SketchUp Status",
73
+ description:
74
+ "Check if SketchUp is running and the MakeIt4Me plugin is active. " +
75
+ "Returns version info and active model details. Always call this first.",
76
+ }, async () => {
77
+ const result = await request("GET", "/status");
78
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
79
+ });
80
+
81
+ server.registerTool("sketchup_models", {
82
+ title: "List Open Models",
83
+ description: "List all open SketchUp models with their titles and file paths.",
84
+ }, async () => {
85
+ const result = await request("GET", "/models");
86
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
87
+ });
88
+
89
+ server.registerTool("sketchup_inspect", {
90
+ title: "Inspect Model (Read-Only)",
91
+ description:
92
+ "Evaluate Ruby code in SketchUp for read-only queries. " +
93
+ "The variable `model` is pre-bound to Sketchup.active_model. " +
94
+ "Use this for reading model state — entity counts, bounds, materials, etc. " +
95
+ "For geometry changes, use sketchup_operation instead.",
96
+ inputSchema: {
97
+ code: z
98
+ .string()
99
+ .describe("Ruby code to evaluate in SketchUp. `model` is pre-bound to Sketchup.active_model."),
100
+ },
101
+ }, async ({ code }) => {
102
+ const result = await request("POST", "/inspect", { code });
103
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
104
+ });
105
+
106
+ server.registerTool("sketchup_operation", {
107
+ title: "Execute Operation (Undoable)",
108
+ description:
109
+ "Execute Ruby code wrapped in a named undo operation. " +
110
+ "Use for ALL geometry changes (add, move, delete) so the user can Ctrl+Z. " +
111
+ "The variable `model` is pre-bound to Sketchup.active_model.",
112
+ inputSchema: {
113
+ name: z
114
+ .string()
115
+ .describe("Undo label shown in Edit > Undo (e.g. 'Add front wall')"),
116
+ code: z
117
+ .string()
118
+ .describe("Ruby code to execute. `model` is pre-bound to Sketchup.active_model."),
119
+ },
120
+ }, async ({ name, code }) => {
121
+ const result = await request("POST", "/operation", { name, code });
122
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
123
+ });
124
+
125
+ server.registerTool("sketchup_selection", {
126
+ title: "Manage Selection",
127
+ description:
128
+ "Read or manipulate the active selection in SketchUp. " +
129
+ "Actions: 'get' (list selected), 'clear', 'add' (by entity GUIDs), 'remove' (by entity GUIDs).",
130
+ inputSchema: {
131
+ action: z.enum(["get", "clear", "add", "remove"]).describe("Selection action"),
132
+ entities: z
133
+ .array(z.string())
134
+ .optional()
135
+ .describe("Entity GUIDs for add/remove actions"),
136
+ },
137
+ }, async ({ action, entities }) => {
138
+ const body = { action };
139
+ if (entities) body.entities = entities;
140
+ const result = await request("POST", "/selection", body);
141
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
142
+ });
143
+
144
+ server.registerTool("sketchup_screenshot", {
145
+ title: "Take Viewport Screenshot",
146
+ description:
147
+ "Capture a screenshot of the current SketchUp viewport. " +
148
+ "Optionally set the camera position first. " +
149
+ "Returns the image so you can visually verify geometry.",
150
+ inputSchema: {
151
+ eye: z
152
+ .array(z.number())
153
+ .length(3)
154
+ .optional()
155
+ .describe("Camera eye position [x, y, z] in inches. If omitted, uses current camera."),
156
+ target: z
157
+ .array(z.number())
158
+ .length(3)
159
+ .optional()
160
+ .describe("Camera target/look-at point [x, y, z] in inches."),
161
+ orthographic: z
162
+ .boolean()
163
+ .optional()
164
+ .describe("Use parallel projection (orthographic) instead of perspective. Good for elevation views."),
165
+ width: z.number().optional().describe("Image width in pixels (default 1920)"),
166
+ height: z.number().optional().describe("Image height in pixels (default 1080)"),
167
+ },
168
+ }, async ({ eye, target, orthographic, width, height }) => {
169
+ const w = width || 1920;
170
+ const h = height || 1080;
171
+ const imgPath = path.join(os.tmpdir(), "sketchup_mcp_screenshot.png");
172
+
173
+ let code = "view = model.active_view\n";
174
+
175
+ if (eye && target) {
176
+ code += `eye = Geom::Point3d.new(${eye.join(", ")})\n`;
177
+ code += `target = Geom::Point3d.new(${target.join(", ")})\n`;
178
+ code += `up = Geom::Vector3d.new(0, 0, 1)\n`;
179
+ code += `cam = Sketchup::Camera.new(eye, target, up)\n`;
180
+ if (orthographic) {
181
+ code += `cam.perspective = false\n`;
182
+ }
183
+ code += `view.camera = cam\n`;
184
+ }
185
+
186
+ code += `view.write_image("${imgPath}", ${w}, ${h}, true)\n`;
187
+ code += `"${imgPath}"`;
188
+
189
+ const result = await request("POST", "/inspect", { code });
190
+
191
+ if (!result.ok) {
192
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
193
+ }
194
+
195
+ // Read the image and return it
196
+ try {
197
+ const imgBuffer = fs.readFileSync(imgPath);
198
+ const base64 = imgBuffer.toString("base64");
199
+ return {
200
+ content: [
201
+ {
202
+ type: "image",
203
+ data: base64,
204
+ mimeType: "image/png",
205
+ },
206
+ ],
207
+ };
208
+ } catch (err) {
209
+ return {
210
+ content: [
211
+ {
212
+ type: "text",
213
+ text: `Screenshot saved to ${imgPath} but failed to read: ${err.message}`,
214
+ },
215
+ ],
216
+ };
217
+ }
218
+ });
219
+
220
+ // ── Start ────────────────────────────────────────────────────────────────────
221
+
222
+ const transport = new StdioServerTransport();
223
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "makeit4me",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "makeit4me": "./index.js"
8
+ },
9
+ "files": [
10
+ "index.js",
11
+ ".claude-plugin/",
12
+ ".mcp.json",
13
+ "skills/"
14
+ ],
15
+ "scripts": {
16
+ "start": "node index.js"
17
+ },
18
+ "author": "Lookahead Labs LLC",
19
+ "license": "SEE LICENSE IN LICENSE",
20
+ "description": "MCP server that bridges AI agents to SketchUp via the MakeIt4Me plugin",
21
+ "keywords": ["mcp", "sketchup", "3d-modeling", "cad", "architecture"],
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://www.makeit4me.net"
25
+ },
26
+ "homepage": "https://www.makeit4me.net",
27
+ "dependencies": {
28
+ "@modelcontextprotocol/sdk": "^1.27.1"
29
+ }
30
+ }
@@ -0,0 +1,221 @@
1
+ ---
2
+ name: sketchup-3d-warehouse-wrangling
3
+ description: |
4
+ Place, orient, trim, and manage 3D Warehouse components in SketchUp via the
5
+ Ruby API. Use when: (1) a component from the warehouse is too large or wrong
6
+ orientation for the model, (2) roof shingles/tiles need to be cropped to fit
7
+ a roof, (3) doors or windows need to be placed in wall openings, (4) components
8
+ need rotation for correct function (e.g., water flow direction on shingles),
9
+ (5) need to inspect nested component hierarchies. Covers orientation, trimming
10
+ by bounds, nested hierarchy navigation, and placement in openings.
11
+ author: Claude Code
12
+ version: 1.0.0
13
+ date: 2026-03-11
14
+ ---
15
+
16
+ # SketchUp 3D Warehouse Component Wrangling
17
+
18
+ ## Problem
19
+ Components from the 3D Warehouse rarely fit your model as-is. They're often the
20
+ wrong size, orientation, or position. Solid Tools often don't work because warehouse
21
+ components aren't manifold solids. This skill covers practical techniques for making
22
+ them fit.
23
+
24
+ ## Inspecting a Component
25
+
26
+ Always start by understanding what you're working with:
27
+
28
+ ```ruby
29
+ defn = model.definitions["Component Name"]
30
+ bb = defn.bounds
31
+ puts "W: #{bb.width.to_l}, D: #{bb.depth.to_l}, H: #{bb.height.to_l}"
32
+ puts "Min: [#{bb.min.x.to_l}, #{bb.min.y.to_l}, #{bb.min.z.to_l}]"
33
+ puts "Max: [#{bb.max.x.to_l}, #{bb.max.y.to_l}, #{bb.max.z.to_l}]"
34
+
35
+ # Check internal structure
36
+ counts = Hash.new(0)
37
+ defn.entities.each { |e| counts[e.typename] += 1 }
38
+ puts counts # Often reveals nested ComponentInstances or Groups
39
+ ```
40
+
41
+ ## Navigating Nested Hierarchies
42
+
43
+ Warehouse components are often deeply nested. A "roof shingles" panel might be:
44
+ ```
45
+ ComponentInstance (panel)
46
+ └─ 22x ComponentInstance (rows)
47
+ └─ 26x Group (individual tiles)
48
+ ```
49
+
50
+ To operate on leaf geometry, you must traverse:
51
+ ```ruby
52
+ instance.definition.entities.each do |row|
53
+ next unless row.is_a?(Sketchup::ComponentInstance)
54
+ row.make_unique # IMPORTANT: avoid modifying shared definition
55
+ row.definition.entities.each do |tile|
56
+ next unless tile.is_a?(Sketchup::Group)
57
+ # Now you can check bounds and delete
58
+ end
59
+ end
60
+ ```
61
+
62
+ ## Trimming Oversized Components (Non-Solid)
63
+
64
+ When Solid Tools won't work (component isn't manifold), trim by deleting
65
+ sub-components outside your target bounds:
66
+
67
+ ### Step 1: Remove entire rows/sections outside bounds
68
+ ```ruby
69
+ # Remove rows outside Y bounds (e.g., for a roof 0..144")
70
+ instance.make_unique
71
+ rows = instance.definition.entities.to_a.select { |e| e.is_a?(Sketchup::ComponentInstance) }
72
+ to_del = rows.select { |r| r.bounds.min.y > 148 || r.bounds.max.y < -4 }
73
+ instance.definition.entities.erase_entities(to_del)
74
+ ```
75
+
76
+ ### Step 2: Remove individual pieces within rows outside other bounds
77
+ ```ruby
78
+ rows.each do |row|
79
+ row.make_unique
80
+ tiles = row.definition.entities.to_a.select { |e| e.is_a?(Sketchup::Group) }
81
+ to_del = tiles.select { |t|
82
+ tb = t.bounds
83
+ min_pt = tb.min.transform(row.transformation)
84
+ min_pt.x > max_x || tb.max.transform(row.transformation).x < min_x
85
+ }
86
+ row.definition.entities.erase_entities(to_del) if to_del.any?
87
+ end
88
+ ```
89
+
90
+ ### Important: `make_unique` before modifying
91
+ Without `make_unique`, modifying a definition affects ALL instances sharing it.
92
+ Always call `make_unique` before deleting sub-geometry.
93
+
94
+ ## Orienting Roof Shingles for Water Flow
95
+
96
+ Shingles must overlap so water runs downhill. The overlap direction depends on
97
+ which way the component's internal rows stack.
98
+
99
+ ### Determine current orientation
100
+ ```ruby
101
+ ci = model.entities.find { |e| e.is_a?(Sketchup::ComponentInstance) }
102
+ t = ci.transformation
103
+ puts "X axis (row direction): #{t.xaxis.to_a}"
104
+ puts "Y axis (stacking direction): #{t.yaxis.to_a}"
105
+ puts "Z axis (normal): #{t.zaxis.to_a}"
106
+ ```
107
+
108
+ ### Fix water flow direction
109
+ If rows stack the wrong way (uphill instead of downhill), flip 180° around the
110
+ component's local X axis, centered on the roof:
111
+
112
+ ```ruby
113
+ roof_center = Geom::Point3d.new(42, 66, 96)
114
+ flip = Geom::Transformation.rotation(roof_center, ci.transformation.xaxis, Math::PI)
115
+ ci.transformation = flip * ci.transformation
116
+ ```
117
+
118
+ ### Rotate rows to run across width (not along slope)
119
+ If shingle rows run along the slope instead of across it, rotate 90° around
120
+ the roof surface normal:
121
+
122
+ ```ruby
123
+ normal = ci.transformation.zaxis
124
+ center = ci.bounds.center
125
+ rot = Geom::Transformation.rotation(center, normal, 90.degrees)
126
+ ci.transformation = rot * ci.transformation
127
+ ```
128
+
129
+ **After any rotation, recenter on the roof:**
130
+ ```ruby
131
+ roof_group = model.entities.find { |e| e.is_a?(Sketchup::Group) && e.bounds.min.z > 80 }
132
+ rb = roof_group.bounds
133
+ sb = ci.bounds
134
+ move = Geom::Transformation.translation([
135
+ rb.center.x - sb.center.x,
136
+ rb.center.y - sb.center.y,
137
+ rb.center.z - sb.center.z
138
+ ])
139
+ ci.transformation = move * ci.transformation
140
+ ```
141
+
142
+ ## Placing Doors in Wall Openings
143
+
144
+ ### Find the opening dimensions
145
+ ```ruby
146
+ # Door frame group or wall opening
147
+ frame = model.entities.find { |e|
148
+ e.is_a?(Sketchup::Group) && e.bounds.min.z == 0 && e.bounds.height > 70
149
+ }
150
+ fb = frame.bounds
151
+ center_x = (fb.min.x + fb.max.x) / 2.0
152
+ ```
153
+
154
+ ### Position the door component
155
+ ```ruby
156
+ door = model.entities.find { |e|
157
+ e.is_a?(Sketchup::ComponentInstance) && e.definition.name == "DoorName"
158
+ }
159
+ defn_bb = door.definition.bounds
160
+ door_w = defn_bb.width
161
+ door_d = defn_bb.depth
162
+
163
+ # Center on opening, straddle the wall plane
164
+ new_x = center_x - (door_w / 2.0)
165
+ new_y = wall_y - (door_d / 2.0) # center depth on wall plane
166
+ new_z = 0.0 # or slab_z
167
+
168
+ door.transformation = Geom::Transformation.new([new_x, new_y, new_z])
169
+ ```
170
+
171
+ ### For doors on non-front walls, rotate first
172
+ ```ruby
173
+ # Door on right wall (X=84): rotate -90° around Z
174
+ t_rot = Geom::Transformation.rotation(ORIGIN, Z_AXIS, -90.degrees)
175
+ t_move = Geom::Transformation.new([wall_x, door_y, 0])
176
+ door.transformation = t_move * t_rot
177
+ ```
178
+
179
+ ### Double doors (mirrored pair)
180
+ ```ruby
181
+ # Left door: hinge at left edge, opens right
182
+ t_rot1 = Geom::Transformation.rotation(ORIGIN, Z_AXIS, -90.degrees)
183
+ t_move1 = Geom::Transformation.new([wall_x, opening_y_min, 0])
184
+ door1.transformation = t_move1 * t_rot1
185
+
186
+ # Right door: hinge at right edge, opens left (flip Y)
187
+ t_rot2 = Geom::Transformation.rotation(ORIGIN, Z_AXIS, 90.degrees)
188
+ t_move2 = Geom::Transformation.new([wall_x, opening_y_max, 0])
189
+ door2.transformation = t_move2 * t_rot2
190
+
191
+ # Shift inward by door width if they open outward
192
+ move_in = Geom::Transformation.translation([0, door_width, 0])
193
+ door1.transformation = move_in * door1.transformation
194
+ ```
195
+
196
+ ## Checking for Solid Tools Compatibility
197
+
198
+ ```ruby
199
+ group.manifold? # true = can use Solid Tools (intersect, trim, union, etc.)
200
+ ```
201
+
202
+ Most warehouse components return `false`. Use the bounds-based trimming
203
+ approach above instead.
204
+
205
+ ## Verification
206
+
207
+ After any component manipulation:
208
+ 1. Screenshot from multiple angles
209
+ 2. Check for z-fighting (flickering surfaces)
210
+ 3. Verify bounds match expected area
211
+ 4. Check entity count hasn't exploded (common after bad explodes)
212
+
213
+ ## Notes
214
+ - Scaling changes individual element sizes (bad for shingles/tiles) — trim instead
215
+ - `explode` inside a group keeps geometry contained; explode at top level creates a mess
216
+ - Components from the warehouse may use non-standard axes — always check `bounds` not just `origin`
217
+ - Some components have hidden geometry — check `entities.length` vs what you see
218
+
219
+ ## See Also
220
+ - `sketchup-bridge`: MCP tools and Ruby API reference
221
+ - `sketchup-building-framing`: Generating realistic lumber framing