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/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
14
14
|
import dotenv from "dotenv";
|
|
15
15
|
import fs from "fs";
|
|
16
|
+
import os from "node:os";
|
|
16
17
|
import path from "path";
|
|
17
18
|
import { createServer as createHttpServer, type IncomingMessage, type Server as HttpServer, type ServerResponse } from "node:http";
|
|
18
19
|
import { randomUUID } from "node:crypto";
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
type SlicerType,
|
|
25
26
|
} from "./stl/stl-manipulator.js";
|
|
26
27
|
import { BambuNetworkBridge, type BambuNetworkBridgeOptions } from "./bambu-network-bridge.js";
|
|
28
|
+
import { BlenderMcpBridge } from "./blender-mcp-bridge.js";
|
|
27
29
|
import { hasAmsMappingInput, normalizeAmsMappingObject, normalizeBridgeAmsTrayValue } from "./ams-mapping.js";
|
|
28
30
|
import { analyze3MFAmsRequirements, analyze3MFPlateObjects, analyzeCollarCharm3MF, extractBambuTemplateSettings, getCollarCharmRolePolicy, parse3MF } from './3mf_parser.js';
|
|
29
31
|
import type { ThreeMFAmsRequirements } from "./types.js";
|
|
@@ -36,7 +38,23 @@ const DEFAULT_BAMBU_SERIAL = process.env.BAMBU_PRINTER_SERIAL || process.env.BAM
|
|
|
36
38
|
const DEFAULT_BAMBU_TOKEN =
|
|
37
39
|
process.env.BAMBU_PRINTER_ACCESS_TOKEN || process.env.BAMBU_TOKEN || "";
|
|
38
40
|
const DEFAULT_BAMBU_DEV_ID = process.env.BAMBU_DEV_ID || DEFAULT_BAMBU_SERIAL;
|
|
39
|
-
|
|
41
|
+
// os.tmpdir(), not process.cwd() — a packaged Claude Desktop extension is
|
|
42
|
+
// spawned with a cwd we don't control (and may not be writable), so a
|
|
43
|
+
// cwd-relative default here can throw at module load via the mkdirSync
|
|
44
|
+
// below and kill the server before it ever opens the stdio transport.
|
|
45
|
+
const AUTOMATIC_TEMP_DIR = process.env.TEMP_DIR ? undefined : fs.mkdtempSync(path.join(os.tmpdir(), "bambu-printer-mcp-"));
|
|
46
|
+
const TEMP_DIR = process.env.TEMP_DIR || AUTOMATIC_TEMP_DIR!;
|
|
47
|
+
if (AUTOMATIC_TEMP_DIR) {
|
|
48
|
+
// The exit event also covers startup failures and normal event-loop exit.
|
|
49
|
+
// Only the directory created above belongs to us; explicit TEMP_DIR is retained.
|
|
50
|
+
process.once("exit", () => {
|
|
51
|
+
try {
|
|
52
|
+
fs.rmSync(AUTOMATIC_TEMP_DIR, { recursive: true, force: true });
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error("Unable to remove the server's temporary directory:", error);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
40
58
|
|
|
41
59
|
// Printer model and bed type
|
|
42
60
|
const DEFAULT_BAMBU_MODEL =
|
|
@@ -601,6 +619,9 @@ type RuntimeConfig = {
|
|
|
601
619
|
enableJsonResponse: boolean;
|
|
602
620
|
allowedOrigins: Set<string>;
|
|
603
621
|
blenderBridgeCommand?: string;
|
|
622
|
+
ffmpegPath?: string;
|
|
623
|
+
allowExecutableArg: boolean;
|
|
624
|
+
allowBridgeCommandArg: boolean;
|
|
604
625
|
};
|
|
605
626
|
|
|
606
627
|
function parseBooleanEnv(rawValue: string | undefined, fallback: boolean): boolean {
|
|
@@ -698,6 +719,9 @@ function readRuntimeConfig(): RuntimeConfig {
|
|
|
698
719
|
enableJsonResponse: parseBooleanEnv(process.env.MCP_HTTP_JSON_RESPONSE, true),
|
|
699
720
|
allowedOrigins: parseCsvEnv(process.env.MCP_HTTP_ALLOWED_ORIGINS),
|
|
700
721
|
blenderBridgeCommand: process.env.BLENDER_MCP_BRIDGE_COMMAND?.trim() || undefined,
|
|
722
|
+
ffmpegPath: process.env.FFMPEG_PATH?.trim() || undefined,
|
|
723
|
+
allowExecutableArg: parseBooleanEnv(process.env.MCP_ALLOW_EXECUTABLE_ARG, false),
|
|
724
|
+
allowBridgeCommandArg: parseBooleanEnv(process.env.MCP_ALLOW_BRIDGE_COMMAND_ARG, false),
|
|
701
725
|
};
|
|
702
726
|
}
|
|
703
727
|
|
|
@@ -1009,16 +1033,18 @@ class BambuPrinterMCPServer {
|
|
|
1009
1033
|
private server: Server;
|
|
1010
1034
|
private bambu: BambuImplementation;
|
|
1011
1035
|
private bambuNetwork: BambuNetworkBridge;
|
|
1036
|
+
private readonly blender = new BlenderMcpBridge();
|
|
1012
1037
|
private stlManipulator: STLManipulator;
|
|
1013
1038
|
private readonly runtimeConfig: RuntimeConfig;
|
|
1014
1039
|
private httpRuntime?: { transport: StreamableHTTPServerTransport; httpServer: HttpServer };
|
|
1040
|
+
private shuttingDown = false;
|
|
1015
1041
|
|
|
1016
1042
|
constructor() {
|
|
1017
1043
|
this.runtimeConfig = readRuntimeConfig();
|
|
1018
1044
|
this.server = new Server(
|
|
1019
1045
|
{
|
|
1020
1046
|
name: "bambu-printer-mcp",
|
|
1021
|
-
version: "
|
|
1047
|
+
version: JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version
|
|
1022
1048
|
},
|
|
1023
1049
|
{
|
|
1024
1050
|
capabilities: {
|
|
@@ -1113,9 +1139,17 @@ class BambuPrinterMCPServer {
|
|
|
1113
1139
|
}
|
|
1114
1140
|
}
|
|
1115
1141
|
|
|
1116
|
-
private bridgeOptionsFromArgs(
|
|
1142
|
+
private bridgeOptionsFromArgs(
|
|
1143
|
+
args: Record<string, any> | undefined,
|
|
1144
|
+
toolName: string
|
|
1145
|
+
): BambuNetworkBridgeOptions {
|
|
1117
1146
|
return {
|
|
1118
|
-
bridgeCommand:
|
|
1147
|
+
bridgeCommand: this.resolveExecutableSelectorArg(
|
|
1148
|
+
args?.bridge_command,
|
|
1149
|
+
toolName,
|
|
1150
|
+
"bridge_command",
|
|
1151
|
+
true
|
|
1152
|
+
),
|
|
1119
1153
|
configDir: args?.bambu_network_config_dir !== undefined ? String(args.bambu_network_config_dir) : undefined,
|
|
1120
1154
|
countryCode: args?.country_code !== undefined ? String(args.country_code) : undefined,
|
|
1121
1155
|
userInfo: args?.user_info !== undefined ? String(args.user_info) : undefined,
|
|
@@ -1123,13 +1157,61 @@ class BambuPrinterMCPServer {
|
|
|
1123
1157
|
};
|
|
1124
1158
|
}
|
|
1125
1159
|
|
|
1160
|
+
private resolveExecutableSelectorArg(
|
|
1161
|
+
rawValue: unknown,
|
|
1162
|
+
toolName: string,
|
|
1163
|
+
argumentName: string,
|
|
1164
|
+
allowLegacyBridgeArg = false
|
|
1165
|
+
): string | undefined {
|
|
1166
|
+
if (rawValue === undefined) {
|
|
1167
|
+
return undefined;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
const value = String(rawValue);
|
|
1171
|
+
if (value.length === 0) {
|
|
1172
|
+
return undefined;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
const isAllowed =
|
|
1176
|
+
this.runtimeConfig.allowExecutableArg ||
|
|
1177
|
+
(allowLegacyBridgeArg && this.runtimeConfig.allowBridgeCommandArg);
|
|
1178
|
+
if (!isAllowed) {
|
|
1179
|
+
throw new Error(
|
|
1180
|
+
`${toolName}: the ${argumentName} argument cannot select an executable by default. ` +
|
|
1181
|
+
"Executable paths and commands are read from server environment configuration. Set " +
|
|
1182
|
+
`MCP_ALLOW_EXECUTABLE_ARG=1 to accept ${argumentName} for process execution.`
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
return value;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
private resolveSlicerConfigFromArgs(
|
|
1190
|
+
args: Record<string, unknown> | undefined,
|
|
1191
|
+
toolName: string
|
|
1192
|
+
): ReturnType<typeof resolveSlicerConfig> {
|
|
1193
|
+
const config = resolveSlicerConfig(args);
|
|
1194
|
+
if (args?.slicer_path !== undefined) {
|
|
1195
|
+
const resolvedSlicerPath = this.resolveExecutableSelectorArg(
|
|
1196
|
+
args.slicer_path,
|
|
1197
|
+
toolName,
|
|
1198
|
+
"slicer_path"
|
|
1199
|
+
);
|
|
1200
|
+
if (resolvedSlicerPath !== undefined) {
|
|
1201
|
+
config.slicerPath = resolvedSlicerPath;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
return config;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1126
1207
|
private async ensurePrintableThreeMFPath(
|
|
1127
1208
|
args: Record<string, any>,
|
|
1128
1209
|
printModel: string,
|
|
1129
1210
|
printPreset: string | undefined,
|
|
1130
1211
|
bedType?: string
|
|
1131
1212
|
): Promise<{ threeMFPath: string; autoSliced: boolean }> {
|
|
1132
|
-
const { slicerType, slicerPath, slicerProfile } =
|
|
1213
|
+
const { slicerType, slicerPath, slicerProfile } =
|
|
1214
|
+
this.resolveSlicerConfigFromArgs(args, "print_3mf_bambu_network");
|
|
1133
1215
|
let threeMFPath = String(args.three_mf_path);
|
|
1134
1216
|
|
|
1135
1217
|
const JSZip = (await import('jszip')).default;
|
|
@@ -1366,7 +1448,7 @@ class BambuPrinterMCPServer {
|
|
|
1366
1448
|
const bridgeResult = await this.bambuNetwork.callWithAgent(
|
|
1367
1449
|
bridgeMethod,
|
|
1368
1450
|
{ client_job_id: clientJobId, params },
|
|
1369
|
-
this.bridgeOptionsFromArgs(args)
|
|
1451
|
+
this.bridgeOptionsFromArgs(args, "print_3mf_bambu_network")
|
|
1370
1452
|
);
|
|
1371
1453
|
|
|
1372
1454
|
if (typeof bridgeResult === "object" && bridgeResult !== null && (bridgeResult as { ok?: unknown }).ok === false) {
|
|
@@ -1949,7 +2031,7 @@ class BambuPrinterMCPServer {
|
|
|
1949
2031
|
enum: SLICER_SCHEMA_VALUES,
|
|
1950
2032
|
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."
|
|
1951
2033
|
},
|
|
1952
|
-
slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env)" },
|
|
2034
|
+
slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env). Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
|
|
1953
2035
|
slicer_profile: { type: "string", description: "Explicit slicer profile/config file. Overrides the named template only when provided in the tool call." },
|
|
1954
2036
|
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm (default: 0.4)" },
|
|
1955
2037
|
bed_type: {
|
|
@@ -1995,7 +2077,7 @@ class BambuPrinterMCPServer {
|
|
|
1995
2077
|
enum: SLICER_SCHEMA_VALUES,
|
|
1996
2078
|
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."
|
|
1997
2079
|
},
|
|
1998
|
-
slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env)" },
|
|
2080
|
+
slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env). Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
|
|
1999
2081
|
slicer_profile: { type: "string", description: "Path to the slicer profile/config file (optional, overrides bambu_model preset)" },
|
|
2000
2082
|
template_3mf_path: { type: "string", description: "Optional template 3MF whose embedded Bambu slicer settings should be reused when slicing a new STL or 3MF." },
|
|
2001
2083
|
template_name: { type: "string", description: "Optional named template from the local registry. Resolves to template_3mf_path automatically." },
|
|
@@ -2049,7 +2131,7 @@ class BambuPrinterMCPServer {
|
|
|
2049
2131
|
type: "object",
|
|
2050
2132
|
properties: {
|
|
2051
2133
|
connect: { type: "boolean", description: "When true, start the bridge command and run a handshake plus agent initialization probe." },
|
|
2052
|
-
bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND." },
|
|
2134
|
+
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." },
|
|
2053
2135
|
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." },
|
|
2054
2136
|
country_code: { type: "string", description: "BambuNetwork country code, such as US, used by the agent during startup." },
|
|
2055
2137
|
user_info: { type: "string", description: "Optional BambuNetwork user_info JSON string to pass to net.change_user after the agent starts." },
|
|
@@ -2066,7 +2148,7 @@ class BambuPrinterMCPServer {
|
|
|
2066
2148
|
method: { type: "string", description: "FULU bridge method name, for example bridge.handshake, net.is_user_login, or net.get_user_selected_machine." },
|
|
2067
2149
|
payload: { type: "object", description: "JSON payload passed to the bridge method." },
|
|
2068
2150
|
with_agent: { type: "boolean", description: "When true, initialize a BambuNetwork agent and add its agent id to the payload before calling the method." },
|
|
2069
|
-
bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND." },
|
|
2151
|
+
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." },
|
|
2070
2152
|
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." },
|
|
2071
2153
|
country_code: { type: "string", description: "BambuNetwork country code, such as US, used by the agent during startup." },
|
|
2072
2154
|
user_info: { type: "string", description: "Optional BambuNetwork user_info JSON string to pass to net.change_user after the agent starts." },
|
|
@@ -2102,13 +2184,13 @@ class BambuPrinterMCPServer {
|
|
|
2102
2184
|
preset_name: { type: "string", description: "Optional preset name sent in FULU PrintParams; defaults to project plus one-based plate index." },
|
|
2103
2185
|
task_name: { type: "string", description: "Optional BambuNetwork task name; defaults to the project name." },
|
|
2104
2186
|
config_filename: { type: "string", description: "Optional config 3MF path for cloud print; defaults to the same 3MF path." },
|
|
2105
|
-
bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND." },
|
|
2187
|
+
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." },
|
|
2106
2188
|
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." },
|
|
2107
2189
|
country_code: { type: "string", description: "BambuNetwork country code, such as US, used by the agent during startup." },
|
|
2108
2190
|
user_info: { type: "string", description: "Optional BambuNetwork user_info JSON string to pass to net.change_user after the agent starts." },
|
|
2109
2191
|
timeout_ms: { type: "number", description: "Bridge request timeout in milliseconds." },
|
|
2110
2192
|
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." },
|
|
2111
|
-
slicer_path: { type: "string", description: "Path to the slicer executable for auto-slicing; defaults to value from env or a platform default." },
|
|
2193
|
+
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." },
|
|
2112
2194
|
slicer_profile: { type: "string", description: "Path to an optional slicer profile/config file for auto-slicing." },
|
|
2113
2195
|
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm for auto-slicing (default: 0.4)." },
|
|
2114
2196
|
use_ams: { type: "boolean", description: "Whether to use the AMS; defaults to auto-detect from the 3MF mapping." },
|
|
@@ -2144,7 +2226,7 @@ class BambuPrinterMCPServer {
|
|
|
2144
2226
|
timeout_ms: { type: "number", description: "Max ms to wait for a full frame (default 8000). Camera may take a few seconds on cold start." },
|
|
2145
2227
|
bambu_model: { type: "string", description: "Printer model. Used to route to the correct protocol or fail fast on unsupported models. Defaults to BAMBU_MODEL." },
|
|
2146
2228
|
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." },
|
|
2147
|
-
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)." },
|
|
2229
|
+
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)." },
|
|
2148
2230
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2149
2231
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2150
2232
|
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
@@ -2462,7 +2544,7 @@ class BambuPrinterMCPServer {
|
|
|
2462
2544
|
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." },
|
|
2463
2545
|
dev_id: { type: "string", description: "Bambu device id for FULU BambuNetwork printing; defaults to BAMBU_DEV_ID or BAMBU_SERIAL." },
|
|
2464
2546
|
dev_ip: { type: "string", description: "Printer IP address for FULU BambuNetwork LAN/local print methods; defaults to host when provided." },
|
|
2465
|
-
bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND." },
|
|
2547
|
+
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." },
|
|
2466
2548
|
bambu_network_config_dir: { type: "string", description: "Config/log directory used by the FULU BambuNetwork agent." },
|
|
2467
2549
|
country_code: { type: "string", description: "BambuNetwork country code, such as US, used by the FULU bridge agent." },
|
|
2468
2550
|
user_info: { type: "string", description: "Optional BambuNetwork user_info JSON string passed to net.change_user for the FULU bridge." },
|
|
@@ -2480,7 +2562,7 @@ class BambuPrinterMCPServer {
|
|
|
2480
2562
|
enum: SLICER_SCHEMA_VALUES,
|
|
2481
2563
|
description: "Slicer to use only if auto-slicing an unsliced 3MF. Bambu-compatible slicer aliases such as fulu-orca and orca-studio are accepted."
|
|
2482
2564
|
},
|
|
2483
|
-
slicer_path: { type: "string", description: "Path to the slicer executable for auto-slicing (default: value from env or a platform default)" },
|
|
2565
|
+
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." },
|
|
2484
2566
|
use_ams: { type: "boolean", description: "Whether to use the AMS (default: auto-detect from 3MF)" },
|
|
2485
2567
|
ams_mapping: {
|
|
2486
2568
|
type: "array",
|
|
@@ -2575,29 +2657,63 @@ class BambuPrinterMCPServer {
|
|
|
2575
2657
|
required: ["stl_path"]
|
|
2576
2658
|
}
|
|
2577
2659
|
},
|
|
2660
|
+
{
|
|
2661
|
+
name: "blender_mcp_status",
|
|
2662
|
+
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.",
|
|
2663
|
+
inputSchema: {
|
|
2664
|
+
type: "object",
|
|
2665
|
+
properties: {
|
|
2666
|
+
connect: { type: "boolean", description: "Initialize the configured stdio MCP server and discover its tools (default false)." },
|
|
2667
|
+
timeout_ms: { type: "integer", minimum: 100, maximum: 300000, description: "Total connection and discovery deadline in milliseconds; defaults to BLENDER_MCP_TIMEOUT_MS or 120000." }
|
|
2668
|
+
},
|
|
2669
|
+
additionalProperties: false
|
|
2670
|
+
}
|
|
2671
|
+
},
|
|
2672
|
+
{
|
|
2673
|
+
name: "blender_mcp_call",
|
|
2674
|
+
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.",
|
|
2675
|
+
inputSchema: {
|
|
2676
|
+
type: "object",
|
|
2677
|
+
properties: {
|
|
2678
|
+
tool_name: { type: "string", description: "Exact name advertised by Blender MCP, such as get_scene_info or execute_blender_code." },
|
|
2679
|
+
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." },
|
|
2680
|
+
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." }
|
|
2681
|
+
},
|
|
2682
|
+
required: ["tool_name"],
|
|
2683
|
+
additionalProperties: false
|
|
2684
|
+
}
|
|
2685
|
+
},
|
|
2578
2686
|
{
|
|
2579
2687
|
name: "blender_mcp_edit_model",
|
|
2580
|
-
description: "
|
|
2688
|
+
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.",
|
|
2581
2689
|
inputSchema: {
|
|
2582
2690
|
type: "object",
|
|
2583
2691
|
properties: {
|
|
2584
2692
|
stl_path: { type: "string", description: "Path to the local STL file" },
|
|
2585
2693
|
operations: {
|
|
2586
2694
|
type: "array",
|
|
2587
|
-
description: "Ordered
|
|
2695
|
+
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.",
|
|
2696
|
+
minItems: 1,
|
|
2697
|
+
maxItems: 64,
|
|
2588
2698
|
items: { type: "string" }
|
|
2589
2699
|
},
|
|
2590
|
-
|
|
2591
|
-
|
|
2700
|
+
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." },
|
|
2701
|
+
user_prompt: { type: "string", description: "The user's own words describing the edit, passed unchanged to Blender MCP." },
|
|
2702
|
+
timeout_ms: { type: "integer", minimum: 100, maximum: 300000, description: "Total Blender request deadline in milliseconds; defaults to BLENDER_MCP_TIMEOUT_MS or 120000." },
|
|
2703
|
+
bridge_command: { type: "string", description: "Legacy custom bridge executable override, not a standard MCP command. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
|
|
2704
|
+
execute: { type: "boolean", description: "Apply edits and export (true) or validate and return the prepared request without connecting (false, default)." }
|
|
2592
2705
|
},
|
|
2593
|
-
required:
|
|
2706
|
+
required: process.env.BLENDER_MCP_COMMAND?.trim()
|
|
2707
|
+
? ["stl_path", "operations", "output_path"]
|
|
2708
|
+
: ["stl_path", "operations"],
|
|
2709
|
+
additionalProperties: false
|
|
2594
2710
|
}
|
|
2595
2711
|
}
|
|
2596
2712
|
]
|
|
2597
2713
|
};
|
|
2598
2714
|
});
|
|
2599
2715
|
|
|
2600
|
-
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2716
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
2601
2717
|
const { name, arguments: args } = request.params;
|
|
2602
2718
|
|
|
2603
2719
|
const host = String(args?.host || DEFAULT_HOST);
|
|
@@ -2696,7 +2812,10 @@ class BambuPrinterMCPServer {
|
|
|
2696
2812
|
|
|
2697
2813
|
case "bambu_network_bridge_status": {
|
|
2698
2814
|
const bridgeArgs = args as Record<string, any> | undefined;
|
|
2699
|
-
const options = this.bridgeOptionsFromArgs(
|
|
2815
|
+
const options = this.bridgeOptionsFromArgs(
|
|
2816
|
+
bridgeArgs,
|
|
2817
|
+
"bambu_network_bridge_status"
|
|
2818
|
+
);
|
|
2700
2819
|
result = this.bambuNetwork.getStatus(options);
|
|
2701
2820
|
if (Boolean(bridgeArgs?.connect)) {
|
|
2702
2821
|
const probe = await this.bambuNetwork.ensureAgent(options);
|
|
@@ -2718,7 +2837,7 @@ class BambuPrinterMCPServer {
|
|
|
2718
2837
|
const payload = bridgeArgs.payload && typeof bridgeArgs.payload === "object"
|
|
2719
2838
|
? bridgeArgs.payload as Record<string, unknown>
|
|
2720
2839
|
: {};
|
|
2721
|
-
const options = this.bridgeOptionsFromArgs(bridgeArgs);
|
|
2840
|
+
const options = this.bridgeOptionsFromArgs(bridgeArgs, "bambu_network_call");
|
|
2722
2841
|
result = bridgeArgs.with_agent === false
|
|
2723
2842
|
? await this.bambuNetwork.request(String(bridgeArgs.method), payload, options)
|
|
2724
2843
|
: await this.bambuNetwork.callWithAgent(String(bridgeArgs.method), payload, options);
|
|
@@ -2737,7 +2856,11 @@ class BambuPrinterMCPServer {
|
|
|
2737
2856
|
timeoutMs: args?.timeout_ms !== undefined ? Number(args.timeout_ms) : undefined,
|
|
2738
2857
|
bambuModel: snapshotModel ? String(snapshotModel) : undefined,
|
|
2739
2858
|
experimental: Boolean(args?.experimental),
|
|
2740
|
-
ffmpegPath:
|
|
2859
|
+
ffmpegPath: this.resolveExecutableSelectorArg(
|
|
2860
|
+
args?.ffmpeg_path,
|
|
2861
|
+
"camera_snapshot",
|
|
2862
|
+
"ffmpeg_path"
|
|
2863
|
+
) ?? this.runtimeConfig.ffmpegPath,
|
|
2741
2864
|
});
|
|
2742
2865
|
break;
|
|
2743
2866
|
}
|
|
@@ -2959,7 +3082,10 @@ class BambuPrinterMCPServer {
|
|
|
2959
3082
|
if (!args?.template_name) {
|
|
2960
3083
|
throw new Error("Missing required parameter: template_name");
|
|
2961
3084
|
}
|
|
2962
|
-
const { slicerType, slicerPath, slicerProfile } =
|
|
3085
|
+
const { slicerType, slicerPath, slicerProfile } = this.resolveSlicerConfigFromArgs(
|
|
3086
|
+
args as Record<string, unknown> | undefined,
|
|
3087
|
+
"slice_with_template"
|
|
3088
|
+
);
|
|
2963
3089
|
const sliceModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
2964
3090
|
const nozzleDiam = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
2965
3091
|
const activeSlicerProfile = await resolveTemplateFirstSlicerProfilePath(
|
|
@@ -3041,7 +3167,10 @@ class BambuPrinterMCPServer {
|
|
|
3041
3167
|
if (!args?.stl_path) {
|
|
3042
3168
|
throw new Error("Missing required parameter: stl_path");
|
|
3043
3169
|
}
|
|
3044
|
-
const { slicerType, slicerPath, slicerProfile } =
|
|
3170
|
+
const { slicerType, slicerPath, slicerProfile } = this.resolveSlicerConfigFromArgs(
|
|
3171
|
+
args as Record<string, unknown> | undefined,
|
|
3172
|
+
"slice_stl"
|
|
3173
|
+
);
|
|
3045
3174
|
const sliceModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
3046
3175
|
const nozzleDiam = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
3047
3176
|
const activeSlicerProfile = await resolveSlicerProfilePath(
|
|
@@ -3131,7 +3260,10 @@ class BambuPrinterMCPServer {
|
|
|
3131
3260
|
throw new Error("Bambu serial number and access token are required for print_3mf.");
|
|
3132
3261
|
}
|
|
3133
3262
|
|
|
3134
|
-
const { slicerType, slicerPath, slicerProfile } =
|
|
3263
|
+
const { slicerType, slicerPath, slicerProfile } = this.resolveSlicerConfigFromArgs(
|
|
3264
|
+
args as Record<string, unknown> | undefined,
|
|
3265
|
+
"print_3mf"
|
|
3266
|
+
);
|
|
3135
3267
|
const printModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
3136
3268
|
const printBedType = resolveBedType(args?.bed_type as string | undefined);
|
|
3137
3269
|
const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
@@ -3151,57 +3283,50 @@ class BambuPrinterMCPServer {
|
|
|
3151
3283
|
|
|
3152
3284
|
let threeMFPath = String(args.three_mf_path);
|
|
3153
3285
|
|
|
3154
|
-
//
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
if (
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3286
|
+
// Inspect and auto-slice before upload. Preserve any failure instead of sending an unsliced project.
|
|
3287
|
+
const JSZip = (await import('jszip')).default;
|
|
3288
|
+
const zipData = fs.readFileSync(threeMFPath);
|
|
3289
|
+
const zip = await JSZip.loadAsync(zipData);
|
|
3290
|
+
const hasGcode = Object.values(zip.files).some(
|
|
3291
|
+
entry => !entry.dir && /\.gcode$/i.test(entry.name)
|
|
3292
|
+
);
|
|
3293
|
+
if (!hasGcode) {
|
|
3294
|
+
if (printBedType === "supertack_plate") {
|
|
3295
|
+
throw new Error(
|
|
3296
|
+
'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.'
|
|
3297
|
+
);
|
|
3298
|
+
}
|
|
3299
|
+
console.log(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
|
|
3300
|
+
const autoSliceOptions: BambuSliceOptions = {
|
|
3301
|
+
uptodate: true,
|
|
3302
|
+
ensureOnBed: true,
|
|
3303
|
+
minSave: true,
|
|
3304
|
+
skipModifiedGcodes: true,
|
|
3305
|
+
bedType: printBedType,
|
|
3306
|
+
};
|
|
3307
|
+
if (!explicitSlicerProfile) {
|
|
3308
|
+
try {
|
|
3309
|
+
const liveFilaments = await this.getResolvedPrinterFilamentInventory(
|
|
3310
|
+
host,
|
|
3311
|
+
bambuSerial,
|
|
3312
|
+
bambuToken,
|
|
3313
|
+
printModel,
|
|
3314
|
+
printNozzle
|
|
3166
3315
|
);
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
const autoSliceOptions: BambuSliceOptions = {
|
|
3170
|
-
uptodate: true,
|
|
3171
|
-
ensureOnBed: true,
|
|
3172
|
-
minSave: true,
|
|
3173
|
-
skipModifiedGcodes: true,
|
|
3174
|
-
bedType: printBedType,
|
|
3175
|
-
};
|
|
3176
|
-
if (!explicitSlicerProfile) {
|
|
3177
|
-
try {
|
|
3178
|
-
const liveFilaments = await this.getResolvedPrinterFilamentInventory(
|
|
3179
|
-
host,
|
|
3180
|
-
bambuSerial,
|
|
3181
|
-
bambuToken,
|
|
3182
|
-
printModel,
|
|
3183
|
-
printNozzle
|
|
3184
|
-
);
|
|
3185
|
-
if (liveFilaments.recommended?.load_filaments) {
|
|
3186
|
-
autoSliceOptions.loadFilaments = liveFilaments.recommended.load_filaments;
|
|
3187
|
-
}
|
|
3188
|
-
} catch (filamentError) {
|
|
3189
|
-
console.warn("Could not resolve live printer filaments for auto-slicing:", filamentError);
|
|
3316
|
+
if (liveFilaments.recommended?.load_filaments) {
|
|
3317
|
+
autoSliceOptions.loadFilaments = liveFilaments.recommended.load_filaments;
|
|
3190
3318
|
}
|
|
3319
|
+
} catch (filamentError) {
|
|
3320
|
+
console.warn("Could not resolve live printer filaments for auto-slicing:", filamentError);
|
|
3191
3321
|
}
|
|
3192
|
-
threeMFPath = await this.stlManipulator.sliceSTL(
|
|
3193
|
-
threeMFPath, slicerType, slicerPath, activeSlicerProfile,
|
|
3194
|
-
undefined, // progressCallback
|
|
3195
|
-
printPreset,
|
|
3196
|
-
autoSliceOptions
|
|
3197
|
-
);
|
|
3198
|
-
console.log("Auto-sliced to: " + threeMFPath);
|
|
3199
|
-
}
|
|
3200
|
-
} catch (sliceCheckErr: any) {
|
|
3201
|
-
if (String(sliceCheckErr?.message || "").includes("SuperTack")) {
|
|
3202
|
-
throw sliceCheckErr;
|
|
3203
3322
|
}
|
|
3204
|
-
|
|
3323
|
+
threeMFPath = await this.stlManipulator.sliceSTL(
|
|
3324
|
+
threeMFPath, slicerType, slicerPath, activeSlicerProfile,
|
|
3325
|
+
undefined, // progressCallback
|
|
3326
|
+
printPreset,
|
|
3327
|
+
autoSliceOptions
|
|
3328
|
+
);
|
|
3329
|
+
console.log("Auto-sliced to: " + threeMFPath);
|
|
3205
3330
|
}
|
|
3206
3331
|
|
|
3207
3332
|
const parsed3MFData = await parse3MF(threeMFPath);
|
|
@@ -3320,7 +3445,10 @@ class BambuPrinterMCPServer {
|
|
|
3320
3445
|
throw new Error("Bambu serial number and access token are required for print_collar_charm.");
|
|
3321
3446
|
}
|
|
3322
3447
|
|
|
3323
|
-
const { slicerType, slicerPath, slicerProfile } =
|
|
3448
|
+
const { slicerType, slicerPath, slicerProfile } = this.resolveSlicerConfigFromArgs(
|
|
3449
|
+
args as Record<string, unknown> | undefined,
|
|
3450
|
+
"print_collar_charm"
|
|
3451
|
+
);
|
|
3324
3452
|
const printModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
3325
3453
|
const printBedType = resolveBedType(args?.bed_type as string | undefined);
|
|
3326
3454
|
const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
@@ -3415,18 +3543,24 @@ class BambuPrinterMCPServer {
|
|
|
3415
3543
|
result = await this.stlManipulator.layFlat(String(args.stl_path));
|
|
3416
3544
|
break;
|
|
3417
3545
|
|
|
3546
|
+
case "blender_mcp_status":
|
|
3547
|
+
result = await this.blender.status(args ?? {}, extra.signal);
|
|
3548
|
+
break;
|
|
3549
|
+
|
|
3550
|
+
case "blender_mcp_call":
|
|
3551
|
+
return await this.blender.call(args ?? {}, extra.signal);
|
|
3552
|
+
|
|
3418
3553
|
case "blender_mcp_edit_model":
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
});
|
|
3554
|
+
result = await this.blender.edit(
|
|
3555
|
+
args ?? {},
|
|
3556
|
+
this.resolveExecutableSelectorArg(
|
|
3557
|
+
args?.bridge_command,
|
|
3558
|
+
"blender_mcp_edit_model",
|
|
3559
|
+
"bridge_command",
|
|
3560
|
+
true
|
|
3561
|
+
) ?? (!process.env.BLENDER_MCP_COMMAND?.trim() ? this.runtimeConfig.blenderBridgeCommand : undefined),
|
|
3562
|
+
extra.signal
|
|
3563
|
+
);
|
|
3430
3564
|
break;
|
|
3431
3565
|
|
|
3432
3566
|
default:
|
|
@@ -3463,49 +3597,27 @@ class BambuPrinterMCPServer {
|
|
|
3463
3597
|
});
|
|
3464
3598
|
}
|
|
3465
3599
|
|
|
3466
|
-
private async invokeBlenderBridge(params: {
|
|
3467
|
-
stlPath: string;
|
|
3468
|
-
operations: string[];
|
|
3469
|
-
execute: boolean;
|
|
3470
|
-
bridgeCommand?: string;
|
|
3471
|
-
}): Promise<any> {
|
|
3472
|
-
const payload = {
|
|
3473
|
-
stlPath: params.stlPath,
|
|
3474
|
-
operations: params.operations,
|
|
3475
|
-
};
|
|
3476
|
-
|
|
3477
|
-
if (!params.execute || !params.bridgeCommand) {
|
|
3478
|
-
return {
|
|
3479
|
-
status: "prepared",
|
|
3480
|
-
payload,
|
|
3481
|
-
note: params.bridgeCommand
|
|
3482
|
-
? "Set execute=true to run the Blender bridge command."
|
|
3483
|
-
: "No BLENDER_MCP_BRIDGE_COMMAND configured. Set the env var or pass bridge_command.",
|
|
3484
|
-
};
|
|
3485
|
-
}
|
|
3486
|
-
|
|
3487
|
-
const { execFile } = await import("node:child_process");
|
|
3488
|
-
const { promisify } = await import("node:util");
|
|
3489
|
-
const execFileAsync = promisify(execFile);
|
|
3490
|
-
|
|
3491
|
-
const { stdout, stderr } = await execFileAsync(params.bridgeCommand, [], {
|
|
3492
|
-
env: { ...process.env, MCP_BLENDER_PAYLOAD: JSON.stringify(payload) },
|
|
3493
|
-
timeout: 120_000,
|
|
3494
|
-
});
|
|
3495
|
-
|
|
3496
|
-
return {
|
|
3497
|
-
status: "executed",
|
|
3498
|
-
stdout: stdout.trim(),
|
|
3499
|
-
stderr: stderr.trim(),
|
|
3500
|
-
};
|
|
3501
|
-
}
|
|
3502
|
-
|
|
3503
3600
|
async startStdio() {
|
|
3504
3601
|
const transport = new StdioServerTransport();
|
|
3505
3602
|
await this.server.connect(transport);
|
|
3603
|
+
process.stdin.once("end", () => { void this.shutdown(); });
|
|
3506
3604
|
console.error("Bambu Printer MCP server running on stdio");
|
|
3507
3605
|
}
|
|
3508
3606
|
|
|
3607
|
+
async shutdown(exitCode = 0) {
|
|
3608
|
+
if (this.shuttingDown) return;
|
|
3609
|
+
this.shuttingDown = true;
|
|
3610
|
+
try {
|
|
3611
|
+
// Closing the MCP transport aborts its active requests before exit cleanup.
|
|
3612
|
+
await this.server.close();
|
|
3613
|
+
this.httpRuntime?.httpServer.close();
|
|
3614
|
+
} catch (error) {
|
|
3615
|
+
console.error("[MCP Shutdown]", error);
|
|
3616
|
+
} finally {
|
|
3617
|
+
process.exit(exitCode);
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
|
|
3509
3621
|
async startHttp() {
|
|
3510
3622
|
const { httpHost, httpPort, httpPath, statefulSession, enableJsonResponse, allowedOrigins } =
|
|
3511
3623
|
this.runtimeConfig;
|
|
@@ -3556,4 +3668,6 @@ class BambuPrinterMCPServer {
|
|
|
3556
3668
|
}
|
|
3557
3669
|
|
|
3558
3670
|
const server = new BambuPrinterMCPServer();
|
|
3671
|
+
process.once("SIGINT", () => { void server.shutdown(130); });
|
|
3672
|
+
process.once("SIGTERM", () => { void server.shutdown(143); });
|
|
3559
3673
|
server.run().catch(console.error);
|