keyshot-mcp 0.0.0 → 0.2.1

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/dist/runner.js ADDED
@@ -0,0 +1,134 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { localFailure } from "./result.js";
6
+ let queue = Promise.resolve();
7
+ export function runKeyShotSerialized(config, request) {
8
+ const run = queue.then(() => runKeyShot(config, request));
9
+ queue = run.catch(() => undefined);
10
+ return run;
11
+ }
12
+ async function runKeyShot(config, request) {
13
+ const exeExists = await exists(config.keyshotHeadlessExe);
14
+ if (!exeExists) {
15
+ return localFailure(`KeyShot headless executable not found: ${config.keyshotHeadlessExe}`);
16
+ }
17
+ const bridgeExists = await exists(config.bridgeScriptPath);
18
+ if (!bridgeExists) {
19
+ return localFailure(`KeyShot bridge script not found: ${config.bridgeScriptPath}`);
20
+ }
21
+ if (request.scenePath && !(await exists(request.scenePath))) {
22
+ return localFailure(`Scene file not found: ${request.scenePath}`);
23
+ }
24
+ await fs.mkdir(config.tmpDir, { recursive: true });
25
+ await fs.mkdir(config.keyshotOutputDir, { recursive: true });
26
+ const id = `${Date.now()}-${randomUUID()}`;
27
+ const argsPath = path.join(config.tmpDir, `${id}.args.json`);
28
+ const resultPath = path.join(config.tmpDir, `${id}.result.json`);
29
+ const payload = {
30
+ ...request,
31
+ defaults: {
32
+ outputDir: config.keyshotOutputDir,
33
+ },
34
+ };
35
+ await fs.writeFile(argsPath, JSON.stringify(payload, null, 2), "utf8");
36
+ const args = [
37
+ ...config.keyshotLicenseArgs,
38
+ "-progress",
39
+ ...(request.scenePath ? [request.scenePath] : []),
40
+ "-script",
41
+ config.bridgeScriptPath,
42
+ argsPath,
43
+ resultPath,
44
+ ];
45
+ const processResult = await spawnWithTimeout(config.keyshotHeadlessExe, args, config.keyshotTimeoutMs);
46
+ const stdoutTail = tail(processResult.stdout, 6000);
47
+ const stderrTail = tail(processResult.stderr, 6000);
48
+ let parsed = null;
49
+ if (await exists(resultPath)) {
50
+ try {
51
+ parsed = JSON.parse(await fs.readFile(resultPath, "utf8"));
52
+ }
53
+ catch (error) {
54
+ return localFailure(`Could not parse KeyShot result JSON: ${errorMessage(error)}`, {
55
+ keyshotStdoutTail: stdoutTail,
56
+ warnings: stderrTail ? [`stderr: ${stderrTail}`] : [],
57
+ });
58
+ }
59
+ }
60
+ if (processResult.timedOut) {
61
+ return localFailure(`KeyShot timed out after ${config.keyshotTimeoutMs}ms`, {
62
+ data: parsed?.data ?? null,
63
+ outputFiles: parsed?.outputFiles ?? [],
64
+ warnings: [...(parsed?.warnings ?? []), ...(stderrTail ? [`stderr: ${stderrTail}`] : [])],
65
+ keyshotStdoutTail: stdoutTail,
66
+ });
67
+ }
68
+ if (!parsed) {
69
+ return localFailure(`KeyShot did not produce a result file. Exit code: ${processResult.exitCode ?? "unknown"}`, {
70
+ keyshotStdoutTail: stdoutTail,
71
+ warnings: stderrTail ? [`stderr: ${stderrTail}`] : [],
72
+ });
73
+ }
74
+ parsed.keyshotStdoutTail = stdoutTail || parsed.keyshotStdoutTail || "";
75
+ if (stderrTail)
76
+ parsed.warnings = [...parsed.warnings, `stderr: ${stderrTail}`];
77
+ if (processResult.exitCode !== 0 && parsed.ok) {
78
+ return {
79
+ ...parsed,
80
+ ok: false,
81
+ error: `KeyShot exited with code ${processResult.exitCode}`,
82
+ };
83
+ }
84
+ return parsed;
85
+ }
86
+ function spawnWithTimeout(command, args, timeoutMs) {
87
+ return new Promise((resolve) => {
88
+ const child = spawn(command, args, {
89
+ windowsHide: true,
90
+ stdio: ["ignore", "pipe", "pipe"],
91
+ });
92
+ let stdout = "";
93
+ let stderr = "";
94
+ let settled = false;
95
+ let timedOut = false;
96
+ const timer = setTimeout(() => {
97
+ timedOut = true;
98
+ child.kill("SIGTERM");
99
+ setTimeout(() => {
100
+ if (!settled)
101
+ child.kill("SIGKILL");
102
+ }, 2500).unref();
103
+ }, timeoutMs);
104
+ child.stdout?.on("data", (chunk) => {
105
+ stdout += String(chunk);
106
+ });
107
+ child.stderr?.on("data", (chunk) => {
108
+ stderr += String(chunk);
109
+ });
110
+ child.on("error", (error) => {
111
+ stderr += errorMessage(error);
112
+ });
113
+ child.on("close", (exitCode) => {
114
+ settled = true;
115
+ clearTimeout(timer);
116
+ resolve({ exitCode, stdout, stderr, timedOut });
117
+ });
118
+ });
119
+ }
120
+ async function exists(filePath) {
121
+ try {
122
+ await fs.access(filePath);
123
+ return true;
124
+ }
125
+ catch {
126
+ return false;
127
+ }
128
+ }
129
+ function tail(value, maxChars) {
130
+ return value.length <= maxChars ? value : value.slice(value.length - maxChars);
131
+ }
132
+ function errorMessage(error) {
133
+ return error instanceof Error ? error.message : String(error);
134
+ }
@@ -0,0 +1,65 @@
1
+ import { z } from "zod";
2
+ const optionalPath = z.string().min(1).optional();
3
+ export const scenePathSchema = z.object({
4
+ scenePath: z.string().min(1),
5
+ });
6
+ export const renderSchema = z.object({
7
+ scenePath: z.string().min(1),
8
+ outputPath: optionalPath,
9
+ width: z.number().int().positive().optional(),
10
+ height: z.number().int().positive().optional(),
11
+ samples: z.number().int().positive().optional(),
12
+ maxTimeSeconds: z.number().positive().optional(),
13
+ camera: z.string().min(1).optional(),
14
+ format: z.enum(["png", "jpg", "jpeg", "tif", "tiff", "exr"]).optional(),
15
+ });
16
+ export const batchRenderSchema = z.object({
17
+ scenePath: z.string().min(1),
18
+ outputDir: z.string().min(1),
19
+ cameras: z.array(z.string().min(1)).min(1),
20
+ width: z.number().int().positive().optional(),
21
+ height: z.number().int().positive().optional(),
22
+ samples: z.number().int().positive().optional(),
23
+ maxTimeSeconds: z.number().positive().optional(),
24
+ format: z.enum(["png", "jpg", "jpeg", "tif", "tiff", "exr"]).optional(),
25
+ overwrite: z.boolean().optional(),
26
+ });
27
+ export const importModelSchema = z.object({
28
+ modelPath: z.string().min(1),
29
+ baseScenePath: optionalPath,
30
+ outputScenePath: z.string().min(1),
31
+ });
32
+ export const applyMaterialInputSchema = z.object({
33
+ scenePath: z.string().min(1),
34
+ objectName: z.string().min(1).optional(),
35
+ objectPath: z.string().min(1).optional(),
36
+ materialName: z.string().min(1).optional(),
37
+ materialPath: z.string().min(1).optional(),
38
+ outputScenePath: z.string().min(1),
39
+ });
40
+ export const applyMaterialSchema = applyMaterialInputSchema.refine((value) => value.objectName || value.objectPath, {
41
+ message: "Provide objectName or objectPath.",
42
+ }).refine((value) => value.materialName || value.materialPath, {
43
+ message: "Provide materialName or materialPath.",
44
+ });
45
+ const vector3 = z.tuple([z.number(), z.number(), z.number()]);
46
+ export const setCameraSchema = z.object({
47
+ scenePath: z.string().min(1),
48
+ cameraName: z.string().min(1).optional(),
49
+ position: vector3,
50
+ lookAt: vector3,
51
+ up: vector3.optional(),
52
+ distance: z.number().positive().optional(),
53
+ outputScenePath: z.string().min(1),
54
+ });
55
+ export const setEnvironmentSchema = z.object({
56
+ scenePath: z.string().min(1),
57
+ environmentName: z.string().min(1).optional(),
58
+ environmentPath: z.string().min(1).optional(),
59
+ brightness: z.number().positive().optional(),
60
+ outputScenePath: z.string().min(1),
61
+ });
62
+ export const saveSceneSchema = z.object({
63
+ scenePath: z.string().min(1),
64
+ outputScenePath: z.string().min(1),
65
+ });
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ {
2
+ "mcpServers": {
3
+ "keyshot": {
4
+ "command": "node",
5
+ "args": [
6
+ "C:/path/to/keyshot-mcp/dist/index.js"
7
+ ],
8
+ "env": {
9
+ "KEYSHOT_HEADLESS_EXE": "C:/Program Files/KeyShot Studio/bin/keyshot_headless.exe"
10
+ }
11
+ }
12
+ }
13
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "mcpServers": {
3
+ "keyshot": {
4
+ "command": "node",
5
+ "args": [
6
+ "C:/path/to/keyshot-mcp/dist/index.js"
7
+ ],
8
+ "env": {
9
+ "KEYSHOT_HEADLESS_EXE": "C:/Program Files/KeyShot Studio/bin/keyshot_headless.exe",
10
+ "KEYSHOT_TIMEOUT_MS": "600000"
11
+ }
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "mcpServers": {
3
+ "keyshot": {
4
+ "command": "node",
5
+ "args": [
6
+ "/absolute/path/to/keyshot-mcp/dist/index.js"
7
+ ],
8
+ "env": {
9
+ "KEYSHOT_HEADLESS_EXE": "/absolute/path/to/keyshot_headless"
10
+ }
11
+ }
12
+ }
13
+ }
package/package.json CHANGED
@@ -1,7 +1,53 @@
1
1
  {
2
2
  "name": "keyshot-mcp",
3
- "version": "0.0.0",
4
- "description": "Placeholder package for configuring npm trusted publishing. The real package is published from GitHub Actions.",
3
+ "version": "0.2.1",
4
+ "description": "Local and portable MCP server for KeyShot headless scripting.",
5
+ "type": "module",
6
+ "homepage": "https://github.com/truman-t3/keyshot-mcp#readme",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/truman-t3/keyshot-mcp.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/truman-t3/keyshot-mcp/issues"
13
+ },
14
+ "bin": {
15
+ "keyshot-mcp": "./dist/index.js"
16
+ },
17
+ "scripts": {
18
+ "build": "tsc -p tsconfig.json",
19
+ "prepare": "tsc -p tsconfig.json",
20
+ "start": "node ./dist/index.js",
21
+ "check": "tsc -p tsconfig.json --noEmit",
22
+ "status": "node ./dist/cli/status.js"
23
+ },
24
+ "keywords": [
25
+ "mcp",
26
+ "model-context-protocol",
27
+ "keyshot",
28
+ "rendering",
29
+ "keyshot-studio",
30
+ "stdio"
31
+ ],
32
+ "files": [
33
+ "dist",
34
+ "scripts/keyshot_bridge.py",
35
+ "examples",
36
+ "README.md",
37
+ "CONTRIBUTING.md",
38
+ "SECURITY.md",
39
+ "LICENSE"
40
+ ],
5
41
  "license": "MIT",
6
- "private": false
7
- }
42
+ "engines": {
43
+ "node": ">=20"
44
+ },
45
+ "dependencies": {
46
+ "@modelcontextprotocol/sdk": "^1.18.1",
47
+ "zod": "^3.25.76"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^22.17.2",
51
+ "typescript": "^5.9.2"
52
+ }
53
+ }