1. 从零构建 Android 开发专用的 MCP 服务
作为一名长期奋战在 Android 开发一线的工程师,我最近一直在思考如何让 AI 助手真正成为开发过程中的得力伙伴。经过多次尝试和迭代,我发现构建一个专用的 MCP(Model-Controlled Procedures)服务器是解决这个问题的关键。今天,我就来分享这个从零开始的构建过程,以及其中的技术细节和工程思考。
1.1 为什么需要自建 MCP 服务器?
在日常开发中,我们经常需要与 AI 助手协作完成各种任务:修复 UI bug、验证新功能、分析崩溃日志等等。传统的交互方式是反复向 AI 描述当前设备状态:"按钮在左上角"、"点击后弹出了对话框"、"Logcat 显示了一个 NullPointerException"。这种模式效率低下,而且容易产生误解。
MCP 服务器通过提供一组标准化的工具接口,让 AI 能够自主地与 Android 设备交互。具体来说,它可以实现:
- 自主观察:主动获取屏幕截图、UI 布局和系统日志
- 自主操作:模拟用户点击、滑动、输入等行为
- 闭环验证:执行操作后立即检查结果
那么,为什么不直接使用现成的 MCP 服务呢?这里有几个重要考量:
- 安全性:第三方服务需要完全控制你的开发设备,这相当于把后门钥匙交给别人
- 定制性:每个团队都有独特的工作流和内部工具,自建服务可以无缝集成
- 合规性:企业环境通常有严格的外部服务访问限制,本地部署更符合要求
1.2 MCP 核心概念解析
在开始编码前,我们需要理解几个关键概念:
- Server/Client 架构:MCP 采用经典的 C/S 模型。Server 提供工具集,Client(通常是 AI Agent)发现并使用这些工具
- 工具(Tools):每个工具都是一个独立功能单元,有明确的输入输出规范
- 传输模式:
stdio:通过标准输入输出通信,适合本地调试streamable-http/sse:基于 HTTP 的协议,支持远程调用
2. 技术选型与环境准备
2.1 核心工具链
虽然我们是 Android 开发者,但 Python 生态提供了更成熟的 MCP 支持。以下是我们的技术栈:
- 核心框架:Python MCP SDK(mcp[cli])
- 图像处理:Pillow 库
- 环境管理:uv(新一代 Python 包管理器)
- Android 交互:adb 命令行工具
- 调试工具:MCP Inspector
2.2 开发环境配置
首先确保你的系统已经安装:
- Python 3.10+
- Node.js(用于运行 MCP Inspector)
- Android SDK 并配置好 adb
然后创建项目目录并初始化环境:
bash复制uv init android-mcp-server
cd android-mcp-server
编辑 pyproject.toml 添加依赖:
toml复制[project]
name = "android-dev-mcp-server"
version = "1.0.0"
dependencies = [
"mcp[cli]==1.22.0",
"Pillow==10.3.0",
]
同步依赖:
bash复制uv sync
3. 核心功能实现
3.1 项目结构与入口设计
我们采用标准的 Python 项目结构:
code复制android-mcp-server/
├── main.py # 主入口
├── pyproject.toml # 项目配置
└── README.md
main.py 首先实现参数解析:
python复制import argparse
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("--mode", choices=["stdio", "streamable-http", "sse"], required=True)
parser.add_argument("--temp-dir", required=True)
parser.add_argument("--port", type=int, default=3001)
return parser.parse_args()
3.2 ADB 工具封装
所有 Android 交互都通过 adb 实现,我们先封装一个基础工具类:
python复制import subprocess
import shlex
class ADBHelper:
@staticmethod
def execute(args, capture_output=False):
try:
result = subprocess.run(["adb"] + args,
capture_output=capture_output,
text=True,
check=True)
return result.stdout if capture_output else None
except subprocess.CalledProcessError as e:
raise RuntimeError(f"ADB command failed: {e.stderr}")
3.3 核心工具实现
3.3.1 日志获取工具
python复制@mcp.tool(structured_output=True)
def get_logcat_output(app_package: str, log_level: str = "DEBUG") -> str:
"""获取指定应用的日志"""
level_map = {"DEBUG": "D", "WARNING": "W", "ERROR": "E"}
if log_level.upper() not in level_map:
raise ToolError("Invalid log level")
output = ADBHelper.execute(["logcat", "-d", "-t", "100", f"*:{level_map[log_level]}"], True)
return "\n".join(line for line in output.splitlines() if app_package in line)
3.3.2 屏幕截图工具
python复制@mcp.tool()
def get_screenshot() -> Image:
"""获取设备截图并压缩"""
try:
# 截图并保存到临时文件
temp_path = os.path.join(args.temp_dir, "screenshot.png")
ADBHelper.execute(["shell", "screencap", "-p", "/sdcard/screenshot.png"])
ADBHelper.execute(["pull", "/sdcard/screenshot.png", temp_path])
ADBHelper.execute(["shell", "rm", "/sdcard/screenshot.png"])
# 使用Pillow压缩图片
with PILImage.open(temp_path) as img:
img.thumbnail((img.width//2, img.height//2))
buffer = io.BytesIO()
img.save(buffer, format="PNG")
return Image(data=buffer.getvalue(), format="png")
finally:
if os.path.exists(temp_path):
os.remove(temp_path)
3.3.3 UI 结构获取工具
python复制@mcp.tool(structured_output=True)
def get_ui_dump(attributes: str) -> str:
"""获取UI层级信息"""
try:
# 获取原始XML
xml_path = os.path.join(args.temp_dir, "ui_dump.xml")
ADBHelper.execute(["shell", "uiautomator", "dump"])
ADBHelper.execute(["pull", "/sdcard/window_dump.xml", xml_path])
ADBHelper.execute(["shell", "rm", "/sdcard/window_dump.xml"])
# 过滤属性
root = ET.parse(xml_path).getroot()
for node in root.iter():
for attr in list(node.attrib):
if attr not in attributes.split(','):
del node.attrib[attr]
return ET.tostring(root, encoding="unicode")
finally:
if os.path.exists(xml_path):
os.remove(xml_path)
3.4 操作类工具实现
3.4.1 点击操作
python复制@mcp.tool(structured_output=True)
def tap_screen(x: int, y: int) -> str:
"""点击屏幕指定位置"""
ADBHelper.execute(["shell", "input", "tap", str(x), str(y)])
return f"Tapped at ({x}, {y})"
3.4.2 滑动操作
python复制@mcp.tool(structured_output=True)
def swipe_screen(x1: int, y1: int, x2: int, y2: int) -> str:
"""从(x1,y1)滑动到(x2,y2)"""
ADBHelper.execute(["shell", "input", "swipe", str(x1), str(y1), str(x2), str(y2)])
return f"Swiped from ({x1},{y1}) to ({x2},{y2})"
3.4.3 文本输入
python复制@mcp.tool(structured_output=True)
def send_text(text: str) -> str:
"""输入文本"""
if not text:
raise ToolError("Text cannot be empty")
ADBHelper.execute(["shell", "input", "text", shlex.quote(text)])
return f"Sent text: {text}"
3.4.4 系统操作
python复制@mcp.tool(structured_output=True)
def perform_system_action(action: str) -> str:
"""执行系统操作"""
action_map = {
"BACK": "KEYCODE_BACK",
"HOME": "KEYCODE_HOME",
"RECENT": "KEYCODE_APP_SWITCH"
}
if action not in action_map:
raise ToolError(f"Invalid action. Valid: {list(action_map.keys())}")
ADBHelper.execute(["shell", "input", "keyevent", action_map[action]])
return f"Performed {action} action"
4. 服务器启动与测试
4.1 启动服务器
在 main.py 中添加启动逻辑:
python复制def start_server(mode, temp_dir, port):
mcp = FastMCP(name="Android MCP Server", port=port)
# 注册所有工具...
print(f"Starting server in {mode} mode...")
if mode == "stdio":
mcp.run(transport="stdio")
elif mode == "streamable-http":
mcp.run(transport="streamable-http")
elif mode == "sse":
mcp.run(transport="sse")
if __name__ == "__main__":
args = parse_args()
start_server(args.mode, args.temp_dir, args.port)
4.2 使用 MCP Inspector 测试
创建配置文件 mcp-inspector-config.json:
json复制{
"mcpServers": {
"android-local": {
"command": "python",
"args": ["main.py", "--mode", "stdio", "--temp-dir", "/tmp/android_mcp"]
}
}
}
启动测试:
bash复制npx @modelcontextprotocol/inspector@latest --config mcp-inspector-config.json --server android-local
5. 工程实践与安全考量
5.1 安全最佳实践
- 最小权限原则:只暴露必要的工具接口
- ADB 安全:
- 仅在有线连接时启用 USB 调试
- 避免使用 adb tcpip 开放网络端口
- 定期检查已授权设备列表
- 输入验证:对所有工具参数进行严格校验
- 资源清理:确保临时文件被正确删除
5.2 性能优化建议
- 截图优化:
- 降低分辨率减少传输数据量
- 使用 JPEG 格式替代 PNG 可进一步减小体积
- 日志过滤:
- 在 adb 命令中直接过滤特定 tag
- 限制返回的日志行数
- UI Dump 优化:
- 只请求必要的属性
- 考虑缓存机制减少重复请求
5.3 扩展可能性
- 企业级集成:
- 添加 OAuth2 认证
- 对接内部设备管理平台
- CI/CD 集成:
- 在自动化测试失败时自动调用
- 与错误报告系统联动
- 高级工具:
- 录制和回放操作序列
- 性能监控工具
- 自动化测试工具
6. 常见问题排查
6.1 设备连接问题
症状:adb devices 列表为空
解决方案:
- 检查 USB 线连接
- 确认设备已开启开发者模式和 USB 调试
- 在设备上确认授权对话框
6.2 权限问题
症状:操作被拒绝
解决方案:
- 检查 adb 是否有 root 权限(如有需要)
- 确认应用已授予必要权限
- 检查 selinux 状态
6.3 性能问题
症状:操作响应缓慢
解决方案:
- 减少截图分辨率
- 限制日志输出量
- 使用更高效的传输模式(如 SSE)
6.4 稳定性问题
症状:随机崩溃或超时
解决方案:
- 增加重试机制
- 添加心跳检测
- 实现超时控制
7. 实际应用案例
7.1 UI Bug 自动修复
AI Agent 可以通过以下流程自动修复 UI 问题:
- 获取当前屏幕截图和 UI 结构
- 识别异常元素
- 定位对应的代码位置
- 修改代码并重新部署
- 验证修复结果
7.2 自动化测试增强
将 MCP 服务器集成到自动化测试中:
- 测试失败时自动收集设备状态
- 分析失败原因
- 尝试自动修复或提供详细诊断信息
7.3 新功能探索
AI Agent 可以:
- 自动探索新功能的可操作性
- 记录操作路径
- 生成测试用例
8. 架构深度解析
8.1 通信流程
- 工具发现:Client 获取 Server 提供的工具列表
- 工具调用:Client 发送 JSON-RPC 格式请求
- 结果返回:Server 返回结构化数据
8.2 数据流分析
- 设备到 Server:
- 截图二进制数据
- UI 结构 XML
- 日志文本
- Server 到 Client:
- 压缩后的图像
- 过滤后的 UI 信息
- 格式化后的日志
8.3 安全边界
- 物理层:USB 有线连接最安全
- 网络层:127.0.0.1 绑定避免网络暴露
- 应用层:严格的参数校验和权限控制
9. 性能优化进阶
9.1 图像处理优化
python复制# 使用更高效的图像处理参数
def compress_image(img):
img = img.convert("RGB") # 移除alpha通道
img = img.resize((width//2, height//2), PILImage.LANCZOS)
buffer = io.BytesIO()
img.save(buffer, format="JPEG", quality=85, optimize=True)
return buffer.getvalue()
9.2 日志处理优化
python复制# 使用更精确的日志过滤
def filter_logs(package, logs):
pid = ADBHelper.execute(["shell", "pidof", package], True).strip()
return [line for line in logs.splitlines() if f"{pid}:" in line]
9.3 并发处理
python复制from concurrent.futures import ThreadPoolExecutor
executor = ThreadPoolExecutor(max_workers=4)
@mcp.tool()
def parallel_task():
future = executor.submit(long_running_task)
return future.result()
10. 项目总结与展望
构建这个 MCP 服务器的过程让我深刻理解了 AI 与开发工具结合的巨大潜力。通过标准化接口,我们成功让 AI Agent 能够"看见"和"操作"Android 设备,大大提升了开发效率。
未来可能的改进方向包括:
- 支持多设备同时管理
- 增加视频流传输能力
- 集成更多开发调试工具
- 支持插件化扩展
这个项目只是一个起点,期待看到更多开发者加入这个领域,共同探索 AI 辅助开发的新范式。