keyshot-mcp 0.7.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
@@ -28,6 +28,7 @@ the local computer.
28
28
  - Apply materials directly or through a local material preset library.
29
29
  - Select environments, change brightness, and rotate the active environment.
30
30
  - Save edited scenes to a controlled output directory.
31
+ - Prepare and render a product from a model or existing scene in one tool call.
31
32
 
32
33
  ### Requirements and compatibility
33
34
 
@@ -40,7 +41,7 @@ the local computer.
40
41
 
41
42
  ### Install
42
43
 
43
- The current release is `0.7.0`.
44
+ The current release is `0.9.0`.
44
45
 
45
46
  #### Option 1: run with npx
46
47
 
@@ -51,7 +52,7 @@ This requires no global npm installation:
51
52
  "mcpServers": {
52
53
  "keyshot": {
53
54
  "command": "npx",
54
- "args": ["-y", "keyshot-mcp@0.7.0"],
55
+ "args": ["-y", "keyshot-mcp@0.9.0"],
55
56
  "env": {
56
57
  "KEYSHOT_HEADLESS_EXE": "C:/Program Files/KeyShot Studio/bin/keyshot_headless.exe"
57
58
  }
@@ -63,7 +64,7 @@ This requires no global npm installation:
63
64
  #### Option 2: install globally
64
65
 
65
66
  ```bash
66
- npm install -g keyshot-mcp@0.7.0
67
+ npm install -g keyshot-mcp@0.9.0
67
68
  ```
68
69
 
69
70
  ```json
@@ -112,14 +113,14 @@ The following prompt can be pasted into a coding agent that is allowed to edit
112
113
  the MCP client configuration:
113
114
 
114
115
  ```text
115
- Install KeyShot MCP 0.7.0 and configure it in my MCP client.
116
+ Install KeyShot MCP 0.9.0 and configure it in my MCP client.
116
117
 
117
118
  1. Find the local KeyShot headless executable.
118
119
  2. Add an MCP server named "keyshot" that runs:
119
- npx -y keyshot-mcp@0.7.0
120
+ npx -y keyshot-mcp@0.9.0
120
121
  3. Set KEYSHOT_HEADLESS_EXE to the executable path.
121
122
  4. Keep KEYSHOT_ALLOW_EXTERNAL_OUTPUTS disabled.
122
- 5. Restart or reload the MCP client, then call keyshot_status.
123
+ 5. Restart or reload the MCP client, call keyshot_status, then use keyshot_product_render for a one-click product render.
123
124
  6. Do not upload or publish any KeyShot scenes, models, renders, or license data.
124
125
  ```
125
126
 
@@ -128,7 +129,7 @@ Install KeyShot MCP 0.7.0 and configure it in my MCP client.
128
129
  | Variable | Default | Purpose |
129
130
  | --- | --- | --- |
130
131
  | `KEYSHOT_HEADLESS_EXE` | `keyshot_headless.exe` on Windows | Absolute executable path or a command available on `PATH`. |
131
- | `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. |
132
133
  | `KEYSHOT_ALLOW_EXTERNAL_OUTPUTS` | `false` | Allows output paths outside `KEYSHOT_OUTPUT_DIR` when explicitly set to `true`. |
133
134
  | `KEYSHOT_TIMEOUT_MS` | `600000` | Timeout for one KeyShot headless process. |
134
135
  | `KEYSHOT_LICENSE_ARGS` | empty | Optional additional KeyShot launch arguments. |
@@ -139,11 +140,17 @@ Relative output paths are resolved inside `KEYSHOT_OUTPUT_DIR`. Parent traversal
139
140
  adjacent-prefix paths, and symbolic-link escapes are rejected by default. Input
140
141
  scene, model, material, and environment paths may be located elsewhere.
141
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
+
142
148
  ### Tools
143
149
 
144
150
  | Tool | Purpose |
145
151
  | --- | --- |
146
- | `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. |
153
+ | `keyshot_product_render` | Prepare, save, and render a model or existing scene in one headless process. |
147
154
  | `keyshot_inspect_scene` | Inspect objects, cameras, materials, model sets, and references. |
148
155
  | `keyshot_list_cameras` | List available camera names. |
149
156
  | `keyshot_render` | Render one image. |
@@ -160,9 +167,61 @@ scene, model, material, and environment paths may be located elsewhere.
160
167
  | `keyshot_set_environment` | Select or adjust an environment, brightness, or rotation. |
161
168
  | `keyshot_save_scene` | Save a scene to a new output path. |
162
169
 
163
- The server also exposes a `keyshot_product_render` MCP prompt and a
170
+ The server exposes 17 tools, a `keyshot_product_render` MCP prompt, and a
164
171
  `keyshot://workflow` resource.
165
172
 
173
+ ### One-click product render
174
+
175
+ In an MCP client, a designer can use a natural-language request:
176
+
177
+ ```text
178
+ Turn C:\models\speaker.obj into a product render. Center it, place it on the
179
+ ground, use the Isometric camera preset, save the KeyShot scene, and render a
180
+ 1600 x 1200 PNG with 128 samples.
181
+ ```
182
+
183
+ The `keyshot_product_render` tool accepts either `modelPath` or `scenePath`. New
184
+ models default to centered and grounded geometry with a `Product Hero`
185
+ isometric camera. Existing scenes keep their current camera, materials, and
186
+ environment unless explicit changes are requested.
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
+
201
+ ```json
202
+ {
203
+ "modelPath": "C:/models/speaker.obj",
204
+ "outputScenePath": "speaker-product.bip",
205
+ "outputPath": "speaker-product.png",
206
+ "materialAssignments": [
207
+ { "objectName": "Body", "presetName": "Brushed Steel" }
208
+ ],
209
+ "cameraPresetName": "Isometric",
210
+ "focalLength": 55,
211
+ "brightness": 1.2,
212
+ "rotation": 45,
213
+ "qualityPreset": "standard"
214
+ }
215
+ ```
216
+
217
+ Use `renderMode: "allCameras"` with `outputDir` to render every named camera.
218
+ Generated names are derived from the source filename when output paths are
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
223
+ explicit object and never overwrite the entire model implicitly.
224
+
166
225
  ### Product composition examples
167
226
 
168
227
  Import a model and prepare its initial composition:
@@ -222,7 +281,8 @@ them.
222
281
  The repository includes a smoke test built from generated cube geometry in
223
282
  `examples/demo`. It verifies startup, import composition, scene inspection,
224
283
  camera presets, focal length, field of view, camera distance, environment
225
- rotation, scene saving, camera discovery, and real PNG rendering:
284
+ rotation, scene saving, camera discovery, one-click model rendering, one-click
285
+ existing-scene rendering, and real PNG output:
226
286
 
227
287
  ```bash
228
288
  npm run smoke:keyshot
@@ -254,18 +314,6 @@ Linux.
254
314
  - Add depth-of-field and additional lens controls when stable headless APIs are
255
315
  available.
256
316
 
257
- ### Star history
258
-
259
- <picture>
260
- <source media="(prefers-color-scheme: dark)" srcset="assets/star-history-dark.svg">
261
- <source media="(prefers-color-scheme: light)" srcset="assets/star-history-light.svg">
262
- <img alt="GitHub star history for truman-t3/keyshot-mcp" src="assets/star-history-light.svg">
263
- </picture>
264
-
265
- The chart uses the actual `starred_at` date for each GitHub star and displays a
266
- cumulative step line. A weekly GitHub Action updates it only when detailed
267
- stargazer dates are available.
268
-
269
317
  ### License and security
270
318
 
271
319
  Released under the [MIT License](LICENSE). See [SECURITY.md](SECURITY.md) for
@@ -273,6 +321,18 @@ security guidance and [CONTRIBUTING.md](CONTRIBUTING.md) for development notes.
273
321
  Do not commit KeyShot licenses, private scenes, customer assets, or unpublished
274
322
  renders.
275
323
 
324
+ ### Trademark and project status
325
+
326
+ KeyShot is a trademark of KeyShot ApS and/or KeyShot Inc. This is an independent,
327
+ open-source community project and is not affiliated with, endorsed by, or
328
+ sponsored by KeyShot.
329
+
330
+ This project provides only an MCP integration. It does not include KeyShot
331
+ Studio, KeyShot assets, or a KeyShot license. Users must install and license
332
+ KeyShot Studio separately and comply with the applicable KeyShot terms. Do not
333
+ use this project to bypass licensing, share credentials, or redistribute KeyShot
334
+ software or proprietary resources.
335
+
276
336
  ---
277
337
 
278
338
  ## 中文
@@ -291,6 +351,7 @@ headless 脚本让兼容 MCP 的 AI Agent 检查、编辑和渲染 KeyShot 场
291
351
  - 直接应用材质,或使用本地材质预设库。
292
352
  - 选择环境、调整亮度并旋转当前环境。
293
353
  - 将修改后的场景保存到受控输出目录。
354
+ - 通过一次工具调用完成模型或现有场景的产品构图与渲染。
294
355
 
295
356
  ### 运行要求与兼容性
296
357
 
@@ -302,7 +363,7 @@ headless 脚本让兼容 MCP 的 AI Agent 检查、编辑和渲染 KeyShot 场
302
363
 
303
364
  ### 安装
304
365
 
305
- 当前版本为 `0.7.0`。
366
+ 当前版本为 `0.9.0`。
306
367
 
307
368
  #### 方式一:使用 npx
308
369
 
@@ -313,7 +374,7 @@ headless 脚本让兼容 MCP 的 AI Agent 检查、编辑和渲染 KeyShot 场
313
374
  "mcpServers": {
314
375
  "keyshot": {
315
376
  "command": "npx",
316
- "args": ["-y", "keyshot-mcp@0.7.0"],
377
+ "args": ["-y", "keyshot-mcp@0.9.0"],
317
378
  "env": {
318
379
  "KEYSHOT_HEADLESS_EXE": "C:/Program Files/KeyShot Studio/bin/keyshot_headless.exe"
319
380
  }
@@ -325,7 +386,7 @@ headless 脚本让兼容 MCP 的 AI Agent 检查、编辑和渲染 KeyShot 场
325
386
  #### 方式二:全局安装
326
387
 
327
388
  ```bash
328
- npm install -g keyshot-mcp@0.7.0
389
+ npm install -g keyshot-mcp@0.9.0
329
390
  ```
330
391
 
331
392
  ```json
@@ -373,14 +434,14 @@ pnpm build
373
434
  下面的提示词适用于有权限修改 MCP 客户端配置的编程 Agent:
374
435
 
375
436
  ```text
376
- 请帮我安装 KeyShot MCP 0.7.0,并添加到我的 MCP 客户端。
437
+ 请帮我安装 KeyShot MCP 0.9.0,并添加到我的 MCP 客户端。
377
438
 
378
439
  1. 查找本机 KeyShot headless 可执行文件。
379
440
  2. 添加名为 keyshot 的 MCP server,运行:
380
- npx -y keyshot-mcp@0.7.0
441
+ npx -y keyshot-mcp@0.9.0
381
442
  3. 将 KEYSHOT_HEADLESS_EXE 设置为可执行文件路径。
382
443
  4. 保持 KEYSHOT_ALLOW_EXTERNAL_OUTPUTS 关闭。
383
- 5. 重启或重新加载 MCP 客户端,然后调用 keyshot_status
444
+ 5. 重启或重新加载 MCP 客户端,调用 keyshot_status,然后使用 keyshot_product_render 一键完成产品出图。
384
445
  6. 不要上传或发布任何 KeyShot 场景、模型、渲染图或许可证数据。
385
446
  ```
386
447
 
@@ -389,7 +450,7 @@ pnpm build
389
450
  | 环境变量 | 默认值 | 用途 |
390
451
  | --- | --- | --- |
391
452
  | `KEYSHOT_HEADLESS_EXE` | Windows 上为 `keyshot_headless.exe` | KeyShot headless 绝对路径,或系统 `PATH` 中的命令。 |
392
- | `KEYSHOT_OUTPUT_DIR` | 包内 `outputs` | 渲染图和已保存场景的根目录。 |
453
+ | `KEYSHOT_OUTPUT_DIR` | `~/Documents/KeyShot MCP Outputs` | 渲染图和已保存场景的根目录。 |
393
454
  | `KEYSHOT_ALLOW_EXTERNAL_OUTPUTS` | `false` | 明确设为 `true` 时允许写入输出根目录之外。 |
394
455
  | `KEYSHOT_TIMEOUT_MS` | `600000` | 单个 KeyShot headless 进程的超时时间。 |
395
456
  | `KEYSHOT_LICENSE_ARGS` | 空 | 可选的 KeyShot 启动参数。 |
@@ -399,11 +460,16 @@ pnpm build
399
460
  相对输出路径会自动放入 `KEYSHOT_OUTPUT_DIR`。默认拒绝 `..`、相邻同名前缀目录和
400
461
  软链接逃逸。输入场景、模型、材质和环境文件可位于其他目录。
401
462
 
463
+ 安装后调用 `keyshot_status`,可检查 MCP 与 KeyShot 版本、实际可执行文件路径、
464
+ 输出目录写入权限、预设文件和 headless 启动状态,并获得可直接执行的修复建议。
465
+ 诊断结果不会回显额外的 KeyShot 启动参数。
466
+
402
467
  ### MCP 工具
403
468
 
404
469
  | 工具 | 用途 |
405
470
  | --- | --- |
406
- | `keyshot_status` | 检查 KeyShot headless 是否可启动并读取版本。 |
471
+ | `keyshot_status` | 诊断本地配置、输出权限、预设文件和 KeyShot headless 启动状态。 |
472
+ | `keyshot_product_render` | 在一个 headless 进程中完成模型或场景准备、保存和渲染。 |
407
473
  | `keyshot_inspect_scene` | 检查对象、相机、材质、模型集和外部引用。 |
408
474
  | `keyshot_list_cameras` | 列出场景中的相机名称。 |
409
475
  | `keyshot_render` | 渲染一张图片。 |
@@ -420,7 +486,54 @@ pnpm build
420
486
  | `keyshot_set_environment` | 选择环境并调整亮度或旋转角度。 |
421
487
  | `keyshot_save_scene` | 将场景保存到新的输出路径。 |
422
488
 
423
- 服务还提供 `keyshot_product_render` MCP 提示词和 `keyshot://workflow` 资源。
489
+ 服务共提供 17 个工具,并提供 `keyshot_product_render` MCP 提示词和
490
+ `keyshot://workflow` 资源。
491
+
492
+ ### 一键产品出图
493
+
494
+ 设计师可以直接在支持 MCP 的 Agent 中描述需求:
495
+
496
+ ```text
497
+ 把 C:\models\speaker.obj 做成产品渲染图。自动居中、贴地,使用 Isometric
498
+ 相机预设,保存 KeyShot 场景,并用 128 采样渲染一张 1600 x 1200 PNG。
499
+ ```
500
+
501
+ `keyshot_product_render` 可以接收 `modelPath` 或 `scenePath`。新模型默认自动居中、
502
+ 贴地并创建名为 `Product Hero` 的等轴测相机;已有场景默认保留当前相机、材质和环境,
503
+ 只有明确提供参数时才修改。
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
+
517
+ ```json
518
+ {
519
+ "modelPath": "C:/models/speaker.obj",
520
+ "outputScenePath": "speaker-product.bip",
521
+ "outputPath": "speaker-product.png",
522
+ "materialAssignments": [
523
+ { "objectName": "Body", "presetName": "Brushed Steel" }
524
+ ],
525
+ "cameraPresetName": "Isometric",
526
+ "focalLength": 55,
527
+ "brightness": 1.2,
528
+ "rotation": 45,
529
+ "qualityPreset": "standard"
530
+ }
531
+ ```
532
+
533
+ 将 `renderMode` 设为 `allCameras` 并提供 `outputDir`,即可渲染场景中的全部命名相机。
534
+ 省略输出路径时会根据源文件名自动生成。自动名称已存在时,场景和图片会统一增加
535
+ `-2`、`-3` 等编号;用户明确填写的路径仍会严格防止覆盖。只有明确设置
536
+ `overwrite: true` 才会替换。材质指定必须包含明确的对象,不会隐式覆盖整个模型。
424
537
 
425
538
  ### 产品构图示例
426
539
 
@@ -474,7 +587,7 @@ pnpm build
474
587
 
475
588
  仓库提供基于 `examples/demo` 生成立方体几何体的 smoke test,用于验证 KeyShot
476
589
  启动、导入构图、场景检查、相机预设、焦距、视野角、相机距离、环境旋转、场景保存、
477
- 相机发现和真实 PNG 渲染:
590
+ 相机发现、模型一键出图、现有场景一键渲染全部相机和真实 PNG 输出:
478
591
 
479
592
  ```bash
480
593
  npm run smoke:keyshot
@@ -503,19 +616,17 @@ CI 在 Windows 和 Ubuntu 上使用 Node.js 20、24 运行。Linux CI 验证 MCP
503
616
  - 验证 macOS 安装和 headless 行为。
504
617
  - 在 headless API 稳定支持后增加景深和更多镜头控制。
505
618
 
506
- ### 星标趋势
507
-
508
- <picture>
509
- <source media="(prefers-color-scheme: dark)" srcset="assets/star-history-dark.svg">
510
- <source media="(prefers-color-scheme: light)" srcset="assets/star-history-light.svg">
511
- <img alt="truman-t3/keyshot-mcp 的 GitHub 星标趋势" src="assets/star-history-light.svg">
512
- </picture>
513
-
514
- 趋势图使用每颗 GitHub Star 的真实 `starred_at` 日期绘制累计阶梯线。GitHub Actions
515
- 每周检查一次,并且只在能够取得逐星日期时更新。
516
-
517
619
  ### 许可证与安全
518
620
 
519
621
  项目采用 [MIT License](LICENSE)。安全说明见 [SECURITY.md](SECURITY.md),开发说明
520
622
  见 [CONTRIBUTING.md](CONTRIBUTING.md)。请勿提交 KeyShot 许可证、私有场景、客户素材
521
623
  或未公开渲染图。
624
+
625
+ ### 商标与项目性质
626
+
627
+ KeyShot 是 KeyShot ApS 和/或 KeyShot Inc. 的商标。本项目是独立的开源社区项目,
628
+ 与 KeyShot 官方无隶属、认可、赞助或其他合作关系。
629
+
630
+ 本项目仅提供 MCP 集成功能,不包含 KeyShot Studio、KeyShot 官方素材或 KeyShot
631
+ 许可证。用户必须自行安装并合法授权 KeyShot Studio,同时遵守适用的 KeyShot 条款。
632
+ 不得使用本项目绕过许可证、共享账号凭据,或重新分发 KeyShot 软件及其专有资源。
@@ -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
@@ -7,8 +7,11 @@ import { runKeyShotSerialized } from "./runner.js";
7
7
  import { runRenderQueue } from "./queue.js";
8
8
  import { loadMaterialPresets, findMaterialPreset } from "./presets.js";
9
9
  import { loadCameraPresets, findCameraPreset } from "./camera-presets.js";
10
+ import { prepareProductRenderRequest } from "./product-render.js";
10
11
  import { VERSION } from "./version.js";
11
- import { applyMaterialSchema, applyMaterialInputSchema, applyMaterialPresetInputSchema, applyMaterialPresetSchema, applyCameraPresetSchema, batchRenderSchema, batchRenderInputSchema, importModelSchema, listCamerasSchema, listMaterialPresetsSchema, listCameraPresetsSchema, renderQueueSchema, renderQueueInputSchema, renderAllCamerasSchema, renderAllCamerasInputSchema, renderSchema, renderInputSchema, saveSceneSchema, scenePathSchema, setCameraInputSchema, setCameraSchema, setEnvironmentSchema, } from "./schemas.js";
12
+ import { applyRenderQuality } from "./quality.js";
13
+ import { runKeyShotDiagnostics } from "./diagnostics.js";
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";
12
15
  const config = getConfig();
13
16
  const server = new McpServer({
14
17
  name: "keyshot-mcp",
@@ -39,6 +42,7 @@ server.registerPrompt("keyshot_product_render", {
39
42
  title: "Render a KeyShot product scene",
40
43
  description: "Create a practical prompt for rendering or batch-rendering a KeyShot product scene.",
41
44
  argsSchema: {
45
+ modelPath: scenePathSchema.shape.scenePath.optional(),
42
46
  scenePath: scenePathSchema.shape.scenePath.optional(),
43
47
  goal: scenePathSchema.shape.scenePath.optional(),
44
48
  },
@@ -50,32 +54,46 @@ server.registerPrompt("keyshot_product_render", {
50
54
  type: "text",
51
55
  text: [
52
56
  "Use KeyShot MCP to prepare a product render.",
53
- args.scenePath ? `Scene path: ${args.scenePath}` : "Ask me for the KeyShot scene path first.",
57
+ args.modelPath
58
+ ? `Model path: ${args.modelPath}`
59
+ : args.scenePath
60
+ ? `Scene path: ${args.scenePath}`
61
+ : "Ask me for either a model path or a KeyShot scene path first.",
54
62
  args.goal ? `Goal: ${args.goal}` : "Inspect the scene, choose a suitable camera, then render a PNG preview.",
55
- "Start with keyshot_status, then keyshot_inspect_scene, then render or batch render as needed.",
63
+ "Call the keyshot_product_render tool for the complete workflow. Use the lower-level tools only when individual steps need manual control.",
56
64
  ].join("\n"),
57
65
  },
58
66
  },
59
67
  ],
60
68
  }));
61
- 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)));
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) => {
71
+ try {
72
+ const parsed = productRenderSchema.parse(args);
73
+ const request = await prepareProductRenderRequest(config, parsed);
74
+ return toolResponse(await runKeyShotSerialized(config, request));
75
+ }
76
+ catch (error) {
77
+ return toolResponse(localFailure(errorMessage(error)));
78
+ }
79
+ });
62
80
  server.tool("keyshot_inspect_scene", "Open a KeyShot scene and return available objects, cameras, materials and scene metadata.", scenePathSchema.shape, async (args) => toolResponse(await runKeyShotSerialized(config, { operation: "inspect_scene", ...args })));
63
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 })));
64
82
  server.tool("keyshot_render", "Render a KeyShot scene to an image file.", renderSchema.shape, async (args) => {
65
83
  const parsed = renderInputSchema.parse(args);
66
- return toolResponse(await runKeyShotSerialized(config, { operation: "render", ...parsed }));
84
+ return toolResponse(await runKeyShotSerialized(config, { operation: "render", ...applyRenderQuality(parsed) }));
67
85
  });
68
86
  server.tool("keyshot_render_queue", "Render several jobs sequentially. Stops at the first failure unless continueOnError is set.", renderQueueSchema.shape, async (args) => {
69
87
  const parsed = renderQueueInputSchema.parse(args);
70
- 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 }));
71
89
  });
72
90
  server.tool("keyshot_batch_render", "Render multiple named cameras from one KeyShot scene into an output directory.", batchRenderSchema.shape, async (args) => {
73
91
  const parsed = batchRenderInputSchema.parse(args);
74
- return toolResponse(await runKeyShotSerialized(config, { operation: "batch_render", ...parsed }));
92
+ return toolResponse(await runKeyShotSerialized(config, { operation: "batch_render", ...applyRenderQuality(parsed) }));
75
93
  });
76
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) => {
77
95
  const parsed = renderAllCamerasInputSchema.parse(args);
78
- return toolResponse(await runKeyShotSerialized(config, { operation: "render_all_cameras", ...parsed }));
96
+ return toolResponse(await runKeyShotSerialized(config, { operation: "render_all_cameras", ...applyRenderQuality(parsed) }));
79
97
  });
80
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, {
81
99
  operation: "import_model",