Skip to main content
Glama
adamlj

Android Screenshot MCP Server

by adamlj

screenshot

Capture screenshots from Android devices over WiFi for UI debugging and visual inspection during app development.

Instructions

Take a screenshot of any connected Android device. Uses existing ADB connections or connects wirelessly if device info provided.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
deviceIPNoIP 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

  • Main handler function for the 'screenshot' tool. Manages device connections, reconnections, offline cleanup, and delegates to screenshot capture.
    async function takeScreenshot(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 {
        const outputPath = args?.outputPath;
        
        // Check for existing connections
        const devices = await getConnectedDevices();
        
        // Handle offline devices by disconnecting them
        await cleanupOfflineDevices(devices);
        
        // Check for online devices after cleanup
        const onlineDevices = devices.filter(d => d.status === 'device');
        if (onlineDevices.length === 0) {
          // Try to reconnect to saved device
          const reconnected = await tryReconnectToSaved();
          if (reconnected) {
            // Successfully reconnected, take screenshot
            return await captureScreenshotFromDevice(outputPath);
          }
          
          return {
            content: [
              {
                type: 'text',
                text: 'No Android device connected. I need your device connection details to take a screenshot. Please provide:\n\n1. IP address and debug port from Settings > Developer Options > Wireless debugging\n2. Pairing code (6 digits) and pairing port from "Pair device with pairing code"\n\nOnce you give me all these details, I\'ll automatically connect and take the screenshot.',
              },
            ],
          };
        }
        
        // Capture screenshot
        return await captureScreenshotFromDevice(outputPath);
        
      } catch (error) {
        return {
          isError: true,
          content: [
            {
              type: 'text',
              text: `Failed to check device status: ${error.message}`,
            },
          ],
        };
      } finally {
        isScreenshotInProgress = false;
      }
    }
  • Input schema defining parameters for the 'screenshot' tool: deviceIP, debugPort, pairingPort, pairingCode, outputPath.
    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: [],
    },
  • src/index.js:110-145 (registration)
    Tool registration in ListTools response, including name, description, and schema.
    {
      name: 'screenshot',
      description: 'Take a screenshot of any connected Android device. Uses existing ADB connections or connects wirelessly if device info provided.',
      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: [],
      },
    },
  • src/index.js:203-205 (registration)
    Dispatch/registration in CallToolRequestSchema handler switch statement calling takeScreenshot for 'screenshot' tool.
    switch (toolName) {
      case 'screenshot':
        return await takeScreenshot(toolArgs);
  • Core helper function that performs the actual ADB screencap, pull, base64 encoding (if small), and returns image content or file path.
    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}`,
            },
          ],
        };
      }
    }
Behavior3/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 explains the connection behavior (uses existing ADB or wireless with device info) and implies a write operation (saving a screenshot), but doesn't mention authentication requirements, rate limits, error conditions, or what happens if no outputPath is specified. It adds some context but leaves 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 perfectly concise with two sentences that each earn their place: the first states the core purpose, the second explains the connection behavior. No wasted words, well-structured and front-loaded with the main action.

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?

For a mutation tool with no annotations and no output schema, the description provides adequate purpose and connection context but lacks details about the screenshot format, error handling, authentication needs, or what happens when parameters are omitted. Given the complexity of device interaction and 5 parameters, more behavioral context would be helpful.

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 fully documents all 5 parameters. The description adds no specific parameter information beyond what's in the schema, though it implies the optional nature of parameters by stating 'if device info provided' and marking outputPath as optional. Baseline 3 is appropriate when schema does the heavy lifting.

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

Purpose5/5

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

The description clearly states the specific action ('Take a screenshot') and target resource ('any connected Android device'), distinguishing it from the sibling 'connect_and_screenshot' by emphasizing it uses existing connections or wireless setup rather than establishing a new connection as part of the operation.

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

Usage Guidelines4/5

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

The description provides clear context on when to use this tool: for taking screenshots on Android devices with existing ADB connections or wireless connections if device info is provided. It distinguishes from 'connect_and_screenshot' by implying this tool doesn't handle connection establishment, but doesn't explicitly state when not to use it or name alternatives beyond the sibling.

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