sceneview-mcp 3.4.12 → 3.4.13

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,325 @@
1
+ /**
2
+ * generate-scene.ts
3
+ *
4
+ * Generates a complete Scene{} or ARScene{} composable from a text description.
5
+ * Maps common objects/concepts to SceneView node types and builds compilable code.
6
+ */
7
+ const OBJECT_MAPPINGS = [
8
+ // Furniture
9
+ { keywords: ["table"], nodeType: "CubeNode", geometryType: "cube", defaultScale: 1.0, defaultPosition: [0, 0.4, 0], comment: "Table (flat cube)" },
10
+ { keywords: ["chair"], nodeType: "ModelNode", defaultScale: 0.8, defaultPosition: [0, 0, -1], comment: "Chair (use GLB model)" },
11
+ { keywords: ["desk"], nodeType: "CubeNode", geometryType: "cube", defaultScale: 1.2, defaultPosition: [0, 0.35, 0], comment: "Desk" },
12
+ { keywords: ["shelf", "bookshelf"], nodeType: "CubeNode", geometryType: "cube", defaultScale: 1.5, defaultPosition: [2, 0.75, 0], comment: "Shelf" },
13
+ { keywords: ["bed"], nodeType: "CubeNode", geometryType: "cube", defaultScale: 2.0, defaultPosition: [0, 0.25, 0], comment: "Bed" },
14
+ { keywords: ["sofa", "couch"], nodeType: "CubeNode", geometryType: "cube", defaultScale: 1.8, defaultPosition: [0, 0.35, 0], comment: "Sofa" },
15
+ // Basic shapes
16
+ { keywords: ["box", "cube", "crate"], nodeType: "CubeNode", geometryType: "cube", defaultScale: 0.5, defaultPosition: [0, 0.25, 0], comment: "Box" },
17
+ { keywords: ["sphere", "ball", "globe", "orb"], nodeType: "SphereNode", geometryType: "sphere", defaultScale: 0.5, defaultPosition: [0, 0.5, 0], comment: "Sphere" },
18
+ { keywords: ["cylinder", "pillar", "column"], nodeType: "CylinderNode", geometryType: "cylinder", defaultScale: 0.5, defaultPosition: [0, 0.5, 0], comment: "Cylinder" },
19
+ { keywords: ["plane", "floor", "ground"], nodeType: "PlaneNode", geometryType: "plane", defaultScale: 5.0, defaultPosition: [0, 0, 0], comment: "Ground plane" },
20
+ { keywords: ["wall"], nodeType: "CubeNode", geometryType: "cube", defaultScale: 3.0, defaultPosition: [0, 1.5, -3], comment: "Wall" },
21
+ // Environment
22
+ { keywords: ["tree", "plant"], nodeType: "ModelNode", defaultScale: 2.0, defaultPosition: [2, 0, 2], comment: "Tree (use GLB model)" },
23
+ { keywords: ["car", "vehicle"], nodeType: "ModelNode", defaultScale: 2.0, defaultPosition: [0, 0, 0], comment: "Car (use GLB model)" },
24
+ { keywords: ["house", "building"], nodeType: "ModelNode", defaultScale: 5.0, defaultPosition: [0, 0, -5], comment: "Building (use GLB model)" },
25
+ // People / characters
26
+ { keywords: ["person", "character", "human", "avatar", "robot"], nodeType: "ModelNode", defaultScale: 1.7, defaultPosition: [0, 0, 0], comment: "Character (use GLB model)" },
27
+ ];
28
+ const LIGHT_KEYWORDS = ["sun", "sunlight", "daylight", "bright", "lit", "sunny", "well-lit"];
29
+ const INDOOR_KEYWORDS = ["room", "indoor", "inside", "interior"];
30
+ const OUTDOOR_KEYWORDS = ["outdoor", "outside", "garden", "park", "street"];
31
+ const AR_KEYWORDS = ["ar", "augmented", "real world", "camera", "place in room", "place on"];
32
+ const DARK_KEYWORDS = ["night", "dark", "dim", "mood", "candlelight"];
33
+ function parseDescription(description) {
34
+ const lower = description.toLowerCase();
35
+ const objects = [];
36
+ // Parse quantity + object patterns
37
+ for (const mapping of OBJECT_MAPPINGS) {
38
+ for (const keyword of mapping.keywords) {
39
+ // Match "two chairs", "3 tables", "a chair", "chairs"
40
+ const countPatterns = [
41
+ new RegExp(`(\\d+)\\s+${keyword}s?`, "i"),
42
+ new RegExp(`(one|two|three|four|five|six|seven|eight|nine|ten)\\s+${keyword}s?`, "i"),
43
+ new RegExp(`(a|an)\\s+${keyword}\\b`, "i"),
44
+ new RegExp(`\\b${keyword}s\\b`, "i"), // plural without number = 2
45
+ new RegExp(`\\b${keyword}\\b`, "i"), // singular without article = 1
46
+ ];
47
+ for (const pat of countPatterns) {
48
+ const match = lower.match(pat);
49
+ if (match) {
50
+ let count = 1;
51
+ if (match[1]) {
52
+ const numWords = {
53
+ one: 1, two: 2, three: 3, four: 4, five: 5,
54
+ six: 6, seven: 7, eight: 8, nine: 9, ten: 10,
55
+ a: 1, an: 1,
56
+ };
57
+ count = numWords[match[1].toLowerCase()] || parseInt(match[1]) || 1;
58
+ }
59
+ if (pat.source.includes("s\\b") && !match[1])
60
+ count = 2; // plural default
61
+ objects.push({ mapping, count });
62
+ break;
63
+ }
64
+ }
65
+ }
66
+ }
67
+ // Deduplicate
68
+ const seen = new Set();
69
+ const unique = objects.filter((o) => {
70
+ const key = o.mapping.keywords[0];
71
+ if (seen.has(key))
72
+ return false;
73
+ seen.add(key);
74
+ return true;
75
+ });
76
+ return {
77
+ objects: unique,
78
+ isAR: AR_KEYWORDS.some((k) => lower.includes(k)),
79
+ isIndoor: INDOOR_KEYWORDS.some((k) => lower.includes(k)),
80
+ isOutdoor: OUTDOOR_KEYWORDS.some((k) => lower.includes(k)),
81
+ needsSky: OUTDOOR_KEYWORDS.some((k) => lower.includes(k)) || lower.includes("sky"),
82
+ isDark: DARK_KEYWORDS.some((k) => lower.includes(k)),
83
+ };
84
+ }
85
+ function spreadPositions(count, basePos, spacing) {
86
+ if (count === 1)
87
+ return [basePos];
88
+ const positions = [];
89
+ const halfSpan = ((count - 1) * spacing) / 2;
90
+ for (let i = 0; i < count; i++) {
91
+ positions.push([
92
+ basePos[0] + i * spacing - halfSpan,
93
+ basePos[1],
94
+ basePos[2],
95
+ ]);
96
+ }
97
+ return positions;
98
+ }
99
+ export function generateScene(description) {
100
+ const parsed = parseDescription(description);
101
+ const elements = [];
102
+ const notes = [];
103
+ const dependencies = [];
104
+ let hasModel = false;
105
+ // Track geometry node counters for naming
106
+ let nodeCounter = 0;
107
+ // Generate elements for each parsed object
108
+ for (const obj of parsed.objects) {
109
+ const positions = spreadPositions(obj.count, obj.mapping.defaultPosition, 1.5);
110
+ for (let i = 0; i < obj.count; i++) {
111
+ const pos = positions[i];
112
+ if (obj.mapping.nodeType === "ModelNode") {
113
+ hasModel = true;
114
+ elements.push({
115
+ type: "model",
116
+ nodeType: "ModelNode",
117
+ properties: {
118
+ modelPath: `models/${obj.mapping.keywords[0]}.glb`,
119
+ scaleToUnits: String(obj.mapping.defaultScale),
120
+ position: `Position(${pos[0].toFixed(1)}f, ${pos[1].toFixed(1)}f, ${pos[2].toFixed(1)}f)`,
121
+ },
122
+ comment: obj.mapping.comment,
123
+ });
124
+ }
125
+ else {
126
+ elements.push({
127
+ type: "geometry",
128
+ nodeType: obj.mapping.nodeType,
129
+ properties: {
130
+ size: String(obj.mapping.defaultScale),
131
+ position: `Position(${pos[0].toFixed(1)}f, ${pos[1].toFixed(1)}f, ${pos[2].toFixed(1)}f)`,
132
+ },
133
+ comment: obj.mapping.comment,
134
+ });
135
+ }
136
+ nodeCounter++;
137
+ }
138
+ }
139
+ // Add light
140
+ if (!parsed.isDark) {
141
+ elements.push({
142
+ type: "light",
143
+ nodeType: "LightNode",
144
+ properties: {
145
+ type: "LightManager.Type.DIRECTIONAL",
146
+ intensity: parsed.isOutdoor ? "110_000f" : "100_000f",
147
+ castShadows: "true",
148
+ },
149
+ comment: parsed.isOutdoor ? "Sunlight" : "Main directional light",
150
+ });
151
+ }
152
+ else {
153
+ elements.push({
154
+ type: "light",
155
+ nodeType: "LightNode",
156
+ properties: {
157
+ type: "LightManager.Type.POINT",
158
+ intensity: "50_000f",
159
+ position: "Position(0f, 2f, 0f)",
160
+ castShadows: "false",
161
+ },
162
+ comment: "Dim ambient light",
163
+ });
164
+ }
165
+ // Add ground plane if not already present
166
+ const hasGround = elements.some((e) => e.nodeType === "PlaneNode" || (e.comment && e.comment.toLowerCase().includes("ground")));
167
+ if (!hasGround && !parsed.isAR) {
168
+ elements.push({
169
+ type: "geometry",
170
+ nodeType: "PlaneNode",
171
+ properties: {
172
+ size: "10.0f",
173
+ position: "Position(0f, 0f, 0f)",
174
+ },
175
+ comment: "Ground plane",
176
+ });
177
+ }
178
+ // Build the code
179
+ const isAR = parsed.isAR;
180
+ dependencies.push(isAR ? "io.github.sceneview:arsceneview:3.3.0" : "io.github.sceneview:sceneview:3.3.0");
181
+ // Build model instance declarations
182
+ const modelElements = elements.filter((e) => e.type === "model");
183
+ const uniqueModels = new Map();
184
+ modelElements.forEach((e) => {
185
+ const path = e.properties.modelPath;
186
+ if (!uniqueModels.has(path)) {
187
+ const varName = path.replace("models/", "").replace(".glb", "").replace(/[^a-zA-Z0-9]/g, "") + "Instance";
188
+ uniqueModels.set(path, varName);
189
+ }
190
+ });
191
+ // Generate Kotlin code
192
+ const lines = [];
193
+ lines.push("@Composable");
194
+ lines.push(`fun GeneratedScene() {`);
195
+ lines.push(" val engine = rememberEngine()");
196
+ lines.push(" val modelLoader = rememberModelLoader(engine)");
197
+ if (!isAR) {
198
+ lines.push(" val environmentLoader = rememberEnvironmentLoader(engine)");
199
+ }
200
+ lines.push("");
201
+ // Model instance declarations
202
+ for (const [path, varName] of uniqueModels) {
203
+ lines.push(` val ${varName} = rememberModelInstance(modelLoader, "${path}")`);
204
+ }
205
+ if (uniqueModels.size > 0)
206
+ lines.push("");
207
+ // Scene or ARScene
208
+ if (isAR) {
209
+ lines.push(" var anchor by remember { mutableStateOf<Anchor?>(null) }");
210
+ lines.push("");
211
+ lines.push(" ARScene(");
212
+ lines.push(" modifier = Modifier.fillMaxSize(),");
213
+ lines.push(" engine = engine,");
214
+ lines.push(" modelLoader = modelLoader,");
215
+ lines.push(" planeRenderer = true,");
216
+ lines.push(" onTouchEvent = { event, hitResult ->");
217
+ lines.push(" if (event.action == MotionEvent.ACTION_UP && hitResult != null) {");
218
+ lines.push(" anchor = hitResult.createAnchor()");
219
+ lines.push(" }");
220
+ lines.push(" true");
221
+ lines.push(" }");
222
+ lines.push(" ) {");
223
+ lines.push(" anchor?.let { a ->");
224
+ lines.push(" AnchorNode(anchor = a) {");
225
+ }
226
+ else {
227
+ lines.push(" Scene(");
228
+ lines.push(" modifier = Modifier.fillMaxSize(),");
229
+ lines.push(" engine = engine,");
230
+ lines.push(" modelLoader = modelLoader,");
231
+ if (!parsed.isDark) {
232
+ lines.push(' environment = rememberEnvironment(environmentLoader) {');
233
+ lines.push(' environmentLoader.createHDREnvironment("environments/sky_2k.hdr")');
234
+ lines.push(' ?: createEnvironment(environmentLoader)');
235
+ lines.push(" }");
236
+ }
237
+ lines.push(" ) {");
238
+ }
239
+ const indent = isAR ? " " : " ";
240
+ // Generate nodes
241
+ for (const elem of elements) {
242
+ lines.push(`${indent}// ${elem.comment}`);
243
+ switch (elem.type) {
244
+ case "model": {
245
+ const varName = uniqueModels.get(elem.properties.modelPath);
246
+ lines.push(`${indent}${varName}?.let { instance ->`);
247
+ lines.push(`${indent} ModelNode(`);
248
+ lines.push(`${indent} modelInstance = instance,`);
249
+ lines.push(`${indent} scaleToUnits = ${elem.properties.scaleToUnits}f,`);
250
+ if (elem.properties.position !== "Position(0.0f, 0.0f, 0.0f)") {
251
+ lines.push(`${indent} centerOrigin = ${elem.properties.position}`);
252
+ }
253
+ lines.push(`${indent} )`);
254
+ lines.push(`${indent}}`);
255
+ break;
256
+ }
257
+ case "geometry": {
258
+ const nodeType = elem.nodeType;
259
+ lines.push(`${indent}${nodeType}(`);
260
+ lines.push(`${indent} engine = engine,`);
261
+ lines.push(`${indent} size = ${elem.properties.size}`);
262
+ lines.push(`${indent})`);
263
+ break;
264
+ }
265
+ case "light": {
266
+ lines.push(`${indent}LightNode(`);
267
+ lines.push(`${indent} engine = engine,`);
268
+ lines.push(`${indent} type = ${elem.properties.type},`);
269
+ lines.push(`${indent} apply = {`);
270
+ lines.push(`${indent} intensity(${elem.properties.intensity})`);
271
+ lines.push(`${indent} castShadows(${elem.properties.castShadows})`);
272
+ lines.push(`${indent} }`);
273
+ lines.push(`${indent})`);
274
+ break;
275
+ }
276
+ }
277
+ lines.push("");
278
+ }
279
+ // Close braces
280
+ if (isAR) {
281
+ lines.push(" }");
282
+ lines.push(" }");
283
+ }
284
+ lines.push(" }");
285
+ lines.push("}");
286
+ // Notes
287
+ if (hasModel) {
288
+ notes.push("This scene references GLB model files. You need to provide actual .glb files in `src/main/assets/models/`.");
289
+ notes.push("Free 3D models: https://sketchfab.com/features/free-3d-models (download as GLB)");
290
+ }
291
+ if (isAR) {
292
+ notes.push("AR requires camera permission at runtime and ARCore on the device. See `get_platform_setup(\"android\", \"ar\")` for full setup.");
293
+ }
294
+ if (elements.length === 1) {
295
+ notes.push("Only a light was generated. The description didn't match any known objects. Try mentioning specific objects like 'table', 'chair', 'sphere', 'cube', etc.");
296
+ }
297
+ return {
298
+ code: lines.join("\n"),
299
+ description,
300
+ elements,
301
+ isAR,
302
+ dependencies,
303
+ notes,
304
+ };
305
+ }
306
+ export function formatGeneratedScene(result) {
307
+ const parts = [];
308
+ parts.push(`## Generated Scene\n`);
309
+ parts.push(`**Description:** "${result.description}"`);
310
+ parts.push(`**Mode:** ${result.isAR ? "AR (ARScene)" : "3D (Scene)"}`);
311
+ parts.push(`**Elements:** ${result.elements.length} nodes\n`);
312
+ parts.push(`### Dependency\n`);
313
+ parts.push("```kotlin");
314
+ result.dependencies.forEach((d) => parts.push(`implementation("${d}")`));
315
+ parts.push("```\n");
316
+ parts.push(`### Code\n`);
317
+ parts.push("```kotlin");
318
+ parts.push(result.code);
319
+ parts.push("```\n");
320
+ if (result.notes.length > 0) {
321
+ parts.push(`### Notes\n`);
322
+ result.notes.forEach((n, i) => parts.push(`${i + 1}. ${n}`));
323
+ }
324
+ return parts.join("\n");
325
+ }
package/dist/index.js CHANGED
@@ -13,6 +13,10 @@ import { parseNodeSections, findNodeSection, listNodeTypes } from "./node-refere
13
13
  import { PLATFORM_ROADMAP, BEST_PRACTICES, AR_SETUP_GUIDE, TROUBLESHOOTING_GUIDE } from "./guides.js";
14
14
  import { buildPreviewUrl, validatePreviewInput, formatPreviewResponse } from "./preview.js";
15
15
  import { validateArtifactInput, generateArtifact, formatArtifactResponse } from "./artifact.js";
16
+ import { getPlatformSetup, PLATFORM_IDS } from "./platform-setup.js";
17
+ import { migrateCode, formatMigrationResult } from "./migrate-code.js";
18
+ import { getDebugGuide, autoDetectIssue, DEBUG_CATEGORIES } from "./debug-issue.js";
19
+ import { generateScene, formatGeneratedScene } from "./generate-scene.js";
16
20
  const __dirname = dirname(fileURLToPath(import.meta.url));
17
21
  // ─── Legal disclaimer ─────────────────────────────────────────────────────────
18
22
  const DISCLAIMER = '\n\n---\n*Generated code suggestion. Review before use in production. See [TERMS.md](https://github.com/SceneView/sceneview/blob/main/mcp/TERMS.md).*';
@@ -33,7 +37,7 @@ catch {
33
37
  API_DOCS = "SceneView API docs not found. Run `npm run prepare` to bundle llms.txt.";
34
38
  }
35
39
  const NODE_SECTIONS = parseNodeSections(API_DOCS);
36
- const server = new Server({ name: "@sceneview/mcp", version: "3.4.11" }, { capabilities: { resources: {}, tools: {} } });
40
+ const server = new Server({ name: "@sceneview/mcp", version: "3.4.13" }, { capabilities: { resources: {}, tools: {} } });
37
41
  // ─── Resources ───────────────────────────────────────────────────────────────
38
42
  server.setRequestHandler(ListResourcesRequestSchema, async () => ({
39
43
  resources: [
@@ -344,6 +348,73 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
344
348
  required: ["type"],
345
349
  },
346
350
  },
351
+ {
352
+ name: "get_platform_setup",
353
+ description: "Returns the complete setup guide for any SceneView-supported platform: Android, iOS, Web, Flutter, React Native, Desktop, or Android TV. Includes dependencies, manifest/permissions, minimum SDK, and a working starter template. Replaces `get_setup`, `get_ios_setup`, and `get_web_setup` with a single unified tool. Use this when a user wants to set up SceneView on any platform.",
354
+ inputSchema: {
355
+ type: "object",
356
+ properties: {
357
+ platform: {
358
+ type: "string",
359
+ enum: PLATFORM_IDS,
360
+ description: `Target platform:\n${PLATFORM_IDS.map((p) => `- "${p}"`).join("\n")}`,
361
+ },
362
+ type: {
363
+ type: "string",
364
+ enum: ["3d", "ar"],
365
+ description: '"3d" for 3D-only scenes. "ar" for augmented reality. Some platforms only support 3D.',
366
+ },
367
+ },
368
+ required: ["platform", "type"],
369
+ },
370
+ },
371
+ {
372
+ name: "migrate_code",
373
+ description: "Automatically migrates SceneView 2.x Kotlin code to 3.x. Applies known renames (SceneView→Scene, ArSceneView→ARScene), replaces deprecated APIs (loadModelAsync→rememberModelInstance, Engine.create→rememberEngine), fixes LightNode trailing-lambda bug, removes Sceneform imports, and more. Returns the migrated code with a detailed changelog. Use this when a user has 2.x code that needs updating, or when you detect 2.x patterns in their code.",
374
+ inputSchema: {
375
+ type: "object",
376
+ properties: {
377
+ code: {
378
+ type: "string",
379
+ description: "The SceneView 2.x Kotlin code to migrate. Can be a snippet, a function, or a full file.",
380
+ },
381
+ },
382
+ required: ["code"],
383
+ },
384
+ },
385
+ {
386
+ name: "debug_issue",
387
+ description: 'Returns a targeted debugging guide for a specific SceneView issue. Categories: "model-not-showing" (invisible models), "ar-not-working" (AR camera/planes), "crash" (SIGABRT/native), "performance" (low FPS/memory), "build-error" (Gradle/dependency), "black-screen" (no rendering), "lighting" (dark/bright/shadows), "gestures" (touch/drag), "ios" (Swift/RealityKit). You can provide a category directly, or describe the problem and it will be auto-detected. Use this when a user reports something not working.',
388
+ inputSchema: {
389
+ type: "object",
390
+ properties: {
391
+ category: {
392
+ type: "string",
393
+ enum: DEBUG_CATEGORIES,
394
+ description: `The issue category. If omitted, provide \`description\` for auto-detection.\n${DEBUG_CATEGORIES.map((c) => `- "${c}"`).join("\n")}`,
395
+ },
396
+ description: {
397
+ type: "string",
398
+ description: 'Free-text description of the issue (e.g., "my model is not showing", "app crashes on destroy"). Used for auto-detection when category is omitted.',
399
+ },
400
+ },
401
+ required: [],
402
+ },
403
+ },
404
+ {
405
+ name: "generate_scene",
406
+ description: 'Generates a complete, compilable Scene{} or ARScene{} Kotlin composable from a natural language description. Parses objects (table, chair, sphere, car, etc.), quantities ("two chairs", "3 spheres"), environment (indoor/outdoor/dark), and mode (3D or AR). Returns working Kotlin code with proper engine setup, model loading, lighting, and ground plane. Use this when a user says "build me a scene with..." or describes a 3D scene they want to create.',
407
+ inputSchema: {
408
+ type: "object",
409
+ properties: {
410
+ description: {
411
+ type: "string",
412
+ description: 'Natural language description of the desired 3D scene. Examples: "a room with a table and two chairs", "AR scene with a robot on the floor", "outdoor scene with three trees and a car", "dark room with a sphere and a cube".',
413
+ },
414
+ },
415
+ required: ["description"],
416
+ },
417
+ },
347
418
  ],
348
419
  }));
349
420
  // ─── Tool handlers ────────────────────────────────────────────────────────────
@@ -874,6 +945,67 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
874
945
  const text = formatArtifactResponse(result);
875
946
  return { content: withDisclaimer([{ type: "text", text }]) };
876
947
  }
948
+ // ── get_platform_setup ─────────────────────────────────────────────────
949
+ case "get_platform_setup": {
950
+ const platform = request.params.arguments?.platform;
951
+ const setupType = request.params.arguments?.type;
952
+ if (!platform || !setupType) {
953
+ return {
954
+ content: [{ type: "text", text: "Missing required parameters: `platform` and `type`." }],
955
+ isError: true,
956
+ };
957
+ }
958
+ const guide = getPlatformSetup(platform, setupType);
959
+ return { content: withDisclaimer([{ type: "text", text: guide }]) };
960
+ }
961
+ // ── migrate_code ─────────────────────────────────────────────────────────
962
+ case "migrate_code": {
963
+ const code = request.params.arguments?.code;
964
+ if (!code || typeof code !== "string") {
965
+ return {
966
+ content: [{ type: "text", text: "Missing required parameter: `code`." }],
967
+ isError: true,
968
+ };
969
+ }
970
+ const migrationResult = migrateCode(code);
971
+ const migrationReport = formatMigrationResult(migrationResult);
972
+ return { content: withDisclaimer([{ type: "text", text: migrationReport }]) };
973
+ }
974
+ // ── debug_issue ──────────────────────────────────────────────────────────
975
+ case "debug_issue": {
976
+ let category = request.params.arguments?.category;
977
+ const desc = request.params.arguments?.description;
978
+ if (!category && desc) {
979
+ category = autoDetectIssue(desc) ?? undefined;
980
+ }
981
+ if (!category) {
982
+ return {
983
+ content: [{
984
+ type: "text",
985
+ text: `Please provide a \`category\` or \`description\`.\n\nAvailable categories: ${DEBUG_CATEGORIES.join(", ")}`,
986
+ }],
987
+ isError: true,
988
+ };
989
+ }
990
+ const debugGuide = getDebugGuide(category);
991
+ const prefix = desc && autoDetectIssue(desc) === category
992
+ ? `> Auto-detected category: **${category}** from your description.\n\n`
993
+ : "";
994
+ return { content: withDisclaimer([{ type: "text", text: prefix + debugGuide }]) };
995
+ }
996
+ // ── generate_scene ───────────────────────────────────────────────────────
997
+ case "generate_scene": {
998
+ const sceneDesc = request.params.arguments?.description;
999
+ if (!sceneDesc || typeof sceneDesc !== "string") {
1000
+ return {
1001
+ content: [{ type: "text", text: "Missing required parameter: `description`." }],
1002
+ isError: true,
1003
+ };
1004
+ }
1005
+ const sceneResult = generateScene(sceneDesc);
1006
+ const sceneReport = formatGeneratedScene(sceneResult);
1007
+ return { content: withDisclaimer([{ type: "text", text: sceneReport }]) };
1008
+ }
877
1009
  default:
878
1010
  return {
879
1011
  content: [{ type: "text", text: `Unknown tool: ${request.params.name}` }],