bambu-printer-mcp 1.1.5 → 1.1.8
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/CONTRIBUTORS.md +26 -0
- package/README.md +100 -27
- package/dist/blender-mcp-bridge.d.ts +10 -0
- package/dist/blender-mcp-bridge.js +389 -0
- package/dist/index.js +166 -102
- package/dist/printers/bambu.js +31 -13
- package/dist/slicer/profile-flatten.d.ts +15 -4
- package/dist/slicer/profile-flatten.js +181 -74
- package/dist/stl/stl-manipulator.d.ts +3 -17
- package/dist/stl/stl-manipulator.js +142 -85
- package/package.json +8 -4
- package/patches/bambu-node+3.22.21.patch +18 -2
- package/scripts/install-patches.mjs +64 -0
- package/src/blender-mcp-bridge.ts +344 -0
- package/src/index.ts +237 -123
- package/src/printers/bambu.ts +29 -13
- package/src/slicer/profile-flatten.ts +202 -80
- package/src/stl/stl-manipulator.ts +161 -98
package/dist/index.js
CHANGED
|
@@ -5,11 +5,13 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
|
|
|
5
5
|
import { ListResourcesRequestSchema, ReadResourceRequestSchema, ListToolsRequestSchema, CallToolRequestSchema, ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
|
|
6
6
|
import dotenv from "dotenv";
|
|
7
7
|
import fs from "fs";
|
|
8
|
+
import os from "node:os";
|
|
8
9
|
import path from "path";
|
|
9
10
|
import { createServer as createHttpServer } from "node:http";
|
|
10
11
|
import { randomUUID } from "node:crypto";
|
|
11
12
|
import { STLManipulator, SLICER_TYPES, normalizeSlicerType, } from "./stl/stl-manipulator.js";
|
|
12
13
|
import { BambuNetworkBridge } from "./bambu-network-bridge.js";
|
|
14
|
+
import { BlenderMcpBridge } from "./blender-mcp-bridge.js";
|
|
13
15
|
import { hasAmsMappingInput, normalizeAmsMappingObject, normalizeBridgeAmsTrayValue } from "./ams-mapping.js";
|
|
14
16
|
import { analyze3MFAmsRequirements, analyze3MFPlateObjects, analyzeCollarCharm3MF, extractBambuTemplateSettings, getCollarCharmRolePolicy, parse3MF } from './3mf_parser.js';
|
|
15
17
|
import { BambuImplementation } from "./printers/bambu.js";
|
|
@@ -18,7 +20,24 @@ const DEFAULT_HOST = process.env.BAMBU_PRINTER_HOST || process.env.PRINTER_HOST
|
|
|
18
20
|
const DEFAULT_BAMBU_SERIAL = process.env.BAMBU_PRINTER_SERIAL || process.env.BAMBU_SERIAL || "";
|
|
19
21
|
const DEFAULT_BAMBU_TOKEN = process.env.BAMBU_PRINTER_ACCESS_TOKEN || process.env.BAMBU_TOKEN || "";
|
|
20
22
|
const DEFAULT_BAMBU_DEV_ID = process.env.BAMBU_DEV_ID || DEFAULT_BAMBU_SERIAL;
|
|
21
|
-
|
|
23
|
+
// os.tmpdir(), not process.cwd() — a packaged Claude Desktop extension is
|
|
24
|
+
// spawned with a cwd we don't control (and may not be writable), so a
|
|
25
|
+
// cwd-relative default here can throw at module load via the mkdirSync
|
|
26
|
+
// below and kill the server before it ever opens the stdio transport.
|
|
27
|
+
const AUTOMATIC_TEMP_DIR = process.env.TEMP_DIR ? undefined : fs.mkdtempSync(path.join(os.tmpdir(), "bambu-printer-mcp-"));
|
|
28
|
+
const TEMP_DIR = process.env.TEMP_DIR || AUTOMATIC_TEMP_DIR;
|
|
29
|
+
if (AUTOMATIC_TEMP_DIR) {
|
|
30
|
+
// The exit event also covers startup failures and normal event-loop exit.
|
|
31
|
+
// Only the directory created above belongs to us; explicit TEMP_DIR is retained.
|
|
32
|
+
process.once("exit", () => {
|
|
33
|
+
try {
|
|
34
|
+
fs.rmSync(AUTOMATIC_TEMP_DIR, { recursive: true, force: true });
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
console.error("Unable to remove the server's temporary directory:", error);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
22
41
|
// Printer model and bed type
|
|
23
42
|
const DEFAULT_BAMBU_MODEL = process.env.BAMBU_PRINTER_MODEL?.trim().toLowerCase() ||
|
|
24
43
|
process.env.BAMBU_MODEL?.trim().toLowerCase() ||
|
|
@@ -498,6 +517,9 @@ function readRuntimeConfig() {
|
|
|
498
517
|
enableJsonResponse: parseBooleanEnv(process.env.MCP_HTTP_JSON_RESPONSE, true),
|
|
499
518
|
allowedOrigins: parseCsvEnv(process.env.MCP_HTTP_ALLOWED_ORIGINS),
|
|
500
519
|
blenderBridgeCommand: process.env.BLENDER_MCP_BRIDGE_COMMAND?.trim() || undefined,
|
|
520
|
+
ffmpegPath: process.env.FFMPEG_PATH?.trim() || undefined,
|
|
521
|
+
allowExecutableArg: parseBooleanEnv(process.env.MCP_ALLOW_EXECUTABLE_ARG, false),
|
|
522
|
+
allowBridgeCommandArg: parseBooleanEnv(process.env.MCP_ALLOW_BRIDGE_COMMAND_ARG, false),
|
|
501
523
|
};
|
|
502
524
|
}
|
|
503
525
|
function expandUserPath(rawPath) {
|
|
@@ -740,10 +762,12 @@ if (!fs.existsSync(TEMP_DIR)) {
|
|
|
740
762
|
}
|
|
741
763
|
class BambuPrinterMCPServer {
|
|
742
764
|
constructor() {
|
|
765
|
+
this.blender = new BlenderMcpBridge();
|
|
766
|
+
this.shuttingDown = false;
|
|
743
767
|
this.runtimeConfig = readRuntimeConfig();
|
|
744
768
|
this.server = new Server({
|
|
745
769
|
name: "bambu-printer-mcp",
|
|
746
|
-
version: "
|
|
770
|
+
version: JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version
|
|
747
771
|
}, {
|
|
748
772
|
capabilities: {
|
|
749
773
|
resources: {},
|
|
@@ -821,17 +845,44 @@ class BambuPrinterMCPServer {
|
|
|
821
845
|
throw elicitError;
|
|
822
846
|
}
|
|
823
847
|
}
|
|
824
|
-
bridgeOptionsFromArgs(args) {
|
|
848
|
+
bridgeOptionsFromArgs(args, toolName) {
|
|
825
849
|
return {
|
|
826
|
-
bridgeCommand: args?.bridge_command
|
|
850
|
+
bridgeCommand: this.resolveExecutableSelectorArg(args?.bridge_command, toolName, "bridge_command", true),
|
|
827
851
|
configDir: args?.bambu_network_config_dir !== undefined ? String(args.bambu_network_config_dir) : undefined,
|
|
828
852
|
countryCode: args?.country_code !== undefined ? String(args.country_code) : undefined,
|
|
829
853
|
userInfo: args?.user_info !== undefined ? String(args.user_info) : undefined,
|
|
830
854
|
timeoutMs: args?.timeout_ms !== undefined ? Number(args.timeout_ms) : undefined,
|
|
831
855
|
};
|
|
832
856
|
}
|
|
857
|
+
resolveExecutableSelectorArg(rawValue, toolName, argumentName, allowLegacyBridgeArg = false) {
|
|
858
|
+
if (rawValue === undefined) {
|
|
859
|
+
return undefined;
|
|
860
|
+
}
|
|
861
|
+
const value = String(rawValue);
|
|
862
|
+
if (value.length === 0) {
|
|
863
|
+
return undefined;
|
|
864
|
+
}
|
|
865
|
+
const isAllowed = this.runtimeConfig.allowExecutableArg ||
|
|
866
|
+
(allowLegacyBridgeArg && this.runtimeConfig.allowBridgeCommandArg);
|
|
867
|
+
if (!isAllowed) {
|
|
868
|
+
throw new Error(`${toolName}: the ${argumentName} argument cannot select an executable by default. ` +
|
|
869
|
+
"Executable paths and commands are read from server environment configuration. Set " +
|
|
870
|
+
`MCP_ALLOW_EXECUTABLE_ARG=1 to accept ${argumentName} for process execution.`);
|
|
871
|
+
}
|
|
872
|
+
return value;
|
|
873
|
+
}
|
|
874
|
+
resolveSlicerConfigFromArgs(args, toolName) {
|
|
875
|
+
const config = resolveSlicerConfig(args);
|
|
876
|
+
if (args?.slicer_path !== undefined) {
|
|
877
|
+
const resolvedSlicerPath = this.resolveExecutableSelectorArg(args.slicer_path, toolName, "slicer_path");
|
|
878
|
+
if (resolvedSlicerPath !== undefined) {
|
|
879
|
+
config.slicerPath = resolvedSlicerPath;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return config;
|
|
883
|
+
}
|
|
833
884
|
async ensurePrintableThreeMFPath(args, printModel, printPreset, bedType) {
|
|
834
|
-
const { slicerType, slicerPath, slicerProfile } =
|
|
885
|
+
const { slicerType, slicerPath, slicerProfile } = this.resolveSlicerConfigFromArgs(args, "print_3mf_bambu_network");
|
|
835
886
|
let threeMFPath = String(args.three_mf_path);
|
|
836
887
|
const JSZip = (await import('jszip')).default;
|
|
837
888
|
const zipData = fs.readFileSync(threeMFPath);
|
|
@@ -993,7 +1044,7 @@ class BambuPrinterMCPServer {
|
|
|
993
1044
|
task_ext_change_assist: args?.external_change_assist !== undefined ? Boolean(args.external_change_assist) : false,
|
|
994
1045
|
try_emmc_print: args?.try_emmc_print !== undefined ? Boolean(args.try_emmc_print) : false,
|
|
995
1046
|
};
|
|
996
|
-
const bridgeResult = await this.bambuNetwork.callWithAgent(bridgeMethod, { client_job_id: clientJobId, params }, this.bridgeOptionsFromArgs(args));
|
|
1047
|
+
const bridgeResult = await this.bambuNetwork.callWithAgent(bridgeMethod, { client_job_id: clientJobId, params }, this.bridgeOptionsFromArgs(args, "print_3mf_bambu_network"));
|
|
997
1048
|
if (typeof bridgeResult === "object" && bridgeResult !== null && bridgeResult.ok === false) {
|
|
998
1049
|
throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} failed: ${String(bridgeResult.error || "unknown bridge error")}`);
|
|
999
1050
|
}
|
|
@@ -1484,7 +1535,7 @@ class BambuPrinterMCPServer {
|
|
|
1484
1535
|
enum: SLICER_SCHEMA_VALUES,
|
|
1485
1536
|
description: "Type of slicer to use. Bambu-compatible choices (bambustudio, orcaslicer, orcaslicer-bambulab) export sliced 3MF; aliases such as fulu-orca and orca-studio are accepted."
|
|
1486
1537
|
},
|
|
1487
|
-
slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env)" },
|
|
1538
|
+
slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env). Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
|
|
1488
1539
|
slicer_profile: { type: "string", description: "Explicit slicer profile/config file. Overrides the named template only when provided in the tool call." },
|
|
1489
1540
|
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm (default: 0.4)" },
|
|
1490
1541
|
bed_type: {
|
|
@@ -1530,7 +1581,7 @@ class BambuPrinterMCPServer {
|
|
|
1530
1581
|
enum: SLICER_SCHEMA_VALUES,
|
|
1531
1582
|
description: "Type of slicer to use. Bambu-compatible choices (bambustudio, orcaslicer, orcaslicer-bambulab) export sliced 3MF; aliases such as fulu-orca and orca-studio are accepted."
|
|
1532
1583
|
},
|
|
1533
|
-
slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env)" },
|
|
1584
|
+
slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env). Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
|
|
1534
1585
|
slicer_profile: { type: "string", description: "Path to the slicer profile/config file (optional, overrides bambu_model preset)" },
|
|
1535
1586
|
template_3mf_path: { type: "string", description: "Optional template 3MF whose embedded Bambu slicer settings should be reused when slicing a new STL or 3MF." },
|
|
1536
1587
|
template_name: { type: "string", description: "Optional named template from the local registry. Resolves to template_3mf_path automatically." },
|
|
@@ -1584,7 +1635,7 @@ class BambuPrinterMCPServer {
|
|
|
1584
1635
|
type: "object",
|
|
1585
1636
|
properties: {
|
|
1586
1637
|
connect: { type: "boolean", description: "When true, start the bridge command and run a handshake plus agent initialization probe." },
|
|
1587
|
-
bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND." },
|
|
1638
|
+
bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
|
|
1588
1639
|
bambu_network_config_dir: { type: "string", description: "Config/log directory used by the BambuNetwork agent; defaults to BAMBU_NETWORK_CONFIG_DIR or a user config directory." },
|
|
1589
1640
|
country_code: { type: "string", description: "BambuNetwork country code, such as US, used by the agent during startup." },
|
|
1590
1641
|
user_info: { type: "string", description: "Optional BambuNetwork user_info JSON string to pass to net.change_user after the agent starts." },
|
|
@@ -1601,7 +1652,7 @@ class BambuPrinterMCPServer {
|
|
|
1601
1652
|
method: { type: "string", description: "FULU bridge method name, for example bridge.handshake, net.is_user_login, or net.get_user_selected_machine." },
|
|
1602
1653
|
payload: { type: "object", description: "JSON payload passed to the bridge method." },
|
|
1603
1654
|
with_agent: { type: "boolean", description: "When true, initialize a BambuNetwork agent and add its agent id to the payload before calling the method." },
|
|
1604
|
-
bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND." },
|
|
1655
|
+
bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
|
|
1605
1656
|
bambu_network_config_dir: { type: "string", description: "Config/log directory used by the BambuNetwork agent; defaults to BAMBU_NETWORK_CONFIG_DIR or a user config directory." },
|
|
1606
1657
|
country_code: { type: "string", description: "BambuNetwork country code, such as US, used by the agent during startup." },
|
|
1607
1658
|
user_info: { type: "string", description: "Optional BambuNetwork user_info JSON string to pass to net.change_user after the agent starts." },
|
|
@@ -1637,13 +1688,13 @@ class BambuPrinterMCPServer {
|
|
|
1637
1688
|
preset_name: { type: "string", description: "Optional preset name sent in FULU PrintParams; defaults to project plus one-based plate index." },
|
|
1638
1689
|
task_name: { type: "string", description: "Optional BambuNetwork task name; defaults to the project name." },
|
|
1639
1690
|
config_filename: { type: "string", description: "Optional config 3MF path for cloud print; defaults to the same 3MF path." },
|
|
1640
|
-
bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND." },
|
|
1691
|
+
bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
|
|
1641
1692
|
bambu_network_config_dir: { type: "string", description: "Config/log directory used by the BambuNetwork agent; defaults to BAMBU_NETWORK_CONFIG_DIR or a user config directory." },
|
|
1642
1693
|
country_code: { type: "string", description: "BambuNetwork country code, such as US, used by the agent during startup." },
|
|
1643
1694
|
user_info: { type: "string", description: "Optional BambuNetwork user_info JSON string to pass to net.change_user after the agent starts." },
|
|
1644
1695
|
timeout_ms: { type: "number", description: "Bridge request timeout in milliseconds." },
|
|
1645
1696
|
slicer_type: { type: "string", enum: SLICER_SCHEMA_VALUES, description: "Slicer to use only if auto-slicing an unsliced 3MF; use orcaslicer-bambulab for FULU's fork." },
|
|
1646
|
-
slicer_path: { type: "string", description: "Path to the slicer executable for auto-slicing; defaults to value from env or a platform default." },
|
|
1697
|
+
slicer_path: { type: "string", description: "Path to the slicer executable for auto-slicing; defaults to value from env or a platform default. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
|
|
1647
1698
|
slicer_profile: { type: "string", description: "Path to an optional slicer profile/config file for auto-slicing." },
|
|
1648
1699
|
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm for auto-slicing (default: 0.4)." },
|
|
1649
1700
|
use_ams: { type: "boolean", description: "Whether to use the AMS; defaults to auto-detect from the 3MF mapping." },
|
|
@@ -1679,7 +1730,7 @@ class BambuPrinterMCPServer {
|
|
|
1679
1730
|
timeout_ms: { type: "number", description: "Max ms to wait for a full frame (default 8000). Camera may take a few seconds on cold start." },
|
|
1680
1731
|
bambu_model: { type: "string", description: "Printer model. Used to route to the correct protocol or fail fast on unsupported models. Defaults to BAMBU_MODEL." },
|
|
1681
1732
|
experimental: { type: "boolean", description: "Deprecated and ignored. Earlier this flag let callers probe H2 series via the A1/P1 TCP-on-6000 path; live testing on an H2S confirmed H2 uses RTSP instead, so the flag has no effect now." },
|
|
1682
|
-
ffmpeg_path: { type: "string", description: "Override path to the ffmpeg binary used by the RTSP path. Defaults to ffmpeg via $PATH. Required only for the RTSP transport (X1, P2S, H2 series)." },
|
|
1733
|
+
ffmpeg_path: { type: "string", description: "Override path to the ffmpeg binary used by the RTSP path. Defaults to FFMPEG_PATH or ffmpeg via $PATH. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1. Required only for the RTSP transport (X1, P2S, H2 series)." },
|
|
1683
1734
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
1684
1735
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
1685
1736
|
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
@@ -1997,7 +2048,7 @@ class BambuPrinterMCPServer {
|
|
|
1997
2048
|
bambu_network_method: { type: "string", enum: BAMBU_NETWORK_PRINT_METHODS, description: "FULU print method when connection_mode is bambu_network; defaults to start_print for cloud and start_local_print for lan." },
|
|
1998
2049
|
dev_id: { type: "string", description: "Bambu device id for FULU BambuNetwork printing; defaults to BAMBU_DEV_ID or BAMBU_SERIAL." },
|
|
1999
2050
|
dev_ip: { type: "string", description: "Printer IP address for FULU BambuNetwork LAN/local print methods; defaults to host when provided." },
|
|
2000
|
-
bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND." },
|
|
2051
|
+
bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
|
|
2001
2052
|
bambu_network_config_dir: { type: "string", description: "Config/log directory used by the FULU BambuNetwork agent." },
|
|
2002
2053
|
country_code: { type: "string", description: "BambuNetwork country code, such as US, used by the FULU bridge agent." },
|
|
2003
2054
|
user_info: { type: "string", description: "Optional BambuNetwork user_info JSON string passed to net.change_user for the FULU bridge." },
|
|
@@ -2015,7 +2066,7 @@ class BambuPrinterMCPServer {
|
|
|
2015
2066
|
enum: SLICER_SCHEMA_VALUES,
|
|
2016
2067
|
description: "Slicer to use only if auto-slicing an unsliced 3MF. Bambu-compatible slicer aliases such as fulu-orca and orca-studio are accepted."
|
|
2017
2068
|
},
|
|
2018
|
-
slicer_path: { type: "string", description: "Path to the slicer executable for auto-slicing (default: value from env or a platform default)" },
|
|
2069
|
+
slicer_path: { type: "string", description: "Path to the slicer executable for auto-slicing (default: value from env or a platform default). Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
|
|
2019
2070
|
use_ams: { type: "boolean", description: "Whether to use the AMS (default: auto-detect from 3MF)" },
|
|
2020
2071
|
ams_mapping: {
|
|
2021
2072
|
type: "array",
|
|
@@ -2110,28 +2161,62 @@ class BambuPrinterMCPServer {
|
|
|
2110
2161
|
required: ["stl_path"]
|
|
2111
2162
|
}
|
|
2112
2163
|
},
|
|
2164
|
+
{
|
|
2165
|
+
name: "blender_mcp_status",
|
|
2166
|
+
description: "Inspect Blender MCP configuration or connect and discover the remote server's tools and schemas. Connecting does not edit the scene; use get_scene_info through blender_mcp_call to check the Blender addon.",
|
|
2167
|
+
inputSchema: {
|
|
2168
|
+
type: "object",
|
|
2169
|
+
properties: {
|
|
2170
|
+
connect: { type: "boolean", description: "Initialize the configured stdio MCP server and discover its tools (default false)." },
|
|
2171
|
+
timeout_ms: { type: "integer", minimum: 100, maximum: 300000, description: "Total connection and discovery deadline in milliseconds; defaults to BLENDER_MCP_TIMEOUT_MS or 120000." }
|
|
2172
|
+
},
|
|
2173
|
+
additionalProperties: false
|
|
2174
|
+
}
|
|
2175
|
+
},
|
|
2176
|
+
{
|
|
2177
|
+
name: "blender_mcp_call",
|
|
2178
|
+
description: "Call a discovered tool on the configured Blender MCP server, preserving its full MCP content and errors. Discover tool schemas with blender_mcp_status first; execute_blender_code accepts Python code and user_prompt. Calls can modify the active Blender scene and are never automatically retried.",
|
|
2179
|
+
inputSchema: {
|
|
2180
|
+
type: "object",
|
|
2181
|
+
properties: {
|
|
2182
|
+
tool_name: { type: "string", description: "Exact name advertised by Blender MCP, such as get_scene_info or execute_blender_code." },
|
|
2183
|
+
arguments: { type: "object", description: "Arguments matching the remote tool's discovered input schema. Preserve the user's own words in user_prompt when the remote tool requests it." },
|
|
2184
|
+
timeout_ms: { type: "integer", minimum: 100, maximum: 300000, description: "Total connection, discovery, and tool deadline in milliseconds; defaults to BLENDER_MCP_TIMEOUT_MS or 120000." }
|
|
2185
|
+
},
|
|
2186
|
+
required: ["tool_name"],
|
|
2187
|
+
additionalProperties: false
|
|
2188
|
+
}
|
|
2189
|
+
},
|
|
2113
2190
|
{
|
|
2114
2191
|
name: "blender_mcp_edit_model",
|
|
2115
|
-
description: "
|
|
2192
|
+
description: "Import, edit, and export a local STL through standard Blender MCP with verified output and existing scene objects preserved. Requires a shared local filesystem and Blender Object Mode. Also supports a separately configured legacy executable bridge.",
|
|
2116
2193
|
inputSchema: {
|
|
2117
2194
|
type: "object",
|
|
2118
2195
|
properties: {
|
|
2119
2196
|
stl_path: { type: "string", description: "Path to the local STL file" },
|
|
2120
2197
|
operations: {
|
|
2121
2198
|
type: "array",
|
|
2122
|
-
description: "Ordered
|
|
2199
|
+
description: "Ordered operations: decimate:<ratio greater than 0 and at most 1>, remesh:<positive voxel size in STL units>, boolean_union:<STL path>. Legacy custom bridges define their own operations.",
|
|
2200
|
+
minItems: 1,
|
|
2201
|
+
maxItems: 64,
|
|
2123
2202
|
items: { type: "string" }
|
|
2124
2203
|
},
|
|
2125
|
-
|
|
2126
|
-
|
|
2204
|
+
output_path: { type: "string", description: "Required for standard MCP previews and execution: new local STL output path whose parent exists. Existing files are never overwritten. Optional for legacy-only bridge configuration." },
|
|
2205
|
+
user_prompt: { type: "string", description: "The user's own words describing the edit, passed unchanged to Blender MCP." },
|
|
2206
|
+
timeout_ms: { type: "integer", minimum: 100, maximum: 300000, description: "Total Blender request deadline in milliseconds; defaults to BLENDER_MCP_TIMEOUT_MS or 120000." },
|
|
2207
|
+
bridge_command: { type: "string", description: "Legacy custom bridge executable override, not a standard MCP command. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
|
|
2208
|
+
execute: { type: "boolean", description: "Apply edits and export (true) or validate and return the prepared request without connecting (false, default)." }
|
|
2127
2209
|
},
|
|
2128
|
-
required:
|
|
2210
|
+
required: process.env.BLENDER_MCP_COMMAND?.trim()
|
|
2211
|
+
? ["stl_path", "operations", "output_path"]
|
|
2212
|
+
: ["stl_path", "operations"],
|
|
2213
|
+
additionalProperties: false
|
|
2129
2214
|
}
|
|
2130
2215
|
}
|
|
2131
2216
|
]
|
|
2132
2217
|
};
|
|
2133
2218
|
});
|
|
2134
|
-
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2219
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
2135
2220
|
const { name, arguments: args } = request.params;
|
|
2136
2221
|
const host = String(args?.host || DEFAULT_HOST);
|
|
2137
2222
|
const bambuSerial = String(args?.bambu_serial || DEFAULT_BAMBU_SERIAL);
|
|
@@ -2195,7 +2280,7 @@ class BambuPrinterMCPServer {
|
|
|
2195
2280
|
break;
|
|
2196
2281
|
case "bambu_network_bridge_status": {
|
|
2197
2282
|
const bridgeArgs = args;
|
|
2198
|
-
const options = this.bridgeOptionsFromArgs(bridgeArgs);
|
|
2283
|
+
const options = this.bridgeOptionsFromArgs(bridgeArgs, "bambu_network_bridge_status");
|
|
2199
2284
|
result = this.bambuNetwork.getStatus(options);
|
|
2200
2285
|
if (Boolean(bridgeArgs?.connect)) {
|
|
2201
2286
|
const probe = await this.bambuNetwork.ensureAgent(options);
|
|
@@ -2216,7 +2301,7 @@ class BambuPrinterMCPServer {
|
|
|
2216
2301
|
const payload = bridgeArgs.payload && typeof bridgeArgs.payload === "object"
|
|
2217
2302
|
? bridgeArgs.payload
|
|
2218
2303
|
: {};
|
|
2219
|
-
const options = this.bridgeOptionsFromArgs(bridgeArgs);
|
|
2304
|
+
const options = this.bridgeOptionsFromArgs(bridgeArgs, "bambu_network_call");
|
|
2220
2305
|
result = bridgeArgs.with_agent === false
|
|
2221
2306
|
? await this.bambuNetwork.request(String(bridgeArgs.method), payload, options)
|
|
2222
2307
|
: await this.bambuNetwork.callWithAgent(String(bridgeArgs.method), payload, options);
|
|
@@ -2233,7 +2318,7 @@ class BambuPrinterMCPServer {
|
|
|
2233
2318
|
timeoutMs: args?.timeout_ms !== undefined ? Number(args.timeout_ms) : undefined,
|
|
2234
2319
|
bambuModel: snapshotModel ? String(snapshotModel) : undefined,
|
|
2235
2320
|
experimental: Boolean(args?.experimental),
|
|
2236
|
-
ffmpegPath: args?.ffmpeg_path
|
|
2321
|
+
ffmpegPath: this.resolveExecutableSelectorArg(args?.ffmpeg_path, "camera_snapshot", "ffmpeg_path") ?? this.runtimeConfig.ffmpegPath,
|
|
2237
2322
|
});
|
|
2238
2323
|
break;
|
|
2239
2324
|
}
|
|
@@ -2380,7 +2465,7 @@ class BambuPrinterMCPServer {
|
|
|
2380
2465
|
if (!args?.template_name) {
|
|
2381
2466
|
throw new Error("Missing required parameter: template_name");
|
|
2382
2467
|
}
|
|
2383
|
-
const { slicerType, slicerPath, slicerProfile } =
|
|
2468
|
+
const { slicerType, slicerPath, slicerProfile } = this.resolveSlicerConfigFromArgs(args, "slice_with_template");
|
|
2384
2469
|
const sliceModel = await this.resolveBambuModel(args?.bambu_model);
|
|
2385
2470
|
const nozzleDiam = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
2386
2471
|
const activeSlicerProfile = await resolveTemplateFirstSlicerProfilePath(args, slicerProfile || undefined, resolveTemplatePathFromName(["json", "config", "3mf"]) || explicitTemplatePath || undefined, TEMP_DIR);
|
|
@@ -2456,7 +2541,7 @@ class BambuPrinterMCPServer {
|
|
|
2456
2541
|
if (!args?.stl_path) {
|
|
2457
2542
|
throw new Error("Missing required parameter: stl_path");
|
|
2458
2543
|
}
|
|
2459
|
-
const { slicerType, slicerPath, slicerProfile } =
|
|
2544
|
+
const { slicerType, slicerPath, slicerProfile } = this.resolveSlicerConfigFromArgs(args, "slice_stl");
|
|
2460
2545
|
const sliceModel = await this.resolveBambuModel(args?.bambu_model);
|
|
2461
2546
|
const nozzleDiam = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
2462
2547
|
const activeSlicerProfile = await resolveSlicerProfilePath(slicerProfile || undefined, resolveTemplatePathFromName(["json", "config", "3mf"]) || explicitTemplatePath || undefined, TEMP_DIR);
|
|
@@ -2541,7 +2626,7 @@ class BambuPrinterMCPServer {
|
|
|
2541
2626
|
if (!bambuSerial || !bambuToken) {
|
|
2542
2627
|
throw new Error("Bambu serial number and access token are required for print_3mf.");
|
|
2543
2628
|
}
|
|
2544
|
-
const { slicerType, slicerPath, slicerProfile } =
|
|
2629
|
+
const { slicerType, slicerPath, slicerProfile } = this.resolveSlicerConfigFromArgs(args, "print_3mf");
|
|
2545
2630
|
const printModel = await this.resolveBambuModel(args?.bambu_model);
|
|
2546
2631
|
const printBedType = resolveBedType(args?.bed_type);
|
|
2547
2632
|
const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
@@ -2553,45 +2638,37 @@ class BambuPrinterMCPServer {
|
|
|
2553
2638
|
throw new Error("plate_index must be a non-negative integer.");
|
|
2554
2639
|
}
|
|
2555
2640
|
let threeMFPath = String(args.three_mf_path);
|
|
2556
|
-
//
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
if (
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
autoSliceOptions.loadFilaments = liveFilaments.recommended.load_filaments;
|
|
2579
|
-
}
|
|
2580
|
-
}
|
|
2581
|
-
catch (filamentError) {
|
|
2582
|
-
console.warn("Could not resolve live printer filaments for auto-slicing:", filamentError);
|
|
2641
|
+
// Inspect and auto-slice before upload. Preserve any failure instead of sending an unsliced project.
|
|
2642
|
+
const JSZip = (await import('jszip')).default;
|
|
2643
|
+
const zipData = fs.readFileSync(threeMFPath);
|
|
2644
|
+
const zip = await JSZip.loadAsync(zipData);
|
|
2645
|
+
const hasGcode = Object.values(zip.files).some(entry => !entry.dir && /\.gcode$/i.test(entry.name));
|
|
2646
|
+
if (!hasGcode) {
|
|
2647
|
+
if (printBedType === "supertack_plate") {
|
|
2648
|
+
throw new Error('BambuStudio CLI SuperTack bed type is not verified; use a pre-sliced 3MF for SuperTack or choose textured_plate, cool_plate, engineering_plate, or hot_plate.');
|
|
2649
|
+
}
|
|
2650
|
+
console.log(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
|
|
2651
|
+
const autoSliceOptions = {
|
|
2652
|
+
uptodate: true,
|
|
2653
|
+
ensureOnBed: true,
|
|
2654
|
+
minSave: true,
|
|
2655
|
+
skipModifiedGcodes: true,
|
|
2656
|
+
bedType: printBedType,
|
|
2657
|
+
};
|
|
2658
|
+
if (!explicitSlicerProfile) {
|
|
2659
|
+
try {
|
|
2660
|
+
const liveFilaments = await this.getResolvedPrinterFilamentInventory(host, bambuSerial, bambuToken, printModel, printNozzle);
|
|
2661
|
+
if (liveFilaments.recommended?.load_filaments) {
|
|
2662
|
+
autoSliceOptions.loadFilaments = liveFilaments.recommended.load_filaments;
|
|
2583
2663
|
}
|
|
2584
2664
|
}
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
}
|
|
2589
|
-
}
|
|
2590
|
-
catch (sliceCheckErr) {
|
|
2591
|
-
if (String(sliceCheckErr?.message || "").includes("SuperTack")) {
|
|
2592
|
-
throw sliceCheckErr;
|
|
2665
|
+
catch (filamentError) {
|
|
2666
|
+
console.warn("Could not resolve live printer filaments for auto-slicing:", filamentError);
|
|
2667
|
+
}
|
|
2593
2668
|
}
|
|
2594
|
-
|
|
2669
|
+
threeMFPath = await this.stlManipulator.sliceSTL(threeMFPath, slicerType, slicerPath, activeSlicerProfile, undefined, // progressCallback
|
|
2670
|
+
printPreset, autoSliceOptions);
|
|
2671
|
+
console.log("Auto-sliced to: " + threeMFPath);
|
|
2595
2672
|
}
|
|
2596
2673
|
const parsed3MFData = await parse3MF(threeMFPath);
|
|
2597
2674
|
const isH2Print = H2_BAMBU_MODELS.has(printModel);
|
|
@@ -2689,7 +2766,7 @@ class BambuPrinterMCPServer {
|
|
|
2689
2766
|
if (!bambuSerial || !bambuToken) {
|
|
2690
2767
|
throw new Error("Bambu serial number and access token are required for print_collar_charm.");
|
|
2691
2768
|
}
|
|
2692
|
-
const { slicerType, slicerPath, slicerProfile } =
|
|
2769
|
+
const { slicerType, slicerPath, slicerProfile } = this.resolveSlicerConfigFromArgs(args, "print_collar_charm");
|
|
2693
2770
|
const printModel = await this.resolveBambuModel(args?.bambu_model);
|
|
2694
2771
|
const printBedType = resolveBedType(args?.bed_type);
|
|
2695
2772
|
const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
@@ -2753,18 +2830,13 @@ class BambuPrinterMCPServer {
|
|
|
2753
2830
|
throw new Error("Missing required parameter: stl_path");
|
|
2754
2831
|
result = await this.stlManipulator.layFlat(String(args.stl_path));
|
|
2755
2832
|
break;
|
|
2833
|
+
case "blender_mcp_status":
|
|
2834
|
+
result = await this.blender.status(args ?? {}, extra.signal);
|
|
2835
|
+
break;
|
|
2836
|
+
case "blender_mcp_call":
|
|
2837
|
+
return await this.blender.call(args ?? {}, extra.signal);
|
|
2756
2838
|
case "blender_mcp_edit_model":
|
|
2757
|
-
|
|
2758
|
-
throw new Error("Missing required parameters: stl_path and operations");
|
|
2759
|
-
}
|
|
2760
|
-
result = await this.invokeBlenderBridge({
|
|
2761
|
-
stlPath: String(args.stl_path),
|
|
2762
|
-
operations: args.operations.map((entry) => String(entry)),
|
|
2763
|
-
execute: Boolean(args.execute ?? false),
|
|
2764
|
-
bridgeCommand: args.bridge_command
|
|
2765
|
-
? String(args.bridge_command)
|
|
2766
|
-
: this.runtimeConfig.blenderBridgeCommand,
|
|
2767
|
-
});
|
|
2839
|
+
result = await this.blender.edit(args ?? {}, this.resolveExecutableSelectorArg(args?.bridge_command, "blender_mcp_edit_model", "bridge_command", true) ?? (!process.env.BLENDER_MCP_COMMAND?.trim() ? this.runtimeConfig.blenderBridgeCommand : undefined), extra.signal);
|
|
2768
2840
|
break;
|
|
2769
2841
|
default:
|
|
2770
2842
|
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
@@ -2795,38 +2867,28 @@ class BambuPrinterMCPServer {
|
|
|
2795
2867
|
}
|
|
2796
2868
|
});
|
|
2797
2869
|
}
|
|
2798
|
-
async invokeBlenderBridge(params) {
|
|
2799
|
-
const payload = {
|
|
2800
|
-
stlPath: params.stlPath,
|
|
2801
|
-
operations: params.operations,
|
|
2802
|
-
};
|
|
2803
|
-
if (!params.execute || !params.bridgeCommand) {
|
|
2804
|
-
return {
|
|
2805
|
-
status: "prepared",
|
|
2806
|
-
payload,
|
|
2807
|
-
note: params.bridgeCommand
|
|
2808
|
-
? "Set execute=true to run the Blender bridge command."
|
|
2809
|
-
: "No BLENDER_MCP_BRIDGE_COMMAND configured. Set the env var or pass bridge_command.",
|
|
2810
|
-
};
|
|
2811
|
-
}
|
|
2812
|
-
const { execFile } = await import("node:child_process");
|
|
2813
|
-
const { promisify } = await import("node:util");
|
|
2814
|
-
const execFileAsync = promisify(execFile);
|
|
2815
|
-
const { stdout, stderr } = await execFileAsync(params.bridgeCommand, [], {
|
|
2816
|
-
env: { ...process.env, MCP_BLENDER_PAYLOAD: JSON.stringify(payload) },
|
|
2817
|
-
timeout: 120000,
|
|
2818
|
-
});
|
|
2819
|
-
return {
|
|
2820
|
-
status: "executed",
|
|
2821
|
-
stdout: stdout.trim(),
|
|
2822
|
-
stderr: stderr.trim(),
|
|
2823
|
-
};
|
|
2824
|
-
}
|
|
2825
2870
|
async startStdio() {
|
|
2826
2871
|
const transport = new StdioServerTransport();
|
|
2827
2872
|
await this.server.connect(transport);
|
|
2873
|
+
process.stdin.once("end", () => { void this.shutdown(); });
|
|
2828
2874
|
console.error("Bambu Printer MCP server running on stdio");
|
|
2829
2875
|
}
|
|
2876
|
+
async shutdown(exitCode = 0) {
|
|
2877
|
+
if (this.shuttingDown)
|
|
2878
|
+
return;
|
|
2879
|
+
this.shuttingDown = true;
|
|
2880
|
+
try {
|
|
2881
|
+
// Closing the MCP transport aborts its active requests before exit cleanup.
|
|
2882
|
+
await this.server.close();
|
|
2883
|
+
this.httpRuntime?.httpServer.close();
|
|
2884
|
+
}
|
|
2885
|
+
catch (error) {
|
|
2886
|
+
console.error("[MCP Shutdown]", error);
|
|
2887
|
+
}
|
|
2888
|
+
finally {
|
|
2889
|
+
process.exit(exitCode);
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2830
2892
|
async startHttp() {
|
|
2831
2893
|
const { httpHost, httpPort, httpPath, statefulSession, enableJsonResponse, allowedOrigins } = this.runtimeConfig;
|
|
2832
2894
|
const transport = new StreamableHTTPServerTransport({
|
|
@@ -2866,4 +2928,6 @@ class BambuPrinterMCPServer {
|
|
|
2866
2928
|
}
|
|
2867
2929
|
}
|
|
2868
2930
|
const server = new BambuPrinterMCPServer();
|
|
2931
|
+
process.once("SIGINT", () => { void server.shutdown(130); });
|
|
2932
|
+
process.once("SIGTERM", () => { void server.shutdown(143); });
|
|
2869
2933
|
server.run().catch(console.error);
|
package/dist/printers/bambu.js
CHANGED
|
@@ -49,6 +49,9 @@ const H2_MODEL_NAMES = new Set(["h2", "h2c", "h2d", "h2dpro", "h2d pro", "h2s"])
|
|
|
49
49
|
function isH2ModelName(model) {
|
|
50
50
|
return H2_MODEL_NAMES.has(String(model ?? "").trim().toLowerCase().replace(/\s+/g, " "));
|
|
51
51
|
}
|
|
52
|
+
function isP2SModelName(model) {
|
|
53
|
+
return String(model ?? "").trim().toLowerCase() === "p2s";
|
|
54
|
+
}
|
|
52
55
|
function sleep(ms) {
|
|
53
56
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
54
57
|
}
|
|
@@ -88,6 +91,10 @@ class TolerantBambuClient extends BambuClient {
|
|
|
88
91
|
return "H2S";
|
|
89
92
|
if (sn.startsWith("094"))
|
|
90
93
|
return "H2D";
|
|
94
|
+
if (sn.startsWith("239"))
|
|
95
|
+
return "H2C";
|
|
96
|
+
if (sn.startsWith("31B"))
|
|
97
|
+
return "H2DPRO";
|
|
91
98
|
if (sn.startsWith("00M"))
|
|
92
99
|
return "X1C";
|
|
93
100
|
if (sn.startsWith("00W"))
|
|
@@ -98,6 +105,8 @@ class TolerantBambuClient extends BambuClient {
|
|
|
98
105
|
return "P1P";
|
|
99
106
|
if (sn.startsWith("01P"))
|
|
100
107
|
return "P1S";
|
|
108
|
+
if (sn.startsWith("22E"))
|
|
109
|
+
return "P2S";
|
|
101
110
|
if (sn.startsWith("030"))
|
|
102
111
|
return "A1";
|
|
103
112
|
if (sn.startsWith("039"))
|
|
@@ -457,24 +466,33 @@ export class BambuImplementation {
|
|
|
457
466
|
let remoteFileName = path.basename(options.filePath);
|
|
458
467
|
remoteFileName = remoteFileName.replace(/\.gcode\.3mf\.gcode\.3mf$/i, ".gcode.3mf");
|
|
459
468
|
// H2-series printers land files at the FTP root and reference them via ftp:///<name>.
|
|
460
|
-
//
|
|
469
|
+
// Full-size A1 uses SD root/project_file (reported on firmware 01.08.01.00).
|
|
470
|
+
// P1/X1/A1 mini retain their legacy cache/gcode_file route.
|
|
471
|
+
// P2S keeps the /cache/<name> upload but only accepts the H2-style
|
|
472
|
+
// project_file command, referenced via ftp:///cache/<name> (verified on
|
|
473
|
+
// P2S firmware 01.02.00.00; file:///sdcard/... fails with ERROR STATE).
|
|
461
474
|
const isH2 = serial.startsWith("093") ||
|
|
462
475
|
serial.startsWith("094") ||
|
|
463
476
|
isH2ModelName(options.bambuModel);
|
|
464
|
-
const
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
|
|
477
|
+
const isP2S = serial.startsWith("22E") || isP2SModelName(options.bambuModel);
|
|
478
|
+
const isA1 = String(options.bambuModel ?? "").trim().toLowerCase() === "a1" ||
|
|
479
|
+
(!options.bambuModel && serial.startsWith("030"));
|
|
480
|
+
const usesH2ProjectFile = isH2 || isP2S;
|
|
481
|
+
const remoteProjectPath = isH2 || isA1 ? remoteFileName : `cache/${remoteFileName}`;
|
|
482
|
+
const remoteUploadPath = `/${remoteProjectPath}`;
|
|
483
|
+
const projectUrl = usesH2ProjectFile
|
|
484
|
+
? `ftp:///${remoteProjectPath}`
|
|
468
485
|
: `file:///sdcard/${remoteProjectPath}`;
|
|
469
486
|
// Upload via basic-ftp directly (bypasses bambu-js double-path bug)
|
|
470
487
|
await this.ftpUpload(host, token, options.filePath, remoteUploadPath);
|
|
471
488
|
// Pre-sliced .gcode.3mf files: routing depends on firmware generation.
|
|
472
|
-
// P1/A1
|
|
489
|
+
// Legacy P1/X1/A1 mini: project_file returns 405004002 for .gcode.3mf (firmware
|
|
473
490
|
// doesn't recognise the container), so use gcode_file instead.
|
|
474
|
-
// H2-series: gcode_file is not supported
|
|
491
|
+
// H2-series and P2S: gcode_file is not supported (P2S answers 0500-4002
|
|
492
|
+
// "Unsupported file path or name"); project_file works because the
|
|
475
493
|
// firmware can open the zip and find Metadata/plate_<n>.gcode directly.
|
|
476
494
|
if (options.filePath.toLowerCase().endsWith(".gcode.3mf")) {
|
|
477
|
-
if (!
|
|
495
|
+
if (!usesH2ProjectFile && !isA1) {
|
|
478
496
|
const printer = await this.getPrinter(host, serial, token);
|
|
479
497
|
await invokeWithoutAck(printer, new GCodeFileCommand({ fileName: remoteProjectPath }));
|
|
480
498
|
return {
|
|
@@ -483,7 +501,7 @@ export class BambuImplementation {
|
|
|
483
501
|
remoteProjectPath,
|
|
484
502
|
};
|
|
485
503
|
}
|
|
486
|
-
// H2-series: fall through to project_file path below
|
|
504
|
+
// H2-series and P2S: fall through to project_file path below
|
|
487
505
|
}
|
|
488
506
|
const projectMetadata = await this.resolveProjectFileMetadata(options.filePath, options.plateIndex);
|
|
489
507
|
// Send project_file command via bambu-node MQTT (bypasses bambu-js
|
|
@@ -550,7 +568,7 @@ export class BambuImplementation {
|
|
|
550
568
|
}
|
|
551
569
|
let amsMapping;
|
|
552
570
|
let amsMapping2;
|
|
553
|
-
if (
|
|
571
|
+
if (usesH2ProjectFile) {
|
|
554
572
|
const projLen = Math.max(projectMetadata.projectFilamentCount, baseMapping.length, 1);
|
|
555
573
|
amsMapping = Array.from({ length: projLen }, (_, i) => i < baseMapping.length ? baseMapping[i] : -1);
|
|
556
574
|
amsMapping2 = amsMapping.map((v) => {
|
|
@@ -564,12 +582,12 @@ export class BambuImplementation {
|
|
|
564
582
|
});
|
|
565
583
|
}
|
|
566
584
|
else {
|
|
567
|
-
amsMapping = Array.from({ length: 5 }, (_, i) => i < baseMapping.length ? baseMapping[i] : -1);
|
|
585
|
+
amsMapping = Array.from({ length: Math.max(5, baseMapping.length, projectMetadata.projectFilamentCount) }, (_, i) => i < baseMapping.length ? baseMapping[i] : -1);
|
|
568
586
|
amsMapping2 = [];
|
|
569
587
|
}
|
|
570
588
|
const b = (v) => (v ? 1 : 0);
|
|
571
589
|
let projectFileCmd;
|
|
572
|
-
if (
|
|
590
|
+
if (usesH2ProjectFile) {
|
|
573
591
|
const submissionId = String(Date.now() & 0x7fffffff);
|
|
574
592
|
projectFileCmd = {
|
|
575
593
|
print: {
|
|
@@ -994,7 +1012,7 @@ export class BambuImplementation {
|
|
|
994
1012
|
// integration's models.py shows the printer reports its own
|
|
995
1013
|
// `ipcam.rtsp_url` for these models, and Parker (H2S) rejects the
|
|
996
1014
|
// A1/P1 80-byte auth packet on port 6000 (verified 2026-04-27 --
|
|
997
|
-
//
|
|
1015
|
+
// confirmed by local H2 camera transport probes).
|
|
998
1016
|
const RTSP_MODELS = new Set([
|
|
999
1017
|
"x1", "x1c", "x1carbon", "x1e", "p2s",
|
|
1000
1018
|
"h2", "h2s", "h2d", "h2c", "h2dpro",
|