From d6f04d6ffd75eb443ace5c7ee11080ff473a3b1c Mon Sep 17 00:00:00 2001 From: DMontgomery40 Date: Tue, 29 Sep 2026 16:41:43 -0600 Subject: [PATCH 1/5] chore: prepare MCP Registry registration @codex review --- CHANGELOG.md | 8 ++++++++ package-lock.json | 4 ++-- package.json | 3 ++- server.json | 29 +++++++++++++++++++++++++++++ 4 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 server.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 1783300..419ae8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 1.2.11 + +### Discovery + +- Add the official MCP Registry ownership field and server metadata for the + npm package. The listing documents printer backend configuration and marks + API keys and Bambu LAN access codes as secrets. + ## 1.2.10 ### Safety diff --git a/package-lock.json b/package-lock.json index 112b361..5f1d223 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mcp-3d-printer-server", - "version": "1.2.10", + "version": "1.2.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mcp-3d-printer-server", - "version": "1.2.10", + "version": "1.2.11", "license": "GPL-2.0", "dependencies": { "@modelcontextprotocol/sdk": "^1.27.0", diff --git a/package.json b/package.json index c8c52be..7f58d99 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "mcp-3d-printer-server", - "version": "1.2.10", + "mcpName": "io.github.DMontgomery40/mcp-3d-printer-server", + "version": "1.2.11", "description": "MCP server for connecting Claude with 3D printer management systems", "main": "dist/index.js", "repository": "https://github.com/DMontgomery40/mcp-3D-printer-server", diff --git a/server.json b/server.json new file mode 100644 index 0000000..6308f6f --- /dev/null +++ b/server.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + "name": "io.github.DMontgomery40/mcp-3d-printer-server", + "title": "MCP 3D Printer Server", + "description": "Control 3D printers, edit STL/3MF models, slice files, and connect Blender through MCP.", + "repository": { + "url": "https://github.com/DMontgomery40/mcp-3D-printer-server", + "source": "github" + }, + "version": "1.2.11", + "packages": [ + { + "registryType": "npm", + "identifier": "mcp-3d-printer-server", + "version": "1.2.11", + "runtimeHint": "npx", + "transport": { "type": "stdio" }, + "environmentVariables": [ + { "name": "PRINTER_TYPE", "description": "Printer backend: octoprint, klipper, duet, repetier, bambu, prusa, or creality.", "isRequired": true }, + { "name": "PRINTER_HOST", "description": "Printer IP address or hostname.", "isRequired": true }, + { "name": "PRINTER_PORT", "description": "Optional backend-specific port." }, + { "name": "API_KEY", "description": "API key for printer backends that require authentication.", "isSecret": true }, + { "name": "BAMBU_SERIAL", "description": "Printer serial number; required for Bambu Lab." }, + { "name": "BAMBU_TOKEN", "description": "LAN access code; required for Bambu Lab.", "isSecret": true }, + { "name": "BAMBU_MODEL", "description": "Exact Bambu model; required for Bambu print operations." } + ] + } + ] +} From 70ba271bde09abe50fb1d1703a244616ff76b8c2 Mon Sep 17 00:00:00 2001 From: Jack Manning Date: Fri, 2 Oct 2026 16:48:50 -0400 Subject: [PATCH 2/5] fix: send X-Api-Key header on Klipper/Moonraker requests --- src/printers/klipper.ts | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/printers/klipper.ts b/src/printers/klipper.ts index 4281de4..3a14f6a 100644 --- a/src/printers/klipper.ts +++ b/src/printers/klipper.ts @@ -9,21 +9,25 @@ function encodePath(filename: string): string { export class KlipperImplementation extends GenericPrinterImplementation { protected readonly printerLabel = "Klipper/Moonraker"; + protected authHeaders(apiKey: string): Record { + return apiKey ? { headers: { "X-Api-Key": apiKey } } : {}; + } + async getStatus(host: string, port: string, apiKey: string) { const url = `http://${host}:${port}/printer/info`; - const response = await this.apiClient.get(url); + const response = await this.apiClient.get(url, this.authHeaders(apiKey)); return response.data; } async getFiles(host: string, port: string, apiKey: string) { const url = `http://${host}:${port}/server/files/list`; - const response = await this.apiClient.get(url); + const response = await this.apiClient.get(url, this.authHeaders(apiKey)); return response.data; } async getFile(host: string, port: string, apiKey: string, filename: string) { const url = `http://${host}:${port}/server/files/metadata?filename=${encodeURIComponent(filename)}`; - const response = await this.apiClient.get(url); + const response = await this.apiClient.get(url, this.authHeaders(apiKey)); return response.data; } @@ -35,7 +39,10 @@ export class KlipperImplementation extends GenericPrinterImplementation { protected async readPrinterState(host: string, port: string, apiKey: string): Promise { let data: any; try { - data = (await this.apiClient.get(`http://${host}:${port}/printer/objects/query?webhooks&print_stats`)).data; + data = (await this.apiClient.get( + `http://${host}:${port}/printer/objects/query?webhooks&print_stats`, + this.authHeaders(apiKey) + )).data; } catch (error) { throw new Error(`Cannot read Klipper/Moonraker printer state; nothing was sent: ${(error as Error).message}`); } @@ -60,7 +67,11 @@ export class KlipperImplementation extends GenericPrinterImplementation { } protected async downloadRemoteFile(host: string, port: string, apiKey: string, filename: string, destination: string): Promise { - await this.downloadToFile(`http://${host}:${port}/server/files/gcodes/${encodePath(filename)}`, {}, destination); + await this.downloadToFile( + `http://${host}:${port}/server/files/gcodes/${encodePath(filename)}`, + apiKey ? { "X-Api-Key": apiKey } : {}, + destination + ); } /** Current Moonraker returns {result:{item}}; older releases returned result "success". */ @@ -77,7 +88,8 @@ export class KlipperImplementation extends GenericPrinterImplementation { const response = await this.apiClient.post(url, formData as any, { headers: { - ...formData.getHeaders() + ...formData.getHeaders(), + ...(apiKey ? { "X-Api-Key": apiKey } : {}) } }); @@ -87,7 +99,7 @@ export class KlipperImplementation extends GenericPrinterImplementation { protected async rawStartJob(host: string, port: string, apiKey: string, filename: string) { const url = `http://${host}:${port}/printer/print/start`; - const response = await this.apiClient.post(url, { filename } as any); + const response = await this.apiClient.post(url, { filename } as any, this.authHeaders(apiKey)); return response.data; } @@ -95,7 +107,7 @@ export class KlipperImplementation extends GenericPrinterImplementation { protected async rawCancelJob(host: string, port: string, apiKey: string) { const url = `http://${host}:${port}/printer/print/cancel`; - const response = await this.apiClient.post(url, null as any); + const response = await this.apiClient.post(url, null as any, this.authHeaders(apiKey)); return response.data; } @@ -114,7 +126,7 @@ export class KlipperImplementation extends GenericPrinterImplementation { const response = await this.apiClient.post(url, { script: gcode - } as any); + } as any, this.authHeaders(apiKey)); return response.data; } From 15aa75e4e3dbf8c12543b5bbcf2324a6a5d422a8 Mon Sep 17 00:00:00 2001 From: nippo22 Date: Tue, 6 Oct 2026 09:16:06 -0600 Subject: [PATCH 3/5] Port A1 M109 H wait compatibility from the Bambu fork --- src/safety/print-file.ts | 4 +++- tests/safety/print-file.test.mjs | 11 +++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/safety/print-file.ts b/src/safety/print-file.ts index 9f51b6b..ce08ff4 100644 --- a/src/safety/print-file.ts +++ b/src/safety/print-file.ts @@ -275,7 +275,9 @@ export async function inspectPrintFile(filePath: string, options: {model:string; } if(['M104','M109','M140','M190','M141','M191'].includes(code)) { const args=parameters(argumentsText,'A'); - for(const key of args.keys()) if(!'SRTA'.includes(key)) fail(`unsupported ${code} temperature parameter ${key}`); + const vendorWait=code==='M109' && ['a1','a1mini'].includes(model); + for(const key of args.keys()) if(!'SRTA'.includes(key) && !(key==='H' && vendorWait)) fail(`unsupported ${code} temperature parameter ${key}`); + if(args.has('H') && (!Number.isFinite(args.get('H')) || args.get('H')!<0 || args.get('H')!>300)) fail(`unsupported ${code} wait parameter H`); if(!args.has('S')&&!args.has('R')) fail(`missing ${code} temperature target`); const component= ['M104','M109'].includes(code)?'nozzle': ['M140','M190'].includes(code)?'bed':'chamber'; // Heater T is a physical nozzle, unlike standalone remapped T filament selection. diff --git a/tests/safety/print-file.test.mjs b/tests/safety/print-file.test.mjs index dfc34a5..9dfd778 100644 --- a/tests/safety/print-file.test.mjs +++ b/tests/safety/print-file.test.mjs @@ -348,3 +348,14 @@ test('ambiguous heater candidates still enforce every possible material temperat const selected=await inspect(t,'',{model:'h2d'},{...entries,'Metadata/plate_1.gcode':'T1\nM104 S300\n'}); assert.deepEqual(selected.usedFilamentPositions,[1]); }); + +test('A1 and A1 mini M109 H wait limits are accepted while S stays the checked target',async t=>{ + // Official Bambu Studio 02.08 start G-code: A1 emits M109 S25 H140 and M109 S220 H300; A1 mini emits M109 S100 H170. + const a1=await inspect(t,header('A1')+'M109 S25 H140\nM109 S220 H300\nG1 X10 Y10 E1\n',{model:'a1'}); + assert.equal(a1.maxNozzleTemperature,220); + assert.equal((await inspect(t,header('A1 mini')+'M109 S100 H170\n',{model:'a1mini'})).maxNozzleTemperature,100); + for(const command of ['M109 S25 H301','M109 S25 H-1','M109 S25 H','M109 S400 H140','M109 R400 H140','M104 S220 H140','M190 S60 H140','M109 H140']) + await assert.rejects(inspect(t,header('A1')+command+'\n',{model:'a1'}),/temperature|unsupported|parameter|limit|target/i,command); + for(const model of ['P1S','X1C','H2D']) + await assert.rejects(inspect(t,header(model,'PLA',model==='H2D'?'0.4;0.4':'0.4')+'M109 S25 H140\n',{model:model.toLowerCase()}),/temperature parameter H/,model); +}); From 8fc3eb055faeaef36a636af35fa1192536fc1ea6 Mon Sep 17 00:00:00 2001 From: DMontgomery40 Date: Tue, 6 Oct 2026 09:17:33 -0600 Subject: [PATCH 4/5] Release 1.2.11: authenticated Moonraker, Bambu safety and MCP interoperability --- AGENTS.md | 5 ++- CHANGELOG.md | 33 ++++++++++++++++- CONTRIBUTORS.md | 4 ++ README.md | 5 ++- docs/SETUP.md | 10 ++--- package-lock.json | 57 +++++++++++++---------------- package.json | 13 ++++--- src/3mf_parser.ts | 16 ++++---- src/index.ts | 18 ++++----- src/printers/bambu.ts | 16 ++++---- src/safety/printer-state.ts | 2 +- src/stl/stl-manipulator.ts | 8 ++-- tests/bambu-file-metadata.test.mjs | 20 ++++++++++ tests/klipper-auth.test.mjs | 36 ++++++++++++++++++ tests/parser-stdio.test.mjs | 27 ++++++++++++++ tests/safety/printer-state.test.mjs | 12 ++++++ 16 files changed, 206 insertions(+), 76 deletions(-) create mode 100644 tests/bambu-file-metadata.test.mjs create mode 100644 tests/klipper-auth.test.mjs create mode 100644 tests/parser-stdio.test.mjs diff --git a/AGENTS.md b/AGENTS.md index 4414be9..65ecfb8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,7 @@ This file is the repository's shared source of truth for local agents, scheduled - Before opening or updating a PR, run Matt Pocock's `code-review` skill against the main merge base and resolve substantive findings locally. GitHub CI is the expensive gate, so it should only see reviewed work. Then use a PR to main and wait for CI on the current head; a passing local test does not authorize ignoring current CI failures. Codex review is not part of this process: don't put `@codex review` in commit messages or PR comments unless the maintainer asks for it. - Prioritize substantive defects and regressions in supported workflows. Document and defer obscure edge cases, speculative hardening, and cosmetic objections instead of extending a sound release into an endless review loop. - The main-branch Publish Package workflow (`.github/workflows/publish.yml`) runs the checks and `npm publish` with trusted publishing; do not publish from a local checkout. Verify that workflow and the exact npm version before claiming publication. Then create a `v` tag and GitHub release with the accumulated changelog notes. Verify npm, the tag, the release, and a fresh `npx -y mcp-3d-printer-server` startup before declaring the release complete. Never move a published tag. -- Changes to src/, scripts/, or printer behavior need a present-tense CHANGELOG.md entry under `## Unreleased`, including evidence limits (mocked transports versus real hardware). +- Changes to src/, scripts/, or printer behavior need a present-tense CHANGELOG.md entry under `## Unreleased`, including evidence limits (mocked transports versus real hardware). Promote those notes to the release version and date before merging; released changes must never remain under Unreleased on main. Synchronize server.json's server and npm package versions with package.json. Verify the Pages deployment and live `/project/changelog` version/date after merging, not just the local site build. - Never add AI or agent attribution anywhere in Git or GitHub: no `Co-Authored-By` trailers for Claude, Codex, or other assistants, no "Generated with" footers, and no agent session links in commits, PR titles or bodies, review replies, issue comments, or release notes. - Preserve original authorship when integrating contributor PRs. Credit code, issue reports, hardware evidence, and useful superseded proposals in CONTRIBUTORS.md. @@ -31,12 +31,15 @@ This file is the repository's shared source of truth for local agents, scheduled - Multi-printer MCP server: OctoPrint, Klipper (Moonraker), Duet, Repetier, Bambu Lab, Prusa Connect/PrusaLink, and Creality Cloud. `PRINTER_TYPE` selects the adapter. Transports: stdio by default and streamable-http. - Bambu uses MQTT on port 8883 for commands/status and FTPS on port 990 for files. Use `basic-ftp` directly for uploads and `bambu-node` for MQTT commands. +- File listings and metadata lookups are read-only FTPS operations with the shared TLS-session options. Runtime diagnostics go to stderr; stdout is reserved for MCP protocol messages. - bambu-printer-mcp is a Bambu-only fork of this repository. Safety-critical changes must be ported to both repositories, each through its own review and release rules. Report a required port you could not complete. ## Printer and slicer safety - Every print and positive-heating path must pass the shared gates in `src/safety/`: finite temperature validation before any connection; independent hardware ceilings per heater (Bambu per model, other printers from server configuration) that a request or file can never raise; declared-material ceilings; inspection of the exact file bytes to be dispatched, including all heater commands and their S/R targets; fresh printer state that is ready and free of actionable errors; and human confirmation through MCP elicitation unless the server explicitly opts out. Heater-off, pause, and cancel/stop are never gated. - `BAMBU_MODEL` (or the explicit tool model) is required for every Bambu print operation. Elicit it when missing and never guess: G-code for the wrong model can damage hardware. +- Fresh returned serial prefix `039` identifies the full-size A1; `030` identifies A1 mini. Configured serial/model values never replace fresh observed identity or suppress conflicts. +- Accept the official A1/A1 mini `M109 H` wait parameter only for those models and from 0 to 300. Independently check S/R heater targets; missing targets, malformed waits, other heater commands, and other models still reject. - Never swallow inspection or auto-slice failures and fall back to printing the original, unsliced file. Stop with the actionable error before upload or dispatch. - Resolve Bambu-compatible slicer profiles recursively (inherits and includes) and require the exact model/nozzle machine preset from the selected installation before CLI slicing. Missing references, cycles, or malformed profiles stop preparation. - Remote-file starts must not bypass inspection. Raw bridge methods (such as the FULU BambuNetwork RPC) must not bypass the shared gates. diff --git a/CHANGELOG.md b/CHANGELOG.md index 419ae8a..5f71d99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,37 @@ # Changelog -## 1.2.11 +## 1.2.11 — 2026-10-06 + +### Fixed + +- Send Moonraker's configured `API_KEY` as `X-Api-Key` on every request, + including multipart uploads and remote-file inspection downloads. Thanks to + Jack Manning (@EastArctica) for [#30](https://github.com/DMontgomery40/mcp-3D-printer-server/pull/30) + and Klipper 0.13.0 authentication evidence. Loopback tests cover authenticated + and trusted-client configurations; no maintainer physical print was run. +- Correct fresh Bambu A1/A1 mini serial identity (`039` / `030`) and accept the + official A1/A1 mini `M109 H` wait parameter without weakening independently + checked S/R temperature targets. Port the fork's fixes reported by @Steavie + ([#39](https://github.com/DMontgomery40/bambu-printer-mcp/issues/39)) and implemented + by @nitpreet22 ([#41](https://github.com/DMontgomery40/bambu-printer-mcp/pull/41)). + Covered by mocked identity and file-inspection regressions. +- Keep model parsing, slicing, and STL diagnostics on stderr so they cannot + corrupt stdio MCP messages, following @travismcashan's report in the fork's + [#44](https://github.com/DMontgomery40/bambu-printer-mcp/issues/44). Subprocess + regressions check the compiled parser and slicer. +- Make Bambu file metadata lookup read-only through the shared FTPS client; + remove the obsolete `bambu-js` dependency that created directories during reads. + +### Dependencies and interoperability + +- Update the MCP SDK to 1.32.1, basic-ftp to 6.2.2, JSZip to 3.10.2, dotenv + within version 16, and proxy-addr to its patched release. The production npm + audit reports no vulnerabilities; unresolved nodemon watcher advisories are + development-only and remain deferred rather than forcing an unsafe downgrade. +- Verify discovery, invocation, and STL export with upstream mcp-for-blender + 2.1.9 and the former blender-mcp 2.0.0 package in isolated Blender 5.0.1. + The exported fixture has 12 triangles and measures 40 × 20 × 10 mm. This + verifies application interoperability, not a user's live scene or a print. ### Discovery diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index f5f1541..dd68716 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -6,6 +6,8 @@ Thank you to everyone who builds, tests, reports problems, and shares real print | Contributor | Contribution | | --- | --- | +| [Jack Manning (EastArctica)](https://github.com/EastArctica) | Sends `X-Api-Key` on every Klipper/Moonraker request, including uploads and remote-file downloads, in [#30](https://github.com/DMontgomery40/mcp-3D-printer-server/pull/30). Supplies Klipper 0.13.0 evidence of authenticated status succeeding after the previous 401 response. | +| [nitpreet22](https://github.com/nitpreet22) | Adds A1/A1 mini `M109 H` wait compatibility and safety regressions in the Bambu fork's [#41](https://github.com/DMontgomery40/bambu-printer-mcp/pull/41), ported here with original code authorship. Shares Windows A1 slicing and file-inspection evidence; this is not physical-print verification. | | [Javier Cortejoso (jcortejoso)](https://github.com/jcortejoso) | Reads the FULU bridge command from server configuration by default, with an explicit opt-in for per-call bridge commands, in [#20](https://github.com/DMontgomery40/mcp-3D-printer-server/pull/20). [#21](https://github.com/DMontgomery40/mcp-3D-printer-server/pull/21) extends the same gate to every per-call executable selector. | | [Ewan Monro (heyitsmeez)](https://github.com/heyitsmeez) | Reports in [#17](https://github.com/DMontgomery40/mcp-3D-printer-server/issues/17) that a top-level `anyOf` in the `upload_gcode` input schema made the Anthropic API reject every tool, and fixes it in [#18](https://github.com/DMontgomery40/mcp-3D-printer-server/pull/18). | | [David Gageot (dgageot)](https://github.com/dgageot) | Fixes the Docker build and hardens the image (non-root user, pinned base image, production dependencies only, build caching) in [#3](https://github.com/DMontgomery40/mcp-3D-printer-server/pull/3). | @@ -16,6 +18,8 @@ Thank you to everyone who builds, tests, reports problems, and shares real print | Contributor | Contribution | | --- | --- | +| [Steavie](https://github.com/Steavie) | Reports reversed A1/A1 mini serial identity in the Bambu fork's [#39](https://github.com/DMontgomery40/bambu-printer-mcp/issues/39). This repository now uses the corrected prefixes for fresh safety identity, with mocked MQTT regressions. | +| [Travis Cashman (travismcashan)](https://github.com/travismcashan) | Reports MCP stdout pollution from model parsing and slicing in the Bambu fork's [#44](https://github.com/DMontgomery40/bambu-printer-mcp/issues/44). The stderr correction and subprocess regressions are also applied here. | | [Lickitysplitted](https://github.com/Lickitysplitted) | Diagnoses Bambu FTPS "Premature close" upload failures on an X1C as missing TLS session reuse in [#22](https://github.com/DMontgomery40/mcp-3D-printer-server/issues/22). Release 1.2.9 addresses it; confirmation on that X1C firmware is still outstanding. | | [CrowSoda](https://github.com/CrowSoda) | Tries slicing and printing on a Creality K1 Max through Klipper and Moonraker, and reports invalid OrcaSlicer CLI flags, missing filament-profile support, and unregistered upload and start tools in [#16](https://github.com/DMontgomery40/mcp-3D-printer-server/issues/16). All three are fixed. | | [dongio20](https://github.com/dongio20) | Reports the PrusaLink 0.8.1 status 404, with firmware details and the working endpoint, in [#11](https://github.com/DMontgomery40/mcp-3D-printer-server/issues/11). Fixed in [#15](https://github.com/DMontgomery40/mcp-3D-printer-server/pull/15). | diff --git a/README.md b/README.md index c772a75..3a57623 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,8 @@ Preserve my existing servers and settings. Prefer the published npm package Ask which printer system I use and set PRINTER_TYPE to match. Never guess it. Then ask only for the values that backend needs: - octoprint: PRINTER_HOST, PRINTER_PORT if not 80, API_KEY -- klipper (Moonraker): PRINTER_HOST, PRINTER_PORT (usually 7125) +- klipper (Moonraker): PRINTER_HOST, PRINTER_PORT (usually 7125), + API_KEY if Moonraker requires authentication - duet: PRINTER_HOST, PRINTER_PORT if not 80 - repetier: PRINTER_HOST, PRINTER_PORT (usually 3344), API_KEY - bambu: PRINTER_HOST, BAMBU_SERIAL, BAMBU_TOKEN (the LAN access code), @@ -764,7 +765,7 @@ For example, `printer://192.168.1.100/status` reads the status of the printer at 1. **Status depth varies by backend.** Bambu status includes progress, layers, and time remaining. OctoPrint status comes from `/api/printer` (state and temperatures, not job progress). Klipper status comes from Moonraker's `/printer/info`, which reports the host state but not job progress or temperatures. Duet, Repetier, and Creality responses have not been verified on hardware. 2. **Cancel, but no pause.** There is no pause or resume tool. `cancel_print` stops the job. 3. **Plain HTTP for most backends.** OctoPrint, Klipper, Duet, Repetier, and Creality adapters connect over `http://`. The Prusa adapter uses HTTPS for Prusa Connect, an `https://` host, or port 443. -4. **Klipper and Duet send no credentials.** Moonraker must trust the MCP host, and a password-protected Duet cannot be reached yet. +4. **Moonraker authentication is optional; Duet has no password support.** Set `API_KEY` for Moonraker hosts that require authentication. Leave it empty only when Moonraker trusts the MCP host. A password-protected Duet cannot be reached yet. 5. **Command sent is not print finished.** A success response means the printer or its host accepted the request. Check status, and the printer itself, before you walk away. 6. **Bambu prints need a sliced project and the right model.** `print_3mf` needs a `.3mf` with `Metadata/plate_.gcode`, uploads it to `cache/`, and starts plate 1. Print settings such as layer height and temperatures cannot be changed at print time. `start_print` handles plain `.gcode` files only. 7. **Bambu AMS mapping is simple.** `ams_mapping` values are sorted and padded to five entries; real behavior still depends on firmware, loaded filament, and the project's metadata. For AMS inventory and color matching, see [bambu-printer-mcp](https://github.com/DMontgomery40/bambu-printer-mcp). diff --git a/docs/SETUP.md b/docs/SETUP.md index 30cced9..595015d 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -55,7 +55,7 @@ npm link |---|---|---|---|---| | `bambu` | Bambu Lab | MQTT over TLS on port 8883, FTPS on port 990 | `BAMBU_SERIAL`, `BAMBU_TOKEN` (LAN access code), `BAMBU_MODEL` | **Most tested.** Maintainer hardware testing, shared with the Bambu-only fork. The FTPS fix for [#22](https://github.com/DMontgomery40/mcp-3D-printer-server/issues/22) is tested against a local FTPS server; confirmation on the reporter's X1C is outstanding. | | `octoprint` | OctoPrint | HTTP REST API, port 80 on OctoPi | `API_KEY`, sent as `X-Api-Key` | **Community-reported.** Used against a real OctoPrint instance in [#4](https://github.com/DMontgomery40/mcp-3D-printer-server/issues/4). | -| `klipper` | Klipper (Moonraker) | HTTP API, usually port 7125 | None sent; Moonraker must trust the MCP host | **Community-reported.** Used with a Creality K1 Max through Moonraker in [#16](https://github.com/DMontgomery40/mcp-3D-printer-server/issues/16). | +| `klipper` | Klipper (Moonraker) | HTTP API, usually port 7125 | Optional `API_KEY`, sent as `X-Api-Key`; otherwise Moonraker must trust the MCP host | **Community-reported.** Used with a Creality K1 Max through Moonraker in [#16](https://github.com/DMontgomery40/mcp-3D-printer-server/issues/16). | | `prusa` | PrusaLink / Prusa Connect | HTTP for local PrusaLink, HTTPS for `connect.prusa3d.com` or port 443 | `API_KEY`, sent as `X-Api-Key` and as a bearer token | **Community-reported.** PrusaLink 0.8.1 status fixed after [#11](https://github.com/DMontgomery40/mcp-3D-printer-server/issues/11); Prusa Connect setup discussed in [#9](https://github.com/DMontgomery40/mcp-3D-printer-server/issues/9). | | `duet` | Duet | HTTP, port 80 | None sent | **Unverified.** No hardware reports yet. | | `repetier` | Repetier-Server | HTTP, usually port 3344 | `API_KEY`, sent as the `apikey` query parameter | **Unverified.** No hardware reports yet. | @@ -74,7 +74,7 @@ Create a `.env` file in the directory where you run the server, or pass environm PRINTER_TYPE=octoprint # octoprint, klipper, duet, repetier, bambu, prusa, creality PRINTER_HOST=192.168.1.100 # Printer or host-software address PRINTER_PORT=80 # 7125 for Moonraker, 3344 for Repetier-Server -API_KEY=your_api_key # OctoPrint, Repetier, Prusa, Creality +API_KEY=your_api_key # OctoPrint, authenticated Moonraker, Repetier, Prusa, Creality # --- Bambu Lab only --- # BAMBU_SERIAL=01P00A123456789 # Printer serial number @@ -118,7 +118,7 @@ API_KEY=your_api_key # OctoPrint, Repetier, Prusa, Creality | `PRINTER_TYPE` | `octoprint` | Yes | Printer adapter: `octoprint`, `klipper`, `duet`, `repetier`, `bambu`, `prusa`, or `creality` | | `PRINTER_HOST` | `localhost` | Yes | Printer or host-software address. Prusa also accepts `connect.prusa3d.com` or a full `https://` URL | | `PRINTER_PORT` | `80` | Depends on backend | HTTP port for non-Bambu backends. Set `7125` for Moonraker and `3344` for Repetier-Server | -| `API_KEY` | | OctoPrint, Repetier, Prusa, Creality | Backend API key or token. Klipper and Duet adapters do not send one | +| `API_KEY` | | OctoPrint, authenticated Moonraker, Repetier, Prusa, Creality | Backend API key or token. Duet does not send one | | `BAMBU_SERIAL` | | Bambu | Printer serial number | | `BAMBU_TOKEN` | | Bambu | LAN access code shown on the printer | | `BAMBU_MODEL` | | **Bambu printing and Bambu slicing** | Printer model: `p1s`, `p1p`, `x1c`, `x1e`, `a1`, `a1mini`, `h2d`. **Required** for `print_3mf`, `start_print`, upload with `print: true`, positive Bambu heating, and Bambu-compatible slicing. Slicing also accepts `p2s`, `h2s`, and `h2c` when the installed slicer has that preset. If omitted and the MCP client supports elicitation, the server asks. It selects the slicer machine preset and is checked against the live printer before printing. | @@ -208,7 +208,7 @@ Replace the `env` block with the values for your backend: | Backend | `env` values | |---|---| | OctoPrint | `PRINTER_TYPE=octoprint`, `PRINTER_HOST`, `PRINTER_PORT` (80 on OctoPi, 5000 for `octoprint serve`), `API_KEY` | -| Klipper (Moonraker) | `PRINTER_TYPE=klipper`, `PRINTER_HOST`, `PRINTER_PORT=7125` | +| Klipper (Moonraker) | `PRINTER_TYPE=klipper`, `PRINTER_HOST`, `PRINTER_PORT=7125`, `API_KEY` if required | | Duet | `PRINTER_TYPE=duet`, `PRINTER_HOST`, `PRINTER_PORT` if not 80 | | Repetier-Server | `PRINTER_TYPE=repetier`, `PRINTER_HOST`, `PRINTER_PORT=3344`, `API_KEY` | | Bambu Lab | `PRINTER_TYPE=bambu`, `PRINTER_HOST`, `BAMBU_SERIAL`, `BAMBU_TOKEN`, `BAMBU_MODEL` | @@ -289,7 +289,7 @@ The adapter connects over plain HTTP and sends the key as `X-Api-Key`. It calls ### Klipper (Moonraker) 1. Make sure Moonraker is reachable from the machine running the MCP server, usually on port 7125. -2. The adapter does not send an API key. Allow the MCP host in Moonraker's `[authorization]` `trusted_clients`, or use an installation without Moonraker authorization. +2. If Moonraker requires authentication, set `API_KEY` to its API key. The adapter sends `X-Api-Key` on status, file, upload, start, cancel, and heater requests. Leave it empty only when Moonraker trusts the MCP host. See [Moonraker authentication](https://moonraker.readthedocs.io/en/latest/external_api/authorization/). 3. Set `PRINTER_TYPE=klipper`, `PRINTER_HOST`, and `PRINTER_PORT=7125`. Status comes from `/printer/info`, which reports the Klipper host state (for example `ready`) but not job progress or temperatures; [#12](https://github.com/DMontgomery40/mcp-3D-printer-server/issues/12) requests richer job status. The safety gate reads `/printer/objects/query?webhooks&print_stats` to refuse a printer that is printing, paused, or in error. Other calls: `/server/files/list`, `/server/files/upload`, `/server/files/gcodes/` (download for inspection before `start_print`), `/printer/print/start`, `/printer/print/cancel`, and `SET_HEATER_TEMPERATURE` through `/printer/gcode/script` for `bed` and `extruder`. diff --git a/package-lock.json b/package-lock.json index 5f1d223..53210c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,14 +9,13 @@ "version": "1.2.11", "license": "GPL-2.0", "dependencies": { - "@modelcontextprotocol/sdk": "^1.27.0", + "@modelcontextprotocol/sdk": "^1.32.1", "axios": "^1.20.0", - "bambu-js": "^2.2.2", "bambu-node": "3.22.21", - "basic-ftp": "^5.3.1", - "dotenv": "^16.3.1", + "basic-ftp": "^6.2.2", + "dotenv": "^16.6.1", "form-data": "^4.0.6", - "jszip": "^3.10.1", + "jszip": "^3.10.2", "three": "^0.173.0", "xml2js": "^0.6.2", "zod": "^3.25.76" @@ -35,7 +34,7 @@ "typescript": "~5.9.3" }, "engines": { - "node": ">=20" + "node": ">=18" } }, "node_modules/@babel/runtime": { @@ -60,12 +59,12 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.0.tgz", - "integrity": "sha512-qOdO524oPMkUsOJTrsH9vz/HN3B5pKyW+9zIW51A9kDMVe7ON70drz1ouoyoyOcfzc+oxhkQ6jWmbyKnlWmYqA==", + "version": "1.32.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.32.1.tgz", + "integrity": "sha512-2DdE+SJDtzLEEWzY1ZjY7Q+VcPhcV1KisD3zI4u0XZyktsjHum1mwbMI+JaulUBi2OZk+KJAi2uPXzxichPkdw==", "license": "MIT", "dependencies": { - "@hono/node-server": "^1.19.9", + "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", @@ -329,16 +328,6 @@ "node": "18 || 20 || >=22" } }, - "node_modules/bambu-js": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/bambu-js/-/bambu-js-2.2.2.tgz", - "integrity": "sha512-A8KGH+CINYDCqYp6UKk4sD4ffabvvEKKL4UeyQwQdd+EwguOHx1cD3eYQBP+4FmTAjGfx0VJAoWtd4/9fAFejQ==", - "license": "MIT", - "dependencies": { - "basic-ftp": "^5.0.5", - "mqtt": "^5.10.1" - } - }, "node_modules/bambu-node": { "version": "3.22.21", "resolved": "https://registry.npmjs.org/bambu-node/-/bambu-node-3.22.21.tgz", @@ -371,9 +360,9 @@ "license": "MIT" }, "node_modules/basic-ftp": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", - "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-6.2.2.tgz", + "integrity": "sha512-LIkJcf/F6bTTfvE3k2YcTxrJsLKKVXiEpHZYkQP6JoqTb7khng7c13ckRTDQbqRh5IkehVH3F00sSlAa2F6DwA==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -716,9 +705,9 @@ } }, "node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -1426,9 +1415,9 @@ "license": "BSD-2-Clause" }, "node_modules/jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.2.tgz", + "integrity": "sha512-3l+rb15IOWtUhU0H5MFqES/T6Kh7abYwjosBey/vD6hDt8zoEffkSC5Ws5SGtgVw3gBx2NEbhTeSW1+kWkpyTQ==", "license": "(MIT OR GPL-3.0-or-later)", "dependencies": { "lie": "~3.3.0", @@ -1790,9 +1779,9 @@ "license": "MIT" }, "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", "license": "MIT", "dependencies": { "forwarded": "0.2.0", @@ -1800,6 +1789,10 @@ }, "engines": { "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/proxy-from-env": { diff --git a/package.json b/package.json index 7f58d99..ba67023 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,9 @@ "dist/blender-mcp-bridge.js", "dist/blender-mcp-bridge.d.ts", "src/blender-mcp-bridge.ts", - "CHANGELOG.md" + "CHANGELOG.md", + "CONTRIBUTORS.md", + "server.json" ], "keywords": [ "mcp", @@ -77,14 +79,13 @@ "author": "David Montgomery", "license": "GPL-2.0", "dependencies": { - "@modelcontextprotocol/sdk": "^1.27.0", + "@modelcontextprotocol/sdk": "^1.32.1", "axios": "^1.20.0", - "bambu-js": "^2.2.2", "bambu-node": "3.22.21", - "basic-ftp": "^5.3.1", - "dotenv": "^16.3.1", + "basic-ftp": "^6.2.2", + "dotenv": "^16.6.1", "form-data": "^4.0.6", - "jszip": "^3.10.1", + "jszip": "^3.10.2", "three": "^0.173.0", "xml2js": "^0.6.2", "zod": "^3.25.76" diff --git a/src/3mf_parser.ts b/src/3mf_parser.ts index eb70a84..bf1a2b4 100644 --- a/src/3mf_parser.ts +++ b/src/3mf_parser.ts @@ -81,7 +81,7 @@ async function parseBambuConfig(zip: JSZip): Promise> if (file) { configFile = file; configContent = await file.async('string'); - console.log(`Found Bambu config file: ${configFile.name}`); + console.error(`Found Bambu config file: ${configFile.name}`); break; // Use the first one found (project usually has the most) } } @@ -91,14 +91,14 @@ async function parseBambuConfig(zip: JSZip): Promise> // Attempt to parse as JSON const jsonData = JSON.parse(configContent); const parsedConfig = parseBambuJSONConfig(jsonData); - console.log('Bambu config parsed successfully as JSON.'); + console.error('Bambu config parsed successfully as JSON.'); return parsedConfig; } catch (jsonError: any) { console.warn(`Failed to parse ${configFile.name} as JSON: ${jsonError.message}. Attempting INI parse as fallback...`); // Fallback: Try parsing as INI if JSON fails (though unlikely based on sample) try { const parsedConfig = parseFallbackINIConfig(configContent); // Keep a simple INI parser as fallback - console.log('Bambu config parsed successfully as INI (fallback).'); + console.error('Bambu config parsed successfully as INI (fallback).'); return parsedConfig; } catch (iniError: any) { console.error(`Error parsing Bambu config ${configFile.name} as INI (fallback):`, iniError); @@ -106,7 +106,7 @@ async function parseBambuConfig(zip: JSZip): Promise> } } } else { - console.log('No Bambu-specific config file found in Metadata directory.'); + console.error('No Bambu-specific config file found in Metadata directory.'); return {}; } } @@ -181,7 +181,7 @@ async function parse3DModelConfig(zip: JSZip): Promise<{ metadata: ThreeMFMetada }); } - console.log('3dmodel.model parsed successfully.'); + console.error('3dmodel.model parsed successfully.'); return { metadata, objects, build: { items: buildItems } }; } catch (error: any) { @@ -193,14 +193,14 @@ async function parse3DModelConfig(zip: JSZip): Promise<{ metadata: ThreeMFMetada export { ThreeMFData }; export async function parse3MF(filePath: string): Promise { - console.log(`Parsing 3MF file: ${filePath}`); + console.error(`Parsing 3MF file: ${filePath}`); try { const data = await fs.readFile(filePath); const zip = await JSZip.loadAsync(data); // List files for debugging // const fileList = Object.keys(zip.files); - // console.log('Files in 3MF:', fileList); + // console.error('Files in 3MF:', fileList); const { metadata, objects, build } = await parse3DModelConfig(zip); const bambuConfig = await parseBambuConfig(zip); @@ -213,7 +213,7 @@ export async function parse3MF(filePath: string): Promise { slicerConfig: bambuConfig }; - console.log('3MF parsing completed.'); + console.error('3MF parsing completed.'); return combinedData; } catch (error: any) { diff --git a/src/index.ts b/src/index.ts index f5f8ee7..c936a92 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2002,7 +2002,7 @@ class ThreeDPrinterMCPServer { // Define progress callback for UI updates const processProgressCallback = (progress: number, message?: string) => { - console.log(`Process progress: ${progress}% - ${message || ''}`); + console.error(`Process progress: ${progress}% - ${message || ''}`); }; // 1. Extend the base of the STL file @@ -2130,7 +2130,7 @@ class ThreeDPrinterMCPServer { // Define progress callback for UI updates const scaleProgressCallback = (progress: number, message?: string) => { - console.log(`Scale progress: ${progress}% - ${message || ''}`); + console.error(`Scale progress: ${progress}% - ${message || ''}`); }; let scaleFactors: number | [number, number, number]; @@ -2162,7 +2162,7 @@ class ThreeDPrinterMCPServer { // Define progress callback for UI updates const rotateProgressCallback = (progress: number, message?: string) => { - console.log(`Rotate progress: ${progress}% - ${message || ''}`); + console.error(`Rotate progress: ${progress}% - ${message || ''}`); }; // Get rotation angles, defaulting to 0 for any undefined axis @@ -2186,7 +2186,7 @@ class ThreeDPrinterMCPServer { // Define progress callback for UI updates const translateProgressCallback = (progress: number, message?: string) => { - console.log(`Translate progress: ${progress}% - ${message || ''}`); + console.error(`Translate progress: ${progress}% - ${message || ''}`); }; // Get translation values, defaulting to 0 for any undefined axis @@ -2210,7 +2210,7 @@ class ThreeDPrinterMCPServer { // Define progress callback for UI updates const modifySectionProgressCallback = (progress: number, message?: string) => { - console.log(`Modify section progress: ${progress}% - ${message || ''}`); + console.error(`Modify section progress: ${progress}% - ${message || ''}`); }; // Determine the section to modify @@ -2287,7 +2287,7 @@ class ThreeDPrinterMCPServer { // Define progress callback for UI updates const visualizationProgressCallback = (progress: number, message?: string) => { - console.log(`Visualization progress: ${progress}% - ${message || ''}`); + console.error(`Visualization progress: ${progress}% - ${message || ''}`); }; // Get width and height parameters, with defaults @@ -2347,7 +2347,7 @@ class ThreeDPrinterMCPServer { "slicer_path" ) ?? DEFAULT_SLICER_PATH; try { - console.log(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`); + console.error(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`); threeMFPath = await this.stlManipulator.sliceSTL( threeMFPath, slicerType, @@ -2359,7 +2359,7 @@ class ThreeDPrinterMCPServer { // Slice for the plate and nozzle the print will be checked against. { bedType: printBedType, ...(resolveNozzleType(args?.nozzle_type) ? { nozzleType: resolveNozzleType(args?.nozzle_type) } : {}) } ); - console.log("Auto-sliced to: " + threeMFPath); + console.error("Auto-sliced to: " + threeMFPath); } catch (sliceErr: any) { // Never fall back to uploading the original unsliced project. // Rethrow the original instance (for example a SlicerError) so @@ -2385,7 +2385,7 @@ class ThreeDPrinterMCPServer { try { // --- Parse 3MF --- const parsed3MFData = await parse3MF(threeMFPath); - console.log(`Successfully parsed 3MF file: ${threeMFPath}`); + console.error(`Successfully parsed 3MF file: ${threeMFPath}`); const amsOptions = this.resolveAmsPrintOptions( parsed3MFData, args as Record | undefined diff --git a/src/printers/bambu.ts b/src/printers/bambu.ts index cb56613..966d990 100644 --- a/src/printers/bambu.ts +++ b/src/printers/bambu.ts @@ -4,7 +4,6 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { Client as FTPClient } from "basic-ftp"; -import { BambuPrinter } from "bambu-js"; import { BambuClient, PushAllCommand } from "bambu-node"; import { MAX_PRINT_FILE_BYTES, readSafe3mfArchive } from "../safety/archive.js"; import { inspectPrintFile, type PrintFileInspection } from "../safety/print-file.js"; @@ -595,8 +594,7 @@ export class BambuImplementation extends PrinterImplementation { } async getFile(host: string, port: string, apiKey: string, filename: string) { - const [serial, token] = this.extractBambuCredentials(apiKey); - const printer = new BambuPrinter(host, serial, token); + const [, token] = this.extractBambuCredentials(apiKey); const normalized = filename.replace(/^\/+/, ""); const directory = path.posix.dirname(normalized) === "." ? "cache" : path.posix.dirname(normalized); @@ -604,10 +602,14 @@ export class BambuImplementation extends PrinterImplementation { let exists = false; - await printer.manipulateFiles(async (context) => { - const entries = await context.readDir(directory); - exists = entries.includes(baseName); - }); + const client = new FTPClient(15_000); + try { + await client.access(this.ftpsOptions(host, token)); + const entries = await client.list(`/${directory}`); + exists = entries.some(entry => entry.name === baseName); + } finally { + client.close(); + } return { name: `${directory}/${baseName}`, diff --git a/src/safety/printer-state.ts b/src/safety/printer-state.ts index 1119f0c..27ae3b7 100644 --- a/src/safety/printer-state.ts +++ b/src/safety/printer-state.ts @@ -21,7 +21,7 @@ const MODEL_IDS: Record = { }; const SERIAL_MODELS: Record = { "093": "H2S", "094": "H2D", "239": "H2C", "31B": "H2D Pro", "20P": "X2D", - "00M": "X1C", "00W": "X1", "03W": "X1E", "01S": "P1P", "01P": "P1S", "22E": "P2S", "030": "A1", "039": "A1 Mini", + "00M": "X1C", "00W": "X1", "03W": "X1E", "01S": "P1P", "01P": "P1S", "22E": "P2S", "039": "A1", "030": "A1 Mini", }; function object(value: unknown): value is Record { diff --git a/src/stl/stl-manipulator.ts b/src/stl/stl-manipulator.ts index f73a187..f37a8d8 100644 --- a/src/stl/stl-manipulator.ts +++ b/src/stl/stl-manipulator.ts @@ -870,7 +870,7 @@ export class STLManipulator extends EventEmitter { try { if (progressCallback) progressCallback(0, "Starting base extension operation..."); - console.log(`Extending base of ${stlFilePath} by ${extensionInches} inches`); + console.error(`Extending base of ${stlFilePath} by ${extensionInches} inches`); // Load the STL file const { geometry, boundingBox } = await this.loadSTL(stlFilePath, progressCallback); @@ -947,7 +947,7 @@ export class STLManipulator extends EventEmitter { output: outputFilePath }); - console.log(`Modified STL saved to ${outputFilePath}`); + console.error(`Modified STL saved to ${outputFilePath}`); return outputFilePath; } catch (error) { console.error("Error extending STL base:", error); @@ -1320,7 +1320,7 @@ export class STLManipulator extends EventEmitter { const mergedGeometry = BufferGeometryUtils.mergeVertices(geometry, tolerance); const newVertexCount = mergedGeometry.attributes.position.count; - console.log(`Merged vertices: ${originalVertexCount} -> ${newVertexCount} (Tolerance: ${tolerance}mm)`); + console.error(`Merged vertices: ${originalVertexCount} -> ${newVertexCount} (Tolerance: ${tolerance}mm)`); if (progressCallback) progressCallback(70, `Vertices merged: ${originalVertexCount} -> ${newVertexCount}`); if (!this.activeOperations.get(operationId)) throw new Error("Operation cancelled"); @@ -1364,7 +1364,7 @@ export class STLManipulator extends EventEmitter { if (center.lengthSq() < 0.0001) { // Already centered (or very close) if (progressCallback) progressCallback(100, "Model is already centered."); - console.log("Model already centered. No changes made."); + console.error("Model already centered. No changes made."); this.emit('operationComplete', { operationId, type: 'centerModel', success: true, output: stlFilePath, message: "Model already centered." }); return stlFilePath; // Return original path } diff --git a/tests/bambu-file-metadata.test.mjs b/tests/bambu-file-metadata.test.mjs new file mode 100644 index 0000000..e3c6568 --- /dev/null +++ b/tests/bambu-file-metadata.test.mjs @@ -0,0 +1,20 @@ +import assert from 'node:assert/strict'; +import {test} from 'node:test'; +import {Client} from 'basic-ftp'; +import {BambuImplementation} from '../dist/printers/bambu.js'; + +test('Bambu file metadata lists read-only through shared FTPS options and closes on failure',async t=>{ + const calls=[];let fail=false; + const original={access:Client.prototype.access,list:Client.prototype.list,ensureDir:Client.prototype.ensureDir,close:Client.prototype.close}; + Client.prototype.access=async function(options){calls.push(['access',options]);}; + Client.prototype.list=async function(directory){calls.push(['list',directory]);if(fail)throw new Error('listing fixture failed');return [{name:'job.gcode'}];}; + Client.prototype.ensureDir=async()=>{throw new Error('read-only metadata must never create directories');}; + Client.prototype.close=function(){calls.push(['close']);}; + t.after(()=>Object.assign(Client.prototype,original)); + const adapter=new BambuImplementation({}); + assert.deepEqual(await adapter.getFile('127.0.0.1','990','FIXTURE:TOKEN','job.gcode'),{name:'cache/job.gcode',exists:true}); + assert.equal(calls[0][1].port,990);assert.equal(calls[0][1].secure,'implicit');assert.equal(calls[0][1].secureOptions.host,'127.0.0.1');assert.ok(calls.some(c=>c[0]==='list'&&c[1]==='/cache')); + assert.deepEqual(await adapter.getFile('127.0.0.1','990','FIXTURE:TOKEN','/logs/missing.txt'),{name:'logs/missing.txt',exists:false}); + fail=true;await assert.rejects(adapter.getFile('127.0.0.1','990','FIXTURE:TOKEN','missing.gcode'),/listing fixture failed/); + assert.equal(calls.filter(c=>c[0]==='close').length,3); +}); diff --git a/tests/klipper-auth.test.mjs b/tests/klipper-auth.test.mjs new file mode 100644 index 0000000..6f94de1 --- /dev/null +++ b/tests/klipper-auth.test.mjs @@ -0,0 +1,36 @@ +import assert from 'node:assert/strict'; +import {test} from 'node:test'; +import {createServer} from 'node:http'; +import {once} from 'node:events'; +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import axios from 'axios'; +import {KlipperImplementation} from '../dist/printers/klipper.js'; + +// All HTTP requests use a loopback Moonraker fixture, never a printer. +for (const key of ['fixture-api-key','']) test(`Moonraker authentication covers every request (${key ? 'authenticated' : 'anonymous'})`,async t=>{ + const requests=[]; + const server=createServer((req,res)=>{ + requests.push({url:req.url,method:req.method,headers:req.headers}); + req.resume(); + if ((req.headers['x-api-key']??'')!==key) {res.writeHead(401);res.end('unauthorized');return;} + if(req.url.startsWith('/server/files/gcodes/')) {res.end('; filament_type = PLA\nM104 S220\n');return;} + res.setHeader('content-type','application/json'); + res.end(JSON.stringify({result:{status:{webhooks:{state:'ready'},print_stats:{state:'standby'}},item:{path:'job.gcode'}}})); + }); + server.listen(0,'127.0.0.1');await once(server,'listening'); + const dir=await fs.mkdtemp(path.join(os.tmpdir(),'moonraker-auth-')); + t.after(async()=>{server.closeAllConnections();await new Promise(resolve=>server.close(resolve));await fs.rm(dir,{recursive:true,force:true});}); + const source=path.join(dir,'job.gcode');await fs.writeFile(source,'; filament_type = PLA\nM104 S220\n'); + const adapter=new KlipperImplementation(axios.create());const args=['127.0.0.1',String(server.address().port),key]; + await adapter.getStatus(...args);await adapter.getFiles(...args);await adapter.getFile(...args,'nested/job.gcode'); + assert.equal((await adapter.readPrinterState(...args)).ready,true); + const downloaded=path.join(dir,'download.gcode');await adapter.downloadRemoteFile(...args,'nested/job.gcode',downloaded); + assert.match(await fs.readFile(downloaded,'utf8'),/M104 S220/); + await adapter.rawUploadFile(...args,source,'job.gcode',false); + await adapter.rawStartJob(...args,'job.gcode');await adapter.rawCancelJob(...args);await adapter.rawSetTemperature(...args,'bed',0); + assert.equal(requests.length,9); + assert.ok(requests.every(req=>key ? req.headers['x-api-key']===key : req.headers['x-api-key']===undefined)); + assert.match(requests.find(req=>req.url==='/server/files/upload').headers['content-type'],/multipart\/form-data; boundary=/); +}); diff --git a/tests/parser-stdio.test.mjs b/tests/parser-stdio.test.mjs new file mode 100644 index 0000000..fef5b08 --- /dev/null +++ b/tests/parser-stdio.test.mjs @@ -0,0 +1,27 @@ +import assert from 'node:assert/strict'; +import {test} from 'node:test'; +import {execFileSync} from 'node:child_process'; +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import JSZip from 'jszip'; +test('3MF parser diagnostics never contaminate stdio protocol output',async t=>{ + const dir=await fs.mkdtemp(path.join(os.tmpdir(),'bambu-parser-stdio-'));t.after(()=>fs.rm(dir,{recursive:true,force:true})); + const file=path.join(dir,'fixture.3mf');const zip=new JSZip(); + zip.file('3D/3dmodel.model',''); + zip.file('Metadata/project_settings.config',JSON.stringify({filament_type:['PLA']})); + await fs.writeFile(file,await zip.generateAsync({type:'nodebuffer'})); + const module=new URL('../dist/3mf_parser.js',import.meta.url).href; + const output=execFileSync(process.execPath,['--input-type=module','-e',`const {parse3MF}=await import(${JSON.stringify(module)});await parse3MF(process.argv[1]);`,file],{encoding:'utf8',stdio:['ignore','pipe','pipe']}); + assert.equal(output,''); +}); + +test('slicer execution diagnostics never contaminate stdio protocol output',async t=>{ + const dir=await fs.mkdtemp(path.join(os.tmpdir(),'bambu-slicer-stdio-'));t.after(()=>fs.rm(dir,{recursive:true,force:true})); + const executable=path.join(dir,'slicer'); + await fs.writeFile(executable,`#!${process.execPath}\nconst fs=require('node:fs');const args=process.argv.slice(2);fs.writeFileSync(args[args.indexOf('--output')+1],'G1 X0\\n');console.log('slicer diagnostic');\n`,{mode:0o755}); + const module=new URL('../dist/stl/stl-manipulator.js',import.meta.url).href; + const input=new URL('../test/sample_cube.stl',import.meta.url).pathname; + const output=execFileSync(process.execPath,['--input-type=module','-e',`const {STLManipulator}=await import(${JSON.stringify(module)});await new STLManipulator(process.argv[1]).sliceSTL(process.argv[2],'prusaslicer',process.argv[3]);`,dir,input,executable],{encoding:'utf8',stdio:['ignore','pipe','pipe']}); + assert.equal(output,''); +}); diff --git a/tests/safety/printer-state.test.mjs b/tests/safety/printer-state.test.mjs index 0192194..4504ba4 100644 --- a/tests/safety/printer-state.test.mjs +++ b/tests/safety/printer-state.test.mjs @@ -207,3 +207,15 @@ test('fresh reads publish pushall and get_version with string sequence ids witho assert.equal(published.find(payload => payload.info).info.command,'get_version'); for (const payload of published) assert.match(Object.values(payload)[0].sequence_id, /^\d+$/); }); + +test('fresh A1 returned serials distinguish full-size 039 from mini 030',async()=>{ + for(const [prefix,expected] of [['039','a1'],['030','a1mini']]) { + const serial=prefix+'FIXTURE'; + const printer=rawPrinter((payload,emit)=>{ + if(payload.pushing) emit({print:{command:'push_status',gcode_state:'IDLE',print_error:0,hms:[],nozzle_diameter:'0.4'}},`device/${serial}/report`); + if(payload.info) emit({info:{command:'get_version',module:[{name:'ota',sn:serial}]}},`device/${serial}/report`); + }); + const result=await safety.readFreshPrinterStatus(printer,serial,50); + assert.equal(result.model,expected);assert.equal(result.observedSerial,serial); + } +}); From 237075cb5b57bbc80f80744383490133492fc1a2 Mon Sep 17 00:00:00 2001 From: DMontgomery40 Date: Tue, 6 Oct 2026 09:20:01 -0600 Subject: [PATCH 5/5] Clarify that 1.2.11 includes the unpublished 1.2.10 batch --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f71d99..b657c6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## 1.2.11 — 2026-10-06 +This release also includes the safety, slicing, Blender, and documentation +changes prepared as 1.2.10 below. Version 1.2.10 was not published to npm. + ### Fixed - Send Moonraker's configured `API_KEY` as `X-Api-Key` on every request, @@ -39,7 +42,7 @@ npm package. The listing documents printer backend configuration and marks API keys and Bambu LAN access codes as secrets. -## 1.2.10 +## 1.2.10 — repository changes, not published to npm ### Safety