replicant-mcp 1.4.1 → 1.4.2

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": {
package/dist/tools/ui.js CHANGED
@@ -107,6 +107,10 @@ export async function handleUiTool(input, context, uiConfig) {
107
107
  // Cache the tree
108
108
  const dumpId = context.cache.generateId("ui-dump");
109
109
  context.cache.set(dumpId, { tree, deviceId }, "ui-dump", CACHE_TTLS.UI_TREE);
110
+ // Warning for empty dumps
111
+ const emptyWarning = tree.length === 0
112
+ ? "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."
113
+ : undefined;
110
114
  if (input.compact) {
111
115
  // Compact mode: flat list of interactive elements only
112
116
  const flat = flattenTree(tree);
@@ -118,7 +122,17 @@ export async function handleUiTool(input, context, uiConfig) {
118
122
  y: n.centerY,
119
123
  resourceId: n.resourceId ? n.resourceId.split("/").pop() : undefined,
120
124
  }));
121
- return { dumpId, elements, count: elements.length, deviceId };
125
+ // Also warn if tree has nodes but no interactive elements
126
+ const noInteractiveWarning = tree.length > 0 && elements.length === 0
127
+ ? "Accessibility tree exists but no interactive elements found. Try 'ui find' with a text selector, or use screenshot for visual targeting."
128
+ : undefined;
129
+ return {
130
+ dumpId,
131
+ elements,
132
+ count: elements.length,
133
+ deviceId,
134
+ warning: emptyWarning || noInteractiveWarning,
135
+ };
122
136
  }
123
137
  // Full mode: hierarchical tree with all details
124
138
  const simplifyNode = (node, depth = 0) => ({
@@ -133,6 +147,7 @@ export async function handleUiTool(input, context, uiConfig) {
133
147
  dumpId,
134
148
  tree: tree.map((n) => simplifyNode(n)),
135
149
  deviceId,
150
+ warning: emptyWarning,
136
151
  };
137
152
  }
138
153
  case "find": {
@@ -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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicant-mcp",
3
- "version": "1.4.1",
3
+ "version": "1.4.2",
4
4
  "description": "Android MCP server for AI-assisted Android development",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",