mcp-3d-printer-server 1.2.20 → 1.2.21

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 (51) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +112 -9
  3. package/dist/blender-mcp-bridge.d.ts +21 -0
  4. package/dist/blender-mcp-bridge.js +3 -3
  5. package/dist/counterpart/contract.d.ts +395 -0
  6. package/dist/counterpart/contract.js +220 -0
  7. package/dist/counterpart/progress.d.ts +32 -0
  8. package/dist/counterpart/progress.js +443 -0
  9. package/dist/counterpart/service.d.ts +56 -0
  10. package/dist/counterpart/service.js +224 -0
  11. package/dist/fusion-mcp-bridge.d.ts +166 -0
  12. package/dist/fusion-mcp-bridge.js +529 -0
  13. package/dist/index.js +272 -8
  14. package/dist/printers/creality.d.ts +26 -1
  15. package/dist/printers/creality.js +75 -2
  16. package/dist/printers/duet.d.ts +16 -0
  17. package/dist/printers/duet.js +64 -0
  18. package/dist/printers/generic-base.d.ts +52 -6
  19. package/dist/printers/generic-base.js +134 -7
  20. package/dist/printers/klipper.d.ts +33 -2
  21. package/dist/printers/klipper.js +106 -0
  22. package/dist/printers/octoprint.d.ts +23 -1
  23. package/dist/printers/octoprint.js +81 -0
  24. package/dist/printers/prusa.d.ts +13 -0
  25. package/dist/printers/prusa.js +59 -0
  26. package/dist/printers/repetier.d.ts +17 -1
  27. package/dist/printers/repetier.js +55 -0
  28. package/dist/slicer/bambu-cli.d.ts +2 -0
  29. package/dist/slicer/bambu-cli.js +11 -0
  30. package/dist/slicer/filament-profiles.d.ts +25 -0
  31. package/dist/slicer/filament-profiles.js +249 -0
  32. package/dist/stl/stl-manipulator.d.ts +21 -1
  33. package/dist/stl/stl-manipulator.js +86 -6
  34. package/package.json +7 -2
  35. package/server.json +2 -2
  36. package/src/blender-mcp-bridge.ts +3 -3
  37. package/src/counterpart/contract.ts +326 -0
  38. package/src/counterpart/progress.ts +470 -0
  39. package/src/counterpart/service.ts +247 -0
  40. package/src/fusion-mcp-bridge.ts +512 -0
  41. package/src/index.ts +314 -10
  42. package/src/printers/creality.ts +84 -1
  43. package/src/printers/duet.ts +67 -0
  44. package/src/printers/generic-base.ts +172 -1
  45. package/src/printers/klipper.ts +118 -2
  46. package/src/printers/octoprint.ts +86 -1
  47. package/src/printers/prusa.ts +60 -0
  48. package/src/printers/repetier.ts +63 -1
  49. package/src/slicer/bambu-cli.ts +13 -0
  50. package/src/slicer/filament-profiles.ts +249 -0
  51. package/src/stl/stl-manipulator.ts +100 -6
@@ -0,0 +1,326 @@
1
+ /**
2
+ * The Counterpart tool contract (`counterpart/1`). bambu-printer-mcp and this
3
+ * server return the same shapes so one client can treat both alike. Optional
4
+ * fields are omitted, never null: JSON Schema `number` rejects null.
5
+ */
6
+ import path from "node:path";
7
+
8
+ export const COUNTERPART_CONTRACT = "counterpart/1";
9
+
10
+ export const PRINTER_KINDS = ["bambu", "octoprint", "klipper", "prusa", "duet", "repetier", "creality"] as const;
11
+ export type PrinterKind = (typeof PRINTER_KINDS)[number];
12
+
13
+ export const PROGRESS_STATES = ["idle", "preparing", "printing", "paused", "finished", "failed", "cancelled", "offline", "busy"] as const;
14
+ export type ProgressState = (typeof PROGRESS_STATES)[number];
15
+
16
+ export const PROBLEM_SEVERITIES = ["fatal", "error", "warning", "info"] as const;
17
+ export type ProblemSeverity = (typeof PROBLEM_SEVERITIES)[number];
18
+
19
+ export type CameraCapability = "bambu-tcp" | "rtsp" | "http-snapshot" | "none";
20
+ export type FilamentCapability = "ams" | "single" | "none";
21
+
22
+ export interface CounterpartProblem {
23
+ code: string;
24
+ severity: ProblemSeverity;
25
+ message: string;
26
+ url?: string;
27
+ }
28
+
29
+ export interface HeaterReading {
30
+ actual: number;
31
+ target: number;
32
+ }
33
+
34
+ export interface CounterpartJob {
35
+ name: string;
36
+ id?: string;
37
+ progress?: number;
38
+ elapsedSec?: number;
39
+ remainingSec?: number;
40
+ layer?: number;
41
+ totalLayers?: number;
42
+ stage?: string;
43
+ }
44
+
45
+ export interface CounterpartProgress {
46
+ contract: typeof COUNTERPART_CONTRACT;
47
+ observedAt: string;
48
+ state: ProgressState;
49
+ job?: CounterpartJob;
50
+ temps?: { nozzle?: HeaterReading; bed?: HeaterReading; chamber?: number };
51
+ activeFilament?: { slot?: string; material?: string; color?: string };
52
+ problems: CounterpartProblem[];
53
+ }
54
+
55
+ export interface CounterpartCapabilities {
56
+ progress: boolean;
57
+ layers: boolean;
58
+ camera: CameraCapability;
59
+ filaments: FilamentCapability;
60
+ pause: boolean;
61
+ resume: boolean;
62
+ cancel: boolean;
63
+ print3mf: boolean;
64
+ gcode: boolean;
65
+ }
66
+
67
+ export interface PrinterIdentity {
68
+ model?: string;
69
+ serial?: string;
70
+ name?: string;
71
+ }
72
+
73
+ export interface CounterpartPrinterInfo {
74
+ contract: typeof COUNTERPART_CONTRACT;
75
+ server: { name: string; version: string };
76
+ printer: { kind: PrinterKind; host: string } & PrinterIdentity;
77
+ capabilities: CounterpartCapabilities;
78
+ }
79
+
80
+ export interface FilamentSlot {
81
+ id: "ext";
82
+ material?: string;
83
+ color?: string;
84
+ name?: string;
85
+ source: "spoolman" | "unknown";
86
+ /** Absolute path of the slicer filament profile for this material, as slice_stl's load_filaments takes it. */
87
+ profile?: string;
88
+ }
89
+
90
+ export interface CameraSnapshotResult {
91
+ status: "success";
92
+ format: "image/jpeg";
93
+ sizeBytes: number;
94
+ base64: string;
95
+ savedTo?: string;
96
+ capturedAt: string;
97
+ /** Where the frame came from, e.g. "octoprint-settings" or "env-override". */
98
+ source: string;
99
+ }
100
+
101
+ const heater = {
102
+ type: "object",
103
+ required: ["actual", "target"],
104
+ properties: { actual: { type: "number" }, target: { type: "number" } },
105
+ };
106
+
107
+ const problem = {
108
+ type: "object",
109
+ required: ["code", "severity", "message"],
110
+ properties: {
111
+ code: { type: "string" },
112
+ severity: { type: "string", enum: [...PROBLEM_SEVERITIES] },
113
+ message: { type: "string" },
114
+ url: { type: "string" },
115
+ },
116
+ };
117
+
118
+ /** outputSchema for counterpart_printer_info (PRD §8.4). */
119
+ export const PRINTER_INFO_OUTPUT_SCHEMA = {
120
+ type: "object",
121
+ required: ["contract", "server", "printer", "capabilities"],
122
+ properties: {
123
+ contract: { type: "string", const: COUNTERPART_CONTRACT },
124
+ server: {
125
+ type: "object",
126
+ required: ["name", "version"],
127
+ properties: { name: { type: "string" }, version: { type: "string" } },
128
+ },
129
+ printer: {
130
+ type: "object",
131
+ required: ["kind", "host"],
132
+ properties: {
133
+ kind: { type: "string", enum: [...PRINTER_KINDS] },
134
+ model: { type: "string" },
135
+ serial: { type: "string" },
136
+ host: { type: "string" },
137
+ name: { type: "string" },
138
+ },
139
+ },
140
+ capabilities: {
141
+ type: "object",
142
+ required: ["progress", "layers", "camera", "filaments", "pause", "resume", "cancel", "print3mf", "gcode"],
143
+ properties: {
144
+ progress: { type: "boolean" },
145
+ layers: { type: "boolean" },
146
+ camera: { type: "string", enum: ["bambu-tcp", "rtsp", "http-snapshot", "none"] },
147
+ filaments: { type: "string", enum: ["ams", "single", "none"] },
148
+ pause: { type: "boolean" },
149
+ resume: { type: "boolean" },
150
+ cancel: { type: "boolean" },
151
+ print3mf: { type: "boolean" },
152
+ gcode: { type: "boolean" },
153
+ },
154
+ },
155
+ },
156
+ } as const;
157
+
158
+ /** outputSchema for counterpart_print_progress (PRD §8.4). */
159
+ export const PRINT_PROGRESS_OUTPUT_SCHEMA = {
160
+ type: "object",
161
+ required: ["contract", "observedAt", "state", "problems"],
162
+ properties: {
163
+ contract: { type: "string", const: COUNTERPART_CONTRACT },
164
+ observedAt: { type: "string" },
165
+ state: { type: "string", enum: [...PROGRESS_STATES] },
166
+ job: {
167
+ type: "object",
168
+ required: ["name"],
169
+ properties: {
170
+ name: { type: "string" },
171
+ id: { type: "string" },
172
+ progress: { type: "number", minimum: 0, maximum: 100 },
173
+ elapsedSec: { type: "number", minimum: 0 },
174
+ remainingSec: { type: "number", minimum: 0 },
175
+ layer: { type: "number" },
176
+ totalLayers: { type: "number" },
177
+ stage: { type: "string" },
178
+ },
179
+ },
180
+ temps: {
181
+ type: "object",
182
+ properties: { nozzle: heater, bed: heater, chamber: { type: "number" } },
183
+ },
184
+ activeFilament: {
185
+ type: "object",
186
+ properties: { slot: { type: "string" }, material: { type: "string" }, color: { type: "string" } },
187
+ },
188
+ problems: { type: "array", items: problem },
189
+ },
190
+ } as const;
191
+
192
+ /** outputSchema for camera_snapshot; the result matches bambu-printer-mcp's plus capturedAt. */
193
+ export const CAMERA_SNAPSHOT_OUTPUT_SCHEMA = {
194
+ type: "object",
195
+ required: ["status", "format", "sizeBytes", "base64", "capturedAt"],
196
+ properties: {
197
+ status: { type: "string" },
198
+ format: { type: "string" },
199
+ sizeBytes: { type: "number" },
200
+ base64: { type: "string" },
201
+ savedTo: { type: "string" },
202
+ capturedAt: { type: "string" },
203
+ source: { type: "string" },
204
+ },
205
+ } as const;
206
+
207
+ /** outputSchema for get_printer_filaments in this server: one external slot. */
208
+ export const FILAMENTS_OUTPUT_SCHEMA = {
209
+ type: "object",
210
+ required: ["slots"],
211
+ properties: {
212
+ slots: {
213
+ type: "array",
214
+ items: {
215
+ type: "object",
216
+ required: ["id", "source"],
217
+ properties: {
218
+ id: { type: "string" },
219
+ material: { type: "string" },
220
+ color: { type: "string" },
221
+ name: { type: "string" },
222
+ source: { type: "string", enum: ["spoolman", "unknown"] },
223
+ profile: {
224
+ type: "string",
225
+ description: "Absolute path of the installed slicer filament profile for this slot's material, which slice_stl accepts as load_filaments. Omitted when none resolves.",
226
+ },
227
+ },
228
+ },
229
+ },
230
+ },
231
+ } as const;
232
+
233
+ /** outputSchema for pause_print and resume_print. */
234
+ export const CONTROL_OUTPUT_SCHEMA = {
235
+ type: "object",
236
+ required: ["status", "message"],
237
+ properties: {
238
+ status: { type: "string" },
239
+ message: { type: "string" },
240
+ action: { type: "string", enum: ["pause", "resume"] },
241
+ jobName: { type: "string" },
242
+ },
243
+ } as const;
244
+
245
+ // ---- Value helpers: every optional number is finite or omitted. ----
246
+
247
+ export function finiteNumber(value: unknown): number | undefined {
248
+ if (typeof value === "number") return Number.isFinite(value) ? value : undefined;
249
+ if (typeof value === "string" && value.trim() !== "") {
250
+ const parsed = Number(value);
251
+ return Number.isFinite(parsed) ? parsed : undefined;
252
+ }
253
+ return undefined;
254
+ }
255
+
256
+ /** A percentage clamped to 0–100, rounded to 0.1. */
257
+ export function percent(value: unknown): number | undefined {
258
+ const number = finiteNumber(value);
259
+ if (number === undefined) return undefined;
260
+ return Math.round(Math.min(100, Math.max(0, number)) * 10) / 10;
261
+ }
262
+
263
+ /** Non-negative whole seconds. */
264
+ export function seconds(value: unknown): number | undefined {
265
+ const number = finiteNumber(value);
266
+ if (number === undefined || number < 0) return undefined;
267
+ return Math.round(number);
268
+ }
269
+
270
+ /** A positive integer layer count or index. */
271
+ export function layerNumber(value: unknown): number | undefined {
272
+ const number = finiteNumber(value);
273
+ return number !== undefined && number > 0 ? Math.round(number) : undefined;
274
+ }
275
+
276
+ export function nonEmptyText(value: unknown): string | undefined {
277
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
278
+ }
279
+
280
+ /** {actual, target} when the actual reading is finite; a missing target means off (0). */
281
+ export function heaterReading(actual: unknown, target: unknown): HeaterReading | undefined {
282
+ const value = finiteNumber(actual);
283
+ if (value === undefined) return undefined;
284
+ return { actual: Math.round(value * 10) / 10, target: Math.round((finiteNumber(target) ?? 0) * 10) / 10 };
285
+ }
286
+
287
+ /**
288
+ * The job name the printer reports, reduced to its file name so it matches the
289
+ * uploaded `jobName` (Duet reports 0:/gcodes/x.gcode, Klipper gcodes-relative paths).
290
+ */
291
+ export function remoteJobName(value: unknown): string | undefined {
292
+ const text = nonEmptyText(value);
293
+ if (!text) return undefined;
294
+ const name = path.posix.basename(text.replace(/\\/g, "/").replace(/^\d+:/, ""));
295
+ return name || undefined;
296
+ }
297
+
298
+ /** Remove undefined members recursively so results never carry null/undefined fields. */
299
+ export function compact<T>(value: T): T {
300
+ if (Array.isArray(value)) return value.map((entry) => compact(entry)) as unknown as T;
301
+ if (value && typeof value === "object") {
302
+ const entries = Object.entries(value as Record<string, unknown>)
303
+ .filter(([, entry]) => entry !== undefined && entry !== null && !(typeof entry === "number" && !Number.isFinite(entry)))
304
+ .map(([key, entry]) => [key, compact(entry)]);
305
+ return Object.fromEntries(entries.filter(([, entry]) =>
306
+ !(entry && typeof entry === "object" && !Array.isArray(entry) && Object.keys(entry as object).length === 0)
307
+ )) as T;
308
+ }
309
+ return value;
310
+ }
311
+
312
+ export function progressResult(
313
+ observedAt: string,
314
+ state: ProgressState,
315
+ parts: Omit<Partial<CounterpartProgress>, "contract" | "observedAt" | "state">
316
+ ): CounterpartProgress {
317
+ return compact({
318
+ contract: COUNTERPART_CONTRACT,
319
+ observedAt,
320
+ state,
321
+ job: parts.job,
322
+ temps: parts.temps,
323
+ activeFilament: parts.activeFilament,
324
+ problems: parts.problems ?? [],
325
+ }) as CounterpartProgress;
326
+ }