@mobileaidev/ai-app-bridge 0.2.5 → 0.2.7

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