replicant-mcp 1.4.8 → 1.5.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/README.md +28 -2
- package/dist/adapters/adb.d.ts +2 -0
- package/dist/adapters/adb.js +12 -1
- package/dist/adapters/ui-fallback-find.js +26 -1
- package/dist/cli/doctor.d.ts +10 -0
- package/dist/cli/doctor.js +154 -0
- package/dist/cli/emulator.js +2 -1
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +1 -0
- package/dist/cli.js +5 -3
- package/dist/index.js +22 -5
- package/dist/parsers/gradle-output.d.ts +1 -0
- package/dist/parsers/gradle-output.js +7 -0
- package/dist/server.js +39 -19
- package/dist/services/config.js +3 -2
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/process-runner.d.ts +1 -0
- package/dist/services/process-runner.js +51 -0
- package/dist/services/test-baseline.d.ts +18 -0
- package/dist/services/test-baseline.js +51 -0
- package/dist/tools/adb-app.js +19 -13
- package/dist/tools/adb-device.js +7 -6
- package/dist/tools/adb-logcat.d.ts +3 -3
- package/dist/tools/adb-logcat.js +4 -2
- package/dist/tools/adb-shell.d.ts +15 -0
- package/dist/tools/adb-shell.js +31 -3
- package/dist/tools/cache.js +2 -1
- package/dist/tools/emulator-device.js +18 -13
- package/dist/tools/gradle-get-details.d.ts +15 -0
- package/dist/tools/gradle-get-details.js +131 -40
- package/dist/tools/gradle-list.js +2 -2
- package/dist/tools/gradle-test.d.ts +14 -0
- package/dist/tools/gradle-test.js +71 -6
- package/dist/tools/rtfm.js +4 -4
- package/dist/tools/ui-find.js +87 -50
- package/dist/tools/ui.d.ts +7 -0
- package/dist/tools/ui.js +20 -12
- package/dist/types/errors.d.ts +3 -0
- package/dist/types/errors.js +4 -0
- package/dist/types/icon-recognition.d.ts +5 -0
- package/dist/types/schemas/adb-app-output.d.ts +83 -0
- package/dist/types/schemas/adb-app-output.js +60 -0
- package/dist/types/schemas/adb-device-output.d.ts +148 -0
- package/dist/types/schemas/adb-device-output.js +72 -0
- package/dist/types/schemas/adb-logcat-output.d.ts +15 -0
- package/dist/types/schemas/adb-logcat-output.js +14 -0
- package/dist/types/schemas/adb-shell-output.d.ts +17 -0
- package/dist/types/schemas/adb-shell-output.js +16 -0
- package/dist/types/schemas/cache-output.d.ts +88 -0
- package/dist/types/schemas/cache-output.js +51 -0
- package/dist/types/schemas/emulator-device-output.d.ts +100 -0
- package/dist/types/schemas/emulator-device-output.js +76 -0
- package/dist/types/schemas/gradle-build-output.d.ts +16 -0
- package/dist/types/schemas/gradle-build-output.js +15 -0
- package/dist/types/schemas/gradle-get-details-output.d.ts +129 -0
- package/dist/types/schemas/gradle-get-details-output.js +86 -0
- package/dist/types/schemas/gradle-list-output.d.ts +56 -0
- package/dist/types/schemas/gradle-list-output.js +39 -0
- package/dist/types/schemas/gradle-test-output.d.ts +82 -0
- package/dist/types/schemas/gradle-test-output.js +54 -0
- package/dist/types/schemas/index.d.ts +12 -0
- package/dist/types/schemas/index.js +12 -0
- package/dist/types/schemas/rtfm-output.d.ts +8 -0
- package/dist/types/schemas/rtfm-output.js +7 -0
- package/dist/types/schemas/ui-output.d.ts +361 -0
- package/dist/types/schemas/ui-output.js +188 -0
- package/dist/utils/logger.d.ts +6 -0
- package/dist/utils/logger.js +34 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +4 -0
- package/docs/contracts/replicant-mcp.contract.json +2361 -0
- package/docs/rtfm/adb.md +11 -1
- package/docs/rtfm/build.md +7 -1
- package/docs/rtfm/cache.md +21 -0
- package/docs/rtfm/ui.md +16 -7
- package/package.json +13 -5
package/README.md
CHANGED
|
@@ -31,12 +31,13 @@ replicant-mcp is a [Model Context Protocol](https://modelcontextprotocol.io/) se
|
|
|
31
31
|
|
|
32
32
|
| Category | Capabilities |
|
|
33
33
|
|----------|-------------|
|
|
34
|
-
| **Build & Test** | Build APKs/bundles, run unit and instrumented tests, list modules/variants/tasks |
|
|
34
|
+
| **Build & Test** | Build APKs/bundles, run unit and instrumented tests, list modules/variants/tasks, test regression detection with baseline comparison |
|
|
35
35
|
| **Emulator** | Create, start, stop, wipe emulators; save/load/delete snapshots |
|
|
36
36
|
| **Device Control** | List connected devices, select active device, query device properties |
|
|
37
37
|
| **App Management** | Install, uninstall, launch, stop apps; clear app data |
|
|
38
38
|
| **Log Analysis** | Filter logcat by package, tag, level, time |
|
|
39
39
|
| **UI Automation** | Accessibility-first element finding, spatial proximity search, tap, text input, screenshots |
|
|
40
|
+
| **Diagnostics** | Environment health checks via `replicant doctor`; structured logging with configurable level and format |
|
|
40
41
|
|
|
41
42
|
---
|
|
42
43
|
|
|
@@ -44,7 +45,6 @@ replicant-mcp is a [Model Context Protocol](https://modelcontextprotocol.io/) se
|
|
|
44
45
|
|
|
45
46
|
- Custom build commands (project-specific overrides, auto-detect gradlew)
|
|
46
47
|
- Video capture (start/stop recording, duration-based capture)
|
|
47
|
-
- Raw screenshot mode for external context management
|
|
48
48
|
|
|
49
49
|
---
|
|
50
50
|
|
|
@@ -68,6 +68,12 @@ emulator -version # Should show Android emulator version
|
|
|
68
68
|
npm install -g replicant-mcp
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
+
After installation, run the built-in diagnostics to verify your environment:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
replicant doctor
|
|
75
|
+
```
|
|
76
|
+
|
|
71
77
|
### Updating
|
|
72
78
|
|
|
73
79
|
```bash
|
|
@@ -181,11 +187,31 @@ replicant-mcp uses progressive disclosure (summaries first, details on demand) t
|
|
|
181
187
|
## More Info
|
|
182
188
|
|
|
183
189
|
- **Configuration:** Set `REPLICANT_CONFIG` for advanced options. See [docs/configuration.md](docs/configuration.md).
|
|
190
|
+
- **Logging:** Set `REPLICANT_LOG_LEVEL` (`error`, `warn`, `info`, `debug`) and `REPLICANT_LOG_FORMAT` (`json` for structured output) to control server logging. Logs are written to stderr.
|
|
184
191
|
- **Troubleshooting:** Common issues and solutions in [docs/troubleshooting.md](docs/troubleshooting.md).
|
|
185
192
|
- **Tool documentation:** Ask Claude to call `rtfm` with a category like "build", "adb", "emulator", or "ui".
|
|
186
193
|
|
|
187
194
|
---
|
|
188
195
|
|
|
196
|
+
## Documentation
|
|
197
|
+
|
|
198
|
+
| Document | Description |
|
|
199
|
+
|----------|-------------|
|
|
200
|
+
| [Architecture](docs/architecture.md) | Design overview and progressive disclosure pattern |
|
|
201
|
+
| [Configuration](docs/configuration.md) | Config file reference, environment variables, Gradle setup |
|
|
202
|
+
| [API Stability](docs/api-stability.md) | Tool API versioning policy and deprecation process |
|
|
203
|
+
| [Security Model](docs/security.md) | adb-shell safety model, command denylist, threat boundaries |
|
|
204
|
+
| [Support Matrix](docs/support-matrix.md) | Tested OS, Node.js, Android SDK, and emulator versions |
|
|
205
|
+
| [Known Limitations](docs/known-limitations.md) | Accessibility gaps, timeouts, single-device focus, and more |
|
|
206
|
+
| [Artifacts](docs/artifacts.md) | `.replicant/` directory contents and privacy considerations |
|
|
207
|
+
| [Troubleshooting](docs/troubleshooting.md) | Common issues and solutions |
|
|
208
|
+
| [Changelog](CHANGELOG.md) | Version history |
|
|
209
|
+
| [Security Policy](SECURITY.md) | Vulnerability reporting process |
|
|
210
|
+
| [Support / Getting Help](SUPPORT.md) | How to report bugs and ask questions |
|
|
211
|
+
| [Contributing](CONTRIBUTING.md) | Development setup and guidelines |
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
189
215
|
## Contributing
|
|
190
216
|
|
|
191
217
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines.
|
package/dist/adapters/adb.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export declare class AdbAdapter {
|
|
|
15
15
|
logcat(deviceId: string, options: {
|
|
16
16
|
lines?: number;
|
|
17
17
|
filter?: string;
|
|
18
|
+
since?: string;
|
|
19
|
+
package?: string;
|
|
18
20
|
}): Promise<string>;
|
|
19
21
|
waitForDevice(deviceId: string, timeoutMs?: number): Promise<void>;
|
|
20
22
|
getProperties(deviceId: string): Promise<Record<string, string>>;
|
package/dist/adapters/adb.js
CHANGED
|
@@ -53,6 +53,9 @@ export class AdbAdapter {
|
|
|
53
53
|
}
|
|
54
54
|
async logcat(deviceId, options) {
|
|
55
55
|
const args = ["-s", deviceId, "logcat", "-d"];
|
|
56
|
+
if (options.since) {
|
|
57
|
+
args.push("-T", options.since);
|
|
58
|
+
}
|
|
56
59
|
if (options.lines) {
|
|
57
60
|
args.push("-t", options.lines.toString());
|
|
58
61
|
}
|
|
@@ -60,7 +63,15 @@ export class AdbAdapter {
|
|
|
60
63
|
args.push(...options.filter.split(" "));
|
|
61
64
|
}
|
|
62
65
|
const result = await this.adb(args);
|
|
63
|
-
|
|
66
|
+
let output = result.stdout;
|
|
67
|
+
// Package filtering: filter output lines containing the package name
|
|
68
|
+
// We use string matching on output lines rather than --pid (requires pidof)
|
|
69
|
+
// or -e regex (varies across adb versions)
|
|
70
|
+
if (options.package) {
|
|
71
|
+
const lines = output.split("\n");
|
|
72
|
+
output = lines.filter((line) => line.includes(options.package)).join("\n");
|
|
73
|
+
}
|
|
74
|
+
return output;
|
|
64
75
|
}
|
|
65
76
|
async waitForDevice(deviceId, timeoutMs = 30000) {
|
|
66
77
|
await this.adb(["-s", deviceId, "wait-for-device"], timeoutMs);
|
|
@@ -3,7 +3,20 @@ import { extractText, searchText } from "../services/ocr.js";
|
|
|
3
3
|
import { matchIconPattern, matchesResourceId } from "../services/icon-patterns.js";
|
|
4
4
|
import { filterIconCandidates, formatBounds, cropCandidateImage } from "../services/visual-candidates.js";
|
|
5
5
|
import { calculateGridCellBounds, calculatePositionCoordinates, createGridOverlay, POSITION_LABELS, } from "../services/grid.js";
|
|
6
|
+
function createEarlyStopResult(tier, source) {
|
|
7
|
+
return {
|
|
8
|
+
elements: [],
|
|
9
|
+
source,
|
|
10
|
+
tier,
|
|
11
|
+
confidence: "low",
|
|
12
|
+
stoppedEarly: true,
|
|
13
|
+
stoppedAtTier: tier,
|
|
14
|
+
nextTierAvailable: tier < 5 ? (tier + 1) : undefined,
|
|
15
|
+
stopReason: "maxTier limit reached",
|
|
16
|
+
};
|
|
17
|
+
}
|
|
6
18
|
export async function findWithFallbacks(deps, deviceId, selector, options = {}) {
|
|
19
|
+
const maxTier = options.maxTier ?? 5;
|
|
7
20
|
// Handle Tier 5 grid refinement FIRST (when gridCell and gridPosition are provided)
|
|
8
21
|
if (options.gridCell !== undefined && options.gridPosition !== undefined) {
|
|
9
22
|
let width, height;
|
|
@@ -42,6 +55,9 @@ export async function findWithFallbacks(deps, deviceId, selector, options = {})
|
|
|
42
55
|
confidence: "high",
|
|
43
56
|
};
|
|
44
57
|
}
|
|
58
|
+
if (maxTier === 1) {
|
|
59
|
+
return createEarlyStopResult(1, "accessibility");
|
|
60
|
+
}
|
|
45
61
|
// Tier 2: ResourceId pattern match (for text-based queries)
|
|
46
62
|
if (selector.text || selector.textContains) {
|
|
47
63
|
const query = selector.text || selector.textContains;
|
|
@@ -62,9 +78,12 @@ export async function findWithFallbacks(deps, deviceId, selector, options = {})
|
|
|
62
78
|
};
|
|
63
79
|
}
|
|
64
80
|
}
|
|
81
|
+
if (maxTier === 2) {
|
|
82
|
+
return createEarlyStopResult(2, "accessibility");
|
|
83
|
+
}
|
|
65
84
|
}
|
|
66
85
|
// Tier 3: OCR
|
|
67
|
-
if (selector.text || selector.textContains) {
|
|
86
|
+
if ((selector.text || selector.textContains) && maxTier >= 3) {
|
|
68
87
|
const searchTerm = selector.text || selector.textContains;
|
|
69
88
|
const screenshotResult = await deps.screenshot(deviceId, {});
|
|
70
89
|
try {
|
|
@@ -81,6 +100,9 @@ export async function findWithFallbacks(deps, deviceId, selector, options = {})
|
|
|
81
100
|
: undefined,
|
|
82
101
|
};
|
|
83
102
|
}
|
|
103
|
+
if (maxTier === 3) {
|
|
104
|
+
return createEarlyStopResult(3, "ocr");
|
|
105
|
+
}
|
|
84
106
|
// Tier 4: Visual candidates (unlabeled clickables)
|
|
85
107
|
const tree = await deps.dump(deviceId);
|
|
86
108
|
const flat = flattenTree(tree);
|
|
@@ -106,6 +128,9 @@ export async function findWithFallbacks(deps, deviceId, selector, options = {})
|
|
|
106
128
|
: undefined,
|
|
107
129
|
};
|
|
108
130
|
}
|
|
131
|
+
if (maxTier === 4) {
|
|
132
|
+
return createEarlyStopResult(4, "visual");
|
|
133
|
+
}
|
|
109
134
|
// Tier 5: Grid fallback
|
|
110
135
|
const gridImage = await createGridOverlay(screenshotResult.path);
|
|
111
136
|
return {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
export interface CheckResult {
|
|
3
|
+
name: string;
|
|
4
|
+
status: "ok" | "warn" | "fail";
|
|
5
|
+
detail: string;
|
|
6
|
+
suggestion?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function runChecks(): CheckResult[];
|
|
9
|
+
export declare function formatJson(checks: CheckResult[]): string;
|
|
10
|
+
export declare function createDoctorCommand(): Command;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import { existsSync } from "fs";
|
|
4
|
+
// All commands below are hardcoded literals (no user input), so execSync is safe here.
|
|
5
|
+
function exec(cmd) {
|
|
6
|
+
return execSync(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
|
|
7
|
+
}
|
|
8
|
+
function checkNode() {
|
|
9
|
+
const version = process.version;
|
|
10
|
+
const major = parseInt(version.slice(1).split(".")[0], 10);
|
|
11
|
+
if (major >= 18) {
|
|
12
|
+
return { name: "Node.js", status: "ok", detail: version };
|
|
13
|
+
}
|
|
14
|
+
return { name: "Node.js", status: "fail", detail: version, suggestion: "Upgrade to Node.js >= 18" };
|
|
15
|
+
}
|
|
16
|
+
function checkNpm() {
|
|
17
|
+
try {
|
|
18
|
+
return { name: "npm", status: "ok", detail: exec("npm --version") };
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return { name: "npm", status: "fail", detail: "not found", suggestion: "Install npm (comes with Node.js)" };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function checkAndroidHome() {
|
|
25
|
+
const home = process.env.ANDROID_HOME;
|
|
26
|
+
if (!home) {
|
|
27
|
+
return { name: "ANDROID_HOME", status: "fail", detail: "not set", suggestion: "Set ANDROID_HOME to your Android SDK path" };
|
|
28
|
+
}
|
|
29
|
+
if (!existsSync(home)) {
|
|
30
|
+
return { name: "ANDROID_HOME", status: "fail", detail: `${home} (not found)`, suggestion: "Path does not exist. Check ANDROID_HOME value" };
|
|
31
|
+
}
|
|
32
|
+
return { name: "ANDROID_HOME", status: "ok", detail: home };
|
|
33
|
+
}
|
|
34
|
+
function checkAdb() {
|
|
35
|
+
try {
|
|
36
|
+
const out = exec("adb version");
|
|
37
|
+
const match = out.match(/Android Debug Bridge version ([\d.]+)/);
|
|
38
|
+
return { name: "adb", status: "ok", detail: match ? match[1] : "installed" };
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return { name: "adb", status: "fail", detail: "not found", suggestion: "Install Android SDK platform-tools and add to PATH" };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function checkEmulator() {
|
|
45
|
+
try {
|
|
46
|
+
const out = exec("emulator -version");
|
|
47
|
+
const match = out.match(/version ([\d.]+)/);
|
|
48
|
+
return { name: "emulator", status: "ok", detail: match ? match[1] : "installed" };
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return { name: "emulator", status: "fail", detail: "not found", suggestion: "Install Android SDK emulator and add to PATH" };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function checkAvdmanager() {
|
|
55
|
+
try {
|
|
56
|
+
exec("avdmanager list avd");
|
|
57
|
+
return { name: "avdmanager", status: "ok", detail: "installed" };
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
const msg = e instanceof Error ? e.message : "";
|
|
61
|
+
const detail = msg.includes("ENOENT") || msg.includes("not found") ? "not found" : "command failed";
|
|
62
|
+
return { name: "avdmanager", status: "fail", detail, suggestion: "Install Android SDK cmdline-tools and add to PATH" };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function checkAvds(avdmanagerResult) {
|
|
66
|
+
if (avdmanagerResult.status === "fail") {
|
|
67
|
+
return { name: "AVDs", status: "fail", detail: "skipped (avdmanager unavailable)", suggestion: "Install avdmanager first" };
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const out = exec("avdmanager list avd");
|
|
71
|
+
const matches = out.match(/Name:/g);
|
|
72
|
+
const count = matches ? matches.length : 0;
|
|
73
|
+
if (count === 0) {
|
|
74
|
+
return { name: "AVDs", status: "warn", detail: "none found", suggestion: "Create an AVD: avdmanager create avd ..." };
|
|
75
|
+
}
|
|
76
|
+
return { name: "AVDs", status: "ok", detail: `${count} available` };
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return { name: "AVDs", status: "fail", detail: "could not list", suggestion: "avdmanager command failed" };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function checkDevices(adbResult) {
|
|
83
|
+
if (adbResult.status === "fail") {
|
|
84
|
+
return { name: "Connected devices", status: "fail", detail: "skipped (adb unavailable)", suggestion: "Install adb first" };
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const out = exec("adb devices");
|
|
88
|
+
const lines = out.split("\n").filter((l) => l.includes("\tdevice"));
|
|
89
|
+
return { name: "Connected devices", status: lines.length > 0 ? "ok" : "warn", detail: `${lines.length} connected` };
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return { name: "Connected devices", status: "fail", detail: "could not query", suggestion: "adb command failed" };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function checkGradle() {
|
|
96
|
+
try {
|
|
97
|
+
const out = exec("gradle --version");
|
|
98
|
+
const match = out.match(/Gradle ([\d.]+)/);
|
|
99
|
+
return { name: "System gradle", status: "ok", detail: match ? match[1] : "installed" };
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return { name: "System gradle", status: "warn", detail: "not found (optional)", suggestion: "Most projects use the Gradle wrapper (gradlew) instead" };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export function runChecks() {
|
|
106
|
+
const adbResult = checkAdb();
|
|
107
|
+
const avdmanagerResult = checkAvdmanager();
|
|
108
|
+
return [
|
|
109
|
+
checkNode(), checkNpm(), checkAndroidHome(), adbResult,
|
|
110
|
+
checkEmulator(), avdmanagerResult, checkAvds(avdmanagerResult), checkDevices(adbResult), checkGradle(),
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
function formatTty(checks) {
|
|
114
|
+
const symbols = { ok: "\u2713 OK", warn: "\u26A0 WARN", fail: "\u2717 FAIL" };
|
|
115
|
+
const colors = { ok: "\x1b[32m", warn: "\x1b[33m", fail: "\x1b[31m" };
|
|
116
|
+
const reset = "\x1b[0m";
|
|
117
|
+
const lines = ["\nReplicant Doctor\n================\n"];
|
|
118
|
+
for (const c of checks) {
|
|
119
|
+
const sym = `${colors[c.status]}${symbols[c.status]}${reset}`;
|
|
120
|
+
lines.push(` ${sym} ${c.name}: ${c.detail}`);
|
|
121
|
+
if (c.suggestion)
|
|
122
|
+
lines.push(` ${c.suggestion}`);
|
|
123
|
+
}
|
|
124
|
+
const summary = { ok: 0, warn: 0, fail: 0 };
|
|
125
|
+
for (const c of checks)
|
|
126
|
+
summary[c.status]++;
|
|
127
|
+
lines.push(`\n${summary.ok} passed, ${summary.warn} warnings, ${summary.fail} failures\n`);
|
|
128
|
+
return lines.join("\n");
|
|
129
|
+
}
|
|
130
|
+
export function formatJson(checks) {
|
|
131
|
+
const summary = { ok: 0, warn: 0, fail: 0 };
|
|
132
|
+
for (const c of checks)
|
|
133
|
+
summary[c.status]++;
|
|
134
|
+
const status = summary.fail > 0 ? "fail" : summary.warn > 0 ? "warn" : "ok";
|
|
135
|
+
return JSON.stringify({ status, checks, summary }, null, 2);
|
|
136
|
+
}
|
|
137
|
+
export function createDoctorCommand() {
|
|
138
|
+
return new Command("doctor")
|
|
139
|
+
.description("Check environment for Android development readiness")
|
|
140
|
+
.option("--json", "Output as JSON")
|
|
141
|
+
.action((options) => {
|
|
142
|
+
const checks = runChecks();
|
|
143
|
+
const useJson = options.json || !process.stdout.isTTY;
|
|
144
|
+
if (useJson) {
|
|
145
|
+
console.log(formatJson(checks));
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
console.log(formatTty(checks));
|
|
149
|
+
}
|
|
150
|
+
const hasFail = checks.some((c) => c.status === "fail");
|
|
151
|
+
if (hasFail)
|
|
152
|
+
process.exit(1);
|
|
153
|
+
});
|
|
154
|
+
}
|
package/dist/cli/emulator.js
CHANGED
|
@@ -135,7 +135,7 @@ export function createEmulatorCommand() {
|
|
|
135
135
|
console.log(`Snapshot loaded: ${snapshotName}`);
|
|
136
136
|
}
|
|
137
137
|
break;
|
|
138
|
-
case "list":
|
|
138
|
+
case "list": {
|
|
139
139
|
const snapshots = await adapter.snapshotList(deviceId);
|
|
140
140
|
if (options.json) {
|
|
141
141
|
console.log(JSON.stringify({ deviceId, snapshots }, null, 2));
|
|
@@ -152,6 +152,7 @@ export function createEmulatorCommand() {
|
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
break;
|
|
155
|
+
}
|
|
155
156
|
case "delete":
|
|
156
157
|
if (!snapshotName) {
|
|
157
158
|
console.error("Error: --name is required for delete action");
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -3,4 +3,5 @@ export { createAdbCommand } from "./adb.js";
|
|
|
3
3
|
export { createEmulatorCommand } from "./emulator.js";
|
|
4
4
|
export { createUiCommand } from "./ui.js";
|
|
5
5
|
export { createCacheCommand } from "./cache.js";
|
|
6
|
+
export { createDoctorCommand } from "./doctor.js";
|
|
6
7
|
export * from "./formatter.js";
|
package/dist/cli/index.js
CHANGED
|
@@ -3,4 +3,5 @@ export { createAdbCommand } from "./adb.js";
|
|
|
3
3
|
export { createEmulatorCommand } from "./emulator.js";
|
|
4
4
|
export { createUiCommand } from "./ui.js";
|
|
5
5
|
export { createCacheCommand } from "./cache.js";
|
|
6
|
+
export { createDoctorCommand } from "./doctor.js";
|
|
6
7
|
export * from "./formatter.js";
|
package/dist/cli.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
|
-
import { createGradleCommand, createAdbCommand, createEmulatorCommand, createUiCommand, createCacheCommand, } from "./cli/index.js";
|
|
3
|
+
import { createGradleCommand, createAdbCommand, createEmulatorCommand, createUiCommand, createCacheCommand, createDoctorCommand, } from "./cli/index.js";
|
|
4
|
+
import { VERSION } from "./version.js";
|
|
4
5
|
const program = new Command();
|
|
5
6
|
program
|
|
6
|
-
.name("replicant")
|
|
7
|
+
.name("replicant-mcp")
|
|
7
8
|
.description("Android development CLI")
|
|
8
|
-
.version(
|
|
9
|
+
.version(VERSION);
|
|
9
10
|
program.addCommand(createGradleCommand());
|
|
10
11
|
program.addCommand(createAdbCommand());
|
|
11
12
|
program.addCommand(createEmulatorCommand());
|
|
12
13
|
program.addCommand(createUiCommand());
|
|
13
14
|
program.addCommand(createCacheCommand());
|
|
15
|
+
program.addCommand(createDoctorCommand());
|
|
14
16
|
program.parse();
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
if (process.argv.length > 2) {
|
|
3
|
+
// CLI mode: has arguments (e.g., --version, doctor, adb, etc.)
|
|
4
|
+
import("./cli.js").catch((error) => {
|
|
5
|
+
console.error("Failed to load CLI:", error);
|
|
6
|
+
process.exit(1);
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
// MCP server mode: no arguments, start the server
|
|
11
|
+
Promise.all([import("./server.js"), import("./utils/logger.js")])
|
|
12
|
+
.then(([{ runServer }, { logger }]) => {
|
|
13
|
+
runServer().catch((error) => {
|
|
14
|
+
logger.error("Server error", { error: String(error) });
|
|
15
|
+
process.exit(1);
|
|
16
|
+
});
|
|
17
|
+
})
|
|
18
|
+
.catch((error) => {
|
|
19
|
+
console.error("Failed to start server:", error);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export {};
|
|
@@ -31,6 +31,12 @@ export function parseTestOutput(output) {
|
|
|
31
31
|
while ((match = failureRegex.exec(output)) !== null) {
|
|
32
32
|
failures.push({ test: `${match[1]}.${match[2]}`, message: "" });
|
|
33
33
|
}
|
|
34
|
+
// Extract passed test names
|
|
35
|
+
const passedTests = [];
|
|
36
|
+
const passedRegex = /(\S+) > (\S+) PASSED/g;
|
|
37
|
+
while ((match = passedRegex.exec(output)) !== null) {
|
|
38
|
+
passedTests.push(`${match[1]}.${match[2]}`);
|
|
39
|
+
}
|
|
34
40
|
return {
|
|
35
41
|
passed,
|
|
36
42
|
failed,
|
|
@@ -38,6 +44,7 @@ export function parseTestOutput(output) {
|
|
|
38
44
|
total,
|
|
39
45
|
duration: durationMatch?.[1],
|
|
40
46
|
failures,
|
|
47
|
+
passedTests,
|
|
41
48
|
};
|
|
42
49
|
}
|
|
43
50
|
export function parseModuleList(output) {
|
package/dist/server.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
3
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { ZodError } from "zod";
|
|
4
5
|
import { CacheManager, DeviceStateManager, ProcessRunner, EnvironmentService, ConfigManager } from "./services/index.js";
|
|
5
6
|
import { AdbAdapter, EmulatorAdapter, GradleAdapter, UiAutomatorAdapter } from "./adapters/index.js";
|
|
6
|
-
import { ReplicantError } from "./types/index.js";
|
|
7
|
-
import {
|
|
7
|
+
import { ReplicantError, ErrorCode } from "./types/index.js";
|
|
8
|
+
import { VERSION } from "./version.js";
|
|
9
|
+
import { cacheInputSchema, cacheToolDefinition, handleCacheTool, rtfmInputSchema, rtfmToolDefinition, handleRtfmTool, adbDeviceInputSchema, adbDeviceToolDefinition, handleAdbDeviceTool, adbAppInputSchema, adbAppToolDefinition, handleAdbAppTool, adbLogcatInputSchema, adbLogcatToolDefinition, handleAdbLogcatTool, adbShellInputSchema, adbShellToolDefinition, handleAdbShellTool, emulatorDeviceInputSchema, emulatorDeviceToolDefinition, handleEmulatorDeviceTool, gradleBuildInputSchema, gradleBuildToolDefinition, handleGradleBuildTool, gradleTestInputSchema, gradleTestToolDefinition, handleGradleTestTool, gradleListInputSchema, gradleListToolDefinition, handleGradleListTool, gradleGetDetailsInputSchema, gradleGetDetailsToolDefinition, handleGradleGetDetailsTool, uiInputSchema, uiToolDefinition, handleUiTool, } from "./tools/index.js";
|
|
8
10
|
export function createServerContext() {
|
|
9
11
|
const environment = new EnvironmentService();
|
|
10
12
|
const processRunner = new ProcessRunner(environment);
|
|
@@ -37,31 +39,49 @@ const toolDefinitions = [
|
|
|
37
39
|
uiToolDefinition,
|
|
38
40
|
];
|
|
39
41
|
async function dispatchToolCall(name, args, context) {
|
|
42
|
+
const rawArgs = args ?? {};
|
|
43
|
+
const parseOrThrow = (toolName, parser) => {
|
|
44
|
+
try {
|
|
45
|
+
return parser.parse(rawArgs);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (error instanceof ZodError) {
|
|
49
|
+
const message = error.issues
|
|
50
|
+
.map((issue) => {
|
|
51
|
+
const path = issue.path.length > 0 ? issue.path.join(".") : "input";
|
|
52
|
+
return `${path}: ${issue.message}`;
|
|
53
|
+
})
|
|
54
|
+
.join("; ");
|
|
55
|
+
throw new ReplicantError(ErrorCode.INPUT_VALIDATION_FAILED, `Invalid input for ${toolName}: ${message}`, "Check the tool input schema and provide valid arguments");
|
|
56
|
+
}
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
40
60
|
switch (name) {
|
|
41
61
|
case "cache":
|
|
42
|
-
return handleCacheTool(
|
|
62
|
+
return handleCacheTool(parseOrThrow("cache", cacheInputSchema), context.cache);
|
|
43
63
|
case "rtfm":
|
|
44
|
-
return handleRtfmTool(
|
|
64
|
+
return handleRtfmTool(parseOrThrow("rtfm", rtfmInputSchema));
|
|
45
65
|
case "adb-device":
|
|
46
|
-
return handleAdbDeviceTool(
|
|
66
|
+
return handleAdbDeviceTool(parseOrThrow("adb-device", adbDeviceInputSchema), context);
|
|
47
67
|
case "adb-app":
|
|
48
|
-
return handleAdbAppTool(
|
|
68
|
+
return handleAdbAppTool(parseOrThrow("adb-app", adbAppInputSchema), context);
|
|
49
69
|
case "adb-logcat":
|
|
50
|
-
return handleAdbLogcatTool(
|
|
70
|
+
return handleAdbLogcatTool(parseOrThrow("adb-logcat", adbLogcatInputSchema), context);
|
|
51
71
|
case "adb-shell":
|
|
52
|
-
return handleAdbShellTool(
|
|
72
|
+
return handleAdbShellTool(parseOrThrow("adb-shell", adbShellInputSchema), context);
|
|
53
73
|
case "emulator-device":
|
|
54
|
-
return handleEmulatorDeviceTool(
|
|
74
|
+
return handleEmulatorDeviceTool(parseOrThrow("emulator-device", emulatorDeviceInputSchema), context);
|
|
55
75
|
case "gradle-build":
|
|
56
|
-
return handleGradleBuildTool(
|
|
76
|
+
return handleGradleBuildTool(parseOrThrow("gradle-build", gradleBuildInputSchema), context);
|
|
57
77
|
case "gradle-test":
|
|
58
|
-
return handleGradleTestTool(
|
|
78
|
+
return handleGradleTestTool(parseOrThrow("gradle-test", gradleTestInputSchema), context);
|
|
59
79
|
case "gradle-list":
|
|
60
|
-
return handleGradleListTool(
|
|
80
|
+
return handleGradleListTool(parseOrThrow("gradle-list", gradleListInputSchema), context);
|
|
61
81
|
case "gradle-get-details":
|
|
62
|
-
return handleGradleGetDetailsTool(
|
|
82
|
+
return handleGradleGetDetailsTool(parseOrThrow("gradle-get-details", gradleGetDetailsInputSchema), context);
|
|
63
83
|
case "ui":
|
|
64
|
-
return handleUiTool(
|
|
84
|
+
return handleUiTool(parseOrThrow("ui", uiInputSchema), context, context.config.getUiConfig());
|
|
65
85
|
default:
|
|
66
86
|
throw new Error(`Unknown tool: ${name}`);
|
|
67
87
|
}
|
|
@@ -69,7 +89,7 @@ async function dispatchToolCall(name, args, context) {
|
|
|
69
89
|
export async function createServer(context) {
|
|
70
90
|
const server = new Server({
|
|
71
91
|
name: "replicant-mcp",
|
|
72
|
-
version:
|
|
92
|
+
version: VERSION,
|
|
73
93
|
}, {
|
|
74
94
|
capabilities: {
|
|
75
95
|
tools: {},
|
|
@@ -85,7 +105,7 @@ Tool mapping:
|
|
|
85
105
|
- Emulator control → emulator-device (not \`emulator\` CLI)
|
|
86
106
|
- Builds → gradle-build (not \`./gradlew\`)
|
|
87
107
|
- Tests → gradle-test (not \`./gradlew test\`)
|
|
88
|
-
- UI automation → ui (accessibility-first, screenshots auto-scaled to
|
|
108
|
+
- UI automation → ui (accessibility-first, screenshots auto-scaled to configured max dimension, default 800px)
|
|
89
109
|
|
|
90
110
|
Start with \`adb-device list\` to see connected devices.
|
|
91
111
|
Use \`rtfm\` for detailed documentation on any tool.`,
|
|
@@ -102,18 +122,18 @@ Use \`rtfm\` for detailed documentation on any tool.`,
|
|
|
102
122
|
return {
|
|
103
123
|
content: [
|
|
104
124
|
{ type: "image", data: base64, mimeType },
|
|
105
|
-
{ type: "text", text: JSON.stringify(metadata
|
|
125
|
+
{ type: "text", text: JSON.stringify(metadata) },
|
|
106
126
|
],
|
|
107
127
|
};
|
|
108
128
|
}
|
|
109
129
|
return {
|
|
110
|
-
content: [{ type: "text", text: JSON.stringify(result
|
|
130
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
111
131
|
};
|
|
112
132
|
}
|
|
113
133
|
catch (error) {
|
|
114
134
|
if (error instanceof ReplicantError) {
|
|
115
135
|
return {
|
|
116
|
-
content: [{ type: "text", text: JSON.stringify(error.toToolError()
|
|
136
|
+
content: [{ type: "text", text: JSON.stringify(error.toToolError()) }],
|
|
117
137
|
isError: true,
|
|
118
138
|
};
|
|
119
139
|
}
|
package/dist/services/config.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFile } from "fs/promises";
|
|
|
2
2
|
import { existsSync } from "fs";
|
|
3
3
|
import { parse as parseYaml } from "yaml";
|
|
4
4
|
import { DEFAULT_CONFIG } from "../types/config.js";
|
|
5
|
+
import { logger } from "../utils/logger.js";
|
|
5
6
|
/**
|
|
6
7
|
* Load configuration from REPLICANT_CONFIG environment variable path
|
|
7
8
|
* Falls back to defaults if not set or file doesn't exist
|
|
@@ -12,7 +13,7 @@ export async function loadConfig() {
|
|
|
12
13
|
return DEFAULT_CONFIG;
|
|
13
14
|
}
|
|
14
15
|
if (!existsSync(configPath)) {
|
|
15
|
-
|
|
16
|
+
logger.warn("REPLICANT_CONFIG set but file not found", { path: configPath });
|
|
16
17
|
return DEFAULT_CONFIG;
|
|
17
18
|
}
|
|
18
19
|
try {
|
|
@@ -29,7 +30,7 @@ export async function loadConfig() {
|
|
|
29
30
|
}
|
|
30
31
|
catch (error) {
|
|
31
32
|
const message = error instanceof Error ? error.message : String(error);
|
|
32
|
-
|
|
33
|
+
logger.warn("Failed to parse REPLICANT_CONFIG", { path: configPath, error: message });
|
|
33
34
|
return DEFAULT_CONFIG;
|
|
34
35
|
}
|
|
35
36
|
}
|
package/dist/services/index.d.ts
CHANGED
package/dist/services/index.js
CHANGED