replicant-mcp 1.0.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.
- package/LICENSE +21 -0
- package/README.md +386 -0
- package/dist/adapters/adb.d.ts +21 -0
- package/dist/adapters/adb.js +75 -0
- package/dist/adapters/emulator.d.ts +19 -0
- package/dist/adapters/emulator.js +72 -0
- package/dist/adapters/gradle.d.ts +20 -0
- package/dist/adapters/gradle.js +80 -0
- package/dist/adapters/index.d.ts +4 -0
- package/dist/adapters/index.js +4 -0
- package/dist/adapters/ui-automator.d.ts +23 -0
- package/dist/adapters/ui-automator.js +53 -0
- package/dist/cli/adb.d.ts +2 -0
- package/dist/cli/adb.js +256 -0
- package/dist/cli/cache.d.ts +2 -0
- package/dist/cli/cache.js +115 -0
- package/dist/cli/emulator.d.ts +2 -0
- package/dist/cli/emulator.js +181 -0
- package/dist/cli/formatter.d.ts +52 -0
- package/dist/cli/formatter.js +68 -0
- package/dist/cli/gradle.d.ts +2 -0
- package/dist/cli/gradle.js +192 -0
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.js +6 -0
- package/dist/cli/ui.d.ts +2 -0
- package/dist/cli/ui.js +218 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +14 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -0
- package/dist/parsers/adb-output.d.ts +4 -0
- package/dist/parsers/adb-output.js +32 -0
- package/dist/parsers/emulator-output.d.ts +9 -0
- package/dist/parsers/emulator-output.js +33 -0
- package/dist/parsers/gradle-output.d.ts +30 -0
- package/dist/parsers/gradle-output.js +80 -0
- package/dist/parsers/index.d.ts +4 -0
- package/dist/parsers/index.js +4 -0
- package/dist/parsers/ui-dump.d.ts +27 -0
- package/dist/parsers/ui-dump.js +142 -0
- package/dist/server.d.ts +15 -0
- package/dist/server.js +113 -0
- package/dist/services/cache-manager.d.ts +22 -0
- package/dist/services/cache-manager.js +90 -0
- package/dist/services/device-state.d.ts +9 -0
- package/dist/services/device-state.js +26 -0
- package/dist/services/index.d.ts +3 -0
- package/dist/services/index.js +3 -0
- package/dist/services/process-runner.d.ts +15 -0
- package/dist/services/process-runner.js +62 -0
- package/dist/tools/adb-app.d.ts +38 -0
- package/dist/tools/adb-app.js +68 -0
- package/dist/tools/adb-device.d.ts +31 -0
- package/dist/tools/adb-device.js +71 -0
- package/dist/tools/adb-logcat.d.ts +54 -0
- package/dist/tools/adb-logcat.js +70 -0
- package/dist/tools/adb-shell.d.ts +26 -0
- package/dist/tools/adb-shell.js +27 -0
- package/dist/tools/cache.d.ts +50 -0
- package/dist/tools/cache.js +57 -0
- package/dist/tools/emulator-device.d.ts +56 -0
- package/dist/tools/emulator-device.js +132 -0
- package/dist/tools/gradle-build.d.ts +35 -0
- package/dist/tools/gradle-build.js +40 -0
- package/dist/tools/gradle-get-details.d.ts +32 -0
- package/dist/tools/gradle-get-details.js +72 -0
- package/dist/tools/gradle-list.d.ts +30 -0
- package/dist/tools/gradle-list.js +55 -0
- package/dist/tools/gradle-test.d.ts +34 -0
- package/dist/tools/gradle-test.js +40 -0
- package/dist/tools/index.d.ts +12 -0
- package/dist/tools/index.js +12 -0
- package/dist/tools/rtfm.d.ts +26 -0
- package/dist/tools/rtfm.js +70 -0
- package/dist/tools/ui.d.ts +77 -0
- package/dist/tools/ui.js +131 -0
- package/dist/types/cache.d.ts +24 -0
- package/dist/types/cache.js +14 -0
- package/dist/types/device.d.ts +11 -0
- package/dist/types/device.js +1 -0
- package/dist/types/errors.d.ts +31 -0
- package/dist/types/errors.js +43 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.js +3 -0
- package/package.json +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Archit Joshi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
# replicant-mcp
|
|
2
|
+
|
|
3
|
+
**Let AI build, test, and debug your Android apps.**
|
|
4
|
+
|
|
5
|
+
[](https://github.com/thecombatwombat/replicant-mcp/actions/workflows/ci.yml)
|
|
6
|
+
[](https://nodejs.org/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
replicant-mcp is a [Model Context Protocol](https://modelcontextprotocol.io/) server that gives AI assistants like Claude the ability to interact with your Android development environment. Build APKs, launch emulators, install apps, navigate UIs, and debug crashes—all through natural conversation.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Why replicant-mcp?
|
|
14
|
+
|
|
15
|
+
Android development involves juggling a lot: Gradle builds, emulator management, ADB commands, logcat filtering, UI testing. Each has its own CLI, flags, and quirks.
|
|
16
|
+
|
|
17
|
+
replicant-mcp wraps all of this into a clean interface that AI can understand and use effectively:
|
|
18
|
+
|
|
19
|
+
| Without replicant-mcp | With replicant-mcp |
|
|
20
|
+
|-----------------------|-------------------|
|
|
21
|
+
| "Run `./gradlew assembleDebug`, then `adb install`, then `adb shell am start`..." | "Build and run the app" |
|
|
22
|
+
| Copy-paste logcat output, lose context | AI reads filtered logs directly |
|
|
23
|
+
| Screenshot → describe UI → guess coordinates | AI sees accessibility tree, taps elements by text |
|
|
24
|
+
| 5,000 tokens of raw Gradle output | 50-token summary + details on demand |
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
### Prerequisites
|
|
31
|
+
|
|
32
|
+
You'll need:
|
|
33
|
+
- **Node.js 18+**
|
|
34
|
+
- **Android SDK** with `adb` and `emulator` in your PATH
|
|
35
|
+
- An Android project with `gradlew` (for build tools)
|
|
36
|
+
|
|
37
|
+
Verify your setup:
|
|
38
|
+
```bash
|
|
39
|
+
node --version # Should be 18+
|
|
40
|
+
adb --version # Should show Android Debug Bridge version
|
|
41
|
+
emulator -version # Should show Android emulator version
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Installation
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Clone the repo
|
|
48
|
+
git clone https://github.com/thecombatwombat/replicant-mcp.git
|
|
49
|
+
cd replicant-mcp
|
|
50
|
+
|
|
51
|
+
# Install dependencies
|
|
52
|
+
npm install
|
|
53
|
+
|
|
54
|
+
# Build
|
|
55
|
+
npm run build
|
|
56
|
+
|
|
57
|
+
# Verify everything works
|
|
58
|
+
npm test
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Connect to Claude Desktop
|
|
62
|
+
|
|
63
|
+
Add this to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"mcpServers": {
|
|
68
|
+
"replicant": {
|
|
69
|
+
"command": "node",
|
|
70
|
+
"args": ["/absolute/path/to/replicant-mcp/dist/index.js"]
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Restart Claude Desktop. You should see "replicant" in the MCP servers list.
|
|
77
|
+
|
|
78
|
+
### Alternative: Claude Code Skill
|
|
79
|
+
|
|
80
|
+
If you use [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (Anthropic's CLI), you can install replicant as a skill instead of an MCP server. This provides shell script wrappers optimized for Claude Code's workflow.
|
|
81
|
+
|
|
82
|
+
**Option 1: Via Plugin Marketplace (Recommended)**
|
|
83
|
+
```bash
|
|
84
|
+
/plugin marketplace add thecombatwombat/replicant-mcp
|
|
85
|
+
/plugin install replicant-dev@replicant-mcp
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Option 2: Manual Installation**
|
|
89
|
+
```bash
|
|
90
|
+
# From the replicant-mcp directory
|
|
91
|
+
npm run install-skill
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
After installation, restart Claude Code to load the skill. The skill provides the same Android development capabilities through shell scripts rather than MCP tools.
|
|
95
|
+
|
|
96
|
+
> **Note:** The Claude Code skill is currently compatible with macOS and Linux only. Windows support is planned for a future release.
|
|
97
|
+
|
|
98
|
+
**Which should you choose?**
|
|
99
|
+
- **MCP Server** — Use with Claude Desktop or any MCP-compatible client
|
|
100
|
+
- **Claude Code Skill** — Use with Claude Code CLI for a shell-native experience
|
|
101
|
+
|
|
102
|
+
Both approaches provide the same core functionality.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## What Can It Do?
|
|
107
|
+
|
|
108
|
+
### Build & Test
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
You: "Build the debug variant of the app"
|
|
112
|
+
|
|
113
|
+
Claude: I'll build the debug APK.
|
|
114
|
+
[Calls gradle-build with operation: "assembleDebug"]
|
|
115
|
+
|
|
116
|
+
Build successful in 34s
|
|
117
|
+
APK: app/build/outputs/apk/debug/app-debug.apk (12.4 MB)
|
|
118
|
+
2 warnings (use gradle-get-details for full output)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
You: "Run the unit tests for the login module"
|
|
123
|
+
|
|
124
|
+
Claude: Running unit tests for :feature:login...
|
|
125
|
+
[Calls gradle-test with operation: "unitTest", module: ":feature:login"]
|
|
126
|
+
|
|
127
|
+
47 tests passed, 2 failed, 0 skipped
|
|
128
|
+
|
|
129
|
+
Failed:
|
|
130
|
+
- LoginViewModelTest.testInvalidEmail
|
|
131
|
+
- LoginRepositoryTest.testNetworkError
|
|
132
|
+
|
|
133
|
+
Want me to show the failure details?
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Emulator Management
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
You: "Start up a Pixel 7 emulator"
|
|
140
|
+
|
|
141
|
+
Claude: I'll start the Pixel_7_API_34 emulator.
|
|
142
|
+
[Calls emulator-device with operation: "start", avdName: "Pixel_7_API_34"]
|
|
143
|
+
|
|
144
|
+
Emulator started: emulator-5554
|
|
145
|
+
Waiting for boot... ready!
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Install & Launch Apps
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
You: "Install the APK we just built and open the app"
|
|
152
|
+
|
|
153
|
+
Claude: Installing and launching...
|
|
154
|
+
[Calls adb-app with operation: "install"]
|
|
155
|
+
[Calls adb-app with operation: "launch"]
|
|
156
|
+
|
|
157
|
+
App installed and running on emulator-5554
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Debug with Logcat
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
You: "The app crashed. What happened?"
|
|
164
|
+
|
|
165
|
+
Claude: Let me check the error logs.
|
|
166
|
+
[Calls adb-logcat with level: "error", lines: 50]
|
|
167
|
+
|
|
168
|
+
Found the crash:
|
|
169
|
+
|
|
170
|
+
java.lang.NullPointerException: Attempt to invoke virtual method
|
|
171
|
+
'void com.example.User.getName()' on a null object reference
|
|
172
|
+
at com.example.ProfileActivity.onCreate(ProfileActivity.kt:47)
|
|
173
|
+
|
|
174
|
+
Looks like `user` is null when ProfileActivity starts.
|
|
175
|
+
Want me to look at that file?
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### UI Automation (No Screenshots Needed!)
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
You: "Tap the Login button"
|
|
182
|
+
|
|
183
|
+
Claude: I'll find and tap the Login button.
|
|
184
|
+
[Calls ui with operation: "find", selector: { text: "Login" }]
|
|
185
|
+
[Calls ui with operation: "tap", elementIndex: 0]
|
|
186
|
+
|
|
187
|
+
Tapped "Login" at coordinates (540, 1847)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
This works by reading the **accessibility tree**—the same data screen readers use. It's faster, cheaper, and more reliable than screenshot-based approaches.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Tool Reference
|
|
195
|
+
|
|
196
|
+
replicant-mcp provides 12 tools organized into categories:
|
|
197
|
+
|
|
198
|
+
### Build & Test
|
|
199
|
+
| Tool | Description |
|
|
200
|
+
|------|-------------|
|
|
201
|
+
| `gradle-build` | Build APKs and bundles (`assembleDebug`, `assembleRelease`, `bundle`) |
|
|
202
|
+
| `gradle-test` | Run unit and instrumented tests with filtering |
|
|
203
|
+
| `gradle-list` | List modules, build variants, and tasks |
|
|
204
|
+
| `gradle-get-details` | Fetch full logs/errors from cached build results |
|
|
205
|
+
|
|
206
|
+
### Emulator
|
|
207
|
+
| Tool | Description |
|
|
208
|
+
|------|-------------|
|
|
209
|
+
| `emulator-device` | Create, start, stop emulators; manage snapshots |
|
|
210
|
+
|
|
211
|
+
### ADB
|
|
212
|
+
| Tool | Description |
|
|
213
|
+
|------|-------------|
|
|
214
|
+
| `adb-device` | List devices, select active device, get properties |
|
|
215
|
+
| `adb-app` | Install, uninstall, launch, stop apps; clear data |
|
|
216
|
+
| `adb-logcat` | Read filtered device logs by package/tag/level |
|
|
217
|
+
| `adb-shell` | Run shell commands (with safety guards) |
|
|
218
|
+
|
|
219
|
+
### UI Automation
|
|
220
|
+
| Tool | Description |
|
|
221
|
+
|------|-------------|
|
|
222
|
+
| `ui` | Dump accessibility tree, find elements, tap, input text, screenshot |
|
|
223
|
+
|
|
224
|
+
### Utilities
|
|
225
|
+
| Tool | Description |
|
|
226
|
+
|------|-------------|
|
|
227
|
+
| `cache` | Manage cached outputs (stats, clear, config) |
|
|
228
|
+
| `rtfm` | On-demand documentation for tools |
|
|
229
|
+
|
|
230
|
+
**Want details?** Ask Claude to call `rtfm` with a category like "build", "adb", "emulator", or "ui".
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Design Philosophy
|
|
235
|
+
|
|
236
|
+
### Progressive Disclosure
|
|
237
|
+
|
|
238
|
+
Gradle builds can produce thousands of lines of output. Dumping all of that into an AI context is wasteful and confusing.
|
|
239
|
+
|
|
240
|
+
Instead, replicant-mcp returns **summaries with cache IDs**:
|
|
241
|
+
|
|
242
|
+
```json
|
|
243
|
+
{
|
|
244
|
+
"buildId": "build-a1b2c3-1705789200",
|
|
245
|
+
"summary": {
|
|
246
|
+
"success": true,
|
|
247
|
+
"duration": "34s",
|
|
248
|
+
"apkSize": "12.4 MB",
|
|
249
|
+
"warnings": 2
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
If the AI needs the full output (e.g., to debug a failure), it can request it:
|
|
255
|
+
|
|
256
|
+
```json
|
|
257
|
+
{ "tool": "gradle-get-details", "id": "build-a1b2c3-1705789200", "detailType": "errors" }
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
This typically reduces token usage by **90-99%**.
|
|
261
|
+
|
|
262
|
+
### Accessibility-First UI
|
|
263
|
+
|
|
264
|
+
Most AI-driven UI automation uses screenshots: capture the screen, send it to a vision model, get coordinates, click.
|
|
265
|
+
|
|
266
|
+
replicant-mcp takes a different approach: it reads the **accessibility tree**—the same structured data that powers screen readers. This is:
|
|
267
|
+
|
|
268
|
+
- **Faster** — No image processing
|
|
269
|
+
- **Cheaper** — Text is smaller than images
|
|
270
|
+
- **More reliable** — Elements are identified by text/ID, not pixel coordinates
|
|
271
|
+
- **Better for apps** — Encourages accessible app development
|
|
272
|
+
|
|
273
|
+
### Single Device Focus
|
|
274
|
+
|
|
275
|
+
Instead of passing `deviceId` to every command, you select a device once:
|
|
276
|
+
|
|
277
|
+
```json
|
|
278
|
+
{ "tool": "adb-device", "operation": "select", "deviceId": "emulator-5554" }
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
All subsequent commands target that device automatically. Simple.
|
|
282
|
+
|
|
283
|
+
### Safety Guards
|
|
284
|
+
|
|
285
|
+
The `adb-shell` tool blocks dangerous commands like `rm -rf /`, `reboot`, and `su`. You can run shell commands, but not brick your device.
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Development
|
|
290
|
+
|
|
291
|
+
### Project Structure
|
|
292
|
+
|
|
293
|
+
```
|
|
294
|
+
src/
|
|
295
|
+
index.ts # Entry point
|
|
296
|
+
server.ts # MCP server setup
|
|
297
|
+
tools/ # Tool implementations (one file per tool)
|
|
298
|
+
adapters/ # CLI wrappers (adb, emulator, gradle)
|
|
299
|
+
services/ # Core services (cache, device state, process runner)
|
|
300
|
+
parsers/ # Output parsers
|
|
301
|
+
types/ # TypeScript types
|
|
302
|
+
docs/rtfm/ # On-demand documentation
|
|
303
|
+
tests/ # Unit and integration tests
|
|
304
|
+
scripts/ # Utility scripts
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Running Tests
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
# All tests
|
|
311
|
+
npm test
|
|
312
|
+
|
|
313
|
+
# Unit tests only
|
|
314
|
+
npm run test:unit
|
|
315
|
+
|
|
316
|
+
# Integration tests (MCP protocol compliance)
|
|
317
|
+
npm run test:integration
|
|
318
|
+
|
|
319
|
+
# With coverage
|
|
320
|
+
npm run test:coverage
|
|
321
|
+
|
|
322
|
+
# Full validation (build + all tests)
|
|
323
|
+
npm run validate
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Checking Prerequisites
|
|
327
|
+
|
|
328
|
+
```bash
|
|
329
|
+
npm run check-prereqs
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
This verifies your Android SDK setup and reports what's available.
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## Troubleshooting
|
|
337
|
+
|
|
338
|
+
### "No device selected"
|
|
339
|
+
|
|
340
|
+
Run `adb-device` with `operation: "list"` to see available devices, then `operation: "select"` to choose one. If only one device is connected, it's auto-selected.
|
|
341
|
+
|
|
342
|
+
### "Gradle wrapper not found"
|
|
343
|
+
|
|
344
|
+
Make sure you're in an Android project directory that contains `gradlew`. The Gradle tools won't work from other locations.
|
|
345
|
+
|
|
346
|
+
### "Command timed out"
|
|
347
|
+
|
|
348
|
+
Long-running operations (builds, tests) have a 5-minute default timeout. If your builds are slower, you may need to adjust the timeout in the adapter.
|
|
349
|
+
|
|
350
|
+
### Emulator won't start
|
|
351
|
+
|
|
352
|
+
Check that:
|
|
353
|
+
1. You have an AVD created (`avdmanager list avd`)
|
|
354
|
+
2. Virtualization is enabled (KVM on Linux, HAXM on Mac/Windows)
|
|
355
|
+
3. Enough disk space for the emulator
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## Contributing
|
|
360
|
+
|
|
361
|
+
Contributions are welcome! Please:
|
|
362
|
+
|
|
363
|
+
1. Fork the repo
|
|
364
|
+
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
365
|
+
3. Make your changes
|
|
366
|
+
4. Run `npm run validate` to ensure tests pass
|
|
367
|
+
5. Commit with a descriptive message
|
|
368
|
+
6. Push and open a PR
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## Acknowledgments
|
|
373
|
+
|
|
374
|
+
- Inspired by [xc-mcp](https://github.com/conorluddy/xc-mcp) for iOS
|
|
375
|
+
- Built on the [Model Context Protocol](https://modelcontextprotocol.io/)
|
|
376
|
+
- Thanks to the Android team for `adb` and the emulator
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
## License
|
|
381
|
+
|
|
382
|
+
[MIT](LICENSE)
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
**Questions? Issues? Ideas?** [Open an issue](https://github.com/thecombatwombat/replicant-mcp/issues) — we'd love to hear from you.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ProcessRunner, RunResult } from "../services/index.js";
|
|
2
|
+
import { Device } from "../types/index.js";
|
|
3
|
+
export declare class AdbAdapter {
|
|
4
|
+
private runner;
|
|
5
|
+
constructor(runner?: ProcessRunner);
|
|
6
|
+
getDevices(): Promise<Device[]>;
|
|
7
|
+
getPackages(deviceId: string): Promise<string[]>;
|
|
8
|
+
install(deviceId: string, apkPath: string): Promise<void>;
|
|
9
|
+
uninstall(deviceId: string, packageName: string): Promise<void>;
|
|
10
|
+
launch(deviceId: string, packageName: string): Promise<void>;
|
|
11
|
+
stop(deviceId: string, packageName: string): Promise<void>;
|
|
12
|
+
clearData(deviceId: string, packageName: string): Promise<void>;
|
|
13
|
+
shell(deviceId: string, command: string, timeoutMs?: number): Promise<RunResult>;
|
|
14
|
+
logcat(deviceId: string, options: {
|
|
15
|
+
lines?: number;
|
|
16
|
+
filter?: string;
|
|
17
|
+
}): Promise<string>;
|
|
18
|
+
waitForDevice(deviceId: string, timeoutMs?: number): Promise<void>;
|
|
19
|
+
getProperties(deviceId: string): Promise<Record<string, string>>;
|
|
20
|
+
private adb;
|
|
21
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { ProcessRunner } from "../services/index.js";
|
|
2
|
+
import { ReplicantError, ErrorCode } from "../types/index.js";
|
|
3
|
+
import { parseDeviceList, parsePackageList } from "../parsers/adb-output.js";
|
|
4
|
+
export class AdbAdapter {
|
|
5
|
+
runner;
|
|
6
|
+
constructor(runner = new ProcessRunner()) {
|
|
7
|
+
this.runner = runner;
|
|
8
|
+
}
|
|
9
|
+
async getDevices() {
|
|
10
|
+
const result = await this.adb(["devices"]);
|
|
11
|
+
return parseDeviceList(result.stdout);
|
|
12
|
+
}
|
|
13
|
+
async getPackages(deviceId) {
|
|
14
|
+
const result = await this.adb(["-s", deviceId, "shell", "pm", "list", "packages"]);
|
|
15
|
+
return parsePackageList(result.stdout);
|
|
16
|
+
}
|
|
17
|
+
async install(deviceId, apkPath) {
|
|
18
|
+
const result = await this.adb(["-s", deviceId, "install", "-r", apkPath]);
|
|
19
|
+
if (result.exitCode !== 0 || result.stdout.includes("Failure")) {
|
|
20
|
+
throw new ReplicantError(ErrorCode.INSTALL_FAILED, `Failed to install APK: ${result.stdout}`, "Check the APK path and device state");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async uninstall(deviceId, packageName) {
|
|
24
|
+
const result = await this.adb(["-s", deviceId, "uninstall", packageName]);
|
|
25
|
+
if (result.exitCode !== 0) {
|
|
26
|
+
throw new ReplicantError(ErrorCode.PACKAGE_NOT_FOUND, `Failed to uninstall ${packageName}`, "Check the package name");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async launch(deviceId, packageName) {
|
|
30
|
+
// Get the main activity using dumpsys
|
|
31
|
+
const result = await this.adb([
|
|
32
|
+
"-s", deviceId, "shell", "monkey",
|
|
33
|
+
"-p", packageName, "-c", "android.intent.category.LAUNCHER", "1"
|
|
34
|
+
]);
|
|
35
|
+
if (result.exitCode !== 0) {
|
|
36
|
+
throw new ReplicantError(ErrorCode.PACKAGE_NOT_FOUND, `Failed to launch ${packageName}`, "Check the package name and ensure the app is installed");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async stop(deviceId, packageName) {
|
|
40
|
+
await this.adb(["-s", deviceId, "shell", "am", "force-stop", packageName]);
|
|
41
|
+
}
|
|
42
|
+
async clearData(deviceId, packageName) {
|
|
43
|
+
await this.adb(["-s", deviceId, "shell", "pm", "clear", packageName]);
|
|
44
|
+
}
|
|
45
|
+
async shell(deviceId, command, timeoutMs) {
|
|
46
|
+
return this.adb(["-s", deviceId, "shell", command], timeoutMs);
|
|
47
|
+
}
|
|
48
|
+
async logcat(deviceId, options) {
|
|
49
|
+
const args = ["-s", deviceId, "logcat", "-d"];
|
|
50
|
+
if (options.lines) {
|
|
51
|
+
args.push("-t", options.lines.toString());
|
|
52
|
+
}
|
|
53
|
+
if (options.filter) {
|
|
54
|
+
args.push(...options.filter.split(" "));
|
|
55
|
+
}
|
|
56
|
+
const result = await this.adb(args);
|
|
57
|
+
return result.stdout;
|
|
58
|
+
}
|
|
59
|
+
async waitForDevice(deviceId, timeoutMs = 30000) {
|
|
60
|
+
await this.adb(["-s", deviceId, "wait-for-device"], timeoutMs);
|
|
61
|
+
}
|
|
62
|
+
async getProperties(deviceId) {
|
|
63
|
+
const result = await this.adb(["-s", deviceId, "shell", "getprop"]);
|
|
64
|
+
const props = {};
|
|
65
|
+
const regex = /\[([^\]]+)\]:\s*\[([^\]]*)\]/g;
|
|
66
|
+
let match;
|
|
67
|
+
while ((match = regex.exec(result.stdout)) !== null) {
|
|
68
|
+
props[match[1]] = match[2];
|
|
69
|
+
}
|
|
70
|
+
return props;
|
|
71
|
+
}
|
|
72
|
+
async adb(args, timeoutMs) {
|
|
73
|
+
return this.runner.run("adb", args, { timeoutMs });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ProcessRunner } from "../services/index.js";
|
|
2
|
+
import { AvdInfo } from "../parsers/emulator-output.js";
|
|
3
|
+
export interface EmulatorListResult {
|
|
4
|
+
available: AvdInfo[];
|
|
5
|
+
running: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare class EmulatorAdapter {
|
|
8
|
+
private runner;
|
|
9
|
+
constructor(runner?: ProcessRunner);
|
|
10
|
+
list(): Promise<EmulatorListResult>;
|
|
11
|
+
create(name: string, device: string, systemImage: string): Promise<void>;
|
|
12
|
+
start(avdName: string): Promise<string>;
|
|
13
|
+
kill(emulatorId: string): Promise<void>;
|
|
14
|
+
wipe(avdName: string): Promise<void>;
|
|
15
|
+
snapshotSave(emulatorId: string, name: string): Promise<void>;
|
|
16
|
+
snapshotLoad(emulatorId: string, name: string): Promise<void>;
|
|
17
|
+
snapshotList(emulatorId: string): Promise<string[]>;
|
|
18
|
+
snapshotDelete(emulatorId: string, name: string): Promise<void>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { ProcessRunner } from "../services/index.js";
|
|
2
|
+
import { ReplicantError, ErrorCode } from "../types/index.js";
|
|
3
|
+
import { parseAvdList, parseEmulatorList, parseSnapshotList } from "../parsers/emulator-output.js";
|
|
4
|
+
export class EmulatorAdapter {
|
|
5
|
+
runner;
|
|
6
|
+
constructor(runner = new ProcessRunner()) {
|
|
7
|
+
this.runner = runner;
|
|
8
|
+
}
|
|
9
|
+
async list() {
|
|
10
|
+
const [avdResult, runningResult] = await Promise.all([
|
|
11
|
+
this.runner.run("avdmanager", ["list", "avd"]),
|
|
12
|
+
this.runner.run("emulator", ["-list-avds"]),
|
|
13
|
+
]);
|
|
14
|
+
return {
|
|
15
|
+
available: parseAvdList(avdResult.stdout),
|
|
16
|
+
running: parseEmulatorList(runningResult.stdout),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
async create(name, device, systemImage) {
|
|
20
|
+
const result = await this.runner.run("avdmanager", [
|
|
21
|
+
"create", "avd",
|
|
22
|
+
"-n", name,
|
|
23
|
+
"-k", systemImage,
|
|
24
|
+
"-d", device,
|
|
25
|
+
"--force",
|
|
26
|
+
], { timeoutMs: 60000 });
|
|
27
|
+
if (result.exitCode !== 0) {
|
|
28
|
+
throw new ReplicantError(ErrorCode.EMULATOR_START_FAILED, `Failed to create AVD: ${result.stderr}`, "Check device and system image names");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async start(avdName) {
|
|
32
|
+
// Start emulator in background - don't wait for it
|
|
33
|
+
// Returns immediately, emulator boots in background
|
|
34
|
+
this.runner.run("emulator", [
|
|
35
|
+
"-avd", avdName,
|
|
36
|
+
"-no-snapshot-load",
|
|
37
|
+
"-no-boot-anim",
|
|
38
|
+
], { timeoutMs: 5000 }).catch(() => {
|
|
39
|
+
// Expected to "timeout" as emulator runs forever
|
|
40
|
+
});
|
|
41
|
+
// Give it a moment to register
|
|
42
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
43
|
+
// Find the new emulator ID
|
|
44
|
+
const result = await this.runner.run("adb", ["devices"]);
|
|
45
|
+
const match = result.stdout.match(/emulator-\d+/);
|
|
46
|
+
if (!match) {
|
|
47
|
+
throw new ReplicantError(ErrorCode.EMULATOR_START_FAILED, `Emulator ${avdName} failed to start`, "Check the AVD name and try again");
|
|
48
|
+
}
|
|
49
|
+
return match[0];
|
|
50
|
+
}
|
|
51
|
+
async kill(emulatorId) {
|
|
52
|
+
await this.runner.run("adb", ["-s", emulatorId, "emu", "kill"]);
|
|
53
|
+
}
|
|
54
|
+
async wipe(avdName) {
|
|
55
|
+
await this.runner.run("emulator", ["-avd", avdName, "-wipe-data", "-no-window"], { timeoutMs: 5000 }).catch(() => {
|
|
56
|
+
// Expected behavior
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
async snapshotSave(emulatorId, name) {
|
|
60
|
+
await this.runner.run("adb", ["-s", emulatorId, "emu", "avd", "snapshot", "save", name]);
|
|
61
|
+
}
|
|
62
|
+
async snapshotLoad(emulatorId, name) {
|
|
63
|
+
await this.runner.run("adb", ["-s", emulatorId, "emu", "avd", "snapshot", "load", name]);
|
|
64
|
+
}
|
|
65
|
+
async snapshotList(emulatorId) {
|
|
66
|
+
const result = await this.runner.run("adb", ["-s", emulatorId, "emu", "avd", "snapshot", "list"]);
|
|
67
|
+
return parseSnapshotList(result.stdout);
|
|
68
|
+
}
|
|
69
|
+
async snapshotDelete(emulatorId, name) {
|
|
70
|
+
await this.runner.run("adb", ["-s", emulatorId, "emu", "avd", "snapshot", "delete", name]);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ProcessRunner } from "../services/index.js";
|
|
2
|
+
import { BuildResult, TestResult, VariantInfo } from "../parsers/gradle-output.js";
|
|
3
|
+
export declare class GradleAdapter {
|
|
4
|
+
private runner;
|
|
5
|
+
private projectPath?;
|
|
6
|
+
constructor(runner?: ProcessRunner, projectPath?: string | undefined);
|
|
7
|
+
build(operation: "assembleDebug" | "assembleRelease" | "bundle", module?: string, flavor?: string): Promise<{
|
|
8
|
+
result: BuildResult;
|
|
9
|
+
fullOutput: string;
|
|
10
|
+
}>;
|
|
11
|
+
test(operation: "unitTest" | "connectedTest", module?: string, filter?: string): Promise<{
|
|
12
|
+
result: TestResult;
|
|
13
|
+
fullOutput: string;
|
|
14
|
+
}>;
|
|
15
|
+
listModules(): Promise<string[]>;
|
|
16
|
+
listVariants(module?: string): Promise<VariantInfo[]>;
|
|
17
|
+
listTasks(module?: string): Promise<string[]>;
|
|
18
|
+
clean(stopDaemons?: boolean): Promise<void>;
|
|
19
|
+
private gradle;
|
|
20
|
+
}
|