replicant-mcp 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +138 -25
  2. package/dist/adapters/adb.d.ts +1 -0
  3. package/dist/adapters/adb.js +7 -1
  4. package/dist/adapters/emulator.js +11 -11
  5. package/dist/adapters/ui-automator.d.ts +41 -1
  6. package/dist/adapters/ui-automator.js +256 -8
  7. package/dist/cli/gradle.js +3 -3
  8. package/dist/cli.js +1 -1
  9. package/dist/server.d.ts +3 -1
  10. package/dist/server.js +23 -3
  11. package/dist/services/config.d.ts +16 -0
  12. package/dist/services/config.js +62 -0
  13. package/dist/services/device-state.d.ts +2 -0
  14. package/dist/services/device-state.js +18 -0
  15. package/dist/services/environment.d.ts +18 -0
  16. package/dist/services/environment.js +130 -0
  17. package/dist/services/grid.d.ts +28 -0
  18. package/dist/services/grid.js +98 -0
  19. package/dist/services/icon-patterns.d.ts +10 -0
  20. package/dist/services/icon-patterns.js +51 -0
  21. package/dist/services/index.d.ts +6 -0
  22. package/dist/services/index.js +6 -0
  23. package/dist/services/ocr.d.ts +4 -0
  24. package/dist/services/ocr.js +59 -0
  25. package/dist/services/process-runner.d.ts +6 -0
  26. package/dist/services/process-runner.js +26 -0
  27. package/dist/services/visual-candidates.d.ts +24 -0
  28. package/dist/services/visual-candidates.js +78 -0
  29. package/dist/tools/adb-app.js +3 -2
  30. package/dist/tools/adb-device.d.ts +1 -0
  31. package/dist/tools/adb-device.js +47 -8
  32. package/dist/tools/adb-logcat.js +3 -2
  33. package/dist/tools/adb-shell.js +3 -2
  34. package/dist/tools/emulator-device.d.ts +1 -1
  35. package/dist/tools/gradle-get-details.d.ts +1 -1
  36. package/dist/tools/ui.d.ts +32 -1
  37. package/dist/tools/ui.js +253 -12
  38. package/dist/types/config.d.ts +34 -0
  39. package/dist/types/config.js +11 -0
  40. package/dist/types/errors.d.ts +25 -2
  41. package/dist/types/errors.js +23 -4
  42. package/dist/types/icon-recognition.d.ts +50 -0
  43. package/dist/types/icon-recognition.js +1 -0
  44. package/dist/types/index.d.ts +3 -0
  45. package/dist/types/index.js +3 -0
  46. package/dist/types/ocr.d.ts +21 -0
  47. package/dist/types/ocr.js +1 -0
  48. package/package.json +5 -2
@@ -8,8 +8,12 @@ const BLOCKED_PATTERNS = [
8
8
  /\bformat\b/, // format commands
9
9
  ];
10
10
  export class ProcessRunner {
11
+ environment;
11
12
  defaultTimeoutMs = 30_000;
12
13
  maxTimeoutMs = 120_000;
14
+ constructor(environment) {
15
+ this.environment = environment;
16
+ }
13
17
  async run(command, args, options = {}) {
14
18
  this.validateCommand(command, args);
15
19
  const timeoutMs = Math.min(options.timeoutMs ?? this.defaultTimeoutMs, this.maxTimeoutMs);
@@ -48,6 +52,28 @@ export class ProcessRunner {
48
52
  throw error;
49
53
  }
50
54
  }
55
+ async runAdb(args, options = {}) {
56
+ if (!this.environment) {
57
+ // Fallback to bare "adb" if no environment service
58
+ return this.run("adb", args, options);
59
+ }
60
+ const adbPath = await this.environment.getAdbPath();
61
+ return this.run(adbPath, args, options);
62
+ }
63
+ async runEmulator(args, options = {}) {
64
+ if (!this.environment) {
65
+ return this.run("emulator", args, options);
66
+ }
67
+ const emulatorPath = await this.environment.getEmulatorPath();
68
+ return this.run(emulatorPath, args, options);
69
+ }
70
+ async runAvdManager(args, options = {}) {
71
+ if (!this.environment) {
72
+ return this.run("avdmanager", args, options);
73
+ }
74
+ const avdManagerPath = await this.environment.getAvdManagerPath();
75
+ return this.run(avdManagerPath, args, options);
76
+ }
51
77
  validateCommand(command, args) {
52
78
  if (BLOCKED_COMMANDS.has(command)) {
53
79
  throw new ReplicantError(ErrorCode.COMMAND_BLOCKED, `Command '${command}' is not allowed`, "Use safe commands only");
@@ -0,0 +1,24 @@
1
+ import { AccessibilityNode, Bounds } from "../parsers/ui-dump.js";
2
+ export declare const MIN_ICON_SIZE = 16;
3
+ export declare const MAX_ICON_SIZE = 200;
4
+ export declare const MIN_ASPECT_RATIO = 0.5;
5
+ export declare const MAX_ASPECT_RATIO = 2;
6
+ export declare const MAX_CANDIDATES = 6;
7
+ /**
8
+ * Check if dimensions are within icon size constraints.
9
+ */
10
+ export declare function isIconSized(width: number, height: number): boolean;
11
+ /**
12
+ * Filter accessibility nodes to find unlabeled clickable elements that are icon-sized.
13
+ * Returns max 6 candidates sorted by position (top-to-bottom, left-to-right).
14
+ */
15
+ export declare function filterIconCandidates(nodes: AccessibilityNode[]): AccessibilityNode[];
16
+ /**
17
+ * Format candidate bounds as string "[x0,y0][x1,y1]"
18
+ */
19
+ export declare function formatBounds(node: AccessibilityNode): string;
20
+ /**
21
+ * Crop a region from an image and return as base64 JPEG.
22
+ * Scales down to max 128x128 if larger.
23
+ */
24
+ export declare function cropCandidateImage(imagePath: string, bounds: Bounds): Promise<string>;
@@ -0,0 +1,78 @@
1
+ import sharp from "sharp";
2
+ export const MIN_ICON_SIZE = 16;
3
+ export const MAX_ICON_SIZE = 200;
4
+ export const MIN_ASPECT_RATIO = 0.5;
5
+ export const MAX_ASPECT_RATIO = 2.0;
6
+ export const MAX_CANDIDATES = 6;
7
+ /**
8
+ * Check if dimensions are within icon size constraints.
9
+ */
10
+ export function isIconSized(width, height) {
11
+ if (width < MIN_ICON_SIZE || width > MAX_ICON_SIZE)
12
+ return false;
13
+ if (height < MIN_ICON_SIZE || height > MAX_ICON_SIZE)
14
+ return false;
15
+ const aspectRatio = width / height;
16
+ if (aspectRatio < MIN_ASPECT_RATIO || aspectRatio > MAX_ASPECT_RATIO)
17
+ return false;
18
+ return true;
19
+ }
20
+ /**
21
+ * Filter accessibility nodes to find unlabeled clickable elements that are icon-sized.
22
+ * Returns max 6 candidates sorted by position (top-to-bottom, left-to-right).
23
+ */
24
+ export function filterIconCandidates(nodes) {
25
+ const candidates = nodes.filter((node) => {
26
+ // Must be clickable
27
+ if (!node.clickable)
28
+ return false;
29
+ // Must not have text or contentDesc (we're looking for unlabeled icons)
30
+ if (node.text || node.contentDesc)
31
+ return false;
32
+ // Must be icon-sized
33
+ const width = node.bounds.right - node.bounds.left;
34
+ const height = node.bounds.bottom - node.bounds.top;
35
+ if (!isIconSized(width, height))
36
+ return false;
37
+ return true;
38
+ });
39
+ // Sort by Y first (top-to-bottom), then X (left-to-right)
40
+ candidates.sort((a, b) => {
41
+ if (a.centerY !== b.centerY)
42
+ return a.centerY - b.centerY;
43
+ return a.centerX - b.centerX;
44
+ });
45
+ // Limit to max candidates
46
+ return candidates.slice(0, MAX_CANDIDATES);
47
+ }
48
+ /**
49
+ * Format candidate bounds as string "[x0,y0][x1,y1]"
50
+ */
51
+ export function formatBounds(node) {
52
+ return `[${node.bounds.left},${node.bounds.top}][${node.bounds.right},${node.bounds.bottom}]`;
53
+ }
54
+ const MAX_CROP_SIZE = 128;
55
+ const JPEG_QUALITY = 70;
56
+ /**
57
+ * Crop a region from an image and return as base64 JPEG.
58
+ * Scales down to max 128x128 if larger.
59
+ */
60
+ export async function cropCandidateImage(imagePath, bounds) {
61
+ const width = bounds.right - bounds.left;
62
+ const height = bounds.bottom - bounds.top;
63
+ let pipeline = sharp(imagePath).extract({
64
+ left: bounds.left,
65
+ top: bounds.top,
66
+ width,
67
+ height,
68
+ });
69
+ // Scale down if larger than max size
70
+ if (width > MAX_CROP_SIZE || height > MAX_CROP_SIZE) {
71
+ pipeline = pipeline.resize(MAX_CROP_SIZE, MAX_CROP_SIZE, {
72
+ fit: "inside",
73
+ withoutEnlargement: true,
74
+ });
75
+ }
76
+ const buffer = await pipeline.jpeg({ quality: JPEG_QUALITY }).toBuffer();
77
+ return buffer.toString("base64");
78
+ }
@@ -5,7 +5,8 @@ export const adbAppInputSchema = z.object({
5
5
  packageName: z.string().optional(),
6
6
  });
7
7
  export async function handleAdbAppTool(input, context) {
8
- const deviceId = context.deviceState.requireCurrentDevice().id;
8
+ const device = await context.deviceState.ensureDevice(context.adb);
9
+ const deviceId = device.id;
9
10
  switch (input.operation) {
10
11
  case "install": {
11
12
  if (!input.apkPath) {
@@ -52,7 +53,7 @@ export async function handleAdbAppTool(input, context) {
52
53
  }
53
54
  export const adbAppToolDefinition = {
54
55
  name: "adb-app",
55
- description: "Manage applications. Operations: install, uninstall, launch, stop, clear-data, list.",
56
+ description: "Manage applications. Auto-selects device if only one connected. Operations: install, uninstall, launch, stop, clear-data, list.",
56
57
  inputSchema: {
57
58
  type: "object",
58
59
  properties: {
@@ -6,6 +6,7 @@ export declare const adbDeviceInputSchema: z.ZodObject<{
6
6
  select: "select";
7
7
  wait: "wait";
8
8
  properties: "properties";
9
+ "health-check": "health-check";
9
10
  }>;
10
11
  deviceId: z.ZodOptional<z.ZodString>;
11
12
  }, z.core.$strip>;
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  export const adbDeviceInputSchema = z.object({
3
- operation: z.enum(["list", "select", "wait", "properties"]),
3
+ operation: z.enum(["list", "select", "wait", "properties", "health-check"]),
4
4
  deviceId: z.string().optional(),
5
5
  });
6
6
  export async function handleAdbDeviceTool(input, context) {
@@ -28,15 +28,18 @@ export async function handleAdbDeviceTool(input, context) {
28
28
  return { selected: device };
29
29
  }
30
30
  case "wait": {
31
- const deviceId = input.deviceId || context.deviceState.getCurrentDevice()?.id;
32
- if (!deviceId) {
33
- throw new Error("No device selected. Call with deviceId or select a device first.");
34
- }
31
+ const device = input.deviceId
32
+ ? { id: input.deviceId }
33
+ : await context.deviceState.ensureDevice(context.adb);
34
+ const deviceId = device.id;
35
35
  await context.adb.waitForDevice(deviceId);
36
36
  return { status: "device ready", deviceId };
37
37
  }
38
38
  case "properties": {
39
- const deviceId = input.deviceId || context.deviceState.requireCurrentDevice().id;
39
+ const device = input.deviceId
40
+ ? { id: input.deviceId }
41
+ : await context.deviceState.ensureDevice(context.adb);
42
+ const deviceId = device.id;
40
43
  const props = await context.adb.getProperties(deviceId);
41
44
  return {
42
45
  deviceId,
@@ -50,19 +53,55 @@ export async function handleAdbDeviceTool(input, context) {
50
53
  allProperties: props,
51
54
  };
52
55
  }
56
+ case "health-check": {
57
+ const env = await context.environment.detect();
58
+ let adbServerRunning = false;
59
+ let connectedDevices = 0;
60
+ const warnings = [];
61
+ const errors = [];
62
+ if (!env.isValid) {
63
+ errors.push(...env.issues);
64
+ }
65
+ else {
66
+ // Test adb server
67
+ try {
68
+ const devices = await context.adb.getDevices();
69
+ adbServerRunning = true;
70
+ connectedDevices = devices.length;
71
+ if (devices.length === 0) {
72
+ warnings.push("No devices connected. Start an emulator or connect a USB device.");
73
+ }
74
+ }
75
+ catch (e) {
76
+ errors.push("adb server not responding. Run 'adb kill-server && adb start-server'");
77
+ }
78
+ }
79
+ return {
80
+ healthy: errors.length === 0,
81
+ environment: {
82
+ sdkPath: env.sdkPath,
83
+ adbPath: env.adbPath,
84
+ platform: env.platform,
85
+ },
86
+ adbServerRunning,
87
+ connectedDevices,
88
+ warnings,
89
+ errors,
90
+ };
91
+ }
53
92
  default:
54
93
  throw new Error(`Unknown operation: ${input.operation}`);
55
94
  }
56
95
  }
57
96
  export const adbDeviceToolDefinition = {
58
97
  name: "adb-device",
59
- description: "Manage device connections. Operations: list, select, wait, properties.",
98
+ description: "Manage device connections. Operations: list, select, wait, properties, health-check.",
60
99
  inputSchema: {
61
100
  type: "object",
62
101
  properties: {
63
102
  operation: {
64
103
  type: "string",
65
- enum: ["list", "select", "wait", "properties"],
104
+ enum: ["list", "select", "wait", "properties", "health-check"],
66
105
  },
67
106
  deviceId: { type: "string", description: "Device ID for select/wait/properties" },
68
107
  },
@@ -9,7 +9,8 @@ export const adbLogcatInputSchema = z.object({
9
9
  since: z.string().optional(),
10
10
  });
11
11
  export async function handleAdbLogcatTool(input, context) {
12
- const deviceId = context.deviceState.requireCurrentDevice().id;
12
+ const device = await context.deviceState.ensureDevice(context.adb);
13
+ const deviceId = device.id;
13
14
  // Build filter string
14
15
  let filter = "";
15
16
  if (input.rawFilter) {
@@ -55,7 +56,7 @@ export async function handleAdbLogcatTool(input, context) {
55
56
  }
56
57
  export const adbLogcatToolDefinition = {
57
58
  name: "adb-logcat",
58
- description: "Read device logs. Returns summary with logId for full output.",
59
+ description: "Read device logs. Auto-selects device if only one connected. Returns summary with logId for full output.",
59
60
  inputSchema: {
60
61
  type: "object",
61
62
  properties: {
@@ -4,7 +4,8 @@ export const adbShellInputSchema = z.object({
4
4
  timeout: z.number().optional(),
5
5
  });
6
6
  export async function handleAdbShellTool(input, context) {
7
- const deviceId = context.deviceState.requireCurrentDevice().id;
7
+ const device = await context.deviceState.ensureDevice(context.adb);
8
+ const deviceId = device.id;
8
9
  const result = await context.adb.shell(deviceId, input.command, input.timeout);
9
10
  return {
10
11
  stdout: result.stdout,
@@ -15,7 +16,7 @@ export async function handleAdbShellTool(input, context) {
15
16
  }
16
17
  export const adbShellToolDefinition = {
17
18
  name: "adb-shell",
18
- description: "Execute shell commands with safety guards. Dangerous commands are blocked.",
19
+ description: "Execute shell commands with safety guards. Auto-selects device if only one connected. Dangerous commands are blocked.",
19
20
  inputSchema: {
20
21
  type: "object",
21
22
  properties: {
@@ -3,8 +3,8 @@ import { ServerContext } from "../server.js";
3
3
  export declare const emulatorDeviceInputSchema: z.ZodObject<{
4
4
  operation: z.ZodEnum<{
5
5
  kill: "kill";
6
- list: "list";
7
6
  create: "create";
7
+ list: "list";
8
8
  start: "start";
9
9
  wipe: "wipe";
10
10
  "snapshot-save": "snapshot-save";
@@ -5,8 +5,8 @@ export declare const gradleGetDetailsInputSchema: z.ZodObject<{
5
5
  detailType: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
6
6
  all: "all";
7
7
  tasks: "tasks";
8
- logs: "logs";
9
8
  errors: "errors";
9
+ logs: "logs";
10
10
  }>>>;
11
11
  }, z.core.$strip>;
12
12
  export type GradleGetDetailsInput = z.infer<typeof gradleGetDetailsInputSchema>;
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { ServerContext } from "../server.js";
3
+ import { UiConfig } from "../types/index.js";
3
4
  export declare const uiInputSchema: z.ZodObject<{
4
5
  operation: z.ZodEnum<{
5
6
  find: "find";
@@ -8,21 +9,27 @@ export declare const uiInputSchema: z.ZodObject<{
8
9
  input: "input";
9
10
  screenshot: "screenshot";
10
11
  "accessibility-check": "accessibility-check";
12
+ "visual-snapshot": "visual-snapshot";
11
13
  }>;
12
14
  selector: z.ZodOptional<z.ZodObject<{
13
15
  resourceId: z.ZodOptional<z.ZodString>;
14
16
  text: z.ZodOptional<z.ZodString>;
15
17
  textContains: z.ZodOptional<z.ZodString>;
16
18
  className: z.ZodOptional<z.ZodString>;
19
+ nearestTo: z.ZodOptional<z.ZodString>;
17
20
  }, z.core.$strip>>;
18
21
  x: z.ZodOptional<z.ZodNumber>;
19
22
  y: z.ZodOptional<z.ZodNumber>;
20
23
  elementIndex: z.ZodOptional<z.ZodNumber>;
21
24
  text: z.ZodOptional<z.ZodString>;
22
25
  localPath: z.ZodOptional<z.ZodString>;
26
+ inline: z.ZodOptional<z.ZodBoolean>;
27
+ debug: z.ZodOptional<z.ZodBoolean>;
28
+ gridCell: z.ZodOptional<z.ZodNumber>;
29
+ gridPosition: z.ZodOptional<z.ZodNumber>;
23
30
  }, z.core.$strip>;
24
31
  export type UiInput = z.infer<typeof uiInputSchema>;
25
- export declare function handleUiTool(input: UiInput, context: ServerContext): Promise<Record<string, unknown>>;
32
+ export declare function handleUiTool(input: UiInput, context: ServerContext, uiConfig?: UiConfig): Promise<Record<string, unknown>>;
26
33
  export declare const uiToolDefinition: {
27
34
  name: string;
28
35
  description: string;
@@ -48,6 +55,10 @@ export declare const uiToolDefinition: {
48
55
  className: {
49
56
  type: string;
50
57
  };
58
+ nearestTo: {
59
+ type: string;
60
+ description: string;
61
+ };
51
62
  };
52
63
  description: string;
53
64
  };
@@ -71,6 +82,26 @@ export declare const uiToolDefinition: {
71
82
  type: string;
72
83
  description: string;
73
84
  };
85
+ inline: {
86
+ type: string;
87
+ description: string;
88
+ };
89
+ debug: {
90
+ type: string;
91
+ description: string;
92
+ };
93
+ gridCell: {
94
+ type: string;
95
+ minimum: number;
96
+ maximum: number;
97
+ description: string;
98
+ };
99
+ gridPosition: {
100
+ type: string;
101
+ minimum: number;
102
+ maximum: number;
103
+ description: string;
104
+ };
74
105
  };
75
106
  required: string[];
76
107
  };