mobile-debug-mcp 0.23.0 → 0.24.0
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/dist/interact/index.js +133 -57
- package/dist/server/common.js +66 -0
- package/dist/server/tool-definitions.js +921 -0
- package/dist/server/tool-handlers.js +320 -0
- package/dist/server-core.js +4 -801
- package/docs/CHANGELOG.md +3 -0
- package/docs/tools/TOOLS.md +15 -7
- package/docs/tools/interact.md +270 -107
- package/docs/tools/manage.md +39 -38
- package/docs/tools/observe.md +30 -8
- package/docs/tools/system.md +1 -1
- package/package.json +1 -1
- package/src/interact/index.ts +186 -58
- package/src/server/common.ts +95 -0
- package/src/server/tool-definitions.ts +921 -0
- package/src/server/tool-handlers.ts +365 -0
- package/src/server-core.ts +4 -844
- package/src/types.ts +59 -6
- package/test/unit/interact/expect_tools.test.ts +77 -0
- package/test/unit/interact/tap_element.test.ts +23 -6
- package/test/unit/server/contract.test.ts +26 -0
- package/test/unit/server/response_shapes.test.ts +69 -4
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { AndroidManage, iOSManage, ToolsManage } from '../manage/index.js';
|
|
2
|
+
import { ToolsInteract } from '../interact/index.js';
|
|
3
|
+
import { ToolsObserve } from '../observe/index.js';
|
|
4
|
+
import { classifyActionOutcome } from '../interact/classify.js';
|
|
5
|
+
import { ToolsNetwork } from '../network/index.js';
|
|
6
|
+
import { getSystemStatus } from '../system/index.js';
|
|
7
|
+
import { buildActionExecutionResult, captureActionFingerprint, inferGenericFailure, inferScrollFailure, wrapResponse } from './common.js';
|
|
8
|
+
async function handleStartApp(args) {
|
|
9
|
+
const { platform, appId, deviceId } = args;
|
|
10
|
+
const uiFingerprintBefore = await captureActionFingerprint(platform, deviceId);
|
|
11
|
+
ToolsNetwork.notifyActionStart();
|
|
12
|
+
const res = await (platform === 'android' ? new AndroidManage().startApp(appId, deviceId) : new iOSManage().startApp(appId, deviceId));
|
|
13
|
+
const uiFingerprintAfter = await captureActionFingerprint(platform, deviceId);
|
|
14
|
+
return wrapResponse(buildActionExecutionResult({
|
|
15
|
+
actionType: 'start_app',
|
|
16
|
+
selector: { appId },
|
|
17
|
+
success: !!res.appStarted,
|
|
18
|
+
uiFingerprintBefore,
|
|
19
|
+
uiFingerprintAfter,
|
|
20
|
+
failure: res.appStarted ? undefined : inferGenericFailure(res.error)
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
async function handleTerminateApp(args) {
|
|
24
|
+
const { platform, appId, deviceId } = args;
|
|
25
|
+
const res = await (platform === 'android' ? new AndroidManage().terminateApp(appId, deviceId) : new iOSManage().terminateApp(appId, deviceId));
|
|
26
|
+
const response = { device: res.device, appTerminated: res.appTerminated };
|
|
27
|
+
return wrapResponse(response);
|
|
28
|
+
}
|
|
29
|
+
async function handleRestartApp(args) {
|
|
30
|
+
const { platform, appId, deviceId } = args;
|
|
31
|
+
const uiFingerprintBefore = await captureActionFingerprint(platform, deviceId);
|
|
32
|
+
ToolsNetwork.notifyActionStart();
|
|
33
|
+
const res = await (platform === 'android' ? new AndroidManage().restartApp(appId, deviceId) : new iOSManage().restartApp(appId, deviceId));
|
|
34
|
+
const uiFingerprintAfter = await captureActionFingerprint(platform, deviceId);
|
|
35
|
+
return wrapResponse(buildActionExecutionResult({
|
|
36
|
+
actionType: 'restart_app',
|
|
37
|
+
selector: { appId },
|
|
38
|
+
success: !!res.appRestarted,
|
|
39
|
+
uiFingerprintBefore,
|
|
40
|
+
uiFingerprintAfter,
|
|
41
|
+
failure: res.appRestarted ? undefined : inferGenericFailure(res.error)
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
async function handleResetAppData(args) {
|
|
45
|
+
const { platform, appId, deviceId } = args;
|
|
46
|
+
const res = await (platform === 'android' ? new AndroidManage().resetAppData(appId, deviceId) : new iOSManage().resetAppData(appId, deviceId));
|
|
47
|
+
const response = { device: res.device, dataCleared: res.dataCleared };
|
|
48
|
+
return wrapResponse(response);
|
|
49
|
+
}
|
|
50
|
+
async function handleInstallApp(args) {
|
|
51
|
+
const { platform, projectType, appPath, deviceId } = args;
|
|
52
|
+
const res = await ToolsManage.installAppHandler({ platform, appPath, deviceId, projectType });
|
|
53
|
+
const response = {
|
|
54
|
+
device: res.device,
|
|
55
|
+
installed: res.installed,
|
|
56
|
+
output: res.output,
|
|
57
|
+
error: res.error
|
|
58
|
+
};
|
|
59
|
+
return wrapResponse(response);
|
|
60
|
+
}
|
|
61
|
+
async function handleBuildApp(args) {
|
|
62
|
+
const { platform, projectType, projectPath, variant } = args;
|
|
63
|
+
const res = await ToolsManage.buildAppHandler({ platform, projectPath, variant, projectType });
|
|
64
|
+
return wrapResponse(res);
|
|
65
|
+
}
|
|
66
|
+
async function handleBuildAndInstall(args) {
|
|
67
|
+
const { platform, projectType, projectPath, deviceId, timeout } = args;
|
|
68
|
+
const res = await ToolsManage.buildAndInstallHandler({ platform, projectPath, deviceId, timeout, projectType });
|
|
69
|
+
return {
|
|
70
|
+
content: [
|
|
71
|
+
{ type: 'text', text: res.ndjson },
|
|
72
|
+
{ type: 'text', text: JSON.stringify(res.result, null, 2) }
|
|
73
|
+
]
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
async function handleGetLogs(args) {
|
|
77
|
+
const { platform, appId, deviceId, pid, tag, level, contains, since_seconds, limit, lines } = args;
|
|
78
|
+
const res = await ToolsObserve.getLogsHandler({ platform, appId, deviceId, pid, tag, level, contains, since_seconds, limit, lines });
|
|
79
|
+
const filtered = !!(pid || tag || level || contains || since_seconds || appId);
|
|
80
|
+
return {
|
|
81
|
+
content: [
|
|
82
|
+
{ type: 'text', text: JSON.stringify({ device: res.device, result: { count: res.logCount, filtered, crashLines: (res.crashLines || []), source: res.source, meta: res.meta || {} } }, null, 2) },
|
|
83
|
+
{ type: 'text', text: JSON.stringify({ logs: res.logs }, null, 2) }
|
|
84
|
+
]
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
async function handleListDevices(args) {
|
|
88
|
+
const { platform, appId } = args;
|
|
89
|
+
const res = await ToolsManage.listDevicesHandler({ platform, appId });
|
|
90
|
+
return wrapResponse(res);
|
|
91
|
+
}
|
|
92
|
+
async function handleGetSystemStatus() {
|
|
93
|
+
const result = await getSystemStatus();
|
|
94
|
+
return wrapResponse(result);
|
|
95
|
+
}
|
|
96
|
+
async function handleCaptureScreenshot(args) {
|
|
97
|
+
const { platform, deviceId } = args;
|
|
98
|
+
const res = await ToolsObserve.captureScreenshotHandler({ platform, deviceId });
|
|
99
|
+
const mime = res.screenshot_mime || 'image/png';
|
|
100
|
+
const content = [
|
|
101
|
+
{ type: 'text', text: JSON.stringify({ device: res.device, result: { resolution: res.resolution, mimeType: mime } }, null, 2) },
|
|
102
|
+
{ type: 'image', data: res.screenshot, mimeType: mime }
|
|
103
|
+
];
|
|
104
|
+
if (res.screenshot_fallback) {
|
|
105
|
+
content.push({ type: 'text', text: JSON.stringify({ note: 'JPEG fallback included for compatibility', mimeType: res.screenshot_fallback_mime || 'image/jpeg' }) });
|
|
106
|
+
content.push({ type: 'image', data: res.screenshot_fallback, mimeType: res.screenshot_fallback_mime || 'image/jpeg' });
|
|
107
|
+
}
|
|
108
|
+
return { content };
|
|
109
|
+
}
|
|
110
|
+
async function handleCaptureDebugSnapshot(args) {
|
|
111
|
+
const { reason, includeLogs, logLines, platform, appId, deviceId, sessionId } = args;
|
|
112
|
+
const res = await ToolsObserve.captureDebugSnapshotHandler({ reason, includeLogs, logLines, platform, appId, deviceId, sessionId });
|
|
113
|
+
return wrapResponse(res);
|
|
114
|
+
}
|
|
115
|
+
async function handleGetUITree(args) {
|
|
116
|
+
const { platform, deviceId } = args;
|
|
117
|
+
const res = await ToolsObserve.getUITreeHandler({ platform, deviceId });
|
|
118
|
+
return wrapResponse(res);
|
|
119
|
+
}
|
|
120
|
+
async function handleGetCurrentScreen(args) {
|
|
121
|
+
const { deviceId } = args;
|
|
122
|
+
const res = await ToolsObserve.getCurrentScreenHandler({ deviceId });
|
|
123
|
+
return wrapResponse(res);
|
|
124
|
+
}
|
|
125
|
+
async function handleGetScreenFingerprint(args) {
|
|
126
|
+
const { platform, deviceId } = args;
|
|
127
|
+
const res = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId });
|
|
128
|
+
return wrapResponse(res);
|
|
129
|
+
}
|
|
130
|
+
async function handleWaitForScreenChange(args) {
|
|
131
|
+
const { platform, previousFingerprint, timeoutMs, pollIntervalMs, deviceId } = args;
|
|
132
|
+
const res = await ToolsInteract.waitForScreenChangeHandler({ platform, previousFingerprint, timeoutMs, pollIntervalMs, deviceId });
|
|
133
|
+
return wrapResponse(res);
|
|
134
|
+
}
|
|
135
|
+
async function handleExpectScreen(args) {
|
|
136
|
+
const { platform, fingerprint, screen, deviceId } = args;
|
|
137
|
+
const res = await ToolsInteract.expectScreenHandler({ platform, fingerprint, screen, deviceId });
|
|
138
|
+
return wrapResponse(res);
|
|
139
|
+
}
|
|
140
|
+
async function handleExpectElementVisible(args) {
|
|
141
|
+
const { selector, element_id, timeout_ms, poll_interval_ms, platform, deviceId } = args;
|
|
142
|
+
const res = await ToolsInteract.expectElementVisibleHandler({ selector, element_id, timeout_ms, poll_interval_ms, platform, deviceId });
|
|
143
|
+
return wrapResponse(res);
|
|
144
|
+
}
|
|
145
|
+
async function handleWaitForUI(args) {
|
|
146
|
+
const { selector, condition = 'exists', timeout_ms = 60000, poll_interval_ms = 300, match, retry, platform, deviceId } = args;
|
|
147
|
+
const res = await ToolsInteract.waitForUIHandler({ selector, condition, timeout_ms, poll_interval_ms, match, retry, platform, deviceId });
|
|
148
|
+
return wrapResponse(res);
|
|
149
|
+
}
|
|
150
|
+
async function handleFindElement(args) {
|
|
151
|
+
const { query, exact = false, timeoutMs = 3000, platform, deviceId } = args;
|
|
152
|
+
const res = await ToolsInteract.findElementHandler({ query, exact, timeoutMs, platform, deviceId });
|
|
153
|
+
return wrapResponse(res);
|
|
154
|
+
}
|
|
155
|
+
async function handleTap(args) {
|
|
156
|
+
const { platform, x, y, deviceId } = args;
|
|
157
|
+
const uiFingerprintBefore = await captureActionFingerprint(platform, deviceId);
|
|
158
|
+
ToolsNetwork.notifyActionStart();
|
|
159
|
+
const res = await ToolsInteract.tapHandler({ platform, x, y, deviceId });
|
|
160
|
+
const uiFingerprintAfter = await captureActionFingerprint(platform, deviceId);
|
|
161
|
+
return wrapResponse(buildActionExecutionResult({
|
|
162
|
+
actionType: 'tap',
|
|
163
|
+
selector: { x, y },
|
|
164
|
+
success: !!res.success,
|
|
165
|
+
uiFingerprintBefore,
|
|
166
|
+
uiFingerprintAfter,
|
|
167
|
+
failure: res.success ? undefined : inferGenericFailure(res.error)
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
async function handleTapElement(args) {
|
|
171
|
+
const { elementId } = args;
|
|
172
|
+
ToolsNetwork.notifyActionStart();
|
|
173
|
+
const res = await ToolsInteract.tapElementHandler({ elementId });
|
|
174
|
+
return wrapResponse(res);
|
|
175
|
+
}
|
|
176
|
+
async function handleSwipe(args) {
|
|
177
|
+
const { platform = 'android', x1, y1, x2, y2, duration, deviceId } = args;
|
|
178
|
+
const uiFingerprintBefore = await captureActionFingerprint(platform, deviceId);
|
|
179
|
+
ToolsNetwork.notifyActionStart();
|
|
180
|
+
const res = await ToolsInteract.swipeHandler({ platform, x1, y1, x2, y2, duration, deviceId });
|
|
181
|
+
const uiFingerprintAfter = await captureActionFingerprint(platform, deviceId);
|
|
182
|
+
return wrapResponse(buildActionExecutionResult({
|
|
183
|
+
actionType: 'swipe',
|
|
184
|
+
selector: { x1, y1, x2, y2, duration },
|
|
185
|
+
success: !!res.success,
|
|
186
|
+
uiFingerprintBefore,
|
|
187
|
+
uiFingerprintAfter,
|
|
188
|
+
failure: res.success ? undefined : inferGenericFailure(res.error)
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
async function handleScrollToElement(args) {
|
|
192
|
+
const { platform, selector, direction, maxScrolls, scrollAmount, deviceId } = args;
|
|
193
|
+
const uiFingerprintBefore = await captureActionFingerprint(platform, deviceId);
|
|
194
|
+
ToolsNetwork.notifyActionStart();
|
|
195
|
+
const res = await ToolsInteract.scrollToElementHandler({ platform, selector, direction, maxScrolls, scrollAmount, deviceId });
|
|
196
|
+
const uiFingerprintAfter = await captureActionFingerprint(platform, deviceId);
|
|
197
|
+
return wrapResponse(buildActionExecutionResult({
|
|
198
|
+
actionType: 'scroll_to_element',
|
|
199
|
+
selector,
|
|
200
|
+
resolved: res?.success && res?.element ? {
|
|
201
|
+
elementId: null,
|
|
202
|
+
text: res.element.text ?? null,
|
|
203
|
+
resource_id: res.element.resourceId ?? null,
|
|
204
|
+
accessibility_id: res.element.contentDesc ?? null,
|
|
205
|
+
class: res.element.className ?? null,
|
|
206
|
+
bounds: res.element.bounds ?? null,
|
|
207
|
+
index: null
|
|
208
|
+
} : null,
|
|
209
|
+
success: !!res.success,
|
|
210
|
+
uiFingerprintBefore,
|
|
211
|
+
uiFingerprintAfter,
|
|
212
|
+
failure: res.success ? undefined : inferScrollFailure(res.reason)
|
|
213
|
+
}));
|
|
214
|
+
}
|
|
215
|
+
async function handleTypeText(args) {
|
|
216
|
+
const { text, deviceId } = args;
|
|
217
|
+
const uiFingerprintBefore = await captureActionFingerprint('android', deviceId);
|
|
218
|
+
ToolsNetwork.notifyActionStart();
|
|
219
|
+
const res = await ToolsInteract.typeTextHandler({ text, deviceId });
|
|
220
|
+
const uiFingerprintAfter = await captureActionFingerprint('android', deviceId);
|
|
221
|
+
return wrapResponse(buildActionExecutionResult({
|
|
222
|
+
actionType: 'type_text',
|
|
223
|
+
selector: { text },
|
|
224
|
+
success: !!res.success,
|
|
225
|
+
uiFingerprintBefore,
|
|
226
|
+
uiFingerprintAfter,
|
|
227
|
+
failure: res.success ? undefined : inferGenericFailure(res.error)
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
230
|
+
async function handlePressBack(args) {
|
|
231
|
+
const { deviceId } = args;
|
|
232
|
+
const uiFingerprintBefore = await captureActionFingerprint('android', deviceId);
|
|
233
|
+
ToolsNetwork.notifyActionStart();
|
|
234
|
+
const res = await ToolsInteract.pressBackHandler({ deviceId });
|
|
235
|
+
const uiFingerprintAfter = await captureActionFingerprint('android', deviceId);
|
|
236
|
+
return wrapResponse(buildActionExecutionResult({
|
|
237
|
+
actionType: 'press_back',
|
|
238
|
+
selector: { key: 'back' },
|
|
239
|
+
success: !!res.success,
|
|
240
|
+
uiFingerprintBefore,
|
|
241
|
+
uiFingerprintAfter,
|
|
242
|
+
failure: res.success ? undefined : inferGenericFailure(res.error)
|
|
243
|
+
}));
|
|
244
|
+
}
|
|
245
|
+
async function handleStartLogStream(args) {
|
|
246
|
+
const { platform, packageName, level, sessionId, deviceId } = args;
|
|
247
|
+
const res = await ToolsObserve.startLogStreamHandler({ platform, packageName, level, sessionId, deviceId });
|
|
248
|
+
return wrapResponse(res);
|
|
249
|
+
}
|
|
250
|
+
async function handleReadLogStream(args) {
|
|
251
|
+
const { platform, sessionId, limit, since } = args;
|
|
252
|
+
const res = await ToolsObserve.readLogStreamHandler({ platform, sessionId, limit, since });
|
|
253
|
+
return wrapResponse(res);
|
|
254
|
+
}
|
|
255
|
+
async function handleStopLogStream(args) {
|
|
256
|
+
const { platform, sessionId } = args;
|
|
257
|
+
const res = await ToolsObserve.stopLogStreamHandler({ platform, sessionId });
|
|
258
|
+
return wrapResponse(res);
|
|
259
|
+
}
|
|
260
|
+
function handleClassifyActionOutcome(args) {
|
|
261
|
+
const { uiChanged, expectedElementVisible, networkRequests, hasLogErrors } = args;
|
|
262
|
+
const result = classifyActionOutcome({
|
|
263
|
+
uiChanged: Boolean(uiChanged),
|
|
264
|
+
expectedElementVisible: expectedElementVisible ?? null,
|
|
265
|
+
networkRequests: networkRequests ?? null,
|
|
266
|
+
hasLogErrors: hasLogErrors ?? null
|
|
267
|
+
});
|
|
268
|
+
return Promise.resolve(wrapResponse(result));
|
|
269
|
+
}
|
|
270
|
+
async function handleGetNetworkActivity(args) {
|
|
271
|
+
const { platform, deviceId } = args;
|
|
272
|
+
const result = await ToolsNetwork.getNetworkActivity({ platform, deviceId });
|
|
273
|
+
return wrapResponse(result);
|
|
274
|
+
}
|
|
275
|
+
export const toolHandlers = {
|
|
276
|
+
start_app: handleStartApp,
|
|
277
|
+
terminate_app: handleTerminateApp,
|
|
278
|
+
restart_app: handleRestartApp,
|
|
279
|
+
reset_app_data: handleResetAppData,
|
|
280
|
+
install_app: handleInstallApp,
|
|
281
|
+
build_app: handleBuildApp,
|
|
282
|
+
build_and_install: handleBuildAndInstall,
|
|
283
|
+
get_logs: handleGetLogs,
|
|
284
|
+
list_devices: handleListDevices,
|
|
285
|
+
get_system_status: handleGetSystemStatus,
|
|
286
|
+
capture_screenshot: handleCaptureScreenshot,
|
|
287
|
+
capture_debug_snapshot: handleCaptureDebugSnapshot,
|
|
288
|
+
get_ui_tree: handleGetUITree,
|
|
289
|
+
get_current_screen: handleGetCurrentScreen,
|
|
290
|
+
get_screen_fingerprint: handleGetScreenFingerprint,
|
|
291
|
+
wait_for_screen_change: handleWaitForScreenChange,
|
|
292
|
+
expect_screen: handleExpectScreen,
|
|
293
|
+
expect_element_visible: handleExpectElementVisible,
|
|
294
|
+
wait_for_ui: handleWaitForUI,
|
|
295
|
+
find_element: handleFindElement,
|
|
296
|
+
tap: handleTap,
|
|
297
|
+
tap_element: handleTapElement,
|
|
298
|
+
swipe: handleSwipe,
|
|
299
|
+
scroll_to_element: handleScrollToElement,
|
|
300
|
+
type_text: handleTypeText,
|
|
301
|
+
press_back: handlePressBack,
|
|
302
|
+
start_log_stream: handleStartLogStream,
|
|
303
|
+
read_log_stream: handleReadLogStream,
|
|
304
|
+
stop_log_stream: handleStopLogStream,
|
|
305
|
+
classify_action_outcome: handleClassifyActionOutcome,
|
|
306
|
+
get_network_activity: handleGetNetworkActivity
|
|
307
|
+
};
|
|
308
|
+
export async function handleToolCall(name, args = {}) {
|
|
309
|
+
const handler = toolHandlers[name];
|
|
310
|
+
if (!handler)
|
|
311
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
312
|
+
try {
|
|
313
|
+
return await handler(args);
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
return {
|
|
317
|
+
content: [{ type: 'text', text: `Error executing tool ${name}: ${error instanceof Error ? error.message : String(error)}` }]
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
}
|