@mobileaidev/ai-app-bridge 0.2.0 → 0.2.2
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 +29 -9
- package/bin/ai-app-bridge.js +192 -47
- package/bin/mcp-server.js +320 -80
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,15 +4,29 @@
|
|
|
4
4
|
npm install -g @mobileaidev/ai-app-bridge
|
|
5
5
|
|
|
6
6
|
ai-app-bridge status --package-name io.github.mobileaidev.aiappbridge.sample
|
|
7
|
-
ai-app-bridge tree --package-name io.github.mobileaidev.aiappbridge.sample
|
|
8
|
-
ai-app-bridge install-apk --package-name io.github.mobileaidev.aiappbridge.sample --apk-path app-debug.apk
|
|
9
|
-
ai-app-bridge
|
|
7
|
+
ai-app-bridge tree --package-name io.github.mobileaidev.aiappbridge.sample
|
|
8
|
+
ai-app-bridge install-apk --package-name io.github.mobileaidev.aiappbridge.sample --apk-path app-debug.apk
|
|
9
|
+
ai-app-bridge launch-app --package-name io.github.mobileaidev.aiappbridge.sample
|
|
10
|
+
ai-app-bridge launch-activity --package-name io.github.mobileaidev.aiappbridge.sample --activity .MainActivity --extra route=/home
|
|
11
|
+
ai-app-bridge screenshot --package-name io.github.mobileaidev.aiappbridge.sample
|
|
10
12
|
ai-app-bridge input-text --package-name io.github.mobileaidev.aiappbridge.sample --text "中文输入" --hide-keyboard
|
|
11
13
|
ai-app-bridge network --package-name io.github.mobileaidev.aiappbridge.sample --compact --url-filter /api/
|
|
12
|
-
ai-app-bridge webview-network --package-name io.github.mobileaidev.aiappbridge.sample --duration-ms 3000
|
|
13
|
-
ai-app-bridge-mcp
|
|
14
|
-
```
|
|
15
|
-
|
|
14
|
+
ai-app-bridge webview-network --package-name io.github.mobileaidev.aiappbridge.sample --duration-ms 3000
|
|
15
|
+
ai-app-bridge-mcp
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
MCP defaults to a compact tool surface to avoid loading every command schema
|
|
19
|
+
into the model context:
|
|
20
|
+
|
|
21
|
+
- `capabilities` lists the bridge domains and command names.
|
|
22
|
+
- `run` executes a selected command with command-specific arguments.
|
|
23
|
+
|
|
24
|
+
This keeps install, launch, UI, Flutter, WebView, logcat, network, and
|
|
25
|
+
permission capabilities discoverable without exposing dozens of full schemas at
|
|
26
|
+
session start. Set `AI_APP_BRIDGE_MCP_SURFACE=full` before launching
|
|
27
|
+
`ai-app-bridge-mcp` only when a client needs the legacy one-tool-per-command
|
|
28
|
+
surface.
|
|
29
|
+
|
|
16
30
|
WebView network and console capture use Android WebView DevTools/CDP when the
|
|
17
31
|
target app is debuggable and WebView debugging is enabled.
|
|
18
32
|
|
|
@@ -25,5 +39,11 @@ When `screenshot` or `smoke` runs without `--out-file`, the CLI writes a unique
|
|
|
25
39
|
PNG under `build/ai_app_bridge_artifacts` instead of reusing a stable filename
|
|
26
40
|
or creating files in the project root.
|
|
27
41
|
It keeps the newest 20 generated screenshots for each command prefix. Use
|
|
28
|
-
`--artifact-dir` to choose that directory, or `--out-file` when a fixed path is
|
|
29
|
-
intentional.
|
|
42
|
+
`--artifact-dir` to choose that directory, or `--out-file` when a fixed path is
|
|
43
|
+
intentional.
|
|
44
|
+
|
|
45
|
+
`launch-app` queries Android LAUNCHER activities before starting the app. If a
|
|
46
|
+
debug dependency exposes multiple launcher entries, it returns
|
|
47
|
+
`launcher_ambiguous` with the candidates instead of guessing. Use
|
|
48
|
+
`launch-activity` or `launch-app --activity/--component` to choose the intended
|
|
49
|
+
entry point explicitly.
|
package/bin/ai-app-bridge.js
CHANGED
|
@@ -68,10 +68,12 @@ Device/action commands:
|
|
|
68
68
|
keyboard-state Read Android soft keyboard visibility from dumpsys.
|
|
69
69
|
hide-keyboard Hide the Android soft keyboard when it is visible.
|
|
70
70
|
|
|
71
|
-
App/permission commands:
|
|
72
|
-
install-apk Install an APK and assist device-side installer screens.
|
|
73
|
-
launch-
|
|
74
|
-
launch-
|
|
71
|
+
App/permission commands:
|
|
72
|
+
install-apk Install an APK and assist device-side installer screens.
|
|
73
|
+
launch-app Launch the target package LAUNCHER Activity.
|
|
74
|
+
launch-activity Launch an explicit Android Activity component.
|
|
75
|
+
launch-native-test Launch the debug native Android bridge test Activity.
|
|
76
|
+
launch-flutter Launch the Flutter Activity.
|
|
75
77
|
permission-state Read Android runtime permission state.
|
|
76
78
|
permission-grant Grant an Android runtime permission.
|
|
77
79
|
permission-revoke Revoke an Android runtime permission.
|
|
@@ -92,7 +94,13 @@ Options:
|
|
|
92
94
|
--adb-timeout-ms <ms> Timeout for ADB subprocesses.
|
|
93
95
|
--out-file <path> Screenshot output path.
|
|
94
96
|
--artifact-dir <path> Directory for generated screenshot/artifact defaults.
|
|
95
|
-
--apk-path <path> APK path used by install-apk.
|
|
97
|
+
--apk-path <path> APK path used by install-apk.
|
|
98
|
+
--activity <name> Activity class for launch-activity or launch-app override.
|
|
99
|
+
--component <pkg/act> Explicit Android component for launch-activity.
|
|
100
|
+
--action <name> Intent action for launch-activity.
|
|
101
|
+
--category <name> Intent category for launch-activity; may be repeated.
|
|
102
|
+
--data <uri> Intent data URI for launch-activity.
|
|
103
|
+
--extra <key=value> String intent extra for launch-activity; may be repeated.
|
|
96
104
|
--text <text> Text used by Unicode-safe bridge input commands.
|
|
97
105
|
--value <text> Text value used by h5-input or flutter-h5-input.
|
|
98
106
|
--selector <css> CSS selector used by H5 commands.
|
|
@@ -319,11 +327,15 @@ async function runCommand(command, options, ctx) {
|
|
|
319
327
|
return appopsSet(ctx, requiredString(options.op, 'op'), requiredString(options.mode, 'mode'));
|
|
320
328
|
case 'tap-uia-text':
|
|
321
329
|
return tapUiaText(ctx, requiredString(options.targetText, 'targetText'), options);
|
|
322
|
-
case 'permission-dialog':
|
|
323
|
-
return permissionDialog(ctx, options);
|
|
324
|
-
case 'launch-
|
|
325
|
-
return
|
|
326
|
-
case 'launch-
|
|
330
|
+
case 'permission-dialog':
|
|
331
|
+
return permissionDialog(ctx, options);
|
|
332
|
+
case 'launch-app':
|
|
333
|
+
return launchApp(ctx, options);
|
|
334
|
+
case 'launch-activity':
|
|
335
|
+
return launchActivity(ctx, options);
|
|
336
|
+
case 'launch-native-test':
|
|
337
|
+
return launchNativeTest(ctx);
|
|
338
|
+
case 'launch-flutter':
|
|
327
339
|
return launchFlutter(ctx, options.initialRoute || '');
|
|
328
340
|
case 'smoke':
|
|
329
341
|
return smoke(ctx, options);
|
|
@@ -347,16 +359,25 @@ function parseArgs(argv) {
|
|
|
347
359
|
const rawName = arg.slice(2);
|
|
348
360
|
const name = rawName.replace(/-([a-z])/g, (_, value) => value.toUpperCase());
|
|
349
361
|
const next = argv[index + 1];
|
|
350
|
-
if (next === undefined || next.startsWith('--')) {
|
|
351
|
-
options
|
|
352
|
-
continue;
|
|
353
|
-
}
|
|
354
|
-
options
|
|
355
|
-
index += 1;
|
|
356
|
-
}
|
|
357
|
-
return { command, options };
|
|
358
|
-
}
|
|
359
|
-
|
|
362
|
+
if (next === undefined || next.startsWith('--')) {
|
|
363
|
+
appendOption(options, name, true);
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
appendOption(options, name, next);
|
|
367
|
+
index += 1;
|
|
368
|
+
}
|
|
369
|
+
return { command, options };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function appendOption(options, name, value) {
|
|
373
|
+
if (name === 'extra' || name === 'category') {
|
|
374
|
+
if (!Array.isArray(options[name])) options[name] = [];
|
|
375
|
+
options[name].push(value);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
options[name] = value;
|
|
379
|
+
}
|
|
380
|
+
|
|
360
381
|
async function adb(ctx, args, { binary = false } = {}) {
|
|
361
382
|
const allArgs = adbArgs(ctx, args);
|
|
362
383
|
return new Promise((resolve, reject) => {
|
|
@@ -3664,13 +3685,17 @@ async function resolveLogcatPid(ctx, options) {
|
|
|
3664
3685
|
return '';
|
|
3665
3686
|
}
|
|
3666
3687
|
|
|
3667
|
-
function filterLogcat(text, options) {
|
|
3668
|
-
const tags = splitCsv(options.tag || options.tags);
|
|
3669
|
-
const grep = options.grep ? String(options.grep) : '';
|
|
3670
|
-
const grepCaseSensitive = Boolean(options.grepCaseSensitive);
|
|
3671
|
-
const minLevel = priorityValue(options.level || options.minLevel || '');
|
|
3672
|
-
const pid = options.pid ? String(options.pid) : '';
|
|
3673
|
-
const
|
|
3688
|
+
function filterLogcat(text, options) {
|
|
3689
|
+
const tags = splitCsv(options.tag || options.tags);
|
|
3690
|
+
const grep = options.grep ? String(options.grep) : '';
|
|
3691
|
+
const grepCaseSensitive = Boolean(options.grepCaseSensitive);
|
|
3692
|
+
const minLevel = priorityValue(options.level || options.minLevel || '');
|
|
3693
|
+
const pid = options.pid ? String(options.pid) : '';
|
|
3694
|
+
const requiresAppPid = options.appPid || options.packagePid || options.pid === 'current';
|
|
3695
|
+
if (requiresAppPid && !pid) {
|
|
3696
|
+
return '';
|
|
3697
|
+
}
|
|
3698
|
+
const lines = String(text || '').split(/\r?\n/);
|
|
3674
3699
|
const filtered = [];
|
|
3675
3700
|
let previousIncluded = false;
|
|
3676
3701
|
for (const line of lines) {
|
|
@@ -3753,19 +3778,134 @@ function adbFollow(ctx, args, durationMs) {
|
|
|
3753
3778
|
});
|
|
3754
3779
|
}
|
|
3755
3780
|
|
|
3756
|
-
async function
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
}
|
|
3781
|
+
async function launchApp(ctx, options = {}) {
|
|
3782
|
+
if (options.component || options.activity) {
|
|
3783
|
+
return launchActivity(ctx, options);
|
|
3784
|
+
}
|
|
3785
|
+
|
|
3786
|
+
const candidates = await launcherActivityCandidates(ctx);
|
|
3787
|
+
if (candidates.length === 0) {
|
|
3788
|
+
return {
|
|
3789
|
+
ok: false,
|
|
3790
|
+
error: 'launcher_not_found',
|
|
3791
|
+
packageName: ctx.packageName,
|
|
3792
|
+
launcherCandidates: candidates,
|
|
3793
|
+
};
|
|
3794
|
+
}
|
|
3795
|
+
if (candidates.length > 1) {
|
|
3796
|
+
return {
|
|
3797
|
+
ok: false,
|
|
3798
|
+
error: 'launcher_ambiguous',
|
|
3799
|
+
packageName: ctx.packageName,
|
|
3800
|
+
launcherCandidates: candidates,
|
|
3801
|
+
suggestion: 'Pass --component or --activity to choose the intended launcher Activity.',
|
|
3802
|
+
};
|
|
3803
|
+
}
|
|
3804
|
+
|
|
3805
|
+
return startActivity(ctx, candidates[0], options, {
|
|
3806
|
+
packageName: ctx.packageName,
|
|
3807
|
+
launcherCandidates: candidates,
|
|
3808
|
+
});
|
|
3809
|
+
}
|
|
3810
|
+
|
|
3811
|
+
async function launchActivity(ctx, options = {}) {
|
|
3812
|
+
const component = normalizeActivityComponent(
|
|
3813
|
+
ctx.packageName,
|
|
3814
|
+
options.component || requiredString(options.activity, 'activity'),
|
|
3815
|
+
);
|
|
3816
|
+
return startActivity(ctx, component, options, { packageName: ctx.packageName });
|
|
3817
|
+
}
|
|
3818
|
+
|
|
3819
|
+
async function launchNativeTest(ctx) {
|
|
3820
|
+
const component = normalizeActivityComponent(ctx.packageName, ctx.nativeActivity);
|
|
3821
|
+
return startActivity(ctx, component, {}, { packageName: ctx.packageName });
|
|
3822
|
+
}
|
|
3823
|
+
|
|
3824
|
+
async function launchFlutter(ctx, initialRoute) {
|
|
3825
|
+
const component = normalizeActivityComponent(ctx.packageName, ctx.flutterActivity);
|
|
3826
|
+
const options = initialRoute ? { extra: [`ai_app_initial_route=${initialRoute}`] } : {};
|
|
3827
|
+
const result = await startActivity(ctx, component, options, { packageName: ctx.packageName });
|
|
3828
|
+
return { ...result, initialRoute };
|
|
3829
|
+
}
|
|
3830
|
+
|
|
3831
|
+
async function launcherActivityCandidates(ctx) {
|
|
3832
|
+
const result = await adb(ctx, [
|
|
3833
|
+
'shell',
|
|
3834
|
+
'cmd',
|
|
3835
|
+
'package',
|
|
3836
|
+
'query-activities',
|
|
3837
|
+
'--brief',
|
|
3838
|
+
'-a',
|
|
3839
|
+
'android.intent.action.MAIN',
|
|
3840
|
+
'-c',
|
|
3841
|
+
'android.intent.category.LAUNCHER',
|
|
3842
|
+
ctx.packageName,
|
|
3843
|
+
]);
|
|
3844
|
+
return parseLauncherActivityCandidates(result.stdout);
|
|
3845
|
+
}
|
|
3846
|
+
|
|
3847
|
+
function parseLauncherActivityCandidates(stdout) {
|
|
3848
|
+
const candidates = [];
|
|
3849
|
+
for (const line of String(stdout || '').split(/\r?\n/)) {
|
|
3850
|
+
const candidate = line.trim();
|
|
3851
|
+
if (/^[A-Za-z0-9_.$]+\/[A-Za-z0-9_.$]+$/.test(candidate) && !candidates.includes(candidate)) {
|
|
3852
|
+
candidates.push(candidate);
|
|
3853
|
+
}
|
|
3854
|
+
}
|
|
3855
|
+
return candidates;
|
|
3856
|
+
}
|
|
3857
|
+
|
|
3858
|
+
function normalizeActivityComponent(packageName, activityOrComponent) {
|
|
3859
|
+
const value = requiredString(activityOrComponent, 'activity');
|
|
3860
|
+
if (value.includes('/')) return value;
|
|
3861
|
+
return `${packageName}/${value}`;
|
|
3862
|
+
}
|
|
3863
|
+
|
|
3864
|
+
async function startActivity(ctx, component, options = {}, extraResult = {}) {
|
|
3865
|
+
const args = buildAmStartArgs(component, options);
|
|
3866
|
+
const result = await adb(ctx, args);
|
|
3867
|
+
return {
|
|
3868
|
+
ok: true,
|
|
3869
|
+
transport: 'adb',
|
|
3870
|
+
component,
|
|
3871
|
+
...extraResult,
|
|
3872
|
+
stdout: result.stdout.trim(),
|
|
3873
|
+
stderr: result.stderr.trim(),
|
|
3874
|
+
};
|
|
3875
|
+
}
|
|
3876
|
+
|
|
3877
|
+
function buildAmStartArgs(component, options = {}) {
|
|
3878
|
+
const args = ['shell', 'am', 'start'];
|
|
3879
|
+
if (options.action) args.push('-a', options.action);
|
|
3880
|
+
for (const category of optionList(options.category)) {
|
|
3881
|
+
args.push('-c', category);
|
|
3882
|
+
}
|
|
3883
|
+
if (options.data) args.push('-d', options.data);
|
|
3884
|
+
for (const extra of parseStartExtras(options.extra)) {
|
|
3885
|
+
args.push('-e', extra.key, extra.value);
|
|
3886
|
+
}
|
|
3887
|
+
args.push('-n', component);
|
|
3888
|
+
return args;
|
|
3889
|
+
}
|
|
3890
|
+
|
|
3891
|
+
function parseStartExtras(rawExtras) {
|
|
3892
|
+
return optionList(rawExtras).map((rawExtra) => {
|
|
3893
|
+
const value = String(rawExtra);
|
|
3894
|
+
const separator = value.indexOf('=');
|
|
3895
|
+
if (separator <= 0) {
|
|
3896
|
+
throw new Error('extra must use key=value');
|
|
3897
|
+
}
|
|
3898
|
+
return {
|
|
3899
|
+
key: value.slice(0, separator),
|
|
3900
|
+
value: value.slice(separator + 1),
|
|
3901
|
+
};
|
|
3902
|
+
});
|
|
3903
|
+
}
|
|
3904
|
+
|
|
3905
|
+
function optionList(value) {
|
|
3906
|
+
if (value === undefined || value === null || value === false || value === '') return [];
|
|
3907
|
+
return Array.isArray(value) ? value : [value];
|
|
3908
|
+
}
|
|
3769
3909
|
|
|
3770
3910
|
async function smoke(ctx, options) {
|
|
3771
3911
|
const summary = {
|
|
@@ -4131,9 +4271,10 @@ module.exports = {
|
|
|
4131
4271
|
compactUiaTree,
|
|
4132
4272
|
defaultArtifactDirectory,
|
|
4133
4273
|
defaultArtifactPath,
|
|
4134
|
-
findFlutterNode,
|
|
4135
|
-
findTappableNodeByText,
|
|
4136
|
-
|
|
4274
|
+
findFlutterNode,
|
|
4275
|
+
findTappableNodeByText,
|
|
4276
|
+
filterLogcat,
|
|
4277
|
+
firstErrorLine,
|
|
4137
4278
|
flutterNodePoint,
|
|
4138
4279
|
flutterPhysicalViewport,
|
|
4139
4280
|
helpText,
|
|
@@ -4141,10 +4282,14 @@ module.exports = {
|
|
|
4141
4282
|
isAdbInputTextSafe,
|
|
4142
4283
|
isLikelyInstallerSurface,
|
|
4143
4284
|
nodeTapState,
|
|
4144
|
-
normalizeBridgeError,
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4285
|
+
normalizeBridgeError,
|
|
4286
|
+
normalizeActivityComponent,
|
|
4287
|
+
parseWebViewDevToolsSockets,
|
|
4288
|
+
parseArgs,
|
|
4289
|
+
parseKeyboardState,
|
|
4290
|
+
parseLauncherActivityCandidates,
|
|
4291
|
+
parseStartExtras,
|
|
4292
|
+
parseUiaBounds,
|
|
4148
4293
|
parseUiaViewport,
|
|
4149
4294
|
parseComponentFromWindowLine,
|
|
4150
4295
|
parseForegroundWindow,
|
package/bin/mcp-server.js
CHANGED
|
@@ -4,11 +4,19 @@ const { spawn } = require('child_process');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
|
|
6
6
|
const packageInfo = require('../package.json');
|
|
7
|
-
const bridgeDir = __dirname;
|
|
8
|
-
const cliScript = path.join(bridgeDir, 'ai-app-bridge.js');
|
|
9
|
-
const nodeBinary = process.env.AI_APP_BRIDGE_NODE || process.execPath;
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
const bridgeDir = __dirname;
|
|
8
|
+
const cliScript = path.join(bridgeDir, 'ai-app-bridge.js');
|
|
9
|
+
const nodeBinary = process.env.AI_APP_BRIDGE_NODE || process.execPath;
|
|
10
|
+
const supportedProtocolVersions = ['2025-06-18', '2024-11-05'];
|
|
11
|
+
const defaultProtocolVersion = supportedProtocolVersions[0];
|
|
12
|
+
const mcpSurface = (process.env.AI_APP_BRIDGE_MCP_SURFACE || 'compact').toLowerCase();
|
|
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.',
|
|
15
|
+
'Default surface is compact: call capabilities to discover domains, then call run with a command and arguments.',
|
|
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
|
+
].join(' ');
|
|
18
|
+
|
|
19
|
+
let buffer = Buffer.alloc(0);
|
|
12
20
|
|
|
13
21
|
process.stdin.on('data', (chunk) => {
|
|
14
22
|
buffer = Buffer.concat([buffer, chunk]);
|
|
@@ -70,20 +78,27 @@ async function handleMessage(body) {
|
|
|
70
78
|
}
|
|
71
79
|
|
|
72
80
|
try {
|
|
73
|
-
if (message.method === 'initialize') {
|
|
74
|
-
sendResult(message.id, {
|
|
75
|
-
protocolVersion: message.params?.protocolVersion
|
|
76
|
-
capabilities: {
|
|
77
|
-
tools: {},
|
|
78
|
-
},
|
|
79
|
-
serverInfo: {
|
|
80
|
-
name: 'ai-app-bridge',
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
if (message.method === 'initialize') {
|
|
82
|
+
sendResult(message.id, {
|
|
83
|
+
protocolVersion: negotiateProtocolVersion(message.params?.protocolVersion),
|
|
84
|
+
capabilities: {
|
|
85
|
+
tools: {},
|
|
86
|
+
},
|
|
87
|
+
serverInfo: {
|
|
88
|
+
name: 'ai-app-bridge',
|
|
89
|
+
title: 'AI App Bridge',
|
|
90
|
+
version: packageInfo.version,
|
|
91
|
+
},
|
|
92
|
+
instructions: serverInstructions,
|
|
93
|
+
});
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (message.method === 'ping') {
|
|
98
|
+
sendResult(message.id, {});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
87
102
|
if (message.method === 'tools/list') {
|
|
88
103
|
sendResult(message.id, { tools: toolDefinitions() });
|
|
89
104
|
return;
|
|
@@ -101,10 +116,46 @@ async function handleMessage(body) {
|
|
|
101
116
|
} catch (error) {
|
|
102
117
|
sendError(message.id, -32000, error.message || String(error));
|
|
103
118
|
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function
|
|
107
|
-
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function negotiateProtocolVersion(requestedVersion) {
|
|
122
|
+
if (supportedProtocolVersions.includes(requestedVersion)) {
|
|
123
|
+
return requestedVersion;
|
|
124
|
+
}
|
|
125
|
+
return defaultProtocolVersion;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function toolDefinitions() {
|
|
129
|
+
if (mcpSurface === 'full' || mcpSurface === 'legacy') {
|
|
130
|
+
return fullToolDefinitions();
|
|
131
|
+
}
|
|
132
|
+
return compactToolDefinitions();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function compactToolDefinitions() {
|
|
136
|
+
return [
|
|
137
|
+
bridgeTool('capabilities', 'List AI App Bridge capability domains and commands. Call this first when planning Android app automation; then use run to execute the selected command.', {
|
|
138
|
+
domain: { type: 'string', description: 'Optional domain filter such as core, app, action, flutter, webview, or diagnostics.' },
|
|
139
|
+
command: { type: 'string', description: 'Optional command name for detailed arguments, such as install-apk, launch-app, tree, input-text, or webview-network.' },
|
|
140
|
+
includeOptions: { type: 'boolean', description: 'Include per-command argument names. Defaults to false to keep output compact.' },
|
|
141
|
+
}),
|
|
142
|
+
bridgeTool('run', 'Run an AI App Bridge command. Use capabilities first to choose the command. Always pass packageName for app-specific commands.', {
|
|
143
|
+
command: { type: 'string', description: 'Command name from capabilities, using CLI form such as status, install-apk, launch-app, input-text, tree, webview-network, or logcat.' },
|
|
144
|
+
packageName: { type: 'string', description: 'Target Android package for app-specific commands. Strongly recommended.' },
|
|
145
|
+
serial: { type: 'string', description: 'ADB serial when multiple devices are connected.' },
|
|
146
|
+
port: { type: 'number', description: 'Explicit bridge port when packageName discovery is not available.' },
|
|
147
|
+
adb: { type: 'string', description: 'ADB executable path or command.' },
|
|
148
|
+
arguments: {
|
|
149
|
+
type: 'object',
|
|
150
|
+
description: 'Command-specific arguments from capabilities. Example: {"apkPath":"app-debug.apk","allowDowngrade":true}.',
|
|
151
|
+
additionalProperties: true,
|
|
152
|
+
},
|
|
153
|
+
}, ['command']),
|
|
154
|
+
];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function fullToolDefinitions() {
|
|
158
|
+
return [
|
|
108
159
|
bridgeTool('status', 'Read compact bridge status, app info, capture counts, and Flutter layout summary. If default port 18080 times out, agents should retry with the target Android packageName so the CLI can discover the app bridge port.', {
|
|
109
160
|
full: { type: 'boolean', description: 'Return the full raw status payload, including large Flutter widget dumps.' },
|
|
110
161
|
}),
|
|
@@ -219,8 +270,10 @@ function toolDefinitions() {
|
|
|
219
270
|
installerTimeoutMs: { type: 'number', description: 'Maximum time to keep assisting installer screens after adb install exits. Defaults to 90000 ms.' },
|
|
220
271
|
intervalMs: { type: 'number', description: 'Installer polling interval. Defaults to 700 ms.' },
|
|
221
272
|
}, ['apkPath']),
|
|
222
|
-
bridgeTool('
|
|
223
|
-
bridgeTool('
|
|
273
|
+
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()),
|
|
274
|
+
bridgeTool('launch_activity', 'Launch an explicit Android Activity component, optionally with action/data/category/string extras.', launchProperties()),
|
|
275
|
+
bridgeTool('launch_native_test', 'Launch the debug native Android bridge test Activity.'),
|
|
276
|
+
bridgeTool('launch_flutter', 'Launch the Flutter Activity, optionally with an initial route.'),
|
|
224
277
|
bridgeTool('tap', 'Tap device coordinates through ADB.', {
|
|
225
278
|
tapX: { type: 'number' },
|
|
226
279
|
tapY: { type: 'number' },
|
|
@@ -233,12 +286,12 @@ function toolDefinitions() {
|
|
|
233
286
|
targetText: { type: 'string' },
|
|
234
287
|
timeoutSec: { type: 'number' },
|
|
235
288
|
}, ['targetText']),
|
|
236
|
-
bridgeTool('input_text', 'Set native Android text through the in-app bridge. Use this for Chinese/Unicode;
|
|
289
|
+
bridgeTool('input_text', 'Set native Android text through the in-app bridge. Use this for Chinese/Unicode; always pass packageName so the tool targets the intended app.', {
|
|
237
290
|
text: { type: 'string', description: 'Text to set in the focused or coordinate-matched native EditText.' },
|
|
238
291
|
tapX: { type: 'number', description: 'Optional X coordinate used to choose a native EditText target.' },
|
|
239
292
|
tapY: { type: 'number', description: 'Optional Y coordinate used to choose a native EditText target.' },
|
|
240
293
|
hideKeyboard: { type: 'boolean', description: 'Hide the soft keyboard after setting text.' },
|
|
241
|
-
}, ['text']),
|
|
294
|
+
}, ['text', 'packageName']),
|
|
242
295
|
bridgeTool('keyboard_state', 'Read Android soft-keyboard visibility from dumpsys input_method.'),
|
|
243
296
|
bridgeTool('hide_keyboard', 'Hide the Android soft keyboard when it is visible.', {
|
|
244
297
|
force: { type: 'boolean', description: 'Send keyboard-dismiss keys even when the visibility probe says the keyboard is hidden.' },
|
|
@@ -287,14 +340,29 @@ function toolDefinitions() {
|
|
|
287
340
|
];
|
|
288
341
|
}
|
|
289
342
|
|
|
290
|
-
function bridgeTool(name, description, properties = {}, required = []) {
|
|
291
|
-
return {
|
|
292
|
-
name,
|
|
293
|
-
description,
|
|
294
|
-
inputSchema: baseSchema(properties, required),
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
|
|
343
|
+
function bridgeTool(name, description, properties = {}, required = []) {
|
|
344
|
+
return {
|
|
345
|
+
name,
|
|
346
|
+
description,
|
|
347
|
+
inputSchema: baseSchema(properties, required),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function launchProperties() {
|
|
352
|
+
return {
|
|
353
|
+
activity: { type: 'string', description: 'Activity class, such as .MainActivity or com.example.MainActivity.' },
|
|
354
|
+
component: { type: 'string', description: 'Explicit Android component, such as com.example/.MainActivity.' },
|
|
355
|
+
action: { type: 'string', description: 'Intent action for explicit Activity launch.' },
|
|
356
|
+
category: { type: 'string', description: 'Intent category.' },
|
|
357
|
+
data: { type: 'string', description: 'Intent data URI.' },
|
|
358
|
+
extra: {
|
|
359
|
+
type: 'object',
|
|
360
|
+
additionalProperties: { type: 'string' },
|
|
361
|
+
description: 'String intent extras as an object of key/value pairs.',
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
298
366
|
function baseSchema(extraProperties = {}, extraRequired = []) {
|
|
299
367
|
return {
|
|
300
368
|
type: 'object',
|
|
@@ -316,19 +384,82 @@ function baseSchema(extraProperties = {}, extraRequired = []) {
|
|
|
316
384
|
};
|
|
317
385
|
}
|
|
318
386
|
|
|
319
|
-
function h5TargetSchema() {
|
|
320
|
-
return {
|
|
321
|
-
selector: { type: 'string', description: 'CSS selector for the target DOM element.' },
|
|
387
|
+
function h5TargetSchema() {
|
|
388
|
+
return {
|
|
389
|
+
selector: { type: 'string', description: 'CSS selector for the target DOM element.' },
|
|
322
390
|
targetText: { type: 'string', description: 'Text, value, aria-label, placeholder, id, name, or role to match.' },
|
|
323
391
|
exact: { type: 'boolean', description: 'Require exact text match instead of substring match.' },
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}
|
|
331
|
-
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const commandDefinitions = [
|
|
396
|
+
{ command: 'status', domain: 'core', summary: 'Read bridge status, app/device metadata, capture counts, and Flutter summary.', targetApp: true, options: ['packageName', 'port', 'serial', 'full'] },
|
|
397
|
+
{ command: 'tree', domain: 'core', summary: 'Read Android View tree from the in-app bridge.', targetApp: true, options: ['packageName', 'port', 'serial', 'compact', 'textFilter', 'resourceIdFilter', 'classFilter', 'visibleOnly', 'maxNodes', 'maxDepth'] },
|
|
398
|
+
{ command: 'uia-tree', domain: 'core', summary: 'Read UIAutomator XML for the current foreground window.', options: ['serial', 'compact', 'textFilter', 'resourceIdFilter', 'classFilter', 'visibleOnly', 'maxNodes'] },
|
|
399
|
+
{ command: 'screenshot', domain: 'core', summary: 'Capture a screenshot, with foreground package verification when packageName is supplied.', options: ['serial', 'packageName', 'outFile', 'artifactDir'] },
|
|
400
|
+
{ command: 'logs', domain: 'core', summary: 'Read in-app log records from the bridge.', targetApp: true, options: ['packageName', 'port', 'serial', 'sinceId', 'sinceMs', 'limit'] },
|
|
401
|
+
{ command: 'network', domain: 'core', summary: 'Read in-app network records from the bridge.', targetApp: true, options: ['packageName', 'port', 'serial', 'compact', 'urlFilter', 'method', 'statusCode', 'noBodies', 'bodyMaxBytes', 'sinceId', 'sinceMs', 'limit'] },
|
|
402
|
+
{ command: 'state', domain: 'core', summary: 'Read in-app state records from the bridge.', targetApp: true, options: ['packageName', 'port', 'serial', 'sinceId', 'sinceMs', 'limit'] },
|
|
403
|
+
{ command: 'events', domain: 'core', summary: 'Read in-app event records from the bridge.', targetApp: true, options: ['packageName', 'port', 'serial', 'sinceId', 'sinceMs', 'limit'] },
|
|
404
|
+
{ 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'] },
|
|
405
|
+
{ 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'] },
|
|
406
|
+
{ 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'] },
|
|
407
|
+
{ 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'] },
|
|
408
|
+
{ command: 'launch-native-test', domain: 'app', summary: 'Launch the debug native bridge test Activity.', targetApp: true, options: ['serial', 'packageName'] },
|
|
409
|
+
{ command: 'launch-flutter', domain: 'app', summary: 'Launch the Flutter Activity, optionally with an initial route.', targetApp: true, options: ['serial', 'packageName', 'initialRoute'] },
|
|
410
|
+
{ command: 'permission-state', domain: 'app', summary: 'Read Android runtime permission state.', targetApp: true, options: ['serial', 'packageName', 'permission'] },
|
|
411
|
+
{ command: 'permission-grant', domain: 'app', summary: 'Grant an Android runtime permission.', targetApp: true, options: ['serial', 'packageName', 'permission'] },
|
|
412
|
+
{ command: 'permission-revoke', domain: 'app', summary: 'Revoke an Android runtime permission.', targetApp: true, options: ['serial', 'packageName', 'permission'] },
|
|
413
|
+
{ command: 'permission-dialog', domain: 'app', summary: 'Tap a visible Android permission dialog allow button.', options: ['serial', 'targetText', 'buttonText', 'resourceId', 'attempts', 'intervalMs', 'exact'] },
|
|
414
|
+
{ command: 'appops-set', domain: 'app', summary: 'Set an Android app-op mode.', targetApp: true, options: ['serial', 'packageName', 'op', 'mode'] },
|
|
415
|
+
{ command: 'tap', domain: 'action', summary: 'Tap device coordinates through ADB.', options: ['serial', 'tapX', 'tapY'] },
|
|
416
|
+
{ command: 'tap-text', domain: 'action', summary: 'Tap a visible Android View node by text/contentDescription through the bridge tree.', targetApp: true, options: ['serial', 'packageName', 'targetText', 'noAutoHideKeyboard'] },
|
|
417
|
+
{ command: 'tap-uia-text', domain: 'action', summary: 'Tap a UIAutomator node by text without relying on the in-app tree.', options: ['serial', 'targetText', 'exact'] },
|
|
418
|
+
{ command: 'wait-text', domain: 'action', summary: 'Wait until text appears in bridge status/tree or UIAutomator output.', targetApp: true, options: ['serial', 'packageName', 'targetText', 'timeoutSec', 'requireText', 'absentText', 'requireActivity'] },
|
|
419
|
+
{ command: 'input-text', domain: 'action', summary: 'Set native Android text through the in-app bridge; use this for Chinese/Unicode.', targetApp: true, options: ['serial', 'packageName', 'text', 'tapX', 'tapY', 'hideKeyboard'] },
|
|
420
|
+
{ command: 'keyboard-state', domain: 'action', summary: 'Read Android soft keyboard visibility.', options: ['serial'] },
|
|
421
|
+
{ command: 'hide-keyboard', domain: 'action', summary: 'Hide the Android soft keyboard.', options: ['serial', 'force', 'intervalMs'] },
|
|
422
|
+
{ command: 'swipe', domain: 'action', summary: 'Swipe device coordinates through ADB.', options: ['serial', 'startX', 'startY', 'endX', 'endY', 'durationMs'] },
|
|
423
|
+
{ command: 'keyevent', domain: 'action', summary: 'Send an Android keyevent through ADB.', options: ['serial', 'keyCode'] },
|
|
424
|
+
{ command: 'flutter-tree', domain: 'flutter', summary: 'Read the latest Flutter layout snapshot.', targetApp: true, options: ['serial', 'packageName', 'port'] },
|
|
425
|
+
{ command: 'flutter-nodes', domain: 'flutter', summary: 'Read Flutter operable nodes.', targetApp: true, options: ['serial', 'packageName', 'port'] },
|
|
426
|
+
{ command: 'flutter-action', domain: 'flutter', summary: 'Dispatch a raw Flutter action payload.', targetApp: true, options: ['serial', 'packageName', 'payload'] },
|
|
427
|
+
{ command: 'tap-flutter-text', domain: 'flutter', summary: 'Tap a Flutter node by visible text.', targetApp: true, options: ['serial', 'packageName', 'targetText'] },
|
|
428
|
+
{ command: 'input-flutter-text', domain: 'flutter', summary: 'Set Flutter TextField text through the Flutter action bridge.', targetApp: true, options: ['serial', 'packageName', 'text', 'tapX', 'tapY', 'hideKeyboard'] },
|
|
429
|
+
{ command: 'scroll-flutter', domain: 'flutter', summary: 'Scroll Flutter content by delta or until text is visible.', targetApp: true, options: ['serial', 'packageName', 'targetText', 'delta', 'maxSwipes'] },
|
|
430
|
+
{ command: 'h5-dom', domain: 'webview', summary: 'Read native Android WebView DOM.', targetApp: true, options: ['serial', 'packageName', 'port'] },
|
|
431
|
+
{ command: 'h5-eval', domain: 'webview', summary: 'Execute JavaScript in the current native Android WebView.', targetApp: true, options: ['serial', 'packageName', 'script'] },
|
|
432
|
+
{ command: 'h5-click', domain: 'webview', summary: 'Click a native WebView element by selector or text.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'exact'] },
|
|
433
|
+
{ command: 'h5-input', domain: 'webview', summary: 'Set text in a native WebView input.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'value', 'exact'] },
|
|
434
|
+
{ command: 'h5-wait', domain: 'webview', summary: 'Wait for native WebView text or selector.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'timeoutSec', 'intervalMs'] },
|
|
435
|
+
{ command: 'h5-scroll', domain: 'webview', summary: 'Scroll native WebView content or a DOM element into view.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'deltaX', 'deltaY'] },
|
|
436
|
+
{ command: 'flutter-h5-dom', domain: 'webview', summary: 'Read DOM through a Flutter H5 adapter.', targetApp: true, options: ['serial', 'packageName', 'port'] },
|
|
437
|
+
{ command: 'flutter-h5-eval', domain: 'webview', summary: 'Execute JavaScript through a Flutter H5 adapter.', targetApp: true, options: ['serial', 'packageName', 'script'] },
|
|
438
|
+
{ command: 'flutter-h5-click', domain: 'webview', summary: 'Click a Flutter H5 DOM element.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'exact'] },
|
|
439
|
+
{ command: 'flutter-h5-input', domain: 'webview', summary: 'Set text in a Flutter H5 input.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'value', 'exact'] },
|
|
440
|
+
{ command: 'flutter-h5-wait', domain: 'webview', summary: 'Wait for Flutter H5 text or selector.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'timeoutSec', 'intervalMs'] },
|
|
441
|
+
{ command: 'flutter-h5-scroll', domain: 'webview', summary: 'Scroll Flutter H5 content or a DOM element into view.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'deltaX', 'deltaY'] },
|
|
442
|
+
{ command: 'webview-pages', domain: 'webview', summary: 'List attachable Android WebView DevTools/CDP pages.', targetApp: true, options: ['serial', 'packageName', 'webviewPort', 'socketName', 'targetId', 'pageUrlFilter', 'keepForward'] },
|
|
443
|
+
{ command: 'webview-network', domain: 'webview', summary: 'Capture WebView Network events through CDP.', targetApp: true, options: ['serial', 'packageName', 'webviewPort', 'socketName', 'targetId', 'pageUrlFilter', 'urlFilter', 'durationMs', 'script', 'includeResponseBody', 'bodyMaxBytes', 'maxEvents'] },
|
|
444
|
+
{ command: 'webview-console', domain: 'webview', summary: 'Capture WebView console/log events through CDP.', targetApp: true, options: ['serial', 'packageName', 'webviewPort', 'socketName', 'targetId', 'pageUrlFilter', 'durationMs', 'script', 'maxEvents'] },
|
|
445
|
+
{ command: 'forward', domain: 'advanced', summary: 'Create the ADB port forward for the bridge.', targetApp: true, options: ['serial', 'packageName', 'port'] },
|
|
446
|
+
{ command: 'remove-forward', domain: 'advanced', summary: 'Remove the ADB port forward for the bridge.', options: ['serial', 'port'] },
|
|
447
|
+
{ command: 'smoke', domain: 'diagnostics', summary: 'Run the native sample smoke test.', options: ['serial', 'packageName', 'outFile', 'artifactDir', 'skipFlutterLaunch'] },
|
|
448
|
+
];
|
|
449
|
+
|
|
450
|
+
const commandByName = new Map(commandDefinitions.map((definition) => [definition.command, definition]));
|
|
451
|
+
|
|
452
|
+
async function callTool(name, args) {
|
|
453
|
+
if (name === 'capabilities') {
|
|
454
|
+
return toolJson(capabilityPayload(args));
|
|
455
|
+
}
|
|
456
|
+
if (name === 'run') {
|
|
457
|
+
return runGeneric(args);
|
|
458
|
+
}
|
|
459
|
+
if (name === 'run_smoke') {
|
|
460
|
+
return runSmoke(args);
|
|
461
|
+
}
|
|
462
|
+
const commandMap = {
|
|
332
463
|
flutter_tree: 'flutter-tree',
|
|
333
464
|
h5_dom: 'h5-dom',
|
|
334
465
|
h5_eval: 'h5-eval',
|
|
@@ -346,9 +477,11 @@ async function callTool(name, args) {
|
|
|
346
477
|
tap_flutter_text: 'tap-flutter-text',
|
|
347
478
|
input_flutter_text: 'input-flutter-text',
|
|
348
479
|
uia_tree: 'uia-tree',
|
|
349
|
-
install_apk: 'install-apk',
|
|
350
|
-
|
|
351
|
-
|
|
480
|
+
install_apk: 'install-apk',
|
|
481
|
+
launch_app: 'launch-app',
|
|
482
|
+
launch_activity: 'launch-activity',
|
|
483
|
+
launch_native_test: 'launch-native-test',
|
|
484
|
+
launch_flutter: 'launch-flutter',
|
|
352
485
|
tap_text: 'tap-text',
|
|
353
486
|
wait_text: 'wait-text',
|
|
354
487
|
input_text: 'input-text',
|
|
@@ -363,16 +496,86 @@ async function callTool(name, args) {
|
|
|
363
496
|
appops_set: 'appops-set',
|
|
364
497
|
tap_uia_text: 'tap-uia-text',
|
|
365
498
|
permission_dialog: 'permission-dialog',
|
|
366
|
-
};
|
|
367
|
-
const command = commandMap[name] || name;
|
|
368
|
-
return
|
|
369
|
-
}
|
|
499
|
+
};
|
|
500
|
+
const command = commandMap[name] || name;
|
|
501
|
+
return runBridgeChecked(command, args);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function capabilityPayload(args = {}) {
|
|
505
|
+
const includeOptions = Boolean(args.includeOptions);
|
|
506
|
+
const requestedCommand = args.command ? normalizeCommandName(args.command) : '';
|
|
507
|
+
if (requestedCommand) {
|
|
508
|
+
const definition = commandByName.get(requestedCommand);
|
|
509
|
+
return {
|
|
510
|
+
ok: Boolean(definition),
|
|
511
|
+
command: requestedCommand,
|
|
512
|
+
...(definition ? shapeCommandDefinition(definition, true) : { error: 'unknown_command' }),
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const requestedDomain = args.domain ? String(args.domain) : '';
|
|
517
|
+
const domains = {};
|
|
518
|
+
for (const definition of commandDefinitions) {
|
|
519
|
+
if (requestedDomain && definition.domain !== requestedDomain) continue;
|
|
520
|
+
if (!domains[definition.domain]) domains[definition.domain] = [];
|
|
521
|
+
domains[definition.domain].push(shapeCommandDefinition(definition, includeOptions));
|
|
522
|
+
}
|
|
523
|
+
return {
|
|
524
|
+
ok: true,
|
|
525
|
+
surface: mcpSurface === 'full' || mcpSurface === 'legacy' ? 'full' : 'compact',
|
|
526
|
+
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.',
|
|
527
|
+
domains,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function shapeCommandDefinition(definition, includeOptions) {
|
|
532
|
+
return {
|
|
533
|
+
command: definition.command,
|
|
534
|
+
summary: definition.summary,
|
|
535
|
+
targetApp: Boolean(definition.targetApp),
|
|
536
|
+
...(includeOptions ? { options: definition.options || [] } : {}),
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async function runGeneric(args = {}) {
|
|
541
|
+
const command = normalizeCommandName(args.command);
|
|
542
|
+
if (!commandByName.has(command)) {
|
|
543
|
+
return toolText(`unknown command: ${args.command || ''}`, true);
|
|
544
|
+
}
|
|
545
|
+
const commandArgs = {
|
|
546
|
+
...(args.arguments && typeof args.arguments === 'object' ? args.arguments : {}),
|
|
547
|
+
};
|
|
548
|
+
for (const key of ['adb', 'serial', 'port', 'packageName']) {
|
|
549
|
+
if (args[key] !== undefined && commandArgs[key] === undefined) {
|
|
550
|
+
commandArgs[key] = args[key];
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return runBridgeChecked(command, commandArgs);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function normalizeCommandName(value) {
|
|
557
|
+
return String(value || '').trim().replace(/_/g, '-');
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function runBridgeChecked(command, args = {}) {
|
|
561
|
+
const definition = commandByName.get(command);
|
|
562
|
+
if (definition?.targetApp && !args.packageName && !args.port) {
|
|
563
|
+
return toolText(`${command}: packageName or explicit port is required in MCP mode so the command cannot fall back to a default package.`, true);
|
|
564
|
+
}
|
|
565
|
+
return runBridge(command, args);
|
|
566
|
+
}
|
|
370
567
|
|
|
371
568
|
async function runBridge(command, args) {
|
|
372
569
|
const cliArgs = [cliScript, command];
|
|
373
570
|
addCommonArgs(cliArgs, args);
|
|
374
|
-
addArg(cliArgs, 'initial-route', args.initialRoute);
|
|
375
|
-
addArg(cliArgs, '
|
|
571
|
+
addArg(cliArgs, 'initial-route', args.initialRoute);
|
|
572
|
+
addArg(cliArgs, 'activity', args.activity);
|
|
573
|
+
addArg(cliArgs, 'component', args.component);
|
|
574
|
+
addArg(cliArgs, 'action', args.action);
|
|
575
|
+
addRepeatedArg(cliArgs, 'category', args.category);
|
|
576
|
+
addArg(cliArgs, 'data', args.data);
|
|
577
|
+
addExtraArgs(cliArgs, args.extra);
|
|
578
|
+
addArg(cliArgs, 'out-file', args.outFile);
|
|
376
579
|
addArg(cliArgs, 'artifact-dir', args.artifactDir || defaultArtifactDirFor(command, args));
|
|
377
580
|
addArg(cliArgs, 'apk-path', args.apkPath);
|
|
378
581
|
addArg(cliArgs, 'tap-x', args.tapX);
|
|
@@ -459,13 +662,35 @@ function addCommonArgs(cliArgs, args) {
|
|
|
459
662
|
addArg(cliArgs, 'package-name', args.packageName);
|
|
460
663
|
}
|
|
461
664
|
|
|
462
|
-
function addArg(cliArgs, name, value) {
|
|
463
|
-
if (value === undefined || value === null || value === '' || value === false) {
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
cliArgs.push(`--${name}`, String(value));
|
|
467
|
-
}
|
|
468
|
-
|
|
665
|
+
function addArg(cliArgs, name, value) {
|
|
666
|
+
if (value === undefined || value === null || value === '' || value === false) {
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
cliArgs.push(`--${name}`, String(value));
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function addRepeatedArg(cliArgs, name, value) {
|
|
673
|
+
if (Array.isArray(value)) {
|
|
674
|
+
for (const item of value) addArg(cliArgs, name, item);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
addArg(cliArgs, name, value);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function addExtraArgs(cliArgs, value) {
|
|
681
|
+
if (Array.isArray(value)) {
|
|
682
|
+
for (const item of value) addArg(cliArgs, 'extra', item);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (value && typeof value === 'object') {
|
|
686
|
+
for (const [key, extraValue] of Object.entries(value)) {
|
|
687
|
+
addArg(cliArgs, 'extra', `${key}=${extraValue}`);
|
|
688
|
+
}
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
addArg(cliArgs, 'extra', value);
|
|
692
|
+
}
|
|
693
|
+
|
|
469
694
|
function runProcess(cliArgs) {
|
|
470
695
|
return new Promise((resolve) => {
|
|
471
696
|
const child = spawn(nodeBinary, cliArgs, {
|
|
@@ -490,12 +715,23 @@ function runProcess(cliArgs) {
|
|
|
490
715
|
code === 0 ? '' : `exitCode: ${code}`,
|
|
491
716
|
retryWithPackageNameHint(cliArgs, stdout, stderr, code),
|
|
492
717
|
].filter(Boolean).join('\n\n');
|
|
493
|
-
resolve(toolText(text ||
|
|
494
|
-
});
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
function
|
|
718
|
+
resolve(toolText(text || emptyProcessText(cliArgs), code !== 0));
|
|
719
|
+
});
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function emptyProcessText(cliArgs) {
|
|
724
|
+
const command = cliArgs[1] || '';
|
|
725
|
+
if (command === 'logcat' && cliArgs.includes('--app-pid')) {
|
|
726
|
+
return 'logcat: no matching lines for current app pid';
|
|
727
|
+
}
|
|
728
|
+
if (command === 'logcat') {
|
|
729
|
+
return 'logcat: no matching lines';
|
|
730
|
+
}
|
|
731
|
+
return 'ok';
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function retryWithPackageNameHint(cliArgs, stdout, stderr, code) {
|
|
499
735
|
if (code === 0 || cliArgs.includes('--package-name') || cliArgs.includes('--port')) {
|
|
500
736
|
return '';
|
|
501
737
|
}
|
|
@@ -506,21 +742,25 @@ function retryWithPackageNameHint(cliArgs, stdout, stderr, code) {
|
|
|
506
742
|
return 'agentHint: Default bridge port 18080 accepted the connection but did not answer. If you know the target app, retry this tool with packageName so the CLI can discover that app bridge port.';
|
|
507
743
|
}
|
|
508
744
|
|
|
509
|
-
function toolText(text, isError = false) {
|
|
510
|
-
return {
|
|
511
|
-
content: [
|
|
512
|
-
{
|
|
513
|
-
type: 'text',
|
|
745
|
+
function toolText(text, isError = false) {
|
|
746
|
+
return {
|
|
747
|
+
content: [
|
|
748
|
+
{
|
|
749
|
+
type: 'text',
|
|
514
750
|
text,
|
|
515
751
|
},
|
|
516
752
|
],
|
|
517
|
-
isError,
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
function
|
|
522
|
-
|
|
523
|
-
}
|
|
753
|
+
isError,
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function toolJson(value, isError = false) {
|
|
758
|
+
return toolText(JSON.stringify(value, null, 2), isError);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function sendResult(id, result) {
|
|
762
|
+
send({ jsonrpc: '2.0', id, result });
|
|
763
|
+
}
|
|
524
764
|
|
|
525
765
|
function sendError(id, code, message) {
|
|
526
766
|
send({
|