@mobileaidev/ai-app-bridge 0.2.8 → 0.2.10

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
@@ -66,13 +66,15 @@ steps as skipped without mixing results from different commands:
66
66
  }
67
67
  ```
68
68
 
69
- For dynamic screens, MCP agents should follow the `ai-app-bridge-use` cadence:
70
- thaw immediately before reading app content or performing an app action, freeze
71
- immediately after evidence or action results are captured, keep the app frozen
72
- while reasoning/planning, and thaw once more before finishing the overall task.
73
- For visible state changes such as panels, dialogs, page transitions, tabs, or
74
- button-triggered content, verify with both `screenshot` and `tree`/`uia-tree`;
75
- do not conclude success from UI tree alone.
69
+ For dynamic or transient screens, MCP agents can use `freeze-app`/`thaw-app` as
70
+ an optional stabilization control: thaw before reads, actions, waits, or
71
+ captures; freeze after evidence capture only when a changing UI would make
72
+ reasoning unreliable; and thaw before the next app operation or before
73
+ finishing so the app is not left frozen. Static screens and ordinary form
74
+ flows usually do not need freezing.
75
+ For visible state changes such as panels, dialogs, page transitions, tabs, or
76
+ button-triggered content, verify with both `screenshot` and `tree`/`uia-tree`;
77
+ do not conclude success from UI tree alone.
76
78
 
77
79
  WebView network and console capture use Android WebView DevTools/CDP when the
78
80
  target app is debuggable and WebView debugging is enabled.
File without changes
package/bin/mcp-server.js CHANGED
@@ -1,12 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { spawn } = require('child_process');
4
- const path = require('path');
5
-
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;
3
+ const { spawn } = require('child_process');
4
+ const path = require('path');
5
+
6
+ const packageInfo = require('../package.json');
7
+ const { WebBridgeProvider } = require('./web-provider');
8
+ const bridgeDir = __dirname;
9
+ const cliScript = path.join(bridgeDir, 'ai-app-bridge.js');
10
+ const nodeBinary = process.env.AI_APP_BRIDGE_NODE || process.execPath;
10
11
  const supportedProtocolVersions = ['2025-06-18', '2024-11-05'];
11
12
  const defaultProtocolVersion = supportedProtocolVersions[0];
12
13
  const mcpSurface = (process.env.AI_APP_BRIDGE_MCP_SURFACE || 'compact').toLowerCase();
@@ -14,8 +15,9 @@ const serverInstructions = [
14
15
  '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
16
  'Default surface is compact: call capabilities to discover domains, then call run with a command and arguments.',
16
17
  '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(' ');
18
+ 'Use freeze-app/thaw-app only as an optional stabilization control for dynamic or transient screens: thaw before reads/actions/captures, freeze after evidence capture only when it helps reasoning, and thaw before the next operation or before finishing so the app is not left frozen.',
19
+ ].join(' ');
20
+ const webProvider = new WebBridgeProvider();
19
21
 
20
22
  let buffer = Buffer.alloc(0);
21
23
  let responseFormat = null;
@@ -257,10 +259,10 @@ function fullToolDefinitions() {
257
259
  deltaY: { type: 'number', description: 'Window scroll delta Y when no selector/text is supplied.' },
258
260
  }),
259
261
  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.', {
262
+ bridgeTool('freeze_app', 'Optionally stop target app processes with SIGSTOP when a dynamic or transient screen needs stable evidence for review.', {
261
263
  pid: { type: 'string', description: 'Optional explicit process id. Defaults to all processes named packageName or packageName:*.' },
262
264
  }, ['packageName']),
263
- bridgeTool('thaw_app', 'Resume the target app processes with SIGCONT before reading app content or dispatching actions, so bridge endpoints can answer.', {
265
+ bridgeTool('thaw_app', 'Resume target app processes with SIGCONT before reads, waits, captures, or actions, and before finishing any task that used freeze-app.', {
264
266
  pid: { type: 'string', description: 'Optional explicit process id. Defaults to all processes named packageName or packageName:*.' },
265
267
  }, ['packageName']),
266
268
  bridgeTool('logcat', 'Read Android logcat through ADB with optional pid/tag/level/grep filters.', {
@@ -458,8 +460,8 @@ const commandDefinitions = [
458
460
  { 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
461
  { 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
462
  { 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: 'freeze-app', domain: 'app', summary: 'Optionally stop target app processes with SIGSTOP when dynamic UI needs stable evidence.', targetApp: true, options: ['serial', 'packageName', 'pid'] },
464
+ { command: 'thaw-app', domain: 'app', summary: 'Resume target app processes with SIGCONT before reads, waits, captures, actions, or final handoff.', targetApp: true, options: ['serial', 'packageName', 'pid'] },
463
465
  { 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
466
  { 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
467
  { command: 'launch-native-test', domain: 'app', summary: 'Launch the debug native bridge test Activity.', targetApp: true, options: ['serial', 'packageName'] },
@@ -498,8 +500,23 @@ const commandDefinitions = [
498
500
  { 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
501
  { command: 'webview-pages', domain: 'webview', summary: 'List attachable Android WebView DevTools/CDP pages.', targetApp: true, options: ['serial', 'packageName', 'webviewPort', 'socketName', 'targetId', 'pageUrlFilter', 'keepForward'] },
500
502
  { 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: 'webview-console', domain: 'webview', summary: 'Capture WebView console/log events through CDP.', targetApp: true, options: ['serial', 'packageName', 'webviewPort', 'socketName', 'targetId', 'pageUrlFilter', 'durationMs', 'script', 'maxEvents'] },
504
+ { command: 'web-provider-status', domain: 'web', summary: 'Read desktop Web Bridge provider status.', options: [] },
505
+ { command: 'web-session-start', domain: 'web', summary: 'Start the desktop Web Bridge WebSocket session server.', options: ['host', 'webPort', 'path', 'token'] },
506
+ { command: 'web-connect-info', domain: 'web', summary: 'Read the Web Bridge endpoint and token for SDK clients.', options: [] },
507
+ { command: 'web-sessions', domain: 'web', summary: 'List connected Web Bridge SDK sessions.', options: [] },
508
+ { command: 'web-status', domain: 'web', summary: 'Read status and capture counts for a Web Bridge session.', targetKind: 'web-target', options: ['sessionId'] },
509
+ { command: 'web-dom', domain: 'web', summary: 'Read or refresh a Web Bridge DOM snapshot.', targetKind: 'web-target', options: ['sessionId', 'targetId', 'selector', 'refresh', 'timeoutMs'] },
510
+ { command: 'web-logs', domain: 'web', summary: 'Read Web Bridge log records.', targetKind: 'web-target', options: ['sessionId', 'sinceId', 'sinceMs', 'limit'] },
511
+ { command: 'web-network', domain: 'web', summary: 'Read Web Bridge network records.', targetKind: 'web-target', options: ['sessionId', 'sinceId', 'sinceMs', 'limit'] },
512
+ { command: 'web-state', domain: 'web', summary: 'Read Web Bridge state records.', targetKind: 'web-target', options: ['sessionId', 'sinceId', 'sinceMs', 'limit'] },
513
+ { command: 'web-events', domain: 'web', summary: 'Read Web Bridge event records.', targetKind: 'web-target', options: ['sessionId', 'sinceId', 'sinceMs', 'limit'] },
514
+ { command: 'web-command', domain: 'web', summary: 'Run a whitelisted command in a connected Web Bridge SDK session.', targetKind: 'web-target', options: ['sessionId', 'targetId', 'name', 'arguments', 'timeoutMs'] },
515
+ { command: 'web-click', domain: 'web', summary: 'Click a DOM element through the Web Bridge SDK command path.', targetKind: 'web-target', options: ['sessionId', 'targetId', 'selector', 'targetText', 'timeoutMs'] },
516
+ { command: 'web-input', domain: 'web', summary: 'Set text in a DOM input through the Web Bridge SDK command path.', targetKind: 'web-target', options: ['sessionId', 'targetId', 'selector', 'value', 'timeoutMs'] },
517
+ { command: 'web-wait', domain: 'web', summary: 'Wait for text or selector through the Web Bridge SDK command path.', targetKind: 'web-target', options: ['sessionId', 'targetId', 'selector', 'targetText', 'timeoutMs'] },
518
+ { command: 'web-scroll', domain: 'web', summary: 'Scroll a Web Bridge DOM target.', targetKind: 'web-target', options: ['sessionId', 'targetId', 'selector', 'deltaX', 'deltaY', 'timeoutMs'] },
519
+ { command: 'forward', domain: 'advanced', summary: 'Create the ADB port forward for the bridge.', targetApp: true, options: ['serial', 'packageName', 'port'] },
503
520
  { command: 'remove-forward', domain: 'advanced', summary: 'Remove the ADB port forward for the bridge.', options: ['serial', 'port'] },
504
521
  { command: 'batch', domain: 'advanced', summary: 'Run multiple AI App Bridge commands serially in one MCP call.', options: ['defaults', 'steps', 'stopOnError', 'includeRaw', 'maxRawChars'] },
505
522
  { command: 'smoke', domain: 'diagnostics', summary: 'Run the native sample smoke test.', options: ['serial', 'packageName', 'outFile', 'artifactDir', 'skipFlutterLaunch'] },
@@ -589,14 +606,15 @@ function capabilityPayload(args = {}) {
589
606
  };
590
607
  }
591
608
 
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
- }
609
+ function shapeCommandDefinition(definition, includeOptions) {
610
+ return {
611
+ command: definition.command,
612
+ summary: definition.summary,
613
+ targetApp: Boolean(definition.targetApp),
614
+ targetKind: definition.targetKind || (definition.targetApp ? 'android-app' : 'none'),
615
+ ...(includeOptions ? { options: definition.options || [] } : {}),
616
+ };
617
+ }
600
618
 
601
619
  async function runGeneric(args = {}) {
602
620
  const command = normalizeCommandName(args.command);
@@ -606,10 +624,10 @@ async function runGeneric(args = {}) {
606
624
  const commandArgs = {
607
625
  ...(args.arguments && typeof args.arguments === 'object' ? args.arguments : {}),
608
626
  };
609
- for (const key of ['adb', 'serial', 'port', 'packageName']) {
610
- if (args[key] !== undefined && commandArgs[key] === undefined) {
611
- commandArgs[key] = args[key];
612
- }
627
+ for (const key of ['adb', 'serial', 'port', 'packageName', 'sessionId', 'targetId', 'webPort']) {
628
+ if (args[key] !== undefined && commandArgs[key] === undefined) {
629
+ commandArgs[key] = args[key];
630
+ }
613
631
  }
614
632
  if (command === 'batch') {
615
633
  return runBatch(commandArgs);
@@ -621,19 +639,27 @@ function normalizeCommandName(value) {
621
639
  return String(value || '').trim().replace(/_/g, '-');
622
640
  }
623
641
 
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
- }
642
+ function runBridgeChecked(command, args = {}) {
643
+ const definition = commandByName.get(command);
644
+ if (definition?.domain === 'web') {
645
+ return runWebChecked(command, args);
646
+ }
647
+ if (command === 'clear-app-data' && !args.packageName) {
648
+ return toolText('clear-app-data: packageName is required in MCP mode so the command cannot clear a default package.', true);
649
+ }
628
650
  if ((command === 'freeze-app' || command === 'thaw-app') && !args.packageName) {
629
651
  return toolText(`${command}: packageName is required in MCP mode so the command cannot signal a default package.`, true);
630
652
  }
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
- }
653
+ if (definition?.targetApp && !args.packageName && !args.port) {
654
+ return toolText(`${command}: packageName or explicit port is required in MCP mode so the command cannot fall back to a default package.`, true);
655
+ }
656
+ return runBridge(command, args);
657
+ }
658
+
659
+ async function runWebChecked(command, args = {}) {
660
+ const result = await webProvider.run(command, args);
661
+ return toolJson(result, result?.ok === false);
662
+ }
637
663
 
638
664
  async function runBatch(args = {}, runner = runBridgeChecked) {
639
665
  const startedAtMs = Date.now();
@@ -654,10 +680,10 @@ async function runBatch(args = {}, runner = runBridgeChecked) {
654
680
  }
655
681
 
656
682
  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
- }
683
+ for (const key of ['adb', 'serial', 'port', 'packageName', 'artifactDir', 'sessionId', 'targetId', 'webPort']) {
684
+ if (args[key] !== undefined && defaults[key] === undefined) {
685
+ defaults[key] = args[key];
686
+ }
661
687
  }
662
688
 
663
689
  const normalizedSteps = [];
@@ -706,10 +732,10 @@ async function runBatch(args = {}, runner = runBridgeChecked) {
706
732
  ...defaults,
707
733
  ...(step.arguments && typeof step.arguments === 'object' ? step.arguments : {}),
708
734
  };
709
- for (const key of ['adb', 'serial', 'port', 'packageName']) {
710
- if (step[key] !== undefined) {
711
- stepArgs[key] = step[key];
712
- }
735
+ for (const key of ['adb', 'serial', 'port', 'packageName', 'sessionId', 'targetId', 'webPort']) {
736
+ if (step[key] !== undefined) {
737
+ stepArgs[key] = step[key];
738
+ }
713
739
  }
714
740
  try {
715
741
  const toolResult = await runner(step.command, stepArgs);
@@ -717,11 +743,13 @@ async function runBatch(args = {}, runner = runBridgeChecked) {
717
743
  const passed = !parsed.isError && parsed.payload?.ok !== false;
718
744
  const stepResult = {
719
745
  id: step.id,
720
- command: step.command,
721
- status: passed ? 'passed' : 'failed',
722
- ok: passed,
723
- packageName: stepArgs.packageName,
724
- port: stepArgs.port,
746
+ command: step.command,
747
+ status: passed ? 'passed' : 'failed',
748
+ ok: passed,
749
+ packageName: stepArgs.packageName,
750
+ sessionId: stepArgs.sessionId,
751
+ targetId: stepArgs.targetId,
752
+ port: stepArgs.port,
725
753
  durationMs: Date.now() - stepStartedAtMs,
726
754
  summary: summarizeToolPayload(parsed),
727
755
  };
@@ -801,8 +829,11 @@ function summarizeToolPayload(parsed) {
801
829
  ok: payload.ok,
802
830
  error: payload.error || null,
803
831
  };
804
- if (payload.packageName) summary.packageName = payload.packageName;
805
- if (payload.app?.packageName) summary.app = payload.app.packageName;
832
+ if (payload.packageName) summary.packageName = payload.packageName;
833
+ if (payload.sessionId) summary.sessionId = payload.sessionId;
834
+ if (payload.targetId) summary.targetId = payload.targetId;
835
+ if (payload.session?.sessionId) summary.sessionId = payload.session.sessionId;
836
+ if (payload.app?.packageName) summary.app = payload.app.packageName;
806
837
  if (payload.activity) summary.activity = payload.activity;
807
838
  if (payload.component) summary.component = payload.component;
808
839
  if (payload.transport) summary.transport = payload.transport;
@@ -814,8 +845,9 @@ function summarizeToolPayload(parsed) {
814
845
  port: payload.debugBridge.port,
815
846
  };
816
847
  }
817
- if (payload.count !== undefined) summary.count = payload.count;
818
- if (payload.nodeCount !== undefined) summary.nodeCount = payload.nodeCount;
848
+ if (payload.count !== undefined) summary.count = payload.count;
849
+ if (payload.sessionCount !== undefined) summary.sessionCount = payload.sessionCount;
850
+ if (payload.nodeCount !== undefined) summary.nodeCount = payload.nodeCount;
819
851
  if (Array.isArray(payload.items)) summary.items = payload.items.length;
820
852
  if (payload.values && typeof payload.values === 'object') {
821
853
  summary.values = Object.keys(payload.values).length;