mcp-3d-printer-server 1.2.26 → 1.2.27
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/CHANGELOG.md +12 -0
- package/dist/bambu-network-bridge.d.ts +11 -0
- package/dist/bambu-network-bridge.js +38 -1
- package/dist/index.js +3 -9
- package/dist/safety/printer-state.d.ts +9 -0
- package/dist/safety/printer-state.js +13 -1
- package/dist/tool-annotations.d.ts +25 -0
- package/dist/tool-annotations.js +58 -0
- package/package.json +3 -1
- package/server.json +2 -2
- package/src/bambu-network-bridge.ts +32 -1
- package/src/index.ts +3 -9
- package/src/safety/printer-state.ts +13 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.2.27] – 2026-11-04
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Every tool now carries the four MCP tool annotations (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`) from one table, `src/tool-annotations.ts`, so clients can tell reads from actions without parsing descriptions. Before, 7 of 45 tools had partial annotations. Destructive means the tool may end or replace something that exists: `cancel_print`, `upload_gcode` (same file name), `camera_snapshot` with `save_path`, `get_slice_settings` (a same-named 3MF replaces the extracted settings file), `save_template`, `counterpart_print_route` with a route, and the pass-through calls `fulu_bambu_network_rpc`, `blender_mcp_call` and `fusion_mcp_call`. Starting a print and heating are marked additive, matching the no-confirmation decision. Annotations are hints for clients; every safety check still applies. A test lists the tools over stdio and fails for any tool without all four.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Resume a Bambu job that the printer paused itself. A P1S paused by Pause or by a `pause_at_layers` stop reports `print_error` 0300_8013 ("paused by the user" / "paused by the pause command in the file"), and Resume refused it as a print error, so a paused job could only be resumed at the printer. The notice is excused only for Resume, only while the printer reports PAUSE; any other print error still refuses, and starting a job still needs no error. Ported from bambu-printer-mcp.
|
|
12
|
+
- FULU's BambuNetwork agent now receives the client headers FULU's own GUI sends before `net.start` (Bambu Studio 02.05.02.51 on Linux, with a per-install device id kept in the config folder). Bambu's cloud issues the printer-control certificate and creates print tasks for this client; without the headers the agent was an unknown client. `BAMBU_NETWORK_CLIENT_NAME` and `BAMBU_NETWORK_CLIENT_VERSION` override the name and version. Ported from bambu-printer-mcp.
|
|
13
|
+
- Evidence: mocked Bambu MQTT/FTPS boundaries and the fake FULU host only; no printer was contacted.
|
|
14
|
+
|
|
3
15
|
## [1.2.26] – 2026-11-04
|
|
4
16
|
|
|
5
17
|
### Changed
|
|
@@ -106,6 +106,17 @@ export type BambuNetworkBridgeStatus = {
|
|
|
106
106
|
export declare function bambuNetworkCertDir(configDir: string, env?: NodeJS.ProcessEnv): string;
|
|
107
107
|
export declare function prepareBambuNetworkCertDir(configDir: string, env?: NodeJS.ProcessEnv): string;
|
|
108
108
|
export declare function defaultBambuNetworkConfigDir(): string;
|
|
109
|
+
/**
|
|
110
|
+
* The client identity FULU's GUI gives the plug-in before agent start
|
|
111
|
+
* (GUI_App::init_http_extra_header). Through the Linux bridge FULU presents
|
|
112
|
+
* itself as Bambu Studio at the version its plug-in ABI belongs to
|
|
113
|
+
* (PJarczakLinuxBridgeConfig.cpp forced_client_version, "02.05.02.51").
|
|
114
|
+
* Bambu's cloud issues the printer-control certificate and creates print
|
|
115
|
+
* tasks for this client, so an agent started without it is an unknown client.
|
|
116
|
+
* The device id is kept in the config folder so it stays the same per install.
|
|
117
|
+
* Ported from bambu-printer-mcp src/bambu-network-bridge.ts.
|
|
118
|
+
*/
|
|
119
|
+
export declare function bambuNetworkClientHeaders(configDir: string, env?: NodeJS.ProcessEnv): Record<string, string>;
|
|
109
120
|
export declare class BambuNetworkBridge {
|
|
110
121
|
private child?;
|
|
111
122
|
private commandLine?;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
@@ -89,6 +89,41 @@ function trimEnv(value) {
|
|
|
89
89
|
const trimmed = value?.trim();
|
|
90
90
|
return trimmed ? trimmed : undefined;
|
|
91
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* The client identity FULU's GUI gives the plug-in before agent start
|
|
94
|
+
* (GUI_App::init_http_extra_header). Through the Linux bridge FULU presents
|
|
95
|
+
* itself as Bambu Studio at the version its plug-in ABI belongs to
|
|
96
|
+
* (PJarczakLinuxBridgeConfig.cpp forced_client_version, "02.05.02.51").
|
|
97
|
+
* Bambu's cloud issues the printer-control certificate and creates print
|
|
98
|
+
* tasks for this client, so an agent started without it is an unknown client.
|
|
99
|
+
* The device id is kept in the config folder so it stays the same per install.
|
|
100
|
+
* Ported from bambu-printer-mcp src/bambu-network-bridge.ts.
|
|
101
|
+
*/
|
|
102
|
+
export function bambuNetworkClientHeaders(configDir, env = process.env) {
|
|
103
|
+
const idFile = path.join(configDir, "client-device-id");
|
|
104
|
+
let deviceId = "";
|
|
105
|
+
try {
|
|
106
|
+
deviceId = fs.readFileSync(idFile, "utf8").trim();
|
|
107
|
+
}
|
|
108
|
+
catch { /* first start */ }
|
|
109
|
+
if (!deviceId) {
|
|
110
|
+
deviceId = randomUUID();
|
|
111
|
+
try {
|
|
112
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
113
|
+
fs.writeFileSync(idFile, `${deviceId}\n`, { mode: 0o600 });
|
|
114
|
+
}
|
|
115
|
+
catch { /* still send this run's id */ }
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
"X-BBL-Client-Type": "slicer",
|
|
119
|
+
"X-BBL-Client-Name": trimEnv(env.BAMBU_NETWORK_CLIENT_NAME) ?? "BambuStudio",
|
|
120
|
+
"X-BBL-Client-Version": trimEnv(env.BAMBU_NETWORK_CLIENT_VERSION) ?? "02.05.02.51",
|
|
121
|
+
"X-BBL-OS-Type": "linux",
|
|
122
|
+
"X-BBL-OS-Version": os.release(),
|
|
123
|
+
"X-BBL-Device-ID": deviceId,
|
|
124
|
+
"X-BBL-Language": "en",
|
|
125
|
+
};
|
|
126
|
+
}
|
|
92
127
|
function fileExists(filePath) {
|
|
93
128
|
try {
|
|
94
129
|
return fs.existsSync(filePath);
|
|
@@ -350,6 +385,8 @@ export class BambuNetworkBridge {
|
|
|
350
385
|
// plug-in verifies a LAN printer with printer.cer from this folder, and
|
|
351
386
|
// FULU's host crashes on connect_printer when no folder was ever set.
|
|
352
387
|
const certificates = await withAgent("net.set_cert_file", { folder: certDir, filename: "slicer_base64.cer" }, false);
|
|
388
|
+
// FULU's GUI order: set_cert_file, then the client headers, then start.
|
|
389
|
+
await withAgent("net.set_extra_http_header", { headers: bambuNetworkClientHeaders(configDir) }, false);
|
|
353
390
|
await withAgent("net.set_country_code", { country_code: countryCode });
|
|
354
391
|
await withAgent("net.start");
|
|
355
392
|
// The plug-in runs callbacks through queue_on_main; FULU's host executes
|
package/dist/index.js
CHANGED
|
@@ -21,6 +21,7 @@ import { STLManipulator } from "./stl/stl-manipulator.js";
|
|
|
21
21
|
import { parsePauseLayers, readPlateGcode, pausedLayers, machinePauseGcode } from "./slicer/layer-pauses.js";
|
|
22
22
|
import { CounterpartService } from "./counterpart/service.js";
|
|
23
23
|
import { CAMERA_STREAM_TOOLS } from "./counterpart/camera-tools.js";
|
|
24
|
+
import { annotateTools } from "./tool-annotations.js";
|
|
24
25
|
import { CAMERA_SNAPSHOT_OUTPUT_SCHEMA, CONTROL_OUTPUT_SCHEMA, FILAMENTS_OUTPUT_SCHEMA, PRINT_PROGRESS_OUTPUT_SCHEMA, PRINTER_INFO_OUTPUT_SCHEMA, } from "./counterpart/contract.js";
|
|
25
26
|
import { SlicerError } from "./slicer/slicer-error.js";
|
|
26
27
|
import { inspectSliceSettings, listTemplateRegistry, resolveTemplatePath, resolveTemplateSlicerProfilePath, saveTemplate, } from "./slicer/templates.js";
|
|
@@ -1104,7 +1105,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1104
1105
|
// List available tools
|
|
1105
1106
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1106
1107
|
return {
|
|
1107
|
-
tools: [
|
|
1108
|
+
tools: annotateTools([
|
|
1108
1109
|
{
|
|
1109
1110
|
name: "get_printer_status",
|
|
1110
1111
|
description: "Get the current status of the 3D printer",
|
|
@@ -1327,14 +1328,12 @@ class ThreeDPrinterMCPServer {
|
|
|
1327
1328
|
description: "Describe this printer for the counterpart/1 contract: server name and version, printer kind and identity, and which features work (progress, layers, camera, filaments, pause, resume, cancel, print_3mf, G-code), detected from the firmware. Read-only.",
|
|
1328
1329
|
inputSchema: { type: "object", properties: { ...PRINTER_TARGET_PROPERTIES } },
|
|
1329
1330
|
outputSchema: PRINTER_INFO_OUTPUT_SCHEMA,
|
|
1330
|
-
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
1331
1331
|
},
|
|
1332
1332
|
{
|
|
1333
1333
|
name: "counterpart_print_progress",
|
|
1334
1334
|
description: "Read the printer's state and current job in the counterpart/1 shape: state, job name, progress (0-100), elapsed and remaining seconds, layers when the firmware reports them, temperatures and problems. job.name matches the jobName that upload_gcode and start_print return. Read-only.",
|
|
1335
1335
|
inputSchema: { type: "object", properties: { ...PRINTER_TARGET_PROPERTIES } },
|
|
1336
1336
|
outputSchema: PRINT_PROGRESS_OUTPUT_SCHEMA,
|
|
1337
|
-
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
1338
1337
|
},
|
|
1339
1338
|
{
|
|
1340
1339
|
name: "camera_snapshot",
|
|
@@ -1348,7 +1347,6 @@ class ThreeDPrinterMCPServer {
|
|
|
1348
1347
|
},
|
|
1349
1348
|
},
|
|
1350
1349
|
outputSchema: CAMERA_SNAPSHOT_OUTPUT_SCHEMA,
|
|
1351
|
-
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
1352
1350
|
},
|
|
1353
1351
|
{
|
|
1354
1352
|
name: "get_printer_filaments",
|
|
@@ -1362,21 +1360,18 @@ class ThreeDPrinterMCPServer {
|
|
|
1362
1360
|
},
|
|
1363
1361
|
},
|
|
1364
1362
|
outputSchema: FILAMENTS_OUTPUT_SCHEMA,
|
|
1365
|
-
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
1366
1363
|
},
|
|
1367
1364
|
{
|
|
1368
1365
|
name: "pause_print",
|
|
1369
1366
|
description: "Pause the current print (Bambu Lab, OctoPrint, Klipper/Moonraker, PrusaLink, Duet M25, Repetier, and Creality printers that run Moonraker). Bambu pauses over the same connection that prints. Pause is never gated.",
|
|
1370
1367
|
inputSchema: { type: "object", properties: { ...PRINTER_TARGET_PROPERTIES, ...CONNECTION_PROPERTIES } },
|
|
1371
1368
|
outputSchema: CONTROL_OUTPUT_SCHEMA,
|
|
1372
|
-
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1373
1369
|
},
|
|
1374
1370
|
{
|
|
1375
1371
|
name: "resume_print",
|
|
1376
1372
|
description: "Resume a paused print. Resume reheats and moves the printer, so it only resumes a job this server instance inspected and started, and only when the printer freshly reports that job paused. Bambu re-reads the job over FTPS (or the plate G-code the printer extracted) and compares its SHA-256, and resumes over the same connection that printed. When the file cannot be re-read, a person confirms through MCP elicitation first (Bambu: only for files Bambu's plug-in placed; a file this server uploaded must re-read). Resume other jobs at the printer.",
|
|
1377
1373
|
inputSchema: { type: "object", properties: { ...PRINTER_TARGET_PROPERTIES, ...CONNECTION_PROPERTIES } },
|
|
1378
1374
|
outputSchema: CONTROL_OUTPUT_SCHEMA,
|
|
1379
|
-
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1380
1375
|
},
|
|
1381
1376
|
{
|
|
1382
1377
|
name: "set_printer_temperature",
|
|
@@ -1815,7 +1810,6 @@ class ThreeDPrinterMCPServer {
|
|
|
1815
1810
|
{
|
|
1816
1811
|
name: "inspect_print_file",
|
|
1817
1812
|
description: "Inspect an exact private snapshot of a prepared sliced 3MF or textual G-code file and selected plate without contacting a printer. Returns versioned file-only evidence; no printer readiness or permission to dispatch is established. Does not auto-slice.",
|
|
1818
|
-
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1819
1813
|
inputSchema: {
|
|
1820
1814
|
type: "object",
|
|
1821
1815
|
additionalProperties: false,
|
|
@@ -2188,7 +2182,7 @@ class ThreeDPrinterMCPServer {
|
|
|
2188
2182
|
},
|
|
2189
2183
|
outputSchema: FUSION_EXPORT_OUTPUT_SCHEMA,
|
|
2190
2184
|
}
|
|
2191
|
-
]
|
|
2185
|
+
])
|
|
2192
2186
|
};
|
|
2193
2187
|
});
|
|
2194
2188
|
// Handle tool calls
|
|
@@ -81,6 +81,15 @@ export declare function isHiddenNotice(serial: string, model: string, entry: Hms
|
|
|
81
81
|
export declare function hiddenHmsNotices(raw: unknown, serial: string, model: string): string[];
|
|
82
82
|
/** Cleared-code keys of reported hidden entries; clearing never removes them, so they are not re-acknowledged. */
|
|
83
83
|
export declare function hiddenHmsKeys(raw: unknown, serial: string, model: string): string[];
|
|
84
|
+
/**
|
|
85
|
+
* 0300_8013, "Printing was paused by the user" / "paused due to the pause command added to the
|
|
86
|
+
* printing file": the printer's report of a deliberate pause (a Pause, or a pause_at_layers stop),
|
|
87
|
+
* which Resume clears. A paused P1S reports it as print_error, so it is not an error for resuming
|
|
88
|
+
* a paused job. requireIdle is false only for Resume here; starting a job still needs no error.
|
|
89
|
+
* Ported from bambu-printer-mcp src/safety/printer-state.ts.
|
|
90
|
+
*/
|
|
91
|
+
export declare const PAUSE_NOTICE = 50364435;
|
|
92
|
+
export declare function isPauseNotice(raw: Record<string, any>, requireIdle: boolean): boolean;
|
|
84
93
|
/** Verifies reported configuration, not a physical nozzle/spool inspection. */
|
|
85
94
|
export declare function validatePrinterState(status: any, requirements: PrinterStateRequirements): ValidatedPrinterState;
|
|
86
95
|
/** Manual M104 cannot remap filament slots. Validate the material already at the nozzle. */
|
|
@@ -316,6 +316,18 @@ function logHiddenNotices(raw, serial, model) {
|
|
|
316
316
|
console.error(`Printer ${serial} reports ${hmsLabel(entry.hex)}, which Bambu lists for it without text and never shows as a fault; it does not block operations.`);
|
|
317
317
|
}
|
|
318
318
|
}
|
|
319
|
+
/**
|
|
320
|
+
* 0300_8013, "Printing was paused by the user" / "paused due to the pause command added to the
|
|
321
|
+
* printing file": the printer's report of a deliberate pause (a Pause, or a pause_at_layers stop),
|
|
322
|
+
* which Resume clears. A paused P1S reports it as print_error, so it is not an error for resuming
|
|
323
|
+
* a paused job. requireIdle is false only for Resume here; starting a job still needs no error.
|
|
324
|
+
* Ported from bambu-printer-mcp src/safety/printer-state.ts.
|
|
325
|
+
*/
|
|
326
|
+
export const PAUSE_NOTICE = 0x03008013;
|
|
327
|
+
export function isPauseNotice(raw, requireIdle) {
|
|
328
|
+
const state = typeof raw?.gcode_state === "string" ? raw.gcode_state.toUpperCase() : "";
|
|
329
|
+
return !requireIdle && state === "PAUSE" && finiteNumber(raw?.print_error) === PAUSE_NOTICE;
|
|
330
|
+
}
|
|
319
331
|
function validateErrors(raw, requireIdle, serial, model) {
|
|
320
332
|
const state = typeof raw.gcode_state === "string" ? raw.gcode_state.toUpperCase() : "UNKNOWN";
|
|
321
333
|
const terminal = isCancelledX2DJob(raw, model) || isFinishedP1SJob(raw, model);
|
|
@@ -323,7 +335,7 @@ function validateErrors(raw, requireIdle, serial, model) {
|
|
|
323
335
|
throw new Error(`Printer state ${state} is not safely idle.`);
|
|
324
336
|
if (!requireIdle && !["IDLE", "FINISH", "RUNNING", "PAUSE"].includes(state) && !terminal)
|
|
325
337
|
throw new Error(`Printer state ${state} is unknown or failed.`);
|
|
326
|
-
if (finiteNumber(raw.print_error) !== 0)
|
|
338
|
+
if (finiteNumber(raw.print_error) !== 0 && !isPauseNotice(raw, requireIdle))
|
|
327
339
|
throw new Error("Printer reports a print error or did not report its error state.");
|
|
328
340
|
if (!Array.isArray(raw.hms))
|
|
329
341
|
throw new Error("Printer HMS error state is missing or malformed.");
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) for every tool.
|
|
3
|
+
* They are hints for clients, never a safety gate: the shared checks in src/safety/ still apply.
|
|
4
|
+
*
|
|
5
|
+
* - readOnlyHint: the tool changes nothing (no file, printer, setting, or external application state).
|
|
6
|
+
* - destructiveHint: it may overwrite or end something that already exists (a file, a setting, a job,
|
|
7
|
+
* an arbitrary remote call). Starting a print and heating are additive: there is no confirmation
|
|
8
|
+
* prompt for them (AGENTS.md, maintainer decision 2026-11-03), and the printer runs its own checks.
|
|
9
|
+
* - idempotentHint: calling it again with the same arguments has no further effect.
|
|
10
|
+
* - openWorldHint: it reaches outside this server and the local files (a printer, its camera, Bambu's
|
|
11
|
+
* network, Blender, or Fusion).
|
|
12
|
+
*/
|
|
13
|
+
export interface ToolAnnotations {
|
|
14
|
+
readOnlyHint: boolean;
|
|
15
|
+
destructiveHint: boolean;
|
|
16
|
+
idempotentHint: boolean;
|
|
17
|
+
openWorldHint: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare const TOOL_ANNOTATIONS: Readonly<Record<string, ToolAnnotations>>;
|
|
20
|
+
/** Give each listed tool its annotations. tests/tool-annotations.test.mjs fails for a tool without an entry. */
|
|
21
|
+
export declare function annotateTools<T extends {
|
|
22
|
+
name: string;
|
|
23
|
+
}>(tools: T[]): Array<T & {
|
|
24
|
+
annotations?: ToolAnnotations;
|
|
25
|
+
}>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const read = (openWorldHint) => ({ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint });
|
|
2
|
+
const write = (destructiveHint, idempotentHint, openWorldHint) => ({ readOnlyHint: false, destructiveHint, idempotentHint, openWorldHint });
|
|
3
|
+
export const TOOL_ANNOTATIONS = {
|
|
4
|
+
// Printer reads.
|
|
5
|
+
get_printer_status: read(true),
|
|
6
|
+
list_printer_files: read(true),
|
|
7
|
+
counterpart_printer_info: read(true),
|
|
8
|
+
counterpart_print_progress: read(true),
|
|
9
|
+
camera_snapshot: write(true, false, true), // save_path writes, and replaces, a file of the caller's choosing
|
|
10
|
+
camera_frame: read(true),
|
|
11
|
+
get_printer_filaments: read(true),
|
|
12
|
+
// Printer actions.
|
|
13
|
+
upload_gcode: write(true, false, true), // can replace a file of the same name on the printer
|
|
14
|
+
start_print: write(false, false, true),
|
|
15
|
+
print_3mf: write(false, false, true),
|
|
16
|
+
process_and_print_stl: write(false, false, true),
|
|
17
|
+
cancel_print: write(true, true, true), // ends the job; it cannot be resumed
|
|
18
|
+
pause_print: write(false, true, true),
|
|
19
|
+
resume_print: write(false, true, true),
|
|
20
|
+
set_printer_temperature: write(false, true, true),
|
|
21
|
+
camera_stream_start: write(false, true, true), // "start or renew" one shared connection
|
|
22
|
+
camera_stream_stop: write(false, true, true),
|
|
23
|
+
counterpart_print_route: write(true, true, false), // with route, replaces the saved connection in .env
|
|
24
|
+
// Local files: each STL edit and slice writes a new output file, so a repeat writes another.
|
|
25
|
+
extend_stl_base: write(false, false, false),
|
|
26
|
+
scale_stl: write(false, false, false),
|
|
27
|
+
rotate_stl: write(false, false, false),
|
|
28
|
+
translate_stl: write(false, false, false),
|
|
29
|
+
modify_stl_section: write(false, false, false),
|
|
30
|
+
merge_vertices: write(false, false, false),
|
|
31
|
+
center_model: write(false, false, false),
|
|
32
|
+
lay_flat: write(false, false, false),
|
|
33
|
+
generate_stl_visualization: write(false, false, false), // writes a new SVG file
|
|
34
|
+
get_stl_info: read(false),
|
|
35
|
+
slice_stl: write(false, false, false),
|
|
36
|
+
slice_with_template: write(false, false, false),
|
|
37
|
+
list_templates: read(false),
|
|
38
|
+
save_template: write(true, true, false), // replaces a template of the same name
|
|
39
|
+
get_slice_settings: write(true, true, false), // extracts a 3MF's settings to a TEMP_DIR path named after the file; a same-named 3MF replaces it
|
|
40
|
+
confirm_temperatures: read(false),
|
|
41
|
+
inspect_print_file: read(false),
|
|
42
|
+
// FULU BambuNetwork.
|
|
43
|
+
check_fulu_orca_setup: read(true), // run_bridge_probe starts FULU's bridge host
|
|
44
|
+
fulu_bambu_network_rpc: write(true, false, true), // allow_mutating_methods reaches agent and session setup
|
|
45
|
+
// Blender and Fusion.
|
|
46
|
+
blender_mcp_status: read(true),
|
|
47
|
+
blender_mcp_call: write(true, false, true), // any Blender MCP tool
|
|
48
|
+
blender_mcp_export_stl: write(false, false, true), // never overwrites; leaves the scene unchanged
|
|
49
|
+
blender_mcp_edit_model: write(false, false, true),
|
|
50
|
+
fusion_mcp_status: write(false, true, true), // launch: true opens Fusion
|
|
51
|
+
fusion_mcp_call: write(true, false, true), // any Fusion tool, execute_code included
|
|
52
|
+
fusion_mcp_new_design: write(false, false, true),
|
|
53
|
+
fusion_mcp_export: write(false, false, true), // never overwrites files
|
|
54
|
+
};
|
|
55
|
+
/** Give each listed tool its annotations. tests/tool-annotations.test.mjs fails for a tool without an entry. */
|
|
56
|
+
export function annotateTools(tools) {
|
|
57
|
+
return tools.map(tool => (TOOL_ANNOTATIONS[tool.name] ? { ...tool, annotations: TOOL_ANNOTATIONS[tool.name] } : tool));
|
|
58
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-3d-printer-server",
|
|
3
3
|
"mcpName": "io.github.DMontgomery40/mcp-3d-printer-server",
|
|
4
|
-
"version": "1.2.
|
|
4
|
+
"version": "1.2.27",
|
|
5
5
|
"description": "MCP server for connecting Claude with 3D printer management systems",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"repository": "https://github.com/DMontgomery40/mcp-3D-printer-server",
|
|
@@ -39,6 +39,8 @@
|
|
|
39
39
|
"dist/slicer",
|
|
40
40
|
"dist/stl",
|
|
41
41
|
"dist/test",
|
|
42
|
+
"dist/tool-annotations.d.ts",
|
|
43
|
+
"dist/tool-annotations.js",
|
|
42
44
|
"dist/types.d.ts",
|
|
43
45
|
"dist/types.js",
|
|
44
46
|
"dist/ensure-web-crypto.d.ts",
|
package/server.json
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"url": "https://github.com/DMontgomery40/mcp-3D-printer-server",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "1.2.
|
|
10
|
+
"version": "1.2.27",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "mcp-3d-printer-server",
|
|
15
|
-
"version": "1.2.
|
|
15
|
+
"version": "1.2.27",
|
|
16
16
|
"runtimeHint": "npx",
|
|
17
17
|
"transport": {
|
|
18
18
|
"type": "stdio"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
@@ -180,6 +180,35 @@ function trimEnv(value: string | undefined): string | undefined {
|
|
|
180
180
|
return trimmed ? trimmed : undefined;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
/**
|
|
184
|
+
* The client identity FULU's GUI gives the plug-in before agent start
|
|
185
|
+
* (GUI_App::init_http_extra_header). Through the Linux bridge FULU presents
|
|
186
|
+
* itself as Bambu Studio at the version its plug-in ABI belongs to
|
|
187
|
+
* (PJarczakLinuxBridgeConfig.cpp forced_client_version, "02.05.02.51").
|
|
188
|
+
* Bambu's cloud issues the printer-control certificate and creates print
|
|
189
|
+
* tasks for this client, so an agent started without it is an unknown client.
|
|
190
|
+
* The device id is kept in the config folder so it stays the same per install.
|
|
191
|
+
* Ported from bambu-printer-mcp src/bambu-network-bridge.ts.
|
|
192
|
+
*/
|
|
193
|
+
export function bambuNetworkClientHeaders(configDir: string, env: NodeJS.ProcessEnv = process.env): Record<string, string> {
|
|
194
|
+
const idFile = path.join(configDir, "client-device-id");
|
|
195
|
+
let deviceId = "";
|
|
196
|
+
try { deviceId = fs.readFileSync(idFile, "utf8").trim(); } catch { /* first start */ }
|
|
197
|
+
if (!deviceId) {
|
|
198
|
+
deviceId = randomUUID();
|
|
199
|
+
try { fs.mkdirSync(configDir, { recursive: true }); fs.writeFileSync(idFile, `${deviceId}\n`, { mode: 0o600 }); } catch { /* still send this run's id */ }
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
"X-BBL-Client-Type": "slicer",
|
|
203
|
+
"X-BBL-Client-Name": trimEnv(env.BAMBU_NETWORK_CLIENT_NAME) ?? "BambuStudio",
|
|
204
|
+
"X-BBL-Client-Version": trimEnv(env.BAMBU_NETWORK_CLIENT_VERSION) ?? "02.05.02.51",
|
|
205
|
+
"X-BBL-OS-Type": "linux",
|
|
206
|
+
"X-BBL-OS-Version": os.release(),
|
|
207
|
+
"X-BBL-Device-ID": deviceId,
|
|
208
|
+
"X-BBL-Language": "en",
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
183
212
|
function fileExists(filePath: string): boolean {
|
|
184
213
|
try {
|
|
185
214
|
return fs.existsSync(filePath);
|
|
@@ -493,6 +522,8 @@ export class BambuNetworkBridge {
|
|
|
493
522
|
// plug-in verifies a LAN printer with printer.cer from this folder, and
|
|
494
523
|
// FULU's host crashes on connect_printer when no folder was ever set.
|
|
495
524
|
const certificates = await withAgent("net.set_cert_file", { folder: certDir, filename: "slicer_base64.cer" }, false);
|
|
525
|
+
// FULU's GUI order: set_cert_file, then the client headers, then start.
|
|
526
|
+
await withAgent("net.set_extra_http_header", { headers: bambuNetworkClientHeaders(configDir) }, false);
|
|
496
527
|
await withAgent("net.set_country_code", { country_code: countryCode });
|
|
497
528
|
await withAgent("net.start");
|
|
498
529
|
// The plug-in runs callbacks through queue_on_main; FULU's host executes
|
package/src/index.ts
CHANGED
|
@@ -38,6 +38,7 @@ import { STLManipulator, type BambuSliceOptions, type ProcessOverrides, type Sli
|
|
|
38
38
|
import { parsePauseLayers, readPlateGcode, pausedLayers, machinePauseGcode } from "./slicer/layer-pauses.js";
|
|
39
39
|
import { CounterpartService, type BambuControlRoute, type PrinterTarget } from "./counterpart/service.js";
|
|
40
40
|
import {CAMERA_STREAM_TOOLS} from "./counterpart/camera-tools.js";
|
|
41
|
+
import { annotateTools } from "./tool-annotations.js";
|
|
41
42
|
import {
|
|
42
43
|
CAMERA_SNAPSHOT_OUTPUT_SCHEMA,
|
|
43
44
|
CONTROL_OUTPUT_SCHEMA,
|
|
@@ -1303,7 +1304,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1303
1304
|
// List available tools
|
|
1304
1305
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1305
1306
|
return {
|
|
1306
|
-
tools: [
|
|
1307
|
+
tools: annotateTools([
|
|
1307
1308
|
{
|
|
1308
1309
|
name: "get_printer_status",
|
|
1309
1310
|
description: "Get the current status of the 3D printer",
|
|
@@ -1526,14 +1527,12 @@ class ThreeDPrinterMCPServer {
|
|
|
1526
1527
|
description: "Describe this printer for the counterpart/1 contract: server name and version, printer kind and identity, and which features work (progress, layers, camera, filaments, pause, resume, cancel, print_3mf, G-code), detected from the firmware. Read-only.",
|
|
1527
1528
|
inputSchema: { type: "object", properties: { ...PRINTER_TARGET_PROPERTIES } },
|
|
1528
1529
|
outputSchema: PRINTER_INFO_OUTPUT_SCHEMA,
|
|
1529
|
-
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
1530
1530
|
},
|
|
1531
1531
|
{
|
|
1532
1532
|
name: "counterpart_print_progress",
|
|
1533
1533
|
description: "Read the printer's state and current job in the counterpart/1 shape: state, job name, progress (0-100), elapsed and remaining seconds, layers when the firmware reports them, temperatures and problems. job.name matches the jobName that upload_gcode and start_print return. Read-only.",
|
|
1534
1534
|
inputSchema: { type: "object", properties: { ...PRINTER_TARGET_PROPERTIES } },
|
|
1535
1535
|
outputSchema: PRINT_PROGRESS_OUTPUT_SCHEMA,
|
|
1536
|
-
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
1537
1536
|
},
|
|
1538
1537
|
{
|
|
1539
1538
|
name: "camera_snapshot",
|
|
@@ -1547,7 +1546,6 @@ class ThreeDPrinterMCPServer {
|
|
|
1547
1546
|
},
|
|
1548
1547
|
},
|
|
1549
1548
|
outputSchema: CAMERA_SNAPSHOT_OUTPUT_SCHEMA,
|
|
1550
|
-
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
1551
1549
|
},
|
|
1552
1550
|
{
|
|
1553
1551
|
name: "get_printer_filaments",
|
|
@@ -1561,21 +1559,18 @@ class ThreeDPrinterMCPServer {
|
|
|
1561
1559
|
},
|
|
1562
1560
|
},
|
|
1563
1561
|
outputSchema: FILAMENTS_OUTPUT_SCHEMA,
|
|
1564
|
-
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
1565
1562
|
},
|
|
1566
1563
|
{
|
|
1567
1564
|
name: "pause_print",
|
|
1568
1565
|
description: "Pause the current print (Bambu Lab, OctoPrint, Klipper/Moonraker, PrusaLink, Duet M25, Repetier, and Creality printers that run Moonraker). Bambu pauses over the same connection that prints. Pause is never gated.",
|
|
1569
1566
|
inputSchema: { type: "object", properties: { ...PRINTER_TARGET_PROPERTIES, ...CONNECTION_PROPERTIES } },
|
|
1570
1567
|
outputSchema: CONTROL_OUTPUT_SCHEMA,
|
|
1571
|
-
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1572
1568
|
},
|
|
1573
1569
|
{
|
|
1574
1570
|
name: "resume_print",
|
|
1575
1571
|
description: "Resume a paused print. Resume reheats and moves the printer, so it only resumes a job this server instance inspected and started, and only when the printer freshly reports that job paused. Bambu re-reads the job over FTPS (or the plate G-code the printer extracted) and compares its SHA-256, and resumes over the same connection that printed. When the file cannot be re-read, a person confirms through MCP elicitation first (Bambu: only for files Bambu's plug-in placed; a file this server uploaded must re-read). Resume other jobs at the printer.",
|
|
1576
1572
|
inputSchema: { type: "object", properties: { ...PRINTER_TARGET_PROPERTIES, ...CONNECTION_PROPERTIES } },
|
|
1577
1573
|
outputSchema: CONTROL_OUTPUT_SCHEMA,
|
|
1578
|
-
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1579
1574
|
},
|
|
1580
1575
|
{
|
|
1581
1576
|
name: "set_printer_temperature",
|
|
@@ -2016,7 +2011,6 @@ class ThreeDPrinterMCPServer {
|
|
|
2016
2011
|
{
|
|
2017
2012
|
name: "inspect_print_file",
|
|
2018
2013
|
description: "Inspect an exact private snapshot of a prepared sliced 3MF or textual G-code file and selected plate without contacting a printer. Returns versioned file-only evidence; no printer readiness or permission to dispatch is established. Does not auto-slice.",
|
|
2019
|
-
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
2020
2014
|
inputSchema: {
|
|
2021
2015
|
type: "object",
|
|
2022
2016
|
additionalProperties: false,
|
|
@@ -2396,7 +2390,7 @@ class ThreeDPrinterMCPServer {
|
|
|
2396
2390
|
},
|
|
2397
2391
|
outputSchema: FUSION_EXPORT_OUTPUT_SCHEMA,
|
|
2398
2392
|
}
|
|
2399
|
-
]
|
|
2393
|
+
])
|
|
2400
2394
|
};
|
|
2401
2395
|
});
|
|
2402
2396
|
|
|
@@ -326,12 +326,24 @@ function logHiddenNotices(raw: Record<string, any>, serial: string, model: strin
|
|
|
326
326
|
}
|
|
327
327
|
}
|
|
328
328
|
|
|
329
|
+
/**
|
|
330
|
+
* 0300_8013, "Printing was paused by the user" / "paused due to the pause command added to the
|
|
331
|
+
* printing file": the printer's report of a deliberate pause (a Pause, or a pause_at_layers stop),
|
|
332
|
+
* which Resume clears. A paused P1S reports it as print_error, so it is not an error for resuming
|
|
333
|
+
* a paused job. requireIdle is false only for Resume here; starting a job still needs no error.
|
|
334
|
+
* Ported from bambu-printer-mcp src/safety/printer-state.ts.
|
|
335
|
+
*/
|
|
336
|
+
export const PAUSE_NOTICE = 0x03008013;
|
|
337
|
+
export function isPauseNotice(raw: Record<string, any>, requireIdle: boolean): boolean {
|
|
338
|
+
const state = typeof raw?.gcode_state === "string" ? raw.gcode_state.toUpperCase() : "";
|
|
339
|
+
return !requireIdle && state === "PAUSE" && finiteNumber(raw?.print_error) === PAUSE_NOTICE;
|
|
340
|
+
}
|
|
329
341
|
function validateErrors(raw: Record<string,any>, requireIdle: boolean, serial: string, model: string) {
|
|
330
342
|
const state = typeof raw.gcode_state === "string" ? raw.gcode_state.toUpperCase() : "UNKNOWN";
|
|
331
343
|
const terminal = isCancelledX2DJob(raw, model) || isFinishedP1SJob(raw, model);
|
|
332
344
|
if (requireIdle && !["IDLE","FINISH"].includes(state) && !terminal) throw new Error(`Printer state ${state} is not safely idle.`);
|
|
333
345
|
if (!requireIdle && !["IDLE","FINISH","RUNNING","PAUSE"].includes(state) && !terminal) throw new Error(`Printer state ${state} is unknown or failed.`);
|
|
334
|
-
if (finiteNumber(raw.print_error) !== 0) throw new Error("Printer reports a print error or did not report its error state.");
|
|
346
|
+
if (finiteNumber(raw.print_error) !== 0 && !isPauseNotice(raw, requireIdle)) throw new Error("Printer reports a print error or did not report its error state.");
|
|
335
347
|
if (!Array.isArray(raw.hms)) throw new Error("Printer HMS error state is missing or malformed.");
|
|
336
348
|
for (const reported of raw.hms) {
|
|
337
349
|
const entry = hmsCode(reported);
|