UNPKG

codingbaby-mobile

Version:

MCP Mobile Agent Server - Node.js implementation for mobile device control via ADB

1,042 lines (916 loc) 32.4 kB
#!/usr/bin/env node /** * 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); });