@mmerterden/multi-agent-toolkit-mcp 3.6.0 → 3.7.1
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 +58 -3
- package/index.js +3 -2
- package/package.json +1 -1
- package/tools/a11y/index.js +29 -3
- package/tools/memory/index.js +11 -5
package/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,60 @@ Releases before this file exists are recorded in the git tags and commit history
|
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
18
|
+
## 3.7.1
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **`android_meminfo` never read the swap total on a real device.** The parser
|
|
23
|
+
was written from the documented output, which spells the line
|
|
24
|
+
`TOTAL SWAP (KB):`. An Android 35 Pixel emulator prints `TOTAL SWAP PSS:`, so
|
|
25
|
+
the regex matched nothing and `total_swap_kb` came back null on every real
|
|
26
|
+
run. Both spellings are accepted now, and the fixture is the emulator's own
|
|
27
|
+
output rather than a reconstruction.
|
|
28
|
+
|
|
29
|
+
This is the defect the 3.3.0 note predicted: that parser shipped marked "not
|
|
30
|
+
verified on a device", and the first real device broke it. Worth recording
|
|
31
|
+
plainly, because the note was right and the only thing that found the bug was
|
|
32
|
+
running it.
|
|
33
|
+
|
|
34
|
+
### Note
|
|
35
|
+
|
|
36
|
+
The Android checks are no longer unverified. `android_accessibility_audit` and
|
|
37
|
+
`android_meminfo` were both run against a booted Android 35 emulator: the audit
|
|
38
|
+
read 14 clickable elements off a real launcher, reported the reading order as
|
|
39
|
+
correct with no false positive on the grid, and produced one critical and seven
|
|
40
|
+
warnings; meminfo read the App Summary and, after the fix above, the swap total.
|
|
41
|
+
|
|
42
|
+
## 3.7.0
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- **`android_accessibility_audit` gains screen-reader reachability and reading
|
|
47
|
+
order.** A clickable node with `focusable="false"` can be tapped by a sighted
|
|
48
|
+
user and never reached by TalkBack at all: the control is invisible to the
|
|
49
|
+
screen reader rather than merely unnamed, so it is a `critical` finding, above
|
|
50
|
+
a missing contentDescription. The reading-order check is the same
|
|
51
|
+
rows-then-columns comparison the iOS side uses, sharing one implementation,
|
|
52
|
+
with `rtl` for right-to-left layouts.
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
|
|
56
|
+
- **A claim of mine in three places was wrong.** The notes on `android_meminfo`
|
|
57
|
+
and the 3.3.0 entry said this machine has no adb. It has adb; what it has no
|
|
58
|
+
Android device or emulator to run against. The distinction matters to anyone
|
|
59
|
+
reading those notes to decide what still needs verifying, so all three now say
|
|
60
|
+
what is actually missing.
|
|
61
|
+
|
|
62
|
+
### Note
|
|
63
|
+
|
|
64
|
+
Stage 4 of 4, and the last one that could not be measured here. The iOS work was
|
|
65
|
+
verified against a live simulator at every step, including a generated app with
|
|
66
|
+
planted defects for Apple's own audit. The Android checks are written against
|
|
67
|
+
the documented `uiautomator dump` shape and pinned by fixtures; nothing on this
|
|
68
|
+
machine could run them against a device. The first real run is their measurement,
|
|
69
|
+
and the hierarchy attribute they depend on, `focusable`, is one uiautomator has
|
|
70
|
+
always emitted.
|
|
71
|
+
|
|
18
72
|
## 3.6.0
|
|
19
73
|
|
|
20
74
|
### Added
|
|
@@ -47,7 +101,7 @@ Releases before this file exists are recorded in the git tags and commit history
|
|
|
47
101
|
### Note
|
|
48
102
|
|
|
49
103
|
Stage 3 of 4. The Android side is last and stays unverifiable here: this machine
|
|
50
|
-
has
|
|
104
|
+
has adb but no device or emulator to run it against.
|
|
51
105
|
|
|
52
106
|
## 3.5.0
|
|
53
107
|
|
|
@@ -85,7 +139,7 @@ has no adb and no emulator.
|
|
|
85
139
|
Stage 2 of 4. Contrast, Dynamic Type and clipped text still cannot be reached
|
|
86
140
|
from a tree dump; they need Apple's own `XCUIAccessibilityAudit`, which is an
|
|
87
141
|
XCUITest run rather than a simctl call. The Android side is stage 4 and is
|
|
88
|
-
unverifiable
|
|
142
|
+
unverifiable without a device or emulator to run adb against.
|
|
89
143
|
|
|
90
144
|
## 3.4.0
|
|
91
145
|
|
|
@@ -161,7 +215,8 @@ order, traits and hints come next, then Apple's own `XCUIAccessibilityAudit`
|
|
|
161
215
|
### Note
|
|
162
216
|
|
|
163
217
|
`android_meminfo`'s parser was written against the documented output shape on a
|
|
164
|
-
machine with
|
|
218
|
+
machine with adb installed but no device or emulator to run it against. The
|
|
219
|
+
`leaks` parser was written against live
|
|
165
220
|
output captured from real runs, including the leaking and the unmeasurable case.
|
|
166
221
|
The tests pin both shapes; the first real Android run is that half's measurement.
|
|
167
222
|
|
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,
|
|
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.
|
|
3
|
+
"version": "3.7.1",
|
|
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",
|
package/tools/a11y/index.js
CHANGED
|
@@ -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
|
|
285
|
-
const
|
|
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
|
}
|
package/tools/memory/index.js
CHANGED
|
@@ -85,10 +85,12 @@ const MEMINFO_ROWS = [
|
|
|
85
85
|
* snapshots of the same flow is the shape a leak takes, where a single
|
|
86
86
|
* absolute number says almost nothing.
|
|
87
87
|
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
88
|
+
* Verified against a real device: an Android 35 Pixel emulator, after the parser
|
|
89
|
+
* had first been written from documentation alone. That first version returned
|
|
90
|
+
* null for the swap total on every real device, because the documented shape
|
|
91
|
+
* says "TOTAL SWAP (KB):" and the device prints "TOTAL SWAP PSS:". Both are
|
|
92
|
+
* accepted now. It is the clearest argument in this file for running a parser
|
|
93
|
+
* against the thing it parses.
|
|
92
94
|
*
|
|
93
95
|
* @param {string} raw
|
|
94
96
|
* @returns {{measurable: boolean, reason: string|null, pss: object, totalPssKb: number|null,
|
|
@@ -117,7 +119,11 @@ export function parseMeminfoOutput(raw) {
|
|
|
117
119
|
};
|
|
118
120
|
const totalPssKb = num(/TOTAL PSS:\s*(\d+)/i);
|
|
119
121
|
const totalRssKb = num(/TOTAL RSS:\s*(\d+)/i);
|
|
120
|
-
|
|
122
|
+
// Two spellings in the wild: "TOTAL SWAP (KB):" in the documented shape and
|
|
123
|
+
// "TOTAL SWAP PSS:" on a real Android 35 device, which is what a Pixel
|
|
124
|
+
// emulator actually printed. The first regex, written from documentation,
|
|
125
|
+
// returned null against every real device.
|
|
126
|
+
const totalSwapKb = num(/TOTAL SWAP(?: \(KB\)| PSS)?:\s*(\d+)/i);
|
|
121
127
|
|
|
122
128
|
if (found === 0 && totalPssKb === null) {
|
|
123
129
|
return { measurable: false, reason: "no App Summary block in the dumpsys output", pss: {}, totalPssKb: null, totalRssKb: null, totalSwapKb: null };
|