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