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 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.26",
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.26",
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.26",
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);