mcp-maestro-mobile-ai 1.1.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/CHANGELOG.md +114 -0
- package/CONTRIBUTING.md +417 -0
- package/LICENSE +22 -0
- package/README.md +719 -0
- package/ROADMAP.md +239 -0
- package/docs/ENTERPRISE_READINESS.md +545 -0
- package/docs/MCP_SETUP.md +180 -0
- package/docs/PRIVACY.md +198 -0
- package/docs/REACT_NATIVE_AUTOMATION_GUIDELINES.md +584 -0
- package/docs/SECURITY.md +573 -0
- package/package.json +69 -0
- package/prompts/example-login-tests.txt +9 -0
- package/prompts/example-youtube-tests.txt +8 -0
- package/src/mcp-server/index.js +625 -0
- package/src/mcp-server/tools/contextTools.js +194 -0
- package/src/mcp-server/tools/promptTools.js +191 -0
- package/src/mcp-server/tools/runTools.js +357 -0
- package/src/mcp-server/tools/utilityTools.js +721 -0
- package/src/mcp-server/tools/validateTools.js +220 -0
- package/src/mcp-server/utils/appContext.js +295 -0
- package/src/mcp-server/utils/logger.js +52 -0
- package/src/mcp-server/utils/maestro.js +508 -0
- package/templates/mcp-config-claude-desktop.json +15 -0
- package/templates/mcp-config-cursor.json +15 -0
- package/templates/mcp-config-vscode.json +13 -0
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maestro Utility Functions
|
|
3
|
+
* Execute Maestro CLI commands and handle results
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { spawn, execSync } from "child_process";
|
|
7
|
+
import fs from "fs/promises";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import os from "os";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
import { dirname, join } from "path";
|
|
12
|
+
import { logger } from "./logger.js";
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = dirname(__filename);
|
|
16
|
+
|
|
17
|
+
// Directories - Use system-level hidden paths for temp files
|
|
18
|
+
// This ensures YAML files are not visible in user's project
|
|
19
|
+
const USER_HOME = os.homedir();
|
|
20
|
+
const APP_DATA_DIR = join(USER_HOME, ".maestro-mcp");
|
|
21
|
+
const TEMP_DIR = join(APP_DATA_DIR, "temp");
|
|
22
|
+
const OUTPUT_DIR = join(APP_DATA_DIR, "output");
|
|
23
|
+
const SCREENSHOTS_DIR = join(OUTPUT_DIR, "screenshots");
|
|
24
|
+
const RESULTS_DIR = join(OUTPUT_DIR, "results");
|
|
25
|
+
const CONTEXT_DIR = join(APP_DATA_DIR, "context");
|
|
26
|
+
|
|
27
|
+
// Configuration from environment
|
|
28
|
+
const DEFAULT_TIMEOUT = parseInt(process.env.DEFAULT_WAIT_TIMEOUT) || 10000;
|
|
29
|
+
const DEFAULT_RETRIES = parseInt(process.env.DEFAULT_RETRIES) || 0;
|
|
30
|
+
const MAX_RESULTS = parseInt(process.env.MAX_RESULTS) || 50;
|
|
31
|
+
|
|
32
|
+
// Selected device (in-memory state)
|
|
33
|
+
let selectedDeviceId = process.env.MAESTRO_DEVICE || null;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get ADB command path
|
|
37
|
+
* Uses ANDROID_HOME if set, otherwise falls back to 'adb' in PATH
|
|
38
|
+
*/
|
|
39
|
+
function getAdbPath() {
|
|
40
|
+
const androidHome = process.env.ANDROID_HOME;
|
|
41
|
+
if (androidHome) {
|
|
42
|
+
// Use path.join for cross-platform compatibility
|
|
43
|
+
return path.join(androidHome, "platform-tools", "adb");
|
|
44
|
+
}
|
|
45
|
+
return "adb";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Execute Maestro CLI command with optional device targeting
|
|
50
|
+
*/
|
|
51
|
+
export function executeMaestro(args, deviceId = null) {
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
// Use specified device, selected device, or let Maestro choose
|
|
54
|
+
const targetDevice = deviceId || selectedDeviceId;
|
|
55
|
+
const finalArgs = targetDevice ? ["--device", targetDevice, ...args] : args;
|
|
56
|
+
|
|
57
|
+
const maestro = spawn("maestro", finalArgs, {
|
|
58
|
+
shell: true,
|
|
59
|
+
env: { ...process.env },
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
let stdout = "";
|
|
63
|
+
let stderr = "";
|
|
64
|
+
|
|
65
|
+
maestro.stdout.on("data", (data) => {
|
|
66
|
+
stdout += data.toString();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
maestro.stderr.on("data", (data) => {
|
|
70
|
+
stderr += data.toString();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
maestro.on("close", (exitCode) => {
|
|
74
|
+
resolve({ exitCode, stdout, stderr });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
maestro.on("error", (error) => {
|
|
78
|
+
resolve({ exitCode: 1, stdout, stderr: error.message });
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* List all connected devices with detailed information
|
|
85
|
+
*/
|
|
86
|
+
export async function listDevices() {
|
|
87
|
+
try {
|
|
88
|
+
const adbPath = getAdbPath();
|
|
89
|
+
let adbOutput;
|
|
90
|
+
try {
|
|
91
|
+
adbOutput = execSync(`"${adbPath}" devices -l`, {
|
|
92
|
+
encoding: "utf8",
|
|
93
|
+
timeout: 5000,
|
|
94
|
+
});
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return {
|
|
97
|
+
success: false,
|
|
98
|
+
error:
|
|
99
|
+
"ADB not found. Make sure Android SDK is installed and ANDROID_HOME is set.",
|
|
100
|
+
devices: [],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Parse ADB output with detailed info
|
|
105
|
+
const lines = adbOutput.split("\n").filter((line) => line.trim());
|
|
106
|
+
const devices = lines
|
|
107
|
+
.slice(1) // Skip header "List of devices attached"
|
|
108
|
+
.map((line) => {
|
|
109
|
+
// Format: "emulator-5554 device product:sdk_gphone64_x86_64 model:sdk_gphone64_x86_64 device:emu64xa transport_id:1"
|
|
110
|
+
// Or: "RF8M12345XY device usb:1-1 product:starqltesq model:SM_G965U device:starqltesq transport_id:2"
|
|
111
|
+
const parts = line.split(/\s+/);
|
|
112
|
+
if (parts.length >= 2 && parts[1] === "device") {
|
|
113
|
+
const id = parts[0];
|
|
114
|
+
const isEmulator = id.startsWith("emulator-");
|
|
115
|
+
|
|
116
|
+
// Extract model name if available
|
|
117
|
+
const modelMatch = line.match(/model:(\S+)/);
|
|
118
|
+
const model = modelMatch ? modelMatch[1].replace(/_/g, " ") : null;
|
|
119
|
+
|
|
120
|
+
// Extract product name
|
|
121
|
+
const productMatch = line.match(/product:(\S+)/);
|
|
122
|
+
const product = productMatch ? productMatch[1] : null;
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
id,
|
|
126
|
+
type: isEmulator ? "emulator" : "physical",
|
|
127
|
+
model:
|
|
128
|
+
model || (isEmulator ? "Android Emulator" : "Unknown Device"),
|
|
129
|
+
product,
|
|
130
|
+
status: "connected",
|
|
131
|
+
isSelected: id === selectedDeviceId,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
})
|
|
136
|
+
.filter((d) => d !== null);
|
|
137
|
+
|
|
138
|
+
logger.info(`Found ${devices.length} device(s)`);
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
success: true,
|
|
142
|
+
devices,
|
|
143
|
+
count: devices.length,
|
|
144
|
+
selectedDevice: selectedDeviceId,
|
|
145
|
+
};
|
|
146
|
+
} catch (error) {
|
|
147
|
+
logger.error("List devices failed", { error: error.message });
|
|
148
|
+
return {
|
|
149
|
+
success: false,
|
|
150
|
+
error: error.message,
|
|
151
|
+
devices: [],
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Select a device for running tests
|
|
158
|
+
*/
|
|
159
|
+
export function selectDevice(deviceId) {
|
|
160
|
+
selectedDeviceId = deviceId;
|
|
161
|
+
logger.info(`Device selected: ${deviceId}`);
|
|
162
|
+
return {
|
|
163
|
+
success: true,
|
|
164
|
+
selectedDevice: deviceId,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get the currently selected device
|
|
170
|
+
*/
|
|
171
|
+
export function getSelectedDevice() {
|
|
172
|
+
return selectedDeviceId;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Clear device selection (use default)
|
|
177
|
+
*/
|
|
178
|
+
export function clearDeviceSelection() {
|
|
179
|
+
selectedDeviceId = null;
|
|
180
|
+
logger.info("Device selection cleared, will use default");
|
|
181
|
+
return {
|
|
182
|
+
success: true,
|
|
183
|
+
message:
|
|
184
|
+
"Device selection cleared. Maestro will use the first available device.",
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Check if ADB is available and device is connected
|
|
190
|
+
*/
|
|
191
|
+
export async function checkDeviceConnection() {
|
|
192
|
+
try {
|
|
193
|
+
const adbPath = getAdbPath();
|
|
194
|
+
// Check if ADB is available
|
|
195
|
+
let adbOutput;
|
|
196
|
+
try {
|
|
197
|
+
adbOutput = execSync(`"${adbPath}" devices`, {
|
|
198
|
+
encoding: "utf8",
|
|
199
|
+
timeout: 5000,
|
|
200
|
+
});
|
|
201
|
+
} catch (error) {
|
|
202
|
+
return {
|
|
203
|
+
connected: false,
|
|
204
|
+
error:
|
|
205
|
+
"ADB not found. Make sure Android SDK is installed and ANDROID_HOME is set.",
|
|
206
|
+
devices: [],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Parse ADB output
|
|
211
|
+
const lines = adbOutput.split("\n").filter((line) => line.trim());
|
|
212
|
+
const devices = lines
|
|
213
|
+
.slice(1) // Skip header
|
|
214
|
+
.map((line) => {
|
|
215
|
+
const parts = line.split("\t");
|
|
216
|
+
if (parts.length >= 2) {
|
|
217
|
+
return {
|
|
218
|
+
id: parts[0].trim(),
|
|
219
|
+
status: parts[1].trim(),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
})
|
|
224
|
+
.filter((d) => d && d.status === "device");
|
|
225
|
+
|
|
226
|
+
if (devices.length === 0) {
|
|
227
|
+
return {
|
|
228
|
+
connected: false,
|
|
229
|
+
error:
|
|
230
|
+
"No Android device/emulator connected. Start an emulator or connect a device.",
|
|
231
|
+
devices: [],
|
|
232
|
+
hint: "Run: emulator -avd YOUR_AVD_NAME",
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
logger.info(`Found ${devices.length} connected device(s)`);
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
connected: true,
|
|
240
|
+
devices,
|
|
241
|
+
activeDevice: selectedDeviceId || devices[0].id,
|
|
242
|
+
selectedDevice: selectedDeviceId,
|
|
243
|
+
};
|
|
244
|
+
} catch (error) {
|
|
245
|
+
logger.error("Device check failed", { error: error.message });
|
|
246
|
+
return {
|
|
247
|
+
connected: false,
|
|
248
|
+
error: error.message,
|
|
249
|
+
devices: [],
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Check if app is installed on device (optionally on specific device)
|
|
256
|
+
*/
|
|
257
|
+
export async function checkAppInstalled(appId, deviceId = null) {
|
|
258
|
+
if (!appId) {
|
|
259
|
+
return {
|
|
260
|
+
installed: false,
|
|
261
|
+
error: "APP_ID not configured",
|
|
262
|
+
hint: "Set APP_ID in .env file or MCP config",
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const targetDevice = deviceId || selectedDeviceId;
|
|
267
|
+
const adbPath = getAdbPath();
|
|
268
|
+
const adbPrefix = targetDevice
|
|
269
|
+
? `"${adbPath}" -s ${targetDevice}`
|
|
270
|
+
: `"${adbPath}"`;
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
const result = execSync(`${adbPrefix} shell pm list packages ${appId}`, {
|
|
274
|
+
encoding: "utf8",
|
|
275
|
+
timeout: 10000,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const isInstalled = result.includes(appId);
|
|
279
|
+
|
|
280
|
+
if (!isInstalled) {
|
|
281
|
+
return {
|
|
282
|
+
installed: false,
|
|
283
|
+
appId,
|
|
284
|
+
device: targetDevice,
|
|
285
|
+
error: `App "${appId}" is not installed on the device`,
|
|
286
|
+
hint: "Install the app on the device before running tests",
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
logger.info(
|
|
291
|
+
`App verified: ${appId} on device: ${targetDevice || "default"}`
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
installed: true,
|
|
296
|
+
appId,
|
|
297
|
+
device: targetDevice,
|
|
298
|
+
};
|
|
299
|
+
} catch (error) {
|
|
300
|
+
// If command fails, try alternative approach
|
|
301
|
+
try {
|
|
302
|
+
const result = execSync(`${adbPrefix} shell pm list packages`, {
|
|
303
|
+
encoding: "utf8",
|
|
304
|
+
timeout: 15000,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const isInstalled = result.includes(appId);
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
installed: isInstalled,
|
|
311
|
+
appId,
|
|
312
|
+
device: targetDevice,
|
|
313
|
+
error: isInstalled
|
|
314
|
+
? null
|
|
315
|
+
: `App "${appId}" is not installed on the device`,
|
|
316
|
+
};
|
|
317
|
+
} catch (e) {
|
|
318
|
+
return {
|
|
319
|
+
installed: false,
|
|
320
|
+
appId,
|
|
321
|
+
error:
|
|
322
|
+
"Could not verify app installation. Make sure device is connected.",
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Run a Maestro flow from YAML content with retry support
|
|
330
|
+
*/
|
|
331
|
+
export async function runMaestroFlow(yamlContent, testName, options = {}) {
|
|
332
|
+
const maxRetries = options.retries ?? DEFAULT_RETRIES;
|
|
333
|
+
const deviceId = options.deviceId || selectedDeviceId;
|
|
334
|
+
const startTime = Date.now();
|
|
335
|
+
|
|
336
|
+
// Ensure directories exist
|
|
337
|
+
await fs.mkdir(TEMP_DIR, { recursive: true });
|
|
338
|
+
await fs.mkdir(SCREENSHOTS_DIR, { recursive: true });
|
|
339
|
+
|
|
340
|
+
// Write YAML to temp file
|
|
341
|
+
const tempFile = join(TEMP_DIR, `${testName}-${Date.now()}.yaml`);
|
|
342
|
+
await fs.writeFile(tempFile, yamlContent, "utf8");
|
|
343
|
+
|
|
344
|
+
let lastResult = null;
|
|
345
|
+
let attempts = 0;
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
// Retry loop
|
|
349
|
+
while (attempts <= maxRetries) {
|
|
350
|
+
attempts++;
|
|
351
|
+
|
|
352
|
+
if (attempts > 1) {
|
|
353
|
+
logger.info(
|
|
354
|
+
`Retry ${attempts - 1}/${maxRetries} for test: ${testName}`
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Execute Maestro with device targeting
|
|
359
|
+
const result = await executeMaestro(["test", tempFile], deviceId);
|
|
360
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
361
|
+
|
|
362
|
+
if (result.exitCode === 0) {
|
|
363
|
+
logger.info(
|
|
364
|
+
`Test passed: ${testName} in ${duration}s (attempt ${attempts})${
|
|
365
|
+
deviceId ? ` on device ${deviceId}` : ""
|
|
366
|
+
}`
|
|
367
|
+
);
|
|
368
|
+
return {
|
|
369
|
+
success: true,
|
|
370
|
+
name: testName,
|
|
371
|
+
duration,
|
|
372
|
+
attempts,
|
|
373
|
+
device: deviceId || "default",
|
|
374
|
+
output: result.stdout,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Test failed
|
|
379
|
+
lastResult = {
|
|
380
|
+
success: false,
|
|
381
|
+
name: testName,
|
|
382
|
+
duration,
|
|
383
|
+
attempts,
|
|
384
|
+
device: deviceId || "default",
|
|
385
|
+
error: parseErrorMessage(result.stderr || result.stdout),
|
|
386
|
+
rawError: result.stderr || result.stdout,
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
// If we have more retries, continue
|
|
390
|
+
if (attempts <= maxRetries) {
|
|
391
|
+
logger.warn(`Test failed, will retry: ${testName}`);
|
|
392
|
+
// Small delay before retry
|
|
393
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// All retries exhausted
|
|
398
|
+
logger.error(`Test failed after ${attempts} attempts: ${testName}`);
|
|
399
|
+
|
|
400
|
+
// Capture failure screenshot
|
|
401
|
+
const screenshotPath = await captureScreenshot(
|
|
402
|
+
`${testName}-failure`,
|
|
403
|
+
deviceId
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
...lastResult,
|
|
408
|
+
screenshot: screenshotPath,
|
|
409
|
+
retriesExhausted: maxRetries > 0,
|
|
410
|
+
};
|
|
411
|
+
} finally {
|
|
412
|
+
// Clean up temp file
|
|
413
|
+
try {
|
|
414
|
+
await fs.unlink(tempFile);
|
|
415
|
+
} catch (e) {
|
|
416
|
+
// Ignore cleanup errors
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Parse and improve error messages
|
|
423
|
+
*/
|
|
424
|
+
function parseErrorMessage(rawError) {
|
|
425
|
+
if (!rawError) return "Unknown error";
|
|
426
|
+
|
|
427
|
+
// Element not found
|
|
428
|
+
if (rawError.includes("Element not found")) {
|
|
429
|
+
const match = rawError.match(/Element not found: (.+)/);
|
|
430
|
+
const element = match ? match[1] : "unknown element";
|
|
431
|
+
return `Element not found: ${element}. The element may not exist, or the app may still be loading. Try adding more wait time or check if the selector is correct.`;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Timeout
|
|
435
|
+
if (rawError.includes("timeout") || rawError.includes("Timeout")) {
|
|
436
|
+
return "Operation timed out. The app may be slow or the element took too long to appear. Consider increasing the timeout value.";
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// App not installed
|
|
440
|
+
if (
|
|
441
|
+
rawError.includes("App not installed") ||
|
|
442
|
+
rawError.includes("could not be found")
|
|
443
|
+
) {
|
|
444
|
+
return "App is not installed on the device. Install the app before running tests.";
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// No device
|
|
448
|
+
if (rawError.includes("No device") || rawError.includes("no devices")) {
|
|
449
|
+
return "No Android device/emulator connected. Start an emulator or connect a device.";
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Truncate very long errors
|
|
453
|
+
if (rawError.length > 500) {
|
|
454
|
+
return rawError.substring(0, 500) + "... (truncated)";
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return rawError;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Capture a screenshot (optionally on specific device)
|
|
462
|
+
*/
|
|
463
|
+
export async function captureScreenshot(name, deviceId = null) {
|
|
464
|
+
await fs.mkdir(SCREENSHOTS_DIR, { recursive: true });
|
|
465
|
+
|
|
466
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
467
|
+
const screenshotPath = join(SCREENSHOTS_DIR, `${name}-${timestamp}.png`);
|
|
468
|
+
|
|
469
|
+
try {
|
|
470
|
+
const result = await executeMaestro(
|
|
471
|
+
["screenshot", screenshotPath],
|
|
472
|
+
deviceId
|
|
473
|
+
);
|
|
474
|
+
if (result.exitCode === 0) {
|
|
475
|
+
logger.info(`Screenshot saved: ${screenshotPath}`);
|
|
476
|
+
return screenshotPath;
|
|
477
|
+
}
|
|
478
|
+
return null;
|
|
479
|
+
} catch (error) {
|
|
480
|
+
logger.warn("Failed to capture screenshot", { error: error.message });
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Get configuration values
|
|
487
|
+
*/
|
|
488
|
+
export function getConfig() {
|
|
489
|
+
return {
|
|
490
|
+
defaultTimeout: DEFAULT_TIMEOUT,
|
|
491
|
+
defaultRetries: DEFAULT_RETRIES,
|
|
492
|
+
maxResults: MAX_RESULTS,
|
|
493
|
+
selectedDevice: selectedDeviceId,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export default {
|
|
498
|
+
executeMaestro,
|
|
499
|
+
runMaestroFlow,
|
|
500
|
+
captureScreenshot,
|
|
501
|
+
checkDeviceConnection,
|
|
502
|
+
checkAppInstalled,
|
|
503
|
+
listDevices,
|
|
504
|
+
selectDevice,
|
|
505
|
+
getSelectedDevice,
|
|
506
|
+
clearDeviceSelection,
|
|
507
|
+
getConfig,
|
|
508
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"maestro": {
|
|
4
|
+
"command": "npx",
|
|
5
|
+
"args": ["mcp-maestro-mobile-ai"],
|
|
6
|
+
"env": {
|
|
7
|
+
"APP_ID": "com.your.app.package",
|
|
8
|
+
"ANDROID_HOME": "/path/to/android/sdk",
|
|
9
|
+
"DEFAULT_RETRIES": "1",
|
|
10
|
+
"MAX_RESULTS": "50"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"maestro": {
|
|
4
|
+
"command": "npx",
|
|
5
|
+
"args": ["mcp-maestro-mobile-ai"],
|
|
6
|
+
"env": {
|
|
7
|
+
"APP_ID": "com.your.app.package",
|
|
8
|
+
"ANDROID_HOME": "/path/to/android/sdk",
|
|
9
|
+
"DEFAULT_RETRIES": "1",
|
|
10
|
+
"MAX_RESULTS": "50"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|