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":
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. It states the action ('Upload') but doesn't describe what happens after upload (e.g., whether it triggers execution, requires confirmation, or has side effects like overwriting existing protocols). It also omits details on authentication needs, rate limits, or error handling, leaving significant gaps for a mutation tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, efficient sentence that directly states the tool's purpose without unnecessary words. It's appropriately sized and front-loaded, with every word earning its place in conveying the core functionality.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a mutation tool with 5 parameters, no annotations, and no output schema, the description is incomplete. It doesn't address behavioral aspects like side effects, success/failure responses, or integration with sibling tools (e.g., what happens after upload relative to 'control_run'). The lack of output schema increases the need for more context, which isn't provided.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 80%, providing a strong baseline. The description adds no parameter-specific information beyond what the schema already documents (e.g., file types, robot IP format, optional keys). It doesn't explain interactions between parameters like 'protocol_kind' and 'run_time_parameters', so it meets but doesn't exceed the baseline expectation.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the action ('Upload') and resource ('a protocol file to an Opentrons robot'), providing a specific verb+resource combination. It distinguishes from siblings like 'get_protocols' (read) and 'create_run' (different action), though it doesn't explicitly mention these distinctions in the description itself.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no guidance on when to use this tool versus alternatives like 'create_run' or 'control_run', nor does it mention prerequisites such as robot connectivity or file format requirements. It lacks explicit when/when-not instructions or named alternatives.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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