index.ts•6.88 kB
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from "@modelcontextprotocol/sdk/types.js";
import { exec } from "child_process";
import { promisify } from "util";
const execAsync = promisify(exec);
const server = new Server(
{
name: "adb-screenshot-mcp",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// ADB WiFi Connection Tool
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "adb_connect_wifi",
description: "Connect to an Android device over WiFi using ADB",
inputSchema: {
type: "object",
properties: {
ip_address: {
type: "string",
description: "IP address of the Android device",
},
port: {
type: "string",
description: "Port number (default: 5555)",
default: "5555",
},
},
required: ["ip_address"],
},
},
{
name: "adb_screenshot",
description: "Take a screenshot of the connected Android device",
inputSchema: {
type: "object",
properties: {
output_path: {
type: "string",
description: "Local path to save the screenshot (default: screenshot.png)",
default: "screenshot.png",
},
device_id: {
type: "string",
description: "Device ID/serial (optional, uses first device if not specified)",
},
},
required: [],
},
},
{
name: "adb_list_devices",
description: "List all connected ADB devices",
inputSchema: {
type: "object",
properties: {},
required: [],
},
},
{
name: "adb_disconnect",
description: "Disconnect from a WiFi ADB device",
inputSchema: {
type: "object",
properties: {
ip_address: {
type: "string",
description: "IP address of the device to disconnect from",
},
port: {
type: "string",
description: "Port number (default: 5555)",
default: "5555",
},
},
required: ["ip_address"],
},
},
{
name: "adb_device_info",
description: "Get information about a connected device",
inputSchema: {
type: "object",
properties: {
device_id: {
type: "string",
description: "Device ID/serial (optional, uses first device if not specified)",
},
},
required: [],
},
},
],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "adb_connect_wifi": {
const { ip_address, port = "5555" } = args as {
ip_address: string;
port?: string;
};
const { stdout, stderr } = await execAsync(`adb connect ${ip_address}:${port}`);
if (stderr && stderr.includes("failed")) {
throw new McpError(ErrorCode.InternalError, `ADB connection failed: ${stderr}`);
}
return {
content: [
{
type: "text",
text: `Successfully connected to ${ip_address}:${port}\n${stdout}`,
},
],
};
}
case "adb_screenshot": {
const { output_path = "screenshot.png", device_id } = args as {
output_path?: string;
device_id?: string;
};
const deviceArg = device_id ? `-s ${device_id}` : "";
const tempPath = "/sdcard/screenshot.png";
// Take screenshot on device
await execAsync(`adb ${deviceArg} shell screencap -p ${tempPath}`);
// Pull screenshot to local machine
const { stdout, stderr } = await execAsync(`adb ${deviceArg} pull ${tempPath} ${output_path}`);
// Clean up temp file on device
await execAsync(`adb ${deviceArg} shell rm ${tempPath}`);
if (stderr && stderr.includes("error")) {
throw new McpError(ErrorCode.InternalError, `Screenshot failed: ${stderr}`);
}
return {
content: [
{
type: "text",
text: `Screenshot saved to ${output_path}`,
},
],
};
}
case "adb_list_devices": {
const { stdout } = await execAsync("adb devices");
return {
content: [
{
type: "text",
text: stdout,
},
],
};
}
case "adb_disconnect": {
const { ip_address, port = "5555" } = args as {
ip_address: string;
port?: string;
};
const { stdout, stderr } = await execAsync(`adb disconnect ${ip_address}:${port}`);
return {
content: [
{
type: "text",
text: `Disconnected from ${ip_address}:${port}\n${stdout}`,
},
],
};
}
case "adb_device_info": {
const { device_id } = args as {
device_id?: string;
};
const deviceArg = device_id ? `-s ${device_id}` : "";
const [model, android, api] = await Promise.all([
execAsync(`adb ${deviceArg} shell getprop ro.product.model`),
execAsync(`adb ${deviceArg} shell getprop ro.build.version.release`),
execAsync(`adb ${deviceArg} shell getprop ro.build.version.sdk`),
]);
const info = {
model: model.stdout.trim(),
android_version: android.stdout.trim(),
api_level: api.stdout.trim(),
};
return {
content: [
{
type: "text",
text: `Device Information:\nModel: ${info.model}\nAndroid Version: ${info.android_version}\nAPI Level: ${info.api_level}`,
},
],
};
}
default:
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
}
} catch (error) {
if (error instanceof McpError) {
throw error;
}
throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${error}`);
}
});
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});