replicant-mcp 1.4.1 → 1.4.3

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 CHANGED
@@ -64,28 +64,30 @@ emulator -version # Should show Android emulator version
64
64
 
65
65
  ### Installation
66
66
 
67
- **npm (recommended):**
68
67
  ```bash
69
68
  npm install -g replicant-mcp
70
69
  ```
71
70
 
72
- **From source:**
71
+ ### Updating
72
+
73
73
  ```bash
74
- git clone https://github.com/thecombatwombat/replicant-mcp.git
75
- cd replicant-mcp
76
- npm install && npm run build
74
+ npm update -g replicant-mcp
77
75
  ```
78
76
 
77
+ ---
78
+
79
+ ## Setup
80
+
79
81
  ### Claude Desktop
80
82
 
81
- Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS):
83
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
82
84
 
83
85
  ```json
84
86
  {
85
87
  "mcpServers": {
86
88
  "replicant": {
87
- "command": "node",
88
- "args": ["/absolute/path/to/replicant-mcp/dist/index.js"]
89
+ "command": "npx",
90
+ "args": ["-y", "replicant-mcp"]
89
91
  }
90
92
  }
91
93
  }
@@ -94,32 +96,78 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
94
96
  ### Claude Code
95
97
 
96
98
  ```bash
97
- claude mcp add replicant \
98
- -e ANDROID_HOME=$HOME/Library/Android/sdk \
99
- -e PATH="$HOME/Library/Android/sdk/platform-tools:$HOME/Library/Android/sdk/emulator:$HOME/Library/Android/sdk/cmdline-tools/latest/bin:$PATH" \
100
- -- node $(npm root -g)/replicant-mcp/dist/index.js
99
+ claude mcp add replicant -- npx -y replicant-mcp
101
100
  ```
102
101
 
103
- > Adjust `ANDROID_HOME` if your SDK is elsewhere. On Linux, it's typically `$HOME/Android/Sdk`.
102
+ ### Cursor
104
103
 
105
- ---
104
+ Add to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project):
106
105
 
107
- ## Example
106
+ ```json
107
+ {
108
+ "mcpServers": {
109
+ "replicant": {
110
+ "command": "npx",
111
+ "args": ["-y", "replicant-mcp"]
112
+ }
113
+ }
114
+ }
115
+ ```
116
+
117
+ ### Windsurf
118
+
119
+ Add to `~/.codeium/windsurf/mcp_config.json`:
120
+
121
+ ```json
122
+ {
123
+ "mcpServers": {
124
+ "replicant": {
125
+ "command": "npx",
126
+ "args": ["-y", "replicant-mcp"]
127
+ }
128
+ }
129
+ }
130
+ ```
131
+
132
+ ### OpenAI Codex CLI
108
133
 
134
+ Add to `~/.codex/config.toml`:
135
+
136
+ ```toml
137
+ [mcp_servers.replicant]
138
+ command = "npx"
139
+ args = ["-y", "replicant-mcp"]
140
+ ```
141
+
142
+ Or via CLI:
143
+
144
+ ```bash
145
+ codex mcp add replicant -- npx -y replicant-mcp
109
146
  ```
110
- You: "The app crashed after I tapped Login. What happened?"
111
147
 
112
- Claude: Let me check the error logs.
113
- [Calls adb-logcat with level: "error", lines: 50]
148
+ > **Note:** Ensure `adb` and `emulator` are in your PATH, or set `ANDROID_HOME` environment variable. On macOS: `~/Library/Android/sdk`, on Linux: `~/Android/Sdk`.
114
149
 
115
- Found the crash:
150
+ ---
116
151
 
117
- java.lang.NullPointerException: Attempt to invoke virtual method
118
- 'void com.example.User.getName()' on a null object reference
119
- at com.example.ProfileActivity.onCreate(ProfileActivity.kt:47)
152
+ ## Example
120
153
 
121
- Looks like `user` is null when ProfileActivity starts.
122
- Want me to look at that file?
154
+ ```
155
+ You: "I just changed the checkout flow. Build the app, go through checkout
156
+ with a test product, and make sure the confirmation screen shows up."
157
+
158
+ Claude: [Builds debug APK, installs on emulator, launches app]
159
+ [Taps "Electronics" category → "Wireless Headphones" → "Add to Cart"]
160
+ [Navigates to cart, taps "Checkout"]
161
+ [Fills shipping form, taps "Place Order"]
162
+
163
+ Confirmation screen is showing:
164
+ - Order #38291
165
+ - "Wireless Headphones" × 1
166
+ - Total: $79.99
167
+ - Estimated delivery: Jan 30
168
+
169
+ The flow works. Want me to test the edge cases—empty cart,
170
+ invalid address, payment failure?
123
171
  ```
124
172
 
125
173
  ---
@@ -23,6 +23,6 @@ export declare function calculatePositionCoordinates(position: 1 | 2 | 3 | 4 | 5
23
23
  export declare const POSITION_LABELS: string[];
24
24
  /**
25
25
  * Create a 4x6 numbered grid overlay on a screenshot.
26
- * Returns base64 PNG.
26
+ * Returns base64 JPEG (compressed for context efficiency).
27
27
  */
28
28
  export declare function createGridOverlay(imagePath: string): Promise<string>;
@@ -54,15 +54,21 @@ export const POSITION_LABELS = [
54
54
  ];
55
55
  const GRID_LINE_WIDTH = 3;
56
56
  const LABEL_FONT_SIZE = 36;
57
+ const MAX_GRID_DIMENSION = 1000;
58
+ const JPEG_QUALITY = 70;
57
59
  /**
58
60
  * Create a 4x6 numbered grid overlay on a screenshot.
59
- * Returns base64 PNG.
61
+ * Returns base64 JPEG (compressed for context efficiency).
60
62
  */
61
63
  export async function createGridOverlay(imagePath) {
62
64
  const image = sharp(imagePath);
63
65
  const metadata = await image.metadata();
64
66
  const width = metadata.width;
65
67
  const height = metadata.height;
68
+ // Calculate scaling to fit within MAX_GRID_DIMENSION
69
+ const scaleFactor = Math.min(1, MAX_GRID_DIMENSION / Math.max(width, height));
70
+ const scaledWidth = Math.round(width * scaleFactor);
71
+ const scaledHeight = Math.round(height * scaleFactor);
66
72
  const cellWidth = width / GRID_COLS;
67
73
  const cellHeight = height / GRID_ROWS;
68
74
  // Create SVG overlay with grid lines and numbers
@@ -90,9 +96,14 @@ export async function createGridOverlay(imagePath) {
90
96
  svgParts.push(`<text x="${centerX}" y="${centerY}" font-family="Arial, sans-serif" font-size="${LABEL_FONT_SIZE}" font-weight="bold" fill="white" text-anchor="middle" dominant-baseline="central">${cell}</text>`);
91
97
  }
92
98
  const svgOverlay = `<svg width="${width}" height="${height}">${svgParts.join("")}</svg>`;
93
- const buffer = await image
99
+ // Composite at original size first, then resize
100
+ const composited = await image
94
101
  .composite([{ input: Buffer.from(svgOverlay), top: 0, left: 0 }])
95
- .png()
102
+ .toBuffer();
103
+ // Resize and compress to JPEG
104
+ const buffer = await sharp(composited)
105
+ .resize(scaledWidth, scaledHeight)
106
+ .jpeg({ quality: JPEG_QUALITY })
96
107
  .toBuffer();
97
108
  return buffer.toString("base64");
98
109
  }
@@ -11,6 +11,9 @@ export declare const adbAppInputSchema: z.ZodObject<{
11
11
  }>;
12
12
  apkPath: z.ZodOptional<z.ZodString>;
13
13
  packageName: z.ZodOptional<z.ZodString>;
14
+ limit: z.ZodOptional<z.ZodNumber>;
15
+ filter: z.ZodOptional<z.ZodString>;
16
+ offset: z.ZodOptional<z.ZodNumber>;
14
17
  }, z.core.$strip>;
15
18
  export type AdbAppInput = z.infer<typeof adbAppInputSchema>;
16
19
  export declare function handleAdbAppTool(input: AdbAppInput, context: ServerContext): Promise<Record<string, unknown>>;
@@ -32,6 +35,18 @@ export declare const adbAppToolDefinition: {
32
35
  type: string;
33
36
  description: string;
34
37
  };
38
+ limit: {
39
+ type: string;
40
+ description: string;
41
+ };
42
+ filter: {
43
+ type: string;
44
+ description: string;
45
+ };
46
+ offset: {
47
+ type: string;
48
+ description: string;
49
+ };
35
50
  };
36
51
  required: string[];
37
52
  };
@@ -1,8 +1,13 @@
1
1
  import { z } from "zod";
2
+ import { CACHE_TTLS } from "../types/index.js";
2
3
  export const adbAppInputSchema = z.object({
3
4
  operation: z.enum(["install", "uninstall", "launch", "stop", "clear-data", "list"]),
4
5
  apkPath: z.string().optional(),
5
6
  packageName: z.string().optional(),
7
+ // List operation options
8
+ limit: z.number().min(1).max(100).optional(),
9
+ filter: z.string().optional(),
10
+ offset: z.number().min(0).optional(),
6
11
  });
7
12
  export async function handleAdbAppTool(input, context) {
8
13
  const device = await context.deviceState.ensureDevice(context.adb);
@@ -44,8 +49,30 @@ export async function handleAdbAppTool(input, context) {
44
49
  return { cleared: input.packageName, deviceId };
45
50
  }
46
51
  case "list": {
47
- const packages = await context.adb.getPackages(deviceId);
48
- return { packages, count: packages.length, deviceId };
52
+ const allPackages = await context.adb.getPackages(deviceId);
53
+ const limit = input.limit ?? 20;
54
+ const offset = input.offset ?? 0;
55
+ const filter = input.filter?.toLowerCase();
56
+ // Apply filter if provided
57
+ const filtered = filter
58
+ ? allPackages.filter((pkg) => pkg.toLowerCase().includes(filter))
59
+ : allPackages;
60
+ // Paginate
61
+ const paginated = filtered.slice(offset, offset + limit);
62
+ const hasMore = offset + limit < filtered.length;
63
+ // Cache full list for subsequent requests
64
+ const cacheId = context.cache.generateId("app-list");
65
+ context.cache.set(cacheId, { packages: filtered, deviceId, filter: filter || null }, "app-list", CACHE_TTLS.APP_LIST);
66
+ return {
67
+ packages: paginated,
68
+ count: paginated.length,
69
+ totalCount: filtered.length,
70
+ hasMore,
71
+ offset,
72
+ limit,
73
+ cacheId,
74
+ deviceId,
75
+ };
49
76
  }
50
77
  default:
51
78
  throw new Error(`Unknown operation: ${input.operation}`);
@@ -63,6 +90,18 @@ export const adbAppToolDefinition = {
63
90
  },
64
91
  apkPath: { type: "string", description: "Path to APK file (for install)" },
65
92
  packageName: { type: "string", description: "Package name (for other operations)" },
93
+ limit: {
94
+ type: "number",
95
+ description: "Max packages to return (default: 20, max: 100). For list operation.",
96
+ },
97
+ filter: {
98
+ type: "string",
99
+ description: "Filter packages by name (case-insensitive contains). For list operation.",
100
+ },
101
+ offset: {
102
+ type: "number",
103
+ description: "Skip first N packages for pagination. For list operation.",
104
+ },
66
105
  },
67
106
  required: ["operation"],
68
107
  },
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { CACHE_TTLS } from "../types/index.js";
2
3
  export const adbDeviceInputSchema = z.object({
3
4
  operation: z.enum(["list", "select", "wait", "properties", "health-check"]),
4
5
  deviceId: z.string().optional(),
@@ -41,16 +42,25 @@ export async function handleAdbDeviceTool(input, context) {
41
42
  : await context.deviceState.ensureDevice(context.adb);
42
43
  const deviceId = device.id;
43
44
  const props = await context.adb.getProperties(deviceId);
45
+ // Cache full properties for retrieval via cache tool
46
+ const cacheId = context.cache.generateId("device-props");
47
+ context.cache.set(cacheId, { deviceId, properties: props }, "device-props", CACHE_TTLS.DEVICE_PROPERTIES);
48
+ // Return summary only (key properties + cache ID for full details)
44
49
  return {
45
50
  deviceId,
46
- properties: {
51
+ summary: {
47
52
  model: props["ro.product.model"],
48
53
  manufacturer: props["ro.product.manufacturer"],
49
54
  sdkVersion: props["ro.build.version.sdk"],
50
55
  androidVersion: props["ro.build.version.release"],
51
56
  buildId: props["ro.build.id"],
57
+ device: props["ro.product.device"],
58
+ product: props["ro.product.name"],
59
+ hardware: props["ro.hardware"],
60
+ abiList: props["ro.product.cpu.abilist"],
52
61
  },
53
- allProperties: props,
62
+ propertyCount: Object.keys(props).length,
63
+ cacheId,
54
64
  };
55
65
  }
56
66
  case "health-check": {
@@ -39,6 +39,8 @@ export declare const uiInputSchema: z.ZodObject<{
39
39
  right: "right";
40
40
  }>>;
41
41
  amount: z.ZodOptional<z.ZodNumber>;
42
+ limit: z.ZodOptional<z.ZodNumber>;
43
+ offset: z.ZodOptional<z.ZodNumber>;
42
44
  }, z.core.$strip>;
43
45
  export type UiInput = z.infer<typeof uiInputSchema>;
44
46
  export declare function handleUiTool(input: UiInput, context: ServerContext, uiConfig?: UiConfig): Promise<Record<string, unknown>>;
@@ -141,6 +143,17 @@ export declare const uiToolDefinition: {
141
143
  maximum: number;
142
144
  description: string;
143
145
  };
146
+ limit: {
147
+ type: string;
148
+ minimum: number;
149
+ maximum: number;
150
+ description: string;
151
+ };
152
+ offset: {
153
+ type: string;
154
+ minimum: number;
155
+ description: string;
156
+ };
144
157
  };
145
158
  required: string[];
146
159
  };
package/dist/tools/ui.js CHANGED
@@ -25,6 +25,9 @@ export const uiInputSchema = z.object({
25
25
  compact: z.boolean().optional(),
26
26
  direction: z.enum(["up", "down", "left", "right"]).optional(),
27
27
  amount: z.number().min(0).max(1).optional(),
28
+ // Pagination for dump operation
29
+ limit: z.number().min(1).max(100).optional(),
30
+ offset: z.number().min(0).optional(),
28
31
  });
29
32
  // Store last find results for elementIndex reference
30
33
  // Updated to support accessibility, OCR, and grid elements
@@ -107,18 +110,47 @@ export async function handleUiTool(input, context, uiConfig) {
107
110
  // Cache the tree
108
111
  const dumpId = context.cache.generateId("ui-dump");
109
112
  context.cache.set(dumpId, { tree, deviceId }, "ui-dump", CACHE_TTLS.UI_TREE);
113
+ // Warning for empty dumps
114
+ const emptyWarning = tree.length === 0
115
+ ? "No accessibility nodes found. Possible causes: (1) UI still loading - wait and retry, (2) App uses custom rendering (Flutter, games, video players) - use screenshot instead, (3) App blocks accessibility services."
116
+ : undefined;
110
117
  if (input.compact) {
111
118
  // Compact mode: flat list of interactive elements only
112
119
  const flat = flattenTree(tree);
113
120
  const interactive = flat.filter((n) => n.clickable || n.focusable);
114
- const elements = interactive.map((n) => ({
121
+ // Pagination
122
+ const limit = input.limit ?? 20;
123
+ const offset = input.offset ?? 0;
124
+ const totalCount = interactive.length;
125
+ const paginated = interactive.slice(offset, offset + limit);
126
+ const hasMore = offset + limit < totalCount;
127
+ const elements = paginated.map((n) => ({
115
128
  text: n.text || n.contentDesc || undefined,
116
129
  type: n.className.split(".").pop(),
117
130
  x: n.centerX,
118
131
  y: n.centerY,
119
132
  resourceId: n.resourceId ? n.resourceId.split("/").pop() : undefined,
120
133
  }));
121
- return { dumpId, elements, count: elements.length, deviceId };
134
+ // Also warn if tree has nodes but no interactive elements
135
+ const noInteractiveWarning = tree.length > 0 && totalCount === 0
136
+ ? "Accessibility tree exists but no interactive elements found. Try 'ui find' with a text selector, or use screenshot for visual targeting."
137
+ : undefined;
138
+ // Hint for pagination
139
+ const hint = hasMore
140
+ ? `${elements.length} of ${totalCount} elements shown. Use 'ui find' for specific elements, or add offset: ${offset + limit} for more.`
141
+ : undefined;
142
+ return {
143
+ dumpId,
144
+ elements,
145
+ count: elements.length,
146
+ totalCount,
147
+ hasMore,
148
+ offset,
149
+ limit,
150
+ deviceId,
151
+ hint,
152
+ warning: emptyWarning || noInteractiveWarning,
153
+ };
122
154
  }
123
155
  // Full mode: hierarchical tree with all details
124
156
  const simplifyNode = (node, depth = 0) => ({
@@ -133,6 +165,7 @@ export async function handleUiTool(input, context, uiConfig) {
133
165
  dumpId,
134
166
  tree: tree.map((n) => simplifyNode(n)),
135
167
  deviceId,
168
+ warning: emptyWarning,
136
169
  };
137
170
  }
138
171
  case "find": {
@@ -410,7 +443,7 @@ export const uiToolDefinition = {
410
443
  },
411
444
  compact: {
412
445
  type: "boolean",
413
- description: "For dump: return flat list of interactive elements with {text, type, x, y, resourceId} instead of full tree.",
446
+ description: "For dump: return paginated flat list of interactive elements (default: 20, use limit/offset for more).",
414
447
  },
415
448
  direction: {
416
449
  type: "string",
@@ -423,6 +456,17 @@ export const uiToolDefinition = {
423
456
  maximum: 1,
424
457
  description: "Scroll amount as fraction of screen (0-1, default: 0.5)",
425
458
  },
459
+ limit: {
460
+ type: "number",
461
+ minimum: 1,
462
+ maximum: 100,
463
+ description: "For dump with compact: max elements to return (default: 20).",
464
+ },
465
+ offset: {
466
+ type: "number",
467
+ minimum: 0,
468
+ description: "For dump with compact: skip first N elements for pagination.",
469
+ },
426
470
  },
427
471
  required: ["operation"],
428
472
  },
@@ -21,4 +21,5 @@ export declare const CACHE_TTLS: {
21
21
  readonly UI_TREE: number;
22
22
  readonly GRADLE_VARIANTS: number;
23
23
  readonly LOGCAT: number;
24
+ readonly DEVICE_PROPERTIES: number;
24
25
  };
@@ -11,4 +11,5 @@ export const CACHE_TTLS = {
11
11
  UI_TREE: 30 * 1000, // 30 sec
12
12
  GRADLE_VARIANTS: 60 * 60 * 1000, // 1 hour
13
13
  LOGCAT: 5 * 60 * 1000, // 5 min
14
+ DEVICE_PROPERTIES: 5 * 60 * 1000, // 5 min
14
15
  };
@@ -0,0 +1,48 @@
1
+ # ADB Tools
2
+
3
+ ## adb-device
4
+
5
+ Manage device connections.
6
+
7
+ **Operations:**
8
+ - `list` - List connected devices
9
+ - `select` - Select active device
10
+ - `wait` - Wait for device to connect
11
+ - `properties` - Get device properties
12
+
13
+ ## adb-app
14
+
15
+ Manage applications.
16
+
17
+ **Operations:**
18
+ - `install` - Install APK
19
+ - `uninstall` - Uninstall package
20
+ - `launch` - Launch app
21
+ - `stop` - Force stop app
22
+ - `clear-data` - Clear app data
23
+
24
+ ## adb-logcat
25
+
26
+ Read device logs.
27
+
28
+ **Structured mode:**
29
+ - `package`: Filter to app's PID
30
+ - `tags`: Array of log tags
31
+ - `level`: "verbose" | "debug" | "info" | "warn" | "error"
32
+
33
+ **Raw mode:**
34
+ - `rawFilter`: Full logcat filter string (e.g., "ActivityManager:I MyApp:D *:S")
35
+
36
+ **Common:**
37
+ - `lines`: Number of lines (default: 100)
38
+ - `since`: Time filter ("5m" or ISO timestamp)
39
+
40
+ ## adb-shell
41
+
42
+ Execute shell commands with safety guards.
43
+
44
+ **Parameters:**
45
+ - `command` (required): Shell command
46
+ - `timeout`: Max execution time (default: 30s, max: 120s)
47
+
48
+ **Blocked commands:** rm -rf /, reboot, shutdown, su, sudo
@@ -0,0 +1,52 @@
1
+ # Build Tools
2
+
3
+ ## gradle-build
4
+
5
+ Build an Android application.
6
+
7
+ **Operations:**
8
+ - `assembleDebug` - Build debug APK
9
+ - `assembleRelease` - Build release APK
10
+ - `bundle` - Build Android App Bundle
11
+
12
+ **Parameters:**
13
+ - `operation` (required): Build operation
14
+ - `module`: Module path (e.g., ":app", ":feature:login")
15
+ - `flavor`: Product flavor name
16
+
17
+ **Returns:** `{ buildId, summary: { success, duration, apkSize, warnings } }`
18
+
19
+ **Example:**
20
+ ```json
21
+ { "operation": "assembleDebug", "module": ":app" }
22
+ ```
23
+
24
+ ## gradle-test
25
+
26
+ Run tests.
27
+
28
+ **Operations:**
29
+ - `unitTest` - Run unit tests
30
+ - `connectedTest` - Run instrumented tests on device
31
+
32
+ **Parameters:**
33
+ - `operation` (required): Test type
34
+ - `module`: Module path
35
+ - `filter`: Test filter (e.g., "com.example.MyTest", "*LoginTest*")
36
+
37
+ ## gradle-list
38
+
39
+ Introspect project structure.
40
+
41
+ **Operations:**
42
+ - `variants` - List build variants
43
+ - `modules` - List project modules
44
+ - `tasks` - List Gradle tasks
45
+
46
+ ## gradle-get-details
47
+
48
+ Fetch full output for a previous build/test.
49
+
50
+ **Parameters:**
51
+ - `id` (required): Build or test ID from previous operation
52
+ - `detailType`: "logs" | "errors" | "tasks"
@@ -0,0 +1,23 @@
1
+ # Emulator Tools
2
+
3
+ ## emulator-device
4
+
5
+ Manage Android emulators.
6
+
7
+ **Operations:**
8
+ - `list` - List available and running emulators
9
+ - `create` - Create new AVD
10
+ - `start` - Start emulator
11
+ - `kill` - Stop running emulator
12
+ - `wipe` - Reset emulator data
13
+ - `snapshot-save` - Save snapshot
14
+ - `snapshot-load` - Load snapshot
15
+ - `snapshot-list` - List snapshots
16
+ - `snapshot-delete` - Delete snapshot
17
+
18
+ **Parameters:**
19
+ - `operation` (required): Operation to perform
20
+ - `avdName`: AVD name (for create/start/wipe)
21
+ - `device`: Device profile (for create, e.g., "pixel_7")
22
+ - `systemImage`: System image (for create)
23
+ - `snapshotName`: Snapshot name (for snapshot operations)
@@ -0,0 +1,9 @@
1
+ # replicant-mcp Documentation
2
+
3
+ ## Categories
4
+
5
+ - **build** - Gradle build and test tools
6
+ - **adb** - Android Debug Bridge tools
7
+ - **emulator** - Emulator management tools
8
+ - **ui** - UI automation tools
9
+ - **cache** - Cache management
@@ -0,0 +1,107 @@
1
+ # UI Automation Tools
2
+
3
+ ## ui
4
+
5
+ Interact with app UI via accessibility tree with intelligent fallback.
6
+
7
+ **Operations:**
8
+ - `dump` - Get full accessibility tree
9
+ - `find` - Find elements by selector (with OCR/visual fallback)
10
+ - `tap` - Tap at coordinates, element index, or grid cell
11
+ - `input` - Enter text
12
+ - `screenshot` - Capture screen to file
13
+ - `accessibility-check` - Quick accessibility assessment
14
+ - `visual-snapshot` - Get screenshot + screen/app metadata
15
+
16
+ **Selectors (for find):**
17
+ - `resourceId`: Match resource ID (partial)
18
+ - `text`: Match exact text
19
+ - `textContains`: Match partial text
20
+ - `className`: Match class name
21
+ - `nearestTo`: Find elements nearest to this text (spatial proximity)
22
+
23
+ **Tap options:**
24
+ - `x`, `y`: Direct coordinates
25
+ - `elementIndex`: Index from previous find result
26
+ - `gridCell`: Grid cell 1-24 (6x4 grid overlay)
27
+ - `gridPosition`: Position within cell (1=TL, 2=TR, 3=Center, 4=BL, 5=BR)
28
+
29
+ **Optional parameters:**
30
+ - `debug`: Include source (accessibility/ocr) and confidence scores
31
+ - `inline`: Return base64 screenshot in response (for screenshot op)
32
+ - `localPath`: Custom path for screenshot output
33
+
34
+ **Fallback chain:**
35
+ 1. Accessibility tree (fast, reliable)
36
+ 2. OCR via Tesseract (when accessibility fails)
37
+ 3. Visual snapshot (screenshot + metadata for AI vision)
38
+
39
+ **Example - Find and tap:**
40
+ ```json
41
+ { "operation": "find", "selector": { "text": "Login" } }
42
+ // Returns: { elements: [{ index: 0, centerX: 540, centerY: 1200, ... }] }
43
+
44
+ { "operation": "tap", "elementIndex": 0 }
45
+ ```
46
+
47
+ **Example - Spatial proximity:**
48
+ ```json
49
+ { "operation": "find", "selector": { "textContains": "edit", "nearestTo": "John" } }
50
+ // Returns elements containing "edit", sorted by distance to "John"
51
+ ```
52
+
53
+ **Example - Grid-based tap (for icons):**
54
+ ```json
55
+ { "operation": "tap", "gridCell": 12, "gridPosition": 3 }
56
+ // Taps center of cell 12 in the 24-cell grid overlay
57
+ ```
58
+
59
+ ## Screenshot Scaling
60
+
61
+ Screenshots are automatically scaled to fit within 1000px (longest side) by default.
62
+ This prevents API context limits and reduces token usage.
63
+
64
+ **All coordinates are in image space.** Tap coordinates are automatically converted
65
+ to device coordinates. You don't need to do any math.
66
+
67
+ ### Scaling Modes
68
+
69
+ | Mode | Parameter | Behavior |
70
+ |------|-----------|----------|
71
+ | Default | (none) | Scale to 1000px max |
72
+ | Custom | `maxDimension: 1500` | Scale to specified size |
73
+ | Raw | `raw: true` | No scaling (may exceed API limits) |
74
+
75
+ ### When to Use Raw Mode
76
+
77
+ - Non-Anthropic models with different limits
78
+ - External context management (compaction, agent respawning)
79
+ - Debugging coordinate issues
80
+
81
+ ### Response Format
82
+
83
+ Screenshot responses now include scaling metadata:
84
+
85
+ ```json
86
+ {
87
+ "mode": "file",
88
+ "path": ".replicant/screenshots/screenshot-1234.png",
89
+ "device": { "width": 1080, "height": 2400 },
90
+ "image": { "width": 450, "height": 1000 },
91
+ "scaleFactor": 2.4
92
+ }
93
+ ```
94
+
95
+ ## Context Management
96
+
97
+ **Prefer accessibility tree (`dump`, `find`) because:**
98
+ - No context cost (text, not images)
99
+ - Coordinates are precise
100
+ - Faster execution
101
+
102
+ **Use screenshots when:**
103
+ - Accessibility tree is empty/unhelpful
104
+ - You need to see visual layout
105
+ - Icons have no text labels
106
+
107
+ **Ask yourself:** Do I need to SEE the screen, or just INTERACT with it?
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicant-mcp",
3
- "version": "1.4.1",
3
+ "version": "1.4.3",
4
4
  "description": "Android MCP server for AI-assisted Android development",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -44,6 +44,7 @@
44
44
  },
45
45
  "files": [
46
46
  "dist/",
47
+ "docs/rtfm/",
47
48
  "README.md",
48
49
  "LICENSE"
49
50
  ],