mcp-3d-printer-server 1.2.24 → 1.2.26

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.
Files changed (96) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/CONTRIBUTORS.md +5 -1
  3. package/README.md +64 -29
  4. package/dist/ams-mapping.d.ts +38 -0
  5. package/dist/ams-mapping.js +125 -0
  6. package/dist/bambu-network-bridge.d.ts +200 -0
  7. package/dist/bambu-network-bridge.js +881 -0
  8. package/dist/bambu-network-certs.d.ts +13 -0
  9. package/dist/bambu-network-certs.js +152 -0
  10. package/dist/control-reply.d.ts +21 -0
  11. package/dist/control-reply.js +44 -0
  12. package/dist/counterpart/camera-sources.d.ts +16 -0
  13. package/dist/counterpart/camera-sources.js +138 -0
  14. package/dist/counterpart/camera-stream.d.ts +66 -0
  15. package/dist/counterpart/camera-stream.js +246 -0
  16. package/dist/counterpart/camera-tools.d.ts +247 -0
  17. package/dist/counterpart/camera-tools.js +35 -0
  18. package/dist/counterpart/contract.d.ts +21 -5
  19. package/dist/counterpart/contract.js +8 -2
  20. package/dist/counterpart/route.d.ts +31 -0
  21. package/dist/counterpart/route.js +70 -0
  22. package/dist/counterpart/service.d.ts +60 -4
  23. package/dist/counterpart/service.js +142 -16
  24. package/dist/design-guidance.d.ts +26 -0
  25. package/dist/design-guidance.js +177 -0
  26. package/dist/fulu-orca.d.ts +7 -0
  27. package/dist/fulu-orca.js +18 -2
  28. package/dist/index.js +528 -72
  29. package/dist/printers/bambu-filaments.d.ts +68 -0
  30. package/dist/printers/bambu-filaments.js +262 -0
  31. package/dist/printers/bambu.d.ts +99 -6
  32. package/dist/printers/bambu.js +478 -72
  33. package/dist/printers/creality.d.ts +2 -1
  34. package/dist/printers/creality.js +5 -1
  35. package/dist/printers/generic-base.d.ts +11 -6
  36. package/dist/printers/generic-base.js +21 -25
  37. package/dist/printers/klipper.d.ts +2 -1
  38. package/dist/printers/klipper.js +10 -7
  39. package/dist/printers/octoprint.d.ts +2 -1
  40. package/dist/printers/octoprint.js +15 -8
  41. package/dist/printers/printer-factory.d.ts +1 -1
  42. package/dist/printers/printer-factory.js +3 -3
  43. package/dist/printers/prusa.js +0 -1
  44. package/dist/safety/confirmation.d.ts +11 -16
  45. package/dist/safety/confirmation.js +8 -32
  46. package/dist/safety/hidden-hms.d.ts +32 -0
  47. package/dist/safety/hidden-hms.js +405 -0
  48. package/dist/safety/limits.d.ts +8 -2
  49. package/dist/safety/limits.js +14 -5
  50. package/dist/safety/print-file.d.ts +5 -0
  51. package/dist/safety/print-file.js +156 -19
  52. package/dist/safety/printer-state.d.ts +59 -8
  53. package/dist/safety/printer-state.js +174 -39
  54. package/dist/slicer/bambu-cli.d.ts +6 -2
  55. package/dist/slicer/bambu-cli.js +19 -12
  56. package/dist/slicer/layer-pauses.d.ts +47 -0
  57. package/dist/slicer/layer-pauses.js +130 -0
  58. package/dist/slicer/slicer-error.d.ts +11 -1
  59. package/dist/slicer/slicer-error.js +16 -2
  60. package/dist/stl/stl-manipulator.d.ts +27 -1
  61. package/dist/stl/stl-manipulator.js +216 -76
  62. package/dist/studio-print-options.d.ts +57 -0
  63. package/dist/studio-print-options.js +179 -0
  64. package/package.json +25 -4
  65. package/server.json +2 -2
  66. package/src/ams-mapping.ts +132 -0
  67. package/src/bambu-network-bridge.ts +1075 -0
  68. package/src/bambu-network-certs.ts +154 -0
  69. package/src/control-reply.ts +41 -0
  70. package/src/counterpart/camera-sources.ts +102 -0
  71. package/src/counterpart/camera-stream.ts +195 -0
  72. package/src/counterpart/camera-tools.ts +35 -0
  73. package/src/counterpart/contract.ts +12 -4
  74. package/src/counterpart/route.ts +82 -0
  75. package/src/counterpart/service.ts +150 -16
  76. package/src/design-guidance.ts +188 -0
  77. package/src/fulu-orca.ts +18 -2
  78. package/src/index.ts +546 -87
  79. package/src/printers/bambu-filaments.ts +346 -0
  80. package/src/printers/bambu.ts +506 -81
  81. package/src/printers/creality.ts +8 -3
  82. package/src/printers/generic-base.ts +33 -40
  83. package/src/printers/klipper.ts +12 -8
  84. package/src/printers/octoprint.ts +16 -9
  85. package/src/printers/printer-factory.ts +3 -3
  86. package/src/printers/prusa.ts +0 -1
  87. package/src/safety/confirmation.ts +15 -38
  88. package/src/safety/hidden-hms.ts +406 -0
  89. package/src/safety/limits.ts +15 -5
  90. package/src/safety/print-file.ts +139 -17
  91. package/src/safety/printer-state.ts +179 -40
  92. package/src/slicer/bambu-cli.ts +22 -13
  93. package/src/slicer/layer-pauses.ts +143 -0
  94. package/src/slicer/slicer-error.ts +21 -2
  95. package/src/stl/stl-manipulator.ts +254 -84
  96. package/src/studio-print-options.ts +201 -0
@@ -0,0 +1,70 @@
1
+ /** counterpart_print_route: how this server reaches the printer for prints and controls (PRD §8.4). */
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ export const PRINT_ROUTES = ["lan", "cloud", "developer"];
5
+ /** Modes that are their own local-network route and stay as configured. */
6
+ const OWN_LOCAL_MODES = new Set(["bambu_native", "bambu_connect"]);
7
+ export function routeOf(connection) {
8
+ if (connection.mode === "bambu_network")
9
+ return connection.type === "cloud" ? "cloud" : "lan";
10
+ if (connection.mode === "lan_mqtt_ftps")
11
+ return "developer";
12
+ return "lan";
13
+ }
14
+ /** Every connection, and whether this server can use it (bridgeConfigured: BAMBU_NETWORK_BRIDGE_COMMAND is set). */
15
+ export function routeOptions(current, bridgeConfigured) {
16
+ const signedControls = "Uses the configured authenticated connection for Print, Pause, Resume and Stop.";
17
+ const needsBridge = "Needs FULU's BambuNetwork bridge (BAMBU_NETWORK_BRIDGE_COMMAND).";
18
+ const ownLocal = OWN_LOCAL_MODES.has(current.mode);
19
+ return [
20
+ { id: "lan", label: "Local network", available: bridgeConfigured || ownLocal, ...(bridgeConfigured || ownLocal ? {} : { reason: needsBridge }), controls: signedControls },
21
+ { id: "cloud", label: "Bambu Cloud", available: bridgeConfigured, ...(bridgeConfigured ? {} : { reason: needsBridge }), controls: signedControls },
22
+ { id: "developer", label: "Local network, Developer Mode", available: true,
23
+ controls: "Pause, Resume and Stop go straight to the printer. Turn on LAN Only and Developer Mode on the printer first; Bambu Cloud and Bambu Handy then stop working for it." },
24
+ ];
25
+ }
26
+ /**
27
+ * The configured defaults for a route. Cloud and the local network use FULU's BambuNetwork; an X2D native or
28
+ * Bambu Connect setup keeps its own mode for the local network. Developer Mode is the direct LAN route.
29
+ */
30
+ export function connectionForRoute(route, current, bridgeConfigured = true) {
31
+ const option = routeOptions(current, bridgeConfigured).find(item => item.id === route);
32
+ if (!option?.available)
33
+ throw new Error(`The ${option?.label ?? route} connection isn't available here. ${option?.reason ?? ""}`.trim());
34
+ if (route === "cloud")
35
+ return { mode: "bambu_network", type: "cloud" };
36
+ if (route === "developer")
37
+ return { mode: "lan_mqtt_ftps", type: "lan" };
38
+ return OWN_LOCAL_MODES.has(current.mode) ? current : { mode: "bambu_network", type: "lan" };
39
+ }
40
+ /** Set KEY=value lines in a dotenv file, keeping every other line and the file's permissions. */
41
+ export function writeEnvValues(file, values) {
42
+ const original = fs.readFileSync(file, "utf8");
43
+ const lines = original.split("\n");
44
+ for (const [key, value] of Object.entries(values)) {
45
+ const pattern = new RegExp(`^\\s*(?:export\\s+)?${key}\\s*=`);
46
+ let found = false;
47
+ for (let index = lines.length - 1; index >= 0; index--) {
48
+ if (!pattern.test(lines[index]))
49
+ continue;
50
+ if (!found) {
51
+ lines[index] = `${key}=${value}`;
52
+ found = true;
53
+ }
54
+ // dotenv lets a later line win: keep one line per key, the one written here.
55
+ else
56
+ lines.splice(index, 1);
57
+ }
58
+ if (!found) {
59
+ if (lines.length && lines[lines.length - 1] === "")
60
+ lines.splice(lines.length - 1, 0, `${key}=${value}`);
61
+ else
62
+ lines.push(`${key}=${value}`);
63
+ }
64
+ }
65
+ const mode = fs.statSync(file).mode & 0o777;
66
+ const temporary = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.tmp`);
67
+ fs.writeFileSync(temporary, lines.join("\n"), { mode });
68
+ fs.chmodSync(temporary, mode);
69
+ fs.renameSync(temporary, file);
70
+ }
@@ -1,6 +1,19 @@
1
1
  import { type AxiosInstance } from "axios";
2
2
  import type { PrinterImplementation } from "../types.js";
3
+ import { type FrameOptions } from "./camera-stream.js";
3
4
  import { type CameraSnapshotResult, type CounterpartPrinterInfo, type CounterpartProgress, type FilamentSlot } from "./contract.js";
5
+ import { type FilamentProfileSource, type PrinterFilamentInventory } from "../printers/bambu-filaments.js";
6
+ /**
7
+ * The Bambu print-control dispatch for the configured connection (FULU's
8
+ * BambuNetwork); undefined keeps LAN MQTT. Resume's dispatch receives the
9
+ * checks to repeat before it sends.
10
+ */
11
+ export type BambuControlRoute = (command: "stop" | "pause" | "resume") => ((assertActive?: () => void, beforeDispatch?: () => Promise<void>) => Promise<any>) | undefined;
12
+ /** Bambu inventory hints: the model and nozzle select model-specific filament profiles. */
13
+ export interface BambuFilamentOptions {
14
+ bambuModel?: string;
15
+ nozzleDiameter?: string;
16
+ }
4
17
  export declare const DEFAULT_SNAPSHOT_TIMEOUT_MS = 8000;
5
18
  export interface PrinterTarget {
6
19
  type: string;
@@ -19,27 +32,43 @@ export declare class CounterpartService {
19
32
  private readonly server;
20
33
  /** The slicer filament profile for a slot's material (undefined: the configured default). */
21
34
  private readonly filamentProfile?;
35
+ /** The slicer whose BBL/filament profiles resolve Bambu trays by tray_info_idx. */
36
+ private readonly bambuProfileSource;
37
+ private readonly streams;
38
+ private readonly generations;
39
+ private closed;
22
40
  constructor(implementation: (type: string) => PrinterImplementation, http: AxiosInstance, server: {
23
41
  name: string;
24
42
  version: string;
25
43
  },
26
44
  /** The slicer filament profile for a slot's material (undefined: the configured default). */
27
- filamentProfile?: ((material: string | undefined) => Promise<string | undefined>) | undefined);
45
+ filamentProfile?: ((material: string | undefined) => Promise<string | undefined>) | undefined,
46
+ /** The slicer whose BBL/filament profiles resolve Bambu trays by tray_info_idx. */
47
+ bambuProfileSource?: () => FilamentProfileSource);
28
48
  private generic;
29
49
  private bambu;
30
50
  printProgress(target: PrinterTarget): Promise<CounterpartProgress>;
31
51
  printerInfo(target: PrinterTarget): Promise<CounterpartPrinterInfo>;
32
52
  /** http-snapshot when the firmware names a snapshot URL, or a conventional fallback answers with a JPEG. */
33
53
  private cameraCapability;
34
- filaments(target: PrinterTarget): Promise<{
54
+ /**
55
+ * bambu-printer-mcp's get_printer_filaments for Bambu: the live AMS inventory
56
+ * from MQTT. The first push from an idle printer can be sparse (model and
57
+ * modules); AMS data follows, so an empty live inventory is read once more.
58
+ */
59
+ bambuFilaments(target: PrinterTarget, options?: BambuFilamentOptions): Promise<PrinterFilamentInventory & {
35
60
  slots: FilamentSlot[];
36
61
  }>;
37
- pause(target: PrinterTarget): Promise<{
62
+ filaments(target: PrinterTarget, options?: BambuFilamentOptions): Promise<{
63
+ slots: FilamentSlot[];
64
+ }>;
65
+ /** Bambu: over the route that printed (route), else LAN MQTT. */
66
+ pause(target: PrinterTarget, route?: BambuControlRoute): Promise<{
38
67
  status: string;
39
68
  action: "pause";
40
69
  message: string;
41
70
  }>;
42
- resume(target: PrinterTarget): Promise<{
71
+ resume(target: PrinterTarget, route?: BambuControlRoute): Promise<{
43
72
  status: string;
44
73
  action: "resume";
45
74
  message: string;
@@ -47,6 +76,33 @@ export declare class CounterpartService {
47
76
  bytesVerified: boolean;
48
77
  }>;
49
78
  cameraSnapshot(target: PrinterTarget, options?: SnapshotOptions): Promise<CameraSnapshotResult>;
79
+ private cameraKey;
80
+ private cameraCandidates;
81
+ cameraStreamStart(target: PrinterTarget, options?: {
82
+ fps?: number;
83
+ idleTimeoutMs?: number;
84
+ }): Promise<{
85
+ status: string;
86
+ reason: string;
87
+ transport?: undefined;
88
+ fps?: undefined;
89
+ startedAt?: undefined;
90
+ } | {
91
+ status: string;
92
+ transport: "http";
93
+ fps: number;
94
+ startedAt: string;
95
+ reason?: undefined;
96
+ }>;
97
+ cameraStreamFrame(target: PrinterTarget, options?: FrameOptions): Promise<import("./camera-stream.js").StreamFrameResult | {
98
+ status: string;
99
+ seq: number;
100
+ }>;
101
+ cameraStreamStop(target: PrinterTarget): {
102
+ status: string;
103
+ };
104
+ close(): void;
105
+ private cameraHeaders;
50
106
  /**
51
107
  * Snapshot URLs are written from the printer host's point of view: relative
52
108
  * paths and loopback hosts mean the printer itself.
@@ -7,9 +7,13 @@ import fs from "node:fs/promises";
7
7
  import path from "node:path";
8
8
  import { isAxiosError } from "axios";
9
9
  import { GenericPrinterImplementation } from "../printers/generic-base.js";
10
+ import { CameraStreams, cameraNativeAvailable } from "./camera-stream.js";
11
+ import { openHttpCameraSource } from "./camera-sources.js";
12
+ import { cameraNumber } from "./camera-tools.js";
10
13
  import { BambuImplementation } from "../printers/bambu.js";
11
14
  import { COUNTERPART_CONTRACT, PRINTER_KINDS, compact, progressResult, } from "./contract.js";
12
15
  import { mapBambu } from "./progress.js";
16
+ import { counterpartSlots, externalSpools, normalizePrinterFilamentInventory, } from "../printers/bambu-filaments.js";
13
17
  export const DEFAULT_SNAPSHOT_TIMEOUT_MS = 8000;
14
18
  const MAX_SNAPSHOT_BYTES = 16 * 1024 * 1024;
15
19
  const LOOPBACK = new Set(["localhost", "127.0.0.1", "::1", "[::1]", "0.0.0.0"]);
@@ -29,11 +33,17 @@ function overrideSnapshotUrl() {
29
33
  export class CounterpartService {
30
34
  constructor(implementation, http, server,
31
35
  /** The slicer filament profile for a slot's material (undefined: the configured default). */
32
- filamentProfile) {
36
+ filamentProfile,
37
+ /** The slicer whose BBL/filament profiles resolve Bambu trays by tray_info_idx. */
38
+ bambuProfileSource = () => ({})) {
33
39
  this.implementation = implementation;
34
40
  this.http = http;
35
41
  this.server = server;
36
42
  this.filamentProfile = filamentProfile;
43
+ this.bambuProfileSource = bambuProfileSource;
44
+ this.streams = new CameraStreams();
45
+ this.generations = new Map();
46
+ this.closed = false;
37
47
  }
38
48
  generic(target) {
39
49
  const implementation = this.implementation(target.type);
@@ -69,10 +79,10 @@ export class CounterpartService {
69
79
  const status = await this.bambu(target).getStatus(target.host, target.port, target.apiKey).catch(() => undefined);
70
80
  const model = typeof status?.model === "string" && status.model !== "Unknown" ? status.model : process.env.BAMBU_MODEL?.trim();
71
81
  identity = { model: model || undefined, serial: typeof status?.serial === "string" && status.serial ? status.serial : undefined };
72
- // This server has no Bambu camera, pause or resume path; bambu-printer-mcp does.
82
+ // This server has no Bambu camera path; bambu-printer-mcp does.
73
83
  capabilities = {
74
- progress: true, layers: true, camera: overrideSnapshotUrl() ? "http-snapshot" : "none", filaments: "none",
75
- pause: false, resume: false, cancel: true, print3mf: true, gcode: true,
84
+ progress: true, layers: true, camera: overrideSnapshotUrl() ? "http-snapshot" : "none", filaments: "ams",
85
+ pause: true, resume: true, cancel: true, print3mf: true, gcode: true,
76
86
  };
77
87
  }
78
88
  else {
@@ -88,6 +98,9 @@ export class CounterpartService {
88
98
  pause: pauseResume, resume: pauseResume, cancel: true, print3mf: false, gcode: true,
89
99
  };
90
100
  }
101
+ const streamCandidates = await this.cameraCandidates(target).catch(() => []);
102
+ capabilities.cameraStream = await cameraNativeAvailable() && streamCandidates.length > 0 &&
103
+ (capabilities.camera !== "none" || streamCandidates.some(candidate => candidate.configured));
91
104
  return compact({
92
105
  contract: COUNTERPART_CONTRACT,
93
106
  server: this.server,
@@ -113,11 +126,32 @@ export class CounterpartService {
113
126
  }
114
127
  return "none";
115
128
  }
116
- async filaments(target) {
117
- const generic = this.generic(target);
118
- if (!generic) {
119
- throw new Error("Unsupported: Bambu AMS and external-spool inventory is read by bambu-printer-mcp's get_printer_filaments.");
129
+ /**
130
+ * bambu-printer-mcp's get_printer_filaments for Bambu: the live AMS inventory
131
+ * from MQTT. The first push from an idle printer can be sparse (model and
132
+ * modules); AMS data follows, so an empty live inventory is read once more.
133
+ */
134
+ async bambuFilaments(target, options = {}) {
135
+ const bambu = this.bambu(target);
136
+ const source = this.bambuProfileSource();
137
+ const read = async () => {
138
+ const status = await bambu.getStatus(target.host, target.port, target.apiKey);
139
+ if (status?.connected === false)
140
+ throw new Error(`Could not read the printer's filament: ${status?.error ?? "not connected"}`);
141
+ return { status, inventory: normalizePrinterFilamentInventory(status, source, options.bambuModel, options.nozzleDiameter) };
142
+ };
143
+ let { status, inventory } = await read();
144
+ if (inventory.trays.length === 0) {
145
+ await new Promise((resolve) => setTimeout(resolve, 1500));
146
+ ({ status, inventory } = await read());
120
147
  }
148
+ const external = externalSpools(status, source, options.bambuModel, options.nozzleDiameter);
149
+ return { slots: counterpartSlots(inventory, external), ...inventory };
150
+ }
151
+ async filaments(target, options = {}) {
152
+ const generic = this.generic(target);
153
+ if (!generic)
154
+ return this.bambuFilaments(target, options);
121
155
  const slots = await generic.counterpartFilaments(target.host, target.port, target.apiKey);
122
156
  const profiled = await Promise.all(slots.map(async (slot) => {
123
157
  const profile = await this.filamentProfile?.(slot.material).catch(() => undefined);
@@ -125,16 +159,19 @@ export class CounterpartService {
125
159
  }));
126
160
  return compact({ slots: profiled });
127
161
  }
128
- async pause(target) {
162
+ /** Bambu: over the route that printed (route), else LAN MQTT. */
163
+ async pause(target, route) {
129
164
  const generic = this.generic(target);
130
165
  if (!generic)
131
- throw new Error("Unsupported: pause_print for Bambu printers is provided by bambu-printer-mcp.");
166
+ return this.bambu(target).pauseJob(target.host, target.port, target.apiKey, route?.("pause"));
132
167
  return generic.pauseJob(target.host, target.port, target.apiKey);
133
168
  }
134
- async resume(target) {
169
+ async resume(target, route) {
135
170
  const generic = this.generic(target);
136
- if (!generic)
137
- throw new Error("Unsupported: resume_print for Bambu printers is provided by bambu-printer-mcp.");
171
+ if (!generic) {
172
+ const dispatch = route?.("resume");
173
+ return this.bambu(target).resumeJob(target.host, target.port, target.apiKey, dispatch);
174
+ }
138
175
  return generic.resumeJob(target.host, target.port, target.apiKey);
139
176
  }
140
177
  async cameraSnapshot(target, options = {}) {
@@ -144,6 +181,16 @@ export class CounterpartService {
144
181
  if (options.savePath !== undefined && (!path.isAbsolute(options.savePath) || options.savePath.includes("\0"))) {
145
182
  throw new Error("Invalid save_path: expected an absolute file path.");
146
183
  }
184
+ const active = this.streams.get(this.cameraKey(target));
185
+ if (active && !active.stopped) {
186
+ const jpeg = await active.latest(Math.min(timeoutMs, 10000));
187
+ if (!jpeg)
188
+ throw new Error("The active camera stream has not produced a frame.");
189
+ if (options.savePath)
190
+ await fs.writeFile(options.savePath, jpeg);
191
+ return compact({ status: "success", format: "image/jpeg", sizeBytes: jpeg.length, base64: jpeg.toString("base64"),
192
+ savedTo: options.savePath, capturedAt: new Date().toISOString(), source: active.transport });
193
+ }
147
194
  const deadline = Date.now() + timeoutMs;
148
195
  const override = overrideSnapshotUrl();
149
196
  let candidates;
@@ -182,6 +229,87 @@ export class CounterpartService {
182
229
  }
183
230
  throw new Error(`No camera snapshot within ${timeoutMs} ms. Tried ${failures.length ? failures.join("; ") : "no URL before the deadline"}.`);
184
231
  }
232
+ cameraKey(target) {
233
+ return JSON.stringify([kindOf(target.type), target.host, target.port, target.type.toLowerCase() === "bambu" ? target.apiKey.split(":")[0] : ""]);
234
+ }
235
+ async cameraCandidates(target) {
236
+ const override = overrideSnapshotUrl();
237
+ const candidates = override ? [{ url: override, source: "env-override", configured: true, transport: "http-snapshot" }]
238
+ : await this.generic(target)?.cameraCandidates(target.host, target.port, target.apiKey) ?? [];
239
+ // Choose one configured camera; unsupported formats cannot select another camera.
240
+ const candidate = candidates[0];
241
+ if (!candidate)
242
+ return [];
243
+ try {
244
+ let supportedURL = true;
245
+ try {
246
+ this.resolve(candidate.url, target);
247
+ }
248
+ catch {
249
+ supportedURL = false;
250
+ }
251
+ if (!supportedURL || candidate.unsupportedStream || /\.m3u8(?:[?#]|$)/i.test(candidate.url)) {
252
+ if (!candidate.snapshotUrl)
253
+ return [];
254
+ this.resolve(candidate.snapshotUrl, target);
255
+ return [{ ...candidate, url: candidate.snapshotUrl, transport: "http-snapshot" }];
256
+ }
257
+ this.resolve(candidate.url, target);
258
+ if (candidate.snapshotUrl)
259
+ this.resolve(candidate.snapshotUrl, target);
260
+ return [candidate];
261
+ }
262
+ catch {
263
+ return [];
264
+ }
265
+ }
266
+ async cameraStreamStart(target, options = {}) {
267
+ const fps = cameraNumber(options.fps, 1, 0.2, 2, "fps");
268
+ const idleTimeoutMs = cameraNumber(options.idleTimeoutMs, 30000, 5000, 120000, "idle_timeout_ms", true);
269
+ const key = this.cameraKey(target), generation = this.generations.get(key) ?? 0;
270
+ if (!await cameraNativeAvailable())
271
+ return { status: "not-supported", reason: "runtime" };
272
+ if (this.closed || generation !== (this.generations.get(key) ?? 0))
273
+ throw new Error("Camera stream start was cancelled.");
274
+ const existing = this.streams.get(key);
275
+ if (existing && !existing.stopped) {
276
+ existing.touch(idleTimeoutMs);
277
+ return { status: "running", transport: existing.transport, fps: existing.fps, startedAt: existing.startedAt };
278
+ }
279
+ const candidates = await this.cameraCandidates(target);
280
+ if (this.closed || generation !== (this.generations.get(key) ?? 0))
281
+ throw new Error("Camera stream start was cancelled.");
282
+ if (!candidates.length)
283
+ throw new Error("No camera is configured for this printer. Existing snapshot configuration is used for streaming.");
284
+ const candidate = candidates[0];
285
+ const urls = (raw) => this.resolve(raw, target).map(url => ({ url, headers: this.cameraHeaders(url, target) }));
286
+ const plan = { urls: urls(candidate.url), snapshots: candidate.snapshotUrl ? urls(candidate.snapshotUrl) : undefined, mode: candidate.transport };
287
+ const { stream, status } = this.streams.start(key, { transport: "http", fps, idleTimeoutMs, skipFirstFrame: false,
288
+ open: handlers => openHttpCameraSource(this.http, plan, fps, handlers) });
289
+ return { status, transport: stream.transport, fps: stream.fps, startedAt: stream.startedAt };
290
+ }
291
+ async cameraStreamFrame(target, options = {}) {
292
+ const sinceSeq = cameraNumber(options.sinceSeq, 0, 0, Number.MAX_SAFE_INTEGER, "since_seq", true);
293
+ const waitMs = cameraNumber(options.waitMs, 0, 0, 10000, "wait_ms", true);
294
+ const minChange = cameraNumber(options.minChange, 0.02, 0, 1, "min_change");
295
+ const maxWidth = options.maxWidth === undefined ? undefined : cameraNumber(options.maxWidth, 300, 1, 4096, "max_width", true);
296
+ const maxBytes = options.maxBytes === undefined ? undefined : cameraNumber(options.maxBytes, 42000, 4096, 5000000, "max_bytes", true);
297
+ if (options.changedOnly !== undefined && typeof options.changedOnly !== "boolean")
298
+ throw new Error("changed_only must be a boolean");
299
+ return this.streams.get(this.cameraKey(target))?.frame({ ...options, sinceSeq, waitMs, minChange, maxWidth, maxBytes }) ?? { status: "stopped", seq: 0 };
300
+ }
301
+ cameraStreamStop(target) {
302
+ const key = this.cameraKey(target);
303
+ this.generations.set(key, (this.generations.get(key) ?? 0) + 1);
304
+ this.streams.stop(key);
305
+ return { status: "stopped" };
306
+ }
307
+ close() { this.closed = true; this.streams.stopAll(); }
308
+ cameraHeaders(url, target) {
309
+ const host = target.host.includes(":") && !target.host.startsWith("[") ? `[${target.host}]` : target.host;
310
+ const sameOrigin = new URL(url).origin === new URL(`http://${host}:${target.port}`).origin;
311
+ return sameOrigin && target.apiKey && target.type.toLowerCase() !== "bambu" ? { "X-Api-Key": target.apiKey } : {};
312
+ }
185
313
  /**
186
314
  * Snapshot URLs are written from the printer host's point of view: relative
187
315
  * paths and loopback hosts mean the printer itself.
@@ -212,10 +340,8 @@ export class CounterpartService {
212
340
  return [parsed.toString()];
213
341
  }
214
342
  async fetchJpeg(url, target, timeout) {
215
- const parsed = new URL(url);
216
343
  // The API key goes only to the printer's own API origin, never to another host.
217
- const sameOrigin = parsed.origin === new URL(`http://${target.host}:${target.port}`).origin;
218
- const headers = sameOrigin && target.apiKey && target.type.toLowerCase() !== "bambu" ? { "X-Api-Key": target.apiKey } : {};
344
+ const headers = this.cameraHeaders(url, target);
219
345
  const response = await this.http.get(url, {
220
346
  headers,
221
347
  responseType: "arraybuffer",
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Manufacturing guidance for agents that design printable models: David Montgomery's FDM design
3
+ * prompt pack (3 November 2026), verbatim. Served as MCP prompts and a resource; the other printer
4
+ * MCP (bambu-printer-mcp / mcp-3D-printer-server) serves the same text. Clients choose when to
5
+ * attach each piece.
6
+ */
7
+ export interface DesignPrompt {
8
+ name: string;
9
+ title: string;
10
+ description: string;
11
+ text: string;
12
+ }
13
+ export declare const FDM_CORE = "Design the finished object the user wants, with the manufacturing plan as part of the design. Preserve its function, fit, and visual identity. Use your design ability freely to make that outcome practical through geometry, orientation, materials, color placement, supports, and assembly.\n\nUse the actual printer and material context: nozzle arrangement and filament routing, build volume, nozzle and line widths, layer profile, cooling, available filaments, and relevant successful prints or calibration. Missing information is unknown, not a capability the printer automatically has. Use reasonable, labeled assumptions for reversible choices; ask only when an unknown materially changes the result.\n\nBefore committing to detailed geometry, choose a plausible way to manufacture and assemble it. For a consequential tradeoff, compare the strongest one-piece approach with a meaningfully different orientation or split. Simple parts do not need a design tournament. Judge total effort to obtain a good finished object: printing, consumed filament, likely rework, support cleanup, assembly, and extra hardware. Respect the user's priorities instead of applying fixed numerical weights.\n\nKeep assembled orientation and print orientation distinct. Preserve critical dimensions and mating surfaces. Account for measurement uncertainty and fit clearance separately; a scan or printed measuring tool does not establish its own accuracy. Use the smallest useful fit sample when uncertainty could otherwise waste a substantial print.\n\nEvaluate printability locally. Distinguish a progressively supported overhang, a bridge anchored at both ends, and a disconnected island. State overhang angles from vertical: 0 degrees is a vertical wall; 90 degrees is a horizontal underside. Convert conventions before setting slicer angles. Use relevant measured capability ahead of generic rules. A 45-degree threshold is not a universal support requirement, and a steep surface is not automatically unprintable. Judge bridge length along the actual unsupported extrusion path, with its anchors, cooling, material, and required underside quality. A feature that survives printing may still miss a cosmetic or dimensional requirement.\n\nResolve a difficult region with the least disruptive effective change: a better orientation, local geometry, suitable process settings, accessible support, a deliberate breakaway feature, or a split. Supports are an ordinary manufacturing option. Preserve attractive curves and useful complexity when modest support or cleanup achieves them. Check support removal and tool access, especially inside cavities. Avoid changing the whole object to solve a local problem.\n\nChoose layer orientation, material, walls, and joints for the actual loads and environment. Check the load path, flexing features, fastener access, and installation sequence. A reduction in supports or color changes must not silently weaken the part or defeat its fit.\n\nOn a shared-nozzle multicolor setup, reason about filament transitions across layers, including support-interface material. Total color count alone is a poor cost estimate. Consider color placement, shallow inlays, separately printed accents, and part orientation while preserving the requested appearance. Separate bodies on one layer-printed plate can still cause repeated swaps. Use the slicer's actual schedule and estimates when available; do not invent a fixed cost per color change.\n\nUse assembly when its benefit is worth the user's additional work. Design alignment, clearance, engagement, and assembly access deliberately. Glue is a valid planned joining method for suitable materials and loads; make joints self-locating and give them useful bonding area. Use mechanical retention when loads, repeated flexing, or serviceability call for it. State needed glue, hardware, tools, and assembly effort plainly. Avoid adding parts when a modest support or a simple one-piece print is the better result.\n\nVerify the chosen geometry in its actual print orientation and inspect the slice when tools permit. Check critical fits, missing or floating features, toolpaths, supports, color assignments, total time, and material consumption. Treat a slicer prediction as a prediction, not a physical test. Fix consequential problems and re-slice relevant changes. Stop when the design meets the brief and no remaining improvement justifies its cost.\n\nKeep the user-facing result short: the recommended design, why its manufacturing approach makes sense, how it prints and assembles, and any consequential unresolved assumption. Distinguish measured dimensions, slicer estimates, and untested predictions. Keep detailed analysis in the working process.";
14
+ export declare const FDM_MULTICOLOR = "Apply this guidance when multiple colors or support materials share a print.\n\nPreserve the requested visual pattern. Evaluate its colors in build coordinates and across layers before committing to color geometry. On a shared nozzle, a small accent spanning many layers can create more transitions than a large region confined to a few layers. Count the slicer's actual transitions when available, including swaps back to a prior filament and support-interface swaps; color count and colored volume are only screening clues. On printers with multiple nozzles, evaluate each shared melt path; AMS slots are not independent nozzles.\n\nFor a costly pattern, consider the most promising alternatives: a different build orientation, color regions confined to fewer layers, shallow face inlays, or separately printed single-color pieces with intentional assembly. A vertical stripe in use may become a low-change feature in a different print orientation. Judge each option against load direction, surface quality, support burden, and the user's exact visual requirements. Finer layers can help an overhang while adding color-change layers; evaluate local adaptive layers and re-slice the combined plan.\n\nSeparating color bodies does not by itself eliminate swaps. Check plate scheduling. Single-color plates can remove repeated color changes; by-layer printing of different-color objects together usually cannot. Use sequential object printing only when supported and its clearance constraints are satisfied. Batch copies only when the user wants them.\n\nUse the actual directional flushing matrix and filament compatibility. Purging clears previous material; priming restores consistent extrusion. Track those separately when the slicer does. Flushing into compatible infill or supports may reduce discarded material, but it does not remove switching time, has finite capacity, and can affect appearance or strength. Do not add unnecessary infill or objects merely to hide waste.\n\nCompare the same requested quantity and finish. Report total filament and time across all plates, plus purge, tower, and support quantities only where available without double counting. Unknown metrics remain unknown. Recommend the original multicolor approach when the user values its seamless result enough to justify its cost.";
15
+ export declare const FDM_ASSEMBLY = "Apply this guidance when a split, insert, or joint may materially improve the result.\n\nCompare assembly against the best practical one-piece approach, including reasonable supports. A split earns its place by improving important surfaces, layer strength, fit, color efficiency, support access, repairability, or bed fit enough to justify handling and joining the pieces. Prefer a one-piece solution when its support cleanup is easier than assembly and the alternatives meet the other important requirements comparably well. Respect an explicit one-piece or no-glue requirement.\n\nPut boundaries where they are structurally sensible and visually deliberate. Use shoulders, keys, overlapping faces, tabs, or pins to locate parts and resist sliding or rotation as appropriate. Include fit clearance, lead-ins, adhesive space where needed, and a feasible insertion path. Check the assembled geometry and the order of operations, not only the separated pieces. Avoid joints whose screws, clamps, or mating faces become inaccessible during assembly.\n\nFor suitable lightly loaded rigid details, a locating pocket plus a compatible adhesive can be the simplest reliable solution. For a loaded clamp, a joint under peel or repeated bending, or a serviceable assembly, consider fasteners or mechanically captured geometry. Do not assume a tiny glued butt joint can carry the same load as an intact printed section. Select adhesive from evidence for the actual materials and conditions; superglue is an option, not a universal material property.\n\nState the practical consequence directly: which parts print in which orientations and colors, how they locate, how they are retained, which supplies are needed, and the estimated hands-on work. Keep hands-on time separate from adhesive cure time. Check a small joint sample only when its uncertain fit or strength would change the design decision.";
16
+ export declare const FDM_AFTER_SLICE = "Review this slice against the user's brief and the selected manufacturing plan. Use the returned slice, geometry, profile, and calibration evidence; do not assume missing metrics are zero.\n\nCheck that the actual toolpaths preserve critical features, fit, connected material, intended colors, and load-bearing sections. Inspect consequential first layers, bridges, islands, supported undersides, and removal access. Compare expected total print time, total material across all plates, and manual work with the best credible alternative. Keep purge, prime-tower, support, and model quantities distinct only when the data provides a non-overlapping breakdown.\n\nIf the plan is unexpectedly costly or fragile, identify the dominant cause and make the smallest change that preserves the brief. Re-slice after a relevant geometry, orientation, material, or process change. Do not optimize a minor imperfection at the expense of function or appearance, and do not invent a redesign simply to produce criticism.\n\nReturn a concise decision: keep, revise, or validate one specific uncertainty. Include the decisive evidence and the exact next action. A clean slice supports a printability prediction; it does not verify physical fit, strength, or bridge quality.";
17
+ export declare const FDM_CAPABILITY_GUIDANCE = "# Bridges and overhangs: useful capability without arbitrary bans\n\nLoad this reference only when the feature matters to the manufacturing decision. These are proposed planning bands, not manufacturer guarantees, support-generation settings, or automatic acceptance/rejection rules.\n\n## Scope and terminology\n\nThe provisional baseline is an ordinary modern Bambu P-series-class FDM setup, a 0.4 mm nozzle, a suitable PLA profile, normal planar toolpaths, and effective part cooling. Actual printer/material/profile evidence takes priority. Do not transfer the same expectations unchanged to PETG, flexible materials, a different layer height, or a different cooling arrangement.\n\nAngles in the proposed review bands are measured **from vertical**: a vertical wall is 0 degrees; a horizontal underside is 90 degrees. Source values retain their stated or unresolved convention as noted. Slicer controls and test models can use a different convention. Establish their convention before comparing or entering a value. For a simple planar slope, the angle from the build plate is 90 degrees minus the angle from vertical.\n\nAn overhang grows sideways from material below. A conventional bridge has extruded paths anchored at both ends. A disconnected island begins with no suitable underlying material or anchorage. They require different decisions.\n\n## What the retrieved evidence establishes\n\n- Prusa's current modeling documentation gives a general 45\u201360 degree range depending on nozzle/settings, and describes up to 75 degrees for its specifically named Nextruder/360-degree-cooling machines. That is evidence against a universal 45-degree limit. It does not certify 75 degrees on every Bambu printer. [C1]\n- A Fab Academy 2026 hands-on comparison of Bambu P2S and H2S used matte-white PLA and reported 50 mm unsupported bridge strands without a visible problem. It also reported some drooping above 60 degrees on the overhang test. This is a useful documented result on particular geometry, not a population benchmark or proof of flat mating-surface accuracy. [C2]\n- An MIT HTMAA 2025 P1S characterization reported no noticeable defects on its longest tested bridge, 20 mm, and explicitly left the actual maximum unmeasured. A largest passing sample is not the machine's limit. [C3]\n- Bambu's PETG Basic V3.0 data sheet lists approximately 30 mm maximum bridging length and approximately 70 degrees maximum overhang; its PLA Tough+ V3.0 sheet lists 30 mm and 55 degrees. These are material-specific reference values. The retrieved sheets do not define the overhang angle convention or a bridge sag/finish acceptance criterion, so keep the angle values as reported rather than silently normalizing them. Do not assume the mechanical-specimen print conditions also describe these feature tests. [C4, C5]\n\nThe material sheets provide useful 30 mm reference points; the firsthand longer prints show why a published or configured maximum should not become a universal geometric prohibition. Filament formulation matters. Avoid a rule that all PLA must outperform all PETG.\n\n## Proposed review bands\n\nThese bands decide how much attention to give a feature. They never force a flat design, a split, supports, or a calibration print by themselves.\n\n| Feature on the provisional PLA setup | Working stance |\n| --- | --- |\n| Progressively supported overhang around 45\u201360 degrees | Keep an unsupported version as an ordinary candidate. Inspect local curvature, overlap, cooling, and required finish. |\n| Overhang around 60\u201370 degrees | Retain the shape as a candidate; examine local layer height, geometry, cooling, and relevant prior results more closely. Modest support is also an ordinary option. |\n| Overhang around 70 degrees or more | Require a stronger process/geometry justification to rely on a clean unsupported result. Consider a representative sample when its result would decide between substantially different plans. |\n| Conventional bridge up to about 20 mm | Treat as a plausible ordinary candidate when anchors, toolpaths, and underside requirements are suitable. |\n| Bridge roughly 20\u201350 mm | Keep bridging as a real design option. Pay more attention to actual unsupported path length, anchorage, width, cooling, and sag acceptance. |\n| Bridge beyond about 50 mm | Keep it in the design space; seek relevant prior evidence or a small representative test before depending on its quality for an expensive or precise part. |\n\nThe 60\u201370-degree and 20\u201350-mm bands are deliberate engineering priors for this app. They are not claims that a manufacturer has validated the whole interval. A 12 mm bridge can be unacceptable for a tight sliding surface; a much longer one may be adequate above a noncritical cavity. Avoid turning these bands into a hidden `max_bridge` or `max_overhang` rejection threshold.\n\n## A useful geometric check\n\nFor an idealized straight overhang at angle alpha from vertical, layer height h produces a lateral step:\n\n`step = h * tan(alpha)`\n\nUsing a nominal extrusion width w, a simple projected overlap estimate is:\n\n`overlap_fraction = max(0, 1 - step / w)`\n\nFor a 60-degree overhang and a 0.45 mm line width:\n\n| Layer height | Lateral step | Nominal overlap |\n| --- | --- | --- |\n| 0.20 mm | 0.346 mm | About 23% |\n| 0.12 mm | 0.208 mm | About 54% |\n\nThese values follow from geometry, not a printer test. The model omits bead shape, curved toolpaths, cooling, adhesion, and other effects. It explains why layer height can change the outlook; it cannot prove a print will succeed. Use the actual extrusion widths when known.\n\nOrdinary overhang-layer reasoning does not prescribe the best bridge extrusion. OrcaSlicer's documented thick-bridge mode uses a line height equal to nozzle diameter and trades a rougher surface for stronger, more reliable bridges and potentially longer spans. Inspect the actual selected mode instead of assuming bridge height equals ordinary layer height. [C6]\n\nReducing layer height throughout a multicolor part can also increase the number of color-change layers. Prefer a local intervention when appropriate and re-slice the whole manufacturing plan to see the tradeoff.\n\n## Local interventions to consider\n\nKeep the intervention proportionate to the specific problem. Useful candidates include changing build orientation, adjusting only a hidden underside, replacing the top of a horizontal hole with a suitable printable roof while preserving its necessary clearance, using local adaptive layers, improving a bridge's anchors or extrusion direction, adding a small removable sacrificial feature, placing accessible support, or splitting at a sensible joint.\n\nPreserve roundness where a mating shaft genuinely needs it. Preserve curves where they matter visually. An unremovable support inside a duct is not a valid plan. Do not trade away layer bonding to improve cooling without considering the part's mechanical job.\n\nTreat the slicer's support-generation thresholds as policy settings, not measured capability. For example, OrcaSlicer's `max_bridge_length` decides which anchored spans tree supports leave unsupported. Its configured value does not establish what the machine can physically bridge. [C7]\n\n## What to learn from physical prints\n\nRecord the feature and its conditions, not merely a global pass/fail label. Useful evidence includes:\n\n- Largest tested span or angle, plus known failures and untested regions.\n- Actual printer, filament identity, nozzle, layer/line profile, cooling, and build orientation.\n- Bridge direction, width, anchors, and nearby geometry.\n- Whether the criterion was survival, cosmetic quality, measured sag, fit, or load performance.\n\nA small combined coupon at onboarding can complement the printed measuring tool, but it should be optional. Later, print a targeted sample when an important unknown would otherwise put a much larger print at risk. Routine geometry should not repeatedly trigger calibration work.\n\n## Primary sources\n\n[C1] Prusa, Modeling with 3D printing in mind:\n\nhttps://help.prusa3d.com/article/modeling-with-3d-printing-in-mind_164135\n\n[C2] FabLab Nagoya, Fab Academy 2026 Week 05, firsthand P2S/H2S characterization:\n\nhttps://fabacademy.org/2026/labs/nagoya/assignments/week05/\n\n[C3] Ryan Hoffman, MIT HTMAA 2025, firsthand P1S characterization:\n\nhttps://fab.cba.mit.edu/classes/863.25/people/RyanHoffman/week3.html\n\n[C4] Bambu PETG Basic Technical Data Sheet V3.0:\n\nhttps://store.bblcdn.com/s1/default/cb94589bf7994fdcbfa833badefae9cd/Bambu_PETG_Basic_Technical_Data_Sheet.pdf\n\n[C5] Bambu PLA Tough+ Technical Data Sheet V3.0:\n\nhttps://store.bblcdn.com/s7/default/80a85371db2e40bda97069d283fed76c/BambuPLA_Tough_TechnicalData_Sheet_%282%29.pdf\n\n[C6] OrcaSlicer bridge settings:\n\nhttps://github.com/OrcaSlicer/OrcaSlicer/wiki/quality_settings_bridging\n\n[C7] OrcaSlicer advanced support settings:\n\nhttps://github.com/OrcaSlicer/OrcaSlicer/wiki/support_settings_advanced\n\nAll sources inspected 3 November 2026.";
18
+ export declare const FDM_CAPABILITY_URI = "guidance://fdm/capability-guidance";
19
+ export declare const FDM_CAPABILITY_RESOURCE: {
20
+ uri: string;
21
+ name: string;
22
+ mimeType: string;
23
+ description: string;
24
+ };
25
+ export declare const DESIGN_PROMPTS: DesignPrompt[];
26
+ export declare const SERVER_INSTRUCTIONS: string;