@mobileaidev/ai-app-bridge 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,10 +11,22 @@ ai-app-bridge launch-activity --package-name io.github.mobileaidev.aiappbridge.s
11
11
  ai-app-bridge screenshot --package-name io.github.mobileaidev.aiappbridge.sample
12
12
  ai-app-bridge input-text --package-name io.github.mobileaidev.aiappbridge.sample --text "中文输入" --hide-keyboard
13
13
  ai-app-bridge network --package-name io.github.mobileaidev.aiappbridge.sample --compact --url-filter /api/
14
- ai-app-bridge webview-network --package-name io.github.mobileaidev.aiappbridge.sample --duration-ms 3000
15
- ai-app-bridge-mcp
16
- ```
17
-
14
+ ai-app-bridge webview-network --package-name io.github.mobileaidev.aiappbridge.sample --duration-ms 3000
15
+ ai-app-bridge-mcp
16
+ ```
17
+
18
+ MCP defaults to a compact tool surface to avoid loading every command schema
19
+ into the model context:
20
+
21
+ - `capabilities` lists the bridge domains and command names.
22
+ - `run` executes a selected command with command-specific arguments.
23
+
24
+ This keeps install, launch, UI, Flutter, WebView, logcat, network, and
25
+ permission capabilities discoverable without exposing dozens of full schemas at
26
+ session start. Set `AI_APP_BRIDGE_MCP_SURFACE=full` before launching
27
+ `ai-app-bridge-mcp` only when a client needs the legacy one-tool-per-command
28
+ surface.
29
+
18
30
  WebView network and console capture use Android WebView DevTools/CDP when the
19
31
  target app is debuggable and WebView debugging is enabled.
20
32
 
@@ -782,25 +782,41 @@ async function ensureForward(ctx) {
782
782
  };
783
783
  }
784
784
 
785
- async function resolveDevicePort(ctx) {
786
- if (ctx.explicitPort) return { port: ctx.port, source: 'explicit-port' };
787
- try {
788
- const result = await adb(ctx, ['shell', 'run-as', ctx.packageName, 'cat', 'files/ai_app_bridge_port.json']);
789
- const state = JSON.parse(result.stdout.trim());
790
- const discoveredPort = Number(state.port);
791
- if (state.ok === true && Number.isInteger(discoveredPort) && discoveredPort > 0) {
792
- return { port: discoveredPort, source: 'package-port-file', state };
793
- }
794
- } catch (error) {
795
- // Fall back to the historical default port for older bridge versions.
796
- return {
797
- port: ctx.port,
798
- source: 'default-port',
799
- error: firstErrorLine(error),
800
- };
801
- }
802
- return { port: ctx.port, source: 'default-port' };
803
- }
785
+ async function resolveDevicePort(ctx) {
786
+ if (ctx.explicitPort) return { port: ctx.port, source: 'explicit-port' };
787
+ try {
788
+ const result = await adb(ctx, ['shell', 'run-as', ctx.packageName, 'cat', 'files/ai_app_bridge_port.json']);
789
+ const state = JSON.parse(result.stdout.trim());
790
+ const discoveredPort = Number(state.port);
791
+ if (state.ok === true && Number.isInteger(discoveredPort) && discoveredPort > 0) {
792
+ return { port: discoveredPort, source: 'package-port-file', state };
793
+ }
794
+ } catch (error) {
795
+ if (!shouldUseDefaultPortFallback(ctx)) {
796
+ ctx.devicePortSource = 'package-port-file';
797
+ ctx.devicePortError = firstErrorLine(error);
798
+ error.aiAppBridgePortDiscovery = true;
799
+ throw error;
800
+ }
801
+ return {
802
+ port: ctx.port,
803
+ source: 'default-port',
804
+ error: firstErrorLine(error),
805
+ };
806
+ }
807
+ if (!shouldUseDefaultPortFallback(ctx)) {
808
+ const error = new Error(`bridge port discovery failed for explicit package: ${ctx.packageName}`);
809
+ error.aiAppBridgePortDiscovery = true;
810
+ ctx.devicePortSource = 'package-port-file';
811
+ ctx.devicePortError = firstErrorLine(error);
812
+ throw error;
813
+ }
814
+ return { port: ctx.port, source: 'default-port' };
815
+ }
816
+
817
+ function shouldUseDefaultPortFallback(ctx) {
818
+ return !ctx.explicitPackageName;
819
+ }
804
820
 
805
821
  async function bridgeStatus(ctx, options = {}) {
806
822
  try {
@@ -953,17 +969,17 @@ function normalizeBridgeError(error) {
953
969
  suggestion: 'Confirm the target app is running and that the resolved bridge port belongs to this package.',
954
970
  };
955
971
  }
956
- if (lower.includes('adb timed out')) {
957
- return {
958
- code: 'adb_timeout',
959
- message,
960
- suggestion: 'Check the device state and retry; if the bridge port is known, pass --port to skip package port discovery.',
961
- };
962
- }
963
- if (lower.includes('run-as') || lower.includes('package not found')) {
964
- return {
965
- code: 'bridge_port_discovery_failed',
966
- message,
972
+ if (lower.includes('adb timed out')) {
973
+ return {
974
+ code: 'adb_timeout',
975
+ message,
976
+ suggestion: 'Check the device state and retry; if the bridge port is known, pass --port to skip package port discovery.',
977
+ };
978
+ }
979
+ if (error?.aiAppBridgePortDiscovery || lower.includes('bridge port discovery failed') || lower.includes('run-as') || lower.includes('package not found')) {
980
+ return {
981
+ code: 'bridge_port_discovery_failed',
982
+ message,
967
983
  suggestion: 'Install a debuggable build for the requested package or pass --port explicitly.',
968
984
  };
969
985
  }
@@ -4299,8 +4315,9 @@ module.exports = {
4299
4315
  compactNetworkRecord,
4300
4316
  pruneGeneratedArtifacts,
4301
4317
  shouldSkipInstallerTapForInstalledPackage,
4302
- shouldDismissKeyboardForPoint,
4303
- screenshotOutputPath,
4318
+ shouldDismissKeyboardForPoint,
4319
+ shouldUseDefaultPortFallback,
4320
+ screenshotOutputPath,
4304
4321
  statusSearchText,
4305
4322
  uiautomatorLockPath,
4306
4323
  waitTextConditionsMet,
package/bin/mcp-server.js CHANGED
@@ -4,11 +4,19 @@ const { spawn } = require('child_process');
4
4
  const path = require('path');
5
5
 
6
6
  const packageInfo = require('../package.json');
7
- const bridgeDir = __dirname;
8
- const cliScript = path.join(bridgeDir, 'ai-app-bridge.js');
9
- const nodeBinary = process.env.AI_APP_BRIDGE_NODE || process.execPath;
10
-
11
- let buffer = Buffer.alloc(0);
7
+ const bridgeDir = __dirname;
8
+ const cliScript = path.join(bridgeDir, 'ai-app-bridge.js');
9
+ const nodeBinary = process.env.AI_APP_BRIDGE_NODE || process.execPath;
10
+ const supportedProtocolVersions = ['2025-06-18', '2024-11-05'];
11
+ const defaultProtocolVersion = supportedProtocolVersions[0];
12
+ const mcpSurface = (process.env.AI_APP_BRIDGE_MCP_SURFACE || 'compact').toLowerCase();
13
+ const serverInstructions = [
14
+ 'AI App Bridge observes and controls Android apps for agent workflows. Prefer these tools over raw adb when inspecting UI, text, WebView, logs, network, app install, launch, and permissions.',
15
+ 'Default surface is compact: call capabilities to discover domains, then call run with a command and arguments.',
16
+ 'Always pass packageName for app-specific commands, or pass an explicit port. Do not rely on a sample/default package in MCP sessions.',
17
+ ].join(' ');
18
+
19
+ let buffer = Buffer.alloc(0);
12
20
 
13
21
  process.stdin.on('data', (chunk) => {
14
22
  buffer = Buffer.concat([buffer, chunk]);
@@ -70,20 +78,27 @@ async function handleMessage(body) {
70
78
  }
71
79
 
72
80
  try {
73
- if (message.method === 'initialize') {
74
- sendResult(message.id, {
75
- protocolVersion: message.params?.protocolVersion || '2024-11-05',
76
- capabilities: {
77
- tools: {},
78
- },
79
- serverInfo: {
80
- name: 'ai-app-bridge',
81
- version: packageInfo.version,
82
- },
83
- });
84
- return;
85
- }
86
-
81
+ if (message.method === 'initialize') {
82
+ sendResult(message.id, {
83
+ protocolVersion: negotiateProtocolVersion(message.params?.protocolVersion),
84
+ capabilities: {
85
+ tools: {},
86
+ },
87
+ serverInfo: {
88
+ name: 'ai-app-bridge',
89
+ title: 'AI App Bridge',
90
+ version: packageInfo.version,
91
+ },
92
+ instructions: serverInstructions,
93
+ });
94
+ return;
95
+ }
96
+
97
+ if (message.method === 'ping') {
98
+ sendResult(message.id, {});
99
+ return;
100
+ }
101
+
87
102
  if (message.method === 'tools/list') {
88
103
  sendResult(message.id, { tools: toolDefinitions() });
89
104
  return;
@@ -101,10 +116,46 @@ async function handleMessage(body) {
101
116
  } catch (error) {
102
117
  sendError(message.id, -32000, error.message || String(error));
103
118
  }
104
- }
105
-
106
- function toolDefinitions() {
107
- return [
119
+ }
120
+
121
+ function negotiateProtocolVersion(requestedVersion) {
122
+ if (supportedProtocolVersions.includes(requestedVersion)) {
123
+ return requestedVersion;
124
+ }
125
+ return defaultProtocolVersion;
126
+ }
127
+
128
+ function toolDefinitions() {
129
+ if (mcpSurface === 'full' || mcpSurface === 'legacy') {
130
+ return fullToolDefinitions();
131
+ }
132
+ return compactToolDefinitions();
133
+ }
134
+
135
+ function compactToolDefinitions() {
136
+ return [
137
+ bridgeTool('capabilities', 'List AI App Bridge capability domains and commands. Call this first when planning Android app automation; then use run to execute the selected command.', {
138
+ domain: { type: 'string', description: 'Optional domain filter such as core, app, action, flutter, webview, or diagnostics.' },
139
+ command: { type: 'string', description: 'Optional command name for detailed arguments, such as install-apk, launch-app, tree, input-text, or webview-network.' },
140
+ includeOptions: { type: 'boolean', description: 'Include per-command argument names. Defaults to false to keep output compact.' },
141
+ }),
142
+ bridgeTool('run', 'Run an AI App Bridge command. Use capabilities first to choose the command. Always pass packageName for app-specific commands.', {
143
+ command: { type: 'string', description: 'Command name from capabilities, using CLI form such as status, install-apk, launch-app, input-text, tree, webview-network, or logcat.' },
144
+ packageName: { type: 'string', description: 'Target Android package for app-specific commands. Strongly recommended.' },
145
+ serial: { type: 'string', description: 'ADB serial when multiple devices are connected.' },
146
+ port: { type: 'number', description: 'Explicit bridge port when packageName discovery is not available.' },
147
+ adb: { type: 'string', description: 'ADB executable path or command.' },
148
+ arguments: {
149
+ type: 'object',
150
+ description: 'Command-specific arguments from capabilities. Example: {"apkPath":"app-debug.apk","allowDowngrade":true}.',
151
+ additionalProperties: true,
152
+ },
153
+ }, ['command']),
154
+ ];
155
+ }
156
+
157
+ function fullToolDefinitions() {
158
+ return [
108
159
  bridgeTool('status', 'Read compact bridge status, app info, capture counts, and Flutter layout summary. If default port 18080 times out, agents should retry with the target Android packageName so the CLI can discover the app bridge port.', {
109
160
  full: { type: 'boolean', description: 'Return the full raw status payload, including large Flutter widget dumps.' },
110
161
  }),
@@ -302,21 +353,12 @@ function launchProperties() {
302
353
  activity: { type: 'string', description: 'Activity class, such as .MainActivity or com.example.MainActivity.' },
303
354
  component: { type: 'string', description: 'Explicit Android component, such as com.example/.MainActivity.' },
304
355
  action: { type: 'string', description: 'Intent action for explicit Activity launch.' },
305
- category: {
306
- oneOf: [
307
- { type: 'string' },
308
- { type: 'array', items: { type: 'string' } },
309
- ],
310
- description: 'Intent category or categories.',
311
- },
356
+ category: { type: 'string', description: 'Intent category.' },
312
357
  data: { type: 'string', description: 'Intent data URI.' },
313
358
  extra: {
314
- oneOf: [
315
- { type: 'string' },
316
- { type: 'array', items: { type: 'string' } },
317
- { type: 'object', additionalProperties: { type: 'string' } },
318
- ],
319
- description: 'String extras. Use key=value strings or an object of string values.',
359
+ type: 'object',
360
+ additionalProperties: { type: 'string' },
361
+ description: 'String intent extras as an object of key/value pairs.',
320
362
  },
321
363
  };
322
364
  }
@@ -342,21 +384,81 @@ function baseSchema(extraProperties = {}, extraRequired = []) {
342
384
  };
343
385
  }
344
386
 
345
- function h5TargetSchema() {
346
- return {
347
- selector: { type: 'string', description: 'CSS selector for the target DOM element.' },
387
+ function h5TargetSchema() {
388
+ return {
389
+ selector: { type: 'string', description: 'CSS selector for the target DOM element.' },
348
390
  targetText: { type: 'string', description: 'Text, value, aria-label, placeholder, id, name, or role to match.' },
349
391
  exact: { type: 'boolean', description: 'Require exact text match instead of substring match.' },
350
- };
351
- }
352
-
392
+ };
393
+ }
394
+
395
+ const commandDefinitions = [
396
+ { command: 'status', domain: 'core', summary: 'Read bridge status, app/device metadata, capture counts, and Flutter summary.', targetApp: true, options: ['packageName', 'port', 'serial', 'full'] },
397
+ { command: 'tree', domain: 'core', summary: 'Read Android View tree from the in-app bridge.', targetApp: true, options: ['packageName', 'port', 'serial', 'compact', 'textFilter', 'resourceIdFilter', 'classFilter', 'visibleOnly', 'maxNodes', 'maxDepth'] },
398
+ { command: 'uia-tree', domain: 'core', summary: 'Read UIAutomator XML for the current foreground window.', options: ['serial', 'compact', 'textFilter', 'resourceIdFilter', 'classFilter', 'visibleOnly', 'maxNodes'] },
399
+ { command: 'screenshot', domain: 'core', summary: 'Capture a screenshot, with foreground package verification when packageName is supplied.', options: ['serial', 'packageName', 'outFile', 'artifactDir'] },
400
+ { command: 'logs', domain: 'core', summary: 'Read in-app log records from the bridge.', targetApp: true, options: ['packageName', 'port', 'serial', 'sinceId', 'sinceMs', 'limit'] },
401
+ { command: 'network', domain: 'core', summary: 'Read in-app network records from the bridge.', targetApp: true, options: ['packageName', 'port', 'serial', 'compact', 'urlFilter', 'method', 'statusCode', 'noBodies', 'bodyMaxBytes', 'sinceId', 'sinceMs', 'limit'] },
402
+ { command: 'state', domain: 'core', summary: 'Read in-app state records from the bridge.', targetApp: true, options: ['packageName', 'port', 'serial', 'sinceId', 'sinceMs', 'limit'] },
403
+ { command: 'events', domain: 'core', summary: 'Read in-app event records from the bridge.', targetApp: true, options: ['packageName', 'port', 'serial', 'sinceId', 'sinceMs', 'limit'] },
404
+ { command: 'logcat', domain: 'diagnostics', summary: 'Read Android logcat with optional app pid, tag, level, and grep filters.', options: ['serial', 'packageName', 'pid', 'appPid', 'tag', 'level', 'grep', 'lines', 'since', 'follow', 'durationSec', 'clear'] },
405
+ { command: 'install-apk', domain: 'app', summary: 'Install an APK and assist device-side installer confirmation screens.', options: ['serial', 'packageName', 'apkPath', 'allowDowngrade', 'streaming', 'installTimeoutMs', 'installerTimeoutMs', 'intervalMs'] },
406
+ { command: 'launch-app', domain: 'app', summary: 'Launch the target package LAUNCHER Activity and report launcher candidates.', targetApp: true, options: ['serial', 'packageName', 'activity', 'component', 'action', 'category', 'data', 'extra'] },
407
+ { command: 'launch-activity', domain: 'app', summary: 'Launch an explicit Android Activity component with optional string extras.', targetApp: true, options: ['serial', 'packageName', 'activity', 'component', 'action', 'category', 'data', 'extra'] },
408
+ { command: 'launch-native-test', domain: 'app', summary: 'Launch the debug native bridge test Activity.', targetApp: true, options: ['serial', 'packageName'] },
409
+ { command: 'launch-flutter', domain: 'app', summary: 'Launch the Flutter Activity, optionally with an initial route.', targetApp: true, options: ['serial', 'packageName', 'initialRoute'] },
410
+ { command: 'permission-state', domain: 'app', summary: 'Read Android runtime permission state.', targetApp: true, options: ['serial', 'packageName', 'permission'] },
411
+ { command: 'permission-grant', domain: 'app', summary: 'Grant an Android runtime permission.', targetApp: true, options: ['serial', 'packageName', 'permission'] },
412
+ { command: 'permission-revoke', domain: 'app', summary: 'Revoke an Android runtime permission.', targetApp: true, options: ['serial', 'packageName', 'permission'] },
413
+ { command: 'permission-dialog', domain: 'app', summary: 'Tap a visible Android permission dialog allow button.', options: ['serial', 'targetText', 'buttonText', 'resourceId', 'attempts', 'intervalMs', 'exact'] },
414
+ { command: 'appops-set', domain: 'app', summary: 'Set an Android app-op mode.', targetApp: true, options: ['serial', 'packageName', 'op', 'mode'] },
415
+ { command: 'tap', domain: 'action', summary: 'Tap device coordinates through ADB.', options: ['serial', 'tapX', 'tapY'] },
416
+ { command: 'tap-text', domain: 'action', summary: 'Tap a visible Android View node by text/contentDescription through the bridge tree.', targetApp: true, options: ['serial', 'packageName', 'targetText', 'noAutoHideKeyboard'] },
417
+ { command: 'tap-uia-text', domain: 'action', summary: 'Tap a UIAutomator node by text without relying on the in-app tree.', options: ['serial', 'targetText', 'exact'] },
418
+ { command: 'wait-text', domain: 'action', summary: 'Wait until text appears in bridge status/tree or UIAutomator output.', targetApp: true, options: ['serial', 'packageName', 'targetText', 'timeoutSec', 'requireText', 'absentText', 'requireActivity'] },
419
+ { command: 'input-text', domain: 'action', summary: 'Set native Android text through the in-app bridge; use this for Chinese/Unicode.', targetApp: true, options: ['serial', 'packageName', 'text', 'tapX', 'tapY', 'hideKeyboard'] },
420
+ { command: 'keyboard-state', domain: 'action', summary: 'Read Android soft keyboard visibility.', options: ['serial'] },
421
+ { command: 'hide-keyboard', domain: 'action', summary: 'Hide the Android soft keyboard.', options: ['serial', 'force', 'intervalMs'] },
422
+ { command: 'swipe', domain: 'action', summary: 'Swipe device coordinates through ADB.', options: ['serial', 'startX', 'startY', 'endX', 'endY', 'durationMs'] },
423
+ { command: 'keyevent', domain: 'action', summary: 'Send an Android keyevent through ADB.', options: ['serial', 'keyCode'] },
424
+ { command: 'flutter-tree', domain: 'flutter', summary: 'Read the latest Flutter layout snapshot.', targetApp: true, options: ['serial', 'packageName', 'port'] },
425
+ { command: 'flutter-nodes', domain: 'flutter', summary: 'Read Flutter operable nodes.', targetApp: true, options: ['serial', 'packageName', 'port'] },
426
+ { command: 'flutter-action', domain: 'flutter', summary: 'Dispatch a raw Flutter action payload.', targetApp: true, options: ['serial', 'packageName', 'payload'] },
427
+ { command: 'tap-flutter-text', domain: 'flutter', summary: 'Tap a Flutter node by visible text.', targetApp: true, options: ['serial', 'packageName', 'targetText'] },
428
+ { command: 'input-flutter-text', domain: 'flutter', summary: 'Set Flutter TextField text through the Flutter action bridge.', targetApp: true, options: ['serial', 'packageName', 'text', 'tapX', 'tapY', 'hideKeyboard'] },
429
+ { command: 'scroll-flutter', domain: 'flutter', summary: 'Scroll Flutter content by delta or until text is visible.', targetApp: true, options: ['serial', 'packageName', 'targetText', 'delta', 'maxSwipes'] },
430
+ { command: 'h5-dom', domain: 'webview', summary: 'Read native Android WebView DOM.', targetApp: true, options: ['serial', 'packageName', 'port'] },
431
+ { command: 'h5-eval', domain: 'webview', summary: 'Execute JavaScript in the current native Android WebView.', targetApp: true, options: ['serial', 'packageName', 'script'] },
432
+ { command: 'h5-click', domain: 'webview', summary: 'Click a native WebView element by selector or text.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'exact'] },
433
+ { command: 'h5-input', domain: 'webview', summary: 'Set text in a native WebView input.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'value', 'exact'] },
434
+ { command: 'h5-wait', domain: 'webview', summary: 'Wait for native WebView text or selector.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'timeoutSec', 'intervalMs'] },
435
+ { command: 'h5-scroll', domain: 'webview', summary: 'Scroll native WebView content or a DOM element into view.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'deltaX', 'deltaY'] },
436
+ { command: 'flutter-h5-dom', domain: 'webview', summary: 'Read DOM through a Flutter H5 adapter.', targetApp: true, options: ['serial', 'packageName', 'port'] },
437
+ { command: 'flutter-h5-eval', domain: 'webview', summary: 'Execute JavaScript through a Flutter H5 adapter.', targetApp: true, options: ['serial', 'packageName', 'script'] },
438
+ { command: 'flutter-h5-click', domain: 'webview', summary: 'Click a Flutter H5 DOM element.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'exact'] },
439
+ { command: 'flutter-h5-input', domain: 'webview', summary: 'Set text in a Flutter H5 input.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'value', 'exact'] },
440
+ { command: 'flutter-h5-wait', domain: 'webview', summary: 'Wait for Flutter H5 text or selector.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'timeoutSec', 'intervalMs'] },
441
+ { command: 'flutter-h5-scroll', domain: 'webview', summary: 'Scroll Flutter H5 content or a DOM element into view.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'deltaX', 'deltaY'] },
442
+ { command: 'webview-pages', domain: 'webview', summary: 'List attachable Android WebView DevTools/CDP pages.', targetApp: true, options: ['serial', 'packageName', 'webviewPort', 'socketName', 'targetId', 'pageUrlFilter', 'keepForward'] },
443
+ { command: 'webview-network', domain: 'webview', summary: 'Capture WebView Network events through CDP.', targetApp: true, options: ['serial', 'packageName', 'webviewPort', 'socketName', 'targetId', 'pageUrlFilter', 'urlFilter', 'durationMs', 'script', 'includeResponseBody', 'bodyMaxBytes', 'maxEvents'] },
444
+ { command: 'webview-console', domain: 'webview', summary: 'Capture WebView console/log events through CDP.', targetApp: true, options: ['serial', 'packageName', 'webviewPort', 'socketName', 'targetId', 'pageUrlFilter', 'durationMs', 'script', 'maxEvents'] },
445
+ { command: 'forward', domain: 'advanced', summary: 'Create the ADB port forward for the bridge.', targetApp: true, options: ['serial', 'packageName', 'port'] },
446
+ { command: 'remove-forward', domain: 'advanced', summary: 'Remove the ADB port forward for the bridge.', options: ['serial', 'port'] },
447
+ { command: 'smoke', domain: 'diagnostics', summary: 'Run the native sample smoke test.', options: ['serial', 'packageName', 'outFile', 'artifactDir', 'skipFlutterLaunch'] },
448
+ ];
449
+
450
+ const commandByName = new Map(commandDefinitions.map((definition) => [definition.command, definition]));
451
+
353
452
  async function callTool(name, args) {
453
+ if (name === 'capabilities') {
454
+ return toolJson(capabilityPayload(args));
455
+ }
456
+ if (name === 'run') {
457
+ return runGeneric(args);
458
+ }
354
459
  if (name === 'run_smoke') {
355
460
  return runSmoke(args);
356
461
  }
357
- if (name === 'input_text' && !args.packageName) {
358
- return toolText('packageName is required for input_text so text input is routed to the intended app bridge.', true);
359
- }
360
462
  const commandMap = {
361
463
  flutter_tree: 'flutter-tree',
362
464
  h5_dom: 'h5-dom',
@@ -394,10 +496,74 @@ async function callTool(name, args) {
394
496
  appops_set: 'appops-set',
395
497
  tap_uia_text: 'tap-uia-text',
396
498
  permission_dialog: 'permission-dialog',
397
- };
398
- const command = commandMap[name] || name;
399
- return runBridge(command, args);
400
- }
499
+ };
500
+ const command = commandMap[name] || name;
501
+ return runBridgeChecked(command, args);
502
+ }
503
+
504
+ function capabilityPayload(args = {}) {
505
+ const includeOptions = Boolean(args.includeOptions);
506
+ const requestedCommand = args.command ? normalizeCommandName(args.command) : '';
507
+ if (requestedCommand) {
508
+ const definition = commandByName.get(requestedCommand);
509
+ return {
510
+ ok: Boolean(definition),
511
+ command: requestedCommand,
512
+ ...(definition ? shapeCommandDefinition(definition, true) : { error: 'unknown_command' }),
513
+ };
514
+ }
515
+
516
+ const requestedDomain = args.domain ? String(args.domain) : '';
517
+ const domains = {};
518
+ for (const definition of commandDefinitions) {
519
+ if (requestedDomain && definition.domain !== requestedDomain) continue;
520
+ if (!domains[definition.domain]) domains[definition.domain] = [];
521
+ domains[definition.domain].push(shapeCommandDefinition(definition, includeOptions));
522
+ }
523
+ return {
524
+ ok: true,
525
+ surface: mcpSurface === 'full' || mcpSurface === 'legacy' ? 'full' : 'compact',
526
+ usage: 'Use run with one of these command names. Prefer packageName for app-specific commands; install-apk, launch-app, UI, WebView, logcat, network, and permission workflows are supported.',
527
+ domains,
528
+ };
529
+ }
530
+
531
+ function shapeCommandDefinition(definition, includeOptions) {
532
+ return {
533
+ command: definition.command,
534
+ summary: definition.summary,
535
+ targetApp: Boolean(definition.targetApp),
536
+ ...(includeOptions ? { options: definition.options || [] } : {}),
537
+ };
538
+ }
539
+
540
+ async function runGeneric(args = {}) {
541
+ const command = normalizeCommandName(args.command);
542
+ if (!commandByName.has(command)) {
543
+ return toolText(`unknown command: ${args.command || ''}`, true);
544
+ }
545
+ const commandArgs = {
546
+ ...(args.arguments && typeof args.arguments === 'object' ? args.arguments : {}),
547
+ };
548
+ for (const key of ['adb', 'serial', 'port', 'packageName']) {
549
+ if (args[key] !== undefined && commandArgs[key] === undefined) {
550
+ commandArgs[key] = args[key];
551
+ }
552
+ }
553
+ return runBridgeChecked(command, commandArgs);
554
+ }
555
+
556
+ function normalizeCommandName(value) {
557
+ return String(value || '').trim().replace(/_/g, '-');
558
+ }
559
+
560
+ function runBridgeChecked(command, args = {}) {
561
+ const definition = commandByName.get(command);
562
+ if (definition?.targetApp && !args.packageName && !args.port) {
563
+ return toolText(`${command}: packageName or explicit port is required in MCP mode so the command cannot fall back to a default package.`, true);
564
+ }
565
+ return runBridge(command, args);
566
+ }
401
567
 
402
568
  async function runBridge(command, args) {
403
569
  const cliArgs = [cliScript, command];
@@ -576,21 +742,25 @@ function retryWithPackageNameHint(cliArgs, stdout, stderr, code) {
576
742
  return 'agentHint: Default bridge port 18080 accepted the connection but did not answer. If you know the target app, retry this tool with packageName so the CLI can discover that app bridge port.';
577
743
  }
578
744
 
579
- function toolText(text, isError = false) {
580
- return {
581
- content: [
582
- {
583
- type: 'text',
745
+ function toolText(text, isError = false) {
746
+ return {
747
+ content: [
748
+ {
749
+ type: 'text',
584
750
  text,
585
751
  },
586
752
  ],
587
- isError,
588
- };
589
- }
590
-
591
- function sendResult(id, result) {
592
- send({ jsonrpc: '2.0', id, result });
593
- }
753
+ isError,
754
+ };
755
+ }
756
+
757
+ function toolJson(value, isError = false) {
758
+ return toolText(JSON.stringify(value, null, 2), isError);
759
+ }
760
+
761
+ function sendResult(id, result) {
762
+ send({ jsonrpc: '2.0', id, result });
763
+ }
594
764
 
595
765
  function sendError(id, code, message) {
596
766
  send({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mobileaidev/ai-app-bridge",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Desktop CLI and MCP server for AI App Bridge.",
5
5
  "repository": {
6
6
  "type": "git",