@mobileaidev/ai-app-bridge 0.2.15 → 0.3.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +7 -0
  3. package/README.md +306 -53
  4. package/bin/ai-app-bridge.js +51 -4504
  5. package/bin/android-permissions.js +152 -0
  6. package/bin/android-uia-xml.js +74 -0
  7. package/bin/artifact-paths.js +127 -1
  8. package/bin/bridge-forward.js +56 -0
  9. package/bin/command-discovery.js +86 -0
  10. package/bin/command-errors.js +60 -0
  11. package/bin/command-registry.js +504 -0
  12. package/bin/command-request.js +14 -0
  13. package/bin/command-router.js +80 -0
  14. package/bin/connection-cache.js +119 -0
  15. package/bin/device-provider.js +2934 -0
  16. package/bin/execution-host.js +596 -0
  17. package/bin/execution-runtime.js +101 -0
  18. package/bin/fact-codec.js +321 -0
  19. package/bin/fact-recorder.js +691 -0
  20. package/bin/fact-store.js +226 -0
  21. package/bin/feedback-probe.js +285 -0
  22. package/bin/intent/install-intent.js +267 -0
  23. package/bin/intent/intent-action-executor.js +129 -0
  24. package/bin/intent/intent-autonomous-adapter.js +46 -0
  25. package/bin/intent/intent-capture-port.js +64 -0
  26. package/bin/intent/intent-entry.js +226 -0
  27. package/bin/intent/intent-errors.js +28 -0
  28. package/bin/intent/intent-evidence-store.js +121 -0
  29. package/bin/intent/intent-lifetime.js +62 -0
  30. package/bin/intent/intent-observation-target.js +33 -0
  31. package/bin/intent/intent-observer.js +194 -0
  32. package/bin/intent/intent-production-adapter.js +334 -0
  33. package/bin/intent/intent-provider.js +27 -0
  34. package/bin/intent/intent-runtime.js +63 -0
  35. package/bin/intent/intent-worker.js +432 -0
  36. package/bin/intent/ios-intent-adapter.js +90 -0
  37. package/bin/intent/permission-intent.js +239 -0
  38. package/bin/intent/web-intent-adapter.js +31 -0
  39. package/bin/ios-device-outcome.js +47 -0
  40. package/bin/ios-execution.js +108 -0
  41. package/bin/ios-provider.js +497 -583
  42. package/bin/ios-runtime-binding.js +54 -0
  43. package/bin/ios-wda-execution.js +70 -0
  44. package/bin/ios-wda-port.js +98 -0
  45. package/bin/ios-wda-project.js +79 -0
  46. package/bin/mcp-server.js +83 -1040
  47. package/bin/mmap-scan-index.js +329 -0
  48. package/bin/observation-collector.js +862 -0
  49. package/bin/runtime-client.js +154 -0
  50. package/bin/runtime-directory.js +107 -0
  51. package/bin/runtime-protocol.js +36 -0
  52. package/bin/script/bounded-script-registry.js +103 -0
  53. package/bin/script/node-runtime-adapter.js +233 -0
  54. package/bin/script/progress-projector.js +63 -0
  55. package/bin/script/python-runtime-adapter.js +111 -0
  56. package/bin/script/rolling-summary.js +134 -0
  57. package/bin/script/script-agent-port.js +40 -0
  58. package/bin/script/script-assert.js +95 -0
  59. package/bin/script/script-capture-port.js +76 -0
  60. package/bin/script/script-catalog.js +85 -0
  61. package/bin/script/script-durable-restore.js +195 -0
  62. package/bin/script/script-entry-code.js +26 -0
  63. package/bin/script/script-entry-route.js +38 -0
  64. package/bin/script/script-entry.js +3 -0
  65. package/bin/script/script-errors.js +29 -0
  66. package/bin/script/script-evidence-store.js +22 -0
  67. package/bin/script/script-format-removed.js +26 -0
  68. package/bin/script/script-host-port.js +397 -0
  69. package/bin/script/script-ledger.js +64 -0
  70. package/bin/script/script-result.js +57 -0
  71. package/bin/script/script-sdk.js +152 -0
  72. package/bin/script/script-sdk.py +153 -0
  73. package/bin/script/script-session-channel.js +127 -0
  74. package/bin/script/script-spec.js +86 -0
  75. package/bin/script/script-supervisor.js +919 -0
  76. package/bin/script/templates/checkpoint-reentry.js +13 -0
  77. package/bin/segment-index.js +481 -0
  78. package/bin/segmented-fact-store.js +1571 -0
  79. package/bin/shared-kernel/android-h5-target.js +10 -0
  80. package/bin/shared-kernel/android-install-execution.js +176 -0
  81. package/bin/shared-kernel/android-sdk-endpoint.js +42 -0
  82. package/bin/shared-kernel/android-shell-execution.js +195 -0
  83. package/bin/shared-kernel/argument-schema.js +117 -0
  84. package/bin/shared-kernel/canonical-path.js +17 -0
  85. package/bin/shared-kernel/device-acknowledgements.js +53 -0
  86. package/bin/shared-kernel/device-completion-history.js +52 -0
  87. package/bin/shared-kernel/device-mutation-lease.js +219 -0
  88. package/bin/shared-kernel/device-ownership-recovery.js +95 -0
  89. package/bin/shared-kernel/device-ownership-store.js +94 -0
  90. package/bin/shared-kernel/evidence-adapters.js +251 -0
  91. package/bin/shared-kernel/evidence-archive.js +329 -0
  92. package/bin/shared-kernel/evidence-recording.js +131 -0
  93. package/bin/shared-kernel/evidence-schema.js +194 -0
  94. package/bin/shared-kernel/evidence-store.js +190 -0
  95. package/bin/shared-kernel/execution-admission.js +22 -0
  96. package/bin/shared-kernel/execution-contracts.js +171 -0
  97. package/bin/shared-kernel/execution-io.js +106 -0
  98. package/bin/shared-kernel/execution-ledger.js +125 -0
  99. package/bin/shared-kernel/execution-scope.js +87 -0
  100. package/bin/shared-kernel/execution-target.js +115 -0
  101. package/bin/shared-kernel/flutter-execution.js +13 -0
  102. package/bin/shared-kernel/flutter-h5-port.js +60 -0
  103. package/bin/shared-kernel/flutter-h5-target.js +9 -0
  104. package/bin/shared-kernel/flutter-target.js +75 -0
  105. package/bin/shared-kernel/h5-execution.js +11 -0
  106. package/bin/shared-kernel/h5-target.js +31 -0
  107. package/bin/shared-kernel/host-fact-store.js +49 -0
  108. package/bin/shared-kernel/ios-h5-target.js +9 -0
  109. package/bin/shared-kernel/ios-native-target.js +71 -0
  110. package/bin/shared-kernel/live-capture-query.js +115 -0
  111. package/bin/shared-kernel/managed-sdk-execution.js +78 -0
  112. package/bin/shared-kernel/native-execution.js +13 -0
  113. package/bin/shared-kernel/native-target.js +156 -0
  114. package/bin/shared-kernel/provider-command-contracts.js +55 -0
  115. package/bin/shared-kernel/recorded-payload-archive.js +195 -0
  116. package/bin/shared-kernel/request-context.js +35 -0
  117. package/bin/shared-kernel/semantic-node.js +55 -0
  118. package/bin/shared-kernel/summary-transformer.js +352 -0
  119. package/bin/shared-kernel/target-lease-protocol.js +47 -0
  120. package/bin/shared-kernel/text-wait.js +111 -0
  121. package/bin/shared-kernel/uia-execution.js +96 -0
  122. package/bin/shared-kernel/uia-protocol.js +214 -0
  123. package/bin/shared-kernel/uia-runtime-port.js +377 -0
  124. package/bin/shared-kernel/uia-target.js +39 -0
  125. package/bin/shared-kernel/web-dom-target.js +44 -0
  126. package/bin/shared-kernel/xml-attributes.js +25 -0
  127. package/bin/target-execution.js +275 -0
  128. package/bin/web/command-schema.js +60 -0
  129. package/bin/web/session-store.js +157 -0
  130. package/bin/web-provider.js +334 -553
  131. package/docs/COMMAND_CONTRACT.md +1563 -0
  132. package/docs/EVIDENCE_ARCHIVE.md +214 -0
  133. package/docs/INTENT_FOREGROUND.md +71 -0
  134. package/docs/INTENT_NATIVE_EDITING.md +79 -0
  135. package/docs/RELEASE.md +59 -0
  136. package/docs/SCRIPT_AUTHORING.md +489 -0
  137. package/node_modules/@mobileaidev/segmented-fact-store-native/LICENSE +201 -0
  138. package/node_modules/@mobileaidev/segmented-fact-store-native/NOTICE +7 -0
  139. package/node_modules/@mobileaidev/segmented-fact-store-native/binding.gyp +36 -0
  140. package/node_modules/@mobileaidev/segmented-fact-store-native/bindings/node/sfs_node.c +597 -0
  141. package/node_modules/@mobileaidev/segmented-fact-store-native/include/sfs.h +178 -0
  142. package/node_modules/@mobileaidev/segmented-fact-store-native/index.js +5 -0
  143. package/node_modules/@mobileaidev/segmented-fact-store-native/package.json +24 -0
  144. package/node_modules/@mobileaidev/segmented-fact-store-native/src/sfs.c +2349 -0
  145. package/package.json +60 -5
  146. package/runtime/ios-wda/AABWDABinding.h +19 -0
  147. package/runtime/ios-wda/AABWDABinding.m +97 -0
  148. package/runtime/ios-wda/AABWDAExecution.h +26 -0
  149. package/runtime/ios-wda/AABWDAExecution.m +172 -0
  150. package/runtime/ios-wda/AABWDAIntegration.h +71 -0
  151. package/runtime/ios-wda/AABWDAManagedRoutes.h +392 -0
  152. package/runtime/ios-wda/AABWDAReceiptStore.h +10 -0
  153. package/runtime/ios-wda/AABWDAReceiptStore.m +116 -0
  154. package/runtime/uia/ai-app-bridge-uia.jar +0 -0
  155. package/runtime/uia/manifest.json +22 -0
  156. package/skills/ai-app-bridge-use/SKILL.md +23 -360
package/bin/mcp-server.js CHANGED
@@ -1,46 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { spawn } = require('child_process');
4
- const path = require('path');
5
-
6
3
  const packageInfo = require('../package.json');
7
- const { IOSBridgeProvider } = require('./ios-provider');
8
- const { WebBridgeProvider } = require('./web-provider');
9
- const { defaultArtifactDirectory } = require('./artifact-paths');
10
- const bridgeDir = __dirname;
11
- const cliScript = path.join(bridgeDir, 'ai-app-bridge.js');
12
- const nodeBinary = process.env.AI_APP_BRIDGE_NODE || process.execPath;
4
+ const runtimeClient = require('./runtime-client');
5
+ const discovery = require('./command-discovery');
6
+ const clientConnection = new AbortController();
7
+ const { supportedTargets, commandDomains, supportedTargetsText, commandDomainsText, discoveryText } = discovery;
13
8
  const supportedProtocolVersions = ['2025-06-18', '2024-11-05'];
14
9
  const defaultProtocolVersion = supportedProtocolVersions[0];
15
- const mcpSurface = (process.env.AI_APP_BRIDGE_MCP_SURFACE || 'compact').toLowerCase();
16
- const supportedTargets = [
17
- 'Android native apps',
18
- 'Android WebView/H5/CDP',
19
- 'Flutter apps on Android and iOS',
20
- 'iOS native apps via AiAppBridgeIOS + WebDriverAgent/XCUITest',
21
- 'WKWebView',
22
- 'desktop Web Bridge sessions',
23
- ];
24
- const commandDomains = {
25
- core: 'status, tree, uia-tree, screenshot, logs, network, state, events',
26
- app: 'install-apk, clear-app-data, launch-*, freeze-app/thaw-app, permission-*, appops-set',
27
- action: 'tap, tap-text, tap-uia-text, input-text, swipe, keyevent, wait-text, keyboard-state, hide-keyboard',
28
- flutter: 'flutter-tree, flutter-nodes, flutter-action, tap/input/scroll Flutter controls',
29
- webview: 'h5-*, flutter-h5-*, webview-pages, webview-network, webview-console',
30
- ios: 'ios-devices, ios-doctor, ios-setup, ios runtime evidence, ios-uia-tree/tap/input/swipe, ios-h5-*, ios-flutter-*',
31
- web: 'web-session-start, web-sessions, web-status, web-dom, web-logs, web-network, web-state, web-events, web-command, web-click, web-input, web-wait, web-scroll',
32
- diagnostics: 'logcat, smoke',
33
- advanced: 'batch, forward, remove-forward',
34
- };
35
- const supportedTargetsText = `AI App Bridge supports ${supportedTargets.join('; ')}.`;
36
- const commandDomainsText = `Command domains: ${Object.entries(commandDomains).map(([domain, summary]) => `${domain}(${summary})`).join('; ')}.`;
37
- const discoveryText = 'Default MCP surface is compact: call capabilities to discover domains, commands, and options, then call run with the selected command and arguments.';
38
10
  const serverInstructions = [
11
+ 'Intent, Script and individual commands share one runtime across CLI and MCP. Disconnecting a client leaves operations running; explicit task cancel or runtime stop owns cancellation. Platform capabilities do not imply full complex-App acceptance.',
12
+ 'Use script status/wait for progress and resultRef; read the final output with script operation=result and the same operationId, including after runtime restart. A completed execution is separate from the business verdict.',
39
13
  supportedTargetsText,
40
14
  commandDomainsText,
41
15
  discoveryText,
42
16
  'Prefer AI App Bridge over raw adb, devicectl, or browser-specific scripts when inspecting UI, text, WebView/WKWebView, logs, network, app install, launch, permissions, or app-level Web evidence.',
43
- 'Always pass packageName for Android app-specific commands, or pass an explicit port. For iOS, pass bundleId plus deviceId when more than one iPhone is connected.',
17
+ 'Always pass packageName for Android app-specific commands. Port only selects the host forwarding port. For iOS, pass bundleId plus deviceId when more than one iPhone is connected.',
18
+ 'WDA App actions require the exact deviceId, wdaRunnerBundleId and wdaSessionId. A forwarded wdaUrl selects transport only; discover per-command requirements through capabilities.',
44
19
  'For Web Bridge sessions, start the provider, connect the browser SDK, then pass sessionId and targetId when needed.',
45
20
  '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.',
46
21
  ].join(' ');
@@ -51,8 +26,7 @@ ${supportedTargetsText}
51
26
  ${commandDomainsText}
52
27
 
53
28
  MCP surface:
54
- compact (default) exposes only capabilities and run.
55
- full/legacy exposes one tool per command; set AI_APP_BRIDGE_MCP_SURFACE=full.
29
+ capabilities and run are the only tools. Per-command aliases were removed.
56
30
 
57
31
  Discovery:
58
32
  1. Call capabilities with optional domain or command filters.
@@ -60,20 +34,22 @@ Discovery:
60
34
  3. Put command-specific options in arguments.
61
35
 
62
36
  Target ids:
63
- Android app commands require packageName or explicit port.
64
- iOS app commands use bundleId; add deviceId when multiple devices exist and wdaUrl for WDA actions.
37
+ Android app commands require packageName; port selects only the host forwarding port.
38
+ iOS app commands use bundleId and the deviceId required by capabilities.
39
+ WDA App actions require deviceId, wdaRunnerBundleId and wdaSessionId; wdaUrl is transport only.
65
40
  Web Bridge commands use sessionId; add targetId for multi-target pages.
66
41
 
67
42
  Examples:
68
43
  capabilities { "domain": "webview", "includeOptions": true }
69
- run { "command": "screenshot", "packageName": "com.example.app" }
44
+ run { "command": "screenshot", "arguments": { "packageName": "com.example.app" } }
70
45
  run { "command": "web-session-start", "arguments": { "webPort": 18180 } }
71
46
  `;
72
- const iosProvider = new IOSBridgeProvider();
73
- const webProvider = new WebBridgeProvider();
74
-
75
47
  let buffer = Buffer.alloc(0);
76
48
  let responseFormat = null;
49
+ const activeMessages = new Set();
50
+ let stdinEnded = false;
51
+ let shutdownPromise = null;
52
+ let signalHandlersInstalled = false;
77
53
 
78
54
  function startServer() {
79
55
  process.stdin.on('data', (chunk) => {
@@ -82,6 +58,11 @@ function startServer() {
82
58
  });
83
59
 
84
60
  process.stdin.on('error', () => {});
61
+ process.stdin.on('end', () => {
62
+ stdinEnded = true;
63
+ maybeShutdownAfterMessages();
64
+ });
65
+ installSignalHandlers();
85
66
  }
86
67
 
87
68
  function drainMessages() {
@@ -92,12 +73,41 @@ function drainMessages() {
92
73
  }
93
74
  buffer = parsed.remaining;
94
75
  setResponseFormat(parsed.format);
95
- handleMessage(parsed.body).catch((error) => {
76
+ const active = handleMessage(parsed.body).catch((error) => {
96
77
  writeLog(`unhandled message error: ${error.stack || error}`);
78
+ }).finally(() => {
79
+ activeMessages.delete(active);
80
+ maybeShutdownAfterMessages();
97
81
  });
82
+ activeMessages.add(active);
98
83
  }
99
84
  }
100
85
 
86
+ function maybeShutdownAfterMessages() {
87
+ if (!stdinEnded) return;
88
+ void shutdownSharedResources();
89
+ }
90
+
91
+ function installSignalHandlers() {
92
+ if (signalHandlersInstalled) return;
93
+ signalHandlersInstalled = true;
94
+ for (const signal of ['SIGINT', 'SIGTERM']) {
95
+ process.once(signal, () => {
96
+ stdinEnded = true;
97
+ void shutdownSharedResources().finally(() => process.exit(0));
98
+ });
99
+ }
100
+ }
101
+
102
+ async function shutdownSharedResources() {
103
+ if (shutdownPromise) return shutdownPromise;
104
+ shutdownPromise = (async () => {
105
+ clientConnection.abort();
106
+ await Promise.allSettled([...activeMessages]);
107
+ })();
108
+ return shutdownPromise;
109
+ }
110
+
101
111
  function readNextMessage(source) {
102
112
  const text = source.toString('utf8');
103
113
  if (/^Content-Length:/i.test(text)) {
@@ -208,7 +218,7 @@ async function handleMessage(body) {
208
218
 
209
219
  if (message.method === 'tools/call') {
210
220
  const name = message.params?.name;
211
- const args = message.params?.arguments || {};
221
+ const args = message.params?.arguments === undefined ? {} : message.params.arguments;
212
222
  const result = await callTool(name, args);
213
223
  sendResult(message.id, result);
214
224
  return;
@@ -228,1013 +238,44 @@ function negotiateProtocolVersion(requestedVersion) {
228
238
  }
229
239
 
230
240
  function toolDefinitions() {
231
- if (mcpSurface === 'full' || mcpSurface === 'legacy') {
232
- return fullToolDefinitions();
233
- }
234
- return compactToolDefinitions();
235
- }
236
-
237
- function compactToolDefinitions() {
238
- return [
239
- bridgeTool('capabilities', `List AI App Bridge capability domains and commands across Android, iOS, Flutter, WebView/H5/CDP, and Web Bridge targets. ${commandDomainsText}`, {
240
- domain: { type: 'string', description: 'Optional domain filter: core, app, action, flutter, webview, ios, web, diagnostics, or advanced.' },
241
- command: { type: 'string', description: 'Optional command name for detailed arguments, such as install-apk, launch-app, tree, input-text, webview-network, ios-setup, ios-tap, web-session-start, or web-command.' },
242
- includeOptions: { type: 'boolean', description: 'Include per-command argument names. Defaults to false to keep output compact.' },
243
- }),
244
- bridgeTool('run', 'Run an AI App Bridge command from capabilities. Pass packageName/port for Android app commands, bundleId/deviceId for iOS app commands, and sessionId/targetId for Web Bridge commands.', {
245
- command: { type: 'string', description: 'Command name from capabilities, using CLI form such as status, install-apk, launch-app, input-text, webview-network, ios-doctor, ios-setup, ios-status, ios-tap, web-session-start, web-status, or web-command.' },
246
- packageName: { type: 'string', description: 'Target Android package for app-specific commands. Strongly recommended.' },
247
- serial: { type: 'string', description: 'ADB serial when multiple devices are connected.' },
248
- port: { type: 'number', description: 'Explicit bridge port when packageName discovery is not available.' },
249
- bundleId: { type: 'string', description: 'Target iOS app bundle identifier for ios-* commands.' },
250
- deviceId: { type: 'string', description: 'iOS devicectl identifier, UDID, serial number, or device name.' },
251
- sessionId: { type: 'string', description: 'Target Web Bridge SDK session id for web-* commands.' },
252
- targetId: { type: 'string', description: 'Optional Web Bridge target id for multi-target pages.' },
253
- iosHost: { type: 'string', description: 'iOS runtime host or CoreDevice tunnel IP.' },
254
- iosPort: { type: 'number', description: 'iOS runtime port when auto-discovery is unavailable.' },
255
- runtimeUrl: { type: 'string', description: 'Explicit iOS runtime base URL.' },
256
- wdaUrl: { type: 'string', description: 'WebDriverAgent base URL for iOS full-control commands.' },
257
- wdaSessionId: { type: 'string', description: 'Existing WebDriverAgent session id to reuse.' },
258
- wdaBundleId: { type: 'string', description: 'Unique WebDriverAgentRunner bundle id for signing.' },
259
- accessibilityId: { type: 'string', description: 'iOS accessibility identifier for element input.' },
260
- elementId: { type: 'string', description: 'Existing WDA element id for element input.' },
261
- adb: { type: 'string', description: 'ADB executable path or command.' },
262
- arguments: {
263
- type: 'object',
264
- description: 'Command-specific arguments from capabilities. Example: {"apkPath":"app-debug.apk","allowDowngrade":true}.',
265
- additionalProperties: true,
266
- },
267
- }, ['command']),
268
- ];
269
- }
270
-
271
- function fullToolDefinitions() {
272
241
  return [
273
- 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.', {
274
- full: { type: 'boolean', description: 'Return the full raw status payload, including large Flutter widget dumps.' },
275
- }),
276
- bridgeTool('tree', 'Read the Android View tree from the in-app bridge.'),
277
- bridgeTool('flutter_tree', 'Read the latest Flutter widget/layout snapshot.'),
278
- bridgeTool('flutter_nodes', 'Read Flutter operable nodes from the Flutter action bridge.'),
279
- bridgeTool('tap_flutter_text', 'Tap a Flutter node by visible text through the Flutter-aware bridge path.', {
280
- targetText: { type: 'string', description: 'Flutter node text to tap.' },
281
- }, ['targetText']),
282
- 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.', {
283
- text: { type: 'string', description: 'Text to set.' },
284
- tapX: { type: 'number', description: 'Optional physical X coordinate for the Flutter input target.' },
285
- tapY: { type: 'number', description: 'Optional physical Y coordinate for the Flutter input target.' },
286
- hideKeyboard: { type: 'boolean', description: 'Hide the soft keyboard after setting text.' },
287
- }, ['text']),
288
- bridgeTool('h5_dom', 'Read native Android WebView DOM from the current Activity.'),
289
- bridgeTool('h5_eval', 'Execute debug JavaScript in the current native Android WebView.', {
290
- script: { type: 'string' },
291
- }, ['script']),
292
- bridgeTool('h5_click', 'Click a native Android WebView DOM element by CSS selector or text.', h5TargetSchema(), []),
293
- bridgeTool('h5_input', 'Set text in a native Android WebView input by CSS selector or text.', {
294
- ...h5TargetSchema(),
295
- value: { type: 'string', description: 'Text value to set.' },
296
- }, ['value']),
297
- bridgeTool('h5_wait', 'Wait for a native Android WebView DOM element or body text.', {
298
- ...h5TargetSchema(),
299
- timeoutSec: { type: 'number', description: 'Maximum wait time. Defaults to 10 seconds.' },
300
- intervalMs: { type: 'number', description: 'Polling interval. Defaults to 500 ms.' },
301
- }),
302
- bridgeTool('h5_scroll', 'Scroll a native Android WebView or scroll a DOM element into view.', {
303
- ...h5TargetSchema(),
304
- deltaX: { type: 'number', description: 'Window scroll delta X when no selector/text is supplied.' },
305
- deltaY: { type: 'number', description: 'Window scroll delta Y when no selector/text is supplied.' },
306
- }),
307
- bridgeTool('flutter_h5_dom', 'Read DOM through a Flutter-registered H5 adapter.'),
308
- bridgeTool('flutter_h5_eval', 'Execute JavaScript through a Flutter-registered H5 adapter.', {
309
- script: { type: 'string' },
310
- }, ['script']),
311
- bridgeTool('flutter_h5_click', 'Click a Flutter H5 DOM element by CSS selector or text.', h5TargetSchema(), []),
312
- bridgeTool('flutter_h5_input', 'Set text in a Flutter H5 input by CSS selector or text.', {
313
- ...h5TargetSchema(),
314
- value: { type: 'string', description: 'Text value to set.' },
315
- }, ['value']),
316
- bridgeTool('flutter_h5_wait', 'Wait for a Flutter H5 DOM element or body text.', {
317
- ...h5TargetSchema(),
318
- timeoutSec: { type: 'number', description: 'Maximum wait time. Defaults to 10 seconds.' },
319
- intervalMs: { type: 'number', description: 'Polling interval. Defaults to 500 ms.' },
320
- }),
321
- bridgeTool('flutter_h5_scroll', 'Scroll a Flutter H5 document or DOM element into view.', {
322
- ...h5TargetSchema(),
323
- deltaX: { type: 'number', description: 'Window scroll delta X when no selector/text is supplied.' },
324
- deltaY: { type: 'number', description: 'Window scroll delta Y when no selector/text is supplied.' },
325
- }),
326
- bridgeTool('logs', 'Read generic in-app log records.'),
327
- bridgeTool('freeze_app', 'Optionally stop target app processes with SIGSTOP when a dynamic or transient screen needs stable evidence for review.', {
328
- pid: { type: 'string', description: 'Optional explicit process id. Defaults to all processes named packageName or packageName:*.' },
329
- }, ['packageName']),
330
- bridgeTool('thaw_app', 'Resume target app processes with SIGCONT before reads, waits, captures, or actions, and before finishing any task that used freeze-app.', {
331
- pid: { type: 'string', description: 'Optional explicit process id. Defaults to all processes named packageName or packageName:*.' },
332
- }, ['packageName']),
333
- bridgeTool('logcat', 'Read Android logcat through ADB with optional pid/tag/level/grep filters.', {
334
- pid: { type: 'string', description: 'Use "current" for the current app pid, or pass a numeric pid.' },
335
- appPid: { type: 'boolean', description: 'Filter by the current package pid.' },
336
- tag: { type: 'string', description: 'Comma-separated exact logcat tags.' },
337
- level: { type: 'string', description: 'Minimum Android log level: V,D,I,W,E,F.' },
338
- grep: { type: 'string', description: 'Substring filter applied after pid/tag/level.' },
339
- lines: { type: 'number', description: 'Input logcat tail line count before filtering.' },
340
- since: { type: 'string', description: 'Passed to adb logcat -T.' },
341
- follow: { type: 'boolean', description: 'Follow live logs for durationSec seconds.' },
342
- durationSec: { type: 'number', description: 'Bounded live follow duration. Max 60 seconds.' },
343
- clear: { type: 'boolean', description: 'Clear logcat before reading/following.' },
344
- }),
345
- bridgeTool('network', 'Read generic in-app network records.', {
346
- compact: { type: 'boolean', description: 'Return one-line-sized network record summaries without bodies.' },
347
- urlFilter: { type: 'string', description: 'Only retain records whose URL contains this string.' },
348
- method: { type: 'string', description: 'Only retain records with this HTTP method.' },
349
- statusCode: { type: 'number', description: 'Only retain records with this HTTP status.' },
350
- noBodies: { type: 'boolean', description: 'Omit requestBody and responseBody fields from full output.' },
351
- bodyMaxBytes: { type: 'number', description: 'Maximum request/response body bytes retained per record.' },
352
- }),
353
- bridgeTool('webview_pages', 'List attachable Android WebView DevTools/CDP pages for the target package.', {
354
- webviewPort: { type: 'number', description: 'Local port used for adb forward. Defaults to the first free port at or above 9222.' },
355
- socketName: { type: 'string', description: 'Explicit webview_devtools_remote socket name.' },
356
- targetId: { type: 'string', description: 'Optional CDP target/page id.' },
357
- pageUrlFilter: { type: 'string', description: 'Prefer a WebView page whose URL contains this string.' },
358
- keepForward: { type: 'boolean', description: 'Leave the adb forward active after listing pages.' },
359
- }),
360
- bridgeTool('webview_network', 'Capture WebView fetch/XHR/resource Network events through Chrome DevTools Protocol.', {
361
- webviewPort: { type: 'number', description: 'Local port used for adb forward. Defaults to the first free port at or above 9222.' },
362
- socketName: { type: 'string', description: 'Explicit webview_devtools_remote socket name.' },
363
- targetId: { type: 'string', description: 'Optional CDP target/page id.' },
364
- pageUrlFilter: { type: 'string', description: 'Prefer a WebView page whose URL contains this string.' },
365
- urlFilter: { type: 'string', description: 'Only retain Network requests whose URL contains this string.' },
366
- durationMs: { type: 'number', description: 'Capture duration after attach. Defaults to 3000 ms.' },
367
- script: { type: 'string', description: 'JavaScript expression to evaluate after Network/Runtime are enabled.' },
368
- includeResponseBody: { type: 'boolean', description: 'Fetch response bodies with Network.getResponseBody when available.' },
369
- bodyMaxBytes: { type: 'number', description: 'Maximum response/request body bytes retained per event.' },
370
- maxEvents: { type: 'number', description: 'Maximum raw CDP events retained. Defaults to 200.' },
371
- }),
372
- bridgeTool('webview_console', 'Capture WebView console and log events through Chrome DevTools Protocol.', {
373
- webviewPort: { type: 'number', description: 'Local port used for adb forward. Defaults to the first free port at or above 9222.' },
374
- socketName: { type: 'string', description: 'Explicit webview_devtools_remote socket name.' },
375
- targetId: { type: 'string', description: 'Optional CDP target/page id.' },
376
- pageUrlFilter: { type: 'string', description: 'Prefer a WebView page whose URL contains this string.' },
377
- durationMs: { type: 'number', description: 'Capture duration after attach. Defaults to 3000 ms.' },
378
- script: { type: 'string', description: 'JavaScript expression to evaluate after Runtime is enabled.' },
379
- maxEvents: { type: 'number', description: 'Maximum raw CDP events retained. Defaults to 200.' },
380
- }),
381
- bridgeTool('state', 'Read generic in-app state records.'),
382
- bridgeTool('events', 'Read generic in-app event records.'),
383
- bridgeTool('uia_tree', 'Read UIAutomator XML for the current device window.'),
384
- bridgeTool('screenshot', 'Capture an ADB screenshot.'),
385
- bridgeTool('install_apk', 'Install an APK through ADB while assisting device-side package-installer confirmation screens with UIAutomator.', {
386
- apkPath: { type: 'string', description: 'Absolute or workspace-relative APK path.' },
387
- allowDowngrade: { type: 'boolean', description: 'Pass -d to adb install.' },
388
- streaming: { type: 'boolean', description: 'Use streaming install instead of the default --no-streaming mode.' },
389
- installTimeoutMs: { type: 'number', description: 'Maximum time for adb install. Defaults to 180000 ms.' },
390
- installerTimeoutMs: { type: 'number', description: 'Maximum time to keep assisting installer screens after adb install exits. Defaults to 90000 ms.' },
391
- intervalMs: { type: 'number', description: 'Installer polling interval. Defaults to 700 ms.' },
392
- }, ['apkPath']),
393
- 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']),
394
- 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()),
395
- bridgeTool('launch_activity', 'Launch an explicit Android Activity component, optionally with action/data/category/string extras.', launchProperties()),
396
- bridgeTool('launch_native_test', 'Launch the debug native Android bridge test Activity.'),
397
- bridgeTool('launch_flutter', 'Launch the Flutter Activity, optionally with an initial route.'),
398
- bridgeTool('tap', 'Tap device coordinates through ADB.', {
399
- tapX: { type: 'number' },
400
- tapY: { type: 'number' },
401
- }, ['tapX', 'tapY']),
402
- bridgeTool('tap_text', 'Tap the center of an Android View node by exact text or contentDescription.', {
403
- targetText: { type: 'string' },
404
- noAutoHideKeyboard: { type: 'boolean', description: 'Disable the default keyboard-risk guard before tapping lower-screen app nodes.' },
405
- }, ['targetText']),
406
- bridgeTool('wait_text', 'Wait until text appears in status, Android tree, or UIAutomator tree.', {
407
- targetText: { type: 'string' },
408
- timeoutSec: { type: 'number' },
409
- }, ['targetText']),
410
- 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.', {
411
- text: { type: 'string', description: 'Text to set in the focused or coordinate-matched native EditText.' },
412
- tapX: { type: 'number', description: 'Optional X coordinate used to choose a native EditText target.' },
413
- tapY: { type: 'number', description: 'Optional Y coordinate used to choose a native EditText target.' },
414
- hideKeyboard: { type: 'boolean', description: 'Hide the soft keyboard after setting text.' },
415
- }, ['text', 'packageName']),
416
- bridgeTool('keyboard_state', 'Read Android soft-keyboard visibility from dumpsys input_method.'),
417
- bridgeTool('hide_keyboard', 'Hide the Android soft keyboard when it is visible.', {
418
- force: { type: 'boolean', description: 'Send keyboard-dismiss keys even when the visibility probe says the keyboard is hidden.' },
419
- intervalMs: { type: 'number', description: 'Delay between dismiss attempts. Defaults to 500 ms.' },
420
- }),
421
- bridgeTool('swipe', 'Swipe device coordinates through ADB.', {
422
- startX: { type: 'number' },
423
- startY: { type: 'number' },
424
- endX: { type: 'number' },
425
- endY: { type: 'number' },
426
- durationMs: { type: 'number' },
427
- }, ['startX', 'startY', 'endX', 'endY']),
428
- bridgeTool('keyevent', 'Send an Android keyevent through ADB.', {
429
- keyCode: { type: 'number' },
430
- }, ['keyCode']),
431
- bridgeTool('permission_state', 'Read Android runtime permission state from dumpsys package.', {
432
- permission: { type: 'string' },
433
- }, ['permission']),
434
- bridgeTool('permission_grant', 'Grant an Android runtime permission with adb pm grant, then read state.', {
435
- permission: { type: 'string' },
436
- }, ['permission']),
437
- bridgeTool('permission_revoke', 'Revoke an Android runtime permission with adb pm revoke, then read state.', {
438
- permission: { type: 'string' },
439
- }, ['permission']),
440
- bridgeTool('appops_set', 'Set an Android app-op mode with adb appops set.', {
441
- op: { type: 'string' },
442
- mode: { type: 'string' },
443
- }, ['op', 'mode']),
444
- bridgeTool('tap_uia_text', 'Tap a UIAutomator node by text without relying on the in-app tree.', {
445
- targetText: { type: 'string' },
446
- exact: { type: 'boolean' },
447
- }, ['targetText']),
448
- bridgeTool('permission_dialog', 'Tap a visible Android permission dialog allow button through UIAutomator.', {
449
- targetText: { type: 'string', description: 'Optional custom allow-button text.' },
450
- buttonText: { type: 'string', description: 'Optional comma-separated allow-button texts.' },
451
- resourceId: { type: 'string', description: 'Optional permission button resource id.' },
452
- attempts: { type: 'number' },
453
- intervalMs: { type: 'number' },
454
- exact: { type: 'boolean' },
455
- }),
456
- {
457
- name: 'run_smoke',
458
- description: 'Run the full Android + Flutter bridge smoke test.',
459
- inputSchema: baseSchema(),
460
- },
242
+ { name: 'capabilities', description: 'Discover commands and their exact argument schemas. Include options or select one command for the complete contract.',
243
+ inputSchema: { type: 'object', additionalProperties: false, properties: {
244
+ domain: { type: 'string' }, command: { type: 'string' }, includeOptions: { type: 'boolean' },
245
+ } } },
246
+ { name: 'run', description: 'Execute a command from capabilities. All command parameters, including target identity, belong in arguments.',
247
+ inputSchema: { type: 'object', additionalProperties: false, required: ['command'], properties: {
248
+ command: { type: 'string' }, arguments: { type: 'object', additionalProperties: true },
249
+ } } },
461
250
  ];
462
251
  }
463
252
 
464
- function bridgeTool(name, description, properties = {}, required = []) {
465
- return {
466
- name,
467
- description,
468
- inputSchema: baseSchema(properties, required),
469
- };
470
- }
471
-
472
- function launchProperties() {
473
- return {
474
- activity: { type: 'string', description: 'Activity class, such as .MainActivity or com.example.MainActivity.' },
475
- component: { type: 'string', description: 'Explicit Android component, such as com.example/.MainActivity.' },
476
- action: { type: 'string', description: 'Intent action for explicit Activity launch.' },
477
- category: { type: 'string', description: 'Intent category.' },
478
- data: { type: 'string', description: 'Intent data URI.' },
479
- extra: {
480
- type: 'object',
481
- additionalProperties: { type: 'string' },
482
- description: 'String intent extras as an object of key/value pairs.',
483
- },
484
- };
485
- }
486
-
487
- function baseSchema(extraProperties = {}, extraRequired = []) {
488
- return {
489
- type: 'object',
490
- properties: {
491
- serial: { type: 'string', description: 'ADB serial. Optional when one device is connected.' },
492
- adb: { type: 'string', description: 'ADB executable path or command.' },
493
- port: { type: 'number', description: 'Raw bridge port override. Defaults to 18080; agents should prefer packageName when targeting a known app.' },
494
- packageName: { type: 'string', description: 'Target Android package name. Use this when default 18080 is unreachable or multiple bridge-enabled apps are installed; the CLI discovers the app bridge port from package-private state.' },
495
- initialRoute: { type: 'string', description: 'Flutter initial route for launch_flutter.' },
496
- outFile: { type: 'string', description: 'Screenshot output path for screenshot.' },
497
- artifactDir: { type: 'string', description: 'Directory for generated default artifacts such as screenshots.' },
498
- sinceId: { type: 'number', description: 'Capture query lower bound by record id.' },
499
- sinceMs: { type: 'number', description: 'Capture query lower bound by timestamp milliseconds.' },
500
- limit: { type: 'number', description: 'Maximum capture records to return.' },
501
- ...extraProperties,
502
- },
503
- required: extraRequired,
504
- additionalProperties: false,
505
- };
506
- }
507
-
508
- function h5TargetSchema() {
509
- return {
510
- selector: { type: 'string', description: 'CSS selector for the target DOM element.' },
511
- targetText: { type: 'string', description: 'Text, value, aria-label, placeholder, id, name, or role to match.' },
512
- exact: { type: 'boolean', description: 'Require exact text match instead of substring match.' },
513
- };
514
- }
515
-
516
- const commandDefinitions = [
517
- { command: 'status', domain: 'core', summary: 'Read bridge status, app/device metadata, capture counts, and Flutter summary.', targetApp: true, options: ['packageName', 'port', 'serial', 'full'] },
518
- { 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'] },
519
- { command: 'uia-tree', domain: 'core', summary: 'Read UIAutomator XML for the current foreground window.', options: ['serial', 'compact', 'textFilter', 'resourceIdFilter', 'classFilter', 'visibleOnly', 'maxNodes'] },
520
- { command: 'screenshot', domain: 'core', summary: 'Capture a screenshot, with foreground package verification when packageName is supplied.', options: ['serial', 'packageName', 'outFile', 'artifactDir'] },
521
- { command: 'logs', domain: 'core', summary: 'Read in-app log records from the bridge.', targetApp: true, options: ['packageName', 'port', 'serial', 'sinceId', 'sinceMs', 'limit'] },
522
- { 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'] },
523
- { command: 'state', domain: 'core', summary: 'Read in-app state records from the bridge.', targetApp: true, options: ['packageName', 'port', 'serial', 'sinceId', 'sinceMs', 'limit'] },
524
- { command: 'events', domain: 'core', summary: 'Read in-app event records from the bridge.', targetApp: true, options: ['packageName', 'port', 'serial', 'sinceId', 'sinceMs', 'limit'] },
525
- { 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'] },
526
- { 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'] },
527
- { command: 'clear-app-data', domain: 'app', summary: 'Clear target app local data through the bridge runtime.', targetApp: true, options: ['serial', 'packageName'] },
528
- { 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'] },
529
- { 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'] },
530
- { 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'] },
531
- { 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'] },
532
- { command: 'launch-native-test', domain: 'app', summary: 'Launch the debug native bridge test Activity.', targetApp: true, options: ['serial', 'packageName'] },
533
- { command: 'launch-flutter', domain: 'app', summary: 'Launch the Flutter Activity, optionally with an initial route.', targetApp: true, options: ['serial', 'packageName', 'initialRoute'] },
534
- { command: 'permission-state', domain: 'app', summary: 'Read Android runtime permission state.', targetApp: true, options: ['serial', 'packageName', 'permission'] },
535
- { command: 'permission-grant', domain: 'app', summary: 'Grant an Android runtime permission.', targetApp: true, options: ['serial', 'packageName', 'permission'] },
536
- { command: 'permission-revoke', domain: 'app', summary: 'Revoke an Android runtime permission.', targetApp: true, options: ['serial', 'packageName', 'permission'] },
537
- { command: 'permission-dialog', domain: 'app', summary: 'Tap a visible Android permission dialog allow button.', options: ['serial', 'targetText', 'buttonText', 'resourceId', 'attempts', 'intervalMs', 'exact'] },
538
- { command: 'appops-set', domain: 'app', summary: 'Set an Android app-op mode.', targetApp: true, options: ['serial', 'packageName', 'op', 'mode'] },
539
- { command: 'tap', domain: 'action', summary: 'Tap device coordinates through ADB.', options: ['serial', 'tapX', 'tapY'] },
540
- { 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'] },
541
- { command: 'tap-uia-text', domain: 'action', summary: 'Tap a UIAutomator node by text without relying on the in-app tree.', options: ['serial', 'targetText', 'exact'] },
542
- { 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'] },
543
- { 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'] },
544
- { command: 'keyboard-state', domain: 'action', summary: 'Read Android soft keyboard visibility.', options: ['serial'] },
545
- { command: 'hide-keyboard', domain: 'action', summary: 'Hide the Android soft keyboard.', options: ['serial', 'force', 'intervalMs'] },
546
- { command: 'swipe', domain: 'action', summary: 'Swipe device coordinates through ADB.', options: ['serial', 'startX', 'startY', 'endX', 'endY', 'durationMs'] },
547
- { command: 'keyevent', domain: 'action', summary: 'Send an Android keyevent through ADB.', options: ['serial', 'keyCode'] },
548
- { command: 'flutter-tree', domain: 'flutter', summary: 'Read the latest Flutter layout snapshot.', targetApp: true, options: ['serial', 'packageName', 'port'] },
549
- { command: 'flutter-nodes', domain: 'flutter', summary: 'Read Flutter operable nodes.', targetApp: true, options: ['serial', 'packageName', 'port'] },
550
- { command: 'flutter-action', domain: 'flutter', summary: 'Dispatch a raw Flutter action payload.', targetApp: true, options: ['serial', 'packageName', 'payload'] },
551
- { command: 'tap-flutter-text', domain: 'flutter', summary: 'Tap a Flutter node by visible text.', targetApp: true, options: ['serial', 'packageName', 'targetText'] },
552
- { 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'] },
553
- { command: 'scroll-flutter', domain: 'flutter', summary: 'Scroll Flutter content by delta or until text is visible.', targetApp: true, options: ['serial', 'packageName', 'targetText', 'delta', 'maxSwipes'] },
554
- { command: 'h5-dom', domain: 'webview', summary: 'Read native Android WebView DOM.', targetApp: true, options: ['serial', 'packageName', 'port'] },
555
- { command: 'h5-eval', domain: 'webview', summary: 'Execute JavaScript in the current native Android WebView.', targetApp: true, options: ['serial', 'packageName', 'script'] },
556
- { command: 'h5-click', domain: 'webview', summary: 'Click a native WebView element by selector or text.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'exact'] },
557
- { command: 'h5-input', domain: 'webview', summary: 'Set text in a native WebView input.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'value', 'exact'] },
558
- { command: 'h5-wait', domain: 'webview', summary: 'Wait for native WebView text or selector.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'timeoutSec', 'intervalMs'] },
559
- { 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'] },
560
- { command: 'flutter-h5-dom', domain: 'webview', summary: 'Read DOM through a Flutter H5 adapter.', targetApp: true, options: ['serial', 'packageName', 'port'] },
561
- { command: 'flutter-h5-eval', domain: 'webview', summary: 'Execute JavaScript through a Flutter H5 adapter.', targetApp: true, options: ['serial', 'packageName', 'script'] },
562
- { command: 'flutter-h5-click', domain: 'webview', summary: 'Click a Flutter H5 DOM element.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'exact'] },
563
- { command: 'flutter-h5-input', domain: 'webview', summary: 'Set text in a Flutter H5 input.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'value', 'exact'] },
564
- { command: 'flutter-h5-wait', domain: 'webview', summary: 'Wait for Flutter H5 text or selector.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'timeoutSec', 'intervalMs'] },
565
- { 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'] },
566
- { command: 'webview-pages', domain: 'webview', summary: 'List attachable Android WebView DevTools/CDP pages.', targetApp: true, options: ['serial', 'packageName', 'webviewPort', 'socketName', 'targetId', 'pageUrlFilter', 'keepForward'] },
567
- { 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'] },
568
- { 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'] },
569
- { command: 'ios-doctor', domain: 'ios', summary: 'Check Xcode, connected iPhone, AiAppBridgeIOS runtime, and WebDriverAgent readiness.', targetKind: 'ios-app', options: ['deviceId', 'bundleId', 'iosHost', 'iosPort', 'runtimeUrl', 'wdaUrl'] },
570
- { command: 'ios-setup', domain: 'ios', summary: 'Verify full-control iOS setup; can install/launch the app and start WDA when signing inputs are supplied.', targetKind: 'ios-app', options: ['deviceId', 'bundleId', 'appPath', 'iosHost', 'iosPort', 'runtimeUrl', 'wdaUrl', 'wdaProjectPath', 'wdaBundleId', 'teamId', 'startWda'] },
571
- { command: 'ios-devices', domain: 'ios', summary: 'List iOS devices known to xcrun devicectl.', targetKind: 'ios-device', options: ['deviceId'] },
572
- { command: 'ios-install-app', domain: 'ios', summary: 'Install an iOS .app bundle through devicectl.', targetKind: 'ios-app', options: ['deviceId', 'appPath'] },
573
- { command: 'ios-launch-app', domain: 'ios', summary: 'Launch an iOS app by bundle identifier through devicectl.', targetKind: 'ios-app', options: ['deviceId', 'bundleId', 'terminateExisting'] },
574
- { command: 'ios-status', domain: 'ios', summary: 'Read AiAppBridgeIOS runtime status.', targetKind: 'ios-app', options: ['deviceId', 'bundleId', 'iosHost', 'iosPort', 'runtimeUrl'] },
575
- { command: 'ios-tree', domain: 'ios', summary: 'Read UIKit tree from the AiAppBridgeIOS runtime.', targetKind: 'ios-app', options: ['deviceId', 'bundleId', 'iosHost', 'iosPort', 'runtimeUrl'] },
576
- { command: 'ios-logs', domain: 'ios', summary: 'Read in-app iOS log records.', targetKind: 'ios-app', options: ['deviceId', 'bundleId', 'iosHost', 'iosPort', 'runtimeUrl', 'sinceId', 'sinceMs', 'limit'] },
577
- { command: 'ios-network', domain: 'ios', summary: 'Read in-app iOS network records.', targetKind: 'ios-app', options: ['deviceId', 'bundleId', 'iosHost', 'iosPort', 'runtimeUrl', 'sinceId', 'sinceMs', 'limit'] },
578
- { command: 'ios-state', domain: 'ios', summary: 'Read in-app iOS state records.', targetKind: 'ios-app', options: ['deviceId', 'bundleId', 'iosHost', 'iosPort', 'runtimeUrl', 'sinceId', 'sinceMs', 'limit'] },
579
- { command: 'ios-events', domain: 'ios', summary: 'Read in-app iOS event records.', targetKind: 'ios-app', options: ['deviceId', 'bundleId', 'iosHost', 'iosPort', 'runtimeUrl', 'sinceId', 'sinceMs', 'limit'] },
580
- { command: 'ios-h5-dom', domain: 'ios', summary: 'Read WKWebView DOM from the AiAppBridgeIOS runtime.', targetKind: 'ios-app', options: ['deviceId', 'bundleId', 'iosHost', 'iosPort', 'runtimeUrl'] },
581
- { command: 'ios-h5-eval', domain: 'ios', summary: 'Execute JavaScript in WKWebView through the AiAppBridgeIOS runtime.', targetKind: 'ios-app', options: ['deviceId', 'bundleId', 'iosHost', 'iosPort', 'runtimeUrl', 'script'] },
582
- { command: 'ios-flutter-tree', domain: 'ios', summary: 'Read Flutter iOS layout snapshot from the runtime.', targetKind: 'ios-app', options: ['deviceId', 'bundleId', 'iosHost', 'iosPort', 'runtimeUrl'] },
583
- { command: 'ios-flutter-nodes', domain: 'ios', summary: 'Read Flutter iOS operable nodes from the runtime.', targetKind: 'ios-app', options: ['deviceId', 'bundleId', 'iosHost', 'iosPort', 'runtimeUrl'] },
584
- { command: 'ios-flutter-action', domain: 'ios', summary: 'Dispatch a Flutter iOS action through the runtime.', targetKind: 'ios-app', options: ['deviceId', 'bundleId', 'iosHost', 'iosPort', 'runtimeUrl', 'payload'] },
585
- { command: 'ios-screenshot', domain: 'ios', summary: 'Capture an iOS device screenshot through devicectl.', targetKind: 'ios-device', options: ['deviceId', 'outFile', 'artifactDir', 'displayUniqueId'] },
586
- { command: 'ios-wda-status', domain: 'ios', summary: 'Check WebDriverAgent status.', targetKind: 'ios-device', options: ['wdaUrl'] },
587
- { command: 'ios-uia-tree', domain: 'ios', summary: 'Read the XCUITest/WebDriverAgent UI tree.', targetKind: 'ios-device', options: ['bundleId', 'wdaUrl', 'wdaSessionId'] },
588
- { command: 'ios-tap', domain: 'ios', summary: 'Tap iOS device coordinates through WebDriverAgent.', targetKind: 'ios-device', options: ['bundleId', 'wdaUrl', 'wdaSessionId', 'tapX', 'tapY'] },
589
- { command: 'ios-input', domain: 'ios', summary: 'Type text through WebDriverAgent, optionally tapping coordinates first.', targetKind: 'ios-device', options: ['bundleId', 'wdaUrl', 'wdaSessionId', 'text', 'tapX', 'tapY', 'accessibilityId', 'elementId', 'clearFirst'] },
590
- { command: 'ios-swipe', domain: 'ios', summary: 'Swipe through WebDriverAgent.', targetKind: 'ios-device', options: ['bundleId', 'wdaUrl', 'wdaSessionId', 'startX', 'startY', 'endX', 'endY', 'durationMs'] },
591
- { command: 'web-provider-status', domain: 'web', summary: 'Read desktop Web Bridge provider status.', options: [] },
592
- { command: 'web-session-start', domain: 'web', summary: 'Start the desktop Web Bridge WebSocket session server.', options: ['host', 'webPort', 'path', 'token'] },
593
- { command: 'web-connect-info', domain: 'web', summary: 'Read the Web Bridge endpoint and token for SDK clients.', options: [] },
594
- { command: 'web-sessions', domain: 'web', summary: 'List connected Web Bridge SDK sessions.', options: [] },
595
- { command: 'web-status', domain: 'web', summary: 'Read status and capture counts for a Web Bridge session.', targetKind: 'web-target', options: ['sessionId'] },
596
- { command: 'web-dom', domain: 'web', summary: 'Read or refresh a Web Bridge DOM snapshot.', targetKind: 'web-target', options: ['sessionId', 'targetId', 'selector', 'refresh', 'timeoutMs'] },
597
- { command: 'web-logs', domain: 'web', summary: 'Read Web Bridge log records.', targetKind: 'web-target', options: ['sessionId', 'sinceId', 'sinceMs', 'limit'] },
598
- { command: 'web-network', domain: 'web', summary: 'Read Web Bridge network records.', targetKind: 'web-target', options: ['sessionId', 'sinceId', 'sinceMs', 'limit'] },
599
- { command: 'web-state', domain: 'web', summary: 'Read Web Bridge state records.', targetKind: 'web-target', options: ['sessionId', 'sinceId', 'sinceMs', 'limit'] },
600
- { command: 'web-events', domain: 'web', summary: 'Read Web Bridge event records.', targetKind: 'web-target', options: ['sessionId', 'sinceId', 'sinceMs', 'limit'] },
601
- { 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'] },
602
- { 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'] },
603
- { 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'] },
604
- { 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'] },
605
- { command: 'web-scroll', domain: 'web', summary: 'Scroll a Web Bridge DOM target.', targetKind: 'web-target', options: ['sessionId', 'targetId', 'selector', 'deltaX', 'deltaY', 'timeoutMs'] },
606
- { command: 'forward', domain: 'advanced', summary: 'Create the ADB port forward for the bridge.', targetApp: true, options: ['serial', 'packageName', 'port'] },
607
- { command: 'remove-forward', domain: 'advanced', summary: 'Remove the ADB port forward for the bridge.', options: ['serial', 'port'] },
608
- { command: 'batch', domain: 'advanced', summary: 'Run multiple AI App Bridge commands serially in one MCP call.', options: ['defaults', 'steps', 'stopOnError', 'includeRaw', 'maxRawChars'] },
609
- { command: 'smoke', domain: 'diagnostics', summary: 'Run the native sample smoke test.', options: ['serial', 'packageName', 'outFile', 'artifactDir', 'skipFlutterLaunch'] },
610
- ];
611
-
612
- const commandByName = new Map(commandDefinitions.map((definition) => [definition.command, definition]));
613
-
614
253
  async function callTool(name, args) {
615
254
  if (name === 'capabilities') {
616
- return toolJson(capabilityPayload(args));
617
- }
618
- if (name === 'run') {
619
- return runGeneric(args);
620
- }
621
- if (name === 'run_smoke') {
622
- return runSmoke(args);
623
- }
624
- const commandMap = {
625
- flutter_tree: 'flutter-tree',
626
- h5_dom: 'h5-dom',
627
- h5_eval: 'h5-eval',
628
- h5_click: 'h5-click',
629
- h5_input: 'h5-input',
630
- h5_wait: 'h5-wait',
631
- h5_scroll: 'h5-scroll',
632
- freeze_app: 'freeze-app',
633
- thaw_app: 'thaw-app',
634
- flutter_h5_dom: 'flutter-h5-dom',
635
- flutter_h5_eval: 'flutter-h5-eval',
636
- flutter_h5_click: 'flutter-h5-click',
637
- flutter_h5_input: 'flutter-h5-input',
638
- flutter_h5_wait: 'flutter-h5-wait',
639
- flutter_h5_scroll: 'flutter-h5-scroll',
640
- flutter_nodes: 'flutter-nodes',
641
- tap_flutter_text: 'tap-flutter-text',
642
- input_flutter_text: 'input-flutter-text',
643
- uia_tree: 'uia-tree',
644
- install_apk: 'install-apk',
645
- clear_app_data: 'clear-app-data',
646
- launch_app: 'launch-app',
647
- launch_activity: 'launch-activity',
648
- launch_native_test: 'launch-native-test',
649
- launch_flutter: 'launch-flutter',
650
- tap_text: 'tap-text',
651
- wait_text: 'wait-text',
652
- input_text: 'input-text',
653
- keyboard_state: 'keyboard-state',
654
- hide_keyboard: 'hide-keyboard',
655
- webview_pages: 'webview-pages',
656
- webview_network: 'webview-network',
657
- webview_console: 'webview-console',
658
- permission_state: 'permission-state',
659
- permission_grant: 'permission-grant',
660
- permission_revoke: 'permission-revoke',
661
- appops_set: 'appops-set',
662
- tap_uia_text: 'tap-uia-text',
663
- permission_dialog: 'permission-dialog',
664
- };
665
- const command = commandMap[name] || normalizeCommandName(name);
666
- return runBridgeChecked(command, args);
667
- }
668
-
669
- function capabilityPayload(args = {}) {
670
- const includeOptions = Boolean(args.includeOptions);
671
- const requestedCommand = args.command ? normalizeCommandName(args.command) : '';
672
- if (requestedCommand) {
673
- const definition = commandByName.get(requestedCommand);
674
- return {
675
- ok: Boolean(definition),
676
- command: requestedCommand,
677
- ...(definition ? shapeCommandDefinition(definition, true) : { error: 'unknown_command' }),
678
- };
255
+ const result = capabilityPayload(args);
256
+ return toolJson(result, result.ok === false);
679
257
  }
680
-
681
- const requestedDomain = args.domain ? String(args.domain) : '';
682
- const domains = {};
683
- for (const definition of commandDefinitions) {
684
- if (requestedDomain && definition.domain !== requestedDomain) continue;
685
- if (!domains[definition.domain]) domains[definition.domain] = [];
686
- domains[definition.domain].push(shapeCommandDefinition(definition, includeOptions));
687
- }
688
- return {
689
- ok: true,
690
- surface: mcpSurface === 'full' || mcpSurface === 'legacy' ? 'full' : 'compact',
691
- supportedTargets,
692
- commandDomains,
693
- usage: `${supportedTargetsText} ${discoveryText} Use run with one of these command names. Prefer packageName for Android app commands, bundleId/deviceId for iOS, and sessionId/targetId for Web Bridge sessions.`,
694
- domains,
695
- };
696
- }
697
-
698
- function shapeCommandDefinition(definition, includeOptions) {
699
- return {
700
- command: definition.command,
701
- summary: definition.summary,
702
- targetApp: Boolean(definition.targetApp),
703
- targetKind: definition.targetKind || (definition.targetApp ? 'android-app' : 'none'),
704
- ...(includeOptions ? { options: definition.options || [] } : {}),
705
- };
258
+ if (name === 'run') return runGeneric(args);
259
+ return toolJson({ ok: false, error: 'unknown_tool', message: `Unknown tool: ${name}. Use capabilities or run.`, dispatched: false, ambiguous: false }, true);
706
260
  }
707
261
 
262
+ // MCP only adapts the shared execution reply to its tool-result format.
263
+ const capabilityPayload = discovery.capabilities;
708
264
  async function runGeneric(args = {}) {
709
- const command = normalizeCommandName(args.command);
710
- if (!commandByName.has(command)) {
711
- return toolText(`unknown command: ${args.command || ''}`, true);
712
- }
713
- const commandArgs = {
714
- ...(args.arguments && typeof args.arguments === 'object' ? args.arguments : {}),
715
- };
716
- for (const key of sharedRunArgKeys()) {
717
- if (args[key] !== undefined && commandArgs[key] === undefined) {
718
- commandArgs[key] = args[key];
719
- }
720
- }
721
- if (command === 'batch') {
722
- return runBatch(commandArgs);
723
- }
724
- return runBridgeChecked(command, commandArgs);
725
- }
726
-
727
- function normalizeCommandName(value) {
728
- return String(value || '').trim().replace(/_/g, '-');
729
- }
730
-
731
- function sharedRunArgKeys() {
732
- return [
733
- 'adb',
734
- 'serial',
735
- 'port',
736
- 'packageName',
737
- 'artifactDir',
738
- 'sessionId',
739
- 'targetId',
740
- 'webPort',
741
- 'deviceId',
742
- 'bundleId',
743
- 'iosHost',
744
- 'iosPort',
745
- 'runtimeUrl',
746
- 'wdaUrl',
747
- 'wdaSessionId',
748
- 'wdaProjectPath',
749
- 'wdaBundleId',
750
- 'accessibilityId',
751
- 'elementId',
752
- 'teamId',
753
- 'appPath',
754
- 'xcodebuild',
755
- 'devicectl',
756
- ];
757
- }
758
-
759
- function runBridgeChecked(command, args = {}) {
760
- const definition = commandByName.get(command);
761
- if (definition?.domain === 'ios') {
762
- return runIOSChecked(command, args);
763
- }
764
- if (definition?.domain === 'web') {
765
- return runWebChecked(command, args);
766
- }
767
- if (command === 'clear-app-data' && !args.packageName) {
768
- return toolText('clear-app-data: packageName is required in MCP mode so the command cannot clear a default package.', true);
769
- }
770
- if ((command === 'freeze-app' || command === 'thaw-app') && !args.packageName) {
771
- return toolText(`${command}: packageName is required in MCP mode so the command cannot signal a default package.`, true);
772
- }
773
- if (definition?.targetApp && !args.packageName && !args.port) {
774
- return toolText(`${command}: packageName or explicit port is required in MCP mode so the command cannot fall back to a default package.`, true);
775
- }
776
- return runBridge(command, args);
777
- }
778
-
779
- async function runWebChecked(command, args = {}) {
780
- const result = await webProvider.run(command, args);
781
- return toolJson(result, result?.ok === false);
782
- }
783
-
784
- async function runIOSChecked(command, args = {}) {
785
- const result = await iosProvider.run(command, args);
786
- return toolJson(result, result?.ok === false);
787
- }
788
-
789
- async function runBatch(args = {}, runner = runBridgeChecked) {
790
- const startedAtMs = Date.now();
791
- const mode = args.mode ? String(args.mode) : 'serial';
792
- if (mode !== 'serial') {
793
- return toolJson({ ok: false, error: 'batch_mode_not_supported', mode }, true);
794
- }
795
- const steps = Array.isArray(args.steps) ? args.steps : [];
796
- if (steps.length === 0) {
797
- return toolJson({ ok: false, error: 'batch_steps_required' }, true);
798
- }
799
- const maxSteps = args.maxSteps === undefined ? 30 : Number(args.maxSteps);
800
- if (!Number.isInteger(maxSteps) || maxSteps < 1) {
801
- return toolJson({ ok: false, error: 'invalid_max_steps', maxSteps: args.maxSteps }, true);
802
- }
803
- if (steps.length > maxSteps) {
804
- return toolJson({ ok: false, error: 'batch_too_many_steps', stepCount: steps.length, maxSteps }, true);
805
- }
806
-
807
- const defaults = args.defaults && typeof args.defaults === 'object' ? { ...args.defaults } : {};
808
- for (const key of sharedRunArgKeys()) {
809
- if (args[key] !== undefined && defaults[key] === undefined) {
810
- defaults[key] = args[key];
811
- }
812
- }
813
-
814
- const normalizedSteps = [];
815
- const seenIds = new Set();
816
- for (let index = 0; index < steps.length; index += 1) {
817
- const rawStep = steps[index] && typeof steps[index] === 'object' ? steps[index] : {};
818
- const stepId = String(rawStep.id || `step_${index + 1}`);
819
- if (seenIds.has(stepId)) {
820
- return toolJson({ ok: false, error: 'duplicate_batch_step_id', stepId }, true);
821
- }
822
- seenIds.add(stepId);
823
- const command = normalizeCommandName(rawStep.command);
824
- if (!commandByName.has(command)) {
825
- return toolJson({ ok: false, error: 'unknown_batch_step_command', stepId, command: rawStep.command || '' }, true);
826
- }
827
- if (command === 'batch') {
828
- return toolJson({ ok: false, error: 'nested_batch_not_supported', stepId }, true);
829
- }
830
- normalizedSteps.push({ ...rawStep, id: stepId, command });
831
- }
832
-
833
- const stopOnError = args.stopOnError !== false;
834
- const includeRaw = Boolean(args.includeRaw);
835
- const maxRawChars = args.maxRawChars === undefined ? 4000 : Number(args.maxRawChars);
836
- if (!Number.isInteger(maxRawChars) || maxRawChars < 0) {
837
- return toolJson({ ok: false, error: 'invalid_max_raw_chars', maxRawChars: args.maxRawChars }, true);
838
- }
839
- const results = [];
840
- let stopped = false;
841
-
842
- for (const step of normalizedSteps) {
843
- if (stopped) {
844
- results.push({
845
- id: step.id,
846
- command: step.command,
847
- status: 'skipped',
848
- ok: false,
849
- skipped: true,
850
- reason: 'stopOnError',
851
- });
852
- continue;
853
- }
854
-
855
- const stepStartedAtMs = Date.now();
856
- const stepArgs = {
857
- ...defaults,
858
- ...(step.arguments && typeof step.arguments === 'object' ? step.arguments : {}),
859
- };
860
- for (const key of sharedRunArgKeys()) {
861
- if (step[key] !== undefined) {
862
- stepArgs[key] = step[key];
863
- }
864
- }
865
- try {
866
- const toolResult = await runner(step.command, stepArgs);
867
- const parsed = parseToolResult(toolResult);
868
- const passed = !parsed.isError && parsed.payload?.ok !== false;
869
- const stepResult = {
870
- id: step.id,
871
- command: step.command,
872
- status: passed ? 'passed' : 'failed',
873
- ok: passed,
874
- packageName: stepArgs.packageName,
875
- bundleId: stepArgs.bundleId,
876
- deviceId: stepArgs.deviceId,
877
- sessionId: stepArgs.sessionId,
878
- targetId: stepArgs.targetId,
879
- port: stepArgs.port,
880
- iosPort: stepArgs.iosPort,
881
- durationMs: Date.now() - stepStartedAtMs,
882
- summary: summarizeToolPayload(parsed),
883
- };
884
- if (!passed) {
885
- stepResult.error = parsed.payload?.error || firstTextLine(parsed.text) || 'command_failed';
886
- }
887
- if (includeRaw) {
888
- stepResult.result = parsed.payload || undefined;
889
- stepResult.rawText = parsed.payload ? undefined : truncateText(parsed.text, maxRawChars);
890
- }
891
- results.push(stepResult);
892
- if (!passed && stopOnError) {
893
- stopped = true;
894
- }
895
- } catch (error) {
896
- const stepResult = {
897
- id: step.id,
898
- command: step.command,
899
- status: 'failed',
900
- ok: false,
901
- packageName: stepArgs.packageName,
902
- bundleId: stepArgs.bundleId,
903
- deviceId: stepArgs.deviceId,
904
- port: stepArgs.port,
905
- iosPort: stepArgs.iosPort,
906
- durationMs: Date.now() - stepStartedAtMs,
907
- error: error.message || String(error),
908
- };
909
- results.push(stepResult);
910
- if (stopOnError) {
911
- stopped = true;
912
- }
913
- }
914
- }
915
-
916
- const failed = results.filter((item) => item.status === 'failed').length;
917
- const skipped = results.filter((item) => item.status === 'skipped').length;
918
- const passed = results.filter((item) => item.status === 'passed').length;
919
- return toolJson({
920
- ok: failed === 0,
921
- batchId: args.batchId || generatedBatchId(),
922
- mode,
923
- stopOnError,
924
- stepCount: normalizedSteps.length,
925
- passed,
926
- failed,
927
- skipped,
928
- durationMs: Date.now() - startedAtMs,
929
- steps: results,
930
- }, failed > 0);
931
- }
932
-
933
- async function runBridge(command, args) {
934
- return runProcess(buildBridgeCliArgs(command, args));
935
- }
936
-
937
- function parseToolResult(toolResult) {
938
- const text = String(toolResult?.content?.[0]?.text || '');
939
- try {
940
- return {
941
- isError: Boolean(toolResult?.isError),
942
- text,
943
- payload: JSON.parse(text),
944
- };
945
- } catch (_) {
946
- return {
947
- isError: Boolean(toolResult?.isError),
948
- text,
949
- payload: null,
950
- };
951
- }
952
- }
953
-
954
- function summarizeToolPayload(parsed) {
955
- const payload = parsed.payload;
956
- if (!payload || typeof payload !== 'object') {
957
- return { text: truncateText(parsed.text, 500) };
958
- }
959
- const summary = {
960
- ok: payload.ok,
961
- error: payload.error || null,
962
- };
963
- if (payload.packageName) summary.packageName = payload.packageName;
964
- if (payload.bundleId) summary.bundleId = payload.bundleId;
965
- if (payload.endpoint) summary.endpoint = payload.endpoint;
966
- if (payload.wdaUrl) summary.wdaUrl = payload.wdaUrl;
967
- if (payload.device?.identifier) summary.deviceId = payload.device.identifier;
968
- if (payload.selectedDevice?.identifier) summary.deviceId = payload.selectedDevice.identifier;
969
- if (payload.sessionId) summary.sessionId = payload.sessionId;
970
- if (payload.targetId) summary.targetId = payload.targetId;
971
- if (payload.session?.sessionId) summary.sessionId = payload.session.sessionId;
972
- if (payload.app?.packageName) summary.app = payload.app.packageName;
973
- if (payload.activity) summary.activity = payload.activity;
974
- if (payload.component) summary.component = payload.component;
975
- if (payload.transport) summary.transport = payload.transport;
976
- if (payload.source) summary.source = payload.source;
977
- if (payload.path) summary.path = payload.path;
978
- if (payload.debugBridge) {
979
- summary.bridge = {
980
- version: payload.debugBridge.version,
981
- port: payload.debugBridge.port,
982
- };
983
- }
984
- if (payload.count !== undefined) summary.count = payload.count;
985
- if (payload.sessionCount !== undefined) summary.sessionCount = payload.sessionCount;
986
- if (payload.nodeCount !== undefined) summary.nodeCount = payload.nodeCount;
987
- if (Array.isArray(payload.items)) summary.items = payload.items.length;
988
- if (payload.values && typeof payload.values === 'object') {
989
- summary.values = Object.keys(payload.values).length;
990
- }
991
- if (payload.counts) summary.counts = payload.counts;
992
- if (Array.isArray(payload.requests)) summary.requests = payload.requests.length;
993
- if (Array.isArray(payload.console)) summary.console = payload.console.length;
994
- if (payload.flutter?.layout?.operable) {
995
- summary.flutterOperable = {
996
- ok: payload.flutter.layout.operable.ok,
997
- count: payload.flutter.layout.operable.count,
998
- };
999
- }
1000
- if (payload.result && typeof payload.result === 'object') {
1001
- summary.result = {
1002
- ok: payload.result.ok,
1003
- error: payload.result.error || null,
1004
- value: truncateText(payload.result.value, 200),
1005
- bodyText: truncateText(payload.result.bodyText, 200),
1006
- };
1007
- }
1008
- return summary;
1009
- }
1010
-
1011
- function truncateText(value, maxChars) {
1012
- if (value === undefined || value === null) return value;
1013
- const text = String(value);
1014
- if (text.length <= maxChars) return text;
1015
- return `${text.slice(0, maxChars)}...`;
1016
- }
1017
-
1018
- function firstTextLine(value) {
1019
- const lines = String(value || '').split(/\r?\n/).filter((line) => line.trim());
1020
- return lines.find((line) => {
1021
- const text = line.trim().toLowerCase();
1022
- return text !== 'stderr:' && text !== 'stdout:';
1023
- }) || lines[0] || '';
1024
- }
1025
-
1026
- function generatedBatchId() {
1027
- return `batch-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1028
- }
1029
-
1030
- function buildBridgeCliArgs(command, args = {}) {
1031
- const cliArgs = [cliScript, command];
1032
- addCommonArgs(cliArgs, args);
1033
- addArg(cliArgs, 'device-id', args.deviceId);
1034
- addArg(cliArgs, 'bundle-id', args.bundleId);
1035
- addArg(cliArgs, 'app-path', args.appPath);
1036
- addArg(cliArgs, 'ios-host', args.iosHost);
1037
- addArg(cliArgs, 'ios-port', args.iosPort);
1038
- addArg(cliArgs, 'runtime-url', args.runtimeUrl);
1039
- addArg(cliArgs, 'wda-url', args.wdaUrl);
1040
- addArg(cliArgs, 'wda-session-id', args.wdaSessionId);
1041
- addArg(cliArgs, 'wda-project-path', args.wdaProjectPath);
1042
- addArg(cliArgs, 'wda-bundle-id', args.wdaBundleId);
1043
- addArg(cliArgs, 'accessibility-id', args.accessibilityId);
1044
- addArg(cliArgs, 'element-id', args.elementId);
1045
- addArg(cliArgs, 'clear-first', args.clearFirst);
1046
- addArg(cliArgs, 'team-id', args.teamId);
1047
- addArg(cliArgs, 'start-wda', args.startWda);
1048
- addArg(cliArgs, 'devicectl', args.devicectl);
1049
- addArg(cliArgs, 'xcodebuild', args.xcodebuild);
1050
- addArg(cliArgs, 'display-unique-id', args.displayUniqueId);
1051
- addArg(cliArgs, 'terminate-existing', args.terminateExisting);
1052
- addArg(cliArgs, 'initial-route', args.initialRoute);
1053
- addArg(cliArgs, 'activity', args.activity);
1054
- addArg(cliArgs, 'component', args.component);
1055
- addArg(cliArgs, 'action', args.action);
1056
- addRepeatedArg(cliArgs, 'category', args.category);
1057
- addArg(cliArgs, 'data', args.data);
1058
- addExtraArgs(cliArgs, args.extra);
1059
- addArg(cliArgs, 'out-file', args.outFile);
1060
- addArg(cliArgs, 'artifact-dir', args.artifactDir || defaultArtifactDirFor(command, args));
1061
- addArg(cliArgs, 'apk-path', args.apkPath);
1062
- addArg(cliArgs, 'tap-x', args.tapX);
1063
- addArg(cliArgs, 'tap-y', args.tapY);
1064
- addArg(cliArgs, 'target-text', args.targetText);
1065
- addArg(cliArgs, 'no-auto-hide-keyboard', args.noAutoHideKeyboard);
1066
- addArg(cliArgs, 'timeout-sec', args.timeoutSec);
1067
- addArg(cliArgs, 'text', args.text);
1068
- addArg(cliArgs, 'hide-keyboard', args.hideKeyboard);
1069
- addArg(cliArgs, 'force', args.force);
1070
- addArg(cliArgs, 'start-x', args.startX);
1071
- addArg(cliArgs, 'start-y', args.startY);
1072
- addArg(cliArgs, 'end-x', args.endX);
1073
- addArg(cliArgs, 'end-y', args.endY);
1074
- addArg(cliArgs, 'duration-ms', args.durationMs);
1075
- addArg(cliArgs, 'allow-downgrade', args.allowDowngrade);
1076
- addArg(cliArgs, 'streaming', args.streaming);
1077
- addArg(cliArgs, 'install-timeout-ms', args.installTimeoutMs);
1078
- addArg(cliArgs, 'installer-timeout-ms', args.installerTimeoutMs);
1079
- addArg(cliArgs, 'webview-port', args.webviewPort);
1080
- addArg(cliArgs, 'socket-name', args.socketName);
1081
- addArg(cliArgs, 'target-id', args.targetId);
1082
- addArg(cliArgs, 'page-url-filter', args.pageUrlFilter);
1083
- addArg(cliArgs, 'url-filter', args.urlFilter);
1084
- addArg(cliArgs, 'method', args.method);
1085
- addArg(cliArgs, 'status-code', args.statusCode);
1086
- addArg(cliArgs, 'compact', args.compact);
1087
- addArg(cliArgs, 'full', args.full);
1088
- addArg(cliArgs, 'text-filter', args.textFilter);
1089
- addArg(cliArgs, 'resource-id-filter', args.resourceIdFilter);
1090
- addArg(cliArgs, 'class-filter', args.classFilter);
1091
- addArg(cliArgs, 'visible-only', args.visibleOnly);
1092
- addArg(cliArgs, 'max-nodes', args.maxNodes);
1093
- addArg(cliArgs, 'max-depth', args.maxDepth);
1094
- addArg(cliArgs, 'no-bodies', args.noBodies);
1095
- addArg(cliArgs, 'duration-ms', args.durationMs);
1096
- addArg(cliArgs, 'include-response-body', args.includeResponseBody);
1097
- addArg(cliArgs, 'body-max-bytes', args.bodyMaxBytes);
1098
- addArg(cliArgs, 'max-events', args.maxEvents);
1099
- addArg(cliArgs, 'keep-forward', args.keepForward);
1100
- addArg(cliArgs, 'key-code', args.keyCode);
1101
- addArg(cliArgs, 'payload', args.payload);
1102
- addArg(cliArgs, 'delta', args.delta);
1103
- addArg(cliArgs, 'max-swipes', args.maxSwipes);
1104
- addArg(cliArgs, 'permission', args.permission);
1105
- addArg(cliArgs, 'op', args.op);
1106
- addArg(cliArgs, 'mode', args.mode);
1107
- addArg(cliArgs, 'script', args.script);
1108
- addArg(cliArgs, 'selector', args.selector);
1109
- addArg(cliArgs, 'target-text', args.targetText);
1110
- addArg(cliArgs, 'value', args.value);
1111
- addArg(cliArgs, 'exact', args.exact);
1112
- addArg(cliArgs, 'button-text', args.buttonText);
1113
- addArg(cliArgs, 'resource-id', args.resourceId);
1114
- addArg(cliArgs, 'attempts', args.attempts);
1115
- addArg(cliArgs, 'interval-ms', args.intervalMs);
1116
- addArg(cliArgs, 'delta-x', args.deltaX);
1117
- addArg(cliArgs, 'delta-y', args.deltaY);
1118
- addArg(cliArgs, 'require-text', args.requireText);
1119
- addArg(cliArgs, 'absent-text', args.absentText);
1120
- addArg(cliArgs, 'require-activity', args.requireActivity);
1121
- addArg(cliArgs, 'since-id', args.sinceId);
1122
- addArg(cliArgs, 'since-ms', args.sinceMs);
1123
- addArg(cliArgs, 'limit', args.limit);
1124
- addArg(cliArgs, 'pid', args.pid);
1125
- addArg(cliArgs, 'app-pid', args.appPid);
1126
- addArg(cliArgs, 'tag', args.tag);
1127
- addArg(cliArgs, 'level', args.level);
1128
- addArg(cliArgs, 'grep', args.grep);
1129
- addArg(cliArgs, 'lines', args.lines);
1130
- addArg(cliArgs, 'since', args.since);
1131
- addArg(cliArgs, 'follow', args.follow);
1132
- addArg(cliArgs, 'duration-sec', args.durationSec);
1133
- addArg(cliArgs, 'clear', args.clear);
1134
- addArg(cliArgs, 'skip-flutter-launch', args.skipFlutterLaunch);
1135
- return cliArgs;
1136
- }
1137
-
1138
- async function runSmoke(args) {
1139
- const cliArgs = [cliScript, 'smoke'];
1140
- addCommonArgs(cliArgs, args);
1141
- addArg(cliArgs, 'out-file', args.outFile);
1142
- addArg(cliArgs, 'artifact-dir', args.artifactDir || defaultArtifactDirFor('smoke', args));
1143
- addArg(cliArgs, 'skip-flutter-launch', args.skipFlutterLaunch);
1144
- return runProcess(cliArgs);
1145
- }
1146
-
1147
- function defaultArtifactDirFor(command, args) {
1148
- if (args.outFile) return '';
1149
- if (command !== 'screenshot' && command !== 'smoke') return '';
1150
- return defaultArtifactDirectory();
1151
- }
1152
-
1153
- function addCommonArgs(cliArgs, args) {
1154
- addArg(cliArgs, 'adb', args.adb);
1155
- addArg(cliArgs, 'serial', args.serial);
1156
- addArg(cliArgs, 'port', args.port);
1157
- addArg(cliArgs, 'package-name', args.packageName);
1158
- }
1159
-
1160
- function addArg(cliArgs, name, value) {
1161
- if (value === undefined || value === null || value === '' || value === false) {
1162
- return;
1163
- }
1164
- cliArgs.push(`--${name}`, String(value));
1165
- }
1166
-
1167
- function addRepeatedArg(cliArgs, name, value) {
1168
- if (Array.isArray(value)) {
1169
- for (const item of value) addArg(cliArgs, name, item);
1170
- return;
1171
- }
1172
- addArg(cliArgs, name, value);
265
+ return toolResultForReply(await runtimeClient.run(args, { signal: clientConnection.signal }));
1173
266
  }
1174
267
 
1175
- function addExtraArgs(cliArgs, value) {
1176
- if (Array.isArray(value)) {
1177
- for (const item of value) addArg(cliArgs, 'extra', item);
1178
- return;
1179
- }
1180
- if (value && typeof value === 'object') {
1181
- for (const [key, extraValue] of Object.entries(value)) {
1182
- addArg(cliArgs, 'extra', `${key}=${extraValue}`);
1183
- }
1184
- return;
1185
- }
1186
- addArg(cliArgs, 'extra', value);
1187
- }
1188
-
1189
- function runProcess(cliArgs) {
1190
- return new Promise((resolve) => {
1191
- const child = spawn(nodeBinary, cliArgs, {
1192
- cwd: bridgeDir,
1193
- windowsHide: true,
1194
- });
1195
- let stdout = '';
1196
- let stderr = '';
1197
- child.stdout.on('data', (chunk) => {
1198
- stdout += chunk.toString();
1199
- });
1200
- child.stderr.on('data', (chunk) => {
1201
- stderr += chunk.toString();
1202
- });
1203
- child.on('error', (error) => {
1204
- resolve(toolText(`failed to start Node bridge CLI: ${error.message}`, true));
1205
- });
1206
- child.on('close', (code) => {
1207
- const text = [
1208
- stdout.trim(),
1209
- stderr.trim() ? `stderr:\n${stderr.trim()}` : '',
1210
- code === 0 ? '' : `exitCode: ${code}`,
1211
- retryWithPackageNameHint(cliArgs, stdout, stderr, code),
1212
- ].filter(Boolean).join('\n\n');
1213
- resolve(toolText(text || emptyProcessText(cliArgs), code !== 0));
1214
- });
1215
- });
1216
- }
1217
-
1218
- function emptyProcessText(cliArgs) {
1219
- const command = cliArgs[1] || '';
1220
- if (command === 'logcat' && cliArgs.includes('--app-pid')) {
1221
- return 'logcat: no matching lines for current app pid';
1222
- }
1223
- if (command === 'logcat') {
1224
- return 'logcat: no matching lines';
1225
- }
1226
- return 'ok';
1227
- }
1228
-
1229
- function retryWithPackageNameHint(cliArgs, stdout, stderr, code) {
1230
- if (code === 0 || cliArgs.includes('--package-name') || cliArgs.includes('--port')) {
1231
- return '';
1232
- }
1233
- const output = `${stdout}\n${stderr}`;
1234
- if (!/HTTP timeout: http:\/\/127\.0\.0\.1:18080\//.test(output)) {
1235
- return '';
268
+ function toolResultForReply({ value: result, history }) {
269
+ let tool;
270
+ if (typeof result === 'string') tool = toolText(result);
271
+ else if (Buffer.isBuffer(result)) tool = toolText(result.toString('utf8'));
272
+ else if (result === undefined) tool = toolJson({ ok: false, error: 'runtime_result_missing', dispatched: null, ambiguous: true }, true);
273
+ else {
274
+ if (history && result && typeof result === 'object' && !Array.isArray(result)) result = { ...result, _history: history };
275
+ tool = toolJson(result, Boolean(result && typeof result === 'object' && result.ok === false));
1236
276
  }
1237
- 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.';
277
+ if (history) tool._meta = { 'ai-app-bridge/history': history };
278
+ return tool;
1238
279
  }
1239
280
 
1240
281
  function toolText(text, isError = false) {
@@ -1288,12 +329,14 @@ if (require.main === module) {
1288
329
  }
1289
330
 
1290
331
  module.exports = {
1291
- buildBridgeCliArgs,
332
+ capabilityPayload,
333
+ callTool,
334
+ toolDefinitions,
335
+ toolResultForReply,
1292
336
  commandDomains,
1293
- defaultArtifactDirFor,
1294
337
  mcpHelpText,
1295
338
  readNextMessage,
1296
- runBatch,
339
+ runGeneric,
1297
340
  startServer,
1298
341
  supportedTargets,
1299
342
  };