@mobileaidev/ai-app-bridge 0.2.6 → 0.2.8

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/bin/mcp-server.js CHANGED
@@ -4,101 +4,102 @@ 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
- 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, data reset, 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);
20
- let responseFormat = null;
21
-
22
- function startServer() {
23
- process.stdin.on('data', (chunk) => {
24
- buffer = Buffer.concat([buffer, chunk]);
25
- drainMessages();
26
- });
27
-
28
- process.stdin.on('error', () => {});
29
- }
30
-
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) {
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, data reset, 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
+ 'When freezing is available, keep the target app frozen while the model thinks or plans. Thaw only immediately before reading app content or performing an app action, freeze again as soon as that evidence/action result is captured, and thaw once more before finishing the overall task so the app is not left frozen.',
18
+ ].join(' ');
19
+
20
+ let buffer = Buffer.alloc(0);
21
+ let responseFormat = null;
22
+
23
+ function startServer() {
24
+ process.stdin.on('data', (chunk) => {
25
+ buffer = Buffer.concat([buffer, chunk]);
26
+ drainMessages();
27
+ });
28
+
29
+ process.stdin.on('error', () => {});
30
+ }
31
+
32
+ function drainMessages() {
33
+ while (true) {
34
+ const parsed = readNextMessage(buffer);
35
+ if (!parsed) {
36
+ return;
37
+ }
38
+ buffer = parsed.remaining;
39
+ setResponseFormat(parsed.format);
40
+ handleMessage(parsed.body).catch((error) => {
41
+ writeLog(`unhandled message error: ${error.stack || error}`);
42
+ });
43
+ }
44
+ }
45
+
46
+ function readNextMessage(source) {
47
+ const text = source.toString('utf8');
48
+ if (/^Content-Length:/i.test(text)) {
49
+ return readContentLengthMessage(source);
50
+ }
51
+ return readLineJsonMessage(source);
52
+ }
53
+
54
+ function readContentLengthMessage(source) {
55
+ const delimiter = findHeaderDelimiter(source);
56
+ const headerEnd = delimiter.index;
57
+ if (headerEnd < 0) {
58
+ return null;
59
+ }
60
+ const header = source.subarray(0, headerEnd).toString('utf8');
61
+ const match = /^Content-Length:\s*(\d+)$/im.exec(header);
62
+ if (!match) {
63
+ return {
64
+ body: source.subarray(headerEnd + delimiter.length).toString('utf8'),
65
+ format: 'frame',
66
+ remaining: Buffer.alloc(0),
67
+ };
68
+ }
69
+ const contentLength = Number(match[1]);
70
+ const messageStart = headerEnd + delimiter.length;
71
+ const messageEnd = messageStart + contentLength;
72
+ if (source.length < messageEnd) {
73
+ return null;
74
+ }
75
+ return {
76
+ body: source.subarray(messageStart, messageEnd).toString('utf8'),
77
+ format: 'frame',
78
+ remaining: source.subarray(messageEnd),
79
+ };
80
+ }
81
+
82
+ function readLineJsonMessage(source) {
83
+ const lfIndex = source.indexOf('\n');
84
+ if (lfIndex < 0) {
85
+ return null;
86
+ }
87
+ const lineEnd = lfIndex > 0 && source[lfIndex - 1] === 13 ? lfIndex - 1 : lfIndex;
88
+ const body = source.subarray(0, lineEnd).toString('utf8');
89
+ return {
90
+ body,
91
+ format: 'line',
92
+ remaining: source.subarray(lfIndex + 1),
93
+ };
94
+ }
95
+
96
+ function setResponseFormat(format) {
97
+ if (!responseFormat) {
98
+ responseFormat = format;
99
+ }
100
+ }
101
+
102
+ function findHeaderDelimiter(source) {
102
103
  const crlfIndex = source.indexOf('\r\n\r\n');
103
104
  const lfIndex = source.indexOf('\n\n');
104
105
  if (crlfIndex < 0) {
@@ -124,27 +125,27 @@ async function handleMessage(body) {
124
125
  }
125
126
 
126
127
  try {
127
- if (message.method === 'initialize') {
128
- sendResult(message.id, {
129
- protocolVersion: negotiateProtocolVersion(message.params?.protocolVersion),
130
- capabilities: {
131
- tools: {},
132
- },
133
- serverInfo: {
134
- name: 'ai-app-bridge',
135
- title: 'AI App Bridge',
136
- version: packageInfo.version,
137
- },
138
- instructions: serverInstructions,
139
- });
140
- return;
141
- }
142
-
143
- if (message.method === 'ping') {
144
- sendResult(message.id, {});
145
- return;
146
- }
147
-
128
+ if (message.method === 'initialize') {
129
+ sendResult(message.id, {
130
+ protocolVersion: negotiateProtocolVersion(message.params?.protocolVersion),
131
+ capabilities: {
132
+ tools: {},
133
+ },
134
+ serverInfo: {
135
+ name: 'ai-app-bridge',
136
+ title: 'AI App Bridge',
137
+ version: packageInfo.version,
138
+ },
139
+ instructions: serverInstructions,
140
+ });
141
+ return;
142
+ }
143
+
144
+ if (message.method === 'ping') {
145
+ sendResult(message.id, {});
146
+ return;
147
+ }
148
+
148
149
  if (message.method === 'tools/list') {
149
150
  sendResult(message.id, { tools: toolDefinitions() });
150
151
  return;
@@ -162,62 +163,62 @@ async function handleMessage(body) {
162
163
  } catch (error) {
163
164
  sendError(message.id, -32000, error.message || String(error));
164
165
  }
165
- }
166
-
167
- function negotiateProtocolVersion(requestedVersion) {
168
- if (supportedProtocolVersions.includes(requestedVersion)) {
169
- return requestedVersion;
170
- }
171
- return defaultProtocolVersion;
172
- }
173
-
174
- function toolDefinitions() {
175
- if (mcpSurface === 'full' || mcpSurface === 'legacy') {
176
- return fullToolDefinitions();
177
- }
178
- return compactToolDefinitions();
179
- }
180
-
181
- function compactToolDefinitions() {
182
- return [
183
- 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.', {
184
- domain: { type: 'string', description: 'Optional domain filter such as core, app, action, flutter, webview, or diagnostics.' },
185
- command: { type: 'string', description: 'Optional command name for detailed arguments, such as install-apk, launch-app, tree, input-text, or webview-network.' },
186
- includeOptions: { type: 'boolean', description: 'Include per-command argument names. Defaults to false to keep output compact.' },
187
- }),
188
- bridgeTool('run', 'Run an AI App Bridge command. Use capabilities first to choose the command. Always pass packageName for app-specific commands.', {
189
- 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.' },
190
- packageName: { type: 'string', description: 'Target Android package for app-specific commands. Strongly recommended.' },
191
- serial: { type: 'string', description: 'ADB serial when multiple devices are connected.' },
192
- port: { type: 'number', description: 'Explicit bridge port when packageName discovery is not available.' },
193
- adb: { type: 'string', description: 'ADB executable path or command.' },
194
- arguments: {
195
- type: 'object',
196
- description: 'Command-specific arguments from capabilities. Example: {"apkPath":"app-debug.apk","allowDowngrade":true}.',
197
- additionalProperties: true,
198
- },
199
- }, ['command']),
200
- ];
201
- }
202
-
203
- function fullToolDefinitions() {
204
- return [
166
+ }
167
+
168
+ function negotiateProtocolVersion(requestedVersion) {
169
+ if (supportedProtocolVersions.includes(requestedVersion)) {
170
+ return requestedVersion;
171
+ }
172
+ return defaultProtocolVersion;
173
+ }
174
+
175
+ function toolDefinitions() {
176
+ if (mcpSurface === 'full' || mcpSurface === 'legacy') {
177
+ return fullToolDefinitions();
178
+ }
179
+ return compactToolDefinitions();
180
+ }
181
+
182
+ function compactToolDefinitions() {
183
+ return [
184
+ 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.', {
185
+ domain: { type: 'string', description: 'Optional domain filter such as core, app, action, flutter, webview, or diagnostics.' },
186
+ command: { type: 'string', description: 'Optional command name for detailed arguments, such as install-apk, launch-app, tree, input-text, or webview-network.' },
187
+ includeOptions: { type: 'boolean', description: 'Include per-command argument names. Defaults to false to keep output compact.' },
188
+ }),
189
+ bridgeTool('run', 'Run an AI App Bridge command. Use capabilities first to choose the command. Always pass packageName for app-specific commands.', {
190
+ command: { type: 'string', description: 'Command name from capabilities, using CLI form such as status, install-apk, launch-app, freeze-app, thaw-app, input-text, tree, webview-network, or logcat.' },
191
+ packageName: { type: 'string', description: 'Target Android package for app-specific commands. Strongly recommended.' },
192
+ serial: { type: 'string', description: 'ADB serial when multiple devices are connected.' },
193
+ port: { type: 'number', description: 'Explicit bridge port when packageName discovery is not available.' },
194
+ adb: { type: 'string', description: 'ADB executable path or command.' },
195
+ arguments: {
196
+ type: 'object',
197
+ description: 'Command-specific arguments from capabilities. Example: {"apkPath":"app-debug.apk","allowDowngrade":true}.',
198
+ additionalProperties: true,
199
+ },
200
+ }, ['command']),
201
+ ];
202
+ }
203
+
204
+ function fullToolDefinitions() {
205
+ return [
205
206
  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.', {
206
207
  full: { type: 'boolean', description: 'Return the full raw status payload, including large Flutter widget dumps.' },
207
208
  }),
208
- bridgeTool('tree', 'Read the Android View tree from the in-app bridge.'),
209
- bridgeTool('flutter_tree', 'Read the latest Flutter widget/layout snapshot.'),
210
- bridgeTool('flutter_nodes', 'Read Flutter operable nodes from the Flutter action bridge.'),
211
- bridgeTool('tap_flutter_text', 'Tap a Flutter node by visible text through the Flutter-aware bridge path.', {
212
- targetText: { type: 'string', description: 'Flutter node text to tap.' },
213
- }, ['targetText']),
214
- bridgeTool('input_flutter_text', 'Set Flutter TextField text through the Flutter action bridge. Use this for Flutter Chinese/Unicode input; do not use raw adb shell input text.', {
215
- text: { type: 'string', description: 'Text to set.' },
216
- tapX: { type: 'number', description: 'Optional physical X coordinate for the Flutter input target.' },
217
- tapY: { type: 'number', description: 'Optional physical Y coordinate for the Flutter input target.' },
218
- hideKeyboard: { type: 'boolean', description: 'Hide the soft keyboard after setting text.' },
219
- }, ['text']),
220
- bridgeTool('h5_dom', 'Read native Android WebView DOM from the current Activity.'),
209
+ bridgeTool('tree', 'Read the Android View tree from the in-app bridge.'),
210
+ bridgeTool('flutter_tree', 'Read the latest Flutter widget/layout snapshot.'),
211
+ bridgeTool('flutter_nodes', 'Read Flutter operable nodes from the Flutter action bridge.'),
212
+ bridgeTool('tap_flutter_text', 'Tap a Flutter node by visible text through the Flutter-aware bridge path.', {
213
+ targetText: { type: 'string', description: 'Flutter node text to tap.' },
214
+ }, ['targetText']),
215
+ bridgeTool('input_flutter_text', 'Set Flutter TextField text through the Flutter action bridge. Use this for Flutter Chinese/Unicode input; do not use raw adb shell input text.', {
216
+ text: { type: 'string', description: 'Text to set.' },
217
+ tapX: { type: 'number', description: 'Optional physical X coordinate for the Flutter input target.' },
218
+ tapY: { type: 'number', description: 'Optional physical Y coordinate for the Flutter input target.' },
219
+ hideKeyboard: { type: 'boolean', description: 'Hide the soft keyboard after setting text.' },
220
+ }, ['text']),
221
+ bridgeTool('h5_dom', 'Read native Android WebView DOM from the current Activity.'),
221
222
  bridgeTool('h5_eval', 'Execute debug JavaScript in the current native Android WebView.', {
222
223
  script: { type: 'string' },
223
224
  }, ['script']),
@@ -256,6 +257,12 @@ function fullToolDefinitions() {
256
257
  deltaY: { type: 'number', description: 'Window scroll delta Y when no selector/text is supplied.' },
257
258
  }),
258
259
  bridgeTool('logs', 'Read generic in-app log records.'),
260
+ bridgeTool('freeze_app', 'Stop the target app processes with SIGSTOP after app content is captured, preventing playback or animation from changing the observed evidence.', {
261
+ pid: { type: 'string', description: 'Optional explicit process id. Defaults to all processes named packageName or packageName:*.' },
262
+ }, ['packageName']),
263
+ bridgeTool('thaw_app', 'Resume the target app processes with SIGCONT before reading app content or dispatching actions, so bridge endpoints can answer.', {
264
+ pid: { type: 'string', description: 'Optional explicit process id. Defaults to all processes named packageName or packageName:*.' },
265
+ }, ['packageName']),
259
266
  bridgeTool('logcat', 'Read Android logcat through ADB with optional pid/tag/level/grep filters.', {
260
267
  pid: { type: 'string', description: 'Use "current" for the current app pid, or pass a numeric pid.' },
261
268
  appPid: { type: 'boolean', description: 'Filter by the current package pid.' },
@@ -308,19 +315,19 @@ function fullToolDefinitions() {
308
315
  bridgeTool('events', 'Read generic in-app event records.'),
309
316
  bridgeTool('uia_tree', 'Read UIAutomator XML for the current device window.'),
310
317
  bridgeTool('screenshot', 'Capture an ADB screenshot.'),
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']),
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()),
321
- bridgeTool('launch_activity', 'Launch an explicit Android Activity component, optionally with action/data/category/string extras.', launchProperties()),
322
- bridgeTool('launch_native_test', 'Launch the debug native Android bridge test Activity.'),
323
- bridgeTool('launch_flutter', 'Launch the Flutter Activity, optionally with an initial route.'),
318
+ bridgeTool('install_apk', 'Install an APK through ADB while assisting device-side package-installer confirmation screens with UIAutomator.', {
319
+ apkPath: { type: 'string', description: 'Absolute or workspace-relative APK path.' },
320
+ allowDowngrade: { type: 'boolean', description: 'Pass -d to adb install.' },
321
+ streaming: { type: 'boolean', description: 'Use streaming install instead of the default --no-streaming mode.' },
322
+ installTimeoutMs: { type: 'number', description: 'Maximum time for adb install. Defaults to 180000 ms.' },
323
+ installerTimeoutMs: { type: 'number', description: 'Maximum time to keep assisting installer screens after adb install exits. Defaults to 90000 ms.' },
324
+ intervalMs: { type: 'number', description: 'Installer polling interval. Defaults to 700 ms.' },
325
+ }, ['apkPath']),
326
+ 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']),
327
+ 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()),
328
+ bridgeTool('launch_activity', 'Launch an explicit Android Activity component, optionally with action/data/category/string extras.', launchProperties()),
329
+ bridgeTool('launch_native_test', 'Launch the debug native Android bridge test Activity.'),
330
+ bridgeTool('launch_flutter', 'Launch the Flutter Activity, optionally with an initial route.'),
324
331
  bridgeTool('tap', 'Tap device coordinates through ADB.', {
325
332
  tapX: { type: 'number' },
326
333
  tapY: { type: 'number' },
@@ -333,12 +340,12 @@ function fullToolDefinitions() {
333
340
  targetText: { type: 'string' },
334
341
  timeoutSec: { type: 'number' },
335
342
  }, ['targetText']),
336
- 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.', {
337
- text: { type: 'string', description: 'Text to set in the focused or coordinate-matched native EditText.' },
338
- tapX: { type: 'number', description: 'Optional X coordinate used to choose a native EditText target.' },
339
- tapY: { type: 'number', description: 'Optional Y coordinate used to choose a native EditText target.' },
340
- hideKeyboard: { type: 'boolean', description: 'Hide the soft keyboard after setting text.' },
341
- }, ['text', 'packageName']),
343
+ 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.', {
344
+ text: { type: 'string', description: 'Text to set in the focused or coordinate-matched native EditText.' },
345
+ tapX: { type: 'number', description: 'Optional X coordinate used to choose a native EditText target.' },
346
+ tapY: { type: 'number', description: 'Optional Y coordinate used to choose a native EditText target.' },
347
+ hideKeyboard: { type: 'boolean', description: 'Hide the soft keyboard after setting text.' },
348
+ }, ['text', 'packageName']),
342
349
  bridgeTool('keyboard_state', 'Read Android soft-keyboard visibility from dumpsys input_method.'),
343
350
  bridgeTool('hide_keyboard', 'Hide the Android soft keyboard when it is visible.', {
344
351
  force: { type: 'boolean', description: 'Send keyboard-dismiss keys even when the visibility probe says the keyboard is hidden.' },
@@ -387,29 +394,29 @@ function fullToolDefinitions() {
387
394
  ];
388
395
  }
389
396
 
390
- function bridgeTool(name, description, properties = {}, required = []) {
391
- return {
392
- name,
393
- description,
394
- inputSchema: baseSchema(properties, required),
395
- };
396
- }
397
-
398
- function launchProperties() {
399
- return {
400
- activity: { type: 'string', description: 'Activity class, such as .MainActivity or com.example.MainActivity.' },
401
- component: { type: 'string', description: 'Explicit Android component, such as com.example/.MainActivity.' },
402
- action: { type: 'string', description: 'Intent action for explicit Activity launch.' },
403
- category: { type: 'string', description: 'Intent category.' },
404
- data: { type: 'string', description: 'Intent data URI.' },
405
- extra: {
406
- type: 'object',
407
- additionalProperties: { type: 'string' },
408
- description: 'String intent extras as an object of key/value pairs.',
409
- },
410
- };
411
- }
412
-
397
+ function bridgeTool(name, description, properties = {}, required = []) {
398
+ return {
399
+ name,
400
+ description,
401
+ inputSchema: baseSchema(properties, required),
402
+ };
403
+ }
404
+
405
+ function launchProperties() {
406
+ return {
407
+ activity: { type: 'string', description: 'Activity class, such as .MainActivity or com.example.MainActivity.' },
408
+ component: { type: 'string', description: 'Explicit Android component, such as com.example/.MainActivity.' },
409
+ action: { type: 'string', description: 'Intent action for explicit Activity launch.' },
410
+ category: { type: 'string', description: 'Intent category.' },
411
+ data: { type: 'string', description: 'Intent data URI.' },
412
+ extra: {
413
+ type: 'object',
414
+ additionalProperties: { type: 'string' },
415
+ description: 'String intent extras as an object of key/value pairs.',
416
+ },
417
+ };
418
+ }
419
+
413
420
  function baseSchema(extraProperties = {}, extraRequired = []) {
414
421
  return {
415
422
  type: 'object',
@@ -431,84 +438,86 @@ function baseSchema(extraProperties = {}, extraRequired = []) {
431
438
  };
432
439
  }
433
440
 
434
- function h5TargetSchema() {
435
- return {
436
- selector: { type: 'string', description: 'CSS selector for the target DOM element.' },
441
+ function h5TargetSchema() {
442
+ return {
443
+ selector: { type: 'string', description: 'CSS selector for the target DOM element.' },
437
444
  targetText: { type: 'string', description: 'Text, value, aria-label, placeholder, id, name, or role to match.' },
438
445
  exact: { type: 'boolean', description: 'Require exact text match instead of substring match.' },
439
- };
440
- }
441
-
442
- const commandDefinitions = [
443
- { command: 'status', domain: 'core', summary: 'Read bridge status, app/device metadata, capture counts, and Flutter summary.', targetApp: true, options: ['packageName', 'port', 'serial', 'full'] },
444
- { 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'] },
445
- { command: 'uia-tree', domain: 'core', summary: 'Read UIAutomator XML for the current foreground window.', options: ['serial', 'compact', 'textFilter', 'resourceIdFilter', 'classFilter', 'visibleOnly', 'maxNodes'] },
446
- { command: 'screenshot', domain: 'core', summary: 'Capture a screenshot, with foreground package verification when packageName is supplied.', options: ['serial', 'packageName', 'outFile', 'artifactDir'] },
447
- { command: 'logs', domain: 'core', summary: 'Read in-app log records from the bridge.', targetApp: true, options: ['packageName', 'port', 'serial', 'sinceId', 'sinceMs', 'limit'] },
448
- { 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'] },
449
- { command: 'state', domain: 'core', summary: 'Read in-app state records from the bridge.', targetApp: true, options: ['packageName', 'port', 'serial', 'sinceId', 'sinceMs', 'limit'] },
450
- { command: 'events', domain: 'core', summary: 'Read in-app event records from the bridge.', targetApp: true, options: ['packageName', 'port', 'serial', 'sinceId', 'sinceMs', 'limit'] },
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'] },
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'] },
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'] },
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'] },
456
- { command: 'launch-native-test', domain: 'app', summary: 'Launch the debug native bridge test Activity.', targetApp: true, options: ['serial', 'packageName'] },
457
- { command: 'launch-flutter', domain: 'app', summary: 'Launch the Flutter Activity, optionally with an initial route.', targetApp: true, options: ['serial', 'packageName', 'initialRoute'] },
458
- { command: 'permission-state', domain: 'app', summary: 'Read Android runtime permission state.', targetApp: true, options: ['serial', 'packageName', 'permission'] },
459
- { command: 'permission-grant', domain: 'app', summary: 'Grant an Android runtime permission.', targetApp: true, options: ['serial', 'packageName', 'permission'] },
460
- { command: 'permission-revoke', domain: 'app', summary: 'Revoke an Android runtime permission.', targetApp: true, options: ['serial', 'packageName', 'permission'] },
461
- { command: 'permission-dialog', domain: 'app', summary: 'Tap a visible Android permission dialog allow button.', options: ['serial', 'targetText', 'buttonText', 'resourceId', 'attempts', 'intervalMs', 'exact'] },
462
- { command: 'appops-set', domain: 'app', summary: 'Set an Android app-op mode.', targetApp: true, options: ['serial', 'packageName', 'op', 'mode'] },
463
- { command: 'tap', domain: 'action', summary: 'Tap device coordinates through ADB.', options: ['serial', 'tapX', 'tapY'] },
464
- { 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'] },
465
- { command: 'tap-uia-text', domain: 'action', summary: 'Tap a UIAutomator node by text without relying on the in-app tree.', options: ['serial', 'targetText', 'exact'] },
466
- { 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'] },
467
- { 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'] },
468
- { command: 'keyboard-state', domain: 'action', summary: 'Read Android soft keyboard visibility.', options: ['serial'] },
469
- { command: 'hide-keyboard', domain: 'action', summary: 'Hide the Android soft keyboard.', options: ['serial', 'force', 'intervalMs'] },
470
- { command: 'swipe', domain: 'action', summary: 'Swipe device coordinates through ADB.', options: ['serial', 'startX', 'startY', 'endX', 'endY', 'durationMs'] },
471
- { command: 'keyevent', domain: 'action', summary: 'Send an Android keyevent through ADB.', options: ['serial', 'keyCode'] },
472
- { command: 'flutter-tree', domain: 'flutter', summary: 'Read the latest Flutter layout snapshot.', targetApp: true, options: ['serial', 'packageName', 'port'] },
473
- { command: 'flutter-nodes', domain: 'flutter', summary: 'Read Flutter operable nodes.', targetApp: true, options: ['serial', 'packageName', 'port'] },
474
- { command: 'flutter-action', domain: 'flutter', summary: 'Dispatch a raw Flutter action payload.', targetApp: true, options: ['serial', 'packageName', 'payload'] },
475
- { command: 'tap-flutter-text', domain: 'flutter', summary: 'Tap a Flutter node by visible text.', targetApp: true, options: ['serial', 'packageName', 'targetText'] },
476
- { 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'] },
477
- { command: 'scroll-flutter', domain: 'flutter', summary: 'Scroll Flutter content by delta or until text is visible.', targetApp: true, options: ['serial', 'packageName', 'targetText', 'delta', 'maxSwipes'] },
478
- { command: 'h5-dom', domain: 'webview', summary: 'Read native Android WebView DOM.', targetApp: true, options: ['serial', 'packageName', 'port'] },
479
- { command: 'h5-eval', domain: 'webview', summary: 'Execute JavaScript in the current native Android WebView.', targetApp: true, options: ['serial', 'packageName', 'script'] },
480
- { command: 'h5-click', domain: 'webview', summary: 'Click a native WebView element by selector or text.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'exact'] },
481
- { command: 'h5-input', domain: 'webview', summary: 'Set text in a native WebView input.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'value', 'exact'] },
482
- { command: 'h5-wait', domain: 'webview', summary: 'Wait for native WebView text or selector.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'timeoutSec', 'intervalMs'] },
483
- { 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'] },
484
- { command: 'flutter-h5-dom', domain: 'webview', summary: 'Read DOM through a Flutter H5 adapter.', targetApp: true, options: ['serial', 'packageName', 'port'] },
485
- { command: 'flutter-h5-eval', domain: 'webview', summary: 'Execute JavaScript through a Flutter H5 adapter.', targetApp: true, options: ['serial', 'packageName', 'script'] },
486
- { command: 'flutter-h5-click', domain: 'webview', summary: 'Click a Flutter H5 DOM element.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'exact'] },
487
- { command: 'flutter-h5-input', domain: 'webview', summary: 'Set text in a Flutter H5 input.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'value', 'exact'] },
488
- { command: 'flutter-h5-wait', domain: 'webview', summary: 'Wait for Flutter H5 text or selector.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'timeoutSec', 'intervalMs'] },
489
- { 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'] },
490
- { command: 'webview-pages', domain: 'webview', summary: 'List attachable Android WebView DevTools/CDP pages.', targetApp: true, options: ['serial', 'packageName', 'webviewPort', 'socketName', 'targetId', 'pageUrlFilter', 'keepForward'] },
491
- { 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'] },
492
- { 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'] },
493
- { command: 'forward', domain: 'advanced', summary: 'Create the ADB port forward for the bridge.', targetApp: true, options: ['serial', 'packageName', 'port'] },
494
- { command: 'remove-forward', domain: 'advanced', summary: 'Remove the ADB port forward for the bridge.', options: ['serial', 'port'] },
495
- { command: 'batch', domain: 'advanced', summary: 'Run multiple AI App Bridge commands serially in one MCP call.', options: ['defaults', 'steps', 'stopOnError', 'includeRaw', 'maxRawChars'] },
496
- { command: 'smoke', domain: 'diagnostics', summary: 'Run the native sample smoke test.', options: ['serial', 'packageName', 'outFile', 'artifactDir', 'skipFlutterLaunch'] },
497
- ];
498
-
499
- const commandByName = new Map(commandDefinitions.map((definition) => [definition.command, definition]));
500
-
501
- async function callTool(name, args) {
502
- if (name === 'capabilities') {
503
- return toolJson(capabilityPayload(args));
504
- }
505
- if (name === 'run') {
506
- return runGeneric(args);
507
- }
508
- if (name === 'run_smoke') {
509
- return runSmoke(args);
510
- }
511
- const commandMap = {
446
+ };
447
+ }
448
+
449
+ const commandDefinitions = [
450
+ { command: 'status', domain: 'core', summary: 'Read bridge status, app/device metadata, capture counts, and Flutter summary.', targetApp: true, options: ['packageName', 'port', 'serial', 'full'] },
451
+ { 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'] },
452
+ { command: 'uia-tree', domain: 'core', summary: 'Read UIAutomator XML for the current foreground window.', options: ['serial', 'compact', 'textFilter', 'resourceIdFilter', 'classFilter', 'visibleOnly', 'maxNodes'] },
453
+ { command: 'screenshot', domain: 'core', summary: 'Capture a screenshot, with foreground package verification when packageName is supplied.', options: ['serial', 'packageName', 'outFile', 'artifactDir'] },
454
+ { command: 'logs', domain: 'core', summary: 'Read in-app log records from the bridge.', targetApp: true, options: ['packageName', 'port', 'serial', 'sinceId', 'sinceMs', 'limit'] },
455
+ { 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'] },
456
+ { command: 'state', domain: 'core', summary: 'Read in-app state records from the bridge.', targetApp: true, options: ['packageName', 'port', 'serial', 'sinceId', 'sinceMs', 'limit'] },
457
+ { command: 'events', domain: 'core', summary: 'Read in-app event records from the bridge.', targetApp: true, options: ['packageName', 'port', 'serial', 'sinceId', 'sinceMs', 'limit'] },
458
+ { 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'] },
459
+ { 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'] },
460
+ { command: 'clear-app-data', domain: 'app', summary: 'Clear target app local data through the bridge runtime.', targetApp: true, options: ['serial', 'packageName'] },
461
+ { command: 'freeze-app', domain: 'app', summary: 'Stop target app processes with SIGSTOP after content capture to keep observed evidence stable.', targetApp: true, options: ['serial', 'packageName', 'pid'] },
462
+ { command: 'thaw-app', domain: 'app', summary: 'Resume target app processes with SIGCONT before content capture or actions so bridge data can be read.', targetApp: true, options: ['serial', 'packageName', 'pid'] },
463
+ { 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'] },
464
+ { 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'] },
465
+ { command: 'launch-native-test', domain: 'app', summary: 'Launch the debug native bridge test Activity.', targetApp: true, options: ['serial', 'packageName'] },
466
+ { command: 'launch-flutter', domain: 'app', summary: 'Launch the Flutter Activity, optionally with an initial route.', targetApp: true, options: ['serial', 'packageName', 'initialRoute'] },
467
+ { command: 'permission-state', domain: 'app', summary: 'Read Android runtime permission state.', targetApp: true, options: ['serial', 'packageName', 'permission'] },
468
+ { command: 'permission-grant', domain: 'app', summary: 'Grant an Android runtime permission.', targetApp: true, options: ['serial', 'packageName', 'permission'] },
469
+ { command: 'permission-revoke', domain: 'app', summary: 'Revoke an Android runtime permission.', targetApp: true, options: ['serial', 'packageName', 'permission'] },
470
+ { command: 'permission-dialog', domain: 'app', summary: 'Tap a visible Android permission dialog allow button.', options: ['serial', 'targetText', 'buttonText', 'resourceId', 'attempts', 'intervalMs', 'exact'] },
471
+ { command: 'appops-set', domain: 'app', summary: 'Set an Android app-op mode.', targetApp: true, options: ['serial', 'packageName', 'op', 'mode'] },
472
+ { command: 'tap', domain: 'action', summary: 'Tap device coordinates through ADB.', options: ['serial', 'tapX', 'tapY'] },
473
+ { 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'] },
474
+ { command: 'tap-uia-text', domain: 'action', summary: 'Tap a UIAutomator node by text without relying on the in-app tree.', options: ['serial', 'targetText', 'exact'] },
475
+ { 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'] },
476
+ { 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'] },
477
+ { command: 'keyboard-state', domain: 'action', summary: 'Read Android soft keyboard visibility.', options: ['serial'] },
478
+ { command: 'hide-keyboard', domain: 'action', summary: 'Hide the Android soft keyboard.', options: ['serial', 'force', 'intervalMs'] },
479
+ { command: 'swipe', domain: 'action', summary: 'Swipe device coordinates through ADB.', options: ['serial', 'startX', 'startY', 'endX', 'endY', 'durationMs'] },
480
+ { command: 'keyevent', domain: 'action', summary: 'Send an Android keyevent through ADB.', options: ['serial', 'keyCode'] },
481
+ { command: 'flutter-tree', domain: 'flutter', summary: 'Read the latest Flutter layout snapshot.', targetApp: true, options: ['serial', 'packageName', 'port'] },
482
+ { command: 'flutter-nodes', domain: 'flutter', summary: 'Read Flutter operable nodes.', targetApp: true, options: ['serial', 'packageName', 'port'] },
483
+ { command: 'flutter-action', domain: 'flutter', summary: 'Dispatch a raw Flutter action payload.', targetApp: true, options: ['serial', 'packageName', 'payload'] },
484
+ { command: 'tap-flutter-text', domain: 'flutter', summary: 'Tap a Flutter node by visible text.', targetApp: true, options: ['serial', 'packageName', 'targetText'] },
485
+ { 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'] },
486
+ { command: 'scroll-flutter', domain: 'flutter', summary: 'Scroll Flutter content by delta or until text is visible.', targetApp: true, options: ['serial', 'packageName', 'targetText', 'delta', 'maxSwipes'] },
487
+ { command: 'h5-dom', domain: 'webview', summary: 'Read native Android WebView DOM.', targetApp: true, options: ['serial', 'packageName', 'port'] },
488
+ { command: 'h5-eval', domain: 'webview', summary: 'Execute JavaScript in the current native Android WebView.', targetApp: true, options: ['serial', 'packageName', 'script'] },
489
+ { command: 'h5-click', domain: 'webview', summary: 'Click a native WebView element by selector or text.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'exact'] },
490
+ { command: 'h5-input', domain: 'webview', summary: 'Set text in a native WebView input.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'value', 'exact'] },
491
+ { command: 'h5-wait', domain: 'webview', summary: 'Wait for native WebView text or selector.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'timeoutSec', 'intervalMs'] },
492
+ { 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'] },
493
+ { command: 'flutter-h5-dom', domain: 'webview', summary: 'Read DOM through a Flutter H5 adapter.', targetApp: true, options: ['serial', 'packageName', 'port'] },
494
+ { command: 'flutter-h5-eval', domain: 'webview', summary: 'Execute JavaScript through a Flutter H5 adapter.', targetApp: true, options: ['serial', 'packageName', 'script'] },
495
+ { command: 'flutter-h5-click', domain: 'webview', summary: 'Click a Flutter H5 DOM element.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'exact'] },
496
+ { command: 'flutter-h5-input', domain: 'webview', summary: 'Set text in a Flutter H5 input.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'value', 'exact'] },
497
+ { command: 'flutter-h5-wait', domain: 'webview', summary: 'Wait for Flutter H5 text or selector.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'timeoutSec', 'intervalMs'] },
498
+ { 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'] },
499
+ { command: 'webview-pages', domain: 'webview', summary: 'List attachable Android WebView DevTools/CDP pages.', targetApp: true, options: ['serial', 'packageName', 'webviewPort', 'socketName', 'targetId', 'pageUrlFilter', 'keepForward'] },
500
+ { 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'] },
501
+ { 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'] },
502
+ { command: 'forward', domain: 'advanced', summary: 'Create the ADB port forward for the bridge.', targetApp: true, options: ['serial', 'packageName', 'port'] },
503
+ { command: 'remove-forward', domain: 'advanced', summary: 'Remove the ADB port forward for the bridge.', options: ['serial', 'port'] },
504
+ { command: 'batch', domain: 'advanced', summary: 'Run multiple AI App Bridge commands serially in one MCP call.', options: ['defaults', 'steps', 'stopOnError', 'includeRaw', 'maxRawChars'] },
505
+ { command: 'smoke', domain: 'diagnostics', summary: 'Run the native sample smoke test.', options: ['serial', 'packageName', 'outFile', 'artifactDir', 'skipFlutterLaunch'] },
506
+ ];
507
+
508
+ const commandByName = new Map(commandDefinitions.map((definition) => [definition.command, definition]));
509
+
510
+ async function callTool(name, args) {
511
+ if (name === 'capabilities') {
512
+ return toolJson(capabilityPayload(args));
513
+ }
514
+ if (name === 'run') {
515
+ return runGeneric(args);
516
+ }
517
+ if (name === 'run_smoke') {
518
+ return runSmoke(args);
519
+ }
520
+ const commandMap = {
512
521
  flutter_tree: 'flutter-tree',
513
522
  h5_dom: 'h5-dom',
514
523
  h5_eval: 'h5-eval',
@@ -516,22 +525,24 @@ async function callTool(name, args) {
516
525
  h5_input: 'h5-input',
517
526
  h5_wait: 'h5-wait',
518
527
  h5_scroll: 'h5-scroll',
528
+ freeze_app: 'freeze-app',
529
+ thaw_app: 'thaw-app',
519
530
  flutter_h5_dom: 'flutter-h5-dom',
520
531
  flutter_h5_eval: 'flutter-h5-eval',
521
532
  flutter_h5_click: 'flutter-h5-click',
522
533
  flutter_h5_input: 'flutter-h5-input',
523
- flutter_h5_wait: 'flutter-h5-wait',
524
- flutter_h5_scroll: 'flutter-h5-scroll',
525
- flutter_nodes: 'flutter-nodes',
526
- tap_flutter_text: 'tap-flutter-text',
527
- input_flutter_text: 'input-flutter-text',
528
- uia_tree: 'uia-tree',
529
- install_apk: 'install-apk',
530
- clear_app_data: 'clear-app-data',
531
- launch_app: 'launch-app',
532
- launch_activity: 'launch-activity',
533
- launch_native_test: 'launch-native-test',
534
- launch_flutter: 'launch-flutter',
534
+ flutter_h5_wait: 'flutter-h5-wait',
535
+ flutter_h5_scroll: 'flutter-h5-scroll',
536
+ flutter_nodes: 'flutter-nodes',
537
+ tap_flutter_text: 'tap-flutter-text',
538
+ input_flutter_text: 'input-flutter-text',
539
+ uia_tree: 'uia-tree',
540
+ install_apk: 'install-apk',
541
+ clear_app_data: 'clear-app-data',
542
+ launch_app: 'launch-app',
543
+ launch_activity: 'launch-activity',
544
+ launch_native_test: 'launch-native-test',
545
+ launch_flutter: 'launch-flutter',
535
546
  tap_text: 'tap-text',
536
547
  wait_text: 'wait-text',
537
548
  input_text: 'input-text',
@@ -546,316 +557,319 @@ async function callTool(name, args) {
546
557
  appops_set: 'appops-set',
547
558
  tap_uia_text: 'tap-uia-text',
548
559
  permission_dialog: 'permission-dialog',
549
- };
550
- const command = commandMap[name] || name;
551
- return runBridgeChecked(command, args);
552
- }
553
-
554
- function capabilityPayload(args = {}) {
555
- const includeOptions = Boolean(args.includeOptions);
556
- const requestedCommand = args.command ? normalizeCommandName(args.command) : '';
557
- if (requestedCommand) {
558
- const definition = commandByName.get(requestedCommand);
559
- return {
560
- ok: Boolean(definition),
561
- command: requestedCommand,
562
- ...(definition ? shapeCommandDefinition(definition, true) : { error: 'unknown_command' }),
563
- };
564
- }
565
-
566
- const requestedDomain = args.domain ? String(args.domain) : '';
567
- const domains = {};
568
- for (const definition of commandDefinitions) {
569
- if (requestedDomain && definition.domain !== requestedDomain) continue;
570
- if (!domains[definition.domain]) domains[definition.domain] = [];
571
- domains[definition.domain].push(shapeCommandDefinition(definition, includeOptions));
572
- }
573
- return {
574
- ok: true,
575
- surface: mcpSurface === 'full' || mcpSurface === 'legacy' ? 'full' : 'compact',
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.',
577
- domains,
578
- };
579
- }
580
-
581
- function shapeCommandDefinition(definition, includeOptions) {
582
- return {
583
- command: definition.command,
584
- summary: definition.summary,
585
- targetApp: Boolean(definition.targetApp),
586
- ...(includeOptions ? { options: definition.options || [] } : {}),
587
- };
588
- }
589
-
590
- async function runGeneric(args = {}) {
591
- const command = normalizeCommandName(args.command);
592
- if (!commandByName.has(command)) {
593
- return toolText(`unknown command: ${args.command || ''}`, true);
594
- }
595
- const commandArgs = {
596
- ...(args.arguments && typeof args.arguments === 'object' ? args.arguments : {}),
597
- };
598
- for (const key of ['adb', 'serial', 'port', 'packageName']) {
599
- if (args[key] !== undefined && commandArgs[key] === undefined) {
600
- commandArgs[key] = args[key];
601
- }
602
- }
603
- if (command === 'batch') {
604
- return runBatch(commandArgs);
605
- }
606
- return runBridgeChecked(command, commandArgs);
607
- }
608
-
609
- function normalizeCommandName(value) {
610
- return String(value || '').trim().replace(/_/g, '-');
611
- }
612
-
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
- }
617
- const definition = commandByName.get(command);
618
- if (definition?.targetApp && !args.packageName && !args.port) {
619
- return toolText(`${command}: packageName or explicit port is required in MCP mode so the command cannot fall back to a default package.`, true);
620
- }
621
- return runBridge(command, args);
622
- }
623
-
624
- async function runBatch(args = {}, runner = runBridgeChecked) {
625
- const startedAtMs = Date.now();
626
- const mode = args.mode ? String(args.mode) : 'serial';
627
- if (mode !== 'serial') {
628
- return toolJson({ ok: false, error: 'batch_mode_not_supported', mode }, true);
629
- }
630
- const steps = Array.isArray(args.steps) ? args.steps : [];
631
- if (steps.length === 0) {
632
- return toolJson({ ok: false, error: 'batch_steps_required' }, true);
633
- }
634
- const maxSteps = args.maxSteps === undefined ? 30 : Number(args.maxSteps);
635
- if (!Number.isInteger(maxSteps) || maxSteps < 1) {
636
- return toolJson({ ok: false, error: 'invalid_max_steps', maxSteps: args.maxSteps }, true);
637
- }
638
- if (steps.length > maxSteps) {
639
- return toolJson({ ok: false, error: 'batch_too_many_steps', stepCount: steps.length, maxSteps }, true);
640
- }
641
-
642
- const defaults = args.defaults && typeof args.defaults === 'object' ? { ...args.defaults } : {};
643
- for (const key of ['adb', 'serial', 'port', 'packageName', 'artifactDir']) {
644
- if (args[key] !== undefined && defaults[key] === undefined) {
645
- defaults[key] = args[key];
646
- }
647
- }
648
-
649
- const normalizedSteps = [];
650
- const seenIds = new Set();
651
- for (let index = 0; index < steps.length; index += 1) {
652
- const rawStep = steps[index] && typeof steps[index] === 'object' ? steps[index] : {};
653
- const stepId = String(rawStep.id || `step_${index + 1}`);
654
- if (seenIds.has(stepId)) {
655
- return toolJson({ ok: false, error: 'duplicate_batch_step_id', stepId }, true);
656
- }
657
- seenIds.add(stepId);
658
- const command = normalizeCommandName(rawStep.command);
659
- if (!commandByName.has(command)) {
660
- return toolJson({ ok: false, error: 'unknown_batch_step_command', stepId, command: rawStep.command || '' }, true);
661
- }
662
- if (command === 'batch') {
663
- return toolJson({ ok: false, error: 'nested_batch_not_supported', stepId }, true);
664
- }
665
- normalizedSteps.push({ ...rawStep, id: stepId, command });
666
- }
667
-
668
- const stopOnError = args.stopOnError !== false;
669
- const includeRaw = Boolean(args.includeRaw);
670
- const maxRawChars = args.maxRawChars === undefined ? 4000 : Number(args.maxRawChars);
671
- if (!Number.isInteger(maxRawChars) || maxRawChars < 0) {
672
- return toolJson({ ok: false, error: 'invalid_max_raw_chars', maxRawChars: args.maxRawChars }, true);
673
- }
674
- const results = [];
675
- let stopped = false;
676
-
677
- for (const step of normalizedSteps) {
678
- if (stopped) {
679
- results.push({
680
- id: step.id,
681
- command: step.command,
682
- status: 'skipped',
683
- ok: false,
684
- skipped: true,
685
- reason: 'stopOnError',
686
- });
687
- continue;
688
- }
689
-
690
- const stepStartedAtMs = Date.now();
691
- const stepArgs = {
692
- ...defaults,
693
- ...(step.arguments && typeof step.arguments === 'object' ? step.arguments : {}),
694
- };
695
- for (const key of ['adb', 'serial', 'port', 'packageName']) {
696
- if (step[key] !== undefined) {
697
- stepArgs[key] = step[key];
698
- }
699
- }
700
- try {
701
- const toolResult = await runner(step.command, stepArgs);
702
- const parsed = parseToolResult(toolResult);
703
- const passed = !parsed.isError && parsed.payload?.ok !== false;
704
- const stepResult = {
705
- id: step.id,
706
- command: step.command,
707
- status: passed ? 'passed' : 'failed',
708
- ok: passed,
709
- packageName: stepArgs.packageName,
710
- port: stepArgs.port,
711
- durationMs: Date.now() - stepStartedAtMs,
712
- summary: summarizeToolPayload(parsed),
713
- };
714
- if (!passed) {
715
- stepResult.error = parsed.payload?.error || firstTextLine(parsed.text) || 'command_failed';
716
- }
717
- if (includeRaw) {
718
- stepResult.result = parsed.payload || undefined;
719
- stepResult.rawText = parsed.payload ? undefined : truncateText(parsed.text, maxRawChars);
720
- }
721
- results.push(stepResult);
722
- if (!passed && stopOnError) {
723
- stopped = true;
724
- }
725
- } catch (error) {
726
- const stepResult = {
727
- id: step.id,
728
- command: step.command,
729
- status: 'failed',
730
- ok: false,
731
- packageName: stepArgs.packageName,
732
- port: stepArgs.port,
733
- durationMs: Date.now() - stepStartedAtMs,
734
- error: error.message || String(error),
735
- };
736
- results.push(stepResult);
737
- if (stopOnError) {
738
- stopped = true;
739
- }
740
- }
741
- }
742
-
743
- const failed = results.filter((item) => item.status === 'failed').length;
744
- const skipped = results.filter((item) => item.status === 'skipped').length;
745
- const passed = results.filter((item) => item.status === 'passed').length;
746
- return toolJson({
747
- ok: failed === 0,
748
- batchId: args.batchId || generatedBatchId(),
749
- mode,
750
- stopOnError,
751
- stepCount: normalizedSteps.length,
752
- passed,
753
- failed,
754
- skipped,
755
- durationMs: Date.now() - startedAtMs,
756
- steps: results,
757
- }, failed > 0);
758
- }
759
-
760
- async function runBridge(command, args) {
761
- return runProcess(buildBridgeCliArgs(command, args));
762
- }
763
-
764
- function parseToolResult(toolResult) {
765
- const text = String(toolResult?.content?.[0]?.text || '');
766
- try {
767
- return {
768
- isError: Boolean(toolResult?.isError),
769
- text,
770
- payload: JSON.parse(text),
771
- };
772
- } catch (_) {
773
- return {
774
- isError: Boolean(toolResult?.isError),
775
- text,
776
- payload: null,
777
- };
778
- }
779
- }
780
-
781
- function summarizeToolPayload(parsed) {
782
- const payload = parsed.payload;
783
- if (!payload || typeof payload !== 'object') {
784
- return { text: truncateText(parsed.text, 500) };
785
- }
786
- const summary = {
787
- ok: payload.ok,
788
- error: payload.error || null,
789
- };
790
- if (payload.packageName) summary.packageName = payload.packageName;
791
- if (payload.app?.packageName) summary.app = payload.app.packageName;
792
- if (payload.activity) summary.activity = payload.activity;
793
- if (payload.component) summary.component = payload.component;
794
- if (payload.transport) summary.transport = payload.transport;
795
- if (payload.source) summary.source = payload.source;
796
- if (payload.path) summary.path = payload.path;
797
- if (payload.debugBridge) {
798
- summary.bridge = {
799
- version: payload.debugBridge.version,
800
- port: payload.debugBridge.port,
801
- };
802
- }
803
- if (payload.count !== undefined) summary.count = payload.count;
804
- if (payload.nodeCount !== undefined) summary.nodeCount = payload.nodeCount;
805
- if (Array.isArray(payload.items)) summary.items = payload.items.length;
806
- if (payload.values && typeof payload.values === 'object') {
807
- summary.values = Object.keys(payload.values).length;
808
- }
809
- if (payload.counts) summary.counts = payload.counts;
810
- if (Array.isArray(payload.requests)) summary.requests = payload.requests.length;
811
- if (Array.isArray(payload.console)) summary.console = payload.console.length;
812
- if (payload.flutter?.layout?.operable) {
813
- summary.flutterOperable = {
814
- ok: payload.flutter.layout.operable.ok,
815
- count: payload.flutter.layout.operable.count,
816
- };
817
- }
818
- if (payload.result && typeof payload.result === 'object') {
819
- summary.result = {
820
- ok: payload.result.ok,
821
- error: payload.result.error || null,
822
- value: truncateText(payload.result.value, 200),
823
- bodyText: truncateText(payload.result.bodyText, 200),
824
- };
825
- }
826
- return summary;
827
- }
828
-
829
- function truncateText(value, maxChars) {
830
- if (value === undefined || value === null) return value;
831
- const text = String(value);
832
- if (text.length <= maxChars) return text;
833
- return `${text.slice(0, maxChars)}...`;
834
- }
835
-
836
- function firstTextLine(value) {
837
- const lines = String(value || '').split(/\r?\n/).filter((line) => line.trim());
838
- return lines.find((line) => {
839
- const text = line.trim().toLowerCase();
840
- return text !== 'stderr:' && text !== 'stdout:';
841
- }) || lines[0] || '';
842
- }
843
-
844
- function generatedBatchId() {
845
- return `batch-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
846
- }
847
-
848
- function buildBridgeCliArgs(command, args = {}) {
849
- const cliArgs = [cliScript, command];
850
- addCommonArgs(cliArgs, args);
851
- addArg(cliArgs, 'initial-route', args.initialRoute);
852
- addArg(cliArgs, 'activity', args.activity);
853
- addArg(cliArgs, 'component', args.component);
854
- addArg(cliArgs, 'action', args.action);
855
- addRepeatedArg(cliArgs, 'category', args.category);
856
- addArg(cliArgs, 'data', args.data);
857
- addExtraArgs(cliArgs, args.extra);
858
- addArg(cliArgs, 'out-file', args.outFile);
560
+ };
561
+ const command = commandMap[name] || name;
562
+ return runBridgeChecked(command, args);
563
+ }
564
+
565
+ function capabilityPayload(args = {}) {
566
+ const includeOptions = Boolean(args.includeOptions);
567
+ const requestedCommand = args.command ? normalizeCommandName(args.command) : '';
568
+ if (requestedCommand) {
569
+ const definition = commandByName.get(requestedCommand);
570
+ return {
571
+ ok: Boolean(definition),
572
+ command: requestedCommand,
573
+ ...(definition ? shapeCommandDefinition(definition, true) : { error: 'unknown_command' }),
574
+ };
575
+ }
576
+
577
+ const requestedDomain = args.domain ? String(args.domain) : '';
578
+ const domains = {};
579
+ for (const definition of commandDefinitions) {
580
+ if (requestedDomain && definition.domain !== requestedDomain) continue;
581
+ if (!domains[definition.domain]) domains[definition.domain] = [];
582
+ domains[definition.domain].push(shapeCommandDefinition(definition, includeOptions));
583
+ }
584
+ return {
585
+ ok: true,
586
+ surface: mcpSurface === 'full' || mcpSurface === 'legacy' ? 'full' : 'compact',
587
+ usage: 'Use run with one of these command names. Prefer packageName for app-specific commands; install-apk, clear-app-data, launch-app, freeze/thaw, UI, WebView, logcat, network, and permission workflows are supported.',
588
+ domains,
589
+ };
590
+ }
591
+
592
+ function shapeCommandDefinition(definition, includeOptions) {
593
+ return {
594
+ command: definition.command,
595
+ summary: definition.summary,
596
+ targetApp: Boolean(definition.targetApp),
597
+ ...(includeOptions ? { options: definition.options || [] } : {}),
598
+ };
599
+ }
600
+
601
+ async function runGeneric(args = {}) {
602
+ const command = normalizeCommandName(args.command);
603
+ if (!commandByName.has(command)) {
604
+ return toolText(`unknown command: ${args.command || ''}`, true);
605
+ }
606
+ const commandArgs = {
607
+ ...(args.arguments && typeof args.arguments === 'object' ? args.arguments : {}),
608
+ };
609
+ for (const key of ['adb', 'serial', 'port', 'packageName']) {
610
+ if (args[key] !== undefined && commandArgs[key] === undefined) {
611
+ commandArgs[key] = args[key];
612
+ }
613
+ }
614
+ if (command === 'batch') {
615
+ return runBatch(commandArgs);
616
+ }
617
+ return runBridgeChecked(command, commandArgs);
618
+ }
619
+
620
+ function normalizeCommandName(value) {
621
+ return String(value || '').trim().replace(/_/g, '-');
622
+ }
623
+
624
+ function runBridgeChecked(command, args = {}) {
625
+ if (command === 'clear-app-data' && !args.packageName) {
626
+ return toolText('clear-app-data: packageName is required in MCP mode so the command cannot clear a default package.', true);
627
+ }
628
+ if ((command === 'freeze-app' || command === 'thaw-app') && !args.packageName) {
629
+ return toolText(`${command}: packageName is required in MCP mode so the command cannot signal a default package.`, true);
630
+ }
631
+ const definition = commandByName.get(command);
632
+ if (definition?.targetApp && !args.packageName && !args.port) {
633
+ return toolText(`${command}: packageName or explicit port is required in MCP mode so the command cannot fall back to a default package.`, true);
634
+ }
635
+ return runBridge(command, args);
636
+ }
637
+
638
+ async function runBatch(args = {}, runner = runBridgeChecked) {
639
+ const startedAtMs = Date.now();
640
+ const mode = args.mode ? String(args.mode) : 'serial';
641
+ if (mode !== 'serial') {
642
+ return toolJson({ ok: false, error: 'batch_mode_not_supported', mode }, true);
643
+ }
644
+ const steps = Array.isArray(args.steps) ? args.steps : [];
645
+ if (steps.length === 0) {
646
+ return toolJson({ ok: false, error: 'batch_steps_required' }, true);
647
+ }
648
+ const maxSteps = args.maxSteps === undefined ? 30 : Number(args.maxSteps);
649
+ if (!Number.isInteger(maxSteps) || maxSteps < 1) {
650
+ return toolJson({ ok: false, error: 'invalid_max_steps', maxSteps: args.maxSteps }, true);
651
+ }
652
+ if (steps.length > maxSteps) {
653
+ return toolJson({ ok: false, error: 'batch_too_many_steps', stepCount: steps.length, maxSteps }, true);
654
+ }
655
+
656
+ const defaults = args.defaults && typeof args.defaults === 'object' ? { ...args.defaults } : {};
657
+ for (const key of ['adb', 'serial', 'port', 'packageName', 'artifactDir']) {
658
+ if (args[key] !== undefined && defaults[key] === undefined) {
659
+ defaults[key] = args[key];
660
+ }
661
+ }
662
+
663
+ const normalizedSteps = [];
664
+ const seenIds = new Set();
665
+ for (let index = 0; index < steps.length; index += 1) {
666
+ const rawStep = steps[index] && typeof steps[index] === 'object' ? steps[index] : {};
667
+ const stepId = String(rawStep.id || `step_${index + 1}`);
668
+ if (seenIds.has(stepId)) {
669
+ return toolJson({ ok: false, error: 'duplicate_batch_step_id', stepId }, true);
670
+ }
671
+ seenIds.add(stepId);
672
+ const command = normalizeCommandName(rawStep.command);
673
+ if (!commandByName.has(command)) {
674
+ return toolJson({ ok: false, error: 'unknown_batch_step_command', stepId, command: rawStep.command || '' }, true);
675
+ }
676
+ if (command === 'batch') {
677
+ return toolJson({ ok: false, error: 'nested_batch_not_supported', stepId }, true);
678
+ }
679
+ normalizedSteps.push({ ...rawStep, id: stepId, command });
680
+ }
681
+
682
+ const stopOnError = args.stopOnError !== false;
683
+ const includeRaw = Boolean(args.includeRaw);
684
+ const maxRawChars = args.maxRawChars === undefined ? 4000 : Number(args.maxRawChars);
685
+ if (!Number.isInteger(maxRawChars) || maxRawChars < 0) {
686
+ return toolJson({ ok: false, error: 'invalid_max_raw_chars', maxRawChars: args.maxRawChars }, true);
687
+ }
688
+ const results = [];
689
+ let stopped = false;
690
+
691
+ for (const step of normalizedSteps) {
692
+ if (stopped) {
693
+ results.push({
694
+ id: step.id,
695
+ command: step.command,
696
+ status: 'skipped',
697
+ ok: false,
698
+ skipped: true,
699
+ reason: 'stopOnError',
700
+ });
701
+ continue;
702
+ }
703
+
704
+ const stepStartedAtMs = Date.now();
705
+ const stepArgs = {
706
+ ...defaults,
707
+ ...(step.arguments && typeof step.arguments === 'object' ? step.arguments : {}),
708
+ };
709
+ for (const key of ['adb', 'serial', 'port', 'packageName']) {
710
+ if (step[key] !== undefined) {
711
+ stepArgs[key] = step[key];
712
+ }
713
+ }
714
+ try {
715
+ const toolResult = await runner(step.command, stepArgs);
716
+ const parsed = parseToolResult(toolResult);
717
+ const passed = !parsed.isError && parsed.payload?.ok !== false;
718
+ const stepResult = {
719
+ id: step.id,
720
+ command: step.command,
721
+ status: passed ? 'passed' : 'failed',
722
+ ok: passed,
723
+ packageName: stepArgs.packageName,
724
+ port: stepArgs.port,
725
+ durationMs: Date.now() - stepStartedAtMs,
726
+ summary: summarizeToolPayload(parsed),
727
+ };
728
+ if (!passed) {
729
+ stepResult.error = parsed.payload?.error || firstTextLine(parsed.text) || 'command_failed';
730
+ }
731
+ if (includeRaw) {
732
+ stepResult.result = parsed.payload || undefined;
733
+ stepResult.rawText = parsed.payload ? undefined : truncateText(parsed.text, maxRawChars);
734
+ }
735
+ results.push(stepResult);
736
+ if (!passed && stopOnError) {
737
+ stopped = true;
738
+ }
739
+ } catch (error) {
740
+ const stepResult = {
741
+ id: step.id,
742
+ command: step.command,
743
+ status: 'failed',
744
+ ok: false,
745
+ packageName: stepArgs.packageName,
746
+ port: stepArgs.port,
747
+ durationMs: Date.now() - stepStartedAtMs,
748
+ error: error.message || String(error),
749
+ };
750
+ results.push(stepResult);
751
+ if (stopOnError) {
752
+ stopped = true;
753
+ }
754
+ }
755
+ }
756
+
757
+ const failed = results.filter((item) => item.status === 'failed').length;
758
+ const skipped = results.filter((item) => item.status === 'skipped').length;
759
+ const passed = results.filter((item) => item.status === 'passed').length;
760
+ return toolJson({
761
+ ok: failed === 0,
762
+ batchId: args.batchId || generatedBatchId(),
763
+ mode,
764
+ stopOnError,
765
+ stepCount: normalizedSteps.length,
766
+ passed,
767
+ failed,
768
+ skipped,
769
+ durationMs: Date.now() - startedAtMs,
770
+ steps: results,
771
+ }, failed > 0);
772
+ }
773
+
774
+ async function runBridge(command, args) {
775
+ return runProcess(buildBridgeCliArgs(command, args));
776
+ }
777
+
778
+ function parseToolResult(toolResult) {
779
+ const text = String(toolResult?.content?.[0]?.text || '');
780
+ try {
781
+ return {
782
+ isError: Boolean(toolResult?.isError),
783
+ text,
784
+ payload: JSON.parse(text),
785
+ };
786
+ } catch (_) {
787
+ return {
788
+ isError: Boolean(toolResult?.isError),
789
+ text,
790
+ payload: null,
791
+ };
792
+ }
793
+ }
794
+
795
+ function summarizeToolPayload(parsed) {
796
+ const payload = parsed.payload;
797
+ if (!payload || typeof payload !== 'object') {
798
+ return { text: truncateText(parsed.text, 500) };
799
+ }
800
+ const summary = {
801
+ ok: payload.ok,
802
+ error: payload.error || null,
803
+ };
804
+ if (payload.packageName) summary.packageName = payload.packageName;
805
+ if (payload.app?.packageName) summary.app = payload.app.packageName;
806
+ if (payload.activity) summary.activity = payload.activity;
807
+ if (payload.component) summary.component = payload.component;
808
+ if (payload.transport) summary.transport = payload.transport;
809
+ if (payload.source) summary.source = payload.source;
810
+ if (payload.path) summary.path = payload.path;
811
+ if (payload.debugBridge) {
812
+ summary.bridge = {
813
+ version: payload.debugBridge.version,
814
+ port: payload.debugBridge.port,
815
+ };
816
+ }
817
+ if (payload.count !== undefined) summary.count = payload.count;
818
+ if (payload.nodeCount !== undefined) summary.nodeCount = payload.nodeCount;
819
+ if (Array.isArray(payload.items)) summary.items = payload.items.length;
820
+ if (payload.values && typeof payload.values === 'object') {
821
+ summary.values = Object.keys(payload.values).length;
822
+ }
823
+ if (payload.counts) summary.counts = payload.counts;
824
+ if (Array.isArray(payload.requests)) summary.requests = payload.requests.length;
825
+ if (Array.isArray(payload.console)) summary.console = payload.console.length;
826
+ if (payload.flutter?.layout?.operable) {
827
+ summary.flutterOperable = {
828
+ ok: payload.flutter.layout.operable.ok,
829
+ count: payload.flutter.layout.operable.count,
830
+ };
831
+ }
832
+ if (payload.result && typeof payload.result === 'object') {
833
+ summary.result = {
834
+ ok: payload.result.ok,
835
+ error: payload.result.error || null,
836
+ value: truncateText(payload.result.value, 200),
837
+ bodyText: truncateText(payload.result.bodyText, 200),
838
+ };
839
+ }
840
+ return summary;
841
+ }
842
+
843
+ function truncateText(value, maxChars) {
844
+ if (value === undefined || value === null) return value;
845
+ const text = String(value);
846
+ if (text.length <= maxChars) return text;
847
+ return `${text.slice(0, maxChars)}...`;
848
+ }
849
+
850
+ function firstTextLine(value) {
851
+ const lines = String(value || '').split(/\r?\n/).filter((line) => line.trim());
852
+ return lines.find((line) => {
853
+ const text = line.trim().toLowerCase();
854
+ return text !== 'stderr:' && text !== 'stdout:';
855
+ }) || lines[0] || '';
856
+ }
857
+
858
+ function generatedBatchId() {
859
+ return `batch-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
860
+ }
861
+
862
+ function buildBridgeCliArgs(command, args = {}) {
863
+ const cliArgs = [cliScript, command];
864
+ addCommonArgs(cliArgs, args);
865
+ addArg(cliArgs, 'initial-route', args.initialRoute);
866
+ addArg(cliArgs, 'activity', args.activity);
867
+ addArg(cliArgs, 'component', args.component);
868
+ addArg(cliArgs, 'action', args.action);
869
+ addRepeatedArg(cliArgs, 'category', args.category);
870
+ addArg(cliArgs, 'data', args.data);
871
+ addExtraArgs(cliArgs, args.extra);
872
+ addArg(cliArgs, 'out-file', args.outFile);
859
873
  addArg(cliArgs, 'artifact-dir', args.artifactDir || defaultArtifactDirFor(command, args));
860
874
  addArg(cliArgs, 'apk-path', args.apkPath);
861
875
  addArg(cliArgs, 'tap-x', args.tapX);
@@ -881,45 +895,45 @@ function buildBridgeCliArgs(command, args = {}) {
881
895
  addArg(cliArgs, 'page-url-filter', args.pageUrlFilter);
882
896
  addArg(cliArgs, 'url-filter', args.urlFilter);
883
897
  addArg(cliArgs, 'method', args.method);
884
- addArg(cliArgs, 'status-code', args.statusCode);
885
- addArg(cliArgs, 'compact', args.compact);
886
- addArg(cliArgs, 'full', args.full);
887
- addArg(cliArgs, 'text-filter', args.textFilter);
888
- addArg(cliArgs, 'resource-id-filter', args.resourceIdFilter);
889
- addArg(cliArgs, 'class-filter', args.classFilter);
890
- addArg(cliArgs, 'visible-only', args.visibleOnly);
891
- addArg(cliArgs, 'max-nodes', args.maxNodes);
892
- addArg(cliArgs, 'max-depth', args.maxDepth);
893
- addArg(cliArgs, 'no-bodies', args.noBodies);
894
- addArg(cliArgs, 'duration-ms', args.durationMs);
895
- addArg(cliArgs, 'include-response-body', args.includeResponseBody);
896
- addArg(cliArgs, 'body-max-bytes', args.bodyMaxBytes);
897
- addArg(cliArgs, 'max-events', args.maxEvents);
898
- addArg(cliArgs, 'keep-forward', args.keepForward);
899
- addArg(cliArgs, 'key-code', args.keyCode);
900
- addArg(cliArgs, 'payload', args.payload);
901
- addArg(cliArgs, 'delta', args.delta);
902
- addArg(cliArgs, 'max-swipes', args.maxSwipes);
903
- addArg(cliArgs, 'permission', args.permission);
904
- addArg(cliArgs, 'op', args.op);
905
- addArg(cliArgs, 'mode', args.mode);
906
- addArg(cliArgs, 'script', args.script);
907
- addArg(cliArgs, 'selector', args.selector);
898
+ addArg(cliArgs, 'status-code', args.statusCode);
899
+ addArg(cliArgs, 'compact', args.compact);
900
+ addArg(cliArgs, 'full', args.full);
901
+ addArg(cliArgs, 'text-filter', args.textFilter);
902
+ addArg(cliArgs, 'resource-id-filter', args.resourceIdFilter);
903
+ addArg(cliArgs, 'class-filter', args.classFilter);
904
+ addArg(cliArgs, 'visible-only', args.visibleOnly);
905
+ addArg(cliArgs, 'max-nodes', args.maxNodes);
906
+ addArg(cliArgs, 'max-depth', args.maxDepth);
907
+ addArg(cliArgs, 'no-bodies', args.noBodies);
908
+ addArg(cliArgs, 'duration-ms', args.durationMs);
909
+ addArg(cliArgs, 'include-response-body', args.includeResponseBody);
910
+ addArg(cliArgs, 'body-max-bytes', args.bodyMaxBytes);
911
+ addArg(cliArgs, 'max-events', args.maxEvents);
912
+ addArg(cliArgs, 'keep-forward', args.keepForward);
913
+ addArg(cliArgs, 'key-code', args.keyCode);
914
+ addArg(cliArgs, 'payload', args.payload);
915
+ addArg(cliArgs, 'delta', args.delta);
916
+ addArg(cliArgs, 'max-swipes', args.maxSwipes);
917
+ addArg(cliArgs, 'permission', args.permission);
918
+ addArg(cliArgs, 'op', args.op);
919
+ addArg(cliArgs, 'mode', args.mode);
920
+ addArg(cliArgs, 'script', args.script);
921
+ addArg(cliArgs, 'selector', args.selector);
908
922
  addArg(cliArgs, 'target-text', args.targetText);
909
923
  addArg(cliArgs, 'value', args.value);
910
924
  addArg(cliArgs, 'exact', args.exact);
911
925
  addArg(cliArgs, 'button-text', args.buttonText);
912
926
  addArg(cliArgs, 'resource-id', args.resourceId);
913
- addArg(cliArgs, 'attempts', args.attempts);
914
- addArg(cliArgs, 'interval-ms', args.intervalMs);
915
- addArg(cliArgs, 'delta-x', args.deltaX);
916
- addArg(cliArgs, 'delta-y', args.deltaY);
917
- addArg(cliArgs, 'require-text', args.requireText);
918
- addArg(cliArgs, 'absent-text', args.absentText);
919
- addArg(cliArgs, 'require-activity', args.requireActivity);
920
- addArg(cliArgs, 'since-id', args.sinceId);
921
- addArg(cliArgs, 'since-ms', args.sinceMs);
922
- addArg(cliArgs, 'limit', args.limit);
927
+ addArg(cliArgs, 'attempts', args.attempts);
928
+ addArg(cliArgs, 'interval-ms', args.intervalMs);
929
+ addArg(cliArgs, 'delta-x', args.deltaX);
930
+ addArg(cliArgs, 'delta-y', args.deltaY);
931
+ addArg(cliArgs, 'require-text', args.requireText);
932
+ addArg(cliArgs, 'absent-text', args.absentText);
933
+ addArg(cliArgs, 'require-activity', args.requireActivity);
934
+ addArg(cliArgs, 'since-id', args.sinceId);
935
+ addArg(cliArgs, 'since-ms', args.sinceMs);
936
+ addArg(cliArgs, 'limit', args.limit);
923
937
  addArg(cliArgs, 'pid', args.pid);
924
938
  addArg(cliArgs, 'app-pid', args.appPid);
925
939
  addArg(cliArgs, 'tag', args.tag);
@@ -927,21 +941,21 @@ function buildBridgeCliArgs(command, args = {}) {
927
941
  addArg(cliArgs, 'grep', args.grep);
928
942
  addArg(cliArgs, 'lines', args.lines);
929
943
  addArg(cliArgs, 'since', args.since);
930
- addArg(cliArgs, 'follow', args.follow);
931
- addArg(cliArgs, 'duration-sec', args.durationSec);
932
- addArg(cliArgs, 'clear', args.clear);
933
- addArg(cliArgs, 'skip-flutter-launch', args.skipFlutterLaunch);
934
- return cliArgs;
935
- }
944
+ addArg(cliArgs, 'follow', args.follow);
945
+ addArg(cliArgs, 'duration-sec', args.durationSec);
946
+ addArg(cliArgs, 'clear', args.clear);
947
+ addArg(cliArgs, 'skip-flutter-launch', args.skipFlutterLaunch);
948
+ return cliArgs;
949
+ }
936
950
 
937
951
  async function runSmoke(args) {
938
952
  const cliArgs = [cliScript, 'smoke'];
939
- addCommonArgs(cliArgs, args);
940
- addArg(cliArgs, 'out-file', args.outFile);
941
- addArg(cliArgs, 'artifact-dir', args.artifactDir || defaultArtifactDirFor('smoke', args));
942
- addArg(cliArgs, 'skip-flutter-launch', args.skipFlutterLaunch);
943
- return runProcess(cliArgs);
944
- }
953
+ addCommonArgs(cliArgs, args);
954
+ addArg(cliArgs, 'out-file', args.outFile);
955
+ addArg(cliArgs, 'artifact-dir', args.artifactDir || defaultArtifactDirFor('smoke', args));
956
+ addArg(cliArgs, 'skip-flutter-launch', args.skipFlutterLaunch);
957
+ return runProcess(cliArgs);
958
+ }
945
959
 
946
960
  function defaultArtifactDirFor(command, args) {
947
961
  if (args.outFile) return '';
@@ -956,35 +970,35 @@ function addCommonArgs(cliArgs, args) {
956
970
  addArg(cliArgs, 'package-name', args.packageName);
957
971
  }
958
972
 
959
- function addArg(cliArgs, name, value) {
960
- if (value === undefined || value === null || value === '' || value === false) {
961
- return;
962
- }
963
- cliArgs.push(`--${name}`, String(value));
964
- }
965
-
966
- function addRepeatedArg(cliArgs, name, value) {
967
- if (Array.isArray(value)) {
968
- for (const item of value) addArg(cliArgs, name, item);
969
- return;
970
- }
971
- addArg(cliArgs, name, value);
972
- }
973
-
974
- function addExtraArgs(cliArgs, value) {
975
- if (Array.isArray(value)) {
976
- for (const item of value) addArg(cliArgs, 'extra', item);
977
- return;
978
- }
979
- if (value && typeof value === 'object') {
980
- for (const [key, extraValue] of Object.entries(value)) {
981
- addArg(cliArgs, 'extra', `${key}=${extraValue}`);
982
- }
983
- return;
984
- }
985
- addArg(cliArgs, 'extra', value);
986
- }
987
-
973
+ function addArg(cliArgs, name, value) {
974
+ if (value === undefined || value === null || value === '' || value === false) {
975
+ return;
976
+ }
977
+ cliArgs.push(`--${name}`, String(value));
978
+ }
979
+
980
+ function addRepeatedArg(cliArgs, name, value) {
981
+ if (Array.isArray(value)) {
982
+ for (const item of value) addArg(cliArgs, name, item);
983
+ return;
984
+ }
985
+ addArg(cliArgs, name, value);
986
+ }
987
+
988
+ function addExtraArgs(cliArgs, value) {
989
+ if (Array.isArray(value)) {
990
+ for (const item of value) addArg(cliArgs, 'extra', item);
991
+ return;
992
+ }
993
+ if (value && typeof value === 'object') {
994
+ for (const [key, extraValue] of Object.entries(value)) {
995
+ addArg(cliArgs, 'extra', `${key}=${extraValue}`);
996
+ }
997
+ return;
998
+ }
999
+ addArg(cliArgs, 'extra', value);
1000
+ }
1001
+
988
1002
  function runProcess(cliArgs) {
989
1003
  return new Promise((resolve) => {
990
1004
  const child = spawn(nodeBinary, cliArgs, {
@@ -1009,23 +1023,23 @@ function runProcess(cliArgs) {
1009
1023
  code === 0 ? '' : `exitCode: ${code}`,
1010
1024
  retryWithPackageNameHint(cliArgs, stdout, stderr, code),
1011
1025
  ].filter(Boolean).join('\n\n');
1012
- resolve(toolText(text || emptyProcessText(cliArgs), code !== 0));
1013
- });
1014
- });
1015
- }
1016
-
1017
- function emptyProcessText(cliArgs) {
1018
- const command = cliArgs[1] || '';
1019
- if (command === 'logcat' && cliArgs.includes('--app-pid')) {
1020
- return 'logcat: no matching lines for current app pid';
1021
- }
1022
- if (command === 'logcat') {
1023
- return 'logcat: no matching lines';
1024
- }
1025
- return 'ok';
1026
- }
1027
-
1028
- function retryWithPackageNameHint(cliArgs, stdout, stderr, code) {
1026
+ resolve(toolText(text || emptyProcessText(cliArgs), code !== 0));
1027
+ });
1028
+ });
1029
+ }
1030
+
1031
+ function emptyProcessText(cliArgs) {
1032
+ const command = cliArgs[1] || '';
1033
+ if (command === 'logcat' && cliArgs.includes('--app-pid')) {
1034
+ return 'logcat: no matching lines for current app pid';
1035
+ }
1036
+ if (command === 'logcat') {
1037
+ return 'logcat: no matching lines';
1038
+ }
1039
+ return 'ok';
1040
+ }
1041
+
1042
+ function retryWithPackageNameHint(cliArgs, stdout, stderr, code) {
1029
1043
  if (code === 0 || cliArgs.includes('--package-name') || cliArgs.includes('--port')) {
1030
1044
  return '';
1031
1045
  }
@@ -1036,25 +1050,25 @@ function retryWithPackageNameHint(cliArgs, stdout, stderr, code) {
1036
1050
  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.';
1037
1051
  }
1038
1052
 
1039
- function toolText(text, isError = false) {
1040
- return {
1041
- content: [
1042
- {
1043
- type: 'text',
1053
+ function toolText(text, isError = false) {
1054
+ return {
1055
+ content: [
1056
+ {
1057
+ type: 'text',
1044
1058
  text,
1045
1059
  },
1046
1060
  ],
1047
- isError,
1048
- };
1049
- }
1050
-
1051
- function toolJson(value, isError = false) {
1052
- return toolText(JSON.stringify(value, null, 2), isError);
1053
- }
1054
-
1055
- function sendResult(id, result) {
1056
- send({ jsonrpc: '2.0', id, result });
1057
- }
1061
+ isError,
1062
+ };
1063
+ }
1064
+
1065
+ function toolJson(value, isError = false) {
1066
+ return toolText(JSON.stringify(value, null, 2), isError);
1067
+ }
1068
+
1069
+ function sendResult(id, result) {
1070
+ send({ jsonrpc: '2.0', id, result });
1071
+ }
1058
1072
 
1059
1073
  function sendError(id, code, message) {
1060
1074
  send({
@@ -1064,27 +1078,27 @@ function sendError(id, code, message) {
1064
1078
  });
1065
1079
  }
1066
1080
 
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
- }
1076
-
1077
- function writeLog(text) {
1078
- process.stderr.write(`${text}\n`);
1079
- }
1080
-
1081
- if (require.main === module) {
1082
- startServer();
1083
- }
1084
-
1085
- module.exports = {
1086
- buildBridgeCliArgs,
1087
- readNextMessage,
1088
- runBatch,
1089
- startServer,
1090
- };
1081
+ function send(message) {
1082
+ const body = Buffer.from(JSON.stringify(message), 'utf8');
1083
+ if (responseFormat === 'line') {
1084
+ process.stdout.write(`${body.toString('utf8')}\n`);
1085
+ return;
1086
+ }
1087
+ process.stdout.write(`Content-Length: ${body.length}\r\n\r\n`);
1088
+ process.stdout.write(body);
1089
+ }
1090
+
1091
+ function writeLog(text) {
1092
+ process.stderr.write(`${text}\n`);
1093
+ }
1094
+
1095
+ if (require.main === module) {
1096
+ startServer();
1097
+ }
1098
+
1099
+ module.exports = {
1100
+ buildBridgeCliArgs,
1101
+ readNextMessage,
1102
+ runBatch,
1103
+ startServer,
1104
+ };