fnf-blender-mcp 0.1.0 → 0.2.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 +27 -53
- package/RELEASING.md +11 -0
- package/UPSTREAM.md +1 -1
- package/dist/cli.js +34 -46
- package/dist/index.js +8 -1
- package/dist/schemas.d.ts +1 -9
- package/dist/schemas.js +3 -14
- package/dist/server.js +2 -2
- package/dist/tools.js +3 -56
- package/dist/transport.d.ts +27 -28
- package/dist/transport.js +169 -66
- package/package.json +14 -5
- package/scripts/background_runner.py +69 -0
- package/skills/blender-scene/SKILL.md +4 -4
- package/skills/use-blender/SKILL.md +7 -7
- package/skills/use-blender/references/installation.md +22 -22
- package/skills/use-blender/references/verification.md +11 -9
- package/addon/higgsfield_use_blender/__init__.py +0 -39
- package/addon/higgsfield_use_blender/bridge.py +0 -197
- package/scripts/install_addon.py +0 -18
- package/scripts/launch.py +0 -10
package/README.md
CHANGED
|
@@ -1,47 +1,29 @@
|
|
|
1
1
|
# Higgsfield use Blender
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The independent `fnf-blender-mcp` package in `fnf-local-pluging-bridge-mcp/blender` controls a dedicated background Blender process. No add-on, HTTP listener, WebSocket, cloud account or open Blender window is required.
|
|
4
4
|
|
|
5
5
|
```text
|
|
6
|
-
Desktop MCP client
|
|
7
|
-
│
|
|
8
|
-
authenticated HTTP on 127.0.0.1
|
|
9
|
-
│
|
|
10
|
-
Blender Python add-on
|
|
11
|
-
│
|
|
12
|
-
main-thread timer → bpy
|
|
6
|
+
Desktop MCP client → stdio → Node MCP → process pipes → Blender --background → bpy
|
|
13
7
|
```
|
|
14
8
|
|
|
15
|
-
|
|
9
|
+
Each MCP connection owns one process. Its scene persists between commands; it cannot access unsaved work in an already-open desktop Blender. Open saved input with `bl_open_project`, edit and render, then save a `.blend` output to inspect in the desktop UI. Disconnecting loses unsaved session changes. The After Effects package at the repository root is independent.
|
|
16
10
|
|
|
17
|
-
##
|
|
11
|
+
## Setup
|
|
18
12
|
|
|
19
|
-
Requires Node.js 24+ with npm and Blender 4.2+.
|
|
20
|
-
|
|
21
|
-
Install into a persistent user directory (macOS/Linux):
|
|
13
|
+
Requires Node.js 24+ with npm and Blender 4.2+. Version 0.2.0 introduces the background runtime; version 0.1.0 uses the previous add-on. Verify that 0.2.0 is published before installing it; this source change alone does not publish a release.
|
|
22
14
|
|
|
23
15
|
```sh
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
node "$
|
|
29
|
-
node "$
|
|
16
|
+
npm view fnf-blender-mcp@0.2.0 version --registry=https://registry.npmjs.org/
|
|
17
|
+
blenderDir="$HOME/.higgsfield/blender-mcp"
|
|
18
|
+
npm install --prefix "$blenderDir" --registry=https://registry.npmjs.org/ fnf-blender-mcp@0.2.0
|
|
19
|
+
blenderCli="$blenderDir/node_modules/fnf-blender-mcp/dist/cli.js"
|
|
20
|
+
node "$blenderCli" doctor --blender "/absolute/path/to/blender"
|
|
21
|
+
node "$blenderCli" config --blender "/absolute/path/to/blender" --format json
|
|
30
22
|
```
|
|
31
23
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
`launch` opens a **new** Blender process with the bundled bridge. It does not attach to an existing process or change saved preferences. Wait for startup before running `doctor`. Use the actual executable, such as `Blender.app/Contents/MacOS/Blender` on macOS or `blender.exe` on Windows.
|
|
35
|
-
|
|
36
|
-
For an existing installation and automatic startup:
|
|
24
|
+
Use the executable inside `Blender.app/Contents/MacOS/Blender` on macOS, or `blender.exe` on Windows. `config --format toml` prints a Codex entry. Both formats include absolute Node/server paths and `BLENDER_EXECUTABLE`; merge the entry into the intended client and refresh its connection. The helper prints configuration but does not register a server. See the [installation skill](skills/use-blender/references/installation.md) for PowerShell and migration steps.
|
|
37
25
|
|
|
38
|
-
|
|
39
|
-
node "$bridgeCli" install-addon --blender "/absolute/path/to/blender"
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
Enable **Higgsfield use Blender** in Blender's Preferences → Add-ons, then save preferences if desired. The installer only copies the add-on into that version's user scripts directory. It never saves or resets preferences, and refuses to overwrite an unrecognized add-on directory. An already-running instance may need refreshing or restarting; preserve unsaved work.
|
|
43
|
-
|
|
44
|
-
Merge the generated JSON entry into the desktop client's MCP configuration. For Codex use `node "$bridgeCli" config --format toml`. This helper prints configuration; it does not register or enable a client connection. Both formats use absolute Node and server paths. Keep the installed package at that persistent location. Refresh the client's MCP connection, then call `bl_health` and `bl_get_scene_summary` from the conversation. A shell `doctor` success alone does not establish conversation tool availability.
|
|
26
|
+
The server starts Blender on its first execution call. `doctor` checks a separate temporary background process and terminates it afterward. Verify conversation access with `bl_health` and `bl_get_scene_summary`. `BLENDER_EXECUTABLE` can be set directly; without it the runtime tries standard installation locations and PATH. No separate Python installation is needed at runtime.
|
|
45
27
|
|
|
46
28
|
## Tools
|
|
47
29
|
|
|
@@ -52,43 +34,35 @@ Merge the generated JSON entry into the desktop client's MCP configuration. For
|
|
|
52
34
|
| Camera/light | `bl_add_camera`, `bl_set_active_camera`, `bl_add_light` |
|
|
53
35
|
| Animation | `bl_set_frame`, `bl_insert_keyframe` |
|
|
54
36
|
| Files | `bl_save_project`, `bl_open_project` |
|
|
55
|
-
| Evidence | `
|
|
37
|
+
| Evidence | `bl_render` |
|
|
56
38
|
| Python/status | `bl_execute`, `bl_job_status` |
|
|
57
39
|
| Offline guidance | `bl_get_skill` |
|
|
58
40
|
|
|
59
|
-
Start with `bl_get_skill(name: "blender-scene")`.
|
|
41
|
+
Start with `bl_get_skill(name: "blender-scene")`. Python runs sequentially on the process's main thread. Scene datablocks persist, while script-local variables do not. Assign a JSON-compatible `result` to return data. Python stdout/stderr are capped at 64 Ki characters each; JSON results are limited to 4 MiB. Native Blender diagnostics do not enter the MCP stdout protocol.
|
|
60
42
|
|
|
61
|
-
|
|
43
|
+
Render a camera frame to PNG; files up to 4 MiB receive an inline preview. Cycles supports sample overrides. Save/render require `overwrite: true` for existing output files; opening a different project refuses unsaved changes unless explicitly discarded. Arbitrary Python is not sandboxed and can bypass these typed-tool guards. Automatic execution of Python embedded in opened `.blend` files is disabled.
|
|
62
44
|
|
|
63
|
-
|
|
45
|
+
## Session lifecycle
|
|
64
46
|
|
|
65
|
-
|
|
47
|
+
A timed-out job continues and keeps its `job_id`. Query `bl_job_status` before retrying; additional execution commands are rejected while it runs. No automatic retries or rollbacks occur. Status retains at most 128 jobs within the MCP process. If Blender crashes, the session fails and is not restarted automatically: inspect output files before reconnecting to a new empty session. Closing the MCP terminates its child process, including a running job; save needed changes first.
|
|
66
48
|
|
|
67
|
-
|
|
68
|
-
- Browser-origin requests, incorrect Host headers and missing/incorrect tokens are rejected. Do not expose the endpoint or share its token.
|
|
69
|
-
- The server refuses ambiguous multi-instance discovery. Set `BLENDER_MCP_PID` in the MCP server environment to select the intended instance.
|
|
70
|
-
- Override `BLENDER_MCP_RUNTIME_DIR` on **both** the Blender process and MCP process when a custom directory is needed. `config` does not include these environment overrides automatically.
|
|
71
|
-
- HTTP only queues work. A persistent Blender timer executes Python on the main thread. Rendering or long scripts can block Blender's UI.
|
|
72
|
-
- Commands are accepted once and receive a job ID. Pending jobs expire before execution after their deadline. A running script cannot be safely interrupted; timeout does not mean cancellation or rollback.
|
|
73
|
-
- After a timeout, use `bl_job_status` on the original PID before retrying. A script exception can leave partial changes. If submission failed before the job ID was received, inspect the scene first.
|
|
74
|
-
- Up to 128 job records are retained in memory, with old completed jobs evicted as new jobs arrive. Restarting Blender loses history. A client-side MCP timeout may occur before the tool returns its job ID; set tool timeouts above 300 seconds for long renders and inspect state before retrying.
|
|
49
|
+
In 0.2.0, `launch`, `install-addon`, `bl_screenshot`, bridge discovery, PID selection and runtime-directory overrides are removed. Use camera renders for visual evidence. No existing desktop scenes, preferences or installed add-ons are modified during startup. Multiple MCP connections have separate background scenes.
|
|
75
50
|
|
|
76
|
-
## Development and
|
|
51
|
+
## Development and validation
|
|
77
52
|
|
|
78
|
-
From
|
|
53
|
+
From the repository root:
|
|
79
54
|
|
|
80
55
|
```sh
|
|
56
|
+
cd blender
|
|
57
|
+
npm ci
|
|
81
58
|
npm test
|
|
82
59
|
npm run typecheck
|
|
83
60
|
npm run test:package
|
|
61
|
+
BLENDER_EXECUTABLE=/absolute/path/to/blender npm run test:live
|
|
84
62
|
```
|
|
85
63
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
**These tests do not prove live Blender behavior.** Blender was not found on the development Mac, so real geometry, viewport rendering, installer behavior inside Blender and Windows/Linux integration remain unverified. For a live check, run `doctor`, inspect a disposable scene, create a primitive, set a material and camera, render/view a small PNG, save/reopen a temporary `.blend`, and confirm unsaved-file guards. Never use an existing unsaved project as the test fixture.
|
|
89
|
-
|
|
90
|
-
## Slash command
|
|
64
|
+
Offline tests exercise actual MCP and child-process pipes using a Python CLI fixture, persistent session state, isolation, timeout recovery, process death, bounded results/history, output formatting, CLI configuration and typed-script guards. They do not replace live Blender validation. `test:package` installs the archive independently and verifies 19 tools, offline guidance and the absence of add-on files. `test:live` uses real Blender for geometry, materials, keyframes, file guards and a small Cycles render in disposable files.
|
|
91
65
|
|
|
92
|
-
|
|
66
|
+
Verified on macOS arm64 with Blender 4.2.23 LTS: persistent scene, mesh/material/camera/light edits, keyframes, save/reopen guards and a Cycles PNG preview. Windows/Linux native Blender execution has not been verified locally.
|
|
93
67
|
|
|
94
|
-
See [UPSTREAM.md](UPSTREAM.md) for provenance and [LICENSE](LICENSE) for the MIT license.
|
|
68
|
+
See [RELEASING.md](RELEASING.md) for release steps. The setup skill is mirrored in fnf-mcp-server's `/use-blender` command; keep its body and references synchronized. See [UPSTREAM.md](UPSTREAM.md) for provenance and [LICENSE](LICENSE) for the MIT license.
|
package/RELEASING.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Release fnf-blender-mcp
|
|
2
|
+
|
|
3
|
+
Run commands from this `blender/` directory. Its package and lockfile are independent of After Effects.
|
|
4
|
+
|
|
5
|
+
1. Run `npm ci`, `npm test`, `npm run typecheck`, `npm run test:package`, and `test:live` with a real `BLENDER_EXECUTABLE` when available.
|
|
6
|
+
2. Version 0.2.0 is prepared for the transition to a background process. Verify that it is unpublished with `npm view fnf-blender-mcp versions --json`. Never overwrite an existing npm version; bump both manifests for subsequent releases.
|
|
7
|
+
3. Keep the version in README.md and skills/use-blender aligned with the fnf-mcp-server command bundle. Review migration instructions for removed commands and tools.
|
|
8
|
+
4. Run `npm pack --json`, review the contents, then publish the reviewed archive with `npm publish ./fnf-blender-mcp-0.2.0.tgz --access public --registry=https://registry.npmjs.org/` when publication is authorized.
|
|
9
|
+
5. Verify the public version and checksum and test a fresh registry installation. Publish the package before deploying the fnf-mcp-server instructions that require it.
|
|
10
|
+
|
|
11
|
+
Merging the source MR does not publish npm. Version 0.1.0 still requires the old add-on; it is not a fallback for 0.2.0 setup. New clients need a refreshed configuration containing BLENDER_EXECUTABLE. Existing desktop files and add-ons are not removed by this release.
|
package/UPSTREAM.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Core Blender tool Python bodies and schemas are adapted from Higgsfield fnf-adobe-mcp-connector, worker/bl-tools.ts and worker/bl-schemas.ts, revision fd5359a8c72f6ce30e62e5b500ffaf08f649bb84 (MIT per upstream package.json). Cloud routing, R2 image storage, cloud generation and remote documentation commands are omitted. The
|
|
1
|
+
Core Blender tool Python bodies and schemas are adapted from Higgsfield fnf-adobe-mcp-connector, worker/bl-tools.ts and worker/bl-schemas.ts, revision fd5359a8c72f6ce30e62e5b500ffaf08f649bb84 (MIT per upstream package.json). Cloud routing, R2 image storage, cloud generation and remote documentation commands are omitted. The background process transport, command runner and setup skill are independent implementations.
|
package/dist/cli.js
CHANGED
|
@@ -1,70 +1,58 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
2
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import {
|
|
5
|
-
import { isAbsolute } from "node:path";
|
|
6
|
-
import { BlenderTransport, discover } from "./transport.js";
|
|
7
|
-
const root = new URL("../", import.meta.url);
|
|
3
|
+
import { BlenderTransport, resolveExecutable } from "./transport.js";
|
|
8
4
|
const usage = `Higgsfield use Blender
|
|
9
|
-
fnf-blender doctor
|
|
10
|
-
fnf-blender config [--format json|toml]
|
|
11
|
-
fnf-blender
|
|
12
|
-
fnf-blender install-addon --blender PATH Install the add-on for that Blender version
|
|
13
|
-
fnf-blender serve Run the stdio MCP server
|
|
5
|
+
fnf-blender doctor [--blender PATH] Verify a new background Blender process
|
|
6
|
+
fnf-blender config [--blender PATH] [--format json|toml] Print local MCP configuration
|
|
7
|
+
fnf-blender serve Run the stdio MCP server
|
|
14
8
|
|
|
15
|
-
Requires Node.js 24+ and Blender 4.2+.
|
|
16
|
-
|
|
17
|
-
No WebSocket or
|
|
9
|
+
Requires Node.js 24+ and Blender 4.2+. BLENDER_EXECUTABLE selects the executable.
|
|
10
|
+
Each MCP session owns a background Blender process. Save .blend files before reconnecting.
|
|
11
|
+
No add-on, HTTP listener, WebSocket or open Blender window is required.`;
|
|
18
12
|
async function main() {
|
|
19
13
|
const [command = "help", ...args] = process.argv.slice(2);
|
|
20
14
|
if (["help", "--help", "-h"].includes(command)) {
|
|
21
15
|
console.log(usage);
|
|
22
16
|
return;
|
|
23
17
|
}
|
|
24
|
-
if (command === "serve") {
|
|
18
|
+
if (command === "serve" && args.length === 0) {
|
|
25
19
|
await import("./index.js");
|
|
26
20
|
return;
|
|
27
21
|
}
|
|
28
|
-
if (
|
|
29
|
-
|
|
22
|
+
if (!["config", "doctor"].includes(command))
|
|
23
|
+
throw new Error(usage);
|
|
24
|
+
let executable, format = "json";
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
for (let index = 0; index < args.length; index += 2) {
|
|
27
|
+
const key = args[index], value = args[index + 1];
|
|
28
|
+
if (!value || seen.has(key))
|
|
30
29
|
throw new Error(usage);
|
|
31
|
-
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
seen.add(key);
|
|
31
|
+
if (key === "--blender")
|
|
32
|
+
executable = value;
|
|
33
|
+
else if (key === "--format" && command === "config" && ["json", "toml"].includes(value))
|
|
34
|
+
format = value;
|
|
35
|
+
else
|
|
36
|
+
throw new Error(usage);
|
|
37
|
+
}
|
|
38
|
+
executable = await resolveExecutable(executable);
|
|
39
|
+
if (command === "config") {
|
|
40
|
+
const entry = { command: process.execPath, args: [fileURLToPath(new URL("./index.js", import.meta.url))], env: { BLENDER_EXECUTABLE: executable } };
|
|
41
|
+
if (format === "toml")
|
|
42
|
+
console.log(`[mcp_servers.higgsfield-use-blender]\ncommand = ${JSON.stringify(entry.command)}\nargs = ${JSON.stringify(entry.args)}\n[mcp_servers.higgsfield-use-blender.env]\nBLENDER_EXECUTABLE = ${JSON.stringify(executable)}`);
|
|
35
43
|
else
|
|
36
44
|
console.log(JSON.stringify({ mcpServers: { "higgsfield-use-blender": entry } }, null, 2));
|
|
37
45
|
return;
|
|
38
46
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const job = await new BlenderTransport().execute("import bpy\nresult = {'version': bpy.app.version_string, 'file': bpy.data.filepath, 'scene': bpy.context.scene.name}", 5);
|
|
47
|
+
const transport = new BlenderTransport({ executable });
|
|
48
|
+
try {
|
|
49
|
+
const job = await transport.execute("import bpy, os\nresult = {'version': bpy.app.version_string, 'background': bpy.app.background, 'pid': os.getpid(), 'file': bpy.data.filepath}", 15);
|
|
43
50
|
if (!job.ok)
|
|
44
51
|
throw new Error(job.error || "Blender execution failed");
|
|
45
|
-
console.log(JSON.stringify(job
|
|
46
|
-
return;
|
|
52
|
+
console.log(JSON.stringify({ executable, ...job }));
|
|
47
53
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
throw new Error("Pass --blender with the absolute Blender executable path.");
|
|
51
|
-
const executable = args[1];
|
|
52
|
-
if (!isAbsolute(executable) || !(await stat(executable)).isFile())
|
|
53
|
-
throw new Error("--blender must point to an absolute executable file, not the .app directory.");
|
|
54
|
-
const script = fileURLToPath(new URL(command === "launch" ? "scripts/launch.py" : "scripts/install_addon.py", root));
|
|
55
|
-
const launchArgs = command === "launch" ? ["--python", script] : ["--background", "--factory-startup", "--python-exit-code", "1", "--python", script];
|
|
56
|
-
const child = spawn(executable, launchArgs, { stdio: command === "launch" ? "ignore" : "inherit", detached: command === "launch" });
|
|
57
|
-
await new Promise((resolve, reject) => {
|
|
58
|
-
child.once("error", reject);
|
|
59
|
-
if (command === "launch")
|
|
60
|
-
child.once("spawn", () => { child.unref(); resolve(); });
|
|
61
|
-
else
|
|
62
|
-
child.once("exit", code => code === 0 ? resolve() : reject(new Error(`Blender installer exited ${code}`)));
|
|
63
|
-
});
|
|
64
|
-
if (command === "launch")
|
|
65
|
-
console.log("Blender launched with the bridge bootstrap. Run doctor after startup to verify the connection.");
|
|
66
|
-
return;
|
|
54
|
+
finally {
|
|
55
|
+
await transport.close();
|
|
67
56
|
}
|
|
68
|
-
throw new Error(usage);
|
|
69
57
|
}
|
|
70
58
|
main().catch(error => { console.error(error instanceof Error ? error.message : String(error)); process.exitCode = 1; });
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
3
3
|
import { createServer } from "./server.js";
|
|
4
|
-
|
|
4
|
+
import { BlenderTransport } from "./transport.js";
|
|
5
|
+
const blender = new BlenderTransport();
|
|
6
|
+
const server = createServer(blender);
|
|
7
|
+
server.server.onclose = () => { void blender.close(); };
|
|
8
|
+
const shutdown = async () => { await blender.close(); await server.close(); };
|
|
9
|
+
process.once("SIGINT", () => { void shutdown(); });
|
|
10
|
+
process.once("SIGTERM", () => { void shutdown(); });
|
|
11
|
+
await server.connect(new StdioServerTransport());
|
package/dist/schemas.d.ts
CHANGED
|
@@ -62,6 +62,7 @@ export declare const setFrameShape: {
|
|
|
62
62
|
};
|
|
63
63
|
export declare const renderShape: {
|
|
64
64
|
output_path: z.ZodString;
|
|
65
|
+
overwrite: z.ZodDefault<z.ZodBoolean>;
|
|
65
66
|
resolution: z.ZodOptional<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>>;
|
|
66
67
|
engine: z.ZodOptional<z.ZodEnum<{
|
|
67
68
|
BLENDER_EEVEE_NEXT: "BLENDER_EEVEE_NEXT";
|
|
@@ -70,15 +71,6 @@ export declare const renderShape: {
|
|
|
70
71
|
}>>;
|
|
71
72
|
samples: z.ZodOptional<z.ZodNumber>;
|
|
72
73
|
};
|
|
73
|
-
export declare const screenshotShape: {
|
|
74
|
-
max_size: z.ZodOptional<z.ZodNumber>;
|
|
75
|
-
shading: z.ZodOptional<z.ZodEnum<{
|
|
76
|
-
SOLID: "SOLID";
|
|
77
|
-
MATERIAL: "MATERIAL";
|
|
78
|
-
RENDERED: "RENDERED";
|
|
79
|
-
WIREFRAME: "WIREFRAME";
|
|
80
|
-
}>>;
|
|
81
|
-
};
|
|
82
74
|
export declare const getObjectShape: {
|
|
83
75
|
name: z.ZodString;
|
|
84
76
|
};
|
package/dist/schemas.js
CHANGED
|
@@ -3,7 +3,7 @@ const vec3 = z.tuple([z.number(), z.number(), z.number()]);
|
|
|
3
3
|
export const executeShape = {
|
|
4
4
|
code: z
|
|
5
5
|
.string().min(1).max(1_000_000)
|
|
6
|
-
.describe("Python source executed inside
|
|
6
|
+
.describe("Python source executed inside this MCP session’s background Blender with full `bpy` "
|
|
7
7
|
+ "access. To return data, assign a JSON-serialisable value to a variable "
|
|
8
8
|
+ "named `result`."),
|
|
9
9
|
};
|
|
@@ -57,7 +57,8 @@ export const setFrameShape = {
|
|
|
57
57
|
frame: z.number().int().describe("Frame number to set as current."),
|
|
58
58
|
};
|
|
59
59
|
export const renderShape = {
|
|
60
|
-
output_path: z.string().min(1).describe("Absolute path for the PNG
|
|
60
|
+
output_path: z.string().min(1).describe("Absolute path for the PNG output."),
|
|
61
|
+
overwrite: z.boolean().default(false).describe("Allow replacing an existing output file."),
|
|
61
62
|
resolution: z
|
|
62
63
|
.tuple([z.number().int().min(1).max(16384), z.number().int().min(1).max(16384)])
|
|
63
64
|
.optional()
|
|
@@ -66,18 +67,6 @@ export const renderShape = {
|
|
|
66
67
|
.describe("Render engine. Default the scene's current engine."),
|
|
67
68
|
samples: z.number().int().min(1).max(65536).optional().describe("Cycles sample count; requires the CYCLES engine."),
|
|
68
69
|
};
|
|
69
|
-
export const screenshotShape = {
|
|
70
|
-
max_size: z
|
|
71
|
-
.number()
|
|
72
|
-
.int()
|
|
73
|
-
.min(64).max(2048)
|
|
74
|
-
.optional()
|
|
75
|
-
.describe("Longest-edge pixel cap for the capture (keeps it under the 1MB message limit). Default 1280."),
|
|
76
|
-
shading: z
|
|
77
|
-
.enum(["SOLID", "MATERIAL", "RENDERED", "WIREFRAME"])
|
|
78
|
-
.optional()
|
|
79
|
-
.describe("Viewport shading to capture with. Default keeps the current viewport shading."),
|
|
80
|
-
};
|
|
81
70
|
export const getObjectShape = {
|
|
82
71
|
name: z.string().describe("Exact object name to inspect."),
|
|
83
72
|
};
|
package/dist/server.js
CHANGED
|
@@ -32,7 +32,7 @@ export async function formatResult(job) {
|
|
|
32
32
|
}
|
|
33
33
|
export function createServer(transport = new BlenderTransport()) {
|
|
34
34
|
const server = new McpServer({ name: "higgsfield-use-blender", title: "Higgsfield use Blender", version: packageInfo.version }, {
|
|
35
|
-
instructions: "
|
|
35
|
+
instructions: "Control a dedicated background Blender process; it cannot access an open desktop window. Save .blend files before disconnecting; unsaved state is lost on reconnect. Read bl_get_skill(blender-scene), then bl_health and bl_get_scene_summary before edits. Prefer typed tools; use bl_execute for other bpy operations. Commands can partially mutate before errors. On timeout, query bl_job_status; never blindly retry. Render and view results. Cloud generation is provided by a separate Higgsfield MCP, not this server.",
|
|
36
36
|
});
|
|
37
37
|
for (const tool of BL_TOOLS) {
|
|
38
38
|
server.registerTool(tool.name, {
|
|
@@ -48,7 +48,7 @@ export function createServer(transport = new BlenderTransport()) {
|
|
|
48
48
|
});
|
|
49
49
|
}
|
|
50
50
|
server.registerTool("bl_job_status", {
|
|
51
|
-
title: "Check Blender Job", description: "Check a previously accepted job after timeout without repeating its mutations.
|
|
51
|
+
title: "Check Blender Job", description: "Check a previously accepted job after timeout without repeating its mutations. Use the same MCP session. Results expire after 128 accepted jobs or reconnecting.",
|
|
52
52
|
inputSchema: z.object({ job_id: z.string().regex(/^[a-f0-9]{32}$/) }).strict(),
|
|
53
53
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
54
54
|
}, async ({ job_id }) => {
|
package/dist/tools.js
CHANGED
|
@@ -9,7 +9,7 @@ function scriptTool(name, title, description, shape, body) {
|
|
|
9
9
|
};
|
|
10
10
|
}
|
|
11
11
|
export const BL_TOOLS = [
|
|
12
|
-
scriptTool("bl_execute", "Execute Python (bpy)", "Run arbitrary Python inside
|
|
12
|
+
scriptTool("bl_execute", "Execute Python (bpy)", "Run arbitrary Python inside this MCP session’s background Blender with full `bpy` access. "
|
|
13
13
|
+ "Assign a JSON-serialisable value to `result` to return data. This is the "
|
|
14
14
|
+ "escape hatch — prefer a specific bl_* tool when one exists.", schemas.executeShape, ""),
|
|
15
15
|
scriptTool("bl_get_scene_summary", "Get Scene Summary", "Return a compact summary of the active scene: name, current frame, render "
|
|
@@ -164,59 +164,6 @@ finally:
|
|
|
164
164
|
_scene.render.resolution_percentage, _scene.render.filepath, _scene.render.image_settings.file_format) = _old
|
|
165
165
|
if _old_samples is not None:
|
|
166
166
|
_scene.cycles.samples = _old_samples`),
|
|
167
|
-
scriptTool("bl_screenshot", "Screenshot Viewport", "Capture what the 3D viewport currently shows (the agent's eyes) as an "
|
|
168
|
-
+ "OpenGL viewport render, and return an inline PNG. Use this to SEE the scene "
|
|
169
|
-
+ "before and after edits instead of guessing.", schemas.screenshotShape, `import base64, os, tempfile
|
|
170
|
-
_scene = bpy.context.scene
|
|
171
|
-
_orig_res = (_scene.render.resolution_x, _scene.render.resolution_y, _scene.render.resolution_percentage)
|
|
172
|
-
_orig_fp = _scene.render.filepath
|
|
173
|
-
_orig_fmt = _scene.render.image_settings.file_format
|
|
174
|
-
_cap = int(_args.get("max_size") or 1280)
|
|
175
|
-
_w, _h = _scene.render.resolution_x, _scene.render.resolution_y
|
|
176
|
-
_longest = max(_w, _h) or _cap
|
|
177
|
-
_scale = min(1.0, _cap / _longest)
|
|
178
|
-
_area = None
|
|
179
|
-
_win = None
|
|
180
|
-
for _wnd in bpy.context.window_manager.windows:
|
|
181
|
-
for _a in _wnd.screen.areas:
|
|
182
|
-
if _a.type == "VIEW_3D":
|
|
183
|
-
_area, _win = _a, _wnd
|
|
184
|
-
break
|
|
185
|
-
if _area:
|
|
186
|
-
break
|
|
187
|
-
_shading_prev = None
|
|
188
|
-
if _args.get("shading") and _area is not None:
|
|
189
|
-
_sp = _area.spaces.active
|
|
190
|
-
_shading_prev = _sp.shading.type
|
|
191
|
-
_sp.shading.type = _args["shading"]
|
|
192
|
-
_fd, _path = tempfile.mkstemp(suffix=".png", prefix="hf_shot_")
|
|
193
|
-
os.close(_fd)
|
|
194
|
-
try:
|
|
195
|
-
_scene.render.resolution_x = max(1, int(_w * _scale))
|
|
196
|
-
_scene.render.resolution_y = max(1, int(_h * _scale))
|
|
197
|
-
_scene.render.resolution_percentage = 100
|
|
198
|
-
_scene.render.filepath = _path
|
|
199
|
-
_scene.render.image_settings.file_format = "PNG"
|
|
200
|
-
if _area is not None:
|
|
201
|
-
_region = next((_r for _r in _area.regions if _r.type == "WINDOW"), None)
|
|
202
|
-
with bpy.context.temp_override(window=_win, area=_area, region=_region):
|
|
203
|
-
bpy.ops.render.opengl(write_still=True)
|
|
204
|
-
else:
|
|
205
|
-
raise RuntimeError("No VIEW_3D area is available; use bl_render with a scene camera")
|
|
206
|
-
with open(_path, "rb") as _fh:
|
|
207
|
-
_raw = _fh.read()
|
|
208
|
-
result = {"pngBase64": base64.b64encode(_raw).decode("ascii"),
|
|
209
|
-
"width": _scene.render.resolution_x, "height": _scene.render.resolution_y}
|
|
210
|
-
finally:
|
|
211
|
-
_scene.render.resolution_x, _scene.render.resolution_y, _scene.render.resolution_percentage = _orig_res
|
|
212
|
-
_scene.render.filepath = _orig_fp
|
|
213
|
-
_scene.render.image_settings.file_format = _orig_fmt
|
|
214
|
-
if _shading_prev is not None and _area is not None:
|
|
215
|
-
_area.spaces.active.shading.type = _shading_prev
|
|
216
|
-
try:
|
|
217
|
-
os.remove(_path)
|
|
218
|
-
except OSError:
|
|
219
|
-
pass`),
|
|
220
167
|
scriptTool("bl_get_object", "Get Object Detail", "Detailed inspection of one object: transform, dimensions, bounding box, "
|
|
221
168
|
+ "material slots, modifiers, parent, and mesh stats (verts/edges/faces).", schemas.getObjectShape, `_o = bpy.data.objects.get(_args["name"])
|
|
222
169
|
if _o is None:
|
|
@@ -240,7 +187,7 @@ if _o.type == "MESH" and _o.data is not None:
|
|
|
240
187
|
"polygons": len(_o.data.polygons)}
|
|
241
188
|
result = _info`),
|
|
242
189
|
];
|
|
243
|
-
BL_TOOLS.push(scriptTool("bl_health", "Inspect Blender", "
|
|
190
|
+
BL_TOOLS.push(scriptTool("bl_health", "Inspect Blender", "Start or inspect this MCP session’s background Blender. Returns its version, PID, active file and scene; does not inspect an open desktop window.", {}, `import os
|
|
244
191
|
result = {"version": bpy.app.version_string, "pid": os.getpid(), "background": bpy.app.background,
|
|
245
192
|
"file": bpy.data.filepath, "dirty": bpy.data.is_dirty, "scene": bpy.context.scene.name}`), scriptTool("bl_save_project", "Save Blender Project", "Save the current scene to an absolute .blend path. Existing files require overwrite=true.", {
|
|
246
193
|
path: z.string().min(1), overwrite: z.boolean().default(false),
|
|
@@ -259,7 +206,7 @@ if not os.path.isabs(_path) or not os.path.isfile(_path) or not _path.lower().en
|
|
|
259
206
|
raise ValueError("Provide an existing absolute .blend path")
|
|
260
207
|
if bpy.data.is_dirty and not _args["discard_unsaved"]:
|
|
261
208
|
raise ValueError("The current project has unsaved changes; save first or explicitly set discard_unsaved=true")
|
|
262
|
-
bpy.ops.wm.open_mainfile(filepath=_path)
|
|
209
|
+
bpy.ops.wm.open_mainfile(filepath=_path, use_scripts=False)
|
|
263
210
|
result = {"path": bpy.data.filepath, "scene": bpy.context.scene.name}`), scriptTool("bl_insert_keyframe", "Insert Object Keyframe", "Key the current location, Euler rotation or scale of a named object at a frame.", {
|
|
264
211
|
name: z.string().min(1), property: z.enum(["location", "rotation_euler", "scale"]), frame: z.number().int(),
|
|
265
212
|
}, `_obj = bpy.data.objects.get(_args["name"])
|
package/dist/transport.d.ts
CHANGED
|
@@ -1,31 +1,30 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
declare
|
|
11
|
-
job_id: z.ZodString;
|
|
12
|
-
state: z.ZodEnum<{
|
|
13
|
-
queued: "queued";
|
|
14
|
-
running: "running";
|
|
15
|
-
completed: "completed";
|
|
16
|
-
expired: "expired";
|
|
17
|
-
}>;
|
|
18
|
-
ok: z.ZodOptional<z.ZodBoolean>;
|
|
19
|
-
result: z.ZodOptional<z.ZodUnknown>;
|
|
20
|
-
error: z.ZodOptional<z.ZodString>;
|
|
21
|
-
stdout: z.ZodOptional<z.ZodString>;
|
|
22
|
-
stderr: z.ZodOptional<z.ZodString>;
|
|
23
|
-
}, z.core.$strip>;
|
|
24
|
-
export type JobResult = z.infer<typeof jobSchema>;
|
|
25
|
-
export declare function discover(directory?: string): Promise<Connection[]>;
|
|
1
|
+
export interface JobResult {
|
|
2
|
+
job_id: string;
|
|
3
|
+
state: "running" | "completed";
|
|
4
|
+
ok?: boolean;
|
|
5
|
+
result?: unknown;
|
|
6
|
+
error?: string;
|
|
7
|
+
stdout?: string;
|
|
8
|
+
stderr?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function resolveExecutable(explicit?: string | undefined): Promise<string>;
|
|
26
11
|
export declare class BlenderTransport {
|
|
27
|
-
|
|
28
|
-
|
|
12
|
+
private options;
|
|
13
|
+
private child?;
|
|
14
|
+
private startup?;
|
|
15
|
+
private fault?;
|
|
16
|
+
private closed;
|
|
17
|
+
private killTimer?;
|
|
18
|
+
private active?;
|
|
19
|
+
private jobs;
|
|
20
|
+
constructor(options?: {
|
|
21
|
+
executable?: string;
|
|
22
|
+
startupTimeoutMs?: number;
|
|
23
|
+
});
|
|
24
|
+
private fail;
|
|
25
|
+
private start;
|
|
26
|
+
private launch;
|
|
27
|
+
status(jobId: string): Promise<JobResult>;
|
|
29
28
|
execute(code: string, timeoutSeconds?: number): Promise<JobResult>;
|
|
29
|
+
close(): Promise<void>;
|
|
30
30
|
}
|
|
31
|
-
export {};
|