mobile-ai-agents 1.0.35 → 1.0.36
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 +10 -2
- package/cli/commands/doctor.js +182 -0
- package/cli/commands/mcp.js +330 -0
- package/cli/commands/router.js +32 -0
- package/cli/index.js +24 -37
- package/cli/lib/options.js +24 -0
- package/docs/android-device-setup.md +138 -0
- package/docs/index.html +6 -1
- package/docs/installation.md +8 -0
- package/docs/mobile-mcp.md +14 -0
- package/docs/release-process.md +1 -0
- package/docs/wiki.md +10 -0
- package/package.json +4 -1
- package/skills/cross-platform/mobile-mcp-qa.md +14 -0
- package/templates/android-device-config.json +16 -0
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Android · iOS · Flutter · React Native · Kotlin Multiplatform · Unity · Un
|
|
|
11
11
|
[](https://github.com/salmanashraf/mobile-agency/stargazers)
|
|
12
12
|
[](#core-loops-and-platform-plugins)
|
|
13
13
|
|
|
14
|
-
[Installation Guide](docs/installation.md) · [Core Loops](docs/core-loops.md) · [mobile-karpathy](mobile-karpathy.md) · [Wiki](https://github.com/salmanashraf/mobile-
|
|
14
|
+
[Installation Guide](docs/installation.md) · [Android Device Setup](docs/android-device-setup.md) · [Core Loops](docs/core-loops.md) · [mobile-karpathy](mobile-karpathy.md) · [Wiki](https://github.com/salmanashraf/mobile-ai-agents/wiki) · [Getting Started](docs/getting-started.md) · [Contributing](CONTRIBUTING.md)
|
|
15
15
|
|
|
16
16
|
> AI agents, skills, and Loop Engineering workflows for building, testing, reviewing, and shipping Android, iOS, Flutter, React Native, Unity, and Unreal apps.
|
|
17
17
|
|
|
@@ -398,6 +398,14 @@ Mobile MCP fits the QA stage next: use it for emulator, simulator, or real-devic
|
|
|
398
398
|
|
|
399
399
|
Mobile MCP gives Mobile AI Agents a device automation layer for iOS and Android simulators, emulators, and real devices.
|
|
400
400
|
|
|
401
|
+
Prepare an Android project with environment diagnostics and settings-preserving MCP setup:
|
|
402
|
+
|
|
403
|
+
```bash
|
|
404
|
+
npx mobile-ai-agents doctor --platform android
|
|
405
|
+
npx mobile-ai-agents mcp setup --client claude
|
|
406
|
+
npx mobile-ai-agents mcp config init --app-id com.example.app
|
|
407
|
+
```
|
|
408
|
+
|
|
401
409
|
```
|
|
402
410
|
/mobile-mcp-qa
|
|
403
411
|
1. List devices
|
|
@@ -408,7 +416,7 @@ Mobile MCP gives Mobile AI Agents a device automation layer for iOS and Android
|
|
|
408
416
|
6. Produce MOBILE_MCP_QA_REPORT.md
|
|
409
417
|
```
|
|
410
418
|
|
|
411
|
-
Use it inside APPFORGE Stage 7 Full QA, UI match review, launch readiness checks, and screenshot validation.
|
|
419
|
+
Use it inside APPFORGE Stage 7 Full QA, UI match review, launch readiness checks, and screenshot validation. Guides: [Android Device QA Setup](docs/android-device-setup.md), [wiki guide](https://github.com/salmanashraf/mobile-ai-agents/wiki/Android-Device-QA-Setup), and [Mobile MCP Integration](docs/mobile-mcp.md).
|
|
412
420
|
|
|
413
421
|
### Device Proof Reports
|
|
414
422
|
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const childProcess = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { parseOptions } = require('../lib/options');
|
|
8
|
+
const { inspectMcpConfiguration, loadProjectConfig } = require('./mcp');
|
|
9
|
+
|
|
10
|
+
function executableOnPath(name, env, fsImpl) {
|
|
11
|
+
const entries = String(env.PATH || '').split(path.delimiter).filter(Boolean);
|
|
12
|
+
for (const entry of entries) {
|
|
13
|
+
const candidate = path.join(entry, name);
|
|
14
|
+
if (fsImpl.existsSync(candidate)) return candidate;
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function defaultRunner(command, args, env) {
|
|
20
|
+
const result = childProcess.spawnSync(command, args, {
|
|
21
|
+
encoding: 'utf8',
|
|
22
|
+
env,
|
|
23
|
+
timeout: 10000,
|
|
24
|
+
});
|
|
25
|
+
return {
|
|
26
|
+
status: result.status,
|
|
27
|
+
stdout: result.stdout || '',
|
|
28
|
+
stderr: result.stderr || '',
|
|
29
|
+
error: result.error ? result.error.message : '',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resultStatus(checks) {
|
|
34
|
+
if (checks.some(check => check.status === 'FAIL')) return 'FAIL';
|
|
35
|
+
if (checks.some(check => check.status === 'WARN')) return 'WARN';
|
|
36
|
+
return 'PASS';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseAdbDevices(output) {
|
|
40
|
+
return String(output)
|
|
41
|
+
.split(/\r?\n/)
|
|
42
|
+
.slice(1)
|
|
43
|
+
.map(line => line.trim())
|
|
44
|
+
.filter(Boolean)
|
|
45
|
+
.map(line => {
|
|
46
|
+
const [serial, state] = line.split(/\s+/);
|
|
47
|
+
return { serial, state };
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function collectAndroidDoctor(options = {}) {
|
|
52
|
+
const cwd = options.cwd || process.cwd();
|
|
53
|
+
const env = options.env || process.env;
|
|
54
|
+
const homedir = options.homedir || os.homedir();
|
|
55
|
+
const fsImpl = options.fsImpl || fs;
|
|
56
|
+
const runner = options.runner || defaultRunner;
|
|
57
|
+
const checks = [];
|
|
58
|
+
const sdkCandidates = [
|
|
59
|
+
env.ANDROID_HOME,
|
|
60
|
+
env.ANDROID_SDK_ROOT,
|
|
61
|
+
path.join(homedir, 'Library', 'Android', 'sdk'),
|
|
62
|
+
path.join(homedir, 'Android', 'Sdk'),
|
|
63
|
+
].filter(Boolean);
|
|
64
|
+
const sdkRoot = sdkCandidates.find(candidate => fsImpl.existsSync(candidate));
|
|
65
|
+
|
|
66
|
+
checks.push(sdkRoot
|
|
67
|
+
? { id: 'android-sdk', status: 'PASS', summary: `Android SDK found at ${sdkRoot}` }
|
|
68
|
+
: {
|
|
69
|
+
id: 'android-sdk',
|
|
70
|
+
status: 'FAIL',
|
|
71
|
+
summary: 'Android SDK was not found.',
|
|
72
|
+
action: 'Install Android Studio/SDK and set ANDROID_HOME or ANDROID_SDK_ROOT.',
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const adb = executableOnPath('adb', env, fsImpl) ||
|
|
76
|
+
(sdkRoot && fsImpl.existsSync(path.join(sdkRoot, 'platform-tools', 'adb')) ? path.join(sdkRoot, 'platform-tools', 'adb') : null);
|
|
77
|
+
if (!adb) {
|
|
78
|
+
checks.push({
|
|
79
|
+
id: 'adb',
|
|
80
|
+
status: 'FAIL',
|
|
81
|
+
summary: 'adb was not found.',
|
|
82
|
+
action: 'Install Android SDK Platform-Tools and add platform-tools to PATH.',
|
|
83
|
+
});
|
|
84
|
+
} else {
|
|
85
|
+
const version = runner(adb, ['version'], env);
|
|
86
|
+
checks.push(version.status === 0
|
|
87
|
+
? { id: 'adb', status: 'PASS', summary: `adb available at ${adb}`, details: version.stdout.split(/\r?\n/)[0] }
|
|
88
|
+
: { id: 'adb', status: 'FAIL', summary: 'adb exists but could not run.', details: version.stderr || version.error, action: 'Repair Platform-Tools or PATH permissions.' });
|
|
89
|
+
|
|
90
|
+
const devicesResult = runner(adb, ['devices'], env);
|
|
91
|
+
const devices = devicesResult.status === 0 ? parseAdbDevices(devicesResult.stdout) : [];
|
|
92
|
+
checks.push(devicesResult.status !== 0
|
|
93
|
+
? { id: 'android-devices', status: 'FAIL', summary: 'adb could not list devices.', details: devicesResult.stderr || devicesResult.error, action: 'Restart adb and reconnect or authorize the target device.' }
|
|
94
|
+
: devices.some(device => device.state === 'device')
|
|
95
|
+
? { id: 'android-devices', status: 'PASS', summary: `${devices.filter(device => device.state === 'device').length} ready Android device(s).`, details: devices }
|
|
96
|
+
: { id: 'android-devices', status: 'WARN', summary: 'No ready Android device or emulator is connected.', details: devices, action: 'Start an emulator or connect and authorize a device.' });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const emulator = executableOnPath('emulator', env, fsImpl) ||
|
|
100
|
+
(sdkRoot && fsImpl.existsSync(path.join(sdkRoot, 'emulator', 'emulator')) ? path.join(sdkRoot, 'emulator', 'emulator') : null);
|
|
101
|
+
if (!emulator) {
|
|
102
|
+
checks.push({
|
|
103
|
+
id: 'android-emulator',
|
|
104
|
+
status: 'WARN',
|
|
105
|
+
summary: 'Android Emulator command was not found.',
|
|
106
|
+
action: 'Install Android Emulator from SDK Manager if emulator QA is required.',
|
|
107
|
+
});
|
|
108
|
+
} else {
|
|
109
|
+
const avdResult = runner(emulator, ['-list-avds'], env);
|
|
110
|
+
const avds = avdResult.status === 0 ? avdResult.stdout.split(/\r?\n/).map(v => v.trim()).filter(Boolean) : [];
|
|
111
|
+
checks.push(avdResult.status !== 0
|
|
112
|
+
? { id: 'android-emulator', status: 'WARN', summary: 'Emulator exists but AVDs could not be listed.', details: avdResult.stderr || avdResult.error, action: 'Open Android Studio Device Manager and repair the emulator installation.' }
|
|
113
|
+
: avds.length
|
|
114
|
+
? { id: 'android-emulator', status: 'PASS', summary: `${avds.length} Android Virtual Device image(s) available.`, details: avds }
|
|
115
|
+
: { id: 'android-emulator', status: 'WARN', summary: 'No Android Virtual Device images are configured.', action: 'Create an AVD in Android Studio Device Manager.' });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const mcpConfigs = inspectMcpConfiguration({ cwd, homedir, fsImpl });
|
|
119
|
+
checks.push(mcpConfigs.length
|
|
120
|
+
? { id: 'mobile-mcp', status: 'PASS', summary: `Mobile MCP configured for ${mcpConfigs.map(item => item.client).join(', ')}.`, details: mcpConfigs.map(item => item.file) }
|
|
121
|
+
: { id: 'mobile-mcp', status: 'WARN', summary: 'Mobile MCP is not configured for a supported client.', action: 'Run mobile-ai-agents mcp setup --client <client>.' });
|
|
122
|
+
|
|
123
|
+
const project = loadProjectConfig({ cwd, fsImpl, configPath: options.configPath });
|
|
124
|
+
if (!project.config) {
|
|
125
|
+
checks.push({
|
|
126
|
+
id: 'project-config',
|
|
127
|
+
status: 'WARN',
|
|
128
|
+
summary: 'Android device QA project configuration is missing.',
|
|
129
|
+
action: 'Run mobile-ai-agents mcp config init --app-id com.example.app.',
|
|
130
|
+
});
|
|
131
|
+
} else if (project.errors.length) {
|
|
132
|
+
checks.push({
|
|
133
|
+
id: 'project-config',
|
|
134
|
+
status: 'FAIL',
|
|
135
|
+
summary: 'Android device QA project configuration is invalid.',
|
|
136
|
+
details: project.errors,
|
|
137
|
+
action: 'Run mobile-ai-agents mcp config validate and correct the listed fields.',
|
|
138
|
+
});
|
|
139
|
+
} else {
|
|
140
|
+
const apk = path.resolve(cwd, project.config.apkPath);
|
|
141
|
+
checks.push(fsImpl.existsSync(apk)
|
|
142
|
+
? { id: 'project-config', status: 'PASS', summary: 'Android device QA project configuration is valid.', details: project.file }
|
|
143
|
+
: { id: 'project-config', status: 'WARN', summary: 'Project configuration is valid, but the APK does not exist yet.', details: project.config.apkPath, action: 'Build the configured APK before device QA.' });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
platform: 'android',
|
|
148
|
+
status: resultStatus(checks),
|
|
149
|
+
checkedAt: new Date().toISOString(),
|
|
150
|
+
checks,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function printDoctorReport(report) {
|
|
155
|
+
console.log('');
|
|
156
|
+
console.log(' Android Environment Doctor');
|
|
157
|
+
console.log(` Result: ${report.status}`);
|
|
158
|
+
console.log('');
|
|
159
|
+
for (const check of report.checks) {
|
|
160
|
+
const icon = check.status === 'PASS' ? '✓' : check.status === 'WARN' ? '!' : '✗';
|
|
161
|
+
console.log(` ${icon} [${check.status}] ${check.summary}`);
|
|
162
|
+
if (check.action) console.log(` Action: ${check.action}`);
|
|
163
|
+
}
|
|
164
|
+
console.log('');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function cmdDoctor(args) {
|
|
168
|
+
const opts = parseOptions(args);
|
|
169
|
+
const platform = opts.platform === true ? '' : opts.platform;
|
|
170
|
+
if (platform !== 'android') throw new Error('doctor currently requires --platform android.');
|
|
171
|
+
const report = collectAndroidDoctor({ configPath: opts.config === true ? undefined : opts.config });
|
|
172
|
+
if (opts.json) console.log(JSON.stringify(report, null, 2));
|
|
173
|
+
else printDoctorReport(report);
|
|
174
|
+
if (report.status === 'FAIL') process.exitCode = 1;
|
|
175
|
+
return report;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
module.exports = {
|
|
179
|
+
cmdDoctor,
|
|
180
|
+
collectAndroidDoctor,
|
|
181
|
+
parseAdbDevices,
|
|
182
|
+
};
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { parseOptions } = require('../lib/options');
|
|
7
|
+
|
|
8
|
+
const SERVER_NAME = 'mobile-mcp';
|
|
9
|
+
const SERVER_COMMAND = 'npx';
|
|
10
|
+
const SERVER_ARGS = ['-y', '@mobilenext/mobile-mcp@latest'];
|
|
11
|
+
const PROJECT_CONFIG_PATH = path.join('.mobile-ai-agents', 'android-device.json');
|
|
12
|
+
|
|
13
|
+
const JSON_CLIENTS = {
|
|
14
|
+
claude: ({ cwd }) => path.join(cwd, '.mcp.json'),
|
|
15
|
+
cursor: ({ cwd }) => path.join(cwd, '.cursor', 'mcp.json'),
|
|
16
|
+
windsurf: ({ homedir }) => path.join(homedir, '.codeium', 'windsurf', 'mcp_config.json'),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function ensureParent(file, fsImpl = fs) {
|
|
20
|
+
fsImpl.mkdirSync(path.dirname(file), { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readJsonObject(file, fsImpl = fs) {
|
|
24
|
+
if (!fsImpl.existsSync(file)) return {};
|
|
25
|
+
try {
|
|
26
|
+
const value = JSON.parse(fsImpl.readFileSync(file, 'utf8'));
|
|
27
|
+
if (!value || Array.isArray(value) || typeof value !== 'object') {
|
|
28
|
+
throw new Error('top-level value must be an object');
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
throw new Error(`Invalid JSON in ${file}: ${error.message}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveClientConfig(client, options = {}) {
|
|
37
|
+
const cwd = options.cwd || process.cwd();
|
|
38
|
+
const homedir = options.homedir || os.homedir();
|
|
39
|
+
if (options.configPath) return path.resolve(cwd, options.configPath);
|
|
40
|
+
if (client === 'codex') return path.join(homedir, '.codex', 'config.toml');
|
|
41
|
+
const resolver = JSON_CLIENTS[client];
|
|
42
|
+
if (!resolver) {
|
|
43
|
+
throw new Error('Unsupported MCP client. Use claude, cursor, windsurf, or codex.');
|
|
44
|
+
}
|
|
45
|
+
return resolver({ cwd, homedir });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function setupJsonClient(file, fsImpl = fs) {
|
|
49
|
+
const config = readJsonObject(file, fsImpl);
|
|
50
|
+
const servers = config.mcpServers && typeof config.mcpServers === 'object' && !Array.isArray(config.mcpServers)
|
|
51
|
+
? { ...config.mcpServers }
|
|
52
|
+
: {};
|
|
53
|
+
const desired = { command: SERVER_COMMAND, args: SERVER_ARGS };
|
|
54
|
+
const current = servers[SERVER_NAME];
|
|
55
|
+
const unchanged = current &&
|
|
56
|
+
current.command === desired.command &&
|
|
57
|
+
JSON.stringify(current.args) === JSON.stringify(desired.args);
|
|
58
|
+
|
|
59
|
+
if (unchanged) return { changed: false, file, clientFormat: 'json' };
|
|
60
|
+
|
|
61
|
+
config.mcpServers = { ...servers, [SERVER_NAME]: desired };
|
|
62
|
+
ensureParent(file, fsImpl);
|
|
63
|
+
fsImpl.writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
|
|
64
|
+
return { changed: true, file, clientFormat: 'json' };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function setupCodexClient(file, fsImpl = fs) {
|
|
68
|
+
const existing = fsImpl.existsSync(file) ? fsImpl.readFileSync(file, 'utf8') : '';
|
|
69
|
+
const block = [
|
|
70
|
+
'[mcp_servers.mobile-mcp]',
|
|
71
|
+
`command = "${SERVER_COMMAND}"`,
|
|
72
|
+
`args = ["${SERVER_ARGS.join('", "')}"]`,
|
|
73
|
+
].join('\n');
|
|
74
|
+
const sectionPattern = /^\[mcp_servers\.(?:mobile-mcp|"mobile-mcp")\]\s*$/m;
|
|
75
|
+
const match = sectionPattern.exec(existing);
|
|
76
|
+
let next;
|
|
77
|
+
|
|
78
|
+
if (!match) {
|
|
79
|
+
const prefix = existing.trimEnd();
|
|
80
|
+
next = `${prefix}${prefix ? '\n\n' : ''}${block}\n`;
|
|
81
|
+
} else {
|
|
82
|
+
const sectionStart = match.index;
|
|
83
|
+
const afterHeader = sectionStart + match[0].length;
|
|
84
|
+
const remainder = existing.slice(afterHeader);
|
|
85
|
+
const nextSection = /^\s*\[[^\]]+\]/m.exec(remainder);
|
|
86
|
+
const sectionEnd = nextSection ? afterHeader + nextSection.index : existing.length;
|
|
87
|
+
next = `${existing.slice(0, sectionStart)}${block}\n${existing.slice(sectionEnd).replace(/^\s+/, '')}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (next === existing) return { changed: false, file, clientFormat: 'toml' };
|
|
91
|
+
ensureParent(file, fsImpl);
|
|
92
|
+
fsImpl.writeFileSync(file, next);
|
|
93
|
+
return { changed: true, file, clientFormat: 'toml' };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function setupMcpClient(client, options = {}) {
|
|
97
|
+
const fsImpl = options.fsImpl || fs;
|
|
98
|
+
const file = resolveClientConfig(client, options);
|
|
99
|
+
const result = client === 'codex'
|
|
100
|
+
? setupCodexClient(file, fsImpl)
|
|
101
|
+
: setupJsonClient(file, fsImpl);
|
|
102
|
+
return { ...result, client, server: SERVER_NAME };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function defaultProjectConfig(values = {}) {
|
|
106
|
+
return {
|
|
107
|
+
version: 1,
|
|
108
|
+
platform: 'android',
|
|
109
|
+
appId: values.appId || '',
|
|
110
|
+
apkPath: values.apkPath || 'app/build/outputs/apk/debug/app-debug.apk',
|
|
111
|
+
device: values.device || 'auto',
|
|
112
|
+
flow: values.flow || ['Launch app', 'Verify the initial screen'],
|
|
113
|
+
timeouts: {
|
|
114
|
+
bootMs: values.bootMs || 120000,
|
|
115
|
+
actionMs: values.actionMs || 15000,
|
|
116
|
+
},
|
|
117
|
+
evidenceDir: values.evidenceDir || '.mobile-ai-agents/evidence',
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function validateProjectConfig(config) {
|
|
122
|
+
const errors = [];
|
|
123
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
124
|
+
return ['Configuration must be a JSON object.'];
|
|
125
|
+
}
|
|
126
|
+
if (config.version !== 1) errors.push('version must be 1.');
|
|
127
|
+
if (config.platform !== 'android') errors.push('platform must be "android".');
|
|
128
|
+
if (typeof config.appId !== 'string' || !/^[A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z][A-Za-z0-9_]*)+$/.test(config.appId)) {
|
|
129
|
+
errors.push('appId must be a valid Android application ID such as com.example.app.');
|
|
130
|
+
}
|
|
131
|
+
if (typeof config.apkPath !== 'string' || !config.apkPath.endsWith('.apk')) {
|
|
132
|
+
errors.push('apkPath must point to an .apk file.');
|
|
133
|
+
}
|
|
134
|
+
if (typeof config.device !== 'string' || !config.device.trim()) {
|
|
135
|
+
errors.push('device must be "auto", an emulator name, or a device serial.');
|
|
136
|
+
}
|
|
137
|
+
if (!Array.isArray(config.flow) || !config.flow.length || config.flow.some(step => typeof step !== 'string' || !step.trim())) {
|
|
138
|
+
errors.push('flow must be a non-empty array of test steps.');
|
|
139
|
+
}
|
|
140
|
+
if (!config.timeouts || !Number.isInteger(config.timeouts.bootMs) || config.timeouts.bootMs <= 0) {
|
|
141
|
+
errors.push('timeouts.bootMs must be a positive integer.');
|
|
142
|
+
}
|
|
143
|
+
if (!config.timeouts || !Number.isInteger(config.timeouts.actionMs) || config.timeouts.actionMs <= 0) {
|
|
144
|
+
errors.push('timeouts.actionMs must be a positive integer.');
|
|
145
|
+
}
|
|
146
|
+
if (typeof config.evidenceDir !== 'string' || !config.evidenceDir.trim()) {
|
|
147
|
+
errors.push('evidenceDir must be a non-empty path.');
|
|
148
|
+
}
|
|
149
|
+
return errors;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function projectConfigFile(options = {}) {
|
|
153
|
+
const cwd = options.cwd || process.cwd();
|
|
154
|
+
return path.resolve(cwd, options.configPath || PROJECT_CONFIG_PATH);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function initProjectConfig(values = {}, options = {}) {
|
|
158
|
+
const fsImpl = options.fsImpl || fs;
|
|
159
|
+
const file = projectConfigFile(options);
|
|
160
|
+
const existing = readJsonObject(file, fsImpl);
|
|
161
|
+
const base = Object.keys(existing).length ? existing : defaultProjectConfig();
|
|
162
|
+
const next = {
|
|
163
|
+
...base,
|
|
164
|
+
version: 1,
|
|
165
|
+
platform: 'android',
|
|
166
|
+
appId: values.appId !== undefined ? values.appId : base.appId,
|
|
167
|
+
apkPath: values.apkPath !== undefined ? values.apkPath : base.apkPath,
|
|
168
|
+
device: values.device !== undefined ? values.device : base.device,
|
|
169
|
+
flow: values.flow !== undefined ? values.flow : base.flow,
|
|
170
|
+
timeouts: {
|
|
171
|
+
...(base.timeouts || {}),
|
|
172
|
+
bootMs: values.bootMs !== undefined ? values.bootMs : base.timeouts?.bootMs || 120000,
|
|
173
|
+
actionMs: values.actionMs !== undefined ? values.actionMs : base.timeouts?.actionMs || 15000,
|
|
174
|
+
},
|
|
175
|
+
evidenceDir: values.evidenceDir !== undefined ? values.evidenceDir : base.evidenceDir,
|
|
176
|
+
};
|
|
177
|
+
const errors = validateProjectConfig(next);
|
|
178
|
+
if (errors.length) throw new Error(`Invalid Android device configuration:\n- ${errors.join('\n- ')}`);
|
|
179
|
+
|
|
180
|
+
const serialized = `${JSON.stringify(next, null, 2)}\n`;
|
|
181
|
+
const current = fsImpl.existsSync(file) ? fsImpl.readFileSync(file, 'utf8') : '';
|
|
182
|
+
if (current !== serialized) {
|
|
183
|
+
ensureParent(file, fsImpl);
|
|
184
|
+
fsImpl.writeFileSync(file, serialized);
|
|
185
|
+
}
|
|
186
|
+
return { changed: current !== serialized, file, config: next };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function loadProjectConfig(options = {}) {
|
|
190
|
+
const fsImpl = options.fsImpl || fs;
|
|
191
|
+
const file = projectConfigFile(options);
|
|
192
|
+
if (!fsImpl.existsSync(file)) return { file, config: null, errors: ['Project configuration does not exist.'] };
|
|
193
|
+
const config = readJsonObject(file, fsImpl);
|
|
194
|
+
return { file, config, errors: validateProjectConfig(config) };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function knownMcpConfigFiles(options = {}) {
|
|
198
|
+
const cwd = options.cwd || process.cwd();
|
|
199
|
+
const homedir = options.homedir || os.homedir();
|
|
200
|
+
return [
|
|
201
|
+
{ client: 'claude', format: 'json', file: path.join(cwd, '.mcp.json') },
|
|
202
|
+
{ client: 'cursor', format: 'json', file: path.join(cwd, '.cursor', 'mcp.json') },
|
|
203
|
+
{ client: 'windsurf', format: 'json', file: path.join(homedir, '.codeium', 'windsurf', 'mcp_config.json') },
|
|
204
|
+
{ client: 'codex', format: 'toml', file: path.join(homedir, '.codex', 'config.toml') },
|
|
205
|
+
];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function inspectMcpConfiguration(options = {}) {
|
|
209
|
+
const fsImpl = options.fsImpl || fs;
|
|
210
|
+
const found = [];
|
|
211
|
+
for (const candidate of knownMcpConfigFiles(options)) {
|
|
212
|
+
if (!fsImpl.existsSync(candidate.file)) continue;
|
|
213
|
+
try {
|
|
214
|
+
let configured;
|
|
215
|
+
if (candidate.format === 'json') {
|
|
216
|
+
const server = readJsonObject(candidate.file, fsImpl).mcpServers?.[SERVER_NAME];
|
|
217
|
+
configured = server?.command === SERVER_COMMAND && JSON.stringify(server.args) === JSON.stringify(SERVER_ARGS);
|
|
218
|
+
} else {
|
|
219
|
+
const content = fsImpl.readFileSync(candidate.file, 'utf8');
|
|
220
|
+
const header = /^\[mcp_servers\.(?:mobile-mcp|"mobile-mcp")\]\s*$/m.exec(content);
|
|
221
|
+
if (!header) {
|
|
222
|
+
configured = false;
|
|
223
|
+
} else {
|
|
224
|
+
const remainder = content.slice(header.index + header[0].length);
|
|
225
|
+
const nextSection = /^\s*\[[^\]]+\]/m.exec(remainder);
|
|
226
|
+
const section = nextSection ? remainder.slice(0, nextSection.index) : remainder;
|
|
227
|
+
configured = /^\s*command\s*=\s*"npx"\s*$/m.test(section) &&
|
|
228
|
+
/^\s*args\s*=\s*\[\s*"-y"\s*,\s*"@mobilenext\/mobile-mcp@latest"\s*\]\s*$/m.test(section);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (configured) found.push(candidate);
|
|
232
|
+
} catch {
|
|
233
|
+
// Invalid files are reported by setup/validation rather than hidden as configured.
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return found;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function printMcpHelp() {
|
|
240
|
+
console.log('');
|
|
241
|
+
console.log(' Mobile MCP setup');
|
|
242
|
+
console.log('');
|
|
243
|
+
console.log(' Commands:');
|
|
244
|
+
console.log(' mobile-ai-agents mcp setup --client <claude|cursor|windsurf|codex>');
|
|
245
|
+
console.log(' mobile-ai-agents mcp config init --app-id com.example.app [options]');
|
|
246
|
+
console.log(' mobile-ai-agents mcp config validate [--json]');
|
|
247
|
+
console.log('');
|
|
248
|
+
console.log(' Config options:');
|
|
249
|
+
console.log(' --apk <path> APK path');
|
|
250
|
+
console.log(' --device <auto|name|serial> Device selection');
|
|
251
|
+
console.log(' --flow "step 1|step 2" Ordered test flow');
|
|
252
|
+
console.log(' --boot-timeout-ms <number> Emulator boot timeout');
|
|
253
|
+
console.log(' --action-timeout-ms <number> Per-action timeout');
|
|
254
|
+
console.log(' --evidence-dir <path> Screenshots and reports');
|
|
255
|
+
console.log(' --config <path> Override target config path');
|
|
256
|
+
console.log('');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function cmdMcp(args) {
|
|
260
|
+
const opts = parseOptions(args);
|
|
261
|
+
const [sub, action] = opts._;
|
|
262
|
+
|
|
263
|
+
if (!sub || sub === 'help') {
|
|
264
|
+
printMcpHelp();
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (sub === 'setup') {
|
|
269
|
+
if (!opts.client || opts.client === true) throw new Error('mcp setup requires --client.');
|
|
270
|
+
const result = setupMcpClient(String(opts.client), { configPath: opts.config === true ? undefined : opts.config });
|
|
271
|
+
if (opts.json) {
|
|
272
|
+
console.log(JSON.stringify(result, null, 2));
|
|
273
|
+
} else {
|
|
274
|
+
console.log('');
|
|
275
|
+
console.log(` ${result.changed ? '✓ Configured' : '✓ Already configured'} mobile-mcp for ${result.client}`);
|
|
276
|
+
console.log(` File: ${result.file}`);
|
|
277
|
+
console.log('');
|
|
278
|
+
}
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (sub === 'config' && action === 'init') {
|
|
283
|
+
const flow = typeof opts.flow === 'string' ? opts.flow.split('|').map(v => v.trim()).filter(Boolean) : undefined;
|
|
284
|
+
const result = initProjectConfig({
|
|
285
|
+
appId: opts['app-id'] === true ? undefined : opts['app-id'],
|
|
286
|
+
apkPath: opts.apk === true ? undefined : opts.apk,
|
|
287
|
+
device: opts.device === true ? undefined : opts.device,
|
|
288
|
+
flow,
|
|
289
|
+
bootMs: opts['boot-timeout-ms'] ? Number(opts['boot-timeout-ms']) : undefined,
|
|
290
|
+
actionMs: opts['action-timeout-ms'] ? Number(opts['action-timeout-ms']) : undefined,
|
|
291
|
+
evidenceDir: opts['evidence-dir'] === true ? undefined : opts['evidence-dir'],
|
|
292
|
+
}, { configPath: opts.config === true ? undefined : opts.config });
|
|
293
|
+
console.log('');
|
|
294
|
+
console.log(` ${result.changed ? '✓ Wrote' : '✓ Unchanged'} ${result.file}`);
|
|
295
|
+
console.log('');
|
|
296
|
+
return result;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (sub === 'config' && action === 'validate') {
|
|
300
|
+
const result = loadProjectConfig({ configPath: opts.config === true ? undefined : opts.config });
|
|
301
|
+
if (opts.json) {
|
|
302
|
+
console.log(JSON.stringify({ valid: result.errors.length === 0, ...result }, null, 2));
|
|
303
|
+
} else if (result.errors.length) {
|
|
304
|
+
console.log('');
|
|
305
|
+
console.log(` ✗ Invalid ${result.file}`);
|
|
306
|
+
result.errors.forEach(error => console.log(` - ${error}`));
|
|
307
|
+
console.log('');
|
|
308
|
+
} else {
|
|
309
|
+
console.log('');
|
|
310
|
+
console.log(` ✓ Valid ${result.file}`);
|
|
311
|
+
console.log('');
|
|
312
|
+
}
|
|
313
|
+
if (result.errors.length) process.exitCode = 1;
|
|
314
|
+
return result;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
throw new Error('Unknown mcp command. Use mcp help.');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
module.exports = {
|
|
321
|
+
PROJECT_CONFIG_PATH,
|
|
322
|
+
cmdMcp,
|
|
323
|
+
defaultProjectConfig,
|
|
324
|
+
initProjectConfig,
|
|
325
|
+
inspectMcpConfiguration,
|
|
326
|
+
loadProjectConfig,
|
|
327
|
+
resolveClientConfig,
|
|
328
|
+
setupMcpClient,
|
|
329
|
+
validateProjectConfig,
|
|
330
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { cmdDoctor } = require('./doctor');
|
|
4
|
+
const { cmdMcp } = require('./mcp');
|
|
5
|
+
|
|
6
|
+
function createCommandRouter(legacyHandlers) {
|
|
7
|
+
const routes = new Map([
|
|
8
|
+
['start', legacyHandlers.start],
|
|
9
|
+
['install', legacyHandlers.install],
|
|
10
|
+
['add', legacyHandlers.add],
|
|
11
|
+
['memory', legacyHandlers.memory],
|
|
12
|
+
['list', legacyHandlers.list],
|
|
13
|
+
['doctor', cmdDoctor],
|
|
14
|
+
['mcp', cmdMcp],
|
|
15
|
+
['help', legacyHandlers.help],
|
|
16
|
+
['--help', legacyHandlers.help],
|
|
17
|
+
['-h', legacyHandlers.help],
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
return async function routeCommand(command, args = []) {
|
|
21
|
+
if (command === undefined) return legacyHandlers.help(args);
|
|
22
|
+
const handler = routes.get(command);
|
|
23
|
+
if (!handler) {
|
|
24
|
+
const error = new Error(`Unknown command: ${command}`);
|
|
25
|
+
error.code = 'UNKNOWN_COMMAND';
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
return handler(args);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { createCommandRouter };
|
package/cli/index.js
CHANGED
|
@@ -6,6 +6,8 @@ const fs = require('fs');
|
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const os = require('os');
|
|
8
8
|
const readline = require('readline');
|
|
9
|
+
const { createCommandRouter } = require('./commands/router');
|
|
10
|
+
const { parseOptions } = require('./lib/options');
|
|
9
11
|
|
|
10
12
|
const REPO = 'salmanashraf/mobile-agency';
|
|
11
13
|
const BRANCH = 'main';
|
|
@@ -283,26 +285,6 @@ function slugify(s) {
|
|
|
283
285
|
.slice(0, 80) || 'memory';
|
|
284
286
|
}
|
|
285
287
|
|
|
286
|
-
function parseOptions(args) {
|
|
287
|
-
const out = { _: [] };
|
|
288
|
-
for (let i = 0; i < args.length; i++) {
|
|
289
|
-
const arg = args[i];
|
|
290
|
-
if (!arg.startsWith('--')) {
|
|
291
|
-
out._.push(arg);
|
|
292
|
-
continue;
|
|
293
|
-
}
|
|
294
|
-
const key = arg.slice(2);
|
|
295
|
-
const next = args[i + 1];
|
|
296
|
-
if (!next || next.startsWith('--')) {
|
|
297
|
-
out[key] = true;
|
|
298
|
-
} else {
|
|
299
|
-
out[key] = next;
|
|
300
|
-
i++;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
return out;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
288
|
function readStdinIfPiped() {
|
|
307
289
|
if (process.stdin.isTTY) return '';
|
|
308
290
|
try {
|
|
@@ -1401,6 +1383,8 @@ function cmdList() {
|
|
|
1401
1383
|
console.log(dim(' npx mobile-ai-agents add skill grill-mobile # one skill'));
|
|
1402
1384
|
console.log(dim(' npx mobile-ai-agents add workflow feature-ship # one workflow'));
|
|
1403
1385
|
console.log(dim(' npx mobile-ai-agents memory init # local Mobile Memory'));
|
|
1386
|
+
console.log(dim(' npx mobile-ai-agents doctor --platform android # check Android setup'));
|
|
1387
|
+
console.log(dim(' npx mobile-ai-agents mcp setup --client claude # configure Mobile MCP'));
|
|
1404
1388
|
console.log('');
|
|
1405
1389
|
}
|
|
1406
1390
|
|
|
@@ -1413,6 +1397,8 @@ function cmdHelp() {
|
|
|
1413
1397
|
console.log(` ${bold('install')} Install agents, skills, and workflows`);
|
|
1414
1398
|
console.log(` ${bold('add')} agent|skill|workflow <name> Install a single item`);
|
|
1415
1399
|
console.log(` ${bold('memory')} init|capture|search|... Local Mobile Memory store`);
|
|
1400
|
+
console.log(` ${bold('doctor')} --platform android Check SDK, adb, emulator, MCP, and project config`);
|
|
1401
|
+
console.log(` ${bold('mcp')} setup|config Configure Mobile MCP and Android device QA`);
|
|
1416
1402
|
console.log(` ${bold('list')} List all available agents, skills, and workflows`);
|
|
1417
1403
|
console.log(` ${bold('help')} Show this help`);
|
|
1418
1404
|
console.log('');
|
|
@@ -1447,6 +1433,11 @@ function cmdHelp() {
|
|
|
1447
1433
|
console.log(dim(' npx mobile-ai-agents add workflow feature-ship'));
|
|
1448
1434
|
console.log(dim(' npx mobile-ai-agents memory init'));
|
|
1449
1435
|
console.log(dim(' npx mobile-ai-agents memory capture --type decision --text "Use Room for offline persistence"'));
|
|
1436
|
+
console.log(dim(' npx mobile-ai-agents doctor --platform android'));
|
|
1437
|
+
console.log(dim(' npx mobile-ai-agents doctor --platform android --json'));
|
|
1438
|
+
console.log(dim(' npx mobile-ai-agents mcp setup --client codex'));
|
|
1439
|
+
console.log(dim(' npx mobile-ai-agents mcp config init --app-id com.example.app'));
|
|
1440
|
+
console.log(dim(' npx mobile-ai-agents mcp config validate'));
|
|
1450
1441
|
console.log('');
|
|
1451
1442
|
console.log(dim(` github.com/${REPO}`));
|
|
1452
1443
|
console.log('');
|
|
@@ -1454,28 +1445,24 @@ function cmdHelp() {
|
|
|
1454
1445
|
|
|
1455
1446
|
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
1456
1447
|
|
|
1448
|
+
const routeCommand = createCommandRouter({
|
|
1449
|
+
start: cmdStart,
|
|
1450
|
+
install: cmdInstall,
|
|
1451
|
+
add: cmdAdd,
|
|
1452
|
+
memory: cmdMemory,
|
|
1453
|
+
list: cmdList,
|
|
1454
|
+
help: cmdHelp,
|
|
1455
|
+
});
|
|
1456
|
+
|
|
1457
1457
|
async function main() {
|
|
1458
1458
|
const [,, cmd, ...args] = process.argv;
|
|
1459
1459
|
|
|
1460
1460
|
try {
|
|
1461
|
-
|
|
1462
|
-
case 'start': await cmdStart(args); break;
|
|
1463
|
-
case 'install': await cmdInstall(args); break;
|
|
1464
|
-
case 'add': await cmdAdd(args); break;
|
|
1465
|
-
case 'memory': cmdMemory(args); break;
|
|
1466
|
-
case 'list': cmdList(); break;
|
|
1467
|
-
case 'help':
|
|
1468
|
-
case '--help':
|
|
1469
|
-
case '-h':
|
|
1470
|
-
case undefined: cmdHelp(); break;
|
|
1471
|
-
default:
|
|
1472
|
-
console.error(`\n Unknown command: ${cmd}\n`);
|
|
1473
|
-
cmdHelp();
|
|
1474
|
-
process.exit(1);
|
|
1475
|
-
}
|
|
1461
|
+
await routeCommand(cmd, args);
|
|
1476
1462
|
} catch (e) {
|
|
1477
|
-
console.error(`\n Error: ${e.message}\n`);
|
|
1478
|
-
|
|
1463
|
+
console.error(`\n ${e.code === 'UNKNOWN_COMMAND' ? e.message : `Error: ${e.message}`}\n`);
|
|
1464
|
+
if (e.code === 'UNKNOWN_COMMAND') cmdHelp();
|
|
1465
|
+
process.exitCode = 1;
|
|
1479
1466
|
}
|
|
1480
1467
|
}
|
|
1481
1468
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function parseOptions(args) {
|
|
4
|
+
const out = { _: [] };
|
|
5
|
+
for (let i = 0; i < args.length; i++) {
|
|
6
|
+
const arg = args[i];
|
|
7
|
+
if (!arg.startsWith('--')) {
|
|
8
|
+
out._.push(arg);
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const key = arg.slice(2);
|
|
13
|
+
const next = args[i + 1];
|
|
14
|
+
if (!next || next.startsWith('--')) {
|
|
15
|
+
out[key] = true;
|
|
16
|
+
} else {
|
|
17
|
+
out[key] = next;
|
|
18
|
+
i++;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { parseOptions };
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Android Device QA Setup
|
|
2
|
+
|
|
3
|
+
Prepare an Android project for the `/mobile-mcp-qa` skill with repeatable environment checks, MCP configuration, and a project-level test definition.
|
|
4
|
+
|
|
5
|
+
This setup layer verifies prerequisites and records the intended QA flow. Device-flow execution is tracked separately in [issue #54](https://github.com/salmanashraf/mobile-ai-agents/issues/54).
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- Node.js 18 or newer
|
|
10
|
+
- Android Studio with Android SDK Platform-Tools
|
|
11
|
+
- Android Emulator plus at least one AVD, or an authorized physical device
|
|
12
|
+
- Claude Code, Cursor, Windsurf, or Codex
|
|
13
|
+
|
|
14
|
+
## 1. Check the Android Environment
|
|
15
|
+
|
|
16
|
+
Run from the mobile app project:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx mobile-ai-agents doctor --platform android
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The doctor checks:
|
|
23
|
+
|
|
24
|
+
- `ANDROID_HOME`, `ANDROID_SDK_ROOT`, and common SDK locations
|
|
25
|
+
- `adb` availability and version
|
|
26
|
+
- connected and authorized devices
|
|
27
|
+
- Android Emulator installation and configured AVDs
|
|
28
|
+
- Mobile MCP configuration for supported AI clients
|
|
29
|
+
- `.mobile-ai-agents/android-device.json` validity and APK availability
|
|
30
|
+
|
|
31
|
+
Use machine-readable output in CI or scripts:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx mobile-ai-agents doctor --platform android --json
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Exit code `1` means at least one required check failed. Warnings identify optional or not-yet-ready items, such as a missing APK before the first build.
|
|
38
|
+
|
|
39
|
+
## 2. Configure Mobile MCP
|
|
40
|
+
|
|
41
|
+
Choose the client you use:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx mobile-ai-agents mcp setup --client claude
|
|
45
|
+
npx mobile-ai-agents mcp setup --client cursor
|
|
46
|
+
npx mobile-ai-agents mcp setup --client windsurf
|
|
47
|
+
npx mobile-ai-agents mcp setup --client codex
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The command adds this server without deleting existing settings or MCP servers:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"command": "npx",
|
|
55
|
+
"args": ["-y", "@mobilenext/mobile-mcp@latest"]
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Running the same setup command again is safe and leaves an already-correct file unchanged.
|
|
60
|
+
|
|
61
|
+
Default configuration files:
|
|
62
|
+
|
|
63
|
+
| Client | File |
|
|
64
|
+
|---|---|
|
|
65
|
+
| Claude Code | `<project>/.mcp.json` |
|
|
66
|
+
| Cursor | `<project>/.cursor/mcp.json` |
|
|
67
|
+
| Windsurf | `~/.codeium/windsurf/mcp_config.json` |
|
|
68
|
+
| Codex | `~/.codex/config.toml` |
|
|
69
|
+
|
|
70
|
+
Use `--config <path>` to target a different client config file.
|
|
71
|
+
|
|
72
|
+
## 3. Define the Project QA Flow
|
|
73
|
+
|
|
74
|
+
Create `.mobile-ai-agents/android-device.json`:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npx mobile-ai-agents mcp config init \
|
|
78
|
+
--app-id com.example.app \
|
|
79
|
+
--apk app/build/outputs/apk/debug/app-debug.apk \
|
|
80
|
+
--device auto \
|
|
81
|
+
--flow "Launch app|Open settings|Enable notifications|Restart app"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Generated shape:
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"version": 1,
|
|
89
|
+
"platform": "android",
|
|
90
|
+
"appId": "com.example.app",
|
|
91
|
+
"apkPath": "app/build/outputs/apk/debug/app-debug.apk",
|
|
92
|
+
"device": "auto",
|
|
93
|
+
"flow": [
|
|
94
|
+
"Launch app",
|
|
95
|
+
"Open settings",
|
|
96
|
+
"Enable notifications",
|
|
97
|
+
"Restart app"
|
|
98
|
+
],
|
|
99
|
+
"timeouts": {
|
|
100
|
+
"bootMs": 120000,
|
|
101
|
+
"actionMs": 15000
|
|
102
|
+
},
|
|
103
|
+
"evidenceDir": ".mobile-ai-agents/evidence"
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
You can also copy [the configuration template](../templates/android-device-config.json).
|
|
108
|
+
|
|
109
|
+
Validate after editing:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
npx mobile-ai-agents mcp config validate
|
|
113
|
+
npx mobile-ai-agents mcp config validate --json
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## 4. Run the Skill
|
|
117
|
+
|
|
118
|
+
After the doctor has no failures, restart the AI client so it loads Mobile MCP, then invoke:
|
|
119
|
+
|
|
120
|
+
```text
|
|
121
|
+
/mobile-mcp-qa
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The skill uses the app ID, build, target device, expected flow, and evidence requirements to guide device QA. See [Mobile MCP Integration](mobile-mcp.md) for the full prompt format and report output.
|
|
125
|
+
|
|
126
|
+
## Troubleshooting
|
|
127
|
+
|
|
128
|
+
| Result | Fix |
|
|
129
|
+
|---|---|
|
|
130
|
+
| Android SDK not found | Install the SDK and set `ANDROID_HOME` or `ANDROID_SDK_ROOT`. |
|
|
131
|
+
| `adb` not found | Install Platform-Tools and add `<sdk>/platform-tools` to `PATH`. |
|
|
132
|
+
| No ready device | Start an AVD or connect a device, enable USB debugging, and authorize the computer. |
|
|
133
|
+
| No AVD configured | Create one in Android Studio Device Manager. |
|
|
134
|
+
| MCP not configured | Run `mcp setup --client <client>`, then restart the client. |
|
|
135
|
+
| Invalid project config | Run `mcp config validate` and correct every listed field. |
|
|
136
|
+
| APK missing | Build the configured APK before device QA. |
|
|
137
|
+
|
|
138
|
+
Do not use production accounts or irreversible actions during automated QA. Prefer test builds, sandbox credentials, and disposable data.
|
package/docs/index.html
CHANGED
|
@@ -679,7 +679,12 @@
|
|
|
679
679
|
<span class="comment"># Or just one Android agent</span><br>
|
|
680
680
|
<span class="cmd2">npx mobile-ai-agents add agent axiom</span><br>
|
|
681
681
|
<span class="cmd2">npx mobile-ai-agents add agent anr-investigation</span><br>
|
|
682
|
-
<span class="cmd2">npx mobile-ai-agents add agent memory-leak-analyzer</span>
|
|
682
|
+
<span class="cmd2">npx mobile-ai-agents add agent memory-leak-analyzer</span><br><br>
|
|
683
|
+
<span class="comment"># Prepare Android device QA with Mobile MCP</span><br>
|
|
684
|
+
<span class="cmd2">npx mobile-ai-agents doctor</span> <span class="flag">--platform</span> <span class="val">android</span><br>
|
|
685
|
+
<span class="cmd2">npx mobile-ai-agents mcp setup</span> <span class="flag">--client</span> <span class="val">claude</span><br>
|
|
686
|
+
<span class="cmd2">npx mobile-ai-agents mcp config init</span> <span class="flag">--app-id</span> <span class="val">com.example.app</span><br><br>
|
|
687
|
+
<a href="android-device-setup.md">Read the Android Device QA Setup guide →</a>
|
|
683
688
|
</div>
|
|
684
689
|
<div class="code-block" id="tab-cursor" style="display:none">
|
|
685
690
|
<span class="comment"># Install for Cursor (.cursorrules in current project)</span><br>
|
package/docs/installation.md
CHANGED
|
@@ -60,6 +60,14 @@ node --version
|
|
|
60
60
|
|
|
61
61
|
If this prints a version like `v18.x`, `v20.x`, or newer, you are ready.
|
|
62
62
|
|
|
63
|
+
If you plan to use `/mobile-mcp-qa` with Android, also install Android Studio, Platform-Tools, and an emulator image or connect an authorized device. Verify the setup with:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npx mobile-ai-agents doctor --platform android
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Then follow the [Android Device QA Setup guide](android-device-setup.md) to configure Mobile MCP and the project test flow.
|
|
70
|
+
|
|
63
71
|
---
|
|
64
72
|
|
|
65
73
|
## Step 1: Pick Your AI Tool
|
package/docs/mobile-mcp.md
CHANGED
|
@@ -33,6 +33,20 @@ It exposes tools for:
|
|
|
33
33
|
|
|
34
34
|
## Install
|
|
35
35
|
|
|
36
|
+
### Automated setup
|
|
37
|
+
|
|
38
|
+
From an Android app project, run the environment doctor and configure your AI client:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npx mobile-ai-agents doctor --platform android
|
|
42
|
+
npx mobile-ai-agents mcp setup --client <claude|cursor|windsurf|codex>
|
|
43
|
+
npx mobile-ai-agents mcp config init --app-id com.example.app
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The setup command preserves existing settings and is idempotent. The project config records the app ID, APK, target device, flow, timeouts, and evidence directory. See [Android Device QA Setup](android-device-setup.md) for the full command reference and troubleshooting.
|
|
47
|
+
|
|
48
|
+
### Manual setup
|
|
49
|
+
|
|
36
50
|
Standard MCP config:
|
|
37
51
|
|
|
38
52
|
```json
|
package/docs/release-process.md
CHANGED
package/docs/wiki.md
CHANGED
|
@@ -41,12 +41,22 @@
|
|
|
41
41
|
|
|
42
42
|
For first-time setup, use the dedicated [Installation Guide](installation.md). It explains which command to run for Claude Code, Cursor, Windsurf, GitHub Copilot, and Codex.
|
|
43
43
|
|
|
44
|
+
For Android device or emulator QA, use the [Android Device QA Setup](android-device-setup.md). It covers the environment doctor, Mobile MCP client setup, and `.mobile-ai-agents/android-device.json` project configuration used by `/mobile-mcp-qa`.
|
|
45
|
+
|
|
44
46
|
Fast path:
|
|
45
47
|
|
|
46
48
|
```bash
|
|
47
49
|
npx mobile-ai-agents install
|
|
48
50
|
```
|
|
49
51
|
|
|
52
|
+
Android QA setup:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npx mobile-ai-agents doctor --platform android
|
|
56
|
+
npx mobile-ai-agents mcp setup --client claude
|
|
57
|
+
npx mobile-ai-agents mcp config init --app-id com.example.app
|
|
58
|
+
```
|
|
59
|
+
|
|
50
60
|
Common installs:
|
|
51
61
|
|
|
52
62
|
| I use... | Run this |
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mobile-ai-agents",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.36",
|
|
4
4
|
"description": "AI agents, skills, and Loop Engineering workflows for building, testing, auditing, and releasing mobile apps.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mobile-ai-agents": "cli/index.js",
|
|
7
7
|
"mobile-agency": "cli/index.js"
|
|
8
8
|
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node --test"
|
|
11
|
+
},
|
|
9
12
|
"files": [
|
|
10
13
|
"AGENTS.md",
|
|
11
14
|
"CLAUDE.md",
|
|
@@ -16,6 +16,20 @@ Mobile MCP is best used after implementation, during APPFORGE Stage 7 Full QA, a
|
|
|
16
16
|
|
|
17
17
|
## Prerequisites
|
|
18
18
|
|
|
19
|
+
For Android, use the Mobile AI Agents setup commands from the app project:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx mobile-ai-agents doctor --platform android
|
|
23
|
+
npx mobile-ai-agents mcp setup --client <claude|cursor|windsurf|codex>
|
|
24
|
+
npx mobile-ai-agents mcp config init \
|
|
25
|
+
--app-id com.example.app \
|
|
26
|
+
--apk app/build/outputs/apk/debug/app-debug.apk \
|
|
27
|
+
--flow "Launch app|Complete the primary flow|Verify success"
|
|
28
|
+
npx mobile-ai-agents mcp config validate
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The setup command preserves existing client settings and is safe to repeat. See `docs/android-device-setup.md` for configuration fields, JSON output, and troubleshooting.
|
|
32
|
+
|
|
19
33
|
Install Mobile MCP in your AI tool:
|
|
20
34
|
|
|
21
35
|
```json
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"platform": "android",
|
|
4
|
+
"appId": "com.example.app",
|
|
5
|
+
"apkPath": "app/build/outputs/apk/debug/app-debug.apk",
|
|
6
|
+
"device": "auto",
|
|
7
|
+
"flow": [
|
|
8
|
+
"Launch app",
|
|
9
|
+
"Verify the initial screen"
|
|
10
|
+
],
|
|
11
|
+
"timeouts": {
|
|
12
|
+
"bootMs": 120000,
|
|
13
|
+
"actionMs": 15000
|
|
14
|
+
},
|
|
15
|
+
"evidenceDir": ".mobile-ai-agents/evidence"
|
|
16
|
+
}
|