@mobileaidev/ai-app-bridge 0.2.5 → 0.2.6
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/README.md +10 -4
- package/bin/ai-app-bridge.js +70 -13
- package/bin/mcp-server.js +98 -43
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
```bash
|
|
4
4
|
npm install -g @mobileaidev/ai-app-bridge
|
|
5
5
|
|
|
6
|
-
ai-app-bridge status --package-name io.github.mobileaidev.aiappbridge.sample
|
|
6
|
+
ai-app-bridge status --package-name io.github.mobileaidev.aiappbridge.sample
|
|
7
7
|
ai-app-bridge tree --package-name io.github.mobileaidev.aiappbridge.sample
|
|
8
8
|
ai-app-bridge install-apk --package-name io.github.mobileaidev.aiappbridge.sample --apk-path app-debug.apk
|
|
9
|
+
ai-app-bridge clear-app-data --package-name io.github.mobileaidev.aiappbridge.sample
|
|
9
10
|
ai-app-bridge launch-app --package-name io.github.mobileaidev.aiappbridge.sample
|
|
10
11
|
ai-app-bridge launch-activity --package-name io.github.mobileaidev.aiappbridge.sample --activity .MainActivity --extra route=/home
|
|
11
12
|
ai-app-bridge screenshot --package-name io.github.mobileaidev.aiappbridge.sample
|
|
@@ -21,12 +22,17 @@ into the model context:
|
|
|
21
22
|
- `capabilities` lists the bridge domains and command names.
|
|
22
23
|
- `run` executes a selected command with command-specific arguments.
|
|
23
24
|
|
|
24
|
-
This keeps install, launch, UI, Flutter, WebView, logcat, network,
|
|
25
|
-
permission capabilities discoverable without exposing dozens of full schemas
|
|
26
|
-
session start. Set `AI_APP_BRIDGE_MCP_SURFACE=full` before launching
|
|
25
|
+
This keeps install, data reset, launch, UI, Flutter, WebView, logcat, network,
|
|
26
|
+
and permission capabilities discoverable without exposing dozens of full schemas
|
|
27
|
+
at session start. Set `AI_APP_BRIDGE_MCP_SURFACE=full` before launching
|
|
27
28
|
`ai-app-bridge-mcp` only when a client needs the legacy one-tool-per-command
|
|
28
29
|
surface.
|
|
29
30
|
|
|
31
|
+
The MCP server accepts both standard `Content-Length` framed JSON-RPC messages
|
|
32
|
+
and single-line JSON messages. Responses use the format of the first request on
|
|
33
|
+
that connection, so standard MCP clients keep framed responses while local
|
|
34
|
+
Node REPL scripts can send and read one JSON object per line.
|
|
35
|
+
|
|
30
36
|
For multi-step app automation, call `run` with `command: "batch"`. Batch steps
|
|
31
37
|
run serially in one MCP call, so a failed step can stop and mark the remaining
|
|
32
38
|
steps as skipped without mixing results from different commands:
|
package/bin/ai-app-bridge.js
CHANGED
|
@@ -70,6 +70,7 @@ Device/action commands:
|
|
|
70
70
|
|
|
71
71
|
App/permission commands:
|
|
72
72
|
install-apk Install an APK and assist device-side installer screens.
|
|
73
|
+
clear-app-data Clear target app local data through the bridge runtime.
|
|
73
74
|
launch-app Launch the target package LAUNCHER Activity.
|
|
74
75
|
launch-activity Launch an explicit Android Activity component.
|
|
75
76
|
launch-native-test Launch the debug native Android bridge test Activity.
|
|
@@ -296,10 +297,12 @@ async function runCommand(command, options, ctx) {
|
|
|
296
297
|
return keyboardState(ctx);
|
|
297
298
|
case 'hide-keyboard':
|
|
298
299
|
return hideKeyboard(ctx, options);
|
|
299
|
-
case 'install-apk':
|
|
300
|
-
return installApk(ctx, options);
|
|
301
|
-
case '
|
|
302
|
-
return
|
|
300
|
+
case 'install-apk':
|
|
301
|
+
return installApk(ctx, options);
|
|
302
|
+
case 'clear-app-data':
|
|
303
|
+
return clearAppData(ctx);
|
|
304
|
+
case 'webview-pages':
|
|
305
|
+
return webviewPages(ctx, options);
|
|
303
306
|
case 'webview-network':
|
|
304
307
|
return webviewCdpCapture(ctx, { ...options, captureNetwork: true, captureConsole: true });
|
|
305
308
|
case 'webview-console':
|
|
@@ -3700,17 +3703,70 @@ async function safePermissionState(ctx, permission) {
|
|
|
3700
3703
|
}
|
|
3701
3704
|
}
|
|
3702
3705
|
|
|
3703
|
-
async function appopsSet(ctx, op, mode) {
|
|
3704
|
-
await adb(ctx, ['shell', 'appops', 'set', ctx.packageName, op, mode]);
|
|
3705
|
-
return {
|
|
3706
|
+
async function appopsSet(ctx, op, mode) {
|
|
3707
|
+
await adb(ctx, ['shell', 'appops', 'set', ctx.packageName, op, mode]);
|
|
3708
|
+
return {
|
|
3706
3709
|
ok: true,
|
|
3707
3710
|
packageName: ctx.packageName,
|
|
3708
3711
|
op,
|
|
3709
3712
|
mode,
|
|
3710
|
-
};
|
|
3711
|
-
}
|
|
3712
|
-
|
|
3713
|
-
async function
|
|
3713
|
+
};
|
|
3714
|
+
}
|
|
3715
|
+
|
|
3716
|
+
async function clearAppData(ctx) {
|
|
3717
|
+
if (!ctx.explicitPackageName) {
|
|
3718
|
+
throw new Error('packageName is required for clear-app-data');
|
|
3719
|
+
}
|
|
3720
|
+
let bridgeError = null;
|
|
3721
|
+
try {
|
|
3722
|
+
const bridgeResult = await bridgePost(ctx, '/v1/app/clear-data', {});
|
|
3723
|
+
return {
|
|
3724
|
+
...bridgeResult,
|
|
3725
|
+
packageName: ctx.packageName,
|
|
3726
|
+
method: 'bridge-runtime',
|
|
3727
|
+
};
|
|
3728
|
+
} catch (error) {
|
|
3729
|
+
bridgeError = firstErrorLine(error);
|
|
3730
|
+
}
|
|
3731
|
+
|
|
3732
|
+
let result;
|
|
3733
|
+
try {
|
|
3734
|
+
result = await adb(ctx, clearAppDataAdbArgs(ctx.packageName));
|
|
3735
|
+
} catch (error) {
|
|
3736
|
+
return {
|
|
3737
|
+
ok: false,
|
|
3738
|
+
packageName: ctx.packageName,
|
|
3739
|
+
action: 'clear-app-data',
|
|
3740
|
+
error: 'clear_app_data_failed',
|
|
3741
|
+
bridgeError,
|
|
3742
|
+
message: error.message || String(error),
|
|
3743
|
+
};
|
|
3744
|
+
}
|
|
3745
|
+
const output = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
|
|
3746
|
+
if (!/^Success\b/im.test(output)) {
|
|
3747
|
+
return {
|
|
3748
|
+
ok: false,
|
|
3749
|
+
packageName: ctx.packageName,
|
|
3750
|
+
error: 'clear_app_data_failed',
|
|
3751
|
+
bridgeError,
|
|
3752
|
+
output,
|
|
3753
|
+
};
|
|
3754
|
+
}
|
|
3755
|
+
return {
|
|
3756
|
+
ok: true,
|
|
3757
|
+
packageName: ctx.packageName,
|
|
3758
|
+
action: 'clear-app-data',
|
|
3759
|
+
method: 'pm-clear',
|
|
3760
|
+
bridgeError,
|
|
3761
|
+
output,
|
|
3762
|
+
};
|
|
3763
|
+
}
|
|
3764
|
+
|
|
3765
|
+
function clearAppDataAdbArgs(packageName) {
|
|
3766
|
+
return ['shell', 'pm', 'clear', packageName];
|
|
3767
|
+
}
|
|
3768
|
+
|
|
3769
|
+
async function resolveLogcatPid(ctx, options) {
|
|
3714
3770
|
if (options.pid && options.pid !== true && options.pid !== 'current') {
|
|
3715
3771
|
return String(options.pid);
|
|
3716
3772
|
}
|
|
@@ -4303,8 +4359,9 @@ function requiredNumber(value, name) {
|
|
|
4303
4359
|
}
|
|
4304
4360
|
|
|
4305
4361
|
module.exports = {
|
|
4306
|
-
buildBridgeFailureResult,
|
|
4307
|
-
|
|
4362
|
+
buildBridgeFailureResult,
|
|
4363
|
+
clearAppDataAdbArgs,
|
|
4364
|
+
defaultInstallerButtonTexts,
|
|
4308
4365
|
artifactTimestamp,
|
|
4309
4366
|
compactBridgeTree,
|
|
4310
4367
|
compactStatus,
|
package/bin/mcp-server.js
CHANGED
|
@@ -11,12 +11,13 @@ const supportedProtocolVersions = ['2025-06-18', '2024-11-05'];
|
|
|
11
11
|
const defaultProtocolVersion = supportedProtocolVersions[0];
|
|
12
12
|
const mcpSurface = (process.env.AI_APP_BRIDGE_MCP_SURFACE || 'compact').toLowerCase();
|
|
13
13
|
const serverInstructions = [
|
|
14
|
-
'AI App Bridge observes and controls Android apps for agent workflows. Prefer these tools over raw adb when inspecting UI, text, WebView, logs, network, app install, launch, and permissions.',
|
|
14
|
+
'AI App Bridge observes and controls Android apps for agent workflows. Prefer these tools over raw adb when inspecting UI, text, WebView, logs, network, app install, data reset, launch, and permissions.',
|
|
15
15
|
'Default surface is compact: call capabilities to discover domains, then call run with a command and arguments.',
|
|
16
16
|
'Always pass packageName for app-specific commands, or pass an explicit port. Do not rely on a sample/default package in MCP sessions.',
|
|
17
17
|
].join(' ');
|
|
18
18
|
|
|
19
19
|
let buffer = Buffer.alloc(0);
|
|
20
|
+
let responseFormat = null;
|
|
20
21
|
|
|
21
22
|
function startServer() {
|
|
22
23
|
process.stdin.on('data', (chunk) => {
|
|
@@ -27,34 +28,77 @@ function startServer() {
|
|
|
27
28
|
process.stdin.on('error', () => {});
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
function drainMessages() {
|
|
31
|
-
while (true) {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
31
|
+
function drainMessages() {
|
|
32
|
+
while (true) {
|
|
33
|
+
const parsed = readNextMessage(buffer);
|
|
34
|
+
if (!parsed) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
buffer = parsed.remaining;
|
|
38
|
+
setResponseFormat(parsed.format);
|
|
39
|
+
handleMessage(parsed.body).catch((error) => {
|
|
40
|
+
writeLog(`unhandled message error: ${error.stack || error}`);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readNextMessage(source) {
|
|
46
|
+
const text = source.toString('utf8');
|
|
47
|
+
if (/^Content-Length:/i.test(text)) {
|
|
48
|
+
return readContentLengthMessage(source);
|
|
49
|
+
}
|
|
50
|
+
return readLineJsonMessage(source);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readContentLengthMessage(source) {
|
|
54
|
+
const delimiter = findHeaderDelimiter(source);
|
|
55
|
+
const headerEnd = delimiter.index;
|
|
56
|
+
if (headerEnd < 0) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const header = source.subarray(0, headerEnd).toString('utf8');
|
|
60
|
+
const match = /^Content-Length:\s*(\d+)$/im.exec(header);
|
|
61
|
+
if (!match) {
|
|
62
|
+
return {
|
|
63
|
+
body: source.subarray(headerEnd + delimiter.length).toString('utf8'),
|
|
64
|
+
format: 'frame',
|
|
65
|
+
remaining: Buffer.alloc(0),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const contentLength = Number(match[1]);
|
|
69
|
+
const messageStart = headerEnd + delimiter.length;
|
|
70
|
+
const messageEnd = messageStart + contentLength;
|
|
71
|
+
if (source.length < messageEnd) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
body: source.subarray(messageStart, messageEnd).toString('utf8'),
|
|
76
|
+
format: 'frame',
|
|
77
|
+
remaining: source.subarray(messageEnd),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function readLineJsonMessage(source) {
|
|
82
|
+
const lfIndex = source.indexOf('\n');
|
|
83
|
+
if (lfIndex < 0) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
const lineEnd = lfIndex > 0 && source[lfIndex - 1] === 13 ? lfIndex - 1 : lfIndex;
|
|
87
|
+
const body = source.subarray(0, lineEnd).toString('utf8');
|
|
88
|
+
return {
|
|
89
|
+
body,
|
|
90
|
+
format: 'line',
|
|
91
|
+
remaining: source.subarray(lfIndex + 1),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function setResponseFormat(format) {
|
|
96
|
+
if (!responseFormat) {
|
|
97
|
+
responseFormat = format;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function findHeaderDelimiter(source) {
|
|
58
102
|
const crlfIndex = source.indexOf('\r\n\r\n');
|
|
59
103
|
const lfIndex = source.indexOf('\n\n');
|
|
60
104
|
if (crlfIndex < 0) {
|
|
@@ -264,14 +308,15 @@ function fullToolDefinitions() {
|
|
|
264
308
|
bridgeTool('events', 'Read generic in-app event records.'),
|
|
265
309
|
bridgeTool('uia_tree', 'Read UIAutomator XML for the current device window.'),
|
|
266
310
|
bridgeTool('screenshot', 'Capture an ADB screenshot.'),
|
|
267
|
-
bridgeTool('install_apk', 'Install an APK through ADB while assisting device-side package-installer confirmation screens with UIAutomator.', {
|
|
268
|
-
apkPath: { type: 'string', description: 'Absolute or workspace-relative APK path.' },
|
|
269
|
-
allowDowngrade: { type: 'boolean', description: 'Pass -d to adb install.' },
|
|
270
|
-
streaming: { type: 'boolean', description: 'Use streaming install instead of the default --no-streaming mode.' },
|
|
271
|
-
installTimeoutMs: { type: 'number', description: 'Maximum time for adb install. Defaults to 180000 ms.' },
|
|
272
|
-
installerTimeoutMs: { type: 'number', description: 'Maximum time to keep assisting installer screens after adb install exits. Defaults to 90000 ms.' },
|
|
273
|
-
intervalMs: { type: 'number', description: 'Installer polling interval. Defaults to 700 ms.' },
|
|
274
|
-
}, ['apkPath']),
|
|
311
|
+
bridgeTool('install_apk', 'Install an APK through ADB while assisting device-side package-installer confirmation screens with UIAutomator.', {
|
|
312
|
+
apkPath: { type: 'string', description: 'Absolute or workspace-relative APK path.' },
|
|
313
|
+
allowDowngrade: { type: 'boolean', description: 'Pass -d to adb install.' },
|
|
314
|
+
streaming: { type: 'boolean', description: 'Use streaming install instead of the default --no-streaming mode.' },
|
|
315
|
+
installTimeoutMs: { type: 'number', description: 'Maximum time for adb install. Defaults to 180000 ms.' },
|
|
316
|
+
installerTimeoutMs: { type: 'number', description: 'Maximum time to keep assisting installer screens after adb install exits. Defaults to 90000 ms.' },
|
|
317
|
+
intervalMs: { type: 'number', description: 'Installer polling interval. Defaults to 700 ms.' },
|
|
318
|
+
}, ['apkPath']),
|
|
319
|
+
bridgeTool('clear_app_data', 'Clear target app local data through the bridge runtime. Requires packageName so it cannot target the sample package by default.', {}, ['packageName']),
|
|
275
320
|
bridgeTool('launch_app', 'Launch the target package LAUNCHER Activity. If multiple launcher Activities exist, returns launcher_ambiguous with candidates unless activity or component is explicit.', launchProperties()),
|
|
276
321
|
bridgeTool('launch_activity', 'Launch an explicit Android Activity component, optionally with action/data/category/string extras.', launchProperties()),
|
|
277
322
|
bridgeTool('launch_native_test', 'Launch the debug native Android bridge test Activity.'),
|
|
@@ -405,6 +450,7 @@ const commandDefinitions = [
|
|
|
405
450
|
{ command: 'events', domain: 'core', summary: 'Read in-app event records from the bridge.', targetApp: true, options: ['packageName', 'port', 'serial', 'sinceId', 'sinceMs', 'limit'] },
|
|
406
451
|
{ command: 'logcat', domain: 'diagnostics', summary: 'Read Android logcat with optional app pid, tag, level, and grep filters.', options: ['serial', 'packageName', 'pid', 'appPid', 'tag', 'level', 'grep', 'lines', 'since', 'follow', 'durationSec', 'clear'] },
|
|
407
452
|
{ command: 'install-apk', domain: 'app', summary: 'Install an APK and assist device-side installer confirmation screens.', options: ['serial', 'packageName', 'apkPath', 'allowDowngrade', 'streaming', 'installTimeoutMs', 'installerTimeoutMs', 'intervalMs'] },
|
|
453
|
+
{ command: 'clear-app-data', domain: 'app', summary: 'Clear target app local data through the bridge runtime.', targetApp: true, options: ['serial', 'packageName'] },
|
|
408
454
|
{ command: 'launch-app', domain: 'app', summary: 'Launch the target package LAUNCHER Activity and report launcher candidates.', targetApp: true, options: ['serial', 'packageName', 'activity', 'component', 'action', 'category', 'data', 'extra'] },
|
|
409
455
|
{ command: 'launch-activity', domain: 'app', summary: 'Launch an explicit Android Activity component with optional string extras.', targetApp: true, options: ['serial', 'packageName', 'activity', 'component', 'action', 'category', 'data', 'extra'] },
|
|
410
456
|
{ command: 'launch-native-test', domain: 'app', summary: 'Launch the debug native bridge test Activity.', targetApp: true, options: ['serial', 'packageName'] },
|
|
@@ -481,6 +527,7 @@ async function callTool(name, args) {
|
|
|
481
527
|
input_flutter_text: 'input-flutter-text',
|
|
482
528
|
uia_tree: 'uia-tree',
|
|
483
529
|
install_apk: 'install-apk',
|
|
530
|
+
clear_app_data: 'clear-app-data',
|
|
484
531
|
launch_app: 'launch-app',
|
|
485
532
|
launch_activity: 'launch-activity',
|
|
486
533
|
launch_native_test: 'launch-native-test',
|
|
@@ -526,7 +573,7 @@ function capabilityPayload(args = {}) {
|
|
|
526
573
|
return {
|
|
527
574
|
ok: true,
|
|
528
575
|
surface: mcpSurface === 'full' || mcpSurface === 'legacy' ? 'full' : 'compact',
|
|
529
|
-
usage: 'Use run with one of these command names. Prefer packageName for app-specific commands; install-apk, launch-app, UI, WebView, logcat, network, and permission workflows are supported.',
|
|
576
|
+
usage: 'Use run with one of these command names. Prefer packageName for app-specific commands; install-apk, clear-app-data, launch-app, UI, WebView, logcat, network, and permission workflows are supported.',
|
|
530
577
|
domains,
|
|
531
578
|
};
|
|
532
579
|
}
|
|
@@ -564,6 +611,9 @@ function normalizeCommandName(value) {
|
|
|
564
611
|
}
|
|
565
612
|
|
|
566
613
|
function runBridgeChecked(command, args = {}) {
|
|
614
|
+
if (command === 'clear-app-data' && !args.packageName) {
|
|
615
|
+
return toolText('clear-app-data: packageName is required in MCP mode so the command cannot clear a default package.', true);
|
|
616
|
+
}
|
|
567
617
|
const definition = commandByName.get(command);
|
|
568
618
|
if (definition?.targetApp && !args.packageName && !args.port) {
|
|
569
619
|
return toolText(`${command}: packageName or explicit port is required in MCP mode so the command cannot fall back to a default package.`, true);
|
|
@@ -1014,11 +1064,15 @@ function sendError(id, code, message) {
|
|
|
1014
1064
|
});
|
|
1015
1065
|
}
|
|
1016
1066
|
|
|
1017
|
-
function send(message) {
|
|
1018
|
-
const body = Buffer.from(JSON.stringify(message), 'utf8');
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1067
|
+
function send(message) {
|
|
1068
|
+
const body = Buffer.from(JSON.stringify(message), 'utf8');
|
|
1069
|
+
if (responseFormat === 'line') {
|
|
1070
|
+
process.stdout.write(`${body.toString('utf8')}\n`);
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
process.stdout.write(`Content-Length: ${body.length}\r\n\r\n`);
|
|
1074
|
+
process.stdout.write(body);
|
|
1075
|
+
}
|
|
1022
1076
|
|
|
1023
1077
|
function writeLog(text) {
|
|
1024
1078
|
process.stderr.write(`${text}\n`);
|
|
@@ -1030,6 +1084,7 @@ if (require.main === module) {
|
|
|
1030
1084
|
|
|
1031
1085
|
module.exports = {
|
|
1032
1086
|
buildBridgeCliArgs,
|
|
1087
|
+
readNextMessage,
|
|
1033
1088
|
runBatch,
|
|
1034
1089
|
startServer,
|
|
1035
1090
|
};
|