keyshot-mcp 0.5.0 → 0.6.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 +76 -10
- package/dist/camera-presets.js +86 -0
- package/dist/config.js +1 -0
- package/dist/index.js +54 -1
- package/dist/schemas.js +7 -0
- package/dist/version.js +1 -1
- package/examples/demo/README.md +2 -2
- package/package.json +1 -1
- package/presets/cameras.json +30 -0
- package/scripts/keyshot-smoke.mjs +33 -23
- package/scripts/keyshot_bridge.py +63 -0
- package/server.json +8 -2
package/README.md
CHANGED
|
@@ -32,6 +32,7 @@ This project does not include KeyShot, does not bypass licensing, and does not s
|
|
|
32
32
|
- Import a model into a scene.
|
|
33
33
|
- Apply a material to an object.
|
|
34
34
|
- Create or update a camera.
|
|
35
|
+
- Apply reusable standard or custom camera presets.
|
|
35
36
|
- Set an environment when the installed KeyShot version exposes that function.
|
|
36
37
|
- Save a scene to a new file.
|
|
37
38
|
|
|
@@ -68,7 +69,7 @@ This project does not include KeyShot, does not bypass licensing, and does not s
|
|
|
68
69
|
npm install -g keyshot-mcp
|
|
69
70
|
```
|
|
70
71
|
|
|
71
|
-
The current release line is `0.
|
|
72
|
+
The current release line is `0.6.0`. The published package is the easiest option
|
|
72
73
|
when you only want to use the MCP server. You still need KeyShot Studio and a
|
|
73
74
|
local KeyShot license.
|
|
74
75
|
|
|
@@ -92,7 +93,7 @@ Use exactly one of these approaches:
|
|
|
92
93
|
**No installation, run with npx**:
|
|
93
94
|
|
|
94
95
|
```json
|
|
95
|
-
{"mcpServers":{"keyshot":{"command":"npx","args":["-y","keyshot-mcp@0.
|
|
96
|
+
{"mcpServers":{"keyshot":{"command":"npx","args":["-y","keyshot-mcp@0.6.0"],"env":{"KEYSHOT_HEADLESS_EXE":"/absolute/path/to/keyshot_headless"}}}}
|
|
96
97
|
```
|
|
97
98
|
|
|
98
99
|
**Run from a cloned source directory** (after `npm install && npm run build`):
|
|
@@ -106,11 +107,11 @@ Use exactly one of these approaches:
|
|
|
106
107
|
If you use Codex or another coding agent, you can copy this prompt and let the agent install the MCP server for you:
|
|
107
108
|
|
|
108
109
|
```text
|
|
109
|
-
Install KeyShot MCP 0.
|
|
110
|
+
Install KeyShot MCP 0.6.0 from npm and configure it in my MCP client.
|
|
110
111
|
|
|
111
112
|
Please:
|
|
112
113
|
1. Find my KeyShot headless executable path.
|
|
113
|
-
2. Add a keyshot MCP server using npx -y keyshot-mcp@0.
|
|
114
|
+
2. Add a keyshot MCP server using npx -y keyshot-mcp@0.6.0.
|
|
114
115
|
3. Set KEYSHOT_HEADLESS_EXE to the detected keyshot_headless path.
|
|
115
116
|
4. Test the setup by running the keyshot_status tool.
|
|
116
117
|
5. Tell me the exact config that was added and whether the status check passed.
|
|
@@ -208,6 +209,11 @@ Find every camera in this KeyShot scene and render all views to the all-cameras
|
|
|
208
209
|
output folder. Continue if one camera fails and report the result for each view.
|
|
209
210
|
```
|
|
210
211
|
|
|
212
|
+
```text
|
|
213
|
+
List the available camera presets, apply the Isometric preset as a camera named
|
|
214
|
+
"Catalog Isometric", save a new scene, and render a preview from that camera.
|
|
215
|
+
```
|
|
216
|
+
|
|
211
217
|
## Environment Variables
|
|
212
218
|
|
|
213
219
|
- `KEYSHOT_HEADLESS_EXE`: path to `keyshot_headless` or `keyshot_headless.exe`.
|
|
@@ -215,6 +221,7 @@ output folder. Continue if one camera fails and report the result for each view.
|
|
|
215
221
|
- `KEYSHOT_LICENSE_ARGS`: optional KeyShot headless license arguments. Empty by default.
|
|
216
222
|
- `KEYSHOT_TIMEOUT_MS`: operation timeout in milliseconds. Default: `600000`.
|
|
217
223
|
- `KEYSHOT_ALLOW_EXTERNAL_OUTPUTS`: allow output paths outside `KEYSHOT_OUTPUT_DIR`. Default: `false`.
|
|
224
|
+
- `KEYSHOT_CAMERA_PRESETS`: camera preset JSON file. Default: `presets/cameras.json`.
|
|
218
225
|
|
|
219
226
|
By default every output file must stay inside `KEYSHOT_OUTPUT_DIR`. Relative paths
|
|
220
227
|
are placed there automatically. Set the compatibility switch to `true` only when
|
|
@@ -237,6 +244,8 @@ or the other, not both.
|
|
|
237
244
|
- `keyshot_list_material_presets`: list presets from the material preset library
|
|
238
245
|
- `keyshot_apply_material_preset`: apply a named preset from the library to an object
|
|
239
246
|
- `keyshot_set_camera`
|
|
247
|
+
- `keyshot_list_camera_presets`: list standard and custom camera presets
|
|
248
|
+
- `keyshot_apply_camera_preset`: create or update a named camera from a preset
|
|
240
249
|
- `keyshot_set_environment`
|
|
241
250
|
- `keyshot_save_scene`
|
|
242
251
|
|
|
@@ -265,6 +274,30 @@ looks instead of remembering exact KeyShot material names. Default location:
|
|
|
265
274
|
Each preset must have a `materialName` or a `materialPath`. Use
|
|
266
275
|
`keyshot_list_material_presets` to see what is available.
|
|
267
276
|
|
|
277
|
+
### Camera preset library
|
|
278
|
+
|
|
279
|
+
The built-in `presets/cameras.json` provides Front, Back, Left, Right, Top,
|
|
280
|
+
Bottom, and Isometric standard views. Use `keyshot_list_camera_presets` to list
|
|
281
|
+
them and `keyshot_apply_camera_preset` to create or update a named camera.
|
|
282
|
+
|
|
283
|
+
Set `KEYSHOT_CAMERA_PRESETS` to a user-managed JSON file for custom presets:
|
|
284
|
+
|
|
285
|
+
```json
|
|
286
|
+
{
|
|
287
|
+
"Front": { "standardView": "front" },
|
|
288
|
+
"Hero": {
|
|
289
|
+
"position": [4.5, 3.5, 4.5],
|
|
290
|
+
"lookAt": [0, 0, 0],
|
|
291
|
+
"up": [0, 1, 0],
|
|
292
|
+
"description": "Custom absolute product camera"
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
A preset file may use the object format shown above or an array whose entries
|
|
298
|
+
include a `name`. Each preset must contain either one supported `standardView`,
|
|
299
|
+
or both `position` and `lookAt`. Preset files are read-only to this MCP server.
|
|
300
|
+
|
|
268
301
|
## MCP Prompts and Resources
|
|
269
302
|
|
|
270
303
|
- Prompt: `keyshot_product_render`
|
|
@@ -287,7 +320,7 @@ KeyShot's Python `lux` API changes across versions. This server keeps the MCP in
|
|
|
287
320
|
|
|
288
321
|
- [x] Auto-discover and batch render all cameras in a scene
|
|
289
322
|
- [x] Material preset library
|
|
290
|
-
- [
|
|
323
|
+
- [x] Camera preset templates
|
|
291
324
|
- [x] Sequential render queue
|
|
292
325
|
- [x] Safer output directory restrictions
|
|
293
326
|
- [ ] More tested KeyShot versions
|
|
@@ -341,6 +374,7 @@ MIT
|
|
|
341
374
|
- 把模型导入场景。
|
|
342
375
|
- 给对象替换材质。
|
|
343
376
|
- 创建或更新相机。
|
|
377
|
+
- 应用可复用的标准或自定义相机预设。
|
|
344
378
|
- 在当前 KeyShot 版本支持时设置环境。
|
|
345
379
|
- 把场景保存为新文件。
|
|
346
380
|
|
|
@@ -377,7 +411,7 @@ MIT
|
|
|
377
411
|
npm install -g keyshot-mcp
|
|
378
412
|
```
|
|
379
413
|
|
|
380
|
-
当前发布版本为 `0.
|
|
414
|
+
当前发布版本为 `0.6.0`。如果你只是想使用 MCP 服务,直接安装 npm 包最简单。
|
|
381
415
|
电脑仍需要安装 KeyShot Studio,并且已经配置好本地 KeyShot 授权。
|
|
382
416
|
|
|
383
417
|
### 从源码安装
|
|
@@ -400,7 +434,7 @@ npm run build
|
|
|
400
434
|
**免安装,直接使用 npx**:
|
|
401
435
|
|
|
402
436
|
```json
|
|
403
|
-
{"mcpServers":{"keyshot":{"command":"npx","args":["-y","keyshot-mcp@0.
|
|
437
|
+
{"mcpServers":{"keyshot":{"command":"npx","args":["-y","keyshot-mcp@0.6.0"],"env":{"KEYSHOT_HEADLESS_EXE":"C:/KeyShot/bin/keyshot_headless.exe"}}}}
|
|
404
438
|
```
|
|
405
439
|
|
|
406
440
|
**使用本地源码**(先运行 `npm install && npm run build`):
|
|
@@ -414,11 +448,11 @@ npm run build
|
|
|
414
448
|
如果你使用 Codex 或其他编程 Agent,可以复制下面这段话,让 Agent 帮你安装和配置 MCP:
|
|
415
449
|
|
|
416
450
|
```text
|
|
417
|
-
请帮我安装 KeyShot MCP 0.
|
|
451
|
+
请帮我安装 KeyShot MCP 0.6.0,并添加到我的 MCP 客户端。
|
|
418
452
|
|
|
419
453
|
请你:
|
|
420
454
|
1. 查找我电脑上的 KeyShot headless 可执行文件路径。
|
|
421
|
-
2. 使用 npx -y keyshot-mcp@0.
|
|
455
|
+
2. 使用 npx -y keyshot-mcp@0.6.0 添加 keyshot MCP server。
|
|
422
456
|
3. 把 KEYSHOT_HEADLESS_EXE 设置为检测到的 keyshot_headless 路径。
|
|
423
457
|
4. 用 keyshot_status 工具测试是否配置成功。
|
|
424
458
|
5. 最后告诉我实际添加的配置,以及状态检查是否通过。
|
|
@@ -516,6 +550,11 @@ examples/codex.example.json
|
|
|
516
550
|
某个相机失败时继续处理其他相机,最后告诉我每个视角的结果。
|
|
517
551
|
```
|
|
518
552
|
|
|
553
|
+
```text
|
|
554
|
+
列出可用的相机预设,把 Isometric 等距预设应用为名为“目录等距视图”的相机,
|
|
555
|
+
保存一份新场景,并用这个相机渲染预览图。
|
|
556
|
+
```
|
|
557
|
+
|
|
519
558
|
## 环境变量
|
|
520
559
|
|
|
521
560
|
- `KEYSHOT_HEADLESS_EXE`:`keyshot_headless` 或 `keyshot_headless.exe` 的路径。
|
|
@@ -524,6 +563,7 @@ examples/codex.example.json
|
|
|
524
563
|
- `KEYSHOT_TIMEOUT_MS`:单次操作超时时间,单位毫秒,默认 `600000`。
|
|
525
564
|
- `KEYSHOT_MATERIAL_PRESETS`:材质预设库 JSON 文件路径,默认 `presets/materials.json`。
|
|
526
565
|
- `KEYSHOT_ALLOW_EXTERNAL_OUTPUTS`:是否允许写到默认输出目录之外,默认 `false`。
|
|
566
|
+
- `KEYSHOT_CAMERA_PRESETS`:相机预设 JSON 文件路径,默认 `presets/cameras.json`。
|
|
527
567
|
|
|
528
568
|
默认情况下,所有输出图片和场景都必须写入 `KEYSHOT_OUTPUT_DIR`,相对路径会自动放入该目录。
|
|
529
569
|
只有确实需要写到外部目录时才设置为 `true`;输入场景和模型路径不受此限制。
|
|
@@ -544,6 +584,8 @@ examples/codex.example.json
|
|
|
544
584
|
- `keyshot_list_material_presets`:列出材质预设库中的预设。
|
|
545
585
|
- `keyshot_apply_material_preset`:把预设库中的某个命名材质应用到物体上。
|
|
546
586
|
- `keyshot_set_camera`:设置相机。
|
|
587
|
+
- `keyshot_list_camera_presets`:列出标准和自定义相机预设。
|
|
588
|
+
- `keyshot_apply_camera_preset`:用预设创建或更新命名相机。
|
|
547
589
|
- `keyshot_set_environment`:设置环境。
|
|
548
590
|
- `keyshot_save_scene`:保存场景。
|
|
549
591
|
|
|
@@ -569,6 +611,30 @@ examples/codex.example.json
|
|
|
569
611
|
|
|
570
612
|
每个预设必须包含 `materialName` 或 `materialPath`。用 `keyshot_list_material_presets` 查看有哪些预设。
|
|
571
613
|
|
|
614
|
+
### 相机预设库
|
|
615
|
+
|
|
616
|
+
内置的 `presets/cameras.json` 包含 Front、Back、Left、Right、Top、Bottom 和
|
|
617
|
+
Isometric 七个 KeyShot 标准视角。使用 `keyshot_list_camera_presets` 查看预设,
|
|
618
|
+
使用 `keyshot_apply_camera_preset` 创建或更新命名相机。
|
|
619
|
+
|
|
620
|
+
把 `KEYSHOT_CAMERA_PRESETS` 指向用户自己的 JSON 文件即可添加自定义预设:
|
|
621
|
+
|
|
622
|
+
```json
|
|
623
|
+
{
|
|
624
|
+
"正视图": { "standardView": "front" },
|
|
625
|
+
"产品主视角": {
|
|
626
|
+
"position": [4.5, 3.5, 4.5],
|
|
627
|
+
"lookAt": [0, 0, 0],
|
|
628
|
+
"up": [0, 1, 0],
|
|
629
|
+
"description": "自定义绝对坐标相机"
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
预设文件既可以使用上面的对象格式,也可以使用每项包含 `name` 的数组格式。
|
|
635
|
+
每个预设只能选择一种形式:填写一个受支持的 `standardView`,或者同时填写
|
|
636
|
+
`position` 与 `lookAt`。MCP 只读取预设文件,不会自动修改它。
|
|
637
|
+
|
|
572
638
|
## MCP 提示词和资源
|
|
573
639
|
|
|
574
640
|
- 提示词:`keyshot_product_render`
|
|
@@ -591,7 +657,7 @@ KeyShot 的 Python `lux` API 会随版本变化。这个 MCP 会尽量保持对
|
|
|
591
657
|
|
|
592
658
|
- [x] 自动发现并批量渲染场景中的所有相机
|
|
593
659
|
- [x] 材质预设库
|
|
594
|
-
- [
|
|
660
|
+
- [x] 相机预设模板
|
|
595
661
|
- [x] 顺序渲染队列
|
|
596
662
|
- [x] 更安全的输出目录限制
|
|
597
663
|
- [ ] 测试更多 KeyShot 版本
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
export const STANDARD_CAMERA_VIEWS = [
|
|
3
|
+
"front",
|
|
4
|
+
"back",
|
|
5
|
+
"left",
|
|
6
|
+
"right",
|
|
7
|
+
"top",
|
|
8
|
+
"bottom",
|
|
9
|
+
"isometric",
|
|
10
|
+
];
|
|
11
|
+
export async function loadCameraPresets(config) {
|
|
12
|
+
let raw;
|
|
13
|
+
try {
|
|
14
|
+
raw = await fs.readFile(config.cameraPresetsPath, "utf8");
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
let parsed;
|
|
20
|
+
try {
|
|
21
|
+
parsed = JSON.parse(raw);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
throw new Error(`Camera presets file is not valid JSON: ${config.cameraPresetsPath}`);
|
|
25
|
+
}
|
|
26
|
+
return normalizeCameraPresets(parsed);
|
|
27
|
+
}
|
|
28
|
+
export function normalizeCameraPresets(parsed) {
|
|
29
|
+
const entries = [];
|
|
30
|
+
if (Array.isArray(parsed)) {
|
|
31
|
+
for (const item of parsed) {
|
|
32
|
+
if (!item || typeof item !== "object")
|
|
33
|
+
continue;
|
|
34
|
+
const record = item;
|
|
35
|
+
if (typeof record.name !== "string" || !record.name)
|
|
36
|
+
continue;
|
|
37
|
+
const preset = toCameraPreset(record.name, record);
|
|
38
|
+
if (preset)
|
|
39
|
+
entries.push(preset);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
else if (parsed && typeof parsed === "object") {
|
|
43
|
+
for (const [name, value] of Object.entries(parsed)) {
|
|
44
|
+
if (!value || typeof value !== "object")
|
|
45
|
+
continue;
|
|
46
|
+
const preset = toCameraPreset(name, value);
|
|
47
|
+
if (preset)
|
|
48
|
+
entries.push(preset);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return entries;
|
|
52
|
+
}
|
|
53
|
+
export function findCameraPreset(presets, name) {
|
|
54
|
+
const lower = name.toLowerCase();
|
|
55
|
+
return (presets.find((preset) => preset.name === name) ??
|
|
56
|
+
presets.find((preset) => preset.name.toLowerCase() === lower));
|
|
57
|
+
}
|
|
58
|
+
function toCameraPreset(name, record) {
|
|
59
|
+
const description = typeof record.description === "string" ? record.description : undefined;
|
|
60
|
+
const standardView = normalizeStandardView(record.standardView);
|
|
61
|
+
const position = toVector(record.position);
|
|
62
|
+
const lookAt = toVector(record.lookAt);
|
|
63
|
+
const up = toVector(record.up);
|
|
64
|
+
const hasStandard = standardView !== undefined;
|
|
65
|
+
const hasAbsolute = position !== undefined || lookAt !== undefined || up !== undefined;
|
|
66
|
+
if (hasStandard && !hasAbsolute) {
|
|
67
|
+
return { name, type: "standard", standardView, description };
|
|
68
|
+
}
|
|
69
|
+
if (!hasStandard && position && lookAt) {
|
|
70
|
+
return { name, type: "absolute", position, lookAt, up, description };
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
function normalizeStandardView(value) {
|
|
75
|
+
if (typeof value !== "string")
|
|
76
|
+
return undefined;
|
|
77
|
+
const normalized = value.toLowerCase();
|
|
78
|
+
return STANDARD_CAMERA_VIEWS.find((view) => view === normalized);
|
|
79
|
+
}
|
|
80
|
+
function toVector(value) {
|
|
81
|
+
if (!Array.isArray(value) || value.length !== 3)
|
|
82
|
+
return undefined;
|
|
83
|
+
if (!value.every((item) => typeof item === "number" && Number.isFinite(item)))
|
|
84
|
+
return undefined;
|
|
85
|
+
return [value[0], value[1], value[2]];
|
|
86
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -15,6 +15,7 @@ export function getConfig() {
|
|
|
15
15
|
tmpDir: path.join(projectRoot, "work", "tmp"),
|
|
16
16
|
bridgeScriptPath: path.join(projectRoot, "scripts", "keyshot_bridge.py"),
|
|
17
17
|
materialPresetsPath: path.resolve(process.env.KEYSHOT_MATERIAL_PRESETS ?? path.join(projectRoot, "presets", "materials.json")),
|
|
18
|
+
cameraPresetsPath: path.resolve(process.env.KEYSHOT_CAMERA_PRESETS ?? path.join(projectRoot, "presets", "cameras.json")),
|
|
18
19
|
};
|
|
19
20
|
}
|
|
20
21
|
function parseBoolean(value) {
|
package/dist/index.js
CHANGED
|
@@ -6,8 +6,9 @@ import { toolResponse, localFailure } from "./result.js";
|
|
|
6
6
|
import { runKeyShotSerialized } from "./runner.js";
|
|
7
7
|
import { runRenderQueue } from "./queue.js";
|
|
8
8
|
import { loadMaterialPresets, findMaterialPreset } from "./presets.js";
|
|
9
|
+
import { loadCameraPresets, findCameraPreset } from "./camera-presets.js";
|
|
9
10
|
import { VERSION } from "./version.js";
|
|
10
|
-
import { applyMaterialSchema, applyMaterialInputSchema, applyMaterialPresetInputSchema, applyMaterialPresetSchema, batchRenderSchema, batchRenderInputSchema, importModelSchema, listCamerasSchema, listMaterialPresetsSchema, renderQueueSchema, renderQueueInputSchema, renderAllCamerasSchema, renderAllCamerasInputSchema, renderSchema, renderInputSchema, saveSceneSchema, scenePathSchema, setCameraSchema, setEnvironmentSchema, } from "./schemas.js";
|
|
11
|
+
import { applyMaterialSchema, applyMaterialInputSchema, applyMaterialPresetInputSchema, applyMaterialPresetSchema, applyCameraPresetSchema, batchRenderSchema, batchRenderInputSchema, importModelSchema, listCamerasSchema, listMaterialPresetsSchema, listCameraPresetsSchema, renderQueueSchema, renderQueueInputSchema, renderAllCamerasSchema, renderAllCamerasInputSchema, renderSchema, renderInputSchema, saveSceneSchema, scenePathSchema, setCameraSchema, setEnvironmentSchema, } from "./schemas.js";
|
|
11
12
|
const config = getConfig();
|
|
12
13
|
const server = new McpServer({
|
|
13
14
|
name: "keyshot-mcp",
|
|
@@ -125,6 +126,58 @@ server.tool("keyshot_apply_material_preset", "Apply a named material preset (fro
|
|
|
125
126
|
}));
|
|
126
127
|
});
|
|
127
128
|
server.tool("keyshot_set_camera", "Create or update a camera from position/look-at vectors and save the resulting scene.", setCameraSchema.shape, async (args) => toolResponse(await runKeyShotSerialized(config, { operation: "set_camera", ...args })));
|
|
129
|
+
server.tool("keyshot_list_camera_presets", "List standard and custom camera presets from presets/cameras.json or KEYSHOT_CAMERA_PRESETS.", listCameraPresetsSchema.shape, async () => {
|
|
130
|
+
try {
|
|
131
|
+
const presets = await loadCameraPresets(config);
|
|
132
|
+
return toolResponse({
|
|
133
|
+
ok: true,
|
|
134
|
+
data: { presets, count: presets.length, source: config.cameraPresetsPath },
|
|
135
|
+
outputFiles: [],
|
|
136
|
+
warnings: presets.length === 0 ? ["No valid camera presets found."] : [],
|
|
137
|
+
keyshotStdoutTail: "",
|
|
138
|
+
error: null,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
return toolResponse(localFailure(errorMessage(error)));
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
server.tool("keyshot_apply_camera_preset", "Create or update a named camera from a standard or custom camera preset and save the scene.", applyCameraPresetSchema.shape, async (args) => {
|
|
146
|
+
let presets;
|
|
147
|
+
try {
|
|
148
|
+
presets = await loadCameraPresets(config);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
return toolResponse(localFailure(errorMessage(error)));
|
|
152
|
+
}
|
|
153
|
+
const preset = findCameraPreset(presets, args.presetName);
|
|
154
|
+
if (!preset) {
|
|
155
|
+
const available = presets.map((entry) => entry.name).join(", ") || "(none)";
|
|
156
|
+
return toolResponse(localFailure(`Camera preset not found: "${args.presetName}". Available: ${available}`));
|
|
157
|
+
}
|
|
158
|
+
const request = preset.type === "standard"
|
|
159
|
+
? {
|
|
160
|
+
operation: "set_standard_camera",
|
|
161
|
+
scenePath: args.scenePath,
|
|
162
|
+
standardView: preset.standardView,
|
|
163
|
+
cameraName: args.cameraName ?? preset.name,
|
|
164
|
+
outputScenePath: args.outputScenePath,
|
|
165
|
+
}
|
|
166
|
+
: {
|
|
167
|
+
operation: "set_camera",
|
|
168
|
+
scenePath: args.scenePath,
|
|
169
|
+
cameraName: args.cameraName ?? preset.name,
|
|
170
|
+
position: preset.position,
|
|
171
|
+
lookAt: preset.lookAt,
|
|
172
|
+
up: preset.up,
|
|
173
|
+
outputScenePath: args.outputScenePath,
|
|
174
|
+
};
|
|
175
|
+
const result = await runKeyShotSerialized(config, request);
|
|
176
|
+
if (result.data && typeof result.data === "object") {
|
|
177
|
+
result.data = { presetName: preset.name, presetType: preset.type, ...result.data };
|
|
178
|
+
}
|
|
179
|
+
return toolResponse(result);
|
|
180
|
+
});
|
|
128
181
|
server.tool("keyshot_set_environment", "Set a scene environment by name or file when supported by KeyShot headless scripting.", setEnvironmentSchema.shape, async (args) => toolResponse(await runKeyShotSerialized(config, { operation: "set_environment", ...args })));
|
|
129
182
|
server.tool("keyshot_save_scene", "Save a KeyShot scene to a new path.", saveSceneSchema.shape, async (args) => toolResponse(await runKeyShotSerialized(config, { operation: "save_scene", ...args })));
|
|
130
183
|
const transport = new StdioServerTransport();
|
package/dist/schemas.js
CHANGED
|
@@ -79,6 +79,13 @@ export const setCameraSchema = z.object({
|
|
|
79
79
|
distance: z.number().positive().optional(),
|
|
80
80
|
outputScenePath: z.string().min(1),
|
|
81
81
|
});
|
|
82
|
+
export const listCameraPresetsSchema = z.object({});
|
|
83
|
+
export const applyCameraPresetSchema = z.object({
|
|
84
|
+
scenePath: z.string().min(1),
|
|
85
|
+
presetName: z.string().min(1),
|
|
86
|
+
cameraName: z.string().min(1).optional(),
|
|
87
|
+
outputScenePath: z.string().min(1),
|
|
88
|
+
});
|
|
82
89
|
export const setEnvironmentSchema = z.object({
|
|
83
90
|
scenePath: z.string().min(1),
|
|
84
91
|
environmentName: z.string().min(1).optional(),
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.
|
|
1
|
+
export const VERSION = "0.6.0";
|
package/examples/demo/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
repository's MIT license. It contains no customer model or third-party asset.
|
|
5
5
|
|
|
6
6
|
Run `npm run smoke:keyshot` after setting `KEYSHOT_HEADLESS_EXE`. The command
|
|
7
|
-
checks KeyShot, imports the cube, saves and inspects the scene,
|
|
8
|
-
|
|
7
|
+
checks KeyShot, imports the cube, saves and inspects the scene, applies the
|
|
8
|
+
built-in Front and Isometric camera presets, automatically discovers them, and renders both views under
|
|
9
9
|
`outputs/demo/all-cameras`. Generated `.bip` and PNG files remain under
|
|
10
10
|
`outputs/` and are ignored by Git.
|
package/package.json
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"Front": {
|
|
3
|
+
"standardView": "front",
|
|
4
|
+
"description": "Front standard view / 正视图"
|
|
5
|
+
},
|
|
6
|
+
"Back": {
|
|
7
|
+
"standardView": "back",
|
|
8
|
+
"description": "Back standard view / 后视图"
|
|
9
|
+
},
|
|
10
|
+
"Left": {
|
|
11
|
+
"standardView": "left",
|
|
12
|
+
"description": "Left standard view / 左视图"
|
|
13
|
+
},
|
|
14
|
+
"Right": {
|
|
15
|
+
"standardView": "right",
|
|
16
|
+
"description": "Right standard view / 右视图"
|
|
17
|
+
},
|
|
18
|
+
"Top": {
|
|
19
|
+
"standardView": "top",
|
|
20
|
+
"description": "Top standard view / 顶视图"
|
|
21
|
+
},
|
|
22
|
+
"Bottom": {
|
|
23
|
+
"standardView": "bottom",
|
|
24
|
+
"description": "Bottom standard view / 底视图"
|
|
25
|
+
},
|
|
26
|
+
"Isometric": {
|
|
27
|
+
"standardView": "isometric",
|
|
28
|
+
"description": "Isometric standard view / 等距视图"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -2,6 +2,7 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { findCameraPreset, loadCameraPresets } from "../dist/camera-presets.js";
|
|
5
6
|
import { getConfig } from "../dist/config.js";
|
|
6
7
|
import { runKeyShotSerialized } from "../dist/runner.js";
|
|
7
8
|
|
|
@@ -20,6 +21,21 @@ async function run(label, request) {
|
|
|
20
21
|
return result;
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
async function applyStandardPreset(label, scenePath, presetName, outputScenePath) {
|
|
25
|
+
const presets = await loadCameraPresets(config);
|
|
26
|
+
const preset = findCameraPreset(presets, presetName);
|
|
27
|
+
if (!preset || preset.type !== "standard") {
|
|
28
|
+
throw new Error(`Standard camera preset not found: ${presetName}`);
|
|
29
|
+
}
|
|
30
|
+
return run(label, {
|
|
31
|
+
operation: "set_standard_camera",
|
|
32
|
+
scenePath,
|
|
33
|
+
cameraName: preset.name,
|
|
34
|
+
standardView: preset.standardView,
|
|
35
|
+
outputScenePath,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
23
39
|
const status = await run("1/7 KeyShot status", { operation: "status" });
|
|
24
40
|
const imported = await run("2/7 Import and save generated OBJ", {
|
|
25
41
|
operation: "import_model",
|
|
@@ -38,29 +54,23 @@ if (!Array.isArray(objects) || objects.length === 0) {
|
|
|
38
54
|
throw new Error("Imported scene did not contain any inspectable objects.");
|
|
39
55
|
}
|
|
40
56
|
|
|
41
|
-
const firstCameraResult = await
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
up: [0, 1, 0],
|
|
48
|
-
outputScenePath: "demo/keyshot-mcp-demo-camera-front.bip",
|
|
49
|
-
});
|
|
57
|
+
const firstCameraResult = await applyStandardPreset(
|
|
58
|
+
"4/7 Apply Front camera preset",
|
|
59
|
+
importedScene,
|
|
60
|
+
"Front",
|
|
61
|
+
"demo/keyshot-mcp-demo-preset-front.bip",
|
|
62
|
+
);
|
|
50
63
|
const firstCameraScene = firstCameraResult.outputFiles[0];
|
|
51
|
-
if (!firstCameraScene) throw new Error("
|
|
64
|
+
if (!firstCameraScene) throw new Error("Front preset did not return a saved scene path.");
|
|
52
65
|
|
|
53
|
-
const secondCameraResult = await
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
up: [0, 1, 0],
|
|
60
|
-
outputScenePath: "demo/keyshot-mcp-demo-camera-all.bip",
|
|
61
|
-
});
|
|
66
|
+
const secondCameraResult = await applyStandardPreset(
|
|
67
|
+
"5/7 Apply Isometric camera preset",
|
|
68
|
+
firstCameraScene,
|
|
69
|
+
"Isometric",
|
|
70
|
+
"demo/keyshot-mcp-demo-presets.bip",
|
|
71
|
+
);
|
|
62
72
|
const cameraScene = secondCameraResult.outputFiles[0];
|
|
63
|
-
if (!cameraScene) throw new Error("
|
|
73
|
+
if (!cameraScene) throw new Error("Isometric preset did not return a saved scene path.");
|
|
64
74
|
|
|
65
75
|
const rendered = await run("6/7 Discover and render every camera", {
|
|
66
76
|
operation: "render_all_cameras",
|
|
@@ -80,10 +90,10 @@ if (!renderData || renderData.failed !== 0 || renderData.succeeded < 2) {
|
|
|
80
90
|
}
|
|
81
91
|
|
|
82
92
|
const namedResults = renderData.results.filter(
|
|
83
|
-
(entry) => entry.camera === "
|
|
93
|
+
(entry) => entry.camera === "Front" || entry.camera === "Isometric",
|
|
84
94
|
);
|
|
85
95
|
if (namedResults.length !== 2 || namedResults.some((entry) => !entry.ok)) {
|
|
86
|
-
throw new Error("The
|
|
96
|
+
throw new Error("The Front and Isometric preset cameras were not both rendered successfully.");
|
|
87
97
|
}
|
|
88
98
|
const hashes = await Promise.all(namedResults.map(async (entry) =>
|
|
89
99
|
crypto.createHash("sha256").update(await fs.readFile(entry.outputPath)).digest("hex")
|
|
@@ -103,5 +113,5 @@ console.log(JSON.stringify({
|
|
|
103
113
|
scenePath: cameraScene,
|
|
104
114
|
cameraCount: renderData.total,
|
|
105
115
|
renderedImages: rendered.outputFiles,
|
|
106
|
-
|
|
116
|
+
verifiedPresets: namedResults.map((entry) => entry.camera),
|
|
107
117
|
}, null, 2));
|
|
@@ -45,6 +45,8 @@ def main():
|
|
|
45
45
|
data = apply_material(payload, output_files, warnings)
|
|
46
46
|
elif operation == "set_camera":
|
|
47
47
|
data = set_camera(payload, output_files, warnings)
|
|
48
|
+
elif operation == "set_standard_camera":
|
|
49
|
+
data = set_standard_camera(payload, output_files, warnings)
|
|
48
50
|
elif operation == "set_environment":
|
|
49
51
|
data = set_environment(payload, output_files, warnings)
|
|
50
52
|
elif operation == "save_scene":
|
|
@@ -469,6 +471,54 @@ def set_camera(payload, output_files, warnings):
|
|
|
469
471
|
}
|
|
470
472
|
|
|
471
473
|
|
|
474
|
+
def set_standard_camera(payload, output_files, warnings):
|
|
475
|
+
output_scene_path = payload.get("outputScenePath")
|
|
476
|
+
camera_name = payload.get("cameraName") or "MCP Camera"
|
|
477
|
+
standard_view = str(payload.get("standardView") or "").lower()
|
|
478
|
+
view_constants = {
|
|
479
|
+
"front": "VIEW_FRONT",
|
|
480
|
+
"back": "VIEW_BACK",
|
|
481
|
+
"left": "VIEW_LEFT",
|
|
482
|
+
"right": "VIEW_RIGHT",
|
|
483
|
+
"top": "VIEW_TOP",
|
|
484
|
+
"bottom": "VIEW_BOTTOM",
|
|
485
|
+
"isometric": "VIEW_ISOMETRIC",
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
constant_name = view_constants.get(standard_view)
|
|
489
|
+
if constant_name is None:
|
|
490
|
+
raise RuntimeError("Unsupported standard camera view: %s" % standard_view)
|
|
491
|
+
if not hasattr(lux, "setStandardView"):
|
|
492
|
+
raise RuntimeError("KeyShot lux.setStandardView is not available.")
|
|
493
|
+
if not hasattr(lux, constant_name):
|
|
494
|
+
raise RuntimeError("KeyShot camera constant is not available: %s" % constant_name)
|
|
495
|
+
|
|
496
|
+
if camera_name in camera_names():
|
|
497
|
+
if not hasattr(lux, "setCamera"):
|
|
498
|
+
raise RuntimeError("KeyShot cannot activate the existing camera: %s" % camera_name)
|
|
499
|
+
call_non_false_variants("activate camera", lambda: lux.setCamera(camera_name))
|
|
500
|
+
else:
|
|
501
|
+
call_non_false_variants(
|
|
502
|
+
"create camera",
|
|
503
|
+
lambda: lux.newCamera(camera_name),
|
|
504
|
+
lambda: lux.createCamera(camera_name),
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
call_variants("set standard camera view", lambda: lux.setStandardView(getattr(lux, constant_name)))
|
|
508
|
+
call_variants(
|
|
509
|
+
"save standard camera",
|
|
510
|
+
lambda: lux.saveCamera(),
|
|
511
|
+
lambda: lux.saveCamera(camera_name),
|
|
512
|
+
)
|
|
513
|
+
save_to(output_scene_path)
|
|
514
|
+
output_files.append(output_scene_path)
|
|
515
|
+
return {
|
|
516
|
+
"cameraName": camera_name,
|
|
517
|
+
"standardView": standard_view,
|
|
518
|
+
"savedScene": output_scene_path,
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
|
|
472
522
|
def set_environment(payload, output_files, warnings):
|
|
473
523
|
output_scene_path = payload.get("outputScenePath")
|
|
474
524
|
environment_name = payload.get("environmentName")
|
|
@@ -602,6 +652,19 @@ def call_variants(label, *callbacks):
|
|
|
602
652
|
raise RuntimeError("%s is unsupported or failed: %s" % (label, " | ".join(errors)))
|
|
603
653
|
|
|
604
654
|
|
|
655
|
+
def call_non_false_variants(label, *callbacks):
|
|
656
|
+
errors = []
|
|
657
|
+
for callback in callbacks:
|
|
658
|
+
try:
|
|
659
|
+
value = callback()
|
|
660
|
+
if value is not False:
|
|
661
|
+
return value
|
|
662
|
+
errors.append("returned false")
|
|
663
|
+
except Exception as exc:
|
|
664
|
+
errors.append(str(exc))
|
|
665
|
+
raise RuntimeError("%s is unsupported or failed: %s" % (label, " | ".join(errors)))
|
|
666
|
+
|
|
667
|
+
|
|
605
668
|
def first_success(*callbacks, default=None):
|
|
606
669
|
for callback in callbacks:
|
|
607
670
|
try:
|
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.
|
|
12
|
+
"version": "0.6.0",
|
|
13
13
|
"packages": [
|
|
14
14
|
{
|
|
15
15
|
"registryType": "npm",
|
|
16
16
|
"identifier": "keyshot-mcp",
|
|
17
|
-
"version": "0.
|
|
17
|
+
"version": "0.6.0",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|
|
20
20
|
},
|
|
@@ -24,6 +24,12 @@
|
|
|
24
24
|
"description": "Absolute path to KeyShot headless, or a command available on PATH.",
|
|
25
25
|
"isRequired": true,
|
|
26
26
|
"isSecret": false
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "KEYSHOT_CAMERA_PRESETS",
|
|
30
|
+
"description": "Optional path to a user-managed camera preset JSON file.",
|
|
31
|
+
"isRequired": false,
|
|
32
|
+
"isSecret": false
|
|
27
33
|
}
|
|
28
34
|
]
|
|
29
35
|
}
|