mobile-mcp-ai 2.6.4__py3-none-any.whl → 2.6.6__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.
@@ -102,19 +102,12 @@ class MobileMCPServer:
102
102
  self.tools = None
103
103
  self._initialized = False
104
104
  self._last_error = None # 保存最后一次连接失败的错误
105
-
106
- # Token 优化配置
107
- try:
108
- from mobile_mcp.config import Config
109
- self._compact_desc = Config.COMPACT_TOOL_DESCRIPTION
110
- except ImportError:
111
- self._compact_desc = True # 默认开启精简模式
112
105
 
113
106
  @staticmethod
114
107
  def format_response(result) -> str:
115
- """统一格式化返回值(Token 优化:无缩进)"""
108
+ """统一格式化返回值"""
116
109
  if isinstance(result, (dict, list)):
117
- return json.dumps(result, ensure_ascii=False, separators=(',', ':'))
110
+ return json.dumps(result, ensure_ascii=False, indent=2)
118
111
  return str(result)
119
112
 
120
113
  async def initialize(self):
@@ -205,55 +198,42 @@ class MobileMCPServer:
205
198
  return "android"
206
199
 
207
200
  def get_tools(self):
208
- """注册 MCP 工具"""
201
+ """注册 MCP 工具(20 个)"""
209
202
  tools = []
210
203
 
211
- # 根据配置选择精简或完整描述
212
- compact = getattr(self, '_compact_desc', True)
213
-
214
204
  # ==================== 元素定位(优先使用)====================
215
- if compact:
216
- desc_list_elements = "📋 列出页面可交互元素。点击前先调用此工具获取 text/id 定位。"
217
- else:
218
- desc_list_elements = ("📋 列出页面所有可交互元素\n\n"
205
+ tools.append(Tool(
206
+ name="mobile_list_elements",
207
+ description="📋 列出页面所有可交互元素\n\n"
219
208
  "⚠️ 【重要】点击元素前必须先调用此工具!\n"
220
209
  "如果元素在控件树中存在,使用 click_by_text 或 click_by_id 定位。\n"
221
210
  "只有当此工具返回空或找不到目标元素时,才使用截图+坐标方式。\n\n"
222
211
  "📌 控件树定位优势:\n"
223
212
  "- 实时检测元素是否存在\n"
224
213
  "- 元素消失时会报错,不会误点击\n"
225
- "- 跨设备兼容性好")
226
-
227
- tools.append(Tool(
228
- name="mobile_list_elements",
229
- description=desc_list_elements,
214
+ "- 跨设备兼容性好",
230
215
  inputSchema={"type": "object", "properties": {}, "required": []}
231
216
  ))
232
217
 
233
218
  # ==================== 截图(视觉兜底)====================
234
- if compact:
235
- desc_screenshot = "📸 截图。推荐用 mobile_screenshot_with_som 代替(带元素编号)。"
236
- else:
237
- desc_screenshot = ("📸 截图查看屏幕内容\n\n"
219
+ tools.append(Tool(
220
+ name="mobile_take_screenshot",
221
+ description="📸 截图查看屏幕内容\n\n"
238
222
  "⚠️ 【推荐使用 mobile_screenshot_with_som 代替!】\n"
239
223
  "SoM 截图会给元素标号,AI 可以直接说'点击几号',更精准!\n\n"
240
224
  "🎯 本工具仅用于:\n"
241
225
  "- 快速确认页面状态(不需要点击时)\n"
242
226
  "- 操作后确认结果\n"
243
227
  "- compress=false 时可获取原始分辨率截图(用于添加模板)\n\n"
244
- "💡 如需点击元素,请用 mobile_screenshot_with_som + mobile_click_by_som")
245
-
246
- tools.append(Tool(
247
- name="mobile_take_screenshot",
248
- description=desc_screenshot,
228
+ "💡 如需点击元素,请用 mobile_screenshot_with_som + mobile_click_by_som",
249
229
  inputSchema={
250
230
  "type": "object",
251
231
  "properties": {
252
- "description": {"type": "string", "description": "截图描述"},
253
- "compress": {"type": "boolean", "description": "是否压缩", "default": True},
254
- "crop_x": {"type": "integer", "description": "裁剪中心 X"},
255
- "crop_y": {"type": "integer", "description": "裁剪中心 Y"},
256
- "crop_size": {"type": "integer", "description": "裁剪大小"}
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 表示不裁剪)"}
257
237
  },
258
238
  "required": []
259
239
  }
@@ -265,10 +245,9 @@ class MobileMCPServer:
265
245
  inputSchema={"type": "object", "properties": {}, "required": []}
266
246
  ))
267
247
 
268
- if compact:
269
- desc_som = "📸 SoM截图(推荐)。给元素标编号,用 click_by_som(编号) 点击。"
270
- else:
271
- desc_som = ("📸🏷️ Set-of-Mark 截图(⭐⭐ 强烈推荐!默认截图方式)\n\n"
248
+ tools.append(Tool(
249
+ name="mobile_screenshot_with_som",
250
+ description="📸🏷️ Set-of-Mark 截图(⭐⭐ 强烈推荐!默认截图方式)\n\n"
272
251
  "【智能标注】给每个可点击元素画框+编号,检测弹窗时额外标注可能的X按钮位置(黄色)。\n"
273
252
  "AI 看图直接说'点击 3 号',调用 mobile_click_by_som(3) 即可!\n\n"
274
253
  "🎯 优势:\n"
@@ -279,21 +258,23 @@ class MobileMCPServer:
279
258
  "1. 任何需要操作的场景,都先调用此工具\n"
280
259
  "2. 看标注图,找到目标元素编号\n"
281
260
  "3. 调用 mobile_click_by_som(编号) 精准点击\n"
282
- "4. 🔴【必须】点击后再次截图确认操作是否成功!")
283
-
284
- tools.append(Tool(
285
- name="mobile_screenshot_with_som",
286
- description=desc_som,
261
+ "4. 🔴【必须】点击后再次截图确认操作是否成功!",
287
262
  inputSchema={"type": "object", "properties": {}, "required": []}
288
263
  ))
289
264
 
290
265
  tools.append(Tool(
291
266
  name="mobile_click_by_som",
292
- description="🎯 根据SoM编号点击。配合screenshot_with_som使用。",
267
+ description="🎯 根据 SoM 编号点击元素\n\n"
268
+ "配合 mobile_screenshot_with_som 使用。\n"
269
+ "看图后直接说'点击 3 号',调用此函数即可。\n\n"
270
+ "⚠️ 【重要】点击后建议再次截图确认操作是否成功!",
293
271
  inputSchema={
294
272
  "type": "object",
295
273
  "properties": {
296
- "index": {"type": "integer", "description": "元素编号(从1开始)"}
274
+ "index": {
275
+ "type": "integer",
276
+ "description": "元素编号(从 1 开始,对应截图中的标注数字)"
277
+ }
297
278
  },
298
279
  "required": ["index"]
299
280
  }
@@ -301,22 +282,38 @@ class MobileMCPServer:
301
282
 
302
283
  tools.append(Tool(
303
284
  name="mobile_screenshot_with_grid",
304
- description="📸 带网格坐标截图。用于精确定位元素坐标。",
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
+ "🔴 【必须】点击后必须再次截图确认操作是否成功!",
305
297
  inputSchema={
306
298
  "type": "object",
307
299
  "properties": {
308
- "grid_size": {"type": "integer", "description": "网格间距(px),默认100"},
309
- "show_popup_hints": {"type": "boolean", "description": "显示弹窗提示"}
300
+ "grid_size": {
301
+ "type": "integer",
302
+ "description": "网格间距(像素),默认 100。值越小网格越密,建议 50-200"
303
+ },
304
+ "show_popup_hints": {
305
+ "type": "boolean",
306
+ "description": "是否显示弹窗关闭按钮提示位置,默认 true"
307
+ }
310
308
  },
311
309
  "required": []
312
310
  }
313
311
  ))
314
312
 
315
313
  # ==================== 点击操作 ====================
316
- if compact:
317
- desc_click_text = "👆 通过文本点击(最推荐)。position 可选 top/bottom/left/right。"
318
- else:
319
- desc_click_text = ("👆 通过文本点击元素(最推荐)\n\n"
314
+ tools.append(Tool(
315
+ name="mobile_click_by_text",
316
+ description="👆 通过文本点击元素(最推荐)\n\n"
320
317
  "✅ 最稳定的定位方式,跨设备兼容\n"
321
318
  "✅ 实时检测元素是否存在,元素不存在会报错\n"
322
319
  "✅ 不会误点击到其他位置\n"
@@ -325,48 +322,38 @@ class MobileMCPServer:
325
322
  "📍 当页面有多个相同文案时,可使用 position 参数指定位置:\n"
326
323
  " - 垂直方向: \"top\"/\"upper\"/\"上\", \"bottom\"/\"lower\"/\"下\", \"middle\"/\"center\"/\"中\"\n"
327
324
  " - 水平方向: \"left\"/\"左\", \"right\"/\"右\", \"center\"/\"中\"\n"
328
- " 例如:点击\"底部\"的\"微剧\"tab,使用 position=\"bottom\"")
329
-
330
- tools.append(Tool(
331
- name="mobile_click_by_text",
332
- description=desc_click_text,
325
+ " 例如:点击\"底部\"的\"微剧\"tab,使用 position=\"bottom\"",
333
326
  inputSchema={
334
327
  "type": "object",
335
328
  "properties": {
336
- "text": {"type": "string", "description": "元素文本"},
337
- "position": {"type": "string", "description": "位置:top/bottom/left/right"}
329
+ "text": {"type": "string", "description": "元素的文本内容(精确匹配)"},
330
+ "position": {"type": "string", "description": "位置信息(可选)。当有多个相同文案时使用,支持:top/bottom/left/right/middle 或 上/下/左/右/中"}
338
331
  },
339
332
  "required": ["text"]
340
333
  }
341
334
  ))
342
335
 
343
- if compact:
344
- desc_click_id = "👆 通过 resource-id 点击。index 指定第几个(从 0 开始)。"
345
- else:
346
- desc_click_id = ("👆 通过 resource-id 点击元素(推荐)\n\n"
336
+ tools.append(Tool(
337
+ name="mobile_click_by_id",
338
+ description="👆 通过 resource-id 点击元素(推荐)\n\n"
347
339
  "✅ 稳定的定位方式\n"
348
340
  "✅ 实时检测元素是否存在,元素不存在会报错\n"
349
341
  "📋 使用前先调用 mobile_list_elements 获取元素 ID\n"
350
342
  "💡 当有多个相同 ID 的元素时,用 index 指定第几个(从 0 开始)\n"
351
- "💡 定位优先级:文本 > ID > 百分比 > 坐标")
352
-
353
- tools.append(Tool(
354
- name="mobile_click_by_id",
355
- description=desc_click_id,
343
+ "💡 定位优先级:文本 > ID > 百分比 > 坐标",
356
344
  inputSchema={
357
345
  "type": "object",
358
346
  "properties": {
359
- "resource_id": {"type": "string", "description": "resource-id"},
360
- "index": {"type": "integer", "description": "第几个(从0开始)", "default": 0}
347
+ "resource_id": {"type": "string", "description": "元素的 resource-id"},
348
+ "index": {"type": "integer", "description": "第几个元素(从 0 开始),默认 0 表示第一个", "default": 0}
361
349
  },
362
350
  "required": ["resource_id"]
363
351
  }
364
352
  ))
365
353
 
366
- if compact:
367
- desc_click_coords = "👆 点击坐标(兜底)。优先用 click_by_text/id。"
368
- else:
369
- desc_click_coords = ("👆 点击指定坐标(兜底方案)\n\n"
354
+ tools.append(Tool(
355
+ name="mobile_click_at_coords",
356
+ description="👆 点击指定坐标(兜底方案)\n\n"
370
357
  "⚠️ 【重要】优先使用 mobile_click_by_text 或 mobile_click_by_id!\n"
371
358
  "仅在 mobile_list_elements 无法获取元素时使用此工具。\n\n"
372
359
  "⚠️ 【时序限制】截图分析期间页面可能变化:\n"
@@ -374,22 +361,18 @@ class MobileMCPServer:
374
361
  "- 如果误点击,调用 mobile_press_key(back) 返回\n"
375
362
  "- 对于定时弹窗(如广告),建议等待其自动消失\n\n"
376
363
  "📐 坐标转换:截图返回的 image_width/height 等参数直接传入即可\n\n"
377
- "🔴 【必须】点击后必须再次截图确认操作是否成功!")
378
-
379
- tools.append(Tool(
380
- name="mobile_click_at_coords",
381
- description=desc_click_coords,
364
+ "🔴 【必须】点击后必须再次截图确认操作是否成功!",
382
365
  inputSchema={
383
366
  "type": "object",
384
367
  "properties": {
385
- "x": {"type": "number", "description": "X 坐标"},
386
- "y": {"type": "number", "description": "Y 坐标"},
387
- "image_width": {"type": "number", "description": "图片宽度"},
388
- "image_height": {"type": "number", "description": "图片高度"},
389
- "original_img_width": {"type": "number", "description": "原图宽"},
390
- "original_img_height": {"type": "number", "description": "原图高"},
391
- "crop_offset_x": {"type": "number", "description": "裁剪X偏移"},
392
- "crop_offset_y": {"type": "number", "description": "裁剪Y偏移"}
368
+ "x": {"type": "number", "description": "X 坐标(来自 AI 分析截图)"},
369
+ "y": {"type": "number", "description": "Y 坐标(来自 AI 分析截图)"},
370
+ "image_width": {"type": "number", "description": "压缩后图片宽度(截图返回的 image_width)"},
371
+ "image_height": {"type": "number", "description": "压缩后图片高度(截图返回的 image_height)"},
372
+ "original_img_width": {"type": "number", "description": "原图宽度(截图返回的 original_img_width)"},
373
+ "original_img_height": {"type": "number", "description": "原图高度(截图返回的 original_img_height)"},
374
+ "crop_offset_x": {"type": "number", "description": "局部截图 X 偏移(裁剪截图时传入)"},
375
+ "crop_offset_y": {"type": "number", "description": "局部截图 Y 偏移(裁剪截图时传入)"}
393
376
  },
394
377
  "required": ["x", "y"]
395
378
  }
@@ -397,7 +380,15 @@ class MobileMCPServer:
397
380
 
398
381
  tools.append(Tool(
399
382
  name="mobile_click_by_percent",
400
- description="👆 百分比点击。(50,50)=屏幕中心。跨设备兼容。",
383
+ description="👆 通过百分比位置点击(跨设备兼容!)。\n\n"
384
+ "🎯 原理:屏幕左上角是 (0%, 0%),右下角是 (100%, 100%)\n"
385
+ "📐 示例:\n"
386
+ " - (50, 50) = 屏幕正中央\n"
387
+ " - (10, 5) = 左上角附近\n"
388
+ " - (85, 90) = 右下角附近\n\n"
389
+ "✅ 优势:同样的百分比在不同分辨率设备上都能点到相同相对位置\n"
390
+ "💡 录制一次,多设备回放\n\n"
391
+ "🔴 【必须】点击后必须再次截图确认操作是否成功!",
401
392
  inputSchema={
402
393
  "type": "object",
403
394
  "properties": {
@@ -411,12 +402,15 @@ class MobileMCPServer:
411
402
  # ==================== 长按操作 ====================
412
403
  tools.append(Tool(
413
404
  name="mobile_long_press_by_id",
414
- description="👆 通过resource-id长按。",
405
+ description="👆 通过 resource-id 长按(⭐⭐ 最稳定!)\n\n"
406
+ "✅ 最稳定的长按定位方式,跨设备完美兼容\n"
407
+ "📋 使用前请先调用 mobile_list_elements 获取元素 ID\n"
408
+ "💡 生成的脚本使用 d(resourceId='...').long_click() 定位,最稳定",
415
409
  inputSchema={
416
410
  "type": "object",
417
411
  "properties": {
418
- "resource_id": {"type": "string", "description": "resource-id"},
419
- "duration": {"type": "number", "description": "长按秒数,默认1.0"}
412
+ "resource_id": {"type": "string", "description": "元素的 resource-id"},
413
+ "duration": {"type": "number", "description": "长按持续时间(秒),默认 1.0"}
420
414
  },
421
415
  "required": ["resource_id"]
422
416
  }
@@ -424,12 +418,15 @@ class MobileMCPServer:
424
418
 
425
419
  tools.append(Tool(
426
420
  name="mobile_long_press_by_text",
427
- description="👆 通过文本长按。",
421
+ description="👆 通过文本长按(⭐ 推荐!)\n\n"
422
+ "✅ 优势:跨设备兼容,不受屏幕分辨率影响\n"
423
+ "📋 使用前请先调用 mobile_list_elements 确认元素有文本\n"
424
+ "💡 生成的脚本使用 d(text='...').long_click() 定位,稳定可靠",
428
425
  inputSchema={
429
426
  "type": "object",
430
427
  "properties": {
431
- "text": {"type": "string", "description": "文本内容"},
432
- "duration": {"type": "number", "description": "长按秒数,默认1.0"}
428
+ "text": {"type": "string", "description": "元素的文本内容(精确匹配)"},
429
+ "duration": {"type": "number", "description": "长按持续时间(秒),默认 1.0"}
433
430
  },
434
431
  "required": ["text"]
435
432
  }
@@ -437,13 +434,20 @@ class MobileMCPServer:
437
434
 
438
435
  tools.append(Tool(
439
436
  name="mobile_long_press_by_percent",
440
- description="👆 百分比长按。(50,50)=屏幕中心。",
437
+ description="👆 通过百分比位置长按(跨设备兼容!)\n\n"
438
+ "🎯 原理:屏幕左上角是 (0%, 0%),右下角是 (100%, 100%)\n"
439
+ "📐 示例:\n"
440
+ " - (50, 50) = 屏幕正中央\n"
441
+ " - (10, 5) = 左上角附近\n"
442
+ " - (85, 90) = 右下角附近\n\n"
443
+ "✅ 优势:同样的百分比在不同分辨率设备上都能长按到相同相对位置\n"
444
+ "💡 录制一次,多设备回放",
441
445
  inputSchema={
442
446
  "type": "object",
443
447
  "properties": {
444
- "x_percent": {"type": "number", "description": "X百分比(0-100)"},
445
- "y_percent": {"type": "number", "description": "Y百分比(0-100)"},
446
- "duration": {"type": "number", "description": "长按秒数,默认1.0"}
448
+ "x_percent": {"type": "number", "description": "X 轴百分比 (0-100)"},
449
+ "y_percent": {"type": "number", "description": "Y 轴百分比 (0-100)"},
450
+ "duration": {"type": "number", "description": "长按持续时间(秒),默认 1.0"}
447
451
  },
448
452
  "required": ["x_percent", "y_percent"]
449
453
  }
@@ -451,19 +455,28 @@ class MobileMCPServer:
451
455
 
452
456
  tools.append(Tool(
453
457
  name="mobile_long_press_at_coords",
454
- description="👆 坐标长按(兜底)。优先用text/id。",
458
+ description="👆 长按指定坐标(⚠️ 兜底方案,优先用文本/ID定位!)\n\n"
459
+ "🎯 仅在以下场景使用:\n"
460
+ "- 游戏(Unity/Cocos)无法获取元素\n"
461
+ "- mobile_list_elements 返回空\n"
462
+ "- 元素没有 id 和 text\n\n"
463
+ "⚠️ 【坐标转换】截图返回的参数直接传入:\n"
464
+ " - image_width/image_height: 压缩后尺寸(AI 看到的)\n"
465
+ " - original_img_width/original_img_height: 原图尺寸(用于转换)\n"
466
+ " - crop_offset_x/crop_offset_y: 局部截图偏移\n\n"
467
+ "✅ 自动记录百分比坐标,生成脚本时转换为跨分辨率兼容的百分比定位",
455
468
  inputSchema={
456
469
  "type": "object",
457
470
  "properties": {
458
- "x": {"type": "number", "description": "X坐标"},
459
- "y": {"type": "number", "description": "Y坐标"},
460
- "duration": {"type": "number", "description": "长按秒数"},
461
- "image_width": {"type": "number", "description": "图片宽"},
462
- "image_height": {"type": "number", "description": "图片高"},
463
- "original_img_width": {"type": "number", "description": "原图宽"},
464
- "original_img_height": {"type": "number", "description": "原图高"},
465
- "crop_offset_x": {"type": "number", "description": "裁剪X偏移"},
466
- "crop_offset_y": {"type": "number", "description": "裁剪Y偏移"}
471
+ "x": {"type": "number", "description": "X 坐标(来自 AI 分析截图)"},
472
+ "y": {"type": "number", "description": "Y 坐标(来自 AI 分析截图)"},
473
+ "duration": {"type": "number", "description": "长按持续时间(秒),默认 1.0"},
474
+ "image_width": {"type": "number", "description": "压缩后图片宽度"},
475
+ "image_height": {"type": "number", "description": "压缩后图片高度"},
476
+ "original_img_width": {"type": "number", "description": "原图宽度"},
477
+ "original_img_height": {"type": "number", "description": "原图高度"},
478
+ "crop_offset_x": {"type": "number", "description": "局部截图 X 偏移"},
479
+ "crop_offset_y": {"type": "number", "description": "局部截图 Y 偏移"}
467
480
  },
468
481
  "required": ["x", "y"]
469
482
  }
@@ -472,12 +485,12 @@ class MobileMCPServer:
472
485
  # ==================== 输入操作 ====================
473
486
  tools.append(Tool(
474
487
  name="mobile_input_text_by_id",
475
- description="⌨️ 通过ID输入文本。",
488
+ description="⌨️ 在输入框输入文本。需要先用 mobile_list_elements 获取输入框 ID",
476
489
  inputSchema={
477
490
  "type": "object",
478
491
  "properties": {
479
- "resource_id": {"type": "string", "description": "resource-id"},
480
- "text": {"type": "string", "description": "输入文本"}
492
+ "resource_id": {"type": "string", "description": "输入框的 resource-id"},
493
+ "text": {"type": "string", "description": "要输入的文本"}
481
494
  },
482
495
  "required": ["resource_id", "text"]
483
496
  }
@@ -485,13 +498,13 @@ class MobileMCPServer:
485
498
 
486
499
  tools.append(Tool(
487
500
  name="mobile_input_at_coords",
488
- description="⌨️ 坐标输入文本。",
501
+ description="⌨️ 点击坐标后输入文本。适合游戏等无法获取元素 ID 的场景。",
489
502
  inputSchema={
490
503
  "type": "object",
491
504
  "properties": {
492
- "x": {"type": "number", "description": "X坐标"},
493
- "y": {"type": "number", "description": "Y坐标"},
494
- "text": {"type": "string", "description": "输入文本"}
505
+ "x": {"type": "number", "description": "输入框 X 坐标"},
506
+ "y": {"type": "number", "description": "输入框 Y 坐标"},
507
+ "text": {"type": "string", "description": "要输入的文本"}
495
508
  },
496
509
  "required": ["x", "y", "text"]
497
510
  }
@@ -500,13 +513,39 @@ class MobileMCPServer:
500
513
  # ==================== 导航操作 ====================
501
514
  tools.append(Tool(
502
515
  name="mobile_swipe",
503
- description="👆 滑动。方向:up/down/left/right",
516
+ description="👆 滑动屏幕。方向:up/down/left/right\n\n"
517
+ "💡 左右滑动时,可指定高度坐标或百分比:\n"
518
+ "- y: 指定高度坐标(像素)\n"
519
+ "- y_percent: 指定高度百分比 (0-100)\n"
520
+ "- 两者都未指定时,使用屏幕中心高度\n\n"
521
+ "💡 横向滑动(left/right)时,可指定滑动距离:\n"
522
+ "- distance: 滑动距离(像素)\n"
523
+ "- distance_percent: 滑动距离百分比 (0-100)\n"
524
+ "- 两者都未指定时,使用默认距离(屏幕宽度的 60%)",
504
525
  inputSchema={
505
526
  "type": "object",
506
527
  "properties": {
507
- "direction": {"type": "string", "enum": ["up", "down", "left", "right"], "description": "方向"},
508
- "y": {"type": "integer", "description": "左右滑动高度(px)"},
509
- "y_percent": {"type": "number", "description": "左右滑动高度(%)"}
528
+ "direction": {
529
+ "type": "string",
530
+ "enum": ["up", "down", "left", "right"],
531
+ "description": "滑动方向"
532
+ },
533
+ "y": {
534
+ "type": "integer",
535
+ "description": "左右滑动时指定的高度坐标(像素,0-屏幕高度)"
536
+ },
537
+ "y_percent": {
538
+ "type": "number",
539
+ "description": "左右滑动时指定的高度百分比 (0-100)"
540
+ },
541
+ "distance": {
542
+ "type": "integer",
543
+ "description": "横向滑动时指定的滑动距离(像素),仅用于 left/right"
544
+ },
545
+ "distance_percent": {
546
+ "type": "number",
547
+ "description": "横向滑动时指定的滑动距离百分比 (0-100),仅用于 left/right"
548
+ }
510
549
  },
511
550
  "required": ["direction"]
512
551
  }
@@ -514,11 +553,11 @@ class MobileMCPServer:
514
553
 
515
554
  tools.append(Tool(
516
555
  name="mobile_press_key",
517
- description="⌨️ 按键:home/back/enter/search",
556
+ description="⌨️ 按键操作。支持:home, back, enter, search",
518
557
  inputSchema={
519
558
  "type": "object",
520
559
  "properties": {
521
- "key": {"type": "string", "description": "按键名"}
560
+ "key": {"type": "string", "description": "按键名称:home, back, enter, search"}
522
561
  },
523
562
  "required": ["key"]
524
563
  }
@@ -526,11 +565,11 @@ class MobileMCPServer:
526
565
 
527
566
  tools.append(Tool(
528
567
  name="mobile_wait",
529
- description="⏰ 等待指定秒数。",
568
+ description="⏰ 等待指定时间。用于等待页面加载、动画完成等。",
530
569
  inputSchema={
531
570
  "type": "object",
532
571
  "properties": {
533
- "seconds": {"type": "number", "description": "等待秒数"}
572
+ "seconds": {"type": "number", "description": "等待时间(秒)"}
534
573
  },
535
574
  "required": ["seconds"]
536
575
  }
@@ -539,11 +578,11 @@ class MobileMCPServer:
539
578
  # ==================== 应用管理 ====================
540
579
  tools.append(Tool(
541
580
  name="mobile_launch_app",
542
- description="🚀 启动应用。",
581
+ description="🚀 启动应用。启动后建议等待 2-3 秒让页面加载。",
543
582
  inputSchema={
544
583
  "type": "object",
545
584
  "properties": {
546
- "package_name": {"type": "string", "description": "包名"}
585
+ "package_name": {"type": "string", "description": "应用包名"}
547
586
  },
548
587
  "required": ["package_name"]
549
588
  }
@@ -555,7 +594,7 @@ class MobileMCPServer:
555
594
  inputSchema={
556
595
  "type": "object",
557
596
  "properties": {
558
- "package_name": {"type": "string", "description": "包名"}
597
+ "package_name": {"type": "string", "description": "应用包名"}
559
598
  },
560
599
  "required": ["package_name"]
561
600
  }
@@ -563,11 +602,11 @@ class MobileMCPServer:
563
602
 
564
603
  tools.append(Tool(
565
604
  name="mobile_list_apps",
566
- description="📦 列出应用。",
605
+ description="📦 列出已安装的应用。可按关键词过滤。",
567
606
  inputSchema={
568
607
  "type": "object",
569
608
  "properties": {
570
- "filter": {"type": "string", "description": "过滤词"}
609
+ "filter": {"type": "string", "description": "过滤关键词(可选)"}
571
610
  },
572
611
  "required": []
573
612
  }
@@ -576,21 +615,20 @@ class MobileMCPServer:
576
615
  # ==================== 设备管理 ====================
577
616
  tools.append(Tool(
578
617
  name="mobile_list_devices",
579
- description="📱 列出设备。",
618
+ description="📱 列出已连接的设备。",
580
619
  inputSchema={"type": "object", "properties": {}, "required": []}
581
620
  ))
582
621
 
583
622
  tools.append(Tool(
584
623
  name="mobile_check_connection",
585
- description="🔌 检查连接。",
624
+ description="🔌 检查设备连接状态。",
586
625
  inputSchema={"type": "object", "properties": {}, "required": []}
587
626
  ))
588
627
 
589
628
  # ==================== 辅助工具 ====================
590
- if compact:
591
- desc_find_close = "🔍 查找关闭按钮(只找不点)。返回坐标和推荐的点击命令。"
592
- else:
593
- desc_find_close = """🔍 智能查找关闭按钮(只找不点,返回位置)
629
+ tools.append(Tool(
630
+ name="mobile_find_close_button",
631
+ description="""🔍 智能查找关闭按钮(只找不点,返回位置)
594
632
 
595
633
  ⚡ 【推荐首选】遇到弹窗时优先调用此工具!无需先截图。
596
634
 
@@ -610,68 +648,79 @@ class MobileMCPServer:
610
648
  💡 使用流程:
611
649
  1. 直接调用此工具(无需先截图/列元素)
612
650
  2. 根据返回的 click_command 执行点击
613
- 3. 如果返回 success=false,才需要截图分析"""
614
-
615
- tools.append(Tool(
616
- name="mobile_find_close_button",
617
- description=desc_find_close,
651
+ 3. 如果返回 success=false,才需要截图分析""",
618
652
  inputSchema={"type": "object", "properties": {}, "required": []}
619
653
  ))
620
654
 
621
- if compact:
622
- desc_close_popup = "🚫 智能检测并关闭弹窗。自动查找×/关闭/跳过按钮。"
623
- else:
624
- desc_close_popup = """🚫 智能检测并关闭弹窗
655
+ tools.append(Tool(
656
+ name="mobile_close_popup",
657
+ description="""🚫 智能关闭弹窗
625
658
 
626
- ⚡ 【自动检测】会先检测是否存在弹窗:
627
- - 如果没有弹窗 → 直接返回"无弹窗",不执行任何操作
628
- - 如果有弹窗 → 自动查找并点击关闭按钮
659
+ 通过控件树识别并点击关闭按钮(×、关闭、跳过等)。
629
660
 
630
- 适用场景:
631
- - 启动应用后检测并关闭可能出现的弹窗
632
- - 页面跳转后检测并关闭弹窗
633
- - 无需先截图确认弹窗是否存在
661
+ 控件树有元素时:直接点击,实时可靠
662
+ 控件树无元素时:截图供 AI 分析
634
663
 
635
- 🎯 检测策略:
636
- - 查找控件树中的关闭按钮(×、关闭、跳过等)
637
- - 检测弹窗区域(Dialog/Popup/Alert 等)
638
- - 查找小尺寸的可点击元素(优先角落位置)
664
+ ⚠️ 【时序限制】如果需要截图分析:
665
+ - 分析期间弹窗可能自动消失
666
+ - 对于定时弹窗(如广告),建议等待其自动消失
667
+ - 点击前可再次截图确认弹窗是否还在
639
668
 
640
- 🔴 【必须】如果返回已点击,需再次截图确认弹窗是否真的关闭了!"""
641
-
642
- tools.append(Tool(
643
- name="mobile_close_popup",
644
- description=desc_close_popup,
669
+ 🔴 【必须】点击关闭后,必须再次截图确认弹窗是否真的关闭了!
670
+ 如果弹窗仍在,需要尝试其他方法或位置。""",
645
671
  inputSchema={"type": "object", "properties": {}, "required": []}
646
672
  ))
647
673
 
648
674
  tools.append(Tool(
649
675
  name="mobile_assert_text",
650
- description="✅ 检查页面是否包含文本。",
676
+ description="✅ 检查页面是否包含指定文本。用于验证操作结果。",
651
677
  inputSchema={
652
678
  "type": "object",
653
679
  "properties": {
654
- "text": {"type": "string", "description": "文本"}
680
+ "text": {"type": "string", "description": "要检查的文本"}
655
681
  },
656
682
  "required": ["text"]
657
683
  }
658
684
  ))
659
685
 
660
- # ==================== Toast 检测(仅 Android)====================
686
+ # ==================== Toast 检测工具(仅 Android)====================
661
687
  tools.append(Tool(
662
688
  name="mobile_start_toast_watch",
663
- description="🔔 开始监听Toast。必须在操作前调用。",
664
- inputSchema={"type": "object", "properties": {}, "required": []}
689
+ description="""🔔 开始监听 Toast(仅 Android)
690
+
691
+ ⚠️ 【重要】必须在执行操作之前调用!
692
+
693
+ 📋 正确流程(三步走):
694
+ 1️⃣ 调用 mobile_start_toast_watch() 开始监听
695
+ 2️⃣ 执行操作(如点击提交按钮)
696
+ 3️⃣ 调用 mobile_get_toast() 或 mobile_assert_toast() 获取结果
697
+
698
+ ❌ 错误用法:先点击按钮,再调用此工具(Toast 可能已消失)""",
699
+ inputSchema={
700
+ "type": "object",
701
+ "properties": {},
702
+ "required": []
703
+ }
665
704
  ))
666
705
 
667
706
  tools.append(Tool(
668
707
  name="mobile_get_toast",
669
- description="🍞 获取Toast消息。配合start_toast_watch使用。",
708
+ description="""🍞 获取 Toast 消息(仅 Android)
709
+
710
+ Toast 是 Android 系统级的短暂提示消息,常用于显示操作结果。
711
+ ⚠️ Toast 不在控件树中,无法通过 mobile_list_elements 获取。
712
+
713
+ 📋 推荐用法(三步走):
714
+ 1️⃣ mobile_start_toast_watch() - 开始监听
715
+ 2️⃣ 执行操作(点击按钮等)
716
+ 3️⃣ mobile_get_toast() - 获取 Toast
717
+
718
+ ⏱️ timeout 设置等待时间,默认 5 秒。""",
670
719
  inputSchema={
671
720
  "type": "object",
672
721
  "properties": {
673
- "timeout": {"type": "number", "description": "超时秒数,默认5"},
674
- "reset_first": {"type": "boolean", "description": "清除旧缓存"}
722
+ "timeout": {"type": "number", "description": "等待 Toast 出现的超时时间(秒),默认 5"},
723
+ "reset_first": {"type": "boolean", "description": "是否先清除旧缓存,默认 False"}
675
724
  },
676
725
  "required": []
677
726
  }
@@ -679,13 +728,22 @@ class MobileMCPServer:
679
728
 
680
729
  tools.append(Tool(
681
730
  name="mobile_assert_toast",
682
- description="✅ 断言Toast内容。",
731
+ description="""✅ 断言 Toast 消息(仅 Android)
732
+
733
+ 等待 Toast 出现并验证内容是否符合预期。
734
+
735
+ 📋 推荐用法(三步走):
736
+ 1️⃣ mobile_start_toast_watch() - 开始监听
737
+ 2️⃣ 执行操作(点击按钮等)
738
+ 3️⃣ mobile_assert_toast(expected_text="成功") - 断言
739
+
740
+ 💡 支持包含匹配(默认)和精确匹配。""",
683
741
  inputSchema={
684
742
  "type": "object",
685
743
  "properties": {
686
- "expected_text": {"type": "string", "description": "期望文本"},
687
- "timeout": {"type": "number", "description": "超时秒数"},
688
- "contains": {"type": "boolean", "description": "包含匹配(默认true)"}
744
+ "expected_text": {"type": "string", "description": "期望的 Toast 文本"},
745
+ "timeout": {"type": "number", "description": "等待超时时间(秒),默认 5"},
746
+ "contains": {"type": "boolean", "description": "True=包含匹配(默认),False=精确匹配"}
689
747
  },
690
748
  "required": ["expected_text"]
691
749
  }
@@ -694,11 +752,11 @@ class MobileMCPServer:
694
752
  # ==================== pytest 脚本生成 ====================
695
753
  tools.append(Tool(
696
754
  name="mobile_get_operation_history",
697
- description="📜 获取操作历史。",
755
+ description="📜 获取操作历史记录。",
698
756
  inputSchema={
699
757
  "type": "object",
700
758
  "properties": {
701
- "limit": {"type": "number", "description": "条数"}
759
+ "limit": {"type": "number", "description": "返回最近的N条记录"}
702
760
  },
703
761
  "required": []
704
762
  }
@@ -706,53 +764,63 @@ class MobileMCPServer:
706
764
 
707
765
  tools.append(Tool(
708
766
  name="mobile_clear_operation_history",
709
- description="🗑️ 清空操作历史。录制前调用。",
767
+ description="🗑️ 清空操作历史记录。\n\n"
768
+ "⚠️ 开始新的测试录制前必须调用!\n"
769
+ "📋 录制流程:清空历史 → 执行操作(优先用文本/ID定位)→ 生成脚本",
710
770
  inputSchema={"type": "object", "properties": {}, "required": []}
711
771
  ))
712
772
 
713
773
  tools.append(Tool(
714
774
  name="mobile_generate_test_script",
715
- description="📝 生成pytest脚本。基于操作历史生成。",
775
+ description="📝 生成 pytest 测试脚本。基于操作历史自动生成。\n\n"
776
+ "⚠️ 【重要】录制操作时请优先使用稳定定位:\n"
777
+ "1️⃣ 先调用 mobile_list_elements 获取元素列表\n"
778
+ "2️⃣ 优先用 mobile_click_by_text(最稳定,跨设备兼容)\n"
779
+ "3️⃣ 其次用 mobile_click_by_id(稳定)\n"
780
+ "4️⃣ 最后才用坐标点击(会自动转百分比,跨分辨率兼容)\n\n"
781
+ "使用流程:\n"
782
+ "1. 清空历史 mobile_clear_operation_history\n"
783
+ "2. 执行操作(优先用文本/ID定位)\n"
784
+ "3. 调用此工具生成脚本\n"
785
+ "4. 脚本保存到 tests/ 目录\n\n"
786
+ "💡 定位优先级:文本 > ID > 百分比 > 坐标",
716
787
  inputSchema={
717
788
  "type": "object",
718
789
  "properties": {
719
- "test_name": {"type": "string", "description": "用例名"},
720
- "package_name": {"type": "string", "description": "包名"},
721
- "filename": {"type": "string", "description": "文件名(不含.py)"}
790
+ "test_name": {"type": "string", "description": "测试用例名称"},
791
+ "package_name": {"type": "string", "description": "App 包名"},
792
+ "filename": {"type": "string", "description": "脚本文件名(不含 .py"}
722
793
  },
723
794
  "required": ["test_name", "package_name", "filename"]
724
795
  }
725
796
  ))
726
797
 
727
798
  # ==================== 广告弹窗关闭工具 ====================
728
- if compact:
729
- desc_close_ad = "🚫 智能关闭广告弹窗。优先级:控件树→截图AI→模板匹配。"
730
- else:
731
- desc_close_ad = """🚫 【推荐】智能检测并关闭广告弹窗
799
+ tools.append(Tool(
800
+ name="mobile_close_ad",
801
+ description="""🚫 【推荐】智能关闭广告弹窗
732
802
 
733
- 【自动检测】会先检测是否存在弹窗:
734
- - 如果没有弹窗 → 直接返回"无弹窗",不执行任何操作
735
- - 如果有弹窗 → 自动按优先级尝试关闭
803
+ 直接调用即可,无需先截图!会自动按优先级尝试:
736
804
 
737
- 🎯 关闭策略(优先级从高到低):
738
- 1️⃣ **控件树查找**(最可靠)
805
+ 1️⃣ **控件树查找**(最可靠,优先)
806
+ - 自动查找 resource-id 包含 close/dismiss
739
807
  - 查找文本"关闭"、"跳过"、"×"等
740
- - 查找 resource-id 包含 close/dismiss
741
-
742
- 2️⃣ **截图 AI 分析**(次优)
743
- - 返回 SoM 标注截图供 AI 视觉分析
744
- - AI 找到 X 按钮后用 click_by_som(编号) 点击
808
+ - 找到直接点击,实时可靠
745
809
 
746
- 3️⃣ **模板匹配**(兜底)
810
+ 2️⃣ **模板匹配**(次优)
747
811
  - 用 OpenCV 匹配已保存的 X 按钮模板
812
+ - 模板越多成功率越高
748
813
 
749
- 适用场景:
750
- - 启动应用后检测并关闭可能出现的广告
751
- - 无需先截图确认弹窗是否存在"""
752
-
753
- tools.append(Tool(
754
- name="mobile_close_ad",
755
- description=desc_close_ad,
814
+ 3️⃣ **返回截图供 AI 分析**(兜底)
815
+ - 前两步都失败才截图
816
+ - AI 分析后用 mobile_click_by_percent 点击
817
+ - 点击成功后用 mobile_template_add 添加模板
818
+
819
+ 💡 正确流程:
820
+ 1. 遇到广告弹窗 → 直接调用此工具
821
+ 2. 如果成功 → 完成
822
+ 3. 只有失败时才需要截图分析
823
+ 3. 如果失败 → 看截图找 X → 点击 → 添加模板""",
756
824
  inputSchema={
757
825
  "type": "object",
758
826
  "properties": {},
@@ -762,12 +830,19 @@ class MobileMCPServer:
762
830
 
763
831
  tools.append(Tool(
764
832
  name="mobile_template_close",
765
- description="🎯 模板匹配关闭弹窗。",
833
+ description="""🎯 模板匹配关闭弹窗(仅模板匹配)
834
+
835
+ 只用 OpenCV 模板匹配,不走控件树。
836
+ 一般建议用 mobile_close_ad 代替(会自动先查控件树)。
837
+
838
+ ⚙️ 参数:
839
+ - click: 是否点击,默认 true
840
+ - threshold: 匹配阈值 0-1,默认 0.75""",
766
841
  inputSchema={
767
842
  "type": "object",
768
843
  "properties": {
769
- "click": {"type": "boolean", "description": "是否点击"},
770
- "threshold": {"type": "number", "description": "阈值0-1"}
844
+ "click": {"type": "boolean", "description": "是否点击,默认 true"},
845
+ "threshold": {"type": "number", "description": "匹配阈值 0-1,默认 0.75"}
771
846
  },
772
847
  "required": []
773
848
  }
@@ -775,19 +850,32 @@ class MobileMCPServer:
775
850
 
776
851
  tools.append(Tool(
777
852
  name="mobile_template_add",
778
- description="➕ 添加X号模板。",
853
+ description="""➕ 添加 X 号模板
854
+
855
+ 遇到新样式 X 号时,截图并添加到模板库。
856
+
857
+ ⚙️ 两种方式(二选一):
858
+ 1. 百分比定位(推荐):提供 x_percent, y_percent, size
859
+ 2. 像素定位:提供 screenshot_path, x, y, width, height
860
+
861
+ 📋 流程:
862
+ 1. mobile_screenshot_with_grid 查看 X 号位置
863
+ 2. 调用此工具添加模板
864
+ 3. 下次同样 X 号就能自动匹配
865
+
866
+ 💡 百分比示例:X 在右上角 → x_percent=85, y_percent=12, size=80""",
779
867
  inputSchema={
780
868
  "type": "object",
781
869
  "properties": {
782
- "template_name": {"type": "string", "description": "模板名"},
783
- "x_percent": {"type": "number", "description": "X百分比"},
784
- "y_percent": {"type": "number", "description": "Y百分比"},
785
- "size": {"type": "integer", "description": "裁剪大小(px)"},
786
- "screenshot_path": {"type": "string", "description": "截图路径"},
787
- "x": {"type": "integer", "description": "左上X"},
788
- "y": {"type": "integer", "description": "左上Y"},
789
- "width": {"type": "integer", "description": ""},
790
- "height": {"type": "integer", "description": ""}
870
+ "template_name": {"type": "string", "description": "模板名称"},
871
+ "x_percent": {"type": "number", "description": "X号中心水平百分比 (0-100)"},
872
+ "y_percent": {"type": "number", "description": "X号中心垂直百分比 (0-100)"},
873
+ "size": {"type": "integer", "description": "裁剪正方形边长(像素)"},
874
+ "screenshot_path": {"type": "string", "description": "截图路径(像素定位时用)"},
875
+ "x": {"type": "integer", "description": "左上角 X 坐标"},
876
+ "y": {"type": "integer", "description": "左上角 Y 坐标"},
877
+ "width": {"type": "integer", "description": "裁剪宽度"},
878
+ "height": {"type": "integer", "description": "裁剪高度"}
791
879
  },
792
880
  "required": ["template_name"]
793
881
  }
@@ -929,7 +1017,9 @@ class MobileMCPServer:
929
1017
  result = await self.tools.swipe(
930
1018
  arguments["direction"],
931
1019
  y=arguments.get("y"),
932
- y_percent=arguments.get("y_percent")
1020
+ y_percent=arguments.get("y_percent"),
1021
+ distance=arguments.get("distance"),
1022
+ distance_percent=arguments.get("distance_percent")
933
1023
  )
934
1024
  return [TextContent(type="text", text=self.format_response(result))]
935
1025