codingbaby-mobile
Version:
MCP Mobile Agent Server - Node.js implementation for mobile device control via ADB
1,042 lines (916 loc) • 32.4 kB
JavaScript
/**
* Mobile Agent MCP Server - Node.js Implementation
* 移动设备操作 MCP 服务器 - Node.js 实现
*
* 提供8个核心工具来操作移动设备:
* 1. mobile_set_adb_path - 设置ADB路径
* 2. mobile_screenshot - 截图
* 3. mobile_tap - 点击
* 4. mobile_swipe - 滑动
* 5. mobile_type - 输入文字
* 6. mobile_back - 返回
* 7. mobile_home - 回到主页
* 8. mobile_get_screen_info - 获取屏幕信息
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { spawn, exec } from 'child_process';
import { promisify } from 'util';
import * as fs from 'fs/promises';
import { existsSync, readFileSync } from 'fs';
import * as path from 'path';
import * as os from 'os';
import sharp from 'sharp';
const execAsync = promisify(exec);
// 全局配置 - 使用绝对路径避免工作目录问题
const SCREENSHOT_DIR = path.join(os.tmpdir(), 'mobile_agent_screenshots');
const TEMP_DIR = path.join(os.tmpdir(), 'mobile_agent_temp');
// 添加全局变量保存截图信息
if (!global.latestScreenshotInfo) {
global.latestScreenshotInfo = null;
}
/**
* 移动代理错误类
*/
class MobileAgentError extends Error {
constructor(message) {
super(message);
this.name = 'MobileAgentError';
}
}
/**
* ADB连接管理器
*/
class ADBManager {
constructor() {
this.adbPath = null;
this.initialized = false;
}
/**
* 确保必要的目录存在
*/
async ensureDirectories() {
if (this.initialized) return;
try {
await fs.mkdir(SCREENSHOT_DIR, { recursive: true });
await fs.mkdir(TEMP_DIR, { recursive: true });
this.initialized = true;
} catch (error) {
// 静默处理,不输出到 stderr,避免影响 MCP 连接
throw new MobileAgentError(`创建目录失败: ${error.message}`);
}
}
/**
* 设置ADB路径
*/
setAdbPath(adbPath) {
this.adbPath = adbPath;
}
/**
* 检查ADB是否可用
*/
async checkAdbAvailable() {
await this.ensureDirectories(); // 确保初始化
if (!this.adbPath) {
throw new MobileAgentError('ADB路径未设置,请在 .cursor/mcp.json 中配置 "config": { "adbPath": "/path/to/adb" }');
}
}
/**
* 执行ADB命令
*/
async runAdbCommand(command) {
await this.checkAdbAvailable();
const fullCommand = `"${this.adbPath}" ${command}`;
try {
const { stdout, stderr } = await execAsync(fullCommand);
return {
success: true,
stdout,
stderr,
command: fullCommand
};
} catch (error) {
return {
success: false,
stdout: error.stdout || '',
stderr: error.stderr || error.message,
command: fullCommand,
code: error.code
};
}
}
/**
* 测试ADB连接
*/
async testConnection() {
try {
const result = await this.runAdbCommand('devices');
if (result.success) {
const lines = result.stdout.split('\n').slice(1).filter(line => line.trim());
const devices = lines.filter(line => line.includes('device')).length;
if (devices > 0) {
return {
status: 'success',
message: `连接成功,发现 ${devices} 个设备`,
devices: lines
};
} else {
return {
status: 'warning',
message: 'ADB运行正常,但未发现连接的设备'
};
}
} else {
return {
status: 'error',
message: `ADB连接失败: ${result.stderr}`
};
}
} catch (error) {
return {
status: 'error',
message: `测试连接时出错: ${error.message}`
};
}
}
/**
* 截取屏幕截图
*/
async takeScreenshot() {
// 确保截图目录存在
await this.ensureDirectories();
// 删除旧截图
await this.runAdbCommand('shell rm /sdcard/screenshot.png');
await new Promise(resolve => setTimeout(resolve, 500));
// 截图
const screencapResult = await this.runAdbCommand('shell screencap -p /sdcard/screenshot.png');
if (!screencapResult.success) {
throw new MobileAgentError(`截图失败: ${screencapResult.stderr}`);
}
await new Promise(resolve => setTimeout(resolve, 500));
// 生成时间戳文件名避免冲突
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const filename = `screenshot-${timestamp}.png`;
const localPath = path.join(SCREENSHOT_DIR, filename);
// 拉取截图
const pullResult = await this.runAdbCommand(`pull /sdcard/screenshot.png "${localPath}"`);
if (!pullResult.success) {
throw new MobileAgentError(`拉取截图失败: ${pullResult.stderr}`);
}
// 检查文件是否存在
try {
await fs.access(localPath);
const stats = await fs.stat(localPath);
// 返回完整的文件信息
return {
path: localPath,
filename: filename,
size: stats.size,
timestamp: timestamp
};
} catch {
throw new MobileAgentError('截图文件未找到');
}
}
}
// 全局变量存储ADB路径,优先级: ENV > 配置文件 > 默认值
let globalAdbPath = process.env.ADB_PATH || "/Users/gongyun/Documents/Projects/AdMonster/MobileAgent-main/Mobile-Agent-v2/mcp_server_files/platform-tools/adb";
// 获取或创建ADB管理器实例的函数
function getAdbManager() {
const manager = new ADBManager();
if (globalAdbPath) {
manager.setAdbPath(globalAdbPath);
}
return manager;
}
// 创建MCP服务器
const server = new Server(
{
name: 'codingbaby-mobile',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
// 添加一个方法来获取传递给服务器的配置
server.getConfig = () => {
// 检查是否存在环境变量中的配置(MCP框架会传递)
const configEnv = process.env.MCP_SERVER_CONFIG;
if (configEnv) {
try {
const parsedConfig = JSON.parse(configEnv);
// 检查是否有顶层配置
if (parsedConfig.config && parsedConfig.config.adbPath) {
return parsedConfig.config;
}
// 检查是否有嵌套配置(优先使用新名称)
if (parsedConfig.mcpServers &&
parsedConfig.mcpServers['codingbaby-mobile'] &&
parsedConfig.mcpServers['codingbaby-mobile'].config &&
parsedConfig.mcpServers['codingbaby-mobile'].config.adbPath) {
return parsedConfig.mcpServers['codingbaby-mobile'].config;
}
// 向后兼容:检查旧的配置名称
if (parsedConfig.mcpServers &&
parsedConfig.mcpServers['mobile-agent-nodejs'] &&
parsedConfig.mcpServers['mobile-agent-nodejs'].config &&
parsedConfig.mcpServers['mobile-agent-nodejs'].config.adbPath) {
console.error(`从本地配置文件读取ADB路径(旧配置): ${parsedConfig.mcpServers['mobile-agent-nodejs'].config.adbPath}`);
return parsedConfig.mcpServers['mobile-agent-nodejs'].config;
}
if (parsedConfig.config && parsedConfig.config.adbPath) {
console.error(`从本地配置文件读取ADB路径: ${parsedConfig.config.adbPath}`);
return parsedConfig.config;
}
} catch (error) {
console.error('解析配置失败:', error);
}
}
// 尝试直接从本地文件系统读取配置
try {
// 检查当前工作目录下的.cursor/mcp.json
const homeDir = os.homedir();
const cursorConfigPath = path.join(process.cwd(), '.cursor', 'mcp.json');
if (existsSync(cursorConfigPath)) {
const configData = readFileSync(cursorConfigPath, 'utf8');
const parsedConfig = JSON.parse(configData);
if (parsedConfig.mcpServers &&
parsedConfig.mcpServers['codingbaby-mobile'] &&
parsedConfig.mcpServers['codingbaby-mobile'].config &&
parsedConfig.mcpServers['codingbaby-mobile'].config.adbPath) {
console.error(`从本地配置文件读取ADB路径: ${parsedConfig.mcpServers['codingbaby-mobile'].config.adbPath}`);
return parsedConfig.mcpServers['codingbaby-mobile'].config;
}
if (parsedConfig.config && parsedConfig.config.adbPath) {
console.error(`从本地配置文件读取ADB路径: ${parsedConfig.config.adbPath}`);
return parsedConfig.config;
}
}
} catch (error) {
console.error('读取本地配置文件失败:', error);
}
return null;
};
// 从配置中读取ADB路径
const config = server.getConfig();
if (config && config.adbPath) {
console.error(`从配置中读取ADB路径: ${config.adbPath}`);
globalAdbPath = config.adbPath;
// 异步验证配置的ADB路径
(async () => {
try {
const manager = getAdbManager();
const testResult = await manager.testConnection();
console.error(`ADB路径验证结果: ${testResult.status} - ${testResult.message}`);
} catch (error) {
console.error(`验证配置的ADB路径失败: ${error.message}`);
}
})();
}
// 工具列表
const TOOLS = [
{
name: 'mobile_screenshot',
description: '获取手机截图',
inputSchema: {
type: 'object',
properties: {},
required: []
}
},
{
name: 'mobile_tap',
description: '点击屏幕指定坐标。使用您在图片中看到的准确坐标,无需转换,系统将自动将坐标转换为设备实际分辨率。',
inputSchema: {
type: 'object',
properties: {
x: {
type: 'integer',
description: 'X坐标(基于图片中看到的位置)'
},
y: {
type: 'integer',
description: 'Y坐标(基于图片中看到的位置)'
}
},
required: ['x', 'y']
}
},
{
name: 'mobile_swipe',
description: '在屏幕上执行滑动操作。使用您在图片中看到的准确坐标,无需转换,系统将自动计算实际设备坐标。',
inputSchema: {
type: 'object',
properties: {
x1: {
type: 'integer',
description: '起始X坐标(基于图片中看到的位置)'
},
y1: {
type: 'integer',
description: '起始Y坐标(基于图片中看到的位置)'
},
x2: {
type: 'integer',
description: '结束X坐标(基于图片中看到的位置)'
},
y2: {
type: 'integer',
description: '结束Y坐标(基于图片中看到的位置)'
},
duration: {
type: 'integer',
description: '滑动持续时间(毫秒)',
default: 500
}
},
required: ['x1', 'y1', 'x2', 'y2']
}
},
{
name: 'mobile_type',
description: '在当前焦点位置输入文字',
inputSchema: {
type: 'object',
properties: {
text: {
type: 'string',
description: '要输入的文字内容'
}
},
required: ['text']
}
},
{
name: 'mobile_back',
description: '执行返回操作',
inputSchema: {
type: 'object',
properties: {},
required: []
}
},
{
name: 'mobile_home',
description: '回到主页',
inputSchema: {
type: 'object',
properties: {},
required: []
}
},
{
name: 'mobile_get_screen_info',
description: '获取屏幕信息和设备状态',
inputSchema: {
type: 'object',
properties: {},
required: []
}
}
];
// 工具实现函数
const toolImplementations = {
/**
* 获取手机截图
*/
async mobile_screenshot() {
try {
const screenshotPath = await getAdbManager().takeScreenshot();
// 获取文件信息
const imageBuffer = await fs.readFile(screenshotPath.path);
const fileStats = await fs.stat(screenshotPath.path);
// 获取图片尺寸
const metadata = await sharp(screenshotPath.path).metadata();
// 同时获取设备屏幕信息
const screenInfoData = await getDetailedScreenInfo();
// 根据Anthropic文档,预处理图片尺寸避免Claude自动压缩
// 设备分辨率1080x1920 (9:16纵横比),根据文档应缩放到819x1456以下
const originalWidth = metadata.width;
const originalHeight = metadata.height;
const aspectRatio = originalWidth / originalHeight;
// 计算目标尺寸,确保不超过Claude的限制
let targetWidth, targetHeight;
if (aspectRatio < 1) {
// 竖屏 (高度 > 宽度)
// 9:16纵横比的最大尺寸是819x1456
if (originalHeight > 1456) {
targetHeight = 1456;
targetWidth = Math.round(targetHeight * aspectRatio);
} else if (originalWidth > 819) {
targetWidth = 819;
targetHeight = Math.round(targetWidth / aspectRatio);
} else {
targetWidth = originalWidth;
targetHeight = originalHeight;
}
} else {
// 横屏 (宽度 >= 高度)
if (originalWidth > 1568) {
targetWidth = 1568;
targetHeight = Math.round(targetWidth / aspectRatio);
} else if (originalHeight > 1568) {
targetHeight = 1568;
targetWidth = Math.round(targetHeight * aspectRatio);
} else {
targetWidth = originalWidth;
targetHeight = originalHeight;
}
}
// 保存截图信息到全局变量
global.latestScreenshotInfo = {
originalWidth,
originalHeight,
targetWidth,
targetHeight,
scaleX: originalWidth / targetWidth,
scaleY: originalHeight / targetHeight,
timestamp: new Date().toISOString(),
path: screenshotPath.path
};
// 转换PNG为JPEG格式并缩放尺寸
const jpegPath = screenshotPath.path.replace('.png', '.jpg');
await sharp(screenshotPath.path)
.resize(targetWidth, targetHeight, {
fit: 'fill', // 保持精确尺寸,不改变纵横比
withoutEnlargement: true
})
.jpeg({ quality: 90 })
.toFile(jpegPath);
// 读取处理后的JPEG文件
const jpegBuffer = await fs.readFile(jpegPath);
const base64Data = jpegBuffer.toString('base64');
// 输出截图路径到日志
console.error(`截图已保存:
PNG路径: ${screenshotPath.path}
JPG路径: ${jpegPath}
临时目录: ${SCREENSHOT_DIR}`);
// 构建新的坐标系统说明文本
const coordinateInstructions = `
=== 屏幕截图信息 ===
📱 设备信息:
- 型号: ${screenInfoData.deviceModel || 'Unknown'}
- Android版本: ${screenInfoData.androidVersion || 'Unknown'}
- 屏幕分辨率: ${originalWidth}x${originalHeight} 像素
- 屏幕密度: ${screenInfoData.density?.dpi || 'Unknown'} DPI
📍 坐标使用说明:
- 使用mobile_tap(x, y)时,直接指定您在图片中看到的位置坐标即可
- 坐标系统: (0,0)位于左上角
- X轴: 0到${targetWidth}(从左到右)
- Y轴: 0到${targetHeight}(从上到下)
- 服务器将自动转换为设备实际坐标
💡 示例:
- 图片中心: (${Math.round(targetWidth/2)}, ${Math.round(targetHeight/2)})
- 左上角区域: (${Math.round(targetWidth/4)}, ${Math.round(targetHeight/4)})
- 右下角区域: (${Math.round(targetWidth*0.75)}, ${Math.round(targetHeight*0.75)})
📋 截图时间: ${new Date().toLocaleString('zh-CN')}
📂 截图路径信息:
- 原始PNG文件: ${screenshotPath.path}
- 处理后JPG文件: ${jpegPath}
- 截图保存目录: ${SCREENSHOT_DIR}
`;
// 返回MCP标准格式
return {
content: [
{
type: 'text',
text: coordinateInstructions
},
{
type: 'image',
data: base64Data,
mimeType: 'image/jpeg'
}
]
};
} catch (error) {
return `截图失败: ${error.message}`;
}
},
/**
* 点击屏幕
*/
async mobile_tap(args) {
try {
let { x, y } = args;
// 获取详细屏幕信息
const screenInfo = await getDetailedScreenInfo();
let scaleX = 1.0;
let scaleY = 1.0;
// 获取最近一次截图的缩放比例
try {
const latestScreenshotInfo = global.latestScreenshotInfo;
if (latestScreenshotInfo) {
scaleX = latestScreenshotInfo.originalWidth / latestScreenshotInfo.targetWidth;
scaleY = latestScreenshotInfo.originalHeight / latestScreenshotInfo.targetHeight;
// 应用缩放转换
x = Math.round(x * scaleX);
y = Math.round(y * scaleY);
console.error(`坐标转换: 输入(${args.x}, ${args.y}) -> 设备(${x}, ${y}), 缩放比例: ${scaleX.toFixed(2)}x${scaleY.toFixed(2)}`);
}
} catch (error) {
console.error(`坐标转换失败: ${error.message}`);
}
const result = await getAdbManager().runAdbCommand(`shell input tap ${x} ${y}`);
if (result.success) {
return `成功点击坐标 (${x}, ${y})${scaleX !== 1.0 ? ` [原始输入: (${args.x}, ${args.y})]` : ''}`;
} else {
return `点击失败: ${result.stderr}`;
}
} catch (error) {
return `点击操作失败: ${error.message}`;
}
},
/**
* 滑动操作
*/
async mobile_swipe(args) {
try {
let { x1, y1, x2, y2, duration = 500 } = args;
// 获取最近一次截图的缩放比例
try {
const latestScreenshotInfo = global.latestScreenshotInfo;
if (latestScreenshotInfo) {
const scaleX = latestScreenshotInfo.originalWidth / latestScreenshotInfo.targetWidth;
const scaleY = latestScreenshotInfo.originalHeight / latestScreenshotInfo.targetHeight;
// 应用缩放转换
const origX1 = x1, origY1 = y1, origX2 = x2, origY2 = y2;
x1 = Math.round(x1 * scaleX);
y1 = Math.round(y1 * scaleY);
x2 = Math.round(x2 * scaleX);
y2 = Math.round(y2 * scaleY);
console.error(`滑动坐标转换: 输入(${origX1},${origY1})→(${origX2},${origY2}) -> 设备(${x1},${y1})→(${x2},${y2}), 缩放比例: ${scaleX.toFixed(2)}x${scaleY.toFixed(2)}`);
}
} catch (error) {
console.error(`坐标转换失败: ${error.message}`);
}
const result = await getAdbManager().runAdbCommand(`shell input swipe ${x1} ${y1} ${x2} ${y2} ${duration}`);
if (result.success) {
return `成功滑动从 (${x1}, ${y1}) 到 (${x2}, ${y2}),持续时间: ${duration}ms`;
} else {
return `滑动失败: ${result.stderr}`;
}
} catch (error) {
return `滑动操作失败: ${error.message}`;
}
},
/**
* 输入文字 - 改进版本,解决空格和特殊字符问题,支持中文
*/
async mobile_type(args) {
try {
const { text } = args;
// 检查是否包含非ASCII字符(如中文)
const hasNonAscii = /[^\x00-\x7F]/.test(text);
if (hasNonAscii) {
console.error(`检测到非ASCII字符,尝试Unicode编码方法: "${text}"`);
// 方法A: Unicode转义序列
try {
let unicodeText = '';
for (let i = 0; i < text.length; i++) {
const char = text[i];
if (/[^\x00-\x7F]/.test(char)) {
// 非ASCII字符转为Unicode转义序列
unicodeText += '\\u' + char.charCodeAt(0).toString(16).padStart(4, '0');
} else {
unicodeText += char;
}
}
console.error(`Unicode编码后: "${unicodeText}"`);
const unicodeResult = await getAdbManager().runAdbCommand(`shell input text "${unicodeText}"`);
if (unicodeResult.success) {
console.error(`Unicode方法成功`);
return `成功输入文字: ${text} (使用Unicode编码)`;
}
console.error(`Unicode方法失败: ${unicodeResult.stderr}`);
} catch (error) {
console.error(`Unicode方法异常: ${error.message}`);
}
// 方法B: 使用剪贴板(如果支持)
try {
console.error(`尝试剪贴板方法`);
// 先将文本设置到剪贴板(通过am broadcast)
const setClipResult = await getAdbManager().runAdbCommand(
`shell am broadcast -a clipper.set -e text "${text}"`
);
if (setClipResult.success) {
await new Promise(resolve => setTimeout(resolve, 500));
// 然后使用Ctrl+V粘贴(keyevent)
const pasteResult = await getAdbManager().runAdbCommand('shell input keyevent 279'); // KEYCODE_PASTE
if (pasteResult.success) {
console.error(`剪贴板方法成功`);
return `成功输入文字: ${text} (使用剪贴板)`;
}
}
console.error(`剪贴板方法失败`);
} catch (error) {
console.error(`剪贴板方法异常: ${error.message}`);
}
// 方法C: 逐字符输入ASCII部分,跳过非ASCII字符
try {
console.error(`尝试混合输入法`);
let success = true;
let skippedChars = '';
for (let i = 0; i < text.length; i++) {
const char = text[i];
if (/[^\x00-\x7F]/.test(char)) {
// 跳过非ASCII字符
skippedChars += char;
continue;
}
if (char === ' ') {
const spaceResult = await getAdbManager().runAdbCommand('shell input keyevent 62');
if (!spaceResult.success) {
success = false;
break;
}
} else {
const charEscaped = char
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/"/g, '\\"')
.replace(/&/g, '\\&');
const charResult = await getAdbManager().runAdbCommand(`shell input text "${charEscaped}"`);
if (!charResult.success) {
success = false;
break;
}
}
await new Promise(resolve => setTimeout(resolve, 50));
}
if (success) {
const message = skippedChars ?
`部分输入成功,跳过了中文字符: "${skippedChars}"` :
`成功输入文字: ${text} (混合模式)`;
return message;
}
} catch (error) {
console.error(`混合输入法异常: ${error.message}`);
}
return `中文输入失败: "${text}"
可能的解决方案:
1. 使用手机的虚拟键盘手动输入中文
2. 预先在剪贴板中复制中文文本再粘贴
3. 使用支持中文的第三方ADB键盘应用
4. 当前ADB输入法对中文支持有限`;
}
// ASCII字符使用原有的方法
// 方法1: 使用更完整的转义,将空格替换为%s
let escapedText = text
.replace(/\\/g, '\\\\') // 反斜杠转义
.replace(/'/g, "\\'") // 单引号转义
.replace(/"/g, '\\"') // 双引号转义
.replace(/ /g, '%s') // 空格替换为%s
.replace(/&/g, '\\&') // &符号转义
.replace(/</g, '\\<') // <符号转义
.replace(/>/g, '\\>') // >符号转义
.replace(/\|/g, '\\|') // 管道符转义
.replace(/;/g, '\\;') // 分号转义
.replace(/\(/g, '\\(') // 左括号转义
.replace(/\)/g, '\\)'); // 右括号转义
console.error(`尝试输入文字: "${text}" -> 转义后: "${escapedText}"`);
// 方法1: 尝试使用转义后的文本
let result = await getAdbManager().runAdbCommand(`shell input text "${escapedText}"`);
if (result.success) {
console.error(`方法1成功: input text with escaping`);
return `成功输入文字: ${text}`;
}
console.error(`方法1失败: ${result.stderr}, 尝试方法2`);
// 方法2: 尝试逐字符输入 (适用于包含空格的文本)
if (text.includes(' ') || text.length > 20) {
console.error(`方法2: 逐字符输入`);
let success = true;
for (let i = 0; i < text.length; i++) {
const char = text[i];
if (char === ' ') {
// 空格使用keyevent 62 (KEYCODE_SPACE)
const spaceResult = await getAdbManager().runAdbCommand('shell input keyevent 62');
if (!spaceResult.success) {
success = false;
break;
}
} else {
// 普通字符
const charEscaped = char
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/"/g, '\\"')
.replace(/&/g, '\\&');
const charResult = await getAdbManager().runAdbCommand(`shell input text "${charEscaped}"`);
if (!charResult.success) {
success = false;
break;
}
}
// 小延迟避免输入太快
await new Promise(resolve => setTimeout(resolve, 50));
}
if (success) {
console.error(`方法2成功: 逐字符输入`);
return `成功输入文字: ${text} (使用逐字符输入)`;
}
console.error(`方法2失败, 尝试方法3`);
}
// 方法3: 使用sendevent或其他替代方法
// 对于简单文本,尝试不使用引号
if (!/[\\'"&<>|;() ]/.test(text)) {
console.error(`方法3: 简单文本无引号输入`);
const simpleResult = await getAdbManager().runAdbCommand(`shell input text ${text}`);
if (simpleResult.success) {
console.error(`方法3成功: 简单文本输入`);
return `成功输入文字: ${text} (简单模式)`;
}
console.error(`方法3失败: ${simpleResult.stderr}`);
}
// 如果所有方法都失败,返回详细错误信息
return `输入文字失败: 原文本"${text}", 所有输入方法都失败。请确保:
1. 输入框已获得焦点
2. 输入法设置正确
3. ADB连接正常
最后错误: ${result.stderr}`;
} catch (error) {
return `输入操作失败: ${error.message}`;
}
},
/**
* 返回操作
*/
async mobile_back() {
try {
const result = await getAdbManager().runAdbCommand('shell input keyevent 4');
if (result.success) {
return '成功执行返回操作';
} else {
return `返回操作失败: ${result.stderr}`;
}
} catch (error) {
return `返回操作失败: ${error.message}`;
}
},
/**
* 回到主页
*/
async mobile_home() {
try {
const result = await getAdbManager().runAdbCommand('shell input keyevent 3');
if (result.success) {
return '成功回到主页';
} else {
return `回到主页失败: ${result.stderr}`;
}
} catch (error) {
return `回到主页操作失败: ${error.message}`;
}
},
/**
* 获取屏幕信息
*/
async mobile_get_screen_info() {
try {
// 获取屏幕尺寸
const sizeResult = await getAdbManager().runAdbCommand('shell wm size');
// 获取屏幕密度
const densityResult = await getAdbManager().runAdbCommand('shell wm density');
// 获取设备信息
const deviceResult = await getAdbManager().runAdbCommand('shell getprop ro.product.model');
// 获取Android版本
const versionResult = await getAdbManager().runAdbCommand('shell getprop ro.build.version.release');
let screenInfo = '设备屏幕信息:\n';
if (sizeResult.success) {
screenInfo += `屏幕尺寸: ${sizeResult.stdout.trim()}\n`;
}
if (densityResult.success) {
screenInfo += `屏幕密度: ${densityResult.stdout.trim()}\n`;
}
if (deviceResult.success) {
screenInfo += `设备型号: ${deviceResult.stdout.trim()}\n`;
}
if (versionResult.success) {
screenInfo += `Android版本: ${versionResult.stdout.trim()}\n`;
}
screenInfo += `获取时间: ${new Date().toLocaleString('zh-CN')}`;
return screenInfo;
} catch (error) {
return `获取屏幕信息失败: ${error.message}`;
}
}
};
/**
* 获取详细屏幕信息 (辅助函数)
*/
async function getDetailedScreenInfo() {
try {
const adbManager = getAdbManager();
// 获取屏幕尺寸
const sizeResult = await adbManager.runAdbCommand('shell wm size');
// 获取屏幕密度
const densityResult = await adbManager.runAdbCommand('shell wm density');
// 获取设备信息
const deviceResult = await adbManager.runAdbCommand('shell getprop ro.product.model');
// 获取Android版本
const versionResult = await adbManager.runAdbCommand('shell getprop ro.build.version.release');
const screenInfo = {
realScreenSize: null,
density: null,
deviceModel: null,
androidVersion: null,
timestamp: new Date().toISOString()
};
// 解析屏幕尺寸
if (sizeResult.success) {
const sizeMatch = sizeResult.stdout.trim().match(/Physical size: (\d+)x(\d+)/);
if (sizeMatch) {
screenInfo.realScreenSize = {
width: parseInt(sizeMatch[1]),
height: parseInt(sizeMatch[2]),
raw: sizeResult.stdout.trim()
};
} else {
screenInfo.realScreenSize = { raw: sizeResult.stdout.trim() };
}
}
// 解析屏幕密度
if (densityResult.success) {
const densityMatch = densityResult.stdout.trim().match(/Physical density: (\d+)/);
if (densityMatch) {
screenInfo.density = {
dpi: parseInt(densityMatch[1]),
raw: densityResult.stdout.trim()
};
} else {
screenInfo.density = { raw: densityResult.stdout.trim() };
}
}
// 设备型号
if (deviceResult.success) {
screenInfo.deviceModel = deviceResult.stdout.trim();
}
// Android版本
if (versionResult.success) {
screenInfo.androidVersion = versionResult.stdout.trim();
}
return screenInfo;
} catch (error) {
return {
error: `获取屏幕信息失败: ${error.message}`,
timestamp: new Date().toISOString()
};
}
}
// 注册工具列表处理器
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: TOOLS,
};
});
// 注册工具调用处理器
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (!toolImplementations[name]) {
throw new Error(`Unknown tool: ${name}`);
}
try {
const result = await toolImplementations[name](args || {});
// 如果工具返回的是包含content的对象(如截图工具),直接返回
if (result && typeof result === 'object' && result.content) {
return result;
}
// 否则按原来的方式包装为文本
return {
content: [
{
type: 'text',
text: result,
},
],
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `工具执行失败: ${error.message}`,
},
],
isError: true,
};
}
});
// 启动服务器
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Mobile Agent MCP Server (Node.js) 已启动');
}
main().catch((error) => {
console.error('服务器启动失败:', error);
process.exit(1);
});