Skip to main content
Glama

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
NameRequiredDescriptionDefault
robot_ipYesRobot IP address (e.g., '192.168.1.100')
file_pathYesPath to protocol file (.py or .json)
protocol_kindNostandard
keyNoOptional client tracking key (~100 chars)
run_time_parametersNoOptional runtime parameter values

Implementation Reference

  • 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'}`
          }]
        };
      }
    }
  • 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":

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/yerbymatey/opentrons-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server