@mmerterden/multi-agent-toolkit-mcp 3.1.0 → 3.1.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 +19 -0
- package/index.js +15 -4
- package/package.json +2 -2
- package/tools/launch-time/index.js +53 -0
package/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,25 @@ Releases before this file exists are recorded in the git tags and commit history
|
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
18
|
+
## 3.1.1
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- `android_launch_time` reported `cold_start: true` on every call. It force-stops
|
|
23
|
+
the package first, which kills the process but leaves the page cache warm, so
|
|
24
|
+
the start it measures is cold only some of the time. Android 10 replaced
|
|
25
|
+
`ThisTime` with `LaunchState` (COLD, WARM, HOT, UNKNOWN), which is the
|
|
26
|
+
platform's own verdict on the start it just performed, and the tool was
|
|
27
|
+
ignoring it while asserting the answer itself. The result now carries
|
|
28
|
+
`launch_state` and derives `cold_start` from it. Below Android 10 nothing
|
|
29
|
+
reports a launch state, so `cold_start` is `null` there rather than a claim
|
|
30
|
+
nothing checked. A failed launch now surfaces its `Error:` line instead of
|
|
31
|
+
returning null timings that read like a measurement.
|
|
32
|
+
- The parser moved to `tools/launch-time/` with a suite covering the Android 10+
|
|
33
|
+
and pre-10 output shapes, a warm start, a failed launch and non-string input.
|
|
34
|
+
It could not be tested where it was: `index.js` connects its transport at
|
|
35
|
+
import time, so nothing in it is reachable from a test.
|
|
36
|
+
|
|
18
37
|
## Unreleased
|
|
19
38
|
|
|
20
39
|
## 3.1.0
|
package/index.js
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
validateApp,
|
|
29
29
|
} from "./tools/ios-testflight/index.js";
|
|
30
30
|
import { DESIGN_TOOLS, handleDesign } from "./tools/design-check/index.js";
|
|
31
|
+
import { parseLaunchOutput } from "./tools/launch-time/index.js";
|
|
31
32
|
import { interactiveElements } from "./tools/ui-inspect/index.js";
|
|
32
33
|
import { selectCrashReports } from "./tools/crash-logs/index.js";
|
|
33
34
|
import {
|
|
@@ -829,7 +830,7 @@ const ANDROID_TOOLS = [
|
|
|
829
830
|
{ 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"] } },
|
|
830
831
|
{ 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"] } },
|
|
831
832
|
{ 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." } } } },
|
|
832
|
-
{ name: "android_launch_time", description: "Measure Android app
|
|
833
|
+
{ 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"] } },
|
|
833
834
|
{ 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"] } },
|
|
834
835
|
{ name: "android_list_crashes", description: "Dump the Android crash log buffer (`adb logcat -b crash -d`), tail-bounded. Empty output means no crashes since the buffer was last cleared.", inputSchema: { type: "object", properties: { lines: { type: "number", description: "Max lines returned, from the end (default 200)" }, device_id: { type: "string" } } } },
|
|
835
836
|
{ name: "android_set_orientation", description: "Rotate the Android screen to portrait or landscape. Disables accelerometer rotation and pins user_rotation, so the device stays put until rotation is re-enabled. Accounts for the device's natural orientation (detected via wm size), so landscape-natural tablets rotate correctly too. No iOS counterpart: simctl exposes no rotation lever.", inputSchema: { type: "object", properties: { orientation: { type: "string", enum: ["portrait", "landscape"] }, device_id: { type: "string" } }, required: ["orientation"] } },
|
|
@@ -1020,9 +1021,19 @@ async function handleAndroid(name, args, ctx = {}) {
|
|
|
1020
1021
|
run(`adb ${df} shell am force-stop ${sanitizeId(args.package_name)}`);
|
|
1021
1022
|
const activity = sanitizeId(args.activity || `${args.package_name}/.MainActivity`);
|
|
1022
1023
|
const result = run(`adb ${df} shell am start -W -n ${activity} 2>&1`);
|
|
1023
|
-
const
|
|
1024
|
-
|
|
1025
|
-
|
|
1024
|
+
const parsed = parseLaunchOutput(result);
|
|
1025
|
+
return JSON.stringify({
|
|
1026
|
+
package: args.package_name,
|
|
1027
|
+
// The platform's own verdict, not ours: force-stop kills the process but
|
|
1028
|
+
// leaves the page cache warm, so this is a cold start only sometimes.
|
|
1029
|
+
// null means the device is below Android 10 and never reported one.
|
|
1030
|
+
launch_state: parsed.launchState,
|
|
1031
|
+
cold_start: parsed.coldStart,
|
|
1032
|
+
total_time_ms: parsed.totalTimeMs,
|
|
1033
|
+
wait_time_ms: parsed.waitTimeMs,
|
|
1034
|
+
error: parsed.error,
|
|
1035
|
+
raw: result,
|
|
1036
|
+
}, null, 2);
|
|
1026
1037
|
}
|
|
1027
1038
|
case "android_apk_audit": {
|
|
1028
1039
|
const p = args.apk_path;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mmerterden/multi-agent-toolkit-mcp",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"description": "MCP server for iOS Simulator, Android Emulator and headless web control. 84 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",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"start": "node index.js",
|
|
13
|
-
"test": "node --test tools/design-check/__tests__/design-check.test.mjs tools/design-check/__tests__/plan-determinism.test.mjs tools/ios-app-store-audit/__tests__/app-store-audit.test.mjs tools/ios-testflight/__tests__/testflight.test.mjs tools/ui-inspect/__tests__/ui-inspect.test.mjs tools/crash-logs/__tests__/crash-logs.test.mjs tools/offload/__tests__/offload.test.mjs __tests__/server-tools.test.mjs __tests__/injection.test.mjs",
|
|
13
|
+
"test": "node --test tools/design-check/__tests__/design-check.test.mjs tools/design-check/__tests__/plan-determinism.test.mjs tools/ios-app-store-audit/__tests__/app-store-audit.test.mjs tools/ios-testflight/__tests__/testflight.test.mjs tools/ui-inspect/__tests__/ui-inspect.test.mjs tools/crash-logs/__tests__/crash-logs.test.mjs tools/launch-time/__tests__/launch-time.test.mjs tools/offload/__tests__/offload.test.mjs __tests__/server-tools.test.mjs __tests__/injection.test.mjs",
|
|
14
14
|
"gates": "bash scripts/gates.sh"
|
|
15
15
|
},
|
|
16
16
|
"keywords": [
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* launch-time - parse `am start -W` output.
|
|
3
|
+
*
|
|
4
|
+
* Lives here rather than inline in index.js because index.js connects its
|
|
5
|
+
* transport at import time and cannot be loaded by a test.
|
|
6
|
+
*
|
|
7
|
+
* The handler force-stops the package first, which kills the process but leaves
|
|
8
|
+
* the page cache warm, so it produces a cold start only some of the time. The
|
|
9
|
+
* tool used to report `cold_start: true` unconditionally, which is an assertion
|
|
10
|
+
* about something the platform already measures: Android 10 replaced `ThisTime`
|
|
11
|
+
* with `LaunchState`, one of COLD, WARM, HOT or UNKNOWN, and that is the
|
|
12
|
+
* system's own verdict on the start it just performed. Below Android 10 there is
|
|
13
|
+
* no LaunchState line, and the honest answer there is "unknown" rather than a
|
|
14
|
+
* claim nothing checked.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const LAUNCH_STATES = new Set(["COLD", "WARM", "HOT", "UNKNOWN"]);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {string} raw - stdout+stderr of `am start -W -n <activity>`
|
|
21
|
+
* @returns {{status: string|null, launchState: string|null, coldStart: boolean|null,
|
|
22
|
+
* activity: string|null, totalTimeMs: number|null, waitTimeMs: number|null,
|
|
23
|
+
* error: string|null}}
|
|
24
|
+
*/
|
|
25
|
+
export function parseLaunchOutput(raw) {
|
|
26
|
+
const text = typeof raw === "string" ? raw : "";
|
|
27
|
+
const field = (name) => text.match(new RegExp(`^\\s*${name}:\\s*(.+?)\\s*$`, "m"))?.[1] ?? null;
|
|
28
|
+
const intField = (name) => {
|
|
29
|
+
const v = text.match(new RegExp(`^\\s*${name}:\\s*(\\d+)\\s*$`, "m"))?.[1];
|
|
30
|
+
return v === undefined ? null : parseInt(v, 10);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const rawState = field("LaunchState");
|
|
34
|
+
const launchState = rawState && LAUNCH_STATES.has(rawState.toUpperCase())
|
|
35
|
+
? rawState.toUpperCase()
|
|
36
|
+
: null;
|
|
37
|
+
|
|
38
|
+
// `am start` reports a failure on a line of its own and still exits 0, so a
|
|
39
|
+
// missing TotalTime with an Error line is a failed launch, not a parse miss.
|
|
40
|
+
const errorLine = text.match(/^\s*Error:\s*(.+?)\s*$/m)?.[1] ?? null;
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
status: field("Status"),
|
|
44
|
+
launchState,
|
|
45
|
+
// COLD is the only state that is a cold start. Below Android 10 nothing
|
|
46
|
+
// reports it, so null means "not measured", never "no".
|
|
47
|
+
coldStart: launchState === null ? null : launchState === "COLD",
|
|
48
|
+
activity: field("Activity"),
|
|
49
|
+
totalTimeMs: intField("TotalTime"),
|
|
50
|
+
waitTimeMs: intField("WaitTime"),
|
|
51
|
+
error: errorLine,
|
|
52
|
+
};
|
|
53
|
+
}
|