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.
Files changed (77) hide show
  1. package/README.md +28 -2
  2. package/dist/adapters/adb.d.ts +2 -0
  3. package/dist/adapters/adb.js +12 -1
  4. package/dist/adapters/ui-fallback-find.js +26 -1
  5. package/dist/cli/doctor.d.ts +10 -0
  6. package/dist/cli/doctor.js +154 -0
  7. package/dist/cli/emulator.js +2 -1
  8. package/dist/cli/index.d.ts +1 -0
  9. package/dist/cli/index.js +1 -0
  10. package/dist/cli.js +5 -3
  11. package/dist/index.js +22 -5
  12. package/dist/parsers/gradle-output.d.ts +1 -0
  13. package/dist/parsers/gradle-output.js +7 -0
  14. package/dist/server.js +39 -19
  15. package/dist/services/config.js +3 -2
  16. package/dist/services/index.d.ts +1 -0
  17. package/dist/services/index.js +1 -0
  18. package/dist/services/process-runner.d.ts +1 -0
  19. package/dist/services/process-runner.js +51 -0
  20. package/dist/services/test-baseline.d.ts +18 -0
  21. package/dist/services/test-baseline.js +51 -0
  22. package/dist/tools/adb-app.js +19 -13
  23. package/dist/tools/adb-device.js +7 -6
  24. package/dist/tools/adb-logcat.d.ts +3 -3
  25. package/dist/tools/adb-logcat.js +4 -2
  26. package/dist/tools/adb-shell.d.ts +15 -0
  27. package/dist/tools/adb-shell.js +31 -3
  28. package/dist/tools/cache.js +2 -1
  29. package/dist/tools/emulator-device.js +18 -13
  30. package/dist/tools/gradle-get-details.d.ts +15 -0
  31. package/dist/tools/gradle-get-details.js +131 -40
  32. package/dist/tools/gradle-list.js +2 -2
  33. package/dist/tools/gradle-test.d.ts +14 -0
  34. package/dist/tools/gradle-test.js +71 -6
  35. package/dist/tools/rtfm.js +4 -4
  36. package/dist/tools/ui-find.js +87 -50
  37. package/dist/tools/ui.d.ts +7 -0
  38. package/dist/tools/ui.js +20 -12
  39. package/dist/types/errors.d.ts +3 -0
  40. package/dist/types/errors.js +4 -0
  41. package/dist/types/icon-recognition.d.ts +5 -0
  42. package/dist/types/schemas/adb-app-output.d.ts +83 -0
  43. package/dist/types/schemas/adb-app-output.js +60 -0
  44. package/dist/types/schemas/adb-device-output.d.ts +148 -0
  45. package/dist/types/schemas/adb-device-output.js +72 -0
  46. package/dist/types/schemas/adb-logcat-output.d.ts +15 -0
  47. package/dist/types/schemas/adb-logcat-output.js +14 -0
  48. package/dist/types/schemas/adb-shell-output.d.ts +17 -0
  49. package/dist/types/schemas/adb-shell-output.js +16 -0
  50. package/dist/types/schemas/cache-output.d.ts +88 -0
  51. package/dist/types/schemas/cache-output.js +51 -0
  52. package/dist/types/schemas/emulator-device-output.d.ts +100 -0
  53. package/dist/types/schemas/emulator-device-output.js +76 -0
  54. package/dist/types/schemas/gradle-build-output.d.ts +16 -0
  55. package/dist/types/schemas/gradle-build-output.js +15 -0
  56. package/dist/types/schemas/gradle-get-details-output.d.ts +129 -0
  57. package/dist/types/schemas/gradle-get-details-output.js +86 -0
  58. package/dist/types/schemas/gradle-list-output.d.ts +56 -0
  59. package/dist/types/schemas/gradle-list-output.js +39 -0
  60. package/dist/types/schemas/gradle-test-output.d.ts +82 -0
  61. package/dist/types/schemas/gradle-test-output.js +54 -0
  62. package/dist/types/schemas/index.d.ts +12 -0
  63. package/dist/types/schemas/index.js +12 -0
  64. package/dist/types/schemas/rtfm-output.d.ts +8 -0
  65. package/dist/types/schemas/rtfm-output.js +7 -0
  66. package/dist/types/schemas/ui-output.d.ts +361 -0
  67. package/dist/types/schemas/ui-output.js +188 -0
  68. package/dist/utils/logger.d.ts +6 -0
  69. package/dist/utils/logger.js +34 -0
  70. package/dist/version.d.ts +1 -0
  71. package/dist/version.js +4 -0
  72. package/docs/contracts/replicant-mcp.contract.json +2361 -0
  73. package/docs/rtfm/adb.md +11 -1
  74. package/docs/rtfm/build.md +7 -1
  75. package/docs/rtfm/cache.md +21 -0
  76. package/docs/rtfm/ui.md +16 -7
  77. 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.
@@ -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>>;
@@ -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
- return result.stdout;
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
+ }
@@ -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");
@@ -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("1.0.0");
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
- import { runServer } from "./server.js";
3
- runServer().catch((error) => {
4
- console.error("Server error:", error);
5
- process.exit(1);
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 {};
@@ -17,6 +17,7 @@ export interface TestResult {
17
17
  test: string;
18
18
  message: string;
19
19
  }>;
20
+ passedTests: string[];
20
21
  }
21
22
  export declare function parseBuildOutput(output: string): BuildResult;
22
23
  export declare function parseTestOutput(output: string): TestResult;
@@ -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 { cacheToolDefinition, handleCacheTool, rtfmToolDefinition, handleRtfmTool, adbDeviceToolDefinition, handleAdbDeviceTool, adbAppToolDefinition, handleAdbAppTool, adbLogcatToolDefinition, handleAdbLogcatTool, adbShellToolDefinition, handleAdbShellTool, emulatorDeviceToolDefinition, handleEmulatorDeviceTool, gradleBuildToolDefinition, handleGradleBuildTool, gradleTestToolDefinition, handleGradleTestTool, gradleListToolDefinition, handleGradleListTool, gradleGetDetailsToolDefinition, handleGradleGetDetailsTool, uiToolDefinition, handleUiTool, } from "./tools/index.js";
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(args, context.cache);
62
+ return handleCacheTool(parseOrThrow("cache", cacheInputSchema), context.cache);
43
63
  case "rtfm":
44
- return handleRtfmTool(args);
64
+ return handleRtfmTool(parseOrThrow("rtfm", rtfmInputSchema));
45
65
  case "adb-device":
46
- return handleAdbDeviceTool(args, context);
66
+ return handleAdbDeviceTool(parseOrThrow("adb-device", adbDeviceInputSchema), context);
47
67
  case "adb-app":
48
- return handleAdbAppTool(args, context);
68
+ return handleAdbAppTool(parseOrThrow("adb-app", adbAppInputSchema), context);
49
69
  case "adb-logcat":
50
- return handleAdbLogcatTool(args, context);
70
+ return handleAdbLogcatTool(parseOrThrow("adb-logcat", adbLogcatInputSchema), context);
51
71
  case "adb-shell":
52
- return handleAdbShellTool(args, context);
72
+ return handleAdbShellTool(parseOrThrow("adb-shell", adbShellInputSchema), context);
53
73
  case "emulator-device":
54
- return handleEmulatorDeviceTool(args, context);
74
+ return handleEmulatorDeviceTool(parseOrThrow("emulator-device", emulatorDeviceInputSchema), context);
55
75
  case "gradle-build":
56
- return handleGradleBuildTool(args, context);
76
+ return handleGradleBuildTool(parseOrThrow("gradle-build", gradleBuildInputSchema), context);
57
77
  case "gradle-test":
58
- return handleGradleTestTool(args, context);
78
+ return handleGradleTestTool(parseOrThrow("gradle-test", gradleTestInputSchema), context);
59
79
  case "gradle-list":
60
- return handleGradleListTool(args, context);
80
+ return handleGradleListTool(parseOrThrow("gradle-list", gradleListInputSchema), context);
61
81
  case "gradle-get-details":
62
- return handleGradleGetDetailsTool(args, context);
82
+ return handleGradleGetDetailsTool(parseOrThrow("gradle-get-details", gradleGetDetailsInputSchema), context);
63
83
  case "ui":
64
- return handleUiTool(args, context, context.config.getUiConfig());
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: "1.0.0",
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 1000px)
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, null, 2) },
125
+ { type: "text", text: JSON.stringify(metadata) },
106
126
  ],
107
127
  };
108
128
  }
109
129
  return {
110
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
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(), null, 2) }],
136
+ content: [{ type: "text", text: JSON.stringify(error.toToolError()) }],
117
137
  isError: true,
118
138
  };
119
139
  }
@@ -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
- console.warn(`REPLICANT_CONFIG set but file not found: ${configPath}. Using defaults.`);
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
- console.warn(`Failed to parse REPLICANT_CONFIG at ${configPath}: ${message}. Using defaults.`);
33
+ logger.warn("Failed to parse REPLICANT_CONFIG", { path: configPath, error: message });
33
34
  return DEFAULT_CONFIG;
34
35
  }
35
36
  }
@@ -8,3 +8,4 @@ export * from "./icon-patterns.js";
8
8
  export * from "./grid.js";
9
9
  export * from "./visual-candidates.js";
10
10
  export * from "./scaling.js";
11
+ export * from "./test-baseline.js";
@@ -8,3 +8,4 @@ export * from "./icon-patterns.js";
8
8
  export * from "./grid.js";
9
9
  export * from "./visual-candidates.js";
10
10
  export * from "./scaling.js";
11
+ export * from "./test-baseline.js";
@@ -18,4 +18,5 @@ export declare class ProcessRunner {
18
18
  runEmulator(args: string[], options?: RunOptions): Promise<RunResult>;
19
19
  runAvdManager(args: string[], options?: RunOptions): Promise<RunResult>;
20
20
  private validateCommand;
21
+ private validateShellPayload;
21
22
  }