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 +72 -24
- package/dist/services/grid.d.ts +1 -1
- package/dist/services/grid.js +14 -3
- package/dist/tools/adb-app.d.ts +15 -0
- package/dist/tools/adb-app.js +41 -2
- package/dist/tools/adb-device.js +12 -2
- package/dist/tools/ui.js +16 -1
- package/dist/types/cache.d.ts +1 -0
- package/dist/types/cache.js +1 -0
- package/package.json +1 -1
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
|
-
|
|
71
|
+
### Updating
|
|
72
|
+
|
|
73
73
|
```bash
|
|
74
|
-
|
|
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": "
|
|
88
|
-
"args": ["
|
|
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
|
-
|
|
102
|
+
### Cursor
|
|
104
103
|
|
|
105
|
-
|
|
104
|
+
Add to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project):
|
|
106
105
|
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
150
|
+
---
|
|
116
151
|
|
|
117
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
---
|
package/dist/services/grid.d.ts
CHANGED
|
@@ -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
|
|
26
|
+
* Returns base64 JPEG (compressed for context efficiency).
|
|
27
27
|
*/
|
|
28
28
|
export declare function createGridOverlay(imagePath: string): Promise<string>;
|
package/dist/services/grid.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
.
|
|
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
|
}
|
package/dist/tools/adb-app.d.ts
CHANGED
|
@@ -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
|
};
|
package/dist/tools/adb-app.js
CHANGED
|
@@ -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
|
|
48
|
-
|
|
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
|
},
|
package/dist/tools/adb-device.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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": {
|
package/dist/types/cache.d.ts
CHANGED
package/dist/types/cache.js
CHANGED