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
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { CACHE_TTLS } from "../types/index.js";
|
|
3
|
+
export const gradleListInputSchema = z.object({
|
|
4
|
+
operation: z.enum(["variants", "modules", "tasks"]),
|
|
5
|
+
module: z.string().optional(),
|
|
6
|
+
});
|
|
7
|
+
export async function handleGradleListTool(input, context) {
|
|
8
|
+
switch (input.operation) {
|
|
9
|
+
case "modules": {
|
|
10
|
+
const modules = await context.gradle.listModules();
|
|
11
|
+
return { modules };
|
|
12
|
+
}
|
|
13
|
+
case "variants": {
|
|
14
|
+
const variants = await context.gradle.listVariants(input.module);
|
|
15
|
+
return { variants, module: input.module || "all" };
|
|
16
|
+
}
|
|
17
|
+
case "tasks": {
|
|
18
|
+
const tasks = await context.gradle.listTasks(input.module);
|
|
19
|
+
// Cache full task list
|
|
20
|
+
const listId = context.cache.generateId("tasks");
|
|
21
|
+
context.cache.set(listId, { tasks }, "tasks", CACHE_TTLS.GRADLE_VARIANTS);
|
|
22
|
+
// Return categorized summary
|
|
23
|
+
const buildTasks = tasks.filter((t) => t.includes("assemble") || t.includes("bundle"));
|
|
24
|
+
const testTasks = tasks.filter((t) => t.includes("test") || t.includes("Test"));
|
|
25
|
+
const cleanTasks = tasks.filter((t) => t.includes("clean"));
|
|
26
|
+
return {
|
|
27
|
+
listId,
|
|
28
|
+
summary: {
|
|
29
|
+
totalTasks: tasks.length,
|
|
30
|
+
buildTasks: buildTasks.slice(0, 10),
|
|
31
|
+
testTasks: testTasks.slice(0, 10),
|
|
32
|
+
cleanTasks: cleanTasks.slice(0, 5),
|
|
33
|
+
},
|
|
34
|
+
module: input.module || "all",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
default:
|
|
38
|
+
throw new Error(`Unknown operation: ${input.operation}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export const gradleListToolDefinition = {
|
|
42
|
+
name: "gradle-list",
|
|
43
|
+
description: "Introspect project structure. Operations: modules, variants, tasks.",
|
|
44
|
+
inputSchema: {
|
|
45
|
+
type: "object",
|
|
46
|
+
properties: {
|
|
47
|
+
operation: {
|
|
48
|
+
type: "string",
|
|
49
|
+
enum: ["variants", "modules", "tasks"],
|
|
50
|
+
},
|
|
51
|
+
module: { type: "string", description: "Module path (for variants/tasks)" },
|
|
52
|
+
},
|
|
53
|
+
required: ["operation"],
|
|
54
|
+
},
|
|
55
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ServerContext } from "../server.js";
|
|
3
|
+
export declare const gradleTestInputSchema: z.ZodObject<{
|
|
4
|
+
operation: z.ZodEnum<{
|
|
5
|
+
unitTest: "unitTest";
|
|
6
|
+
connectedTest: "connectedTest";
|
|
7
|
+
}>;
|
|
8
|
+
module: z.ZodOptional<z.ZodString>;
|
|
9
|
+
filter: z.ZodOptional<z.ZodString>;
|
|
10
|
+
}, z.core.$strip>;
|
|
11
|
+
export type GradleTestInput = z.infer<typeof gradleTestInputSchema>;
|
|
12
|
+
export declare function handleGradleTestTool(input: GradleTestInput, context: ServerContext): Promise<Record<string, unknown>>;
|
|
13
|
+
export declare const gradleTestToolDefinition: {
|
|
14
|
+
name: string;
|
|
15
|
+
description: string;
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: string;
|
|
18
|
+
properties: {
|
|
19
|
+
operation: {
|
|
20
|
+
type: string;
|
|
21
|
+
enum: string[];
|
|
22
|
+
};
|
|
23
|
+
module: {
|
|
24
|
+
type: string;
|
|
25
|
+
description: string;
|
|
26
|
+
};
|
|
27
|
+
filter: {
|
|
28
|
+
type: string;
|
|
29
|
+
description: string;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
required: string[];
|
|
33
|
+
};
|
|
34
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { CACHE_TTLS } from "../types/index.js";
|
|
3
|
+
export const gradleTestInputSchema = z.object({
|
|
4
|
+
operation: z.enum(["unitTest", "connectedTest"]),
|
|
5
|
+
module: z.string().optional(),
|
|
6
|
+
filter: z.string().optional(),
|
|
7
|
+
});
|
|
8
|
+
export async function handleGradleTestTool(input, context) {
|
|
9
|
+
const { result, fullOutput } = await context.gradle.test(input.operation, input.module, input.filter);
|
|
10
|
+
// Cache full output for later retrieval
|
|
11
|
+
const testId = context.cache.generateId("test");
|
|
12
|
+
context.cache.set(testId, { fullOutput, result, operation: input.operation }, "test", CACHE_TTLS.TEST_RESULTS);
|
|
13
|
+
return {
|
|
14
|
+
testId,
|
|
15
|
+
summary: {
|
|
16
|
+
passed: result.passed,
|
|
17
|
+
failed: result.failed,
|
|
18
|
+
skipped: result.skipped,
|
|
19
|
+
total: result.total,
|
|
20
|
+
duration: result.duration,
|
|
21
|
+
},
|
|
22
|
+
failures: result.failures,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export const gradleTestToolDefinition = {
|
|
26
|
+
name: "gradle-test",
|
|
27
|
+
description: "Run tests. Operations: unitTest, connectedTest. Returns summary with testId.",
|
|
28
|
+
inputSchema: {
|
|
29
|
+
type: "object",
|
|
30
|
+
properties: {
|
|
31
|
+
operation: {
|
|
32
|
+
type: "string",
|
|
33
|
+
enum: ["unitTest", "connectedTest"],
|
|
34
|
+
},
|
|
35
|
+
module: { type: "string", description: "Module path" },
|
|
36
|
+
filter: { type: "string", description: "Test filter (e.g., '*LoginTest*')" },
|
|
37
|
+
},
|
|
38
|
+
required: ["operation"],
|
|
39
|
+
},
|
|
40
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from "./cache.js";
|
|
2
|
+
export * from "./rtfm.js";
|
|
3
|
+
export * from "./adb-device.js";
|
|
4
|
+
export * from "./adb-app.js";
|
|
5
|
+
export * from "./adb-logcat.js";
|
|
6
|
+
export * from "./adb-shell.js";
|
|
7
|
+
export * from "./emulator-device.js";
|
|
8
|
+
export * from "./gradle-build.js";
|
|
9
|
+
export * from "./gradle-test.js";
|
|
10
|
+
export * from "./gradle-list.js";
|
|
11
|
+
export * from "./gradle-get-details.js";
|
|
12
|
+
export * from "./ui.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from "./cache.js";
|
|
2
|
+
export * from "./rtfm.js";
|
|
3
|
+
export * from "./adb-device.js";
|
|
4
|
+
export * from "./adb-app.js";
|
|
5
|
+
export * from "./adb-logcat.js";
|
|
6
|
+
export * from "./adb-shell.js";
|
|
7
|
+
export * from "./emulator-device.js";
|
|
8
|
+
export * from "./gradle-build.js";
|
|
9
|
+
export * from "./gradle-test.js";
|
|
10
|
+
export * from "./gradle-list.js";
|
|
11
|
+
export * from "./gradle-get-details.js";
|
|
12
|
+
export * from "./ui.js";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const rtfmInputSchema: z.ZodObject<{
|
|
3
|
+
category: z.ZodOptional<z.ZodString>;
|
|
4
|
+
tool: z.ZodOptional<z.ZodString>;
|
|
5
|
+
}, z.core.$strip>;
|
|
6
|
+
export type RtfmInput = z.infer<typeof rtfmInputSchema>;
|
|
7
|
+
export declare function handleRtfmTool(input: RtfmInput): Promise<{
|
|
8
|
+
content: string;
|
|
9
|
+
}>;
|
|
10
|
+
export declare const rtfmToolDefinition: {
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
inputSchema: {
|
|
14
|
+
type: string;
|
|
15
|
+
properties: {
|
|
16
|
+
category: {
|
|
17
|
+
type: string;
|
|
18
|
+
description: string;
|
|
19
|
+
};
|
|
20
|
+
tool: {
|
|
21
|
+
type: string;
|
|
22
|
+
description: string;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { readFile } from "fs/promises";
|
|
3
|
+
import { join, dirname } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const RTFM_DIR = join(__dirname, "../../docs/rtfm");
|
|
7
|
+
export const rtfmInputSchema = z.object({
|
|
8
|
+
category: z.string().optional(),
|
|
9
|
+
tool: z.string().optional(),
|
|
10
|
+
});
|
|
11
|
+
export async function handleRtfmTool(input) {
|
|
12
|
+
if (!input.category && !input.tool) {
|
|
13
|
+
// Return index
|
|
14
|
+
const content = await readFile(join(RTFM_DIR, "index.md"), "utf-8");
|
|
15
|
+
return { content };
|
|
16
|
+
}
|
|
17
|
+
if (input.category) {
|
|
18
|
+
try {
|
|
19
|
+
const content = await readFile(join(RTFM_DIR, `${input.category}.md`), "utf-8");
|
|
20
|
+
return { content };
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return { content: `Category '${input.category}' not found. Available: build, adb, emulator, ui` };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (input.tool) {
|
|
27
|
+
// Map tool to category
|
|
28
|
+
const toolToCategory = {
|
|
29
|
+
"gradle-build": "build",
|
|
30
|
+
"gradle-test": "build",
|
|
31
|
+
"gradle-list": "build",
|
|
32
|
+
"gradle-get-details": "build",
|
|
33
|
+
"adb-device": "adb",
|
|
34
|
+
"adb-app": "adb",
|
|
35
|
+
"adb-logcat": "adb",
|
|
36
|
+
"adb-shell": "adb",
|
|
37
|
+
"emulator-device": "emulator",
|
|
38
|
+
"ui": "ui",
|
|
39
|
+
"cache": "build",
|
|
40
|
+
"rtfm": "build",
|
|
41
|
+
};
|
|
42
|
+
const category = toolToCategory[input.tool] || "index";
|
|
43
|
+
try {
|
|
44
|
+
const content = await readFile(join(RTFM_DIR, `${category}.md`), "utf-8");
|
|
45
|
+
// Try to extract just the relevant section
|
|
46
|
+
const toolSection = extractToolSection(content, input.tool);
|
|
47
|
+
return { content: toolSection || content };
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return { content: `Tool '${input.tool}' not found.` };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { content: "No documentation found." };
|
|
54
|
+
}
|
|
55
|
+
function extractToolSection(content, toolName) {
|
|
56
|
+
const regex = new RegExp(`## ${toolName}[\\s\\S]*?(?=## |$)`, "i");
|
|
57
|
+
const match = content.match(regex);
|
|
58
|
+
return match ? match[0].trim() : null;
|
|
59
|
+
}
|
|
60
|
+
export const rtfmToolDefinition = {
|
|
61
|
+
name: "rtfm",
|
|
62
|
+
description: "Get documentation. Pass category or tool name.",
|
|
63
|
+
inputSchema: {
|
|
64
|
+
type: "object",
|
|
65
|
+
properties: {
|
|
66
|
+
category: { type: "string", description: "Category: build, adb, emulator, ui" },
|
|
67
|
+
tool: { type: "string", description: "Tool name for specific docs" },
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ServerContext } from "../server.js";
|
|
3
|
+
export declare const uiInputSchema: z.ZodObject<{
|
|
4
|
+
operation: z.ZodEnum<{
|
|
5
|
+
find: "find";
|
|
6
|
+
dump: "dump";
|
|
7
|
+
tap: "tap";
|
|
8
|
+
input: "input";
|
|
9
|
+
screenshot: "screenshot";
|
|
10
|
+
"accessibility-check": "accessibility-check";
|
|
11
|
+
}>;
|
|
12
|
+
selector: z.ZodOptional<z.ZodObject<{
|
|
13
|
+
resourceId: z.ZodOptional<z.ZodString>;
|
|
14
|
+
text: z.ZodOptional<z.ZodString>;
|
|
15
|
+
textContains: z.ZodOptional<z.ZodString>;
|
|
16
|
+
className: z.ZodOptional<z.ZodString>;
|
|
17
|
+
}, z.core.$strip>>;
|
|
18
|
+
x: z.ZodOptional<z.ZodNumber>;
|
|
19
|
+
y: z.ZodOptional<z.ZodNumber>;
|
|
20
|
+
elementIndex: z.ZodOptional<z.ZodNumber>;
|
|
21
|
+
text: z.ZodOptional<z.ZodString>;
|
|
22
|
+
localPath: z.ZodOptional<z.ZodString>;
|
|
23
|
+
}, z.core.$strip>;
|
|
24
|
+
export type UiInput = z.infer<typeof uiInputSchema>;
|
|
25
|
+
export declare function handleUiTool(input: UiInput, context: ServerContext): Promise<Record<string, unknown>>;
|
|
26
|
+
export declare const uiToolDefinition: {
|
|
27
|
+
name: string;
|
|
28
|
+
description: string;
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: string;
|
|
31
|
+
properties: {
|
|
32
|
+
operation: {
|
|
33
|
+
type: string;
|
|
34
|
+
enum: string[];
|
|
35
|
+
};
|
|
36
|
+
selector: {
|
|
37
|
+
type: string;
|
|
38
|
+
properties: {
|
|
39
|
+
resourceId: {
|
|
40
|
+
type: string;
|
|
41
|
+
};
|
|
42
|
+
text: {
|
|
43
|
+
type: string;
|
|
44
|
+
};
|
|
45
|
+
textContains: {
|
|
46
|
+
type: string;
|
|
47
|
+
};
|
|
48
|
+
className: {
|
|
49
|
+
type: string;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
description: string;
|
|
53
|
+
};
|
|
54
|
+
x: {
|
|
55
|
+
type: string;
|
|
56
|
+
description: string;
|
|
57
|
+
};
|
|
58
|
+
y: {
|
|
59
|
+
type: string;
|
|
60
|
+
description: string;
|
|
61
|
+
};
|
|
62
|
+
elementIndex: {
|
|
63
|
+
type: string;
|
|
64
|
+
description: string;
|
|
65
|
+
};
|
|
66
|
+
text: {
|
|
67
|
+
type: string;
|
|
68
|
+
description: string;
|
|
69
|
+
};
|
|
70
|
+
localPath: {
|
|
71
|
+
type: string;
|
|
72
|
+
description: string;
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
required: string[];
|
|
76
|
+
};
|
|
77
|
+
};
|
package/dist/tools/ui.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { CACHE_TTLS } from "../types/index.js";
|
|
3
|
+
export const uiInputSchema = z.object({
|
|
4
|
+
operation: z.enum(["dump", "find", "tap", "input", "screenshot", "accessibility-check"]),
|
|
5
|
+
selector: z.object({
|
|
6
|
+
resourceId: z.string().optional(),
|
|
7
|
+
text: z.string().optional(),
|
|
8
|
+
textContains: z.string().optional(),
|
|
9
|
+
className: z.string().optional(),
|
|
10
|
+
}).optional(),
|
|
11
|
+
x: z.number().optional(),
|
|
12
|
+
y: z.number().optional(),
|
|
13
|
+
elementIndex: z.number().optional(),
|
|
14
|
+
text: z.string().optional(),
|
|
15
|
+
localPath: z.string().optional(),
|
|
16
|
+
});
|
|
17
|
+
// Store last find results for elementIndex reference
|
|
18
|
+
let lastFindResults = [];
|
|
19
|
+
export async function handleUiTool(input, context) {
|
|
20
|
+
const deviceId = context.deviceState.requireCurrentDevice().id;
|
|
21
|
+
switch (input.operation) {
|
|
22
|
+
case "dump": {
|
|
23
|
+
const tree = await context.ui.dump(deviceId);
|
|
24
|
+
// Cache the tree
|
|
25
|
+
const dumpId = context.cache.generateId("ui-dump");
|
|
26
|
+
context.cache.set(dumpId, { tree, deviceId }, "ui-dump", CACHE_TTLS.UI_TREE);
|
|
27
|
+
// Create a simplified view
|
|
28
|
+
const simplifyNode = (node, depth = 0) => ({
|
|
29
|
+
className: node.className.split(".").pop(),
|
|
30
|
+
text: node.text || undefined,
|
|
31
|
+
resourceId: node.resourceId ? node.resourceId.split("/").pop() : undefined,
|
|
32
|
+
bounds: `[${node.bounds.left},${node.bounds.top}][${node.bounds.right},${node.bounds.bottom}]`,
|
|
33
|
+
clickable: node.clickable || undefined,
|
|
34
|
+
children: node.children?.map((c) => simplifyNode(c, depth + 1)),
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
dumpId,
|
|
38
|
+
tree: tree.map((n) => simplifyNode(n)),
|
|
39
|
+
deviceId,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
case "find": {
|
|
43
|
+
if (!input.selector) {
|
|
44
|
+
throw new Error("selector is required for find operation");
|
|
45
|
+
}
|
|
46
|
+
const elements = await context.ui.find(deviceId, input.selector);
|
|
47
|
+
lastFindResults = elements;
|
|
48
|
+
return {
|
|
49
|
+
elements: elements.map((el, index) => ({
|
|
50
|
+
index,
|
|
51
|
+
text: el.text,
|
|
52
|
+
resourceId: el.resourceId,
|
|
53
|
+
className: el.className,
|
|
54
|
+
centerX: el.centerX,
|
|
55
|
+
centerY: el.centerY,
|
|
56
|
+
bounds: el.bounds,
|
|
57
|
+
clickable: el.clickable,
|
|
58
|
+
})),
|
|
59
|
+
count: elements.length,
|
|
60
|
+
deviceId,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
case "tap": {
|
|
64
|
+
let x, y;
|
|
65
|
+
if (input.elementIndex !== undefined) {
|
|
66
|
+
if (!lastFindResults[input.elementIndex]) {
|
|
67
|
+
throw new Error(`Element at index ${input.elementIndex} not found. Run 'find' first.`);
|
|
68
|
+
}
|
|
69
|
+
const element = lastFindResults[input.elementIndex];
|
|
70
|
+
x = element.centerX;
|
|
71
|
+
y = element.centerY;
|
|
72
|
+
}
|
|
73
|
+
else if (input.x !== undefined && input.y !== undefined) {
|
|
74
|
+
x = input.x;
|
|
75
|
+
y = input.y;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
throw new Error("Either x/y coordinates or elementIndex is required for tap");
|
|
79
|
+
}
|
|
80
|
+
await context.ui.tap(deviceId, x, y);
|
|
81
|
+
return { tapped: { x, y }, deviceId };
|
|
82
|
+
}
|
|
83
|
+
case "input": {
|
|
84
|
+
if (!input.text) {
|
|
85
|
+
throw new Error("text is required for input operation");
|
|
86
|
+
}
|
|
87
|
+
await context.ui.input(deviceId, input.text);
|
|
88
|
+
return { input: input.text, deviceId };
|
|
89
|
+
}
|
|
90
|
+
case "screenshot": {
|
|
91
|
+
const localPath = input.localPath || `/tmp/screenshot-${Date.now()}.png`;
|
|
92
|
+
await context.ui.screenshot(deviceId, localPath);
|
|
93
|
+
return { path: localPath, deviceId };
|
|
94
|
+
}
|
|
95
|
+
case "accessibility-check": {
|
|
96
|
+
const result = await context.ui.accessibilityCheck(deviceId);
|
|
97
|
+
return { ...result, deviceId };
|
|
98
|
+
}
|
|
99
|
+
default:
|
|
100
|
+
throw new Error(`Unknown operation: ${input.operation}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export const uiToolDefinition = {
|
|
104
|
+
name: "ui",
|
|
105
|
+
description: "Interact with app UI via accessibility tree. Operations: dump, find, tap, input, screenshot, accessibility-check.",
|
|
106
|
+
inputSchema: {
|
|
107
|
+
type: "object",
|
|
108
|
+
properties: {
|
|
109
|
+
operation: {
|
|
110
|
+
type: "string",
|
|
111
|
+
enum: ["dump", "find", "tap", "input", "screenshot", "accessibility-check"],
|
|
112
|
+
},
|
|
113
|
+
selector: {
|
|
114
|
+
type: "object",
|
|
115
|
+
properties: {
|
|
116
|
+
resourceId: { type: "string" },
|
|
117
|
+
text: { type: "string" },
|
|
118
|
+
textContains: { type: "string" },
|
|
119
|
+
className: { type: "string" },
|
|
120
|
+
},
|
|
121
|
+
description: "Element selector (for find)",
|
|
122
|
+
},
|
|
123
|
+
x: { type: "number", description: "X coordinate (for tap)" },
|
|
124
|
+
y: { type: "number", description: "Y coordinate (for tap)" },
|
|
125
|
+
elementIndex: { type: "number", description: "Element index from last find (for tap)" },
|
|
126
|
+
text: { type: "string", description: "Text to input" },
|
|
127
|
+
localPath: { type: "string", description: "Local path for screenshot" },
|
|
128
|
+
},
|
|
129
|
+
required: ["operation"],
|
|
130
|
+
},
|
|
131
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface CacheEntry<T = unknown> {
|
|
2
|
+
data: T;
|
|
3
|
+
expiresAt: number;
|
|
4
|
+
metadata: {
|
|
5
|
+
createdAt: number;
|
|
6
|
+
type: string;
|
|
7
|
+
sizeBytes?: number;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export interface CacheConfig {
|
|
11
|
+
maxEntries: number;
|
|
12
|
+
maxEntrySizeBytes: number;
|
|
13
|
+
defaultTtlMs: number;
|
|
14
|
+
}
|
|
15
|
+
export declare const DEFAULT_CACHE_CONFIG: CacheConfig;
|
|
16
|
+
export declare const CACHE_TTLS: {
|
|
17
|
+
readonly BUILD_OUTPUT: number;
|
|
18
|
+
readonly TEST_RESULTS: number;
|
|
19
|
+
readonly EMULATOR_LIST: number;
|
|
20
|
+
readonly APP_LIST: number;
|
|
21
|
+
readonly UI_TREE: number;
|
|
22
|
+
readonly GRADLE_VARIANTS: number;
|
|
23
|
+
readonly LOGCAT: number;
|
|
24
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const DEFAULT_CACHE_CONFIG = {
|
|
2
|
+
maxEntries: 100,
|
|
3
|
+
maxEntrySizeBytes: 1024 * 1024, // 1MB
|
|
4
|
+
defaultTtlMs: 5 * 60 * 1000, // 5 minutes
|
|
5
|
+
};
|
|
6
|
+
export const CACHE_TTLS = {
|
|
7
|
+
BUILD_OUTPUT: 30 * 60 * 1000, // 30 min
|
|
8
|
+
TEST_RESULTS: 30 * 60 * 1000, // 30 min
|
|
9
|
+
EMULATOR_LIST: 5 * 60 * 1000, // 5 min
|
|
10
|
+
APP_LIST: 2 * 60 * 1000, // 2 min
|
|
11
|
+
UI_TREE: 30 * 1000, // 30 sec
|
|
12
|
+
GRADLE_VARIANTS: 60 * 60 * 1000, // 1 hour
|
|
13
|
+
LOGCAT: 5 * 60 * 1000, // 5 min
|
|
14
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type DeviceType = "emulator" | "physical";
|
|
2
|
+
export type DeviceStatus = "online" | "offline" | "booting";
|
|
3
|
+
export interface Device {
|
|
4
|
+
id: string;
|
|
5
|
+
type: DeviceType;
|
|
6
|
+
name: string;
|
|
7
|
+
status: DeviceStatus;
|
|
8
|
+
}
|
|
9
|
+
export interface DeviceState {
|
|
10
|
+
currentDevice: Device | null;
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export declare const ErrorCode: {
|
|
2
|
+
readonly NO_DEVICE_SELECTED: "NO_DEVICE_SELECTED";
|
|
3
|
+
readonly DEVICE_NOT_FOUND: "DEVICE_NOT_FOUND";
|
|
4
|
+
readonly DEVICE_OFFLINE: "DEVICE_OFFLINE";
|
|
5
|
+
readonly BUILD_FAILED: "BUILD_FAILED";
|
|
6
|
+
readonly GRADLE_NOT_FOUND: "GRADLE_NOT_FOUND";
|
|
7
|
+
readonly MODULE_NOT_FOUND: "MODULE_NOT_FOUND";
|
|
8
|
+
readonly APK_NOT_FOUND: "APK_NOT_FOUND";
|
|
9
|
+
readonly PACKAGE_NOT_FOUND: "PACKAGE_NOT_FOUND";
|
|
10
|
+
readonly INSTALL_FAILED: "INSTALL_FAILED";
|
|
11
|
+
readonly AVD_NOT_FOUND: "AVD_NOT_FOUND";
|
|
12
|
+
readonly EMULATOR_START_FAILED: "EMULATOR_START_FAILED";
|
|
13
|
+
readonly SNAPSHOT_NOT_FOUND: "SNAPSHOT_NOT_FOUND";
|
|
14
|
+
readonly COMMAND_BLOCKED: "COMMAND_BLOCKED";
|
|
15
|
+
readonly TIMEOUT: "TIMEOUT";
|
|
16
|
+
readonly CACHE_MISS: "CACHE_MISS";
|
|
17
|
+
};
|
|
18
|
+
export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
|
|
19
|
+
export interface ToolError {
|
|
20
|
+
error: ErrorCode;
|
|
21
|
+
message: string;
|
|
22
|
+
suggestion?: string;
|
|
23
|
+
details?: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
export declare class ReplicantError extends Error {
|
|
26
|
+
readonly code: ErrorCode;
|
|
27
|
+
readonly suggestion?: string | undefined;
|
|
28
|
+
readonly details?: Record<string, unknown> | undefined;
|
|
29
|
+
constructor(code: ErrorCode, message: string, suggestion?: string | undefined, details?: Record<string, unknown> | undefined);
|
|
30
|
+
toToolError(): ToolError;
|
|
31
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export const ErrorCode = {
|
|
2
|
+
// Device errors
|
|
3
|
+
NO_DEVICE_SELECTED: "NO_DEVICE_SELECTED",
|
|
4
|
+
DEVICE_NOT_FOUND: "DEVICE_NOT_FOUND",
|
|
5
|
+
DEVICE_OFFLINE: "DEVICE_OFFLINE",
|
|
6
|
+
// Build errors
|
|
7
|
+
BUILD_FAILED: "BUILD_FAILED",
|
|
8
|
+
GRADLE_NOT_FOUND: "GRADLE_NOT_FOUND",
|
|
9
|
+
MODULE_NOT_FOUND: "MODULE_NOT_FOUND",
|
|
10
|
+
// App errors
|
|
11
|
+
APK_NOT_FOUND: "APK_NOT_FOUND",
|
|
12
|
+
PACKAGE_NOT_FOUND: "PACKAGE_NOT_FOUND",
|
|
13
|
+
INSTALL_FAILED: "INSTALL_FAILED",
|
|
14
|
+
// Emulator errors
|
|
15
|
+
AVD_NOT_FOUND: "AVD_NOT_FOUND",
|
|
16
|
+
EMULATOR_START_FAILED: "EMULATOR_START_FAILED",
|
|
17
|
+
SNAPSHOT_NOT_FOUND: "SNAPSHOT_NOT_FOUND",
|
|
18
|
+
// Safety errors
|
|
19
|
+
COMMAND_BLOCKED: "COMMAND_BLOCKED",
|
|
20
|
+
TIMEOUT: "TIMEOUT",
|
|
21
|
+
// Cache errors
|
|
22
|
+
CACHE_MISS: "CACHE_MISS",
|
|
23
|
+
};
|
|
24
|
+
export class ReplicantError extends Error {
|
|
25
|
+
code;
|
|
26
|
+
suggestion;
|
|
27
|
+
details;
|
|
28
|
+
constructor(code, message, suggestion, details) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.code = code;
|
|
31
|
+
this.suggestion = suggestion;
|
|
32
|
+
this.details = details;
|
|
33
|
+
this.name = "ReplicantError";
|
|
34
|
+
}
|
|
35
|
+
toToolError() {
|
|
36
|
+
return {
|
|
37
|
+
error: this.code,
|
|
38
|
+
message: this.message,
|
|
39
|
+
suggestion: this.suggestion,
|
|
40
|
+
details: this.details,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "replicant-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Android MCP server for AI-assisted Android development",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"replicant-mcp": "dist/index.js",
|
|
9
|
+
"replicant": "dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"dev": "tsc --watch",
|
|
14
|
+
"test": "vitest",
|
|
15
|
+
"test:unit": "vitest --run tests/services tests/adapters tests/tools",
|
|
16
|
+
"test:integration": "vitest --run tests/integration",
|
|
17
|
+
"test:coverage": "vitest --run --coverage",
|
|
18
|
+
"check-prereqs": "bash scripts/check-prerequisites.sh",
|
|
19
|
+
"smoke-test": "bash scripts/smoke-test.sh",
|
|
20
|
+
"test:device": "tsx scripts/real-device-test.ts",
|
|
21
|
+
"start": "node dist/index.js",
|
|
22
|
+
"validate": "npm run build && npm run test -- --run",
|
|
23
|
+
"install-skill": "bash scripts/install-skill.sh",
|
|
24
|
+
"prepublishOnly": "npm run build && npm test -- --run"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"mcp",
|
|
28
|
+
"model-context-protocol",
|
|
29
|
+
"android",
|
|
30
|
+
"adb",
|
|
31
|
+
"gradle",
|
|
32
|
+
"emulator",
|
|
33
|
+
"ui-automation",
|
|
34
|
+
"ai",
|
|
35
|
+
"llm"
|
|
36
|
+
],
|
|
37
|
+
"author": "Archit Joshi",
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "https://github.com/thecombatwombat/replicant-mcp.git"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"dist/",
|
|
45
|
+
"README.md",
|
|
46
|
+
"LICENSE"
|
|
47
|
+
],
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=18.0.0"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
53
|
+
"commander": "^14.0.2",
|
|
54
|
+
"execa": "^9.6.1",
|
|
55
|
+
"zod": "^4.3.5"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@types/node": "^25.0.9",
|
|
59
|
+
"@vitest/coverage-v8": "^4.0.17",
|
|
60
|
+
"tsx": "^4.21.0",
|
|
61
|
+
"typescript": "^5.9.3",
|
|
62
|
+
"vitest": "^4.0.17"
|
|
63
|
+
}
|
|
64
|
+
}
|