mobile-mcp-ai 2.1.2__py3-none-any.whl → 2.5.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. mobile_mcp/__init__.py +34 -0
  2. mobile_mcp/config.py +142 -0
  3. mobile_mcp/core/basic_tools_lite.py +3266 -0
  4. {core → mobile_mcp/core}/device_manager.py +2 -2
  5. mobile_mcp/core/dynamic_config.py +272 -0
  6. mobile_mcp/core/ios_client_wda.py +569 -0
  7. mobile_mcp/core/ios_device_manager_wda.py +306 -0
  8. {core → mobile_mcp/core}/mobile_client.py +279 -39
  9. mobile_mcp/core/template_matcher.py +429 -0
  10. mobile_mcp/core/templates/close_buttons/auto_x_0112_151217.png +0 -0
  11. mobile_mcp/core/templates/close_buttons/auto_x_0112_152037.png +0 -0
  12. mobile_mcp/core/templates/close_buttons/auto_x_0112_152840.png +0 -0
  13. mobile_mcp/core/templates/close_buttons/auto_x_0112_153256.png +0 -0
  14. mobile_mcp/core/templates/close_buttons/auto_x_0112_154847.png +0 -0
  15. mobile_mcp/core/templates/close_buttons/gray_x_stock_ad.png +0 -0
  16. {core → mobile_mcp/core}/utils/smart_wait.py +3 -3
  17. mobile_mcp/mcp_tools/__init__.py +10 -0
  18. mobile_mcp/mcp_tools/mcp_server.py +1071 -0
  19. mobile_mcp_ai-2.5.8.dist-info/METADATA +469 -0
  20. mobile_mcp_ai-2.5.8.dist-info/RECORD +32 -0
  21. mobile_mcp_ai-2.5.8.dist-info/entry_points.txt +2 -0
  22. mobile_mcp_ai-2.5.8.dist-info/licenses/LICENSE +201 -0
  23. mobile_mcp_ai-2.5.8.dist-info/top_level.txt +1 -0
  24. core/ai/__init__.py +0 -11
  25. core/ai/ai_analyzer.py +0 -197
  26. core/ai/ai_config.py +0 -116
  27. core/ai/ai_platform_adapter.py +0 -399
  28. core/ai/smart_test_executor.py +0 -520
  29. core/ai/test_generator.py +0 -365
  30. core/ai/test_generator_from_history.py +0 -391
  31. core/ai/test_generator_standalone.py +0 -293
  32. core/assertion/__init__.py +0 -9
  33. core/assertion/smart_assertion.py +0 -341
  34. core/basic_tools.py +0 -377
  35. core/h5/__init__.py +0 -10
  36. core/h5/h5_handler.py +0 -548
  37. core/ios_client.py +0 -219
  38. core/ios_device_manager.py +0 -252
  39. core/locator/__init__.py +0 -10
  40. core/locator/cursor_ai_auto_analyzer.py +0 -119
  41. core/locator/cursor_vision_helper.py +0 -414
  42. core/locator/mobile_smart_locator.py +0 -1640
  43. core/locator/position_analyzer.py +0 -813
  44. core/locator/script_updater.py +0 -157
  45. core/nl_test_runner.py +0 -585
  46. core/smart_app_launcher.py +0 -334
  47. core/smart_tools.py +0 -311
  48. mcp/__init__.py +0 -8
  49. mcp/mcp_server.py +0 -1919
  50. mcp/mcp_server_simple.py +0 -476
  51. mobile_mcp_ai-2.1.2.dist-info/METADATA +0 -567
  52. mobile_mcp_ai-2.1.2.dist-info/RECORD +0 -45
  53. mobile_mcp_ai-2.1.2.dist-info/entry_points.txt +0 -2
  54. mobile_mcp_ai-2.1.2.dist-info/top_level.txt +0 -4
  55. vision/__init__.py +0 -10
  56. vision/vision_locator.py +0 -404
  57. {core → mobile_mcp/core}/__init__.py +0 -0
  58. {core → mobile_mcp/core}/utils/__init__.py +0 -0
  59. {core → mobile_mcp/core}/utils/logger.py +0 -0
  60. {core → mobile_mcp/core}/utils/operation_history_manager.py +0 -0
  61. {utils → mobile_mcp/utils}/__init__.py +0 -0
  62. {utils → mobile_mcp/utils}/logger.py +0 -0
  63. {utils → mobile_mcp/utils}/xml_formatter.py +0 -0
  64. {utils → mobile_mcp/utils}/xml_parser.py +0 -0
  65. {mobile_mcp_ai-2.1.2.dist-info → mobile_mcp_ai-2.5.8.dist-info}/WHEEL +0 -0
@@ -0,0 +1,1071 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Mobile MCP Server - 统一入口
5
+
6
+ 纯 MCP 方案,完全依赖 Cursor 视觉能力:
7
+ - 不需要 AI 密钥
8
+ - 24 个核心工具(含 4 个长按工具)
9
+ - 支持 Android 和 iOS
10
+ - 保留 pytest 脚本生成
11
+
12
+ 使用方式:
13
+ python mcp_server.py
14
+
15
+ 配置 Cursor:
16
+ {
17
+ "mcpServers": {
18
+ "mobile": {
19
+ "command": "/path/to/venv/bin/python",
20
+ "args": ["/path/to/mobile_mcp/mcp_server.py"],
21
+ "env": {
22
+ "MOBILE_PLATFORM": "android" // 或 "ios"
23
+ }
24
+ }
25
+ }
26
+ }
27
+ """
28
+
29
+ import asyncio
30
+ import json
31
+ import os
32
+ import sys
33
+ from pathlib import Path
34
+ from typing import Optional
35
+
36
+ # 添加项目根目录到 Python 路径
37
+ # 支持两种运行方式:
38
+ # 1. 从源码运行:__file__ 在 mcp_tools/ 目录下,往上两级到项目根目录
39
+ # 2. 从已安装包运行:包已安装时,mobile_mcp 应该可以直接导入
40
+ # 先尝试从已安装的包导入,如果失败则从源码路径导入
41
+ try:
42
+ # 尝试导入已安装的包
43
+ import mobile_mcp.core.mobile_client
44
+ import mobile_mcp.core.basic_tools_lite
45
+ # 如果成功,说明包已安装,不需要添加路径
46
+ except ImportError:
47
+ # 包未安装或导入失败,从源码运行
48
+ # __file__ 在 mcp_tools/ 目录下,往上两级到项目根目录
49
+ project_root = Path(__file__).parent.parent
50
+ if str(project_root) not in sys.path:
51
+ sys.path.insert(0, str(project_root))
52
+
53
+ # 尝试导入 MCP,处理可能的路径冲突
54
+ try:
55
+ from mcp.types import Tool, TextContent
56
+ from mcp.server import Server
57
+ from mcp.server.stdio import stdio_server
58
+ except ImportError:
59
+ # 如果本地 mcp 目录冲突,从 site-packages 加载
60
+ import importlib.util
61
+ import site
62
+
63
+ for site_dir in site.getsitepackages():
64
+ mcp_types_path = Path(site_dir) / 'mcp' / 'types.py'
65
+ if mcp_types_path.exists():
66
+ mcp_pkg_path = Path(site_dir) / 'mcp'
67
+
68
+ # 加载 mcp.types
69
+ spec = importlib.util.spec_from_file_location("mcp.types", mcp_types_path)
70
+ mcp_types = importlib.util.module_from_spec(spec)
71
+ sys.modules['mcp.types'] = mcp_types
72
+ spec.loader.exec_module(mcp_types)
73
+
74
+ # 加载 mcp.server
75
+ server_init = mcp_pkg_path / 'server' / '__init__.py'
76
+ spec = importlib.util.spec_from_file_location("mcp.server", server_init)
77
+ mcp_server_mod = importlib.util.module_from_spec(spec)
78
+ sys.modules['mcp.server'] = mcp_server_mod
79
+ spec.loader.exec_module(mcp_server_mod)
80
+
81
+ # 加载 mcp.server.stdio
82
+ stdio_path = mcp_pkg_path / 'server' / 'stdio.py'
83
+ spec = importlib.util.spec_from_file_location("mcp.server.stdio", stdio_path)
84
+ mcp_stdio = importlib.util.module_from_spec(spec)
85
+ sys.modules['mcp.server.stdio'] = mcp_stdio
86
+ spec.loader.exec_module(mcp_stdio)
87
+
88
+ Tool = mcp_types.Tool
89
+ TextContent = mcp_types.TextContent
90
+ Server = mcp_server_mod.Server
91
+ stdio_server = mcp_stdio.stdio_server
92
+ break
93
+ else:
94
+ raise ImportError("Cannot find mcp package")
95
+
96
+
97
+ class MobileMCPServer:
98
+ """Mobile MCP Server - 精简版"""
99
+
100
+ def __init__(self):
101
+ self.client = None
102
+ self.tools = None
103
+ self._initialized = False
104
+ self._last_error = None # 保存最后一次连接失败的错误
105
+
106
+ @staticmethod
107
+ def format_response(result) -> str:
108
+ """统一格式化返回值"""
109
+ if isinstance(result, (dict, list)):
110
+ return json.dumps(result, ensure_ascii=False, indent=2)
111
+ return str(result)
112
+
113
+ async def initialize(self):
114
+ """延迟初始化设备连接"""
115
+ # 如果已成功初始化,检查连接是否仍然有效
116
+ if self._initialized and self.tools is not None:
117
+ # 验证设备连接是否仍然有效
118
+ if self._is_connection_valid():
119
+ return
120
+ else:
121
+ # 连接已失效,重置状态
122
+ print("⚠️ 检测到设备连接已断开,正在重新连接...", file=sys.stderr)
123
+ self._initialized = False
124
+ self.client = None
125
+ self.tools = None
126
+
127
+ platform = self._detect_platform()
128
+
129
+ try:
130
+ # 尝试导入,如果失败会抛出 ImportError
131
+ try:
132
+ from mobile_mcp.core.mobile_client import MobileClient
133
+ from mobile_mcp.core.basic_tools_lite import BasicMobileToolsLite
134
+ except ImportError as import_err:
135
+ # 如果导入失败,尝试从源码路径导入
136
+ # 这通常发生在开发模式下,包未安装时
137
+ project_root = Path(__file__).parent.parent
138
+ if str(project_root) not in sys.path:
139
+ sys.path.insert(0, str(project_root))
140
+ # 再次尝试导入
141
+ from mobile_mcp.core.mobile_client import MobileClient
142
+ from mobile_mcp.core.basic_tools_lite import BasicMobileToolsLite
143
+
144
+ self.client = MobileClient(platform=platform)
145
+ self.tools = BasicMobileToolsLite(self.client)
146
+ self._initialized = True # 只在成功时标记
147
+ print(f"📱 已连接到 {platform.upper()} 设备", file=sys.stderr)
148
+ except Exception as e:
149
+ error_msg = str(e)
150
+ print(f"⚠️ 设备连接失败: {error_msg},下次调用时将重试", file=sys.stderr)
151
+ self.client = None
152
+ self.tools = None
153
+ self._last_error = error_msg # 保存错误信息
154
+ # 不设置 _initialized = True,下次调用会重试
155
+
156
+ def _is_connection_valid(self) -> bool:
157
+ """检查设备连接是否仍然有效"""
158
+ try:
159
+ if self.client is None:
160
+ return False
161
+
162
+ # Android: 检查 u2 连接
163
+ if hasattr(self.client, 'u2') and self.client.u2:
164
+ # 尝试获取设备信息,如果失败说明连接断开
165
+ self.client.u2.info
166
+ return True
167
+
168
+ # iOS: 检查 wda 连接
169
+ if hasattr(self.client, 'wda') and self.client.wda:
170
+ self.client.wda.status()
171
+ return True
172
+
173
+ # iOS (通过 _ios_client)
174
+ if hasattr(self.client, '_ios_client') and self.client._ios_client:
175
+ if hasattr(self.client._ios_client, 'wda') and self.client._ios_client.wda:
176
+ self.client._ios_client.wda.status()
177
+ return True
178
+
179
+ return False
180
+ except Exception:
181
+ return False
182
+
183
+ def _detect_platform(self) -> str:
184
+ """自动检测设备平台"""
185
+ platform = os.getenv("MOBILE_PLATFORM", "").lower()
186
+ if platform in ["android", "ios"]:
187
+ return platform
188
+
189
+ # 尝试检测 iOS 设备
190
+ try:
191
+ from mobile_mcp.core.ios_device_manager_wda import IOSDeviceManagerWDA
192
+ ios_manager = IOSDeviceManagerWDA()
193
+ if ios_manager.list_devices():
194
+ return "ios"
195
+ except:
196
+ pass
197
+
198
+ return "android"
199
+
200
+ def get_tools(self):
201
+ """注册 MCP 工具(20 个)"""
202
+ tools = []
203
+
204
+ # ==================== 元素定位(优先使用)====================
205
+ tools.append(Tool(
206
+ name="mobile_list_elements",
207
+ description="📋 列出页面所有可交互元素\n\n"
208
+ "⚠️ 【重要】点击元素前必须先调用此工具!\n"
209
+ "如果元素在控件树中存在,使用 click_by_id 或 click_by_text 定位。\n"
210
+ "只有当此工具返回空或找不到目标元素时,才使用截图+坐标方式。\n\n"
211
+ "📌 控件树定位优势:\n"
212
+ "- 实时检测元素是否存在\n"
213
+ "- 元素消失时会报错,不会误点击\n"
214
+ "- 跨设备兼容性好",
215
+ inputSchema={"type": "object", "properties": {}, "required": []}
216
+ ))
217
+
218
+ # ==================== 截图(视觉兜底)====================
219
+ tools.append(Tool(
220
+ name="mobile_take_screenshot",
221
+ description="📸 截图查看屏幕内容\n\n"
222
+ "⚠️ 【推荐使用 mobile_screenshot_with_som 代替!】\n"
223
+ "SoM 截图会给元素标号,AI 可以直接说'点击几号',更精准!\n\n"
224
+ "🎯 本工具仅用于:\n"
225
+ "- 快速确认页面状态(不需要点击时)\n"
226
+ "- 操作后确认结果\n"
227
+ "- compress=false 时可获取原始分辨率截图(用于添加模板)\n\n"
228
+ "💡 如需点击元素,请用 mobile_screenshot_with_som + mobile_click_by_som",
229
+ inputSchema={
230
+ "type": "object",
231
+ "properties": {
232
+ "description": {"type": "string", "description": "截图描述(可选)"},
233
+ "compress": {"type": "boolean", "description": "是否压缩,默认 true。设为 false 可获取原始分辨率(用于模板添加)", "default": True},
234
+ "crop_x": {"type": "integer", "description": "局部裁剪中心 X 坐标(屏幕坐标,0 表示不裁剪)"},
235
+ "crop_y": {"type": "integer", "description": "局部裁剪中心 Y 坐标(屏幕坐标,0 表示不裁剪)"},
236
+ "crop_size": {"type": "integer", "description": "裁剪区域大小(推荐 200-400,0 表示不裁剪)"}
237
+ },
238
+ "required": []
239
+ }
240
+ ))
241
+
242
+ tools.append(Tool(
243
+ name="mobile_get_screen_size",
244
+ description="📐 获取屏幕尺寸。用于确认坐标范围。",
245
+ inputSchema={"type": "object", "properties": {}, "required": []}
246
+ ))
247
+
248
+ tools.append(Tool(
249
+ name="mobile_screenshot_with_som",
250
+ description="📸🏷️ Set-of-Mark 截图(⭐⭐ 强烈推荐!默认截图方式)\n\n"
251
+ "【智能标注】给每个可点击元素画框+编号,检测弹窗时额外标注可能的X按钮位置(黄色)。\n"
252
+ "AI 看图直接说'点击 3 号',调用 mobile_click_by_som(3) 即可!\n\n"
253
+ "🎯 优势:\n"
254
+ "- 元素有编号,精准点击不会误触\n"
255
+ "- 自动检测弹窗,标注可能的关闭按钮位置\n"
256
+ "- 适用于所有页面和所有操作\n\n"
257
+ "⚡ 推荐流程:\n"
258
+ "1. 任何需要操作的场景,都先调用此工具\n"
259
+ "2. 看标注图,找到目标元素编号\n"
260
+ "3. 调用 mobile_click_by_som(编号) 精准点击\n"
261
+ "4. 🔴【必须】点击后再次截图确认操作是否成功!",
262
+ inputSchema={"type": "object", "properties": {}, "required": []}
263
+ ))
264
+
265
+ tools.append(Tool(
266
+ name="mobile_click_by_som",
267
+ description="🎯 根据 SoM 编号点击元素\n\n"
268
+ "配合 mobile_screenshot_with_som 使用。\n"
269
+ "看图后直接说'点击 3 号',调用此函数即可。\n\n"
270
+ "⚠️ 【重要】点击后建议再次截图确认操作是否成功!",
271
+ inputSchema={
272
+ "type": "object",
273
+ "properties": {
274
+ "index": {
275
+ "type": "integer",
276
+ "description": "元素编号(从 1 开始,对应截图中的标注数字)"
277
+ }
278
+ },
279
+ "required": ["index"]
280
+ }
281
+ ))
282
+
283
+ tools.append(Tool(
284
+ name="mobile_screenshot_with_grid",
285
+ description="📸📏 带网格坐标的截图(精确定位神器!)\n\n"
286
+ "在截图上绘制网格线和坐标刻度,帮助快速定位元素位置。\n"
287
+ "如果检测到弹窗,会用绿色圆圈标注可能的关闭按钮位置。\n\n"
288
+ "🎯 适用场景:\n"
289
+ "- 需要精确知道某个元素的坐标\n"
290
+ "- 关闭广告弹窗时定位 X 按钮\n"
291
+ "- 元素不在控件树中时的视觉定位\n\n"
292
+ "💡 返回信息:\n"
293
+ "- 带网格标注的截图\n"
294
+ "- 弹窗边界坐标(如果检测到)\n"
295
+ "- 可能的关闭按钮位置列表(带优先级)\n\n"
296
+ "🔴 【必须】点击后必须再次截图确认操作是否成功!",
297
+ inputSchema={
298
+ "type": "object",
299
+ "properties": {
300
+ "grid_size": {
301
+ "type": "integer",
302
+ "description": "网格间距(像素),默认 100。值越小网格越密,建议 50-200"
303
+ },
304
+ "show_popup_hints": {
305
+ "type": "boolean",
306
+ "description": "是否显示弹窗关闭按钮提示位置,默认 true"
307
+ }
308
+ },
309
+ "required": []
310
+ }
311
+ ))
312
+
313
+ # ==================== 点击操作 ====================
314
+ tools.append(Tool(
315
+ name="mobile_click_by_text",
316
+ description="👆 通过文本点击元素(推荐)\n\n"
317
+ "✅ 实时检测元素是否存在,元素不存在会报错\n"
318
+ "✅ 不会误点击到其他位置\n"
319
+ "📋 使用前先调用 mobile_list_elements 确认元素文本",
320
+ inputSchema={
321
+ "type": "object",
322
+ "properties": {
323
+ "text": {"type": "string", "description": "元素的文本内容(精确匹配)"}
324
+ },
325
+ "required": ["text"]
326
+ }
327
+ ))
328
+
329
+ tools.append(Tool(
330
+ name="mobile_click_by_id",
331
+ description="👆 通过 resource-id 点击元素(最推荐)\n\n"
332
+ "✅ 最稳定的定位方式\n"
333
+ "✅ 实时检测元素是否存在,元素不存在会报错\n"
334
+ "📋 使用前先调用 mobile_list_elements 获取元素 ID\n"
335
+ "💡 当有多个相同 ID 的元素时,用 index 指定第几个(从 0 开始)",
336
+ inputSchema={
337
+ "type": "object",
338
+ "properties": {
339
+ "resource_id": {"type": "string", "description": "元素的 resource-id"},
340
+ "index": {"type": "integer", "description": "第几个元素(从 0 开始),默认 0 表示第一个", "default": 0}
341
+ },
342
+ "required": ["resource_id"]
343
+ }
344
+ ))
345
+
346
+ tools.append(Tool(
347
+ name="mobile_click_at_coords",
348
+ description="👆 点击指定坐标(兜底方案)\n\n"
349
+ "⚠️ 【重要】优先使用 mobile_click_by_id 或 mobile_click_by_text!\n"
350
+ "仅在 mobile_list_elements 无法获取元素时使用此工具。\n\n"
351
+ "⚠️ 【时序限制】截图分析期间页面可能变化:\n"
352
+ "- 坐标是基于截图时刻的,点击时页面可能已不同\n"
353
+ "- 如果误点击,调用 mobile_press_key(back) 返回\n"
354
+ "- 对于定时弹窗(如广告),建议等待其自动消失\n\n"
355
+ "📐 坐标转换:截图返回的 image_width/height 等参数直接传入即可\n\n"
356
+ "🔴 【必须】点击后必须再次截图确认操作是否成功!",
357
+ inputSchema={
358
+ "type": "object",
359
+ "properties": {
360
+ "x": {"type": "number", "description": "X 坐标(来自 AI 分析截图)"},
361
+ "y": {"type": "number", "description": "Y 坐标(来自 AI 分析截图)"},
362
+ "image_width": {"type": "number", "description": "压缩后图片宽度(截图返回的 image_width)"},
363
+ "image_height": {"type": "number", "description": "压缩后图片高度(截图返回的 image_height)"},
364
+ "original_img_width": {"type": "number", "description": "原图宽度(截图返回的 original_img_width)"},
365
+ "original_img_height": {"type": "number", "description": "原图高度(截图返回的 original_img_height)"},
366
+ "crop_offset_x": {"type": "number", "description": "局部截图 X 偏移(裁剪截图时传入)"},
367
+ "crop_offset_y": {"type": "number", "description": "局部截图 Y 偏移(裁剪截图时传入)"}
368
+ },
369
+ "required": ["x", "y"]
370
+ }
371
+ ))
372
+
373
+ tools.append(Tool(
374
+ name="mobile_click_by_percent",
375
+ description="👆 通过百分比位置点击(跨设备兼容!)。\n\n"
376
+ "🎯 原理:屏幕左上角是 (0%, 0%),右下角是 (100%, 100%)\n"
377
+ "📐 示例:\n"
378
+ " - (50, 50) = 屏幕正中央\n"
379
+ " - (10, 5) = 左上角附近\n"
380
+ " - (85, 90) = 右下角附近\n\n"
381
+ "✅ 优势:同样的百分比在不同分辨率设备上都能点到相同相对位置\n"
382
+ "💡 录制一次,多设备回放\n\n"
383
+ "🔴 【必须】点击后必须再次截图确认操作是否成功!",
384
+ inputSchema={
385
+ "type": "object",
386
+ "properties": {
387
+ "x_percent": {"type": "number", "description": "X 轴百分比 (0-100),0=最左,50=中间,100=最右"},
388
+ "y_percent": {"type": "number", "description": "Y 轴百分比 (0-100),0=最上,50=中间,100=最下"}
389
+ },
390
+ "required": ["x_percent", "y_percent"]
391
+ }
392
+ ))
393
+
394
+ # ==================== 长按操作 ====================
395
+ tools.append(Tool(
396
+ name="mobile_long_press_by_id",
397
+ description="👆 通过 resource-id 长按(⭐⭐ 最稳定!)\n\n"
398
+ "✅ 最稳定的长按定位方式,跨设备完美兼容\n"
399
+ "📋 使用前请先调用 mobile_list_elements 获取元素 ID\n"
400
+ "💡 生成的脚本使用 d(resourceId='...').long_click() 定位,最稳定",
401
+ inputSchema={
402
+ "type": "object",
403
+ "properties": {
404
+ "resource_id": {"type": "string", "description": "元素的 resource-id"},
405
+ "duration": {"type": "number", "description": "长按持续时间(秒),默认 1.0"}
406
+ },
407
+ "required": ["resource_id"]
408
+ }
409
+ ))
410
+
411
+ tools.append(Tool(
412
+ name="mobile_long_press_by_text",
413
+ description="👆 通过文本长按(⭐ 推荐!)\n\n"
414
+ "✅ 优势:跨设备兼容,不受屏幕分辨率影响\n"
415
+ "📋 使用前请先调用 mobile_list_elements 确认元素有文本\n"
416
+ "💡 生成的脚本使用 d(text='...').long_click() 定位,稳定可靠",
417
+ inputSchema={
418
+ "type": "object",
419
+ "properties": {
420
+ "text": {"type": "string", "description": "元素的文本内容(精确匹配)"},
421
+ "duration": {"type": "number", "description": "长按持续时间(秒),默认 1.0"}
422
+ },
423
+ "required": ["text"]
424
+ }
425
+ ))
426
+
427
+ tools.append(Tool(
428
+ name="mobile_long_press_by_percent",
429
+ description="👆 通过百分比位置长按(跨设备兼容!)\n\n"
430
+ "🎯 原理:屏幕左上角是 (0%, 0%),右下角是 (100%, 100%)\n"
431
+ "📐 示例:\n"
432
+ " - (50, 50) = 屏幕正中央\n"
433
+ " - (10, 5) = 左上角附近\n"
434
+ " - (85, 90) = 右下角附近\n\n"
435
+ "✅ 优势:同样的百分比在不同分辨率设备上都能长按到相同相对位置\n"
436
+ "💡 录制一次,多设备回放",
437
+ inputSchema={
438
+ "type": "object",
439
+ "properties": {
440
+ "x_percent": {"type": "number", "description": "X 轴百分比 (0-100)"},
441
+ "y_percent": {"type": "number", "description": "Y 轴百分比 (0-100)"},
442
+ "duration": {"type": "number", "description": "长按持续时间(秒),默认 1.0"}
443
+ },
444
+ "required": ["x_percent", "y_percent"]
445
+ }
446
+ ))
447
+
448
+ tools.append(Tool(
449
+ name="mobile_long_press_at_coords",
450
+ description="👆 长按指定坐标(⚠️ 兜底方案,优先用 ID/文本定位!)\n\n"
451
+ "🎯 仅在以下场景使用:\n"
452
+ "- 游戏(Unity/Cocos)无法获取元素\n"
453
+ "- mobile_list_elements 返回空\n"
454
+ "- 元素没有 id 和 text\n\n"
455
+ "⚠️ 【坐标转换】截图返回的参数直接传入:\n"
456
+ " - image_width/image_height: 压缩后尺寸(AI 看到的)\n"
457
+ " - original_img_width/original_img_height: 原图尺寸(用于转换)\n"
458
+ " - crop_offset_x/crop_offset_y: 局部截图偏移\n\n"
459
+ "✅ 自动记录百分比坐标,生成脚本时转换为跨分辨率兼容的百分比定位",
460
+ inputSchema={
461
+ "type": "object",
462
+ "properties": {
463
+ "x": {"type": "number", "description": "X 坐标(来自 AI 分析截图)"},
464
+ "y": {"type": "number", "description": "Y 坐标(来自 AI 分析截图)"},
465
+ "duration": {"type": "number", "description": "长按持续时间(秒),默认 1.0"},
466
+ "image_width": {"type": "number", "description": "压缩后图片宽度"},
467
+ "image_height": {"type": "number", "description": "压缩后图片高度"},
468
+ "original_img_width": {"type": "number", "description": "原图宽度"},
469
+ "original_img_height": {"type": "number", "description": "原图高度"},
470
+ "crop_offset_x": {"type": "number", "description": "局部截图 X 偏移"},
471
+ "crop_offset_y": {"type": "number", "description": "局部截图 Y 偏移"}
472
+ },
473
+ "required": ["x", "y"]
474
+ }
475
+ ))
476
+
477
+ # ==================== 输入操作 ====================
478
+ tools.append(Tool(
479
+ name="mobile_input_text_by_id",
480
+ description="⌨️ 在输入框输入文本。需要先用 mobile_list_elements 获取输入框 ID。",
481
+ inputSchema={
482
+ "type": "object",
483
+ "properties": {
484
+ "resource_id": {"type": "string", "description": "输入框的 resource-id"},
485
+ "text": {"type": "string", "description": "要输入的文本"}
486
+ },
487
+ "required": ["resource_id", "text"]
488
+ }
489
+ ))
490
+
491
+ tools.append(Tool(
492
+ name="mobile_input_at_coords",
493
+ description="⌨️ 点击坐标后输入文本。适合游戏等无法获取元素 ID 的场景。",
494
+ inputSchema={
495
+ "type": "object",
496
+ "properties": {
497
+ "x": {"type": "number", "description": "输入框 X 坐标"},
498
+ "y": {"type": "number", "description": "输入框 Y 坐标"},
499
+ "text": {"type": "string", "description": "要输入的文本"}
500
+ },
501
+ "required": ["x", "y", "text"]
502
+ }
503
+ ))
504
+
505
+ # ==================== 导航操作 ====================
506
+ tools.append(Tool(
507
+ name="mobile_swipe",
508
+ description="👆 滑动屏幕。方向:up/down/left/right\n\n"
509
+ "💡 左右滑动时,可指定高度坐标或百分比:\n"
510
+ "- y: 指定高度坐标(像素)\n"
511
+ "- y_percent: 指定高度百分比 (0-100)\n"
512
+ "- 两者都未指定时,使用屏幕中心高度",
513
+ inputSchema={
514
+ "type": "object",
515
+ "properties": {
516
+ "direction": {
517
+ "type": "string",
518
+ "enum": ["up", "down", "left", "right"],
519
+ "description": "滑动方向"
520
+ },
521
+ "y": {
522
+ "type": "integer",
523
+ "description": "左右滑动时指定的高度坐标(像素,0-屏幕高度)"
524
+ },
525
+ "y_percent": {
526
+ "type": "number",
527
+ "description": "左右滑动时指定的高度百分比 (0-100)"
528
+ }
529
+ },
530
+ "required": ["direction"]
531
+ }
532
+ ))
533
+
534
+ tools.append(Tool(
535
+ name="mobile_press_key",
536
+ description="⌨️ 按键操作。支持:home, back, enter, search",
537
+ inputSchema={
538
+ "type": "object",
539
+ "properties": {
540
+ "key": {"type": "string", "description": "按键名称:home, back, enter, search"}
541
+ },
542
+ "required": ["key"]
543
+ }
544
+ ))
545
+
546
+ tools.append(Tool(
547
+ name="mobile_wait",
548
+ description="⏰ 等待指定时间。用于等待页面加载、动画完成等。",
549
+ inputSchema={
550
+ "type": "object",
551
+ "properties": {
552
+ "seconds": {"type": "number", "description": "等待时间(秒)"}
553
+ },
554
+ "required": ["seconds"]
555
+ }
556
+ ))
557
+
558
+ # ==================== 应用管理 ====================
559
+ tools.append(Tool(
560
+ name="mobile_launch_app",
561
+ description="🚀 启动应用。启动后建议等待 2-3 秒让页面加载。",
562
+ inputSchema={
563
+ "type": "object",
564
+ "properties": {
565
+ "package_name": {"type": "string", "description": "应用包名"}
566
+ },
567
+ "required": ["package_name"]
568
+ }
569
+ ))
570
+
571
+ tools.append(Tool(
572
+ name="mobile_terminate_app",
573
+ description="⏹️ 终止应用。",
574
+ inputSchema={
575
+ "type": "object",
576
+ "properties": {
577
+ "package_name": {"type": "string", "description": "应用包名"}
578
+ },
579
+ "required": ["package_name"]
580
+ }
581
+ ))
582
+
583
+ tools.append(Tool(
584
+ name="mobile_list_apps",
585
+ description="📦 列出已安装的应用。可按关键词过滤。",
586
+ inputSchema={
587
+ "type": "object",
588
+ "properties": {
589
+ "filter": {"type": "string", "description": "过滤关键词(可选)"}
590
+ },
591
+ "required": []
592
+ }
593
+ ))
594
+
595
+ # ==================== 设备管理 ====================
596
+ tools.append(Tool(
597
+ name="mobile_list_devices",
598
+ description="📱 列出已连接的设备。",
599
+ inputSchema={"type": "object", "properties": {}, "required": []}
600
+ ))
601
+
602
+ tools.append(Tool(
603
+ name="mobile_check_connection",
604
+ description="🔌 检查设备连接状态。",
605
+ inputSchema={"type": "object", "properties": {}, "required": []}
606
+ ))
607
+
608
+ # ==================== 辅助工具 ====================
609
+ tools.append(Tool(
610
+ name="mobile_find_close_button",
611
+ description="""🔍 智能查找关闭按钮(只找不点,返回位置)
612
+
613
+ 从元素树中找最可能的关闭按钮,返回坐标和百分比位置。
614
+
615
+ 🎯 识别策略(优先级):
616
+ 1. 文本匹配:×、X、关闭、取消、跳过 等
617
+ 2. 描述匹配:content-desc 包含 close/关闭
618
+ 3. 小尺寸 clickable 元素(右上角优先)
619
+
620
+ ✅ 返回内容:
621
+ - 坐标 (x, y) 和百分比 (x%, y%)
622
+ - 推荐的点击命令:mobile_click_by_percent(x%, y%)
623
+ - 多个候选位置(供确认)
624
+
625
+ 💡 使用流程:
626
+ 1. 调用此工具找到关闭按钮位置
627
+ 2. 确认位置正确后,用 mobile_click_by_percent 点击
628
+ 3. 百分比点击兼容不同分辨率手机""",
629
+ inputSchema={"type": "object", "properties": {}, "required": []}
630
+ ))
631
+
632
+ tools.append(Tool(
633
+ name="mobile_close_popup",
634
+ description="""🚫 智能关闭弹窗
635
+
636
+ 通过控件树识别并点击关闭按钮(×、关闭、跳过等)。
637
+
638
+ ✅ 控件树有元素时:直接点击,实时可靠
639
+ ❌ 控件树无元素时:截图供 AI 分析
640
+
641
+ ⚠️ 【时序限制】如果需要截图分析:
642
+ - 分析期间弹窗可能自动消失
643
+ - 对于定时弹窗(如广告),建议等待其自动消失
644
+ - 点击前可再次截图确认弹窗是否还在
645
+
646
+ 🔴 【必须】点击关闭后,必须再次截图确认弹窗是否真的关闭了!
647
+ 如果弹窗仍在,需要尝试其他方法或位置。""",
648
+ inputSchema={"type": "object", "properties": {}, "required": []}
649
+ ))
650
+
651
+ tools.append(Tool(
652
+ name="mobile_assert_text",
653
+ description="✅ 检查页面是否包含指定文本。用于验证操作结果。",
654
+ inputSchema={
655
+ "type": "object",
656
+ "properties": {
657
+ "text": {"type": "string", "description": "要检查的文本"}
658
+ },
659
+ "required": ["text"]
660
+ }
661
+ ))
662
+
663
+ # ==================== pytest 脚本生成 ====================
664
+ tools.append(Tool(
665
+ name="mobile_get_operation_history",
666
+ description="📜 获取操作历史记录。",
667
+ inputSchema={
668
+ "type": "object",
669
+ "properties": {
670
+ "limit": {"type": "number", "description": "返回最近的N条记录"}
671
+ },
672
+ "required": []
673
+ }
674
+ ))
675
+
676
+ tools.append(Tool(
677
+ name="mobile_clear_operation_history",
678
+ description="🗑️ 清空操作历史记录。\n\n"
679
+ "⚠️ 开始新的测试录制前必须调用!\n"
680
+ "📋 录制流程:清空历史 → 执行操作(优先用ID/文本定位)→ 生成脚本",
681
+ inputSchema={"type": "object", "properties": {}, "required": []}
682
+ ))
683
+
684
+ tools.append(Tool(
685
+ name="mobile_generate_test_script",
686
+ description="📝 生成 pytest 测试脚本。基于操作历史自动生成。\n\n"
687
+ "⚠️ 【重要】录制操作时请优先使用稳定定位:\n"
688
+ "1️⃣ 先调用 mobile_list_elements 获取元素列表\n"
689
+ "2️⃣ 优先用 mobile_click_by_id(最稳定,跨设备兼容)\n"
690
+ "3️⃣ 其次用 mobile_click_by_text(稳定)\n"
691
+ "4️⃣ 最后才用坐标点击(会自动转百分比,跨分辨率兼容)\n\n"
692
+ "使用流程:\n"
693
+ "1. 清空历史 mobile_clear_operation_history\n"
694
+ "2. 执行操作(优先用 ID/文本定位)\n"
695
+ "3. 调用此工具生成脚本\n"
696
+ "4. 脚本保存到 tests/ 目录\n\n"
697
+ "💡 定位优先级:ID > 文本 > 百分比 > 坐标",
698
+ inputSchema={
699
+ "type": "object",
700
+ "properties": {
701
+ "test_name": {"type": "string", "description": "测试用例名称"},
702
+ "package_name": {"type": "string", "description": "App 包名"},
703
+ "filename": {"type": "string", "description": "脚本文件名(不含 .py)"}
704
+ },
705
+ "required": ["test_name", "package_name", "filename"]
706
+ }
707
+ ))
708
+
709
+ # ==================== 广告弹窗关闭工具 ====================
710
+ tools.append(Tool(
711
+ name="mobile_close_ad",
712
+ description="""🚫 【推荐】智能关闭广告弹窗
713
+
714
+ 专门用于关闭广告弹窗,按优先级自动尝试多种方式:
715
+
716
+ 1️⃣ **控件树查找**(最可靠)
717
+ - 自动查找"关闭"、"跳过"、"×"等关闭按钮
718
+ - 找到直接点击,实时可靠
719
+
720
+ 2️⃣ **模板匹配**(次优)
721
+ - 用 OpenCV 匹配已保存的 X 按钮模板
722
+ - 需要积累模板库,模板越多成功率越高
723
+
724
+ 3️⃣ **返回截图供 AI 分析**(兜底)
725
+ - 如果前两步失败,返回截图
726
+ - AI 分析后用 mobile_click_by_percent 点击
727
+ - 点击成功后用 mobile_template_add 添加模板(自动学习)
728
+
729
+ 💡 使用流程:
730
+ 1. 遇到广告弹窗 → 调用此工具
731
+ 2. 如果成功 → 完成
732
+ 3. 如果失败 → 看截图找 X → 点击 → 添加模板""",
733
+ inputSchema={
734
+ "type": "object",
735
+ "properties": {},
736
+ "required": []
737
+ }
738
+ ))
739
+
740
+ tools.append(Tool(
741
+ name="mobile_template_close",
742
+ description="""🎯 模板匹配关闭弹窗(仅模板匹配)
743
+
744
+ 只用 OpenCV 模板匹配,不走控件树。
745
+ 一般建议用 mobile_close_ad 代替(会自动先查控件树)。
746
+
747
+ ⚙️ 参数:
748
+ - click: 是否点击,默认 true
749
+ - threshold: 匹配阈值 0-1,默认 0.75""",
750
+ inputSchema={
751
+ "type": "object",
752
+ "properties": {
753
+ "click": {"type": "boolean", "description": "是否点击,默认 true"},
754
+ "threshold": {"type": "number", "description": "匹配阈值 0-1,默认 0.75"}
755
+ },
756
+ "required": []
757
+ }
758
+ ))
759
+
760
+ tools.append(Tool(
761
+ name="mobile_template_add",
762
+ description="""➕ 添加 X 号模板
763
+
764
+ 遇到新样式 X 号时,截图并添加到模板库。
765
+
766
+ ⚙️ 两种方式(二选一):
767
+ 1. 百分比定位(推荐):提供 x_percent, y_percent, size
768
+ 2. 像素定位:提供 screenshot_path, x, y, width, height
769
+
770
+ 📋 流程:
771
+ 1. mobile_screenshot_with_grid 查看 X 号位置
772
+ 2. 调用此工具添加模板
773
+ 3. 下次同样 X 号就能自动匹配
774
+
775
+ 💡 百分比示例:X 在右上角 → x_percent=85, y_percent=12, size=80""",
776
+ inputSchema={
777
+ "type": "object",
778
+ "properties": {
779
+ "template_name": {"type": "string", "description": "模板名称"},
780
+ "x_percent": {"type": "number", "description": "X号中心水平百分比 (0-100)"},
781
+ "y_percent": {"type": "number", "description": "X号中心垂直百分比 (0-100)"},
782
+ "size": {"type": "integer", "description": "裁剪正方形边长(像素)"},
783
+ "screenshot_path": {"type": "string", "description": "截图路径(像素定位时用)"},
784
+ "x": {"type": "integer", "description": "左上角 X 坐标"},
785
+ "y": {"type": "integer", "description": "左上角 Y 坐标"},
786
+ "width": {"type": "integer", "description": "裁剪宽度"},
787
+ "height": {"type": "integer", "description": "裁剪高度"}
788
+ },
789
+ "required": ["template_name"]
790
+ }
791
+ ))
792
+
793
+ return tools
794
+
795
+ async def handle_tool_call(self, name: str, arguments: dict):
796
+ """处理工具调用"""
797
+ await self.initialize()
798
+
799
+ if not self.tools:
800
+ # 提供详细的错误信息和解决方案
801
+ error_detail = self._last_error or "未知错误"
802
+ help_msg = (
803
+ f"❌ 设备连接失败\n\n"
804
+ f"错误详情: {error_detail}\n\n"
805
+ f"🔧 解决方案:\n"
806
+ f"1. 检查 USB 连接: adb devices\n"
807
+ f"2. 重启 adb: adb kill-server && adb start-server\n"
808
+ f"3. 初始化 uiautomator2: python -m uiautomator2 init\n"
809
+ f"4. 手机上允许 USB 调试授权\n"
810
+ f"5. 确保手机已解锁\n\n"
811
+ f"完成后请重试操作。"
812
+ )
813
+ return [TextContent(type="text", text=help_msg)]
814
+
815
+ try:
816
+ # 截图
817
+ if name == "mobile_take_screenshot":
818
+ result = self.tools.take_screenshot(
819
+ description=arguments.get("description", ""),
820
+ compress=arguments.get("compress", True),
821
+ crop_x=arguments.get("crop_x", 0),
822
+ crop_y=arguments.get("crop_y", 0),
823
+ crop_size=arguments.get("crop_size", 0)
824
+ )
825
+ return [TextContent(type="text", text=self.format_response(result))]
826
+
827
+ elif name == "mobile_get_screen_size":
828
+ result = self.tools.get_screen_size()
829
+ return [TextContent(type="text", text=self.format_response(result))]
830
+
831
+ elif name == "mobile_screenshot_with_grid":
832
+ result = self.tools.take_screenshot_with_grid(
833
+ grid_size=arguments.get("grid_size", 100),
834
+ show_popup_hints=arguments.get("show_popup_hints", True)
835
+ )
836
+ return [TextContent(type="text", text=self.format_response(result))]
837
+
838
+ elif name == "mobile_screenshot_with_som":
839
+ result = self.tools.take_screenshot_with_som()
840
+ return [TextContent(type="text", text=self.format_response(result))]
841
+
842
+ elif name == "mobile_click_by_som":
843
+ result = self.tools.click_by_som(arguments["index"])
844
+ return [TextContent(type="text", text=self.format_response(result))]
845
+
846
+ # 点击
847
+ elif name == "mobile_click_at_coords":
848
+ result = self.tools.click_at_coords(
849
+ arguments["x"],
850
+ arguments["y"],
851
+ arguments.get("image_width", 0),
852
+ arguments.get("image_height", 0),
853
+ arguments.get("crop_offset_x", 0),
854
+ arguments.get("crop_offset_y", 0),
855
+ arguments.get("original_img_width", 0),
856
+ arguments.get("original_img_height", 0)
857
+ )
858
+ return [TextContent(type="text", text=self.format_response(result))]
859
+
860
+ elif name == "mobile_click_by_text":
861
+ result = self.tools.click_by_text(arguments["text"])
862
+ return [TextContent(type="text", text=self.format_response(result))]
863
+
864
+ elif name == "mobile_click_by_id":
865
+ result = self.tools.click_by_id(
866
+ arguments["resource_id"],
867
+ arguments.get("index", 0)
868
+ )
869
+ return [TextContent(type="text", text=self.format_response(result))]
870
+
871
+ elif name == "mobile_click_by_percent":
872
+ result = self.tools.click_by_percent(arguments["x_percent"], arguments["y_percent"])
873
+ return [TextContent(type="text", text=self.format_response(result))]
874
+
875
+ # 长按
876
+ elif name == "mobile_long_press_by_id":
877
+ result = self.tools.long_press_by_id(
878
+ arguments["resource_id"],
879
+ arguments.get("duration", 1.0)
880
+ )
881
+ return [TextContent(type="text", text=self.format_response(result))]
882
+
883
+ elif name == "mobile_long_press_by_text":
884
+ result = self.tools.long_press_by_text(
885
+ arguments["text"],
886
+ arguments.get("duration", 1.0)
887
+ )
888
+ return [TextContent(type="text", text=self.format_response(result))]
889
+
890
+ elif name == "mobile_long_press_by_percent":
891
+ result = self.tools.long_press_by_percent(
892
+ arguments["x_percent"],
893
+ arguments["y_percent"],
894
+ arguments.get("duration", 1.0)
895
+ )
896
+ return [TextContent(type="text", text=self.format_response(result))]
897
+
898
+ elif name == "mobile_long_press_at_coords":
899
+ result = self.tools.long_press_at_coords(
900
+ arguments["x"],
901
+ arguments["y"],
902
+ arguments.get("duration", 1.0),
903
+ arguments.get("image_width", 0),
904
+ arguments.get("image_height", 0),
905
+ arguments.get("crop_offset_x", 0),
906
+ arguments.get("crop_offset_y", 0),
907
+ arguments.get("original_img_width", 0),
908
+ arguments.get("original_img_height", 0)
909
+ )
910
+ return [TextContent(type="text", text=self.format_response(result))]
911
+
912
+ # 输入
913
+ elif name == "mobile_input_text_by_id":
914
+ result = self.tools.input_text_by_id(arguments["resource_id"], arguments["text"])
915
+ return [TextContent(type="text", text=self.format_response(result))]
916
+
917
+ elif name == "mobile_input_at_coords":
918
+ result = self.tools.input_at_coords(arguments["x"], arguments["y"], arguments["text"])
919
+ return [TextContent(type="text", text=self.format_response(result))]
920
+
921
+ # 导航
922
+ elif name == "mobile_swipe":
923
+ result = await self.tools.swipe(
924
+ arguments["direction"],
925
+ y=arguments.get("y"),
926
+ y_percent=arguments.get("y_percent")
927
+ )
928
+ return [TextContent(type="text", text=self.format_response(result))]
929
+
930
+ elif name == "mobile_press_key":
931
+ result = await self.tools.press_key(arguments["key"])
932
+ return [TextContent(type="text", text=self.format_response(result))]
933
+
934
+ elif name == "mobile_wait":
935
+ result = self.tools.wait(arguments["seconds"])
936
+ return [TextContent(type="text", text=self.format_response(result))]
937
+
938
+ # 应用管理
939
+ elif name == "mobile_launch_app":
940
+ result = await self.tools.launch_app(arguments["package_name"])
941
+ return [TextContent(type="text", text=self.format_response(result))]
942
+
943
+ elif name == "mobile_terminate_app":
944
+ result = self.tools.terminate_app(arguments["package_name"])
945
+ return [TextContent(type="text", text=self.format_response(result))]
946
+
947
+ elif name == "mobile_list_apps":
948
+ result = self.tools.list_apps(arguments.get("filter", ""))
949
+ return [TextContent(type="text", text=self.format_response(result))]
950
+
951
+ # 设备管理
952
+ elif name == "mobile_list_devices":
953
+ result = self.tools.list_devices()
954
+ return [TextContent(type="text", text=self.format_response(result))]
955
+
956
+ elif name == "mobile_check_connection":
957
+ result = self.tools.check_connection()
958
+ return [TextContent(type="text", text=self.format_response(result))]
959
+
960
+ # 辅助
961
+ elif name == "mobile_list_elements":
962
+ result = self.tools.list_elements()
963
+ return [TextContent(type="text", text=self.format_response(result))]
964
+
965
+ elif name == "mobile_find_close_button":
966
+ result = self.tools.find_close_button()
967
+ return [TextContent(type="text", text=self.format_response(result))]
968
+
969
+ elif name == "mobile_close_popup":
970
+ result = self.tools.close_popup()
971
+ return [TextContent(type="text", text=self.format_response(result))]
972
+
973
+ elif name == "mobile_assert_text":
974
+ result = self.tools.assert_text(arguments["text"])
975
+ return [TextContent(type="text", text=self.format_response(result))]
976
+
977
+ # 脚本生成
978
+ elif name == "mobile_get_operation_history":
979
+ result = self.tools.get_operation_history(arguments.get("limit"))
980
+ return [TextContent(type="text", text=self.format_response(result))]
981
+
982
+ elif name == "mobile_clear_operation_history":
983
+ result = self.tools.clear_operation_history()
984
+ return [TextContent(type="text", text=self.format_response(result))]
985
+
986
+ elif name == "mobile_generate_test_script":
987
+ result = self.tools.generate_test_script(
988
+ arguments["test_name"],
989
+ arguments["package_name"],
990
+ arguments["filename"]
991
+ )
992
+ return [TextContent(type="text", text=self.format_response(result))]
993
+
994
+ # 智能关闭广告弹窗
995
+ elif name == "mobile_close_ad":
996
+ result = self.tools.close_ad_popup(auto_learn=True)
997
+ return [TextContent(type="text", text=self.format_response(result))]
998
+
999
+ # 模板匹配(精简版)
1000
+ elif name == "mobile_template_close":
1001
+ click = arguments.get("click", True)
1002
+ threshold = arguments.get("threshold", 0.75)
1003
+ if click:
1004
+ result = self.tools.template_click_close(threshold=threshold)
1005
+ else:
1006
+ result = self.tools.template_match_close(threshold=threshold)
1007
+ return [TextContent(type="text", text=self.format_response(result))]
1008
+
1009
+ elif name == "mobile_template_add":
1010
+ template_name = arguments["template_name"]
1011
+ # 判断使用哪种方式
1012
+ if "x_percent" in arguments and "y_percent" in arguments:
1013
+ # 百分比方式
1014
+ result = self.tools.template_add_by_percent(
1015
+ arguments["x_percent"],
1016
+ arguments["y_percent"],
1017
+ arguments.get("size", 80),
1018
+ template_name
1019
+ )
1020
+ elif "screenshot_path" in arguments:
1021
+ # 像素方式
1022
+ result = self.tools.template_add(
1023
+ arguments["screenshot_path"],
1024
+ arguments["x"],
1025
+ arguments["y"],
1026
+ arguments["width"],
1027
+ arguments["height"],
1028
+ template_name
1029
+ )
1030
+ else:
1031
+ result = {"success": False, "error": "请提供 x_percent/y_percent 或 screenshot_path/x/y/width/height"}
1032
+ return [TextContent(type="text", text=self.format_response(result))]
1033
+
1034
+ else:
1035
+ return [TextContent(type="text", text=f"❌ 未知工具: {name}")]
1036
+
1037
+ except Exception as e:
1038
+ import traceback
1039
+ error_msg = f"❌ 执行失败: {str(e)}\n{traceback.format_exc()}"
1040
+ return [TextContent(type="text", text=error_msg)]
1041
+
1042
+
1043
+ async def async_main():
1044
+ """启动 MCP Server(异步版本)"""
1045
+ server = MobileMCPServer()
1046
+ mcp_server = Server("mobile-mcp")
1047
+
1048
+ @mcp_server.list_tools()
1049
+ async def list_tools():
1050
+ return server.get_tools()
1051
+
1052
+ @mcp_server.call_tool()
1053
+ async def call_tool(name: str, arguments: dict):
1054
+ return await server.handle_tool_call(name, arguments)
1055
+
1056
+ print("🚀 Mobile MCP Server 启动中... [26 个工具]", file=sys.stderr)
1057
+ print("📱 支持 Android / iOS", file=sys.stderr)
1058
+ print("👁️ 完全依赖 Cursor 视觉能力,无需 AI 密钥", file=sys.stderr)
1059
+
1060
+ async with stdio_server() as (read_stream, write_stream):
1061
+ await mcp_server.run(read_stream, write_stream, mcp_server.create_initialization_options())
1062
+
1063
+
1064
+ def main():
1065
+ """入口点函数(供 pip 安装后使用)"""
1066
+ asyncio.run(async_main())
1067
+
1068
+
1069
+ if __name__ == "__main__":
1070
+ main()
1071
+