基于MCP协议构建跨平台移动自动化测试框架:5分钟实现iOS与Android统一测试

发布时间:2026/7/2 0:56:12
基于MCP协议构建跨平台移动自动化测试框架:5分钟实现iOS与Android统一测试 1. 项目概述当移动测试遇上MCP如果你是一名移动应用开发者或测试工程师过去几年里你很可能在跨平台自动化测试上投入了大量精力。传统的方案比如维护两套独立的测试脚本一套用Appium for iOS一套用Appium for Android或者尝试用Flutter Driver、Detox等框架都面临着学习曲线陡峭、环境配置复杂、脚本维护成本高以及执行速度慢的痛点。尤其是在追求快速迭代和持续交付的今天测试环节的效率瓶颈愈发明显。最近一个名为MCPModel Context Protocol的技术协议开始在开发者社区特别是与Claude等AI助手深度集成的场景下被频繁讨论。它最初的设计目标是为AI模型提供结构化访问工具和数据的能力。但敏锐的工程师们很快发现MCP的核心思想——通过标准化的协议让一个“大脑”AI或自动化引擎能够无缝调用和控制各种“工具”如浏览器、移动设备、API——正是解决跨平台自动化测试困境的一把钥匙。想象一下你不再需要为iOS和Android分别编写冗长的定位符和交互逻辑而是用一套统一的、接近自然语言的指令来描述测试用例然后由一个“智能协调器”去自动适配并执行在两个平台上。这听起来像未来但借助MCP及其生态工具我们完全可以在5分钟内搭建起这样一个高效测试框架的雏形。本文将带你深入拆解如何利用MCP的思想和相关工具链快速构建一个面向iOS和Android的跨平台自动化测试解决方案实现真正的“一次编写随处运行”。2. MCP协议的核心思想与测试领域的契合点2.1 MCP是什么不仅仅是AI的“手和脚”MCP全称Model Context Protocol你可以把它理解为一套“插件标准”或“工具调用说明书”。它的主要作用是让像Claude、ChatGPT这样的AI大模型能够安全、规范地使用外部工具比如执行终端命令、读取文件、查询数据库或者——对我们至关重要——控制应用程序和模拟用户操作。从技术角度看MCP定义了一个基于JSON-RPC的轻量级协议。一个MCP Server服务器负责暴露一系列“工具”Tools或“资源”Resources而一个MCP Client客户端通常是AI助手则可以发现这些工具并调用它们。关键在于这个协议是声明式和标准化的。Server告诉Client“我这里有一个叫click_element的工具它需要platform和selector两个参数。” Client只需要按照这个格式发送请求无需关心Server内部是用Appium、UIAutomator2还是XCUITest实现的。2.2 为什么MCP是跨平台测试的“理想粘合剂”传统跨平台测试框架试图在“脚本层”实现统一例如使用相同的WebDriver API。但到了底层iOS的XCUITest和Android的UIAutomator2/Espresso差异巨大导致很多API只是“形似而神不似”定位策略如XPath、ID的兼容性更是噩梦。MCP从另一个角度切入它统一的是“意图层”或“任务描述层”。我们不再直接写“在iOS上通过XCUIElementTypeButton[name‘Login’]点击在Android上通过android.widget.Button[text‘Login’]点击”而是向MCP Server发送一个意图“点击登录按钮”。MCP Server内部封装了针对不同平台的适配逻辑它来负责将这个通用意图翻译成平台特定的操作。这种架构带来了几个革命性优势脚本极度简化与统一测试用例可以用更高级、更稳定的抽象如基于AI的自然语言或基于页面对象模型的通用指令来编写。底层实现可插拔今天用Appium做MCP Server的后端明天如果有了更快的原生框架可以替换掉后端而测试脚本无需任何改动。易于集成AI既然MCP本就是为AI设计的那么用AI来生成、维护甚至解释测试用例就变得异常自然可以实现真正的智能测试。注意目前截至我撰写时并没有一个官方命名为“MCP Mobile Test Server”的现成产品。我们所说的“用MCP搞定测试”是指利用MCP的思想、协议和开发生态快速构建一个具备MCP接口的移动测试服务。社区已经有一些将Playwright用于Web通过MCP暴露的案例这为我们构建移动版本提供了完美的蓝图。3. 构建你的MCP移动测试服务器核心架构解析要实现“5分钟搭建”我们需要一个尽可能简洁且功能强大的架构。这里我推荐一个基于Node.js Appium MCP Server SDK的组合方案。选择Node.js是因为其异步特性适合IO密集型的自动化操作并且有丰富的MCP生态库Appium依然是移动自动化领域最广泛支持的标准而MCP Server SDK则提供了快速搭建协议服务器的能力。3.1 技术栈选型与快速初始化首先确保你的开发环境已经准备好Node.js(v18及以上)这是我们的运行时环境。Java JDK(11或8)Appium依赖。Android SDK与Xcode Command Line Tools分别用于Android和iOS应用的编译和驱动。Appium Server可以通过npm全局安装appium或者使用Appium Desktop。iOS模拟器或Android模拟器/真机用于执行测试。接下来我们创建一个新的项目并安装核心依赖# 1. 创建项目目录并初始化 mkdir mcp-mobile-test-server cd mcp-mobile-test-server npm init -y # 2. 安装核心依赖 npm install modelcontextprotocol/sdk appium-webdriverdriver webdriverio # modelcontextprotocol/sdk: 官方MCP Server开发工具包 # appium-webdriverdriver: 虽然我们通过Appium服务但WDIO可以直接连接Appium # webdriverio: 一个强大的WebDriver/Appium客户端库封装了丰富的API # 3. 安装TypeScript及相关类型定义推荐用于更好的开发体验 npm install --save-dev typescript types/node ts-node npx tsc --init这个依赖组合的精妙之处在于我们用webdriverio作为与Appium通信的实际驱动层它比直接使用WebDriver原生协议更友好、功能更全。而modelcontextprotocol/sdk则帮助我们快速创建一个符合MCP标准的服务器将WebdriverIO的能力“包装”成标准的MCP工具。3.2 设计MCP Server的工具Tools清单一个MCP Server的核心是它向外提供的“工具”。对于移动自动化测试我们需要设计一套能覆盖核心交互的通用工具集。以下是我设计的一个最小可行工具集每个工具都考虑了跨平台兼容性start_session: 启动一个测试会话。参数包括platform(ios/android),deviceName,appPath或appPackage/appActivity/bundleId。terminate_session: 结束当前测试会话释放资源。find_element: 查找元素。这是跨平台最大的挑战之一。我们将设计一个统一的定位策略内部进行平台转换。例如接受一个strategy如accessibility_id,xpath,class_name和selector。element_action: 对找到的元素执行操作。参数包括elementId和action如click,send_keys,get_text,get_attribute。execute_script: 在移动端上下文中执行JavaScript对于混合或WebView应用非常有用。take_screenshot: 截取当前屏幕并返回图片的Base64数据或保存路径。这个设计的关键在于抽象和适配。例如find_element工具内部需要判断当前平台是iOS还是Android然后将统一的accessibility_id策略分别映射到iOS的accessibility id和Android的content-desc。这层逻辑被封装在MCP Server内部对调用者Client完全透明。4. 手把手实现从零编写MCP移动测试服务器让我们开始编写核心代码。首先在项目根目录创建src/server.ts文件。4.1 初始化MCP Server与WebdriverIO管理import { Server } from modelcontextprotocol/sdk/server/index.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { WebDriver } from webdriverio; import { Capabilities } from wdio/types; // 声明工具的参数和返回类型接口 interface StartSessionArgs { platform: ios | android; deviceName: string; // 对于Android: appPackage appActivity, 对于iOS: bundleId appPackage?: string; appActivity?: string; bundleId?: string; appPath?: string; // .apk 或 .ipa/.app 的路径 } // 全局变量管理WebdriverIO会话简化示例生产环境需更健壮的管理 let driver: WebDriver | null null; // 创建MCP Server实例 const server new Server( { name: mobile-automation-server, version: 1.0.0, }, { capabilities: { tools: {}, // 工具将在后面注册 }, } );4.2 实现核心工具启动会话与查找元素接下来我们注册第一个也是最关键的工具start_session。// 注册 start_session 工具 server.setRequestHandler(tools/call, async (request) { if (request.params.name start_session) { const args request.params.arguments as unknown as StartSessionArgs; const capabilities: Capabilities { platformName: args.platform ios ? iOS : Android, appium:automationName: args.platform ios ? XCUITest : UiAutomator2, appium:deviceName: args.deviceName, appium:autoGrantPermissions: true, appium:noReset: false, // 根据测试需求调整 }; // 处理应用标识 if (args.platform android) { if (args.appPackage args.appActivity) { capabilities[appium:appPackage] args.appPackage; capabilities[appium:appActivity] args.appActivity; } else if (args.appPath) { capabilities[appium:app] args.appPath; } } else { // iOS if (args.bundleId) { capabilities[appium:bundleId] args.bundleId; } else if (args.appPath) { capabilities[appium:app] args.appPath; } } try { // 连接到本地Appium服务器默认地址 http://localhost:4723 driver await WebDriver.remote({ path: /wd/hub, port: 4723, capabilities, connectionRetryCount: 3, }); return { content: [{ type: text, text: Session started successfully for ${args.platform} on ${args.deviceName}. Session ID: ${driver.sessionId}, }], }; } catch (error: any) { return { content: [{ type: text, text: Failed to start session: ${error.message}, }], isError: true, }; } } // ... 其他工具的处理 });紧接着实现find_element工具。这是跨平台适配的核心。// 在 tools/call 处理器中继续添加条件分支 if (request.params.name find_element) { if (!driver) { return { content: [{ type: text, text: No active session. Please start a session first. }], isError: true }; } const args request.params.arguments as { strategy: accessibility_id | id | xpath | class_name; selector: string; }; try { let locatedElement; // 根据策略选择定位方式这里进行平台适配映射 switch (args.strategy) { case accessibility_id: // Appium中accessibility_id在iOS和Android上是通用的定位方式 locatedElement await driver.$(~${args.selector}); break; case id: // 对于Android是resource-id对于iOS是name或accessibility id的一部分这里简化处理 // 实际项目中可能需要更复杂的映射或使用不同的策略 locatedElement await driver.$(#${args.selector}); break; case xpath: // XPath理论上跨平台但不同平台生成的元素树结构不同编写通用的XPath难度大。 // 这里假设传入的XPath是精心编写、兼容双端的。 locatedElement await driver.$(args.selector); break; case class_name: // 类名在iOS和Android上完全不同此策略通常不用于跨平台脚本。 // 此处仅作演示生产环境慎用。 locatedElement await driver.$(args.selector); break; default: throw new Error(Unsupported strategy: ${args.strategy}); } // 生成一个唯一ID与此元素关联返回给客户端。实际应使用更稳定的元素标识。 const elementId elem_${Date.now()}; // 简单起见这里用一个全局Map存储元素引用。生产环境需考虑会话隔离和清理。 (global as any).elementCache (global as any).elementCache || new Map(); (global as any).elementCache.set(elementId, locatedElement); return { content: [{ type: text, text: JSON.stringify({ elementId, elementFound: true }), }], }; } catch (error: any) { return { content: [{ type: text, text: Element not found with strategy ${args.strategy} and selector ${args.selector}: ${error.message}, }], isError: true, }; } }4.3 实现元素操作与截图工具有了元素ID我们就可以实现element_action工具。if (request.params.name element_action) { if (!driver) { return { content: [{ type: text, text: No active session. }], isError: true }; } const args request.params.arguments as { elementId: string; action: click | send_keys | get_text | get_attribute; value?: string; // 用于 send_keys 的文本或 get_attribute 的属性名 }; const elementCache (global as any).elementCache; if (!elementCache || !elementCache.has(args.elementId)) { return { content: [{ type: text, text: Invalid or expired elementId. }], isError: true }; } const element elementCache.get(args.elementId); try { let result: string ; switch (args.action) { case click: await element.click(); result Element clicked successfully.; break; case send_keys: if (!args.value) throw new Error(Value is required for send_keys action.); await element.setValue(args.value); result Text ${args.value} sent successfully.; break; case get_text: const text await element.getText(); result text; break; case get_attribute: if (!args.value) throw new Error(Attribute name is required for get_attribute action.); const attr await element.getAttribute(args.value); result attr || (empty); break; } return { content: [{ type: text, text: result, }], }; } catch (error: any) { return { content: [{ type: text, text: Action ${args.action} failed: ${error.message}, }], isError: true, }; } }最后实现一个简单的截图工具take_screenshot。if (request.params.name take_screenshot) { if (!driver) { return { content: [{ type: text, text: No active session. }], isError: true }; } try { // 截图并保存为Base64字符串 const screenshot await driver.takeScreenshot(); // 在实际使用中可能直接返回Base64给客户端或保存到文件返回路径 // 这里我们返回一个包含Base64数据的JSON消息 return { content: [{ type: text, text: JSON.stringify({ format: base64, data: screenshot, note: Screenshot taken. Data is base64 encoded PNG. }), }], }; } catch (error: any) { return { content: [{ type: text, text: Screenshot failed: ${error.message}, }], isError: true, }; } }4.4 启动服务器与连接测试在src/server.ts末尾添加启动代码。// 启动MCP Server使用stdio传输便于被Claude Code等客户端调用 async function main() { const transport new StdioServerTransport(); await server.connect(transport); console.error(MCP Mobile Automation Server is running on stdio...); } main().catch((error) { console.error(Server fatal error:, error); process.exit(1); });现在编译并运行这个服务器。首先确保你的Appium服务已经在http://localhost:4723运行起来。# 编译TypeScript (如果使用了ts) npx tsc # 运行服务器 (假设编译输出在dist目录) node dist/server.js # 或者直接使用ts-node运行 npx ts-node src/server.ts服务器现在正在标准输入/输出上监听等待MCP客户端如配置了此Server的Claude for Desktop的连接和指令。5. 实战演练5分钟编写并执行跨平台测试用例现在我们的MCP Server已经就绪。但如何调用它呢我们可以创建一个简单的MCP客户端脚本或者更酷的方式——直接利用已经支持MCP的AI助手。这里我演示两种方式。5.1 方式一使用Node.js脚本作为MCP客户端创建一个client.js文件模拟MCP客户端调用我们的工具。这有助于我们调试和验证服务器功能。// 这是一个简化的示例实际MCP客户端通信需遵循JSON-RPC over stdio // 此处我们用fetch模拟向一个假设的HTTP适配器发送请求实际MCP Server通常是stdio/socket // 为了演示我们假设将MCP Server包装了一个HTTP网关。 async function runTest() { const baseUrl http://localhost:3000/mcp; // 假设的HTTP网关地址 console.log(1. 启动iOS模拟器会话...); const startResp await fetch(${baseUrl}/tools/call, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ name: start_session, arguments: { platform: ios, deviceName: iPhone 15 Pro Simulator, bundleId: com.example.myapp, // 替换为你的App bundle ID } }) }); console.log(await startResp.json()); console.log(2. 查找登录按钮...); const findResp await fetch(${baseUrl}/tools/call, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ name: find_element, arguments: { strategy: accessibility_id, selector: LoginButton } }) }); const findResult await findResp.json(); console.log(findResult); const elementId JSON.parse(findResult.content[0].text).elementId; console.log(3. 点击登录按钮...); const clickResp await fetch(${baseUrl}/tools/call, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ name: element_action, arguments: { elementId: elementId, action: click } }) }); console.log(await clickResp.json()); console.log(4. 截图...); const screenshotResp await fetch(${baseUrl}/tools/call, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ name: take_screenshot, arguments: {} }) }); const screenshotResult await screenshotResp.json(); console.log(Screenshot data received (first 100 chars):, JSON.parse(screenshotResult.content[0].text).data.substring(0, 100)); } runTest();5.2 方式二集成Claude Desktop进行自然语言测试未来态这才是MCP的终极魅力所在。你可以将编写好的MCP Server配置到Claude Desktop中。配置方法通常是在Claude的配置目录下创建一个claude_desktop_config.json文件添加你的服务器信息。{ mcpServers: { mobile-automator: { command: node, args: [/absolute/path/to/your/dist/server.js], env: {} } } }配置成功后重启Claude Desktop。在与Claude的对话中你就可以直接使用自然语言来驱动测试了例如你“Claude请帮我测试一下iOS模拟器上的‘我的App’先启动它。”Claude通过MCP调用start_session工具 “好的我已经在iPhone 15 Pro模拟器上启动了‘我的App’。”你“找到那个标识为‘欢迎’的文本看看它显示的是什么。”Claude调用find_element和element_actionwithget_text “找到的文本内容是‘欢迎回来张三’。”你“很好现在点击右下角的‘设置’按钮然后截个图给我看看。”Claude依次调用find_element、click、take_screenshot “已点击设置按钮。这是当前的屏幕截图[展示Base64图片或保存的图片路径]”这种方式将测试用例的编写从代码编写变成了对话和意图描述极大地降低了自动化测试的门槛也让测试逻辑的调整变得无比灵活。实操心得在真正与Claude集成时你可能会发现直接返回Base64图片数据在聊天框中显示不友好。一个更实用的做法是在take_screenshot工具中将截图保存到本地一个临时文件然后返回文件的路径。Claude可以识别这是一个本地文件路径有时甚至能将其作为附件插入到回复中或者你可以手动打开查看。6. 避坑指南与高级技巧让跨平台测试真正稳定高效构建出原型只是第一步要让这个基于MCP的测试方案在生产环境中可靠运行还需要解决一系列实际问题。6.1 定位策略的“一次编写”陷阱与解决方案最大的坑莫过于元素的统一定位。accessibility_id在iOS上是accessibilityIdentifier在Android上是contentDescription是最接近“一次编写”的理想选择但需要开发同学在编写UI代码时为可交互元素统一添加这些标识。如果做不到这一点我们就需要更智能的适配层。解决方案在MCP Server的find_element内部实现一个定位策略转换器。我们可以定义一套自定义的、平台无关的定位符语法。例如定义一个cross_platform_id策略在服务器端维护一个映射表{ “login_button”: { “ios”: “//XCUIElementTypeButton[name‘登录’]”, “android”: “//android.widget.Button[text‘登录’]” } }或者利用AI视觉识别如基于Appium的appium-image-plugin进行图标按钮的点击但这会牺牲一些执行速度。核心原则是将平台差异的复杂性收敛到MCP Server内部对外提供稳定的接口。6.2 会话管理与状态隔离我们的示例中使用全局变量存储driver和elementCache这只能用于单会话测试。实际中可能需要并行测试多台设备。解决方案为每个start_session调用生成一个唯一的sessionId并以此sessionId为键在服务器端维护一个会话池Session Pool存储该会话对应的WebdriverIO驱动实例和元素缓存。所有后续的工具调用都必须携带sessionId参数以便路由到正确的设备和会话上下文。这模仿了Selenium Grid或Appium Grid的会话管理机制。6.3 异步操作与等待机制移动应用常有网络请求、动画等异步操作。直接执行click()后立即find_element查找下一个元素很可能因为页面未加载完成而失败。解决方案在MCP Server工具中内置智能等待。可以在element_action的click操作后自动执行一个隐式等待或者提供额外的wait_for_element工具。更好的做法是在Server端利用WebdriverIO提供的丰富等待条件如waitUntil、waitForDisplayed对外暴露一个wait工具允许客户端指定等待条件如元素出现、元素可点击、特定文本出现。6.4 错误处理与日志收集自动化测试失败是常态清晰的错误信息和日志对于排查问题至关重要。解决方案完善每个工具调用的try-catch块不仅返回简单的错误消息更应返回结构化的错误信息包括错误类型如ElementNotFoundError、TimeoutError、平台信息、当前屏幕的截图Base64、以及可能的Appium服务端日志片段。这能极大提升使用AI助手或开发者排查问题的效率。可以考虑在Server端集成日志框架如Winston将所有的请求、响应、底层Appium指令都记录到文件或日志服务中。6.5 性能优化连接池与指令批处理频繁地通过MCP协议尤其是经过HTTP网关或stdio发送单个指令如“找元素A - 点击 - 找元素B - 输入”会产生较高的网络和进程间通信开销。解决方案指令批处理设计一个execute_commands工具接受一个命令数组Command List在Server端顺序执行最后统一返回结果数组。这减少了往返延迟RTT。连接池对于HTTP网关模式保持与Appium Server的WebdriverIO连接长连接避免为每个MCP请求都创建和销毁驱动会话。7. 扩展蓝图不止于基础操作当基础框架跑通后你可以考虑集成更多强大功能将这个MCP测试服务器打造成一个真正的移动测试中台。集成AI视觉测试封装appium-image-plugin或OpenCV的功能提供find_element_by_image图像识别定位、assert_screen_match屏幕对比等工具解决动态内容或游戏UI的测试难题。性能监控在工具调用间隙通过Appium的performance命令或直接调用系统工具如Android的adb shell dumpsys gfxinfoiOS的Instruments采集FPS、内存、CPU数据并暴露start_perf_recording和stop_perf_recording工具。网络流量拦截与Mock集成mitmproxy或browserMobProxy提供intercept_network_request、mock_api_response等工具实现前后端分离测试和异常场景模拟。测试报告生成在terminate_session工具中自动收集本次会话的所有操作步骤、截图、错误信息生成一个结构化的测试报告如JSON、HTML并通过get_test_report工具提供下载。这个基于MCP的移动自动化测试方案其价值不在于替代了Appium或WebdriverIO而在于它构建了一个更高层次的、协议化的抽象层。它让测试逻辑与底层驱动彻底解耦让测试用例可以用更灵活的方式自然语言、YAML、JSON来编写和驱动并且天然具备了与AI工作流集成的能力。从“5分钟搭建原型”到“构建企业级测试中台”这条路已经清晰地展现在我们面前。剩下的就是根据实际项目需求去填充和打磨这个框架的每一个细节了。