Skip to content

Commit 1838855

Browse files
rowbotikclaude
andcommitted
feat: add delete_printer_file MCP tool
- New deleteFile() in BambuImplementation: FTPS DELETE via basic-ftp using the same TLS-session-ticket dance as ftpUpload. - Confirm-gated (confirm:true required) so a default invocation can't destroy data; returns status:"skipped" otherwise without contacting the printer. - Path safety: rejects ".." segments, restricts deletes to cache/, timelapse/, and logs/ to prevent walking the filesystem. - Bare filenames default to cache/<name>; relative paths to other allowed dirs are honored as-is. - README features bullet + delete_printer_file tool section updated. - 5 new unit tests cover the gating, path safety, and happy path. npm test: 28/28. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 12ae6b9 commit 1838855

8 files changed

Lines changed: 383 additions & 1 deletion

File tree

PROGRESS.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,49 @@
11
# Progress
22

3+
## Latest commit (2026-04-27)
4+
5+
`12ae6b9 fix(h2): require explicit AMS mapping; reject SuperTack on CLI slicing`
6+
landed on `codex/collar-charm-h2-cleanup` (not pushed). Stacked on top:
7+
new `delete_printer_file` MCP tool with confirm-gate + path-allowlist
8+
guard rails. `npm test` now passes **28/28** (added 5 unit tests for the
9+
delete behavior). Not committed yet — see "Working state" below.
10+
11+
### Working state
12+
13+
Tracked local changes since `12ae6b9`:
14+
15+
- `README.md` — features bullet, new `delete_printer_file` section
16+
- `PROGRESS.md` — this section
17+
- `src/index.ts` — tool registration + dispatch case
18+
- `src/printers/bambu.ts``deleteFile()` (public) and `ftpDelete()` (private)
19+
- `tests/behavior.test.mjs` — 5 new tests:
20+
- `confirm:true` required, otherwise `status: "skipped"` with no FTP
21+
- path traversal rejected
22+
- paths outside `cache/`/`timelapse/`/`logs/` rejected
23+
- happy path: bare names normalize to `cache/`, ftpDelete called with absolute path
24+
- `timelapse/` and `logs/` paths accepted as-is
25+
- rebuilt `dist/` (`dist/index.js`, `dist/printers/bambu.js`)
26+
27+
Suggested commit message:
28+
29+
```
30+
feat: add delete_printer_file MCP tool
31+
32+
- New deleteFile() in BambuImplementation: FTPS DELETE via basic-ftp
33+
using the same TLS-session-ticket dance as ftpUpload.
34+
- Confirm-gated (confirm:true required) so a default invocation can't
35+
destroy data; returns status:"skipped" otherwise without contacting
36+
the printer.
37+
- Path safety: rejects ".." segments, restricts deletes to cache/,
38+
timelapse/, and logs/ to prevent walking the filesystem.
39+
- Bare filenames default to cache/<name>; relative paths to other
40+
allowed dirs are honored as-is.
41+
- README features bullet + delete_printer_file tool section updated.
42+
- 5 new unit tests cover the gating, path safety, and happy path.
43+
44+
Total npm test: 28/28.
45+
```
46+
347
## Current handoff (2026-04-27)
448

549
Branch: `codex/collar-charm-h2-cleanup`.

README.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ Local handoff note: see [REMOTE-DEPLOYMENT.md](./REMOTE-DEPLOYMENT.md) for the c
6868

6969
- Get detailed printer status: temperatures (nozzle, bed, chamber), print progress, current layer, time remaining, and live AMS slot data
7070
- Query live AMS inventory with resolved Bambu/Orca filament profile paths via `get_printer_filaments`
71-
- List, upload, and manage files on the printer's SD card via FTPS
71+
- List, upload, and delete files on the printer's SD card via FTPS
7272
- Upload and print pre-sliced `.gcode.3mf` files with full plate selection and calibration flag control (recommended path — see [docs/SLICING.md](./docs/SLICING.md))
7373
- Optional auto-slice path via BambuStudio CLI. Set `BAMBU_CLI_FLATTEN=true` to enable a workaround that flattens BBL profile inheritance before invoking the CLI — works around upstream bugs in BambuStudio CLI mode ([#9636](https://github.com/bambulab/BambuStudio/issues/9636), [#9968](https://github.com/bambulab/BambuStudio/issues/9968)). Verified on H2S/H2D/X1C/P1S. Default off; Path A (GUI-slice) remains the recommended workflow for non-BBL profiles or first-time prints. See [docs/SLICING.md](./docs/SLICING.md).
7474
- Parse AMS mapping from the 3MF's embedded slicer metadata (`Metadata/plate_<n>.json` + gcode filament header) and send it correctly formatted per the OpenBambuAPI spec
@@ -678,6 +678,27 @@ List files stored on the printer's SD card. Scans the `cache/`, `timelapse/`, an
678678
}
679679
```
680680

681+
#### delete_printer_file
682+
683+
Delete a single file from the printer's SD card via FTPS. **Destructive.** Requires `confirm: true` — without it the call returns `status: "skipped"` and does not contact the printer. Path traversal segments (`..`) are rejected. Only files under `cache/`, `timelapse/`, and `logs/` can be deleted.
684+
685+
```json
686+
{
687+
"filename": "old_print.gcode.3mf",
688+
"confirm": true,
689+
"host": "192.168.1.100",
690+
"bambu_serial": "01P00A123456789",
691+
"bambu_token": "your_access_token"
692+
}
693+
```
694+
695+
A bare filename defaults to `cache/<filename>`. To target other directories pass a relative path:
696+
697+
```json
698+
{ "filename": "timelapse/2026-04-26_12-00.mp4", "confirm": true }
699+
{ "filename": "logs/printer.log", "confirm": true }
700+
```
701+
681702
#### upload_gcode
682703

683704
Write G-code content from a string directly to the printer's `cache/` directory. The content is written to a temporary file and uploaded via FTPS.

dist/index.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1039,6 +1039,27 @@ class BambuPrinterMCPServer {
10391039
}
10401040
}
10411041
},
1042+
{
1043+
name: "delete_printer_file",
1044+
description: "Delete a file from the Bambu Lab printer's SD card via FTPS. Destructive: requires confirm:true. Restricted to cache/, timelapse/, and logs/ directories. Path traversal segments (..) are rejected.",
1045+
inputSchema: {
1046+
type: "object",
1047+
properties: {
1048+
filename: {
1049+
type: "string",
1050+
description: "File to delete. Bare names default to cache/<name>; pass a relative path like timelapse/foo.mp4 to target other allowed directories."
1051+
},
1052+
confirm: {
1053+
type: "boolean",
1054+
description: "Must be true to actually delete. When false or omitted the call returns without sending an FTP request."
1055+
},
1056+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
1057+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
1058+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
1059+
},
1060+
required: ["filename"]
1061+
}
1062+
},
10421063
{
10431064
name: "upload_gcode",
10441065
description: "Upload a G-code file to the Bambu Lab printer",
@@ -1458,6 +1479,12 @@ class BambuPrinterMCPServer {
14581479
case "list_printer_files":
14591480
result = await this.bambu.getFiles(host, bambuSerial, bambuToken);
14601481
break;
1482+
case "delete_printer_file":
1483+
if (!args?.filename) {
1484+
throw new Error("Missing required parameter: filename");
1485+
}
1486+
result = await this.bambu.deleteFile(host, bambuSerial, bambuToken, String(args.filename), Boolean(args.confirm));
1487+
break;
14611488
case "upload_gcode": {
14621489
if (!args?.filename || !args?.gcode) {
14631490
throw new Error("Missing required parameters: filename and gcode");

dist/printers/bambu.d.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,29 @@ export declare class BambuImplementation {
7171
message: string;
7272
file: string;
7373
}>;
74+
/**
75+
* Delete a single file from the printer's SD card via FTPS.
76+
*
77+
* Destructive. Caller MUST set confirm=true; otherwise we return without
78+
* touching the printer. Path is normalized the same way uploadFile()
79+
* normalizes -- if the caller passes a bare filename, we look in cache/.
80+
* Path traversal (`..`) is rejected.
81+
*
82+
* Only the printer-managed directories (cache/, timelapse/, logs/) are
83+
* accepted as parents to avoid letting an agent wander further into the
84+
* filesystem than expected.
85+
*/
86+
deleteFile(host: string, _serial: string, token: string, filename: string, confirm: boolean): Promise<{
87+
status: string;
88+
deleted: boolean;
89+
remotePath: string;
90+
message?: string;
91+
}>;
92+
/**
93+
* Delete a single remote file via FTPS, using basic-ftp directly so we
94+
* get the same TLS-session-ticket handshake as ftpUpload().
95+
*/
96+
private ftpDelete;
7497
/**
7598
* Upload a file to the printer via FTP using basic-ftp directly.
7699
* Bypasses bambu-js's sendFile which has a double-path bug (ensureDir CDs

dist/printers/bambu.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -910,6 +910,72 @@ export class BambuImplementation {
910910
file: remoteFile,
911911
};
912912
}
913+
/**
914+
* Delete a single file from the printer's SD card via FTPS.
915+
*
916+
* Destructive. Caller MUST set confirm=true; otherwise we return without
917+
* touching the printer. Path is normalized the same way uploadFile()
918+
* normalizes -- if the caller passes a bare filename, we look in cache/.
919+
* Path traversal (`..`) is rejected.
920+
*
921+
* Only the printer-managed directories (cache/, timelapse/, logs/) are
922+
* accepted as parents to avoid letting an agent wander further into the
923+
* filesystem than expected.
924+
*/
925+
async deleteFile(host, _serial, token, filename, confirm) {
926+
if (!confirm) {
927+
return {
928+
status: "skipped",
929+
deleted: false,
930+
remotePath: filename,
931+
message: "delete_printer_file requires confirm:true. No FTP request was made.",
932+
};
933+
}
934+
const normalizedFileName = filename.replace(/^\/+/, "");
935+
if (normalizedFileName.length === 0) {
936+
throw new Error("delete_printer_file: filename is required.");
937+
}
938+
if (normalizedFileName.split("/").some((seg) => seg === "..")) {
939+
throw new Error(`delete_printer_file: path traversal segments are not allowed (got "${filename}").`);
940+
}
941+
const remotePath = normalizedFileName.includes("/")
942+
? normalizedFileName
943+
: `cache/${normalizedFileName}`;
944+
const topDir = remotePath.split("/")[0];
945+
const ALLOWED_DIRS = new Set(["cache", "timelapse", "logs"]);
946+
if (!ALLOWED_DIRS.has(topDir)) {
947+
throw new Error(`delete_printer_file: refusing to delete outside cache/, timelapse/, logs/. Got "${remotePath}".`);
948+
}
949+
await this.ftpDelete(host, token, `/${remotePath}`);
950+
return {
951+
status: "success",
952+
deleted: true,
953+
remotePath,
954+
};
955+
}
956+
/**
957+
* Delete a single remote file via FTPS, using basic-ftp directly so we
958+
* get the same TLS-session-ticket handshake as ftpUpload().
959+
*/
960+
async ftpDelete(host, token, remotePath) {
961+
const client = new FTPClient(15000);
962+
try {
963+
await client.access({
964+
host,
965+
port: 990,
966+
user: "bblp",
967+
password: token,
968+
secure: "implicit",
969+
secureOptions: ftpsSecureOptions(),
970+
});
971+
await this.waitForTlsSession(client);
972+
const absoluteRemote = remotePath.startsWith("/") ? remotePath : `/${remotePath}`;
973+
await client.remove(absoluteRemote);
974+
}
975+
finally {
976+
client.close();
977+
}
978+
}
913979
/**
914980
* Upload a file to the printer via FTP using basic-ftp directly.
915981
* Bypasses bambu-js's sendFile which has a double-path bug (ensureDir CDs

src/index.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1324,6 +1324,27 @@ class BambuPrinterMCPServer {
13241324
}
13251325
}
13261326
},
1327+
{
1328+
name: "delete_printer_file",
1329+
description: "Delete a file from the Bambu Lab printer's SD card via FTPS. Destructive: requires confirm:true. Restricted to cache/, timelapse/, and logs/ directories. Path traversal segments (..) are rejected.",
1330+
inputSchema: {
1331+
type: "object",
1332+
properties: {
1333+
filename: {
1334+
type: "string",
1335+
description: "File to delete. Bare names default to cache/<name>; pass a relative path like timelapse/foo.mp4 to target other allowed directories."
1336+
},
1337+
confirm: {
1338+
type: "boolean",
1339+
description: "Must be true to actually delete. When false or omitted the call returns without sending an FTP request."
1340+
},
1341+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
1342+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
1343+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
1344+
},
1345+
required: ["filename"]
1346+
}
1347+
},
13271348
{
13281349
name: "upload_gcode",
13291350
description: "Upload a G-code file to the Bambu Lab printer",
@@ -1782,6 +1803,19 @@ class BambuPrinterMCPServer {
17821803
result = await this.bambu.getFiles(host, bambuSerial, bambuToken);
17831804
break;
17841805

1806+
case "delete_printer_file":
1807+
if (!args?.filename) {
1808+
throw new Error("Missing required parameter: filename");
1809+
}
1810+
result = await this.bambu.deleteFile(
1811+
host,
1812+
bambuSerial,
1813+
bambuToken,
1814+
String(args.filename),
1815+
Boolean(args.confirm)
1816+
);
1817+
break;
1818+
17851819
case "upload_gcode": {
17861820
if (!args?.filename || !args?.gcode) {
17871821
throw new Error("Missing required parameters: filename and gcode");

src/printers/bambu.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1140,6 +1140,90 @@ export class BambuImplementation {
11401140
};
11411141
}
11421142

1143+
/**
1144+
* Delete a single file from the printer's SD card via FTPS.
1145+
*
1146+
* Destructive. Caller MUST set confirm=true; otherwise we return without
1147+
* touching the printer. Path is normalized the same way uploadFile()
1148+
* normalizes -- if the caller passes a bare filename, we look in cache/.
1149+
* Path traversal (`..`) is rejected.
1150+
*
1151+
* Only the printer-managed directories (cache/, timelapse/, logs/) are
1152+
* accepted as parents to avoid letting an agent wander further into the
1153+
* filesystem than expected.
1154+
*/
1155+
async deleteFile(
1156+
host: string,
1157+
_serial: string,
1158+
token: string,
1159+
filename: string,
1160+
confirm: boolean
1161+
): Promise<{ status: string; deleted: boolean; remotePath: string; message?: string }> {
1162+
if (!confirm) {
1163+
return {
1164+
status: "skipped",
1165+
deleted: false,
1166+
remotePath: filename,
1167+
message:
1168+
"delete_printer_file requires confirm:true. No FTP request was made.",
1169+
};
1170+
}
1171+
1172+
const normalizedFileName = filename.replace(/^\/+/, "");
1173+
if (normalizedFileName.length === 0) {
1174+
throw new Error("delete_printer_file: filename is required.");
1175+
}
1176+
if (
1177+
normalizedFileName.split("/").some((seg) => seg === "..")
1178+
) {
1179+
throw new Error(
1180+
`delete_printer_file: path traversal segments are not allowed (got "${filename}").`
1181+
);
1182+
}
1183+
1184+
const remotePath = normalizedFileName.includes("/")
1185+
? normalizedFileName
1186+
: `cache/${normalizedFileName}`;
1187+
const topDir = remotePath.split("/")[0];
1188+
const ALLOWED_DIRS = new Set(["cache", "timelapse", "logs"]);
1189+
if (!ALLOWED_DIRS.has(topDir)) {
1190+
throw new Error(
1191+
`delete_printer_file: refusing to delete outside cache/, timelapse/, logs/. Got "${remotePath}".`
1192+
);
1193+
}
1194+
1195+
await this.ftpDelete(host, token, `/${remotePath}`);
1196+
1197+
return {
1198+
status: "success",
1199+
deleted: true,
1200+
remotePath,
1201+
};
1202+
}
1203+
1204+
/**
1205+
* Delete a single remote file via FTPS, using basic-ftp directly so we
1206+
* get the same TLS-session-ticket handshake as ftpUpload().
1207+
*/
1208+
private async ftpDelete(host: string, token: string, remotePath: string): Promise<void> {
1209+
const client = new FTPClient(15_000);
1210+
try {
1211+
await client.access({
1212+
host,
1213+
port: 990,
1214+
user: "bblp",
1215+
password: token,
1216+
secure: "implicit",
1217+
secureOptions: ftpsSecureOptions(),
1218+
});
1219+
await this.waitForTlsSession(client);
1220+
const absoluteRemote = remotePath.startsWith("/") ? remotePath : `/${remotePath}`;
1221+
await client.remove(absoluteRemote);
1222+
} finally {
1223+
client.close();
1224+
}
1225+
}
1226+
11431227
/**
11441228
* Upload a file to the printer via FTP using basic-ftp directly.
11451229
* Bypasses bambu-js's sendFile which has a double-path bug (ensureDir CDs

0 commit comments

Comments
 (0)