@mmerterden/multi-agent-toolkit-mcp 3.6.0 → 3.7.0

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.
package/CHANGELOG.md CHANGED
@@ -15,6 +15,36 @@ Releases before this file exists are recorded in the git tags and commit history
15
15
 
16
16
  ---
17
17
 
18
+ ## 3.7.0
19
+
20
+ ### Added
21
+
22
+ - **`android_accessibility_audit` gains screen-reader reachability and reading
23
+ order.** A clickable node with `focusable="false"` can be tapped by a sighted
24
+ user and never reached by TalkBack at all: the control is invisible to the
25
+ screen reader rather than merely unnamed, so it is a `critical` finding, above
26
+ a missing contentDescription. The reading-order check is the same
27
+ rows-then-columns comparison the iOS side uses, sharing one implementation,
28
+ with `rtl` for right-to-left layouts.
29
+
30
+ ### Fixed
31
+
32
+ - **A claim of mine in three places was wrong.** The notes on `android_meminfo`
33
+ and the 3.3.0 entry said this machine has no adb. It has adb; what it has no
34
+ Android device or emulator to run against. The distinction matters to anyone
35
+ reading those notes to decide what still needs verifying, so all three now say
36
+ what is actually missing.
37
+
38
+ ### Note
39
+
40
+ Stage 4 of 4, and the last one that could not be measured here. The iOS work was
41
+ verified against a live simulator at every step, including a generated app with
42
+ planted defects for Apple's own audit. The Android checks are written against
43
+ the documented `uiautomator dump` shape and pinned by fixtures; nothing on this
44
+ machine could run them against a device. The first real run is their measurement,
45
+ and the hierarchy attribute they depend on, `focusable`, is one uiautomator has
46
+ always emitted.
47
+
18
48
  ## 3.6.0
19
49
 
20
50
  ### Added
@@ -47,7 +77,7 @@ Releases before this file exists are recorded in the git tags and commit history
47
77
  ### Note
48
78
 
49
79
  Stage 3 of 4. The Android side is last and stays unverifiable here: this machine
50
- has no adb and no emulator.
80
+ has adb but no device or emulator to run it against.
51
81
 
52
82
  ## 3.5.0
53
83
 
@@ -85,7 +115,7 @@ has no adb and no emulator.
85
115
  Stage 2 of 4. Contrast, Dynamic Type and clipped text still cannot be reached
86
116
  from a tree dump; they need Apple's own `XCUIAccessibilityAudit`, which is an
87
117
  XCUITest run rather than a simctl call. The Android side is stage 4 and is
88
- unverifiable on a machine with no adb.
118
+ unverifiable without a device or emulator to run adb against.
89
119
 
90
120
  ## 3.4.0
91
121
 
@@ -161,7 +191,8 @@ order, traits and hints come next, then Apple's own `XCUIAccessibilityAudit`
161
191
  ### Note
162
192
 
163
193
  `android_meminfo`'s parser was written against the documented output shape on a
164
- machine with no adb and no emulator. The `leaks` parser was written against live
194
+ machine with adb installed but no device or emulator to run it against. The
195
+ `leaks` parser was written against live
165
196
  output captured from real runs, including the leaking and the unmeasurable case.
166
197
  The tests pin both shapes; the first real Android run is that half's measurement.
167
198
 
package/index.js CHANGED
@@ -923,7 +923,7 @@ const ANDROID_TOOLS = [
923
923
  { name: "android_get_screen_size", description: "Get Android screen resolution", inputSchema: { type: "object", properties: { device_id: { type: "string" } } } },
924
924
  { name: "android_open_url", description: "Open URL or deep link on Android", inputSchema: { type: "object", properties: { url: { type: "string" }, device_id: { type: "string" } }, required: ["url"] } },
925
925
  { name: "android_clear_app_data", description: "Clear all data for Android app", inputSchema: { type: "object", properties: { package_name: { type: "string" }, device_id: { type: "string" } }, required: ["package_name"] } },
926
- { name: "android_accessibility_audit", description: "Audit Android app accessibility: missing contentDescription, small touch targets (<48dp), missing resource-id. Use scope to filter by resource-id prefix.", inputSchema: { type: "object", properties: { device_id: { type: "string" }, scope: { type: "string", description: "Filter: only audit elements whose resource-id contains this prefix (e.g. 'login_', 'com.example:id/login_'). Omit to audit all." } } } },
926
+ { name: "android_accessibility_audit", description: "Audit Android app accessibility on the connected device: missing contentDescription, clickable nodes TalkBack cannot focus, touch targets under 48dp, missing resource-ids, and whether the reading order follows the visual layout. Reports measurable:false with a reason rather than a clean result when the dump could not be read.", inputSchema: { type: "object", properties: { device_id: { type: "string" }, scope: { type: "string", description: "Filter: only audit elements whose resource-id contains this prefix (e.g. 'login_', 'com.example:id/login_'). Omit to audit all." }, rtl: { type: "boolean", description: "Expect right-to-left reading within a row. Default false." } } } },
927
927
  { name: "android_launch_time", description: "Measure Android app launch time: force-stops the package, starts it with am start -W, and reports TotalTime/WaitTime in ms plus the platform's own LaunchState (COLD/WARM/HOT). Below Android 10 there is no LaunchState and cold_start is null rather than assumed.", inputSchema: { type: "object", properties: { package_name: { type: "string" }, activity: { type: "string" }, device_id: { type: "string" } }, required: ["package_name"] } },
928
928
  { name: "android_apk_audit", description: "Audit APK/AAB for Play Store compliance: debug flag, target SDK, permissions, signing, ProGuard", inputSchema: { type: "object", properties: { apk_path: { type: "string", description: "Path to .apk file" } }, required: ["apk_path"] } },
929
929
  { name: "android_meminfo", description: "Read an Android app's memory via `adb shell dumpsys meminfo` (KB, no root). mode=snapshot returns the App Summary rows and totals; mode=diff compares two snapshots so growth across the same flow is visible, which is the signal a leak actually produces - a single absolute number says almost nothing. Reports measurable:false when the package has no running process rather than returning zeros.", inputSchema: { type: "object", properties: { package_name: { type: "string" }, device_id: { type: "string" }, mode: { type: "string", enum: ["snapshot", "diff"], description: "Default: snapshot" }, baseline_json: { type: "string", description: "mode=diff: the JSON returned by an earlier snapshot call" } }, required: ["package_name"] } },
@@ -1084,13 +1084,14 @@ async function handleAndroid(name, args, ctx = {}) {
1084
1084
  run(`adb ${df} pull /sdcard/_mcp_a11y.xml ${shq(f)}`);
1085
1085
  run(`adb ${df} shell rm /sdcard/_mcp_a11y.xml`);
1086
1086
  const xml = existsSync(f) ? readFileSync(f, "utf-8") : "";
1087
- const r = auditAndroidDump({ xml, scope: args.scope || null });
1087
+ const r = auditAndroidDump({ xml, scope: args.scope || null, rtl: args.rtl === true });
1088
1088
  return JSON.stringify({
1089
1089
  scope: r.scope,
1090
1090
  measurable: r.measurable,
1091
1091
  reason: r.reason,
1092
1092
  elements_scanned: r.elementsScanned,
1093
1093
  elements_skipped: r.elementsSkipped,
1094
+ reading_order_ok: r.readingOrderOk ?? null,
1094
1095
  total_issues: r.totalIssues,
1095
1096
  critical: r.critical,
1096
1097
  important: r.important,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmerterden/multi-agent-toolkit-mcp",
3
- "version": "3.6.0",
3
+ "version": "3.7.0",
4
4
  "description": "MCP server for iOS Simulator, Android Emulator and headless web control. 87 tools: device automation (tap/swipe/type), accessibility audits, visual diff, crash logs, App Store / Play Store pre-submission compliance. Runs standalone over stdio with any MCP client.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -250,7 +250,7 @@ export function auditIosTree({ tree, scope = null, rtl = false }) {
250
250
  * @param {string|null} [params.scope] - only audit resource-ids containing this
251
251
  * @returns {object} same shape as auditIosTree
252
252
  */
253
- export function auditAndroidDump({ xml, scope = null }) {
253
+ export function auditAndroidDump({ xml, scope = null, rtl = false }) {
254
254
  const text = typeof xml === "string" ? xml : "";
255
255
  if (!/<node\b/.test(text)) {
256
256
  return unmeasurable(scope, "the uiautomator dump held no nodes; the dump failed or the screen was not ready");
@@ -259,6 +259,7 @@ export function auditAndroidDump({ xml, scope = null }) {
259
259
  const issues = [];
260
260
  let elementsScanned = 0;
261
261
  let elementsSkipped = 0;
262
+ const sequence = [];
262
263
 
263
264
  for (const match of text.matchAll(/<node[^>]*>/g)) {
264
265
  const node = match[0];
@@ -276,13 +277,25 @@ export function auditAndroidDump({ xml, scope = null }) {
276
277
  }
277
278
  elementsScanned++;
278
279
 
280
+ // TalkBack moves between FOCUSABLE nodes. A clickable node that is not
281
+ // focusable can be tapped by a sighted user and never reached by a screen
282
+ // reader at all, which is a harder failure than a missing label: the
283
+ // control is invisible to the user rather than merely unnamed.
284
+ if (/focusable="false"/.test(node)) {
285
+ issues.push({ severity: "critical", issue: "Clickable but not focusable: TalkBack cannot reach this control", element: cls, resourceId: rid || null });
286
+ }
287
+
279
288
  if (!desc && !label) issues.push({ severity: "critical", issue: "Missing contentDescription", element: cls, resourceId: rid || null });
280
289
  if (!rid) issues.push({ severity: "warning", issue: "Missing resource-id (UI testing)", element: cls });
281
290
 
282
291
  const bounds = node.match(/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/);
283
292
  if (bounds) {
284
- const w = parseInt(bounds[3], 10) - parseInt(bounds[1], 10);
285
- const h = parseInt(bounds[4], 10) - parseInt(bounds[2], 10);
293
+ const x = parseInt(bounds[1], 10);
294
+ const y = parseInt(bounds[2], 10);
295
+ const w = parseInt(bounds[3], 10) - x;
296
+ const h = parseInt(bounds[4], 10) - y;
297
+ // Same shape the iOS side uses, so one reading-order check serves both.
298
+ sequence.push({ role: cls, identifier: rid || null, title: desc || label || null, frame: { x, y, w, h } });
286
299
  if (w < ANDROID_MIN_TAP_DP || h < ANDROID_MIN_TAP_DP) {
287
300
  issues.push({ severity: "important", issue: `Touch target too small: ${w}x${h}dp (min ${ANDROID_MIN_TAP_DP}x${ANDROID_MIN_TAP_DP})`, element: cls, resourceId: rid || null });
288
301
  }
@@ -297,12 +310,25 @@ export function auditAndroidDump({ xml, scope = null }) {
297
310
  );
298
311
  }
299
312
 
313
+ const order = checkReadingOrder(sequence, rtl);
314
+ if (!order.ok) {
315
+ const el = order.firstOutOfPlace;
316
+ issues.push({
317
+ severity: "important",
318
+ issue: `Reading order does not follow the visual layout: this element is read at position ${sequence.indexOf(el) + 1} but sits at position ${order.expectedIndex + 1} on screen`,
319
+ element: el.role,
320
+ resourceId: el.identifier || null,
321
+ label: el.title || null,
322
+ });
323
+ }
324
+
300
325
  return {
301
326
  measurable: true,
302
327
  reason: null,
303
328
  scope: scope || "all",
304
329
  elementsScanned,
305
330
  elementsSkipped,
331
+ readingOrderOk: order.ok,
306
332
  ...tally(issues),
307
333
  };
308
334
  }
@@ -86,7 +86,8 @@ const MEMINFO_ROWS = [
86
86
  * absolute number says almost nothing.
87
87
  *
88
88
  * NOT VERIFIED ON A DEVICE. Written against the documented output shape on a
89
- * machine with no adb and no emulator, unlike the leaks parser above, which was
89
+ * machine with adb installed but no device or emulator to run it against,
90
+ * unlike the leaks parser above, which was
90
91
  * checked against live output. The tests below pin the shape this expects; the
91
92
  * first real run is the measurement.
92
93
  *