upload_protocol
Transfer protocol files to Opentrons robots for automated liquid handling experiments. Specify robot IP and file path to upload Python or JSON protocols.
Instructions
Upload a protocol file to an Opentrons robot
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| robot_ip | Yes | Robot IP address (e.g., '192.168.1.100') | |
| file_path | Yes | Path to protocol file (.py or .json) | |
| protocol_kind | No | standard | |
| key | No | Optional client tracking key (~100 chars) | |
| run_time_parameters | No | Optional runtime parameter values |
Implementation Reference
- index.js:1507-1659 (handler)The main handler function that executes the upload_protocol tool. It validates the input file, constructs a curl command to POST multipart form data to the Opentrons robot's /protocols endpoint, handles errors, and returns formatted success/error responses.async uploadProtocol(args) { const { robot_ip, file_path, support_files = [], protocol_kind = "standard" } = args; try { // Import required modules const fs = await import('fs'); const path = await import('path'); // Check if main protocol file exists and is readable if (!fs.existsSync(file_path)) { return { content: [{ type: "text", text: `❌ **File not found**: ${file_path}\n\nPlease check:\n- File path is correct\n- File exists\n- You have read permissions` }] }; } // Check file permissions try { fs.accessSync(file_path, fs.constants.R_OK); } catch (err) { return { content: [{ type: "text", text: `❌ **Permission denied**: Cannot read ${file_path}\n\nTry:\n- \`chmod 644 "${file_path}"\`\n- Moving file to a readable location\n- Running with proper permissions` }] }; } // Validate file extension const ext = path.extname(file_path).toLowerCase(); if (!['.py', '.json'].includes(ext)) { return { content: [{ type: "text", text: `❌ **Invalid file type**: ${ext}\n\nOpentrons protocols must be:\n- Python files (.py)\n- JSON protocol files (.json)` }] }; } // For now, let's use curl instead of trying to fight FormData const { exec } = await import('child_process'); const { promisify } = await import('util'); const execAsync = promisify(exec); // Build curl command let curlCmd = `curl -X POST "http://${robot_ip}:31950/protocols"`; curlCmd += ` -H "Opentrons-Version: *"`; curlCmd += ` -H "accept: application/json"`; curlCmd += ` -F "files=@${file_path}"`; // Add support files for (const supportPath of support_files) { if (fs.existsSync(supportPath)) { curlCmd += ` -F "supportFiles=@${supportPath}"`; } } // Add protocol kind if not standard if (protocol_kind !== "standard") { curlCmd += ` -F "protocolKind=${protocol_kind}"`; } console.error(`Executing: ${curlCmd}`); const { stdout, stderr } = await execAsync(curlCmd); if (stderr && !stderr.includes('% Total')) { throw new Error(`Curl error: ${stderr}`); } let responseData; try { responseData = JSON.parse(stdout); } catch (parseErr) { return { content: [{ type: "text", text: `❌ **Upload failed** - Invalid response from robot\n\n**Response**: ${stdout.slice(0, 500)}${stdout.length > 500 ? '...' : ''}\n\n**Possible issues**:\n- Robot not reachable at ${robot_ip}:31950\n- Robot server not running\n- Network connectivity problems` }] }; } // Check for errors in response if (responseData.errors || (responseData.data && responseData.data.errors)) { const errors = responseData.errors || responseData.data.errors || []; let errorDetails = `❌ **Upload failed**\n\n`; if (errors.length > 0) { errorDetails += `**Protocol Errors**:\n${errors.map(err => `- ${err.detail || err.message || err}`).join('\n')}\n`; } else { errorDetails += `**Error**: ${responseData.message || 'Unknown error'}\n`; } errorDetails += `\n**Troubleshooting**:\n`; errorDetails += `- Check robot is connected: \`curl http://${robot_ip}:31950/health\`\n`; errorDetails += `- Verify protocol file syntax\n`; errorDetails += `- Try uploading via Opentrons App first\n`; return { content: [{ type: "text", text: errorDetails }] }; } // Success response const protocolId = responseData?.data?.id; const protocolName = responseData?.data?.metadata?.protocolName || path.basename(file_path); const apiVersion = responseData?.data?.metadata?.apiLevel || 'Unknown'; let successMsg = `✅ **Protocol uploaded successfully!**\n\n`; successMsg += `**Protocol ID**: \`${protocolId}\`\n`; successMsg += `**Name**: ${protocolName}\n`; successMsg += `**API Version**: ${apiVersion}\n`; successMsg += `**File**: ${path.basename(file_path)}\n`; if (support_files.length > 0) { successMsg += `**Support Files**: ${support_files.length} files\n`; } successMsg += `\n**Next Steps**:\n`; successMsg += `1. Create a run: \`POST /runs\` with \`{"data": {"protocolId": "${protocolId}"}}\`\n`; successMsg += `2. Start run: \`POST /runs/{run_id}/actions\` with \`{"data": {"actionType": "play"}}\`\n`; // Check for analysis warnings if (responseData?.data?.analyses?.length > 0) { const analysis = responseData.data.analyses[0]; if (analysis.status === 'completed' && analysis.result === 'ok') { successMsg += `\n✅ **Protocol analysis passed** - Ready to run\n`; } else if (analysis.status === 'completed' && analysis.result === 'error') { successMsg += `\n⚠️ **Protocol analysis found issues** - Check protocol before running\n`; } } return { content: [{ type: "text", text: successMsg }] }; } catch (error) { return { content: [{ type: "text", text: `❌ **Upload error**: ${error.message}\n\n**Possible causes**:\n- Robot not reachable at ${robot_ip}:31950\n- Network connectivity issues\n- File permissions\n- curl not installed\n\n**Debug info**: ${error.stack?.split('\n')[0] || 'No stack trace'}` }] }; } }
- index.js:112-125 (schema)The JSON schema defining the input parameters and structure for the upload_protocol tool, including required fields robot_ip and file_path.name: "upload_protocol", description: "Upload a protocol file to an Opentrons robot", inputSchema: { type: "object", properties: { robot_ip: { type: "string", description: "Robot IP address (e.g., '192.168.1.100')" }, file_path: { type: "string", description: "Path to protocol file (.py or .json)" }, protocol_kind: { type: "string", enum: ["standard", "quick-transfer"], default: "standard" }, key: { type: "string", description: "Optional client tracking key (~100 chars)" }, run_time_parameters: { type: "object", description: "Optional runtime parameter values" } }, required: ["robot_ip", "file_path"] } },
- index.js:250-252 (registration)The dispatch case in the CallToolRequestSchema handler that routes calls to the uploadProtocol method.case "upload_protocol": return this.uploadProtocol(args); case "get_protocols":