keyshot-mcp 0.8.0 → 0.9.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.
package/README.md CHANGED
@@ -41,7 +41,7 @@ the local computer.
41
41
 
42
42
  ### Install
43
43
 
44
- The current release is `0.8.0`.
44
+ The current release is `0.9.0`.
45
45
 
46
46
  #### Option 1: run with npx
47
47
 
@@ -52,7 +52,7 @@ This requires no global npm installation:
52
52
  "mcpServers": {
53
53
  "keyshot": {
54
54
  "command": "npx",
55
- "args": ["-y", "keyshot-mcp@0.8.0"],
55
+ "args": ["-y", "keyshot-mcp@0.9.0"],
56
56
  "env": {
57
57
  "KEYSHOT_HEADLESS_EXE": "C:/Program Files/KeyShot Studio/bin/keyshot_headless.exe"
58
58
  }
@@ -64,7 +64,7 @@ This requires no global npm installation:
64
64
  #### Option 2: install globally
65
65
 
66
66
  ```bash
67
- npm install -g keyshot-mcp@0.8.0
67
+ npm install -g keyshot-mcp@0.9.0
68
68
  ```
69
69
 
70
70
  ```json
@@ -113,11 +113,11 @@ The following prompt can be pasted into a coding agent that is allowed to edit
113
113
  the MCP client configuration:
114
114
 
115
115
  ```text
116
- Install KeyShot MCP 0.8.0 and configure it in my MCP client.
116
+ Install KeyShot MCP 0.9.0 and configure it in my MCP client.
117
117
 
118
118
  1. Find the local KeyShot headless executable.
119
119
  2. Add an MCP server named "keyshot" that runs:
120
- npx -y keyshot-mcp@0.8.0
120
+ npx -y keyshot-mcp@0.9.0
121
121
  3. Set KEYSHOT_HEADLESS_EXE to the executable path.
122
122
  4. Keep KEYSHOT_ALLOW_EXTERNAL_OUTPUTS disabled.
123
123
  5. Restart or reload the MCP client, call keyshot_status, then use keyshot_product_render for a one-click product render.
@@ -129,7 +129,7 @@ Install KeyShot MCP 0.8.0 and configure it in my MCP client.
129
129
  | Variable | Default | Purpose |
130
130
  | --- | --- | --- |
131
131
  | `KEYSHOT_HEADLESS_EXE` | `keyshot_headless.exe` on Windows | Absolute executable path or a command available on `PATH`. |
132
- | `KEYSHOT_OUTPUT_DIR` | Package-local `outputs` | Root directory for rendered images and saved scenes. |
132
+ | `KEYSHOT_OUTPUT_DIR` | `~/Documents/KeyShot MCP Outputs` | Root directory for rendered images and saved scenes. |
133
133
  | `KEYSHOT_ALLOW_EXTERNAL_OUTPUTS` | `false` | Allows output paths outside `KEYSHOT_OUTPUT_DIR` when explicitly set to `true`. |
134
134
  | `KEYSHOT_TIMEOUT_MS` | `600000` | Timeout for one KeyShot headless process. |
135
135
  | `KEYSHOT_LICENSE_ARGS` | empty | Optional additional KeyShot launch arguments. |
@@ -140,11 +140,16 @@ Relative output paths are resolved inside `KEYSHOT_OUTPUT_DIR`. Parent traversal
140
140
  adjacent-prefix paths, and symbolic-link escapes are rejected by default. Input
141
141
  scene, model, material, and environment paths may be located elsewhere.
142
142
 
143
+ Run `keyshot_status` after installation. It reports the MCP and KeyShot
144
+ versions, resolved executable, output write access, preset validity, headless
145
+ startup result, and actionable suggestions. Additional KeyShot launch arguments
146
+ are never included in the diagnostic output.
147
+
143
148
  ### Tools
144
149
 
145
150
  | Tool | Purpose |
146
151
  | --- | --- |
147
- | `keyshot_status` | Verify that KeyShot headless can start and report its version. |
152
+ | `keyshot_status` | Diagnose local configuration, output access, presets, and KeyShot headless startup. |
148
153
  | `keyshot_product_render` | Prepare, save, and render a model or existing scene in one headless process. |
149
154
  | `keyshot_inspect_scene` | Inspect objects, cameras, materials, model sets, and references. |
150
155
  | `keyshot_list_cameras` | List available camera names. |
@@ -180,6 +185,19 @@ models default to centered and grounded geometry with a `Product Hero`
180
185
  isometric camera. Existing scenes keep their current camera, materials, and
181
186
  environment unless explicit changes are requested.
182
187
 
188
+ One-click product rendering defaults to the `standard` quality preset. All
189
+ rendering tools accept:
190
+
191
+ | Preset | Resolution | Samples |
192
+ | --- | ---: | ---: |
193
+ | `preview` | 960 × 540 | 16 |
194
+ | `standard` | 1920 × 1080 | 64 |
195
+ | `final` | 3840 × 2160 | 256 |
196
+
197
+ Explicit `width`, `height`, or `samples` values override the corresponding
198
+ preset value. `maxTimeSeconds` replaces preset samples. Explicit `samples` and
199
+ `maxTimeSeconds` cannot be used together.
200
+
183
201
  ```json
184
202
  {
185
203
  "modelPath": "C:/models/speaker.obj",
@@ -192,16 +210,16 @@ environment unless explicit changes are requested.
192
210
  "focalLength": 55,
193
211
  "brightness": 1.2,
194
212
  "rotation": 45,
195
- "width": 1600,
196
- "height": 1200,
197
- "samples": 128
213
+ "qualityPreset": "standard"
198
214
  }
199
215
  ```
200
216
 
201
217
  Use `renderMode: "allCameras"` with `outputDir` to render every named camera.
202
218
  Generated names are derived from the source filename when output paths are
203
- omitted. Existing files are protected by default; set `overwrite: true` only
204
- when replacement is intentional. Material assignments always require an
219
+ omitted. If an automatically named result exists, the scene and image receive
220
+ the same `-2`, `-3`, and later suffix. Explicit output paths remain protected
221
+ and return an error when occupied; set `overwrite: true` only when replacement
222
+ is intentional. Material assignments always require an
205
223
  explicit object and never overwrite the entire model implicitly.
206
224
 
207
225
  ### Product composition examples
@@ -345,7 +363,7 @@ headless 脚本让兼容 MCP 的 AI Agent 检查、编辑和渲染 KeyShot 场
345
363
 
346
364
  ### 安装
347
365
 
348
- 当前版本为 `0.8.0`。
366
+ 当前版本为 `0.9.0`。
349
367
 
350
368
  #### 方式一:使用 npx
351
369
 
@@ -356,7 +374,7 @@ headless 脚本让兼容 MCP 的 AI Agent 检查、编辑和渲染 KeyShot 场
356
374
  "mcpServers": {
357
375
  "keyshot": {
358
376
  "command": "npx",
359
- "args": ["-y", "keyshot-mcp@0.8.0"],
377
+ "args": ["-y", "keyshot-mcp@0.9.0"],
360
378
  "env": {
361
379
  "KEYSHOT_HEADLESS_EXE": "C:/Program Files/KeyShot Studio/bin/keyshot_headless.exe"
362
380
  }
@@ -368,7 +386,7 @@ headless 脚本让兼容 MCP 的 AI Agent 检查、编辑和渲染 KeyShot 场
368
386
  #### 方式二:全局安装
369
387
 
370
388
  ```bash
371
- npm install -g keyshot-mcp@0.8.0
389
+ npm install -g keyshot-mcp@0.9.0
372
390
  ```
373
391
 
374
392
  ```json
@@ -416,11 +434,11 @@ pnpm build
416
434
  下面的提示词适用于有权限修改 MCP 客户端配置的编程 Agent:
417
435
 
418
436
  ```text
419
- 请帮我安装 KeyShot MCP 0.8.0,并添加到我的 MCP 客户端。
437
+ 请帮我安装 KeyShot MCP 0.9.0,并添加到我的 MCP 客户端。
420
438
 
421
439
  1. 查找本机 KeyShot headless 可执行文件。
422
440
  2. 添加名为 keyshot 的 MCP server,运行:
423
- npx -y keyshot-mcp@0.8.0
441
+ npx -y keyshot-mcp@0.9.0
424
442
  3. 将 KEYSHOT_HEADLESS_EXE 设置为可执行文件路径。
425
443
  4. 保持 KEYSHOT_ALLOW_EXTERNAL_OUTPUTS 关闭。
426
444
  5. 重启或重新加载 MCP 客户端,调用 keyshot_status,然后使用 keyshot_product_render 一键完成产品出图。
@@ -432,7 +450,7 @@ pnpm build
432
450
  | 环境变量 | 默认值 | 用途 |
433
451
  | --- | --- | --- |
434
452
  | `KEYSHOT_HEADLESS_EXE` | Windows 上为 `keyshot_headless.exe` | KeyShot headless 绝对路径,或系统 `PATH` 中的命令。 |
435
- | `KEYSHOT_OUTPUT_DIR` | 包内 `outputs` | 渲染图和已保存场景的根目录。 |
453
+ | `KEYSHOT_OUTPUT_DIR` | `~/Documents/KeyShot MCP Outputs` | 渲染图和已保存场景的根目录。 |
436
454
  | `KEYSHOT_ALLOW_EXTERNAL_OUTPUTS` | `false` | 明确设为 `true` 时允许写入输出根目录之外。 |
437
455
  | `KEYSHOT_TIMEOUT_MS` | `600000` | 单个 KeyShot headless 进程的超时时间。 |
438
456
  | `KEYSHOT_LICENSE_ARGS` | 空 | 可选的 KeyShot 启动参数。 |
@@ -442,11 +460,15 @@ pnpm build
442
460
  相对输出路径会自动放入 `KEYSHOT_OUTPUT_DIR`。默认拒绝 `..`、相邻同名前缀目录和
443
461
  软链接逃逸。输入场景、模型、材质和环境文件可位于其他目录。
444
462
 
463
+ 安装后调用 `keyshot_status`,可检查 MCP 与 KeyShot 版本、实际可执行文件路径、
464
+ 输出目录写入权限、预设文件和 headless 启动状态,并获得可直接执行的修复建议。
465
+ 诊断结果不会回显额外的 KeyShot 启动参数。
466
+
445
467
  ### MCP 工具
446
468
 
447
469
  | 工具 | 用途 |
448
470
  | --- | --- |
449
- | `keyshot_status` | 检查 KeyShot headless 是否可启动并读取版本。 |
471
+ | `keyshot_status` | 诊断本地配置、输出权限、预设文件和 KeyShot headless 启动状态。 |
450
472
  | `keyshot_product_render` | 在一个 headless 进程中完成模型或场景准备、保存和渲染。 |
451
473
  | `keyshot_inspect_scene` | 检查对象、相机、材质、模型集和外部引用。 |
452
474
  | `keyshot_list_cameras` | 列出场景中的相机名称。 |
@@ -480,6 +502,18 @@ pnpm build
480
502
  贴地并创建名为 `Product Hero` 的等轴测相机;已有场景默认保留当前相机、材质和环境,
481
503
  只有明确提供参数时才修改。
482
504
 
505
+ 一键产品出图未指定质量时默认使用 `standard`。全部渲染工具都支持:
506
+
507
+ | 预设 | 分辨率 | 采样 |
508
+ | --- | ---: | ---: |
509
+ | `preview` | 960 × 540 | 16 |
510
+ | `standard` | 1920 × 1080 | 64 |
511
+ | `final` | 3840 × 2160 | 256 |
512
+
513
+ 显式填写的 `width`、`height` 或 `samples` 会分别覆盖预设值。
514
+ `maxTimeSeconds` 会替代预设采样;不能同时显式提供 `samples` 和
515
+ `maxTimeSeconds`。
516
+
483
517
  ```json
484
518
  {
485
519
  "modelPath": "C:/models/speaker.obj",
@@ -492,14 +526,13 @@ pnpm build
492
526
  "focalLength": 55,
493
527
  "brightness": 1.2,
494
528
  "rotation": 45,
495
- "width": 1600,
496
- "height": 1200,
497
- "samples": 128
529
+ "qualityPreset": "standard"
498
530
  }
499
531
  ```
500
532
 
501
533
  将 `renderMode` 设为 `allCameras` 并提供 `outputDir`,即可渲染场景中的全部命名相机。
502
- 省略输出路径时会根据源文件名自动生成。默认不会覆盖已有文件,只有明确设置
534
+ 省略输出路径时会根据源文件名自动生成。自动名称已存在时,场景和图片会统一增加
535
+ `-2`、`-3` 等编号;用户明确填写的路径仍会严格防止覆盖。只有明确设置
503
536
  `overwrite: true` 才会替换。材质指定必须包含明确的对象,不会隐式覆盖整个模型。
504
537
 
505
538
  ### 产品构图示例
@@ -1,5 +1,5 @@
1
1
  import { getConfig } from "../config.js";
2
- import { runKeyShotSerialized } from "../runner.js";
3
- const result = await runKeyShotSerialized(getConfig(), { operation: "status" });
2
+ import { runKeyShotDiagnostics } from "../diagnostics.js";
3
+ const result = await runKeyShotDiagnostics(getConfig());
4
4
  console.log(JSON.stringify(result, null, 2));
5
5
  process.exit(result.ok ? 0 : 1);
package/dist/config.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import path from "node:path";
2
+ import os from "node:os";
2
3
  import { fileURLToPath } from "node:url";
3
4
  const DEFAULT_KEYSHOT_EXE = process.platform === "win32" ? "keyshot_headless.exe" : "keyshot_headless";
4
5
  const DEFAULT_TIMEOUT_MS = 600_000;
5
6
  export function getConfig() {
6
7
  const projectRoot = path.resolve(fileURLToPath(new URL("..", import.meta.url)));
7
- const keyshotOutputDir = path.resolve(process.env.KEYSHOT_OUTPUT_DIR ?? path.join(projectRoot, "outputs"));
8
+ const keyshotOutputDir = path.resolve(process.env.KEYSHOT_OUTPUT_DIR ?? defaultOutputDir());
8
9
  return {
9
10
  projectRoot,
10
11
  keyshotHeadlessExe: process.env.KEYSHOT_HEADLESS_EXE ?? DEFAULT_KEYSHOT_EXE,
@@ -18,6 +19,9 @@ export function getConfig() {
18
19
  cameraPresetsPath: path.resolve(process.env.KEYSHOT_CAMERA_PRESETS ?? path.join(projectRoot, "presets", "cameras.json")),
19
20
  };
20
21
  }
22
+ export function defaultOutputDir() {
23
+ return path.join(os.homedir(), "Documents", "KeyShot MCP Outputs");
24
+ }
21
25
  function parseBoolean(value) {
22
26
  return ["1", "true", "yes", "on"].includes((value ?? "").trim().toLowerCase());
23
27
  }
@@ -0,0 +1,191 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ import { loadCameraPresets } from "./camera-presets.js";
5
+ import { loadMaterialPresets } from "./presets.js";
6
+ import { runKeyShotSerialized } from "./runner.js";
7
+ import { VERSION } from "./version.js";
8
+ export async function runKeyShotDiagnostics(config, runStatus = runKeyShotSerialized) {
9
+ const checks = [];
10
+ const resolvedExecutable = await resolveExecutable(config.keyshotHeadlessExe);
11
+ checks.push({
12
+ id: "keyshot-executable",
13
+ label: "KeyShot headless executable",
14
+ ok: resolvedExecutable !== null,
15
+ severity: "error",
16
+ message: resolvedExecutable
17
+ ? `Resolved KeyShot headless executable: ${resolvedExecutable}`
18
+ : `KeyShot headless executable was not found: ${config.keyshotHeadlessExe}`,
19
+ suggestion: resolvedExecutable
20
+ ? undefined
21
+ : "Set KEYSHOT_HEADLESS_EXE to the full path of keyshot_headless.exe and restart the MCP client.",
22
+ });
23
+ checks.push(await fileCheck("bridge-script", "KeyShot bridge script", config.bridgeScriptPath, "Reinstall keyshot-mcp so scripts/keyshot_bridge.py is included."));
24
+ checks.push(await outputWriteCheck(config.keyshotOutputDir));
25
+ checks.push(await presetCheck("camera-presets", "Camera presets", () => loadCameraPresets(config), config.cameraPresetsPath));
26
+ checks.push(await presetCheck("material-presets", "Material presets", () => loadMaterialPresets(config), config.materialPresetsPath));
27
+ let keyshotResult;
28
+ if (resolvedExecutable && checks.find((check) => check.id === "bridge-script")?.ok) {
29
+ keyshotResult = await runStatus(config, { operation: "status" });
30
+ }
31
+ else {
32
+ keyshotResult = {
33
+ ok: false,
34
+ data: null,
35
+ outputFiles: [],
36
+ warnings: [],
37
+ keyshotStdoutTail: "",
38
+ error: "KeyShot startup was skipped because a required local file was not available.",
39
+ errorCode: "DIAGNOSTIC_PREREQUISITE_FAILED",
40
+ suggestions: [],
41
+ };
42
+ }
43
+ checks.push({
44
+ id: "keyshot-startup",
45
+ label: "KeyShot headless startup",
46
+ ok: keyshotResult.ok,
47
+ severity: "error",
48
+ message: keyshotResult.ok
49
+ ? "KeyShot headless scripting started successfully."
50
+ : keyshotResult.error ?? "KeyShot headless scripting did not start.",
51
+ suggestion: keyshotResult.ok
52
+ ? undefined
53
+ : keyshotResult.suggestions?.[0] ?? "Open KeyShot normally, confirm the license is active, and retry.",
54
+ });
55
+ const ready = checks.every((check) => check.severity !== "error" || check.ok);
56
+ const suggestions = unique([
57
+ ...checks.flatMap((check) => !check.ok && check.suggestion ? [check.suggestion] : []),
58
+ ...(keyshotResult.suggestions ?? []),
59
+ ]);
60
+ const bridgeData = keyshotResult.data && typeof keyshotResult.data === "object"
61
+ ? keyshotResult.data
62
+ : {};
63
+ const availableFunctions = Array.isArray(bridgeData.availableFunctions)
64
+ ? bridgeData.availableFunctions.filter((name) => typeof name === "string")
65
+ : [];
66
+ const { availableFunctions: _omittedFunctions, ...bridgeSummary } = bridgeData;
67
+ return {
68
+ ...keyshotResult,
69
+ ok: ready,
70
+ data: {
71
+ ...bridgeSummary,
72
+ availableFunctionCount: availableFunctions.length,
73
+ capabilities: capabilitySummary(availableFunctions),
74
+ serverVersion: VERSION,
75
+ ready,
76
+ config: {
77
+ keyshotHeadlessExe: config.keyshotHeadlessExe,
78
+ resolvedExecutable,
79
+ outputDir: config.keyshotOutputDir,
80
+ allowExternalOutputs: config.keyshotAllowExternalOutputs,
81
+ timeoutMs: config.keyshotTimeoutMs,
82
+ licenseArgsConfigured: config.keyshotLicenseArgs.length > 0,
83
+ bridgeScriptPath: config.bridgeScriptPath,
84
+ cameraPresetsPath: config.cameraPresetsPath,
85
+ materialPresetsPath: config.materialPresetsPath,
86
+ },
87
+ checks,
88
+ suggestions,
89
+ },
90
+ error: ready ? null : keyshotResult.error ?? "KeyShot MCP is not ready. Review the failed diagnostic checks.",
91
+ errorCode: ready ? null : keyshotResult.errorCode ?? "DIAGNOSTIC_FAILED",
92
+ suggestions,
93
+ };
94
+ }
95
+ function capabilitySummary(availableFunctions) {
96
+ const available = new Set(availableFunctions);
97
+ return {
98
+ render: available.has("renderImage"),
99
+ inspectObjects: available.has("getObjects"),
100
+ cameras: available.has("getCameras") && available.has("newCamera"),
101
+ standardViews: available.has("setStandardView"),
102
+ importModel: available.has("importFile"),
103
+ materials: available.has("setObjectMaterial"),
104
+ renderOptions: available.has("getRenderOptions"),
105
+ environments: available.has("getActiveEnvironment"),
106
+ };
107
+ }
108
+ export async function resolveExecutable(command) {
109
+ if (path.isAbsolute(command) || command.includes("/") || command.includes("\\")) {
110
+ const candidate = path.resolve(command);
111
+ return await isFile(candidate) ? candidate : null;
112
+ }
113
+ const pathEntries = (process.env.PATH ?? "").split(path.delimiter).filter(Boolean);
114
+ const extensions = process.platform === "win32" && path.extname(command) === ""
115
+ ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";")
116
+ : [""];
117
+ for (const entry of pathEntries) {
118
+ for (const extension of extensions) {
119
+ const candidate = path.join(entry.replace(/^"|"$/g, ""), `${command}${extension}`);
120
+ if (await isFile(candidate))
121
+ return path.resolve(candidate);
122
+ }
123
+ }
124
+ return null;
125
+ }
126
+ async function fileCheck(id, label, filePath, suggestion) {
127
+ const ok = await isFile(filePath);
128
+ return { id, label, ok, severity: "error", message: ok ? `Found: ${filePath}` : `Missing: ${filePath}`, suggestion: ok ? undefined : suggestion };
129
+ }
130
+ async function outputWriteCheck(outputDir) {
131
+ const probe = path.join(outputDir, `.keyshot-mcp-write-test-${randomUUID()}`);
132
+ try {
133
+ await fs.mkdir(outputDir, { recursive: true });
134
+ await fs.writeFile(probe, "ok", "utf8");
135
+ await fs.rm(probe, { force: true });
136
+ return {
137
+ id: "output-directory",
138
+ label: "Output directory",
139
+ ok: true,
140
+ severity: "error",
141
+ message: `Output directory is writable: ${outputDir}`,
142
+ };
143
+ }
144
+ catch (error) {
145
+ await fs.rm(probe, { force: true }).catch(() => undefined);
146
+ return {
147
+ id: "output-directory",
148
+ label: "Output directory",
149
+ ok: false,
150
+ severity: "error",
151
+ message: `Output directory is not writable: ${outputDir} (${errorMessage(error)})`,
152
+ suggestion: "Set KEYSHOT_OUTPUT_DIR to a local folder where the current user can create files.",
153
+ };
154
+ }
155
+ }
156
+ async function presetCheck(id, label, loader, source) {
157
+ try {
158
+ const presets = await loader();
159
+ return {
160
+ id,
161
+ label,
162
+ ok: true,
163
+ severity: "warning",
164
+ message: `${label} loaded successfully (${presets.length}) from ${source}`,
165
+ };
166
+ }
167
+ catch (error) {
168
+ return {
169
+ id,
170
+ label,
171
+ ok: false,
172
+ severity: "warning",
173
+ message: errorMessage(error),
174
+ suggestion: `Fix the JSON syntax in ${source}, or remove the custom environment variable to use built-in presets.`,
175
+ };
176
+ }
177
+ }
178
+ async function isFile(value) {
179
+ try {
180
+ return (await fs.stat(value)).isFile();
181
+ }
182
+ catch {
183
+ return false;
184
+ }
185
+ }
186
+ function unique(values) {
187
+ return [...new Set(values)];
188
+ }
189
+ function errorMessage(error) {
190
+ return error instanceof Error ? error.message : String(error);
191
+ }
package/dist/index.js CHANGED
@@ -9,6 +9,8 @@ import { loadMaterialPresets, findMaterialPreset } from "./presets.js";
9
9
  import { loadCameraPresets, findCameraPreset } from "./camera-presets.js";
10
10
  import { prepareProductRenderRequest } from "./product-render.js";
11
11
  import { VERSION } from "./version.js";
12
+ import { applyRenderQuality } from "./quality.js";
13
+ import { runKeyShotDiagnostics } from "./diagnostics.js";
12
14
  import { applyMaterialSchema, applyMaterialInputSchema, applyMaterialPresetInputSchema, applyMaterialPresetSchema, applyCameraPresetSchema, batchRenderSchema, batchRenderInputSchema, importModelSchema, listCamerasSchema, listMaterialPresetsSchema, listCameraPresetsSchema, productRenderInputSchema, productRenderSchema, renderQueueSchema, renderQueueInputSchema, renderAllCamerasSchema, renderAllCamerasInputSchema, renderSchema, renderInputSchema, saveSceneSchema, scenePathSchema, setCameraInputSchema, setCameraSchema, setEnvironmentSchema, } from "./schemas.js";
13
15
  const config = getConfig();
14
16
  const server = new McpServer({
@@ -64,7 +66,7 @@ server.registerPrompt("keyshot_product_render", {
64
66
  },
65
67
  ],
66
68
  }));
67
- server.tool("keyshot_status", "Check KeyShot headless availability and script startup.", {}, async () => toolResponse(await runKeyShotSerialized(config, { operation: "status" })));
69
+ server.tool("keyshot_status", "Diagnose the local KeyShot MCP installation and verify headless startup.", {}, async () => toolResponse(await runKeyShotDiagnostics(config)));
68
70
  server.tool("keyshot_product_render", "Prepare and render a product from either a model file or an existing KeyShot scene in one headless process.", productRenderInputSchema.shape, async (args) => {
69
71
  try {
70
72
  const parsed = productRenderSchema.parse(args);
@@ -79,19 +81,19 @@ server.tool("keyshot_inspect_scene", "Open a KeyShot scene and return available
79
81
  server.tool("keyshot_list_cameras", "Open a KeyShot scene and return the list of available camera names (useful before batch rendering).", listCamerasSchema.shape, async (args) => toolResponse(await runKeyShotSerialized(config, { operation: "list_cameras", ...args })));
80
82
  server.tool("keyshot_render", "Render a KeyShot scene to an image file.", renderSchema.shape, async (args) => {
81
83
  const parsed = renderInputSchema.parse(args);
82
- return toolResponse(await runKeyShotSerialized(config, { operation: "render", ...parsed }));
84
+ return toolResponse(await runKeyShotSerialized(config, { operation: "render", ...applyRenderQuality(parsed) }));
83
85
  });
84
86
  server.tool("keyshot_render_queue", "Render several jobs sequentially. Stops at the first failure unless continueOnError is set.", renderQueueSchema.shape, async (args) => {
85
87
  const parsed = renderQueueInputSchema.parse(args);
86
- return toolResponse(await runRenderQueue(config, parsed.jobs, { continueOnError: parsed.continueOnError ?? false }));
88
+ return toolResponse(await runRenderQueue(config, parsed.jobs.map((job) => applyRenderQuality(job)), { continueOnError: parsed.continueOnError ?? false }));
87
89
  });
88
90
  server.tool("keyshot_batch_render", "Render multiple named cameras from one KeyShot scene into an output directory.", batchRenderSchema.shape, async (args) => {
89
91
  const parsed = batchRenderInputSchema.parse(args);
90
- return toolResponse(await runKeyShotSerialized(config, { operation: "batch_render", ...parsed }));
92
+ return toolResponse(await runKeyShotSerialized(config, { operation: "batch_render", ...applyRenderQuality(parsed) }));
91
93
  });
92
94
  server.tool("keyshot_render_all_cameras", "Discover every camera in one KeyShot scene and render each view into an output directory.", renderAllCamerasSchema.shape, async (args) => {
93
95
  const parsed = renderAllCamerasInputSchema.parse(args);
94
- return toolResponse(await runKeyShotSerialized(config, { operation: "render_all_cameras", ...parsed }));
96
+ return toolResponse(await runKeyShotSerialized(config, { operation: "render_all_cameras", ...applyRenderQuality(parsed) }));
95
97
  });
96
98
  server.tool("keyshot_import_model", "Import a model into an optional base scene, optionally center and ground it, adjust the camera or environment, and save the resulting scene.", importModelSchema.shape, async (args) => toolResponse(await runKeyShotSerialized(config, {
97
99
  operation: "import_model",
@@ -0,0 +1,43 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ const PRODUCT_OUTPUT_FIELDS = ["outputScenePath", "outputPath", "outputDir"];
4
+ export async function allocateAutomaticProductOutputs(request) {
5
+ const configured = Array.isArray(request._automaticOutputFields) ? request._automaticOutputFields : [];
6
+ const automatic = new Set(configured.filter((field) => PRODUCT_OUTPUT_FIELDS.includes(field)));
7
+ const result = { ...request };
8
+ delete result._automaticOutputFields;
9
+ if (result.overwrite === true || automatic.size === 0)
10
+ return result;
11
+ for (let sequence = 1; sequence < 10_000; sequence += 1) {
12
+ const candidates = new Map();
13
+ for (const field of automatic) {
14
+ const value = result[field];
15
+ if (typeof value === "string")
16
+ candidates.set(field, numberedPath(value, field, sequence));
17
+ }
18
+ const occupied = await Promise.all([...candidates.values()].map(exists));
19
+ if (occupied.some(Boolean))
20
+ continue;
21
+ for (const [field, value] of candidates)
22
+ result[field] = value;
23
+ return result;
24
+ }
25
+ throw new Error("Could not find an available automatic output name after 9999 attempts.");
26
+ }
27
+ function numberedPath(value, field, sequence) {
28
+ if (sequence === 1)
29
+ return value;
30
+ if (field === "outputDir")
31
+ return `${value}-${sequence}`;
32
+ const parsed = path.parse(value);
33
+ return path.join(parsed.dir, `${parsed.name}-${sequence}${parsed.ext}`);
34
+ }
35
+ async function exists(value) {
36
+ try {
37
+ await fs.lstat(value);
38
+ return true;
39
+ }
40
+ catch {
41
+ return false;
42
+ }
43
+ }
@@ -1,12 +1,20 @@
1
1
  import path from "node:path";
2
2
  import { findCameraPreset, loadCameraPresets } from "./camera-presets.js";
3
3
  import { findMaterialPreset, loadMaterialPresets } from "./presets.js";
4
+ import { applyRenderQuality } from "./quality.js";
4
5
  export async function prepareProductRenderRequest(config, input) {
5
6
  const sourcePath = input.modelPath ?? input.scenePath;
6
7
  const stem = safeStem(sourcePath);
7
8
  const format = input.format ?? "png";
8
9
  const renderMode = input.renderMode ?? "single";
9
- const request = {
10
+ const automaticOutputFields = [];
11
+ if (!input.outputScenePath)
12
+ automaticOutputFields.push("outputScenePath");
13
+ if (renderMode === "single" && !input.outputPath)
14
+ automaticOutputFields.push("outputPath");
15
+ if (renderMode === "allCameras" && !input.outputDir)
16
+ automaticOutputFields.push("outputDir");
17
+ const request = applyRenderQuality({
10
18
  ...input,
11
19
  operation: "product_render",
12
20
  renderMode,
@@ -14,7 +22,8 @@ export async function prepareProductRenderRequest(config, input) {
14
22
  outputScenePath: input.outputScenePath ?? `${stem}-product.bip`,
15
23
  overwrite: input.overwrite ?? false,
16
24
  continueOnError: input.continueOnError ?? true,
17
- };
25
+ _automaticOutputFields: automaticOutputFields,
26
+ }, "standard");
18
27
  if (renderMode === "single") {
19
28
  request.outputPath = input.outputPath ?? `${stem}-product.${format}`;
20
29
  delete request.outputDir;
@@ -0,0 +1,24 @@
1
+ export const QUALITY_PRESETS = {
2
+ preview: { width: 960, height: 540, samples: 16 },
3
+ standard: { width: 1920, height: 1080, samples: 64 },
4
+ final: { width: 3840, height: 2160, samples: 256 },
5
+ };
6
+ export function applyRenderQuality(input, defaultPreset) {
7
+ const selected = input.qualityPreset ?? defaultPreset;
8
+ if (!selected)
9
+ return { ...input };
10
+ const preset = QUALITY_PRESETS[selected];
11
+ const result = {
12
+ ...input,
13
+ qualityPreset: selected,
14
+ width: input.width ?? preset.width,
15
+ height: input.height ?? preset.height,
16
+ };
17
+ if (input.maxTimeSeconds !== undefined) {
18
+ delete result.samples;
19
+ }
20
+ else {
21
+ result.samples = input.samples ?? preset.samples;
22
+ }
23
+ return result;
24
+ }
package/dist/result.js CHANGED
@@ -10,6 +10,7 @@ export function toolResponse(result) {
10
10
  };
11
11
  }
12
12
  export function localFailure(error, extra) {
13
+ const guidance = classifyError(error);
13
14
  return {
14
15
  ok: false,
15
16
  data: extra?.data ?? null,
@@ -17,5 +18,81 @@ export function localFailure(error, extra) {
17
18
  warnings: extra?.warnings ?? [],
18
19
  keyshotStdoutTail: extra?.keyshotStdoutTail ?? "",
19
20
  error,
21
+ errorCode: extra?.errorCode ?? guidance.errorCode,
22
+ suggestions: extra?.suggestions ?? guidance.suggestions,
23
+ };
24
+ }
25
+ export function withErrorGuidance(result) {
26
+ if (result.ok || !result.error)
27
+ return result;
28
+ const guidance = classifyError(result.error);
29
+ return {
30
+ ...result,
31
+ errorCode: result.errorCode ?? guidance.errorCode,
32
+ suggestions: result.suggestions ?? guidance.suggestions,
33
+ };
34
+ }
35
+ function classifyError(error) {
36
+ const normalized = error.toLowerCase();
37
+ if (normalized.includes("headless executable not found") ||
38
+ (normalized.includes("path command") && normalized.includes("not found"))) {
39
+ return {
40
+ errorCode: "KEYSHOT_NOT_FOUND",
41
+ suggestions: ["Set KEYSHOT_HEADLESS_EXE to the full path of keyshot_headless.exe, then restart the MCP client."],
42
+ };
43
+ }
44
+ if (normalized.includes("license") || normalized.includes("activation")) {
45
+ return {
46
+ errorCode: "LICENSE_UNAVAILABLE",
47
+ suggestions: ["Open KeyShot normally and confirm that its local license is active, then retry."],
48
+ };
49
+ }
50
+ if (normalized.includes("could not start keyshot")) {
51
+ return {
52
+ errorCode: "KEYSHOT_START_FAILED",
53
+ suggestions: ["Check the KeyShot executable path and start KeyShot normally once before retrying headless mode."],
54
+ };
55
+ }
56
+ if (normalized.includes("timed out")) {
57
+ return {
58
+ errorCode: "KEYSHOT_TIMEOUT",
59
+ suggestions: ["Try the preview quality preset or increase KEYSHOT_TIMEOUT_MS for a long render."],
60
+ };
61
+ }
62
+ if (normalized.includes("output already exists") || normalized.includes("overwrite is false")) {
63
+ return {
64
+ errorCode: "OUTPUT_EXISTS",
65
+ suggestions: ["Choose another explicit output name or set overwrite=true only when replacement is intentional."],
66
+ };
67
+ }
68
+ if (normalized.includes("output path must stay inside")) {
69
+ return {
70
+ errorCode: "OUTPUT_OUTSIDE_ALLOWED_DIRECTORY",
71
+ suggestions: ["Use a path inside KEYSHOT_OUTPUT_DIR, or explicitly enable external outputs if that location is trusted."],
72
+ };
73
+ }
74
+ if (normalized.includes("scene file not found") ||
75
+ normalized.includes("model file not found") ||
76
+ normalized.includes("base scene file not found")) {
77
+ return {
78
+ errorCode: "INPUT_NOT_FOUND",
79
+ suggestions: ["Check that the input file still exists and provide its full local path."],
80
+ };
81
+ }
82
+ if (normalized.includes("preset not found")) {
83
+ return {
84
+ errorCode: "PRESET_NOT_FOUND",
85
+ suggestions: ["List the available presets first, then retry with one of the returned names."],
86
+ };
87
+ }
88
+ if (normalized.includes("unsupported")) {
89
+ return {
90
+ errorCode: "UNSUPPORTED_KEYSHOT_API",
91
+ suggestions: ["This KeyShot version does not expose the required headless API; use a supported option or verify a newer KeyShot release."],
92
+ };
93
+ }
94
+ return {
95
+ errorCode: "KEYSHOT_OPERATION_FAILED",
96
+ suggestions: ["Review the error and KeyShot output, then retry with simpler settings or run keyshot_status."],
20
97
  };
21
98
  }
package/dist/runner.js CHANGED
@@ -2,8 +2,9 @@ import { spawn } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
- import { localFailure } from "./result.js";
5
+ import { localFailure, withErrorGuidance } from "./result.js";
6
6
  import { normalizeOutputPaths } from "./output-paths.js";
7
+ import { allocateAutomaticProductOutputs } from "./output-collisions.js";
7
8
  let queue = Promise.resolve();
8
9
  export function runKeyShotSerialized(config, request) {
9
10
  const run = queue.then(() => runKeyShot(config, request));
@@ -26,6 +27,9 @@ async function runKeyShot(config, request) {
26
27
  let normalizedRequest;
27
28
  try {
28
29
  normalizedRequest = await normalizeOutputPaths(config, request);
30
+ if (normalizedRequest.operation === "product_render") {
31
+ normalizedRequest = await allocateAutomaticProductOutputs(normalizedRequest);
32
+ }
29
33
  }
30
34
  catch (error) {
31
35
  return localFailure(errorMessage(error));
@@ -105,13 +109,13 @@ async function runKeyShot(config, request) {
105
109
  if (stderrTail)
106
110
  parsed.warnings = [...parsed.warnings, `stderr: ${stderrTail}`];
107
111
  if (processResult.exitCode !== 0 && parsed.ok) {
108
- return {
112
+ return withErrorGuidance({
109
113
  ...parsed,
110
114
  ok: false,
111
115
  error: `KeyShot exited with code ${processResult.exitCode}`,
112
- };
116
+ });
113
117
  }
114
- return parsed;
118
+ return withErrorGuidance(parsed);
115
119
  }
116
120
  finally {
117
121
  // Best-effort cleanup so work/tmp does not accumulate args/result files.
package/dist/schemas.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  const optionalPath = z.string().min(1).optional();
3
3
  const imageFormat = z.enum(["png", "jpg", "jpeg", "tif", "tiff", "exr"]);
4
+ export const qualityPresetSchema = z.enum(["preview", "standard", "final"]);
4
5
  export const scenePathSchema = z.object({
5
6
  scenePath: z.string().min(1),
6
7
  });
@@ -16,6 +17,7 @@ export const renderSchema = z.object({
16
17
  maxTimeSeconds: z.number().positive().optional(),
17
18
  camera: z.string().min(1).optional(),
18
19
  format: z.enum(["png", "jpg", "jpeg", "tif", "tiff", "exr"]).optional(),
20
+ qualityPreset: qualityPresetSchema.optional(),
19
21
  });
20
22
  export const renderInputSchema = renderSchema.refine((value) => !(value.samples !== undefined && value.maxTimeSeconds !== undefined), {
21
23
  message: "Choose either samples or maxTimeSeconds, not both.",
@@ -30,6 +32,7 @@ export const batchRenderSchema = z.object({
30
32
  samples: z.number().int().positive().optional(),
31
33
  maxTimeSeconds: z.number().positive().optional(),
32
34
  format: z.enum(["png", "jpg", "jpeg", "tif", "tiff", "exr"]).optional(),
35
+ qualityPreset: qualityPresetSchema.optional(),
33
36
  overwrite: z.boolean().optional(),
34
37
  });
35
38
  export const batchRenderInputSchema = batchRenderSchema.refine((value) => !(value.samples !== undefined && value.maxTimeSeconds !== undefined), {
@@ -44,6 +47,7 @@ export const renderAllCamerasSchema = z.object({
44
47
  samples: z.number().int().positive().optional(),
45
48
  maxTimeSeconds: z.number().positive().optional(),
46
49
  format: imageFormat.optional(),
50
+ qualityPreset: qualityPresetSchema.optional(),
47
51
  overwrite: z.boolean().optional(),
48
52
  continueOnError: z.boolean().default(true),
49
53
  });
@@ -123,6 +127,7 @@ export const productRenderInputSchema = z.object({
123
127
  samples: z.number().int().positive().optional(),
124
128
  maxTimeSeconds: z.number().positive().optional(),
125
129
  format: imageFormat.optional(),
130
+ qualityPreset: qualityPresetSchema.optional(),
126
131
  overwrite: z.boolean().default(false),
127
132
  continueOnError: z.boolean().default(true),
128
133
  });
@@ -218,6 +223,7 @@ export const renderJobSchema = z.object({
218
223
  samples: z.number().int().positive().optional(),
219
224
  maxTimeSeconds: z.number().positive().optional(),
220
225
  format: imageFormat.optional(),
226
+ qualityPreset: qualityPresetSchema.optional(),
221
227
  });
222
228
  export const renderJobInputSchema = renderJobSchema.refine((value) => !(value.samples !== undefined && value.maxTimeSeconds !== undefined), {
223
229
  message: "Choose either samples or maxTimeSeconds, not both.",
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.8.0";
1
+ export const VERSION = "0.9.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keyshot-mcp",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "mcpName": "io.github.truman-t3/keyshot-mcp",
5
5
  "description": "Local MCP server for KeyShot product visualization, scene editing, camera control, and rendering.",
6
6
  "type": "module",
@@ -22,7 +22,7 @@
22
22
  "check": "tsc -p tsconfig.json --noEmit",
23
23
  "test": "vitest run",
24
24
  "status": "node ./dist/cli/status.js",
25
- "smoke:keyshot": "npm run build && node ./scripts/keyshot-smoke.mjs"
25
+ "smoke:keyshot": "tsc -p tsconfig.json && node ./scripts/keyshot-smoke.mjs"
26
26
  },
27
27
  "keywords": [
28
28
  "mcp",
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { findCameraPreset, loadCameraPresets } from "../dist/camera-presets.js";
6
6
  import { getConfig } from "../dist/config.js";
7
+ import { runKeyShotDiagnostics } from "../dist/diagnostics.js";
7
8
  import { prepareProductRenderRequest } from "../dist/product-render.js";
8
9
  import { runKeyShotSerialized } from "../dist/runner.js";
9
10
 
@@ -37,8 +38,14 @@ async function applyStandardPreset(label, scenePath, presetName, outputScenePath
37
38
  });
38
39
  }
39
40
 
40
- const status = await run("1/13 KeyShot status", { operation: "status" });
41
- const imported = await run("2/13 Import, center, and ground generated OBJ", {
41
+ process.stdout.write("1/14 KeyShot diagnostics... ");
42
+ const status = await runKeyShotDiagnostics(config);
43
+ if (!status.ok || !status.data?.ready) {
44
+ process.stdout.write("failed\n");
45
+ throw new Error(`KeyShot diagnostics: ${status.error}\n${(status.suggestions ?? []).join("\n")}`);
46
+ }
47
+ process.stdout.write("ok\n");
48
+ const imported = await run("2/14 Import, center, and ground generated OBJ", {
42
49
  operation: "import_model",
43
50
  modelPath,
44
51
  outputScenePath: "demo/keyshot-mcp-demo-import.bip",
@@ -50,7 +57,7 @@ const imported = await run("2/13 Import, center, and ground generated OBJ", {
50
57
  const importedScene = imported.outputFiles[0];
51
58
  if (!importedScene) throw new Error("Import did not return a saved scene path.");
52
59
 
53
- const inspected = await run("3/13 Inspect imported scene", {
60
+ const inspected = await run("3/14 Inspect imported scene", {
54
61
  operation: "inspect_scene",
55
62
  scenePath: importedScene,
56
63
  });
@@ -60,7 +67,7 @@ if (!Array.isArray(objects) || objects.length === 0) {
60
67
  }
61
68
 
62
69
  const firstCameraResult = await applyStandardPreset(
63
- "4/13 Apply Front camera preset",
70
+ "4/14 Apply Front camera preset",
64
71
  importedScene,
65
72
  "Front",
66
73
  "demo/keyshot-mcp-demo-preset-front.bip",
@@ -68,7 +75,7 @@ const firstCameraResult = await applyStandardPreset(
68
75
  const firstCameraScene = firstCameraResult.outputFiles[0];
69
76
  if (!firstCameraScene) throw new Error("Front preset did not return a saved scene path.");
70
77
 
71
- const focalLengthResult = await run("5/13 Set Front camera focal length", {
78
+ const focalLengthResult = await run("5/14 Set Front camera focal length", {
72
79
  operation: "set_camera",
73
80
  scenePath: firstCameraScene,
74
81
  cameraName: "Front",
@@ -78,7 +85,7 @@ const focalLengthResult = await run("5/13 Set Front camera focal length", {
78
85
  const focalLengthScene = focalLengthResult.outputFiles[0];
79
86
  if (!focalLengthScene) throw new Error("Focal-length update did not return a saved scene path.");
80
87
 
81
- const environmentResult = await run("6/13 Rotate the active environment", {
88
+ const environmentResult = await run("6/14 Rotate the active environment", {
82
89
  operation: "set_environment",
83
90
  scenePath: focalLengthScene,
84
91
  rotation: 45,
@@ -88,7 +95,7 @@ const environmentScene = environmentResult.outputFiles[0];
88
95
  if (!environmentScene) throw new Error("Environment update did not return a saved scene path.");
89
96
 
90
97
  const secondCameraResult = await applyStandardPreset(
91
- "7/13 Apply Isometric camera preset",
98
+ "7/14 Apply Isometric camera preset",
92
99
  environmentScene,
93
100
  "Isometric",
94
101
  "demo/keyshot-mcp-demo-presets.bip",
@@ -96,7 +103,7 @@ const secondCameraResult = await applyStandardPreset(
96
103
  const cameraScene = secondCameraResult.outputFiles[0];
97
104
  if (!cameraScene) throw new Error("Isometric preset did not return a saved scene path.");
98
105
 
99
- const fieldOfViewResult = await run("8/13 Set Isometric camera field of view and distance", {
106
+ const fieldOfViewResult = await run("8/14 Set Isometric camera field of view and distance", {
100
107
  operation: "set_camera",
101
108
  scenePath: cameraScene,
102
109
  cameraName: "Isometric",
@@ -107,7 +114,7 @@ const fieldOfViewResult = await run("8/13 Set Isometric camera field of view and
107
114
  const productCameraScene = fieldOfViewResult.outputFiles[0];
108
115
  if (!productCameraScene) throw new Error("Field-of-view update did not return a saved scene path.");
109
116
 
110
- const rendered = await run("9/13 Discover and render every camera", {
117
+ const rendered = await run("9/14 Discover and render every camera", {
111
118
  operation: "render_all_cameras",
112
119
  scenePath: productCameraScene,
113
120
  outputDir: "demo/all-cameras",
@@ -140,26 +147,35 @@ const minimumSizes = await Promise.all(namedResults.map(async (entry) => (await
140
147
  if (minimumSizes.some((size) => size < 10000)) {
141
148
  throw new Error("A demo camera render is unexpectedly small and may be blank.");
142
149
  }
143
- process.stdout.write("10/13 Verify two different camera images... ok\n");
144
- process.stdout.write("11/13 Verify composition, lens, and environment operations... ok\n");
150
+ process.stdout.write("10/14 Verify two different camera images... ok\n");
151
+ process.stdout.write("11/14 Verify composition, lens, and environment operations... ok\n");
145
152
 
146
153
  const oneClickModelRequest = await prepareProductRenderRequest(config, {
147
154
  modelPath,
148
- outputScenePath: "demo/one-click-cube.bip",
149
- outputPath: "demo/one-click-cube.png",
150
155
  renderMode: "single",
151
- width: 640,
152
- height: 480,
153
- maxTimeSeconds: 8,
156
+ qualityPreset: "standard",
154
157
  format: "png",
155
- overwrite: true,
158
+ overwrite: false,
156
159
  continueOnError: true,
157
160
  });
158
- const oneClickModel = await run("12/13 One-click model-to-product render", oneClickModelRequest);
161
+ const oneClickModel = await run("12/14 Standard-quality one-click product render", oneClickModelRequest);
159
162
  if (!oneClickModel.data?.savedScene || oneClickModel.data?.renders?.length !== 1) {
160
163
  throw new Error("One-click model workflow did not return a scene and one render.");
161
164
  }
162
165
 
166
+ const repeatedRequest = await prepareProductRenderRequest(config, {
167
+ modelPath,
168
+ renderMode: "single",
169
+ qualityPreset: "preview",
170
+ format: "png",
171
+ overwrite: false,
172
+ continueOnError: true,
173
+ });
174
+ const repeatedModel = await run("13/14 Repeated preview render with automatic numbering", repeatedRequest);
175
+ if (!repeatedModel.data?.savedScene || repeatedModel.data?.savedScene === oneClickModel.data.savedScene) {
176
+ throw new Error("Repeated one-click render did not allocate a new numbered scene path.");
177
+ }
178
+
163
179
  const oneClickSceneRequest = await prepareProductRenderRequest(config, {
164
180
  scenePath: productCameraScene,
165
181
  outputScenePath: "demo/one-click-existing-scene.bip",
@@ -172,7 +188,7 @@ const oneClickSceneRequest = await prepareProductRenderRequest(config, {
172
188
  overwrite: true,
173
189
  continueOnError: true,
174
190
  });
175
- const oneClickScene = await run("13/13 One-click existing-scene all-camera render", oneClickSceneRequest);
191
+ const oneClickScene = await run("14/14 One-click existing-scene all-camera render", oneClickSceneRequest);
176
192
  if (!oneClickScene.data?.savedScene || oneClickScene.data?.renders?.length < 2) {
177
193
  throw new Error("One-click scene workflow did not preserve the scene and render all cameras.");
178
194
  }
@@ -183,7 +199,9 @@ console.log(JSON.stringify({
183
199
  scenePath: productCameraScene,
184
200
  cameraCount: renderData.total,
185
201
  renderedImages: rendered.outputFiles,
186
- oneClickOutputs: [...oneClickModel.outputFiles, ...oneClickScene.outputFiles],
202
+ oneClickOutputs: [...oneClickModel.outputFiles, ...repeatedModel.outputFiles, ...oneClickScene.outputFiles],
203
+ diagnosticChecks: status.data?.checks ?? [],
204
+ verifiedQualities: ["standard", "preview"],
187
205
  verifiedPresets: namedResults.map((entry) => entry.camera),
188
206
  verifiedControls: {
189
207
  importOptions: ["centerGeometry", "snapToGround", "adjustCameraLookAt", "adjustEnvironment"],
@@ -181,6 +181,7 @@ def render(payload, output_files, warnings):
181
181
  "height": height,
182
182
  "samples": samples,
183
183
  "maxTimeSeconds": max_time,
184
+ "qualityPreset": payload.get("qualityPreset"),
184
185
  }
185
186
 
186
187
 
package/server.json CHANGED
@@ -9,12 +9,12 @@
9
9
  "url": "https://github.com/truman-t3/keyshot-mcp",
10
10
  "source": "github"
11
11
  },
12
- "version": "0.8.0",
12
+ "version": "0.9.0",
13
13
  "packages": [
14
14
  {
15
15
  "registryType": "npm",
16
16
  "identifier": "keyshot-mcp",
17
- "version": "0.8.0",
17
+ "version": "0.9.0",
18
18
  "transport": {
19
19
  "type": "stdio"
20
20
  },