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 CHANGED
@@ -1,47 +1,29 @@
1
1
  # Higgsfield use Blender
2
2
 
3
- An independent local MCP project for editing native Blender scenes. No WebSocket, Cloudflare Worker, R2 bucket or cloud bridge account is required.
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 ── stdio ── Node MCP server
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
- Install the public [fnf-blender-mcp](https://www.npmjs.com/package/fnf-blender-mcp) package, published by `arsu_higgsfield_ai`. No source checkout or build is needed for installation.
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
- ## Quick start
11
+ ## Setup
18
12
 
19
- Requires Node.js 24+ with npm and Blender 4.2+. Blender supplies the add-on's Python runtime; a separate Python installation is only needed for development tests.
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
- bridgeDir="$HOME/.higgsfield/blender-mcp"
25
- npm install --prefix "$bridgeDir" --registry=https://registry.npmjs.org/ fnf-blender-mcp@0.1.0
26
- bridgeCli="$bridgeDir/node_modules/fnf-blender-mcp/dist/cli.js"
27
- node "$bridgeCli" launch --blender "/absolute/path/to/blender"
28
- node "$bridgeCli" doctor
29
- node "$bridgeCli" config --format json
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
- For Windows PowerShell, use `$bridgeDir = Join-Path $env:LOCALAPPDATA 'Higgsfield/blender-mcp'`, install with `npm.cmd install --prefix $bridgeDir --registry=https://registry.npmjs.org/ fnf-blender-mcp@0.1.0`, then set `$bridgeCli = Join-Path $bridgeDir 'node_modules/fnf-blender-mcp/dist/cli.js'` and invoke `node $bridgeCli` with the commands above. The [installation skill](skills/use-blender/references/installation.md) contains the full setup procedure.
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
- ```sh
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 | `bl_screenshot`, `bl_render` |
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")`. It routes to modeling, materials, lighting/camera and animation guidance. Use `bl_execute` for bpy operations beyond the typed tools; assign a JSON-compatible value to `result`. stdout and stderr are returned with a 64 KiB cap each. Results are limited to 4 MiB; return a file path for larger data.
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
- Screenshots return inline MCP images without cloud uploads and need a VIEW_3D area. Camera renders write a local PNG and include a preview for files up to 4 MiB. Render overrides are restored afterward; an explicit sample override currently requires Cycles. Saving or rendering over an existing file requires `overwrite: true`. Opening a project refuses unsaved changes unless `discard_unsaved: true`.
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
- This package does not bundle cloud generation, offline Blender API/manual search, or the original connector's model-generation and motion-import handlers. Use a separate Higgsfield MCP for generation, download finished assets, and import an absolute local path. Local Python has the full permissions of the Blender process; it is not a sandbox.
45
+ ## Session lifecycle
64
46
 
65
- ## Connection and timeout behavior
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
- - The add-on binds an ephemeral loopback port and generates a random bearer token. Discovery files live in `~/.higgsfield/blender/bridge-<pid>.json`, with owner-only file permissions on POSIX. Windows uses the user's profile-directory ACLs.
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 verification
51
+ ## Development and validation
77
52
 
78
- From a source checkout, run `npm ci` first. Development checks:
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
- The automated tests cover a real stdio MCP subprocess talking to the real Python HTTP bridge, authenticated discovery, multiple instances, execution errors, result images, input validation, queue overload, expiration, timeouts and status recovery. A minimal bpy fixture supplies metadata in those tests. Every typed Python script is syntax-compiled. `test:package` builds a local `.tgz`, installs it in an isolated directory, and checks the shipped runtime, skills and setup files; it does not publish anything.
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
- The matching `/use-blender` bundle is added to `fnf-mcp-server` under `src/tools/preset-instructions/resources/commands/use-blender`. It returns installation and verification instructions before any remote preset lookup. The standalone source lives in `skills/use-blender`; keep their bodies synchronized when editing setup instructions. The server command adds a `title` frontmatter field for its catalog.
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 local transport, add-on, installer and setup skill are independent implementations.
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 { stat } from "node:fs/promises";
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 Discover bridges and verify live bpy execution
10
- fnf-blender config [--format json|toml] Print local MCP configuration
11
- fnf-blender launch --blender PATH Open Blender with the bundled bridge
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+. BLENDER_MCP_PID selects an instance.
16
- BLENDER_MCP_RUNTIME_DIR overrides local bridge discovery on both processes.
17
- No WebSocket or cloud account is required. npm package: fnf-blender-mcp.`;
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 (command === "config") {
29
- if (args.length && (args[0] !== "--format" || args.length !== 2 || !["json", "toml"].includes(args[1])))
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
- const entry = { command: process.execPath, args: [fileURLToPath(new URL("dist/index.js", root))] };
32
- if (args[1] === "toml") {
33
- console.log(`[mcp_servers.higgsfield-use-blender]\ncommand = ${JSON.stringify(entry.command)}\nargs = ${JSON.stringify(entry.args)}`);
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
- if (command === "doctor") {
40
- const connections = await discover();
41
- console.log(JSON.stringify({ node: process.version, bridges: connections.map(({ pid, port }) => ({ pid, port })) }));
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.result));
46
- return;
52
+ console.log(JSON.stringify({ executable, ...job }));
47
53
  }
48
- if (command === "install-addon" || command === "launch") {
49
- if (args[0] !== "--blender" || args.length !== 2)
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
- await createServer().connect(new StdioServerTransport());
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 the connected Blender with full `bpy` "
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. Default a temp file."),
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: "Local Blender control. 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.",
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. Select the original Blender PID when multiple instances are open. Completed results expire after 128 newer accepted jobs or Blender exits.",
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 the connected Blender with full `bpy` access. "
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", "Read the live Blender version, process, active file and scene; proves main-thread execution.", {}, `import os
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"])
@@ -1,31 +1,30 @@
1
- import { z } from "zod";
2
- export declare const runtimeDirectory: () => string;
3
- declare const connectionSchema: z.ZodObject<{
4
- protocol: z.ZodLiteral<"higgsfield-blender/1">;
5
- pid: z.ZodNumber;
6
- port: z.ZodNumber;
7
- token: z.ZodString;
8
- }, z.core.$strip>;
9
- export type Connection = z.infer<typeof connectionSchema>;
10
- declare const jobSchema: z.ZodObject<{
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
- connection(): Promise<Connection>;
28
- status(jobId: string, connection?: Connection): Promise<JobResult>;
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 {};