Skip to main content
Glama
adamlj

Android Screenshot MCP Server

by adamlj

connect_and_screenshot

Connect to an Android device and capture screenshots for UI debugging and visual inspection during app development.

Instructions

Connect to an Android device and take a screenshot. Use when no device is connected.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
deviceIPYesIP address of the Android device. Can include port like "192.168.1.100:12345"
debugPortNoWireless debugging port (different from pairing port)
pairingPortNoPairing port shown on device for initial pairing
pairingCodeNoPairing code shown on device for initial pairing
outputPathNoPath where to save the screenshot (optional)

Implementation Reference

  • Primary handler function that orchestrates device connection (parsing IP/port, reconnecting saved, pairing if needed) and screenshot capture for the 'connect_and_screenshot' tool.
    async function connectAndTakeScreenshot(args) {
      // Prevent overlapping screenshot operations
      if (isScreenshotInProgress) {
        return {
          content: [
            {
              type: 'text',
              text: 'Screenshot operation already in progress. Please wait for it to complete.',
            },
          ],
        };
      }
    
      isScreenshotInProgress = true;
      
      try {
        let deviceIP = args?.deviceIP;
        let debugPort = args?.debugPort;
        const pairingPort = args?.pairingPort;
        const pairingCode = args?.pairingCode;
        const outputPath = args?.outputPath;
        
        // If no device info provided, try to use saved connection
        if (!deviceIP && !debugPort) {
          const reconnected = await tryReconnectToSaved();
          if (reconnected) {
            return await captureScreenshotFromDevice(outputPath);
          }
        }
        
        // Parse IP:PORT format
        if (deviceIP && deviceIP.includes(':')) {
          const parts = deviceIP.split(':');
          deviceIP = parts[0];
          debugPort = debugPort || parseInt(parts[1]);
        }
        
        // Check existing connections first
        const devices = await getConnectedDevices();
        
        if (deviceIP && debugPort) {
          const deviceAddress = `${deviceIP}:${debugPort}`;
          const device = devices.find(d => d.address === deviceAddress);
          
          if (device && device.status === 'device') {
            return await captureScreenshotFromDevice(outputPath);
          } else if (device && device.status === 'offline') {
            await execAsync(`adb disconnect ${deviceAddress}`);
            await new Promise(resolve => setTimeout(resolve, 1000));
          }
        } else {
          const onlineDevice = devices.find(d => d.status === 'device');
          if (onlineDevice) {
            return await captureScreenshotFromDevice(outputPath);
          }
        }
        
        // Try to connect
        if (deviceIP && debugPort) {
          try {
            await connectToDevice(deviceIP, debugPort);
            return await captureScreenshotFromDevice(outputPath);
          } catch (error) {
            if (pairingPort && pairingCode) {
              try {
                await pairAndConnect(deviceIP, debugPort, pairingPort, pairingCode);
                return await captureScreenshotFromDevice(outputPath);
              } catch (pairingError) {
                return {
                  isError: true,
                  content: [
                    {
                      type: 'text',
                      text: `Failed to pair and connect: ${pairingError.message}. Please check your pairing code and ports.`,
                    },
                  ],
                };
              }
            } else {
              return {
                content: [
                  {
                    type: 'text',
                    text: `Failed to connect to ${deviceIP}:${debugPort}. I need the pairing code and pairing port. Please go to Settings > Developer Options > Wireless debugging > Pair device with pairing code, then provide me with the 6-digit pairing code and pairing port so I can connect automatically.`,
                  },
                ],
              };
            }
          }
        }
        
        return {
          content: [
            {
              type: 'text',
              text: 'I need your Android device connection details to take a screenshot. Please provide:\n\n1. IP address and port from Settings > Developer Options > Wireless debugging (e.g., "192.168.1.100:12345")\n2. If first time connecting, also provide the pairing code and pairing port from "Pair device with pairing code"\n\nOnce you give me these details, I\'ll automatically connect and take the screenshot.',
            },
          ],
        };
      } catch (error) {
        return {
          isError: true,
          content: [
            {
              type: 'text',
              text: `Error: ${error.message}`,
            },
          ],
        };
      } finally {
        isScreenshotInProgress = false;
      }
    }
  • src/index.js:146-181 (registration)
    Registration of the 'connect_and_screenshot' tool in the ListToolsRequestSchema response, including name, description, and input schema.
    {
      name: 'connect_and_screenshot',
      description: 'Connect to an Android device and take a screenshot. Use when no device is connected.',
      inputSchema: {
        type: 'object',
        properties: {
          deviceIP: {
            type: 'string',
            description: 'IP address of the Android device. Can include port like "192.168.1.100:12345"',
            pattern: '^(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})(:\\d{1,5})?$',
          },
          debugPort: {
            type: 'number',
            description: 'Wireless debugging port (different from pairing port)',
            minimum: 1,
            maximum: 65535,
          },
          pairingPort: {
            type: 'number',
            description: 'Pairing port shown on device for initial pairing',
            minimum: 1,
            maximum: 65535,
          },
          pairingCode: {
            type: 'string',
            description: 'Pairing code shown on device for initial pairing',
            pattern: '^\\d{6}$',
          },
          outputPath: {
            type: 'string',
            description: 'Path where to save the screenshot (optional)',
          },
        },
        required: ['deviceIP'],
      },
    },
  • src/index.js:206-207 (registration)
    Handler dispatch registration in the CallToolRequestSchema switch statement.
    case 'connect_and_screenshot':
      return await connectAndTakeScreenshot(toolArgs);
  • Key helper function that performs the actual screenshot capture using ADB screencap and pull, handles large file fallback to disk save, base64 encoding for small images, and cleanup.
    async function captureScreenshotFromDevice(outputPath) {
      let tempPath = null;
      let finalOutputPath = null;
      let targetDevice = null;
      
      try {
        // Get the first connected device
        const devices = await getConnectedDevices();
        const onlineDevices = devices.filter(d => d.status === 'device');
        
        if (onlineDevices.length === 0) {
          throw new Error('No connected Android devices found');
        }
        
        // Use the first available device
        targetDevice = onlineDevices[0].address;
        
        const timestamp = Date.now();
        tempPath = `/sdcard/temp_screenshot_${timestamp}.png`;
        const defaultOutputPath = path.join(os.tmpdir(), `screenshot-${timestamp}.png`);
        finalOutputPath = outputPath || defaultOutputPath;
    
        // Take screenshot with unique temp filename, specifying the device
        await execAsync(`adb -s ${targetDevice} shell screencap -p ${tempPath}`);
        
        // Pull screenshot to local machine, specifying the device
        await execAsync(`adb -s ${targetDevice} pull ${tempPath} "${finalOutputPath}"`);
    
        // Get file stats first
        const stats = await fs.stat(finalOutputPath);
        const fileSizeKB = (stats.size / 1024).toFixed(2);
        
        // Clean up temp file on device immediately
        try {
          await execAsync(`adb -s ${targetDevice} shell rm ${tempPath}`);
        } catch (cleanupError) {
        }
    
        // For images over 2MB, skip base64 encoding entirely to prevent call stack overflow
        if (stats.size > 2 * 1024 * 1024) { // 2MB threshold
          return {
            content: [
              {
                type: 'text',
                text: `Screenshot captured (${fileSizeKB} KB) and saved to: ${finalOutputPath}\n\nImage too large for inline display (>2MB). File saved to disk. This typically happens with photo wallpapers - consider using a simpler background.`,
              },
            ],
          };
        }
    
        // For smaller images, try base64 encoding with error handling
        let base64Image;
        try {
          const imageBuffer = await fs.readFile(finalOutputPath);
          base64Image = imageBuffer.toString('base64');
          // Clear buffer immediately
          imageBuffer.fill(0);
        } catch (readError) {
          // If base64 conversion fails, just return file path
          return {
            content: [
              {
                type: 'text',
                text: `Screenshot captured (${fileSizeKB} KB) and saved to: ${finalOutputPath}\n\nCould not display image inline: ${readError.message}`,
              },
            ],
          };
        }
        
        // Clean up temp file if it was auto-generated
        if (!outputPath) {
          try {
            await fs.unlink(finalOutputPath);
          } catch (unlinkError) {
          }
        }
    
        return {
          content: [
            {
              type: 'text',
              text: `Screenshot captured (${fileSizeKB} KB) and saved to: ${finalOutputPath}`,
            },
            {
              type: 'image',
              data: base64Image,
              mimeType: 'image/png',
            },
          ],
        };
      } catch (error) {
        // Emergency cleanup on error
        if (tempPath && targetDevice) {
          try {
            await execAsync(`adb -s ${targetDevice} shell rm ${tempPath}`);
          } catch (e) {
          }
        }
        
        if (finalOutputPath && !outputPath) {
          try {
            await fs.unlink(finalOutputPath);
          } catch (e) {
          }
        }
    
        return {
          isError: true,
          content: [
            {
              type: 'text',
              text: `Failed to capture screenshot: ${error.message}`,
            },
          ],
        };
      }
    }
  • Helper function to connect to Android device via ADB and verify connection, saves successful connection.
    async function connectToDevice(deviceIP, debugPort) {
      const deviceAddress = `${deviceIP}:${debugPort}`;
      await execAsync(`adb connect ${deviceAddress}`);
      await new Promise(resolve => setTimeout(resolve, 2000));
      
      const { stdout } = await execAsync('adb devices');
      const isConnected = stdout.includes(deviceAddress) && 
                         stdout.includes('\tdevice');
      
      if (!isConnected) {
        throw new Error('Failed to establish connection. Device might need pairing.');
      }
      
      // Save successful connection
      await saveLastConnection(deviceAddress);
    }
Behavior2/5

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

No annotations are provided, so the description carries the full burden of behavioral disclosure. It mentions connecting and taking a screenshot but doesn't describe what happens during connection (e.g., pairing process, authentication needs, potential errors), the screenshot format, or any rate limits. For a tool with multiple parameters and no annotations, this is a significant gap.

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 two sentences, front-loaded with the core action and followed by usage guidance. Every sentence earns its place with no wasted words, making it highly efficient and well-structured.

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

Completeness3/5

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

Given the complexity (5 parameters, no output schema, no annotations), the description is somewhat complete but lacks details on behavioral aspects like connection process and screenshot output. It covers purpose and usage well but doesn't fully compensate for the missing annotations and output schema, leaving gaps for an AI agent.

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 100%, so the schema already documents all parameters thoroughly. The description doesn't add any meaning beyond what the schema provides, such as explaining how parameters interact (e.g., pairingPort and pairingCode are needed together). Baseline 3 is appropriate when the schema does the heavy lifting.

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 ('Connect to an Android device and take a screenshot') and resource ('Android device'), making the purpose explicit. However, it doesn't distinguish this from the sibling 'screenshot' tool, which might handle already-connected devices, so it misses full sibling differentiation.

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

Usage Guidelines5/5

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

The description provides explicit guidance on when to use this tool ('Use when no device is connected'), which clearly distinguishes it from the sibling 'screenshot' tool that likely handles connected devices. This gives clear context and 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/adamlj/android-screenshot-mcp'

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