UNPKG

3d-printer-mcp-server

Version:

MCP server for connecting Claude with 3D printer management systems

1,140 lines (1,001 loc) 38.2 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { ListResourcesRequestSchema, ReadResourceRequestSchema, ListToolsRequestSchema, CallToolRequestSchema, ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js"; import axios, { AxiosInstance } from "axios"; import dotenv from "dotenv"; import fs from "fs"; import path from "path"; import FormData from "form-data"; import { BambuPrinter } from "bambu-js"; // Import the type for manipulateFiles context type BambuFTP = { readDir: (path: string) => Promise<string[]>; sendFile: (sourcePath: string, destinationPath: string, progressCallback?: (progress: number) => void) => Promise<void>; removeFile: (path: string) => Promise<void>; }; // Load environment variables from .env file dotenv.config(); // Default values const DEFAULT_HOST = process.env.PRINTER_HOST || "localhost"; const DEFAULT_PORT = process.env.PRINTER_PORT || "80"; const DEFAULT_API_KEY = process.env.API_KEY || ""; const DEFAULT_TYPE = process.env.PRINTER_TYPE || "octoprint"; // Default to OctoPrint const TEMP_DIR = process.env.TEMP_DIR || path.join(process.cwd(), "temp"); // Bambu-specific default values const DEFAULT_BAMBU_SERIAL = process.env.BAMBU_SERIAL || ""; const DEFAULT_BAMBU_TOKEN = process.env.BAMBU_TOKEN || ""; // Ensure temp directory exists if (!fs.existsSync(TEMP_DIR)) { fs.mkdirSync(TEMP_DIR, { recursive: true }); } class ThreeDPrinterMCPServer { private server: Server; private apiClient: AxiosInstance; private bambuPrinters: Map<string, InstanceType<typeof BambuPrinter>> = new Map(); constructor() { this.server = new Server( { name: "3d-printer-mcp-server", version: "1.0.0" }, { capabilities: { resources: {}, tools: {} } } ); // Create axios instance with default configuration this.apiClient = axios.create({ timeout: 10000, }); this.setupHandlers(); this.setupErrorHandling(); } setupErrorHandling() { this.server.onerror = (error) => { console.error("[MCP Error]", error); }; process.on("SIGINT", async () => { // Disconnect all Bambu printers for (const printer of this.bambuPrinters.values()) { await printer.disconnect(); } await this.server.close(); process.exit(0); }); } setupHandlers() { this.setupResourceHandlers(); this.setupToolHandlers(); } setupResourceHandlers() { // List available resources this.server.setRequestHandler(ListResourcesRequestSchema, async () => { return { resources: [ { uri: `printer://${DEFAULT_HOST}/status`, name: "3D Printer Status", mimeType: "application/json", description: "Current status of the 3D printer including temperatures, print progress, and more" }, { uri: `printer://${DEFAULT_HOST}/files`, name: "3D Printer Files", mimeType: "application/json", description: "List of files available on the 3D printer" } ], templates: [ { uriTemplate: "printer://{host}/status", name: "3D Printer Status", mimeType: "application/json" }, { uriTemplate: "printer://{host}/files", name: "3D Printer Files", mimeType: "application/json" }, { uriTemplate: "printer://{host}/file/{filename}", name: "3D Printer File Content", mimeType: "application/gcode" } ] }; }); // Read resource this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const uri = request.params.uri; const match = uri.match(/^printer:\/\/([^\/]+)\/(.+)$/); if (!match) { throw new McpError(ErrorCode.InvalidRequest, `Invalid resource URI: ${uri}`); } const [, host, resource] = match; let content; try { if (resource === "status") { content = await this.getPrinterStatus(host); } else if (resource === "files") { content = await this.getPrinterFiles(host); } else if (resource.startsWith("file/")) { const filename = resource.substring(5); content = await this.getPrinterFile(host, filename); } else { throw new McpError(ErrorCode.InvalidRequest, `Unknown resource: ${resource}`); } return { contents: [ { uri, mimeType: resource.startsWith("file/") ? "application/gcode" : "application/json", text: typeof content === "string" ? content : JSON.stringify(content, null, 2) } ] }; } catch (error) { if (axios.isAxiosError(error)) { throw new McpError( ErrorCode.InternalError, `API error: ${error.response?.data?.error || error.message}` ); } throw error; } }); } setupToolHandlers() { // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "get_printer_status", description: "Get the current status of the 3D printer", inputSchema: { type: "object", properties: { host: { type: "string", description: "Hostname or IP address of the printer (default: value from env)" }, port: { type: "string", description: "Port of the printer API (default: value from env)" }, type: { type: "string", description: "Type of printer management system (octoprint, klipper, duet, repetier, bambu) (default: value from env)" }, api_key: { type: "string", description: "API key for authentication (default: value from env)" }, bambu_serial: { type: "string", description: "Serial number for Bambu Lab printers (default: value from env)" }, bambu_token: { type: "string", description: "Access token for Bambu Lab printers (default: value from env)" } } } }, { name: "list_printer_files", description: "List files available on the printer", inputSchema: { type: "object", properties: { host: { type: "string", description: "Hostname or IP address of the printer (default: value from env)" }, port: { type: "string", description: "Port of the printer API (default: value from env)" }, type: { type: "string", description: "Type of printer management system (octoprint, klipper, duet, repetier, bambu) (default: value from env)" }, api_key: { type: "string", description: "API key for authentication (default: value from env)" }, bambu_serial: { type: "string", description: "Serial number for Bambu Lab printers (default: value from env)" }, bambu_token: { type: "string", description: "Access token for Bambu Lab printers (default: value from env)" } } } }, { name: "upload_gcode", description: "Upload a G-code file to the printer", inputSchema: { type: "object", properties: { host: { type: "string", description: "Hostname or IP address of the printer (default: value from env)" }, port: { type: "string", description: "Port of the printer API (default: value from env)" }, type: { type: "string", description: "Type of printer management system (octoprint, klipper, duet, repetier, bambu) (default: value from env)" }, api_key: { type: "string", description: "API key for authentication (default: value from env)" }, bambu_serial: { type: "string", description: "Serial number for Bambu Lab printers (default: value from env)" }, bambu_token: { type: "string", description: "Access token for Bambu Lab printers (default: value from env)" }, filename: { type: "string", description: "Name of the file to upload" }, gcode: { type: "string", description: "G-code content to upload" }, print: { type: "boolean", description: "Whether to start printing the file after upload (default: false)" } }, required: ["filename", "gcode"] } }, { name: "start_print", description: "Start printing a file that is already on the printer", inputSchema: { type: "object", properties: { host: { type: "string", description: "Hostname or IP address of the printer (default: value from env)" }, port: { type: "string", description: "Port of the printer API (default: value from env)" }, type: { type: "string", description: "Type of printer management system (octoprint, klipper, duet, repetier, bambu) (default: value from env)" }, api_key: { type: "string", description: "API key for authentication (default: value from env)" }, bambu_serial: { type: "string", description: "Serial number for Bambu Lab printers (default: value from env)" }, bambu_token: { type: "string", description: "Access token for Bambu Lab printers (default: value from env)" }, filename: { type: "string", description: "Name of the file to print" } }, required: ["filename"] } }, { name: "cancel_print", description: "Cancel the current print job", inputSchema: { type: "object", properties: { host: { type: "string", description: "Hostname or IP address of the printer (default: value from env)" }, port: { type: "string", description: "Port of the printer API (default: value from env)" }, type: { type: "string", description: "Type of printer management system (octoprint, klipper, duet, repetier, bambu) (default: value from env)" }, api_key: { type: "string", description: "API key for authentication (default: value from env)" }, bambu_serial: { type: "string", description: "Serial number for Bambu Lab printers (default: value from env)" }, bambu_token: { type: "string", description: "Access token for Bambu Lab printers (default: value from env)" } } } }, { name: "set_printer_temperature", description: "Set the temperature of a printer component", inputSchema: { type: "object", properties: { host: { type: "string", description: "Hostname or IP address of the printer (default: value from env)" }, port: { type: "string", description: "Port of the printer API (default: value from env)" }, type: { type: "string", description: "Type of printer management system (octoprint, klipper, duet, repetier, bambu) (default: value from env)" }, api_key: { type: "string", description: "API key for authentication (default: value from env)" }, bambu_serial: { type: "string", description: "Serial number for Bambu Lab printers (default: value from env)" }, bambu_token: { type: "string", description: "Access token for Bambu Lab printers (default: value from env)" }, component: { type: "string", description: "Component to set temperature for (e.g., 'extruder', 'bed')" }, temperature: { type: "number", description: "Temperature to set in degrees Celsius" } }, required: ["component", "temperature"] } } ] }; }); // Handle tool calls this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; // Set default values for common parameters const host = String(args?.host || DEFAULT_HOST); const port = String(args?.port || DEFAULT_PORT); const type = String(args?.type || DEFAULT_TYPE); const apiKey = String(args?.api_key || DEFAULT_API_KEY); const bambuSerial = String(args?.bambu_serial || DEFAULT_BAMBU_SERIAL); const bambuToken = String(args?.bambu_token || DEFAULT_BAMBU_TOKEN); try { let result; switch (name) { case "get_printer_status": result = await this.getPrinterStatus(host, port, type, apiKey, bambuSerial, bambuToken); break; case "list_printer_files": result = await this.getPrinterFiles(host, port, type, apiKey, bambuSerial, bambuToken); break; case "upload_gcode": if (!args?.filename || !args?.gcode) { throw new Error("Missing required parameters: filename and gcode"); } result = await this.uploadGcode( host, port, type, apiKey, bambuSerial, bambuToken, String(args.filename), String(args.gcode), Boolean(args.print || false) ); break; case "start_print": if (!args?.filename) { throw new Error("Missing required parameter: filename"); } result = await this.startPrint(host, port, type, apiKey, bambuSerial, bambuToken, String(args.filename)); break; case "cancel_print": result = await this.cancelPrint(host, port, type, apiKey, bambuSerial, bambuToken); break; case "set_printer_temperature": if (!args?.component || args?.temperature === undefined) { throw new Error("Missing required parameters: component and temperature"); } result = await this.setPrinterTemperature( host, port, type, apiKey, bambuSerial, bambuToken, String(args.component), Number(args.temperature) ); break; default: throw new Error(`Unknown tool: ${name}`); } return { content: [ { type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) } ] }; } catch (error: unknown) { console.error(`Error calling tool ${name}:`, error); const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: `Error: ${errorMessage}` } ], isError: true }; } }); } // Get or create a Bambu printer private getBambuPrinter(host: string, serial: string, token: string): InstanceType<typeof BambuPrinter> { const key = `${host}-${serial}`; if (!this.bambuPrinters.has(key)) { const printer = new BambuPrinter(host, serial, token); this.bambuPrinters.set(key, printer); } return this.bambuPrinters.get(key)!; } // Resource and Tool Implementation Methods async getPrinterStatus( host: string, port = DEFAULT_PORT, type = DEFAULT_TYPE, apiKey = DEFAULT_API_KEY, bambuSerial = DEFAULT_BAMBU_SERIAL, bambuToken = DEFAULT_BAMBU_TOKEN ) { switch (type.toLowerCase()) { case "octoprint": return this.getOctoPrintStatus(host, port, apiKey); case "klipper": return this.getKlipperStatus(host, port, apiKey); case "duet": return this.getDuetStatus(host, port, apiKey); case "repetier": return this.getRepetierStatus(host, port, apiKey); case "bambu": return this.getBambuStatus(host, bambuSerial, bambuToken); default: throw new Error(`Unsupported printer type: ${type}`); } } async getPrinterFiles( host: string, port = DEFAULT_PORT, type = DEFAULT_TYPE, apiKey = DEFAULT_API_KEY, bambuSerial = DEFAULT_BAMBU_SERIAL, bambuToken = DEFAULT_BAMBU_TOKEN ) { switch (type.toLowerCase()) { case "octoprint": return this.getOctoPrintFiles(host, port, apiKey); case "klipper": return this.getKlipperFiles(host, port, apiKey); case "duet": return this.getDuetFiles(host, port, apiKey); case "repetier": return this.getRepetierFiles(host, port, apiKey); case "bambu": return this.getBambuFiles(host, bambuSerial, bambuToken); default: throw new Error(`Unsupported printer type: ${type}`); } } async getPrinterFile( host: string, filename: string, port = DEFAULT_PORT, type = DEFAULT_TYPE, apiKey = DEFAULT_API_KEY, bambuSerial = DEFAULT_BAMBU_SERIAL, bambuToken = DEFAULT_BAMBU_TOKEN ) { switch (type.toLowerCase()) { case "octoprint": return this.getOctoPrintFile(host, port, apiKey, filename); case "klipper": return this.getKlipperFile(host, port, apiKey, filename); case "duet": return this.getDuetFile(host, port, apiKey, filename); case "repetier": return this.getRepetierFile(host, port, apiKey, filename); case "bambu": return this.getBambuFile(host, bambuSerial, bambuToken, filename); default: throw new Error(`Unsupported printer type: ${type}`); } } async uploadGcode( host: string, port: string, type: string, apiKey: string, bambuSerial: string, bambuToken: string, filename: string, gcode: string, print: boolean ) { const tempFilePath = path.join(TEMP_DIR, filename); // Write gcode to temporary file fs.writeFileSync(tempFilePath, gcode); try { switch (type.toLowerCase()) { case "octoprint": return await this.uploadToOctoPrint(host, port, apiKey, tempFilePath, filename, print); case "klipper": return await this.uploadToKlipper(host, port, apiKey, tempFilePath, filename, print); case "duet": return await this.uploadToDuet(host, port, apiKey, tempFilePath, filename, print); case "repetier": return await this.uploadToRepetier(host, port, apiKey, tempFilePath, filename, print); case "bambu": return await this.uploadToBambu(host, bambuSerial, bambuToken, tempFilePath, filename, print); default: throw new Error(`Unsupported printer type: ${type}`); } } finally { // Clean up temporary file if (fs.existsSync(tempFilePath)) { fs.unlinkSync(tempFilePath); } } } async startPrint( host: string, port: string, type: string, apiKey: string, bambuSerial: string, bambuToken: string, filename: string ) { switch (type.toLowerCase()) { case "octoprint": return await this.startOctoPrintJob(host, port, apiKey, filename); case "klipper": return await this.startKlipperJob(host, port, apiKey, filename); case "duet": return await this.startDuetJob(host, port, apiKey, filename); case "repetier": return await this.startRepetierJob(host, port, apiKey, filename); case "bambu": return await this.startBambuJob(host, bambuSerial, bambuToken, filename); default: throw new Error(`Unsupported printer type: ${type}`); } } async cancelPrint( host: string, port: string, type: string, apiKey: string, bambuSerial: string, bambuToken: string ) { switch (type.toLowerCase()) { case "octoprint": return await this.cancelOctoPrintJob(host, port, apiKey); case "klipper": return await this.cancelKlipperJob(host, port, apiKey); case "duet": return await this.cancelDuetJob(host, port, apiKey); case "repetier": return await this.cancelRepetierJob(host, port, apiKey); case "bambu": return await this.cancelBambuJob(host, bambuSerial, bambuToken); default: throw new Error(`Unsupported printer type: ${type}`); } } async setPrinterTemperature( host: string, port: string, type: string, apiKey: string, bambuSerial: string, bambuToken: string, component: string, temperature: number ) { switch (type.toLowerCase()) { case "octoprint": return await this.setOctoPrintTemperature(host, port, apiKey, component, temperature); case "klipper": return await this.setKlipperTemperature(host, port, apiKey, component, temperature); case "duet": return await this.setDuetTemperature(host, port, apiKey, component, temperature); case "repetier": return await this.setRepetierTemperature(host, port, apiKey, component, temperature); case "bambu": return await this.setBambuTemperature(host, bambuSerial, bambuToken, component, temperature); default: throw new Error(`Unsupported printer type: ${type}`); } } // Bambu Labs API Implementation async getBambuStatus(host: string, serial: string, token: string) { const printer = this.getBambuPrinter(host, serial, token); // Connect if not already connected if (!printer.isConnected) { await printer.connect(); // Wait for initial state await printer.awaitInitialState(10000); // 10 second timeout } return printer.getState(); } async getBambuFiles(host: string, serial: string, token: string) { const printer = this.getBambuPrinter(host, serial, token); // Connect if not already connected if (!printer.isConnected) { await printer.connect(); } // Using the manipulateFiles API to list files const fileList: string[] = []; await printer.manipulateFiles(async (context: BambuFTP) => { const files = await context.readDir("gcodes"); fileList.push(...files); }); return { files: fileList }; } async getBambuFile(host: string, serial: string, token: string, filename: string) { // Bambu doesn't have a direct API to get file content // Instead, this returns metadata about the file by confirming it exists const printer = this.getBambuPrinter(host, serial, token); // Connect if not already connected if (!printer.isConnected) { await printer.connect(); } let fileExists = false; await printer.manipulateFiles(async (context: BambuFTP) => { const files = await context.readDir("gcodes"); fileExists = files.includes(filename); }); if (!fileExists) { throw new Error(`File not found: ${filename}`); } return { name: filename, exists: true }; } async uploadToBambu(host: string, serial: string, token: string, filePath: string, filename: string, print: boolean) { const printer = this.getBambuPrinter(host, serial, token); // Connect if not already connected if (!printer.isConnected) { await printer.connect(); } // Upload file via FTP await printer.manipulateFiles(async (context: BambuFTP) => { await context.sendFile(filePath, `gcodes/${filename}`); }); if (print) { // To start a print directly, we would need more info // This is a placeholder - starting a print needs more details throw new Error("Direct printing of uploaded files is not implemented yet"); } return { status: "success", message: `File ${filename} uploaded successfully` }; } async startBambuJob(host: string, serial: string, token: string, filename: string) { // Starting a job requires more information for Bambu printers // This is a simplified implementation - in reality, we need // more details about the 3MF project file structure throw new Error("Starting a print job on Bambu printers requires more information about the file structure"); } async cancelBambuJob(host: string, serial: string, token: string) { const printer = this.getBambuPrinter(host, serial, token); // Connect if not already connected if (!printer.isConnected) { await printer.connect(); } // Cancel print printer.stop(); return { status: "success", message: "Print job cancelled" }; } async setBambuTemperature(host: string, serial: string, token: string, component: string, temperature: number) { // Bambu API doesn't have direct temperature controls // We would need to send custom G-code commands throw new Error("Setting temperatures directly is not supported via the Bambu API"); } // OctoPrint API Implementation async getOctoPrintStatus(host: string, port: string, apiKey: string) { const url = `http://${host}:${port}/api/printer`; const response = await this.apiClient.get(url, { headers: { "X-Api-Key": apiKey } }); return response.data; } async getOctoPrintFiles(host: string, port: string, apiKey: string) { const url = `http://${host}:${port}/api/files`; const response = await this.apiClient.get(url, { headers: { "X-Api-Key": apiKey } }); return response.data; } async getOctoPrintFile(host: string, port: string, apiKey: string, filename: string) { const url = `http://${host}:${port}/api/files/local/${filename}`; const response = await this.apiClient.get(url, { headers: { "X-Api-Key": apiKey } }); return response.data; } async uploadToOctoPrint(host: string, port: string, apiKey: string, filePath: string, filename: string, print: boolean) { const url = `http://${host}:${port}/api/files/local`; const formData = new FormData(); formData.append("file", fs.createReadStream(filePath)); formData.append("filename", filename); if (print) { formData.append("print", "true"); } const response = await this.apiClient.post(url, formData as any, { headers: { "X-Api-Key": apiKey, ...formData.getHeaders() } }); return response.data; } async startOctoPrintJob(host: string, port: string, apiKey: string, filename: string) { const url = `http://${host}:${port}/api/files/local/${filename}`; const response = await this.apiClient.post(url, { command: "select", print: true } as any, { headers: { "X-Api-Key": apiKey, "Content-Type": "application/json" } }); return response.data; } async cancelOctoPrintJob(host: string, port: string, apiKey: string) { const url = `http://${host}:${port}/api/job`; const response = await this.apiClient.post(url, { command: "cancel" } as any, { headers: { "X-Api-Key": apiKey, "Content-Type": "application/json" } }); return response.data; } async setOctoPrintTemperature(host: string, port: string, apiKey: string, component: string, temperature: number) { let url = `http://${host}:${port}/api/printer/tool`; const data: Record<string, any> = {}; if (component === "bed") { data.command = "target"; data.target = temperature; url = `http://${host}:${port}/api/printer/bed`; } else if (component.startsWith("extruder")) { data.command = "target"; data.targets = {}; data.targets[component] = temperature; } else { throw new Error(`Unsupported component: ${component}`); } const response = await this.apiClient.post(url, data as any, { headers: { "X-Api-Key": apiKey, "Content-Type": "application/json" } }); return response.data; } // Klipper API Implementation (via Moonraker) async getKlipperStatus(host: string, port: string, apiKey: string) { const url = `http://${host}:${port}/printer/info`; const response = await this.apiClient.get(url); return response.data; } async getKlipperFiles(host: string, port: string, apiKey: string) { const url = `http://${host}:${port}/server/files/list`; const response = await this.apiClient.get(url); return response.data; } async getKlipperFile(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); return response.data; } async uploadToKlipper(host: string, port: string, apiKey: string, filePath: string, filename: string, print: boolean) { const url = `http://${host}:${port}/server/files/upload`; const formData = new FormData(); formData.append("file", fs.createReadStream(filePath)); formData.append("filename", filename); const response = await this.apiClient.post(url, formData as any, { headers: { ...formData.getHeaders() } }); if (print && response.data.result === "success") { await this.startKlipperJob(host, port, apiKey, filename); } return response.data; } async startKlipperJob(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); return response.data; } async cancelKlipperJob(host: string, port: string, apiKey: string) { const url = `http://${host}:${port}/printer/print/cancel`; const response = await this.apiClient.post(url, null as any); return response.data; } async setKlipperTemperature(host: string, port: string, apiKey: string, component: string, temperature: number) { const url = `http://${host}:${port}/printer/gcode/script`; let gcode; if (component === "bed") { gcode = `SET_HEATER_TEMPERATURE HEATER=heater_bed TARGET=${temperature}`; } else if (component === "extruder") { gcode = `SET_HEATER_TEMPERATURE HEATER=extruder TARGET=${temperature}`; } else { throw new Error(`Unsupported component: ${component}`); } const response = await this.apiClient.post(url, { script: gcode } as any); return response.data; } // Duet API Implementation async getDuetStatus(host: string, port: string, apiKey: string) { const url = `http://${host}:${port}/machine/status`; const response = await this.apiClient.get(url); return response.data; } async getDuetFiles(host: string, port: string, apiKey: string) { const url = `http://${host}:${port}/machine/file-list`; const response = await this.apiClient.get(url); return response.data; } async getDuetFile(host: string, port: string, apiKey: string, filename: string) { const url = `http://${host}:${port}/machine/file/${encodeURIComponent(filename)}`; const response = await this.apiClient.get(url); return response.data; } async uploadToDuet(host: string, port: string, apiKey: string, filePath: string, filename: string, print: boolean) { const url = `http://${host}:${port}/machine/file-upload`; const formData = new FormData(); formData.append("file", fs.createReadStream(filePath)); formData.append("filename", filename); const response = await this.apiClient.post(url, formData as any, { headers: { ...formData.getHeaders() } }); if (print && response.data.err === 0) { await this.startDuetJob(host, port, apiKey, filename); } return response.data; } async startDuetJob(host: string, port: string, apiKey: string, filename: string) { const url = `http://${host}:${port}/machine/code`; const response = await this.apiClient.post(url, { code: `M32 "${filename}"` } as any); return response.data; } async cancelDuetJob(host: string, port: string, apiKey: string) { const url = `http://${host}:${port}/machine/code`; const response = await this.apiClient.post(url, { code: "M0" } as any); return response.data; } async setDuetTemperature(host: string, port: string, apiKey: string, component: string, temperature: number) { const url = `http://${host}:${port}/machine/code`; let gcode; if (component === "bed") { gcode = `M140 S${temperature}`; } else if (component === "extruder") { gcode = `M104 S${temperature}`; } else { throw new Error(`Unsupported component: ${component}`); } const response = await this.apiClient.post(url, { code: gcode } as any); return response.data; } // Repetier API Implementation async getRepetierStatus(host: string, port: string, apiKey: string) { const url = `http://${host}:${port}/printer/api/?a=getPrinterInfo&apikey=${apiKey}`; const response = await this.apiClient.get(url); return response.data; } async getRepetierFiles(host: string, port: string, apiKey: string) { const url = `http://${host}:${port}/printer/api/?a=ls&apikey=${apiKey}`; const response = await this.apiClient.get(url); return response.data; } async getRepetierFile(host: string, port: string, apiKey: string, filename: string) { const url = `http://${host}:${port}/printer/api/?a=getFileInfo&apikey=${apiKey}&filename=${encodeURIComponent(filename)}`; const response = await this.apiClient.get(url); return response.data; } async uploadToRepetier(host: string, port: string, apiKey: string, filePath: string, filename: string, print: boolean) { const url = `http://${host}:${port}/printer/api/`; const formData = new FormData(); formData.append("a", "upload"); formData.append("apikey", apiKey); formData.append("filename", filename); formData.append("print", print ? "1" : "0"); formData.append("file", fs.createReadStream(filePath)); const response = await this.apiClient.post(url, formData as any, { headers: { ...formData.getHeaders() } }); return response.data; } async startRepetierJob(host: string, port: string, apiKey: string, filename: string) { const url = `http://${host}:${port}/printer/api/?a=startJob&apikey=${apiKey}&filename=${encodeURIComponent(filename)}`; const response = await this.apiClient.get(url); return response.data; } async cancelRepetierJob(host: string, port: string, apiKey: string) { const url = `http://${host}:${port}/printer/api/?a=stopJob&apikey=${apiKey}`; const response = await this.apiClient.get(url); return response.data; } async setRepetierTemperature(host: string, port: string, apiKey: string, component: string, temperature: number) { let url; if (component === "bed") { url = `http://${host}:${port}/printer/api/?a=setBedTemp&apikey=${apiKey}&temp=${temperature}`; } else if (component === "extruder") { url = `http://${host}:${port}/printer/api/?a=setExtruderTemp&apikey=${apiKey}&temp=${temperature}`; } else { throw new Error(`Unsupported component: ${component}`); } const response = await this.apiClient.get(url); return response.data; } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error("3D Printer MCP server running on stdio transport"); } } const server = new ThreeDPrinterMCPServer(); server.run().catch(console.error);