mobile-debug-mcp 0.32.1 → 0.32.4

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.
@@ -1,34 +0,0 @@
1
- import { execFileSync } from 'node:child_process'
2
- import { existsSync, mkdirSync } from 'node:fs'
3
- import path from 'node:path'
4
- import { fileURLToPath } from 'node:url'
5
-
6
- export function buildBridge() {
7
- const root = path.dirname(fileURLToPath(import.meta.url))
8
- const out = path.join(root, 'build')
9
- const sdk = process.env.ANDROID_SDK_ROOT || process.env.ANDROID_HOME ||
10
- (process.platform === 'darwin' && process.env.HOME ? path.join(process.env.HOME, 'Library/Android/sdk') : '')
11
- if (!sdk) throw new Error('Android SDK not found: set ANDROID_SDK_ROOT or ANDROID_HOME')
12
- const buildTools = path.join(sdk, 'build-tools', '36.0.0')
13
- const androidJar = path.join(sdk, 'platforms', 'android-36', 'android.jar')
14
- if (!existsSync(androidJar) || !existsSync(buildTools)) {
15
- throw new Error(`Android SDK 36 platform and build-tools 36.0.0 are required under ${sdk}`)
16
- }
17
- const java = process.env.JAVA_HOME
18
- const tool = (name: string) => java ? path.join(java, 'bin', name) : name
19
- const run = (cmd: string, args: string[]) => execFileSync(cmd, args, { cwd: out, stdio: 'pipe' })
20
- mkdirSync(path.join(out, 'classes'), { recursive: true })
21
- run(tool('javac'), ['--release', '8', '-classpath', androidJar, '-d', 'classes', path.join(root, 'src/TreeInstrumentation.java')])
22
- run(tool('jar'), ['cf', 'classes.jar', '-C', 'classes', '.'])
23
- run(path.join(buildTools, 'd8'), ['--lib', androidJar, '--output', out, 'classes.jar'])
24
- run(path.join(buildTools, 'aapt2'), ['link', '-I', androidJar, '--manifest', path.join(root, 'AndroidManifest.xml'), '-o', 'unsigned.apk'])
25
- run('zip', ['-q', 'unsigned.apk', 'classes.dex'])
26
- run(path.join(buildTools, 'zipalign'), ['-f', '4', 'unsigned.apk', 'aligned.apk'])
27
- try { run(tool('keytool'), ['-genkeypair', '-keystore', 'fixture.jks', '-storepass', 'android', '-keypass', 'android', '-alias', 'fixture', '-dname', 'CN=UI Tree Bridge', '-keyalg', 'RSA', '-validity', '3650']) } catch { /* Existing local fixture key. */ }
28
- run(path.join(buildTools, 'apksigner'), ['sign', '--ks', 'fixture.jks', '--ks-pass', 'pass:android', '--out', 'bridge.apk', 'aligned.apk'])
29
- return path.join(out, 'bridge.apk')
30
- }
31
-
32
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
33
- console.log(buildBridge())
34
- }
@@ -1,119 +0,0 @@
1
- package dev.mobiledebugmcp.treebridge;
2
-
3
- import android.app.Instrumentation;
4
- import android.app.UiAutomation;
5
- import android.graphics.Rect;
6
- import android.os.Bundle;
7
- import android.util.Xml;
8
- import android.util.Log;
9
- import android.view.accessibility.AccessibilityNodeInfo;
10
- import java.io.BufferedReader;
11
- import java.io.InputStreamReader;
12
- import java.io.OutputStreamWriter;
13
- import java.io.StringWriter;
14
- import java.net.InetAddress;
15
- import java.net.ServerSocket;
16
- import java.net.Socket;
17
- import org.xmlpull.v1.XmlSerializer;
18
-
19
- /** Benchmark-only bridge: one live accessibility acquisition per TREE request. */
20
- public final class TreeInstrumentation extends Instrumentation {
21
- private static final int PORT = 39019;
22
-
23
- @Override public void onCreate(Bundle arguments) {
24
- super.onCreate(arguments);
25
- start();
26
- }
27
-
28
- @Override public void onStart() {
29
- try (ServerSocket server = new ServerSocket(PORT, 1, InetAddress.getByName("127.0.0.1"))) {
30
- UiAutomation automation = getUiAutomation(UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES);
31
- while (true) {
32
- try (Socket socket = server.accept()) {
33
- socket.setSoTimeout(10000);
34
- BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
35
- OutputStreamWriter output = new OutputStreamWriter(socket.getOutputStream(), "UTF-8");
36
- String command = input.readLine();
37
- if ("STOP".equals(command)) {
38
- output.write("OK\n");
39
- output.flush();
40
- break;
41
- }
42
- if ("PING".equals(command)) {
43
- output.write("OK\n");
44
- output.flush();
45
- continue;
46
- }
47
- if (!"TREE".equals(command)) {
48
- output.write("ERROR invalid_command\n");
49
- } else {
50
- AccessibilityNodeInfo root = automation.getRootInActiveWindow();
51
- if (root == null) {
52
- output.write("ERROR null_root\n");
53
- } else {
54
- output.write("OK\n");
55
- output.write(serialize(root));
56
- root.recycle();
57
- }
58
- }
59
- output.flush();
60
- } catch (Exception error) {
61
- // A failed client request cannot make a later acquisition reuse its tree.
62
- Log.e("TreeBridge", "request failed", error);
63
- }
64
- }
65
- } catch (Exception error) {
66
- // The host-side probe detects failure to start or connect.
67
- Log.e("TreeBridge", "bridge failed", error);
68
- }
69
- finish(0, new Bundle());
70
- }
71
-
72
- private static String serialize(AccessibilityNodeInfo root) throws Exception {
73
- StringWriter writer = new StringWriter();
74
- XmlSerializer xml = Xml.newSerializer();
75
- xml.setOutput(writer);
76
- xml.startDocument("UTF-8", true);
77
- xml.startTag("", "hierarchy");
78
- writeNode(xml, root, 0);
79
- xml.endTag("", "hierarchy");
80
- xml.endDocument();
81
- return writer.toString();
82
- }
83
-
84
- private static void writeNode(XmlSerializer xml, AccessibilityNodeInfo node, int index) throws Exception {
85
- Rect bounds = new Rect();
86
- node.getBoundsInScreen(bounds);
87
- xml.startTag("", "node");
88
- xml.attribute("", "index", Integer.toString(index));
89
- xml.attribute("", "class", value(node.getClassName()));
90
- xml.attribute("", "package", value(node.getPackageName()));
91
- xml.attribute("", "text", value(node.getText()));
92
- xml.attribute("", "content-desc", value(node.getContentDescription()));
93
- xml.attribute("", "resource-id", value(node.getViewIdResourceName()));
94
- xml.attribute("", "checkable", Boolean.toString(node.isCheckable()));
95
- xml.attribute("", "checked", Boolean.toString(node.isChecked()));
96
- xml.attribute("", "clickable", Boolean.toString(node.isClickable()));
97
- xml.attribute("", "enabled", Boolean.toString(node.isEnabled()));
98
- xml.attribute("", "focusable", Boolean.toString(node.isFocusable()));
99
- xml.attribute("", "focused", Boolean.toString(node.isFocused()));
100
- xml.attribute("", "scrollable", Boolean.toString(node.isScrollable()));
101
- xml.attribute("", "long-clickable", Boolean.toString(node.isLongClickable()));
102
- xml.attribute("", "password", Boolean.toString(node.isPassword()));
103
- xml.attribute("", "selected", Boolean.toString(node.isSelected()));
104
- xml.attribute("", "visible-to-user", Boolean.toString(node.isVisibleToUser()));
105
- xml.attribute("", "bounds", bounds.toShortString());
106
- for (int childIndex = 0; childIndex < node.getChildCount(); childIndex++) {
107
- AccessibilityNodeInfo child = node.getChild(childIndex);
108
- if (child != null && child.isVisibleToUser()) {
109
- writeNode(xml, child, childIndex);
110
- }
111
- if (child != null) child.recycle();
112
- }
113
- xml.endTag("", "node");
114
- }
115
-
116
- private static String value(CharSequence input) {
117
- return input == null ? "" : input.toString();
118
- }
119
- }
@@ -1,76 +0,0 @@
1
- import assert from 'node:assert/strict'
2
- import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
3
- import { createServer } from 'node:net'
4
- import type { Socket } from 'node:net'
5
- import { tmpdir } from 'node:os'
6
- import path from 'node:path'
7
- import { readAndroidTreeFromBridge } from '../../../src/observe/android-tree-bridge.js'
8
-
9
- async function run() {
10
- const directory = mkdtempSync(path.join(tmpdir(), 'mcp-tree-bridge-'))
11
- const adbPath = path.join(directory, 'adb')
12
- const apkPath = path.join(directory, 'bridge.apk')
13
- const launchesPath = path.join(directory, 'launches')
14
- const previous = new Map(['ADB_PATH', 'MOBILE_DEBUG_MCP_ANDROID_TREE_BRIDGE_APK', 'MCP_FAKE_BRIDGE_PORT', 'MCP_FAKE_BRIDGE_LAUNCHES', 'MCP_FAKE_FORCE_STOP_FAIL'].map((key) => [key, process.env[key]]))
15
- let invalidTree = false
16
- const sockets = new Set<Socket>()
17
- const server = createServer((socket) => {
18
- sockets.add(socket)
19
- socket.once('close', () => sockets.delete(socket))
20
- socket.once('data', (chunk) => {
21
- const command = chunk.toString().trim()
22
- socket.end(command === 'TREE'
23
- ? invalidTree ? 'ERROR null_root\n' : 'OK\n<hierarchy><node class="android.view.View" /></hierarchy>'
24
- : 'OK\n')
25
- })
26
- })
27
- writeFileSync(apkPath, '')
28
- writeFileSync(launchesPath, '')
29
- writeFileSync(adbPath, `#!/usr/bin/env node
30
- const fs = require('node:fs')
31
- const args = process.argv.slice(2).join(' ')
32
- if (args.includes('am instrument')) {
33
- fs.appendFileSync(process.env.MCP_FAKE_BRIDGE_LAUNCHES, 'launch\\n')
34
- setInterval(() => {}, 1000)
35
- } else if (args.includes('am force-stop') && process.env.MCP_FAKE_FORCE_STOP_FAIL === '1') {
36
- process.exit(1)
37
- } else if (args.includes('forward tcp:0')) {
38
- console.log(process.env.MCP_FAKE_BRIDGE_PORT)
39
- }
40
- `)
41
- chmodSync(adbPath, 0o755)
42
- await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve))
43
- const address = server.address()
44
- if (!address || typeof address === 'string') throw new Error('No test port')
45
- process.env.ADB_PATH = adbPath
46
- process.env.MOBILE_DEBUG_MCP_ANDROID_TREE_BRIDGE_APK = apkPath
47
- process.env.MCP_FAKE_BRIDGE_PORT = String(address.port)
48
- process.env.MCP_FAKE_BRIDGE_LAUNCHES = launchesPath
49
- try {
50
- assert.match(await readAndroidTreeFromBridge('test-device') ?? '', /<hierarchy>/)
51
- invalidTree = true
52
- process.env.MCP_FAKE_FORCE_STOP_FAIL = '1'
53
- await assert.rejects(() => readAndroidTreeFromBridge('test-device'), /BRIDGE_CLEANUP_UNCONFIRMED/)
54
- await assert.rejects(() => readAndroidTreeFromBridge('test-device'), /BRIDGE_CLEANUP_UNCONFIRMED/)
55
- assert.equal(readFileSync(launchesPath, 'utf8').trim().split('\n').length, 1, 'failed cleanup must block a second launch')
56
- invalidTree = false
57
- delete process.env.MCP_FAKE_FORCE_STOP_FAIL
58
- assert.match(await readAndroidTreeFromBridge('test-device') ?? '', /<hierarchy>/, 'successful cleanup allows relaunch')
59
- for (let attempt = 0; attempt < 20 && readFileSync(launchesPath, 'utf8').trim().split('\n').length < 2; attempt++) {
60
- await new Promise((resolve) => setTimeout(resolve, 50))
61
- }
62
- assert.equal(readFileSync(launchesPath, 'utf8').trim().split('\n').length, 2)
63
- invalidTree = true
64
- assert.equal(await readAndroidTreeFromBridge('test-device'), null, 'clean shutdown permits legacy fallback')
65
- } finally {
66
- for (const [key, value] of previous) {
67
- if (value === undefined) delete process.env[key]
68
- else process.env[key] = value
69
- }
70
- for (const socket of sockets) socket.destroy()
71
- await new Promise<void>((resolve) => server.close(() => resolve()))
72
- rmSync(directory, { recursive: true, force: true })
73
- }
74
- }
75
-
76
- run().catch((error) => { console.error(error); process.exitCode = 1 })