replicant-mcp 1.0.0 → 1.1.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/README.md +138 -25
- package/dist/adapters/adb.d.ts +1 -0
- package/dist/adapters/adb.js +7 -1
- package/dist/adapters/emulator.js +11 -11
- package/dist/adapters/ui-automator.d.ts +41 -1
- package/dist/adapters/ui-automator.js +256 -8
- package/dist/cli/gradle.js +3 -3
- package/dist/cli.js +1 -1
- package/dist/server.d.ts +3 -1
- package/dist/server.js +23 -3
- package/dist/services/config.d.ts +16 -0
- package/dist/services/config.js +62 -0
- package/dist/services/device-state.d.ts +2 -0
- package/dist/services/device-state.js +18 -0
- package/dist/services/environment.d.ts +18 -0
- package/dist/services/environment.js +130 -0
- package/dist/services/grid.d.ts +28 -0
- package/dist/services/grid.js +98 -0
- package/dist/services/icon-patterns.d.ts +10 -0
- package/dist/services/icon-patterns.js +51 -0
- package/dist/services/index.d.ts +6 -0
- package/dist/services/index.js +6 -0
- package/dist/services/ocr.d.ts +4 -0
- package/dist/services/ocr.js +59 -0
- package/dist/services/process-runner.d.ts +6 -0
- package/dist/services/process-runner.js +26 -0
- package/dist/services/visual-candidates.d.ts +24 -0
- package/dist/services/visual-candidates.js +78 -0
- package/dist/tools/adb-app.js +3 -2
- package/dist/tools/adb-device.d.ts +1 -0
- package/dist/tools/adb-device.js +47 -8
- package/dist/tools/adb-logcat.js +3 -2
- package/dist/tools/adb-shell.js +3 -2
- package/dist/tools/emulator-device.d.ts +1 -1
- package/dist/tools/gradle-get-details.d.ts +1 -1
- package/dist/tools/ui.d.ts +32 -1
- package/dist/tools/ui.js +253 -12
- package/dist/types/config.d.ts +34 -0
- package/dist/types/config.js +11 -0
- package/dist/types/errors.d.ts +25 -2
- package/dist/types/errors.js +23 -4
- package/dist/types/icon-recognition.d.ts +50 -0
- package/dist/types/icon-recognition.js +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.js +3 -0
- package/dist/types/ocr.d.ts +21 -0
- package/dist/types/ocr.js +1 -0
- package/package.json +5 -2
|
@@ -8,8 +8,12 @@ const BLOCKED_PATTERNS = [
|
|
|
8
8
|
/\bformat\b/, // format commands
|
|
9
9
|
];
|
|
10
10
|
export class ProcessRunner {
|
|
11
|
+
environment;
|
|
11
12
|
defaultTimeoutMs = 30_000;
|
|
12
13
|
maxTimeoutMs = 120_000;
|
|
14
|
+
constructor(environment) {
|
|
15
|
+
this.environment = environment;
|
|
16
|
+
}
|
|
13
17
|
async run(command, args, options = {}) {
|
|
14
18
|
this.validateCommand(command, args);
|
|
15
19
|
const timeoutMs = Math.min(options.timeoutMs ?? this.defaultTimeoutMs, this.maxTimeoutMs);
|
|
@@ -48,6 +52,28 @@ export class ProcessRunner {
|
|
|
48
52
|
throw error;
|
|
49
53
|
}
|
|
50
54
|
}
|
|
55
|
+
async runAdb(args, options = {}) {
|
|
56
|
+
if (!this.environment) {
|
|
57
|
+
// Fallback to bare "adb" if no environment service
|
|
58
|
+
return this.run("adb", args, options);
|
|
59
|
+
}
|
|
60
|
+
const adbPath = await this.environment.getAdbPath();
|
|
61
|
+
return this.run(adbPath, args, options);
|
|
62
|
+
}
|
|
63
|
+
async runEmulator(args, options = {}) {
|
|
64
|
+
if (!this.environment) {
|
|
65
|
+
return this.run("emulator", args, options);
|
|
66
|
+
}
|
|
67
|
+
const emulatorPath = await this.environment.getEmulatorPath();
|
|
68
|
+
return this.run(emulatorPath, args, options);
|
|
69
|
+
}
|
|
70
|
+
async runAvdManager(args, options = {}) {
|
|
71
|
+
if (!this.environment) {
|
|
72
|
+
return this.run("avdmanager", args, options);
|
|
73
|
+
}
|
|
74
|
+
const avdManagerPath = await this.environment.getAvdManagerPath();
|
|
75
|
+
return this.run(avdManagerPath, args, options);
|
|
76
|
+
}
|
|
51
77
|
validateCommand(command, args) {
|
|
52
78
|
if (BLOCKED_COMMANDS.has(command)) {
|
|
53
79
|
throw new ReplicantError(ErrorCode.COMMAND_BLOCKED, `Command '${command}' is not allowed`, "Use safe commands only");
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { AccessibilityNode, Bounds } from "../parsers/ui-dump.js";
|
|
2
|
+
export declare const MIN_ICON_SIZE = 16;
|
|
3
|
+
export declare const MAX_ICON_SIZE = 200;
|
|
4
|
+
export declare const MIN_ASPECT_RATIO = 0.5;
|
|
5
|
+
export declare const MAX_ASPECT_RATIO = 2;
|
|
6
|
+
export declare const MAX_CANDIDATES = 6;
|
|
7
|
+
/**
|
|
8
|
+
* Check if dimensions are within icon size constraints.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isIconSized(width: number, height: number): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Filter accessibility nodes to find unlabeled clickable elements that are icon-sized.
|
|
13
|
+
* Returns max 6 candidates sorted by position (top-to-bottom, left-to-right).
|
|
14
|
+
*/
|
|
15
|
+
export declare function filterIconCandidates(nodes: AccessibilityNode[]): AccessibilityNode[];
|
|
16
|
+
/**
|
|
17
|
+
* Format candidate bounds as string "[x0,y0][x1,y1]"
|
|
18
|
+
*/
|
|
19
|
+
export declare function formatBounds(node: AccessibilityNode): string;
|
|
20
|
+
/**
|
|
21
|
+
* Crop a region from an image and return as base64 JPEG.
|
|
22
|
+
* Scales down to max 128x128 if larger.
|
|
23
|
+
*/
|
|
24
|
+
export declare function cropCandidateImage(imagePath: string, bounds: Bounds): Promise<string>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import sharp from "sharp";
|
|
2
|
+
export const MIN_ICON_SIZE = 16;
|
|
3
|
+
export const MAX_ICON_SIZE = 200;
|
|
4
|
+
export const MIN_ASPECT_RATIO = 0.5;
|
|
5
|
+
export const MAX_ASPECT_RATIO = 2.0;
|
|
6
|
+
export const MAX_CANDIDATES = 6;
|
|
7
|
+
/**
|
|
8
|
+
* Check if dimensions are within icon size constraints.
|
|
9
|
+
*/
|
|
10
|
+
export function isIconSized(width, height) {
|
|
11
|
+
if (width < MIN_ICON_SIZE || width > MAX_ICON_SIZE)
|
|
12
|
+
return false;
|
|
13
|
+
if (height < MIN_ICON_SIZE || height > MAX_ICON_SIZE)
|
|
14
|
+
return false;
|
|
15
|
+
const aspectRatio = width / height;
|
|
16
|
+
if (aspectRatio < MIN_ASPECT_RATIO || aspectRatio > MAX_ASPECT_RATIO)
|
|
17
|
+
return false;
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Filter accessibility nodes to find unlabeled clickable elements that are icon-sized.
|
|
22
|
+
* Returns max 6 candidates sorted by position (top-to-bottom, left-to-right).
|
|
23
|
+
*/
|
|
24
|
+
export function filterIconCandidates(nodes) {
|
|
25
|
+
const candidates = nodes.filter((node) => {
|
|
26
|
+
// Must be clickable
|
|
27
|
+
if (!node.clickable)
|
|
28
|
+
return false;
|
|
29
|
+
// Must not have text or contentDesc (we're looking for unlabeled icons)
|
|
30
|
+
if (node.text || node.contentDesc)
|
|
31
|
+
return false;
|
|
32
|
+
// Must be icon-sized
|
|
33
|
+
const width = node.bounds.right - node.bounds.left;
|
|
34
|
+
const height = node.bounds.bottom - node.bounds.top;
|
|
35
|
+
if (!isIconSized(width, height))
|
|
36
|
+
return false;
|
|
37
|
+
return true;
|
|
38
|
+
});
|
|
39
|
+
// Sort by Y first (top-to-bottom), then X (left-to-right)
|
|
40
|
+
candidates.sort((a, b) => {
|
|
41
|
+
if (a.centerY !== b.centerY)
|
|
42
|
+
return a.centerY - b.centerY;
|
|
43
|
+
return a.centerX - b.centerX;
|
|
44
|
+
});
|
|
45
|
+
// Limit to max candidates
|
|
46
|
+
return candidates.slice(0, MAX_CANDIDATES);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Format candidate bounds as string "[x0,y0][x1,y1]"
|
|
50
|
+
*/
|
|
51
|
+
export function formatBounds(node) {
|
|
52
|
+
return `[${node.bounds.left},${node.bounds.top}][${node.bounds.right},${node.bounds.bottom}]`;
|
|
53
|
+
}
|
|
54
|
+
const MAX_CROP_SIZE = 128;
|
|
55
|
+
const JPEG_QUALITY = 70;
|
|
56
|
+
/**
|
|
57
|
+
* Crop a region from an image and return as base64 JPEG.
|
|
58
|
+
* Scales down to max 128x128 if larger.
|
|
59
|
+
*/
|
|
60
|
+
export async function cropCandidateImage(imagePath, bounds) {
|
|
61
|
+
const width = bounds.right - bounds.left;
|
|
62
|
+
const height = bounds.bottom - bounds.top;
|
|
63
|
+
let pipeline = sharp(imagePath).extract({
|
|
64
|
+
left: bounds.left,
|
|
65
|
+
top: bounds.top,
|
|
66
|
+
width,
|
|
67
|
+
height,
|
|
68
|
+
});
|
|
69
|
+
// Scale down if larger than max size
|
|
70
|
+
if (width > MAX_CROP_SIZE || height > MAX_CROP_SIZE) {
|
|
71
|
+
pipeline = pipeline.resize(MAX_CROP_SIZE, MAX_CROP_SIZE, {
|
|
72
|
+
fit: "inside",
|
|
73
|
+
withoutEnlargement: true,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
const buffer = await pipeline.jpeg({ quality: JPEG_QUALITY }).toBuffer();
|
|
77
|
+
return buffer.toString("base64");
|
|
78
|
+
}
|
package/dist/tools/adb-app.js
CHANGED
|
@@ -5,7 +5,8 @@ export const adbAppInputSchema = z.object({
|
|
|
5
5
|
packageName: z.string().optional(),
|
|
6
6
|
});
|
|
7
7
|
export async function handleAdbAppTool(input, context) {
|
|
8
|
-
const
|
|
8
|
+
const device = await context.deviceState.ensureDevice(context.adb);
|
|
9
|
+
const deviceId = device.id;
|
|
9
10
|
switch (input.operation) {
|
|
10
11
|
case "install": {
|
|
11
12
|
if (!input.apkPath) {
|
|
@@ -52,7 +53,7 @@ export async function handleAdbAppTool(input, context) {
|
|
|
52
53
|
}
|
|
53
54
|
export const adbAppToolDefinition = {
|
|
54
55
|
name: "adb-app",
|
|
55
|
-
description: "Manage applications. Operations: install, uninstall, launch, stop, clear-data, list.",
|
|
56
|
+
description: "Manage applications. Auto-selects device if only one connected. Operations: install, uninstall, launch, stop, clear-data, list.",
|
|
56
57
|
inputSchema: {
|
|
57
58
|
type: "object",
|
|
58
59
|
properties: {
|
package/dist/tools/adb-device.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export const adbDeviceInputSchema = z.object({
|
|
3
|
-
operation: z.enum(["list", "select", "wait", "properties"]),
|
|
3
|
+
operation: z.enum(["list", "select", "wait", "properties", "health-check"]),
|
|
4
4
|
deviceId: z.string().optional(),
|
|
5
5
|
});
|
|
6
6
|
export async function handleAdbDeviceTool(input, context) {
|
|
@@ -28,15 +28,18 @@ export async function handleAdbDeviceTool(input, context) {
|
|
|
28
28
|
return { selected: device };
|
|
29
29
|
}
|
|
30
30
|
case "wait": {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
const device = input.deviceId
|
|
32
|
+
? { id: input.deviceId }
|
|
33
|
+
: await context.deviceState.ensureDevice(context.adb);
|
|
34
|
+
const deviceId = device.id;
|
|
35
35
|
await context.adb.waitForDevice(deviceId);
|
|
36
36
|
return { status: "device ready", deviceId };
|
|
37
37
|
}
|
|
38
38
|
case "properties": {
|
|
39
|
-
const
|
|
39
|
+
const device = input.deviceId
|
|
40
|
+
? { id: input.deviceId }
|
|
41
|
+
: await context.deviceState.ensureDevice(context.adb);
|
|
42
|
+
const deviceId = device.id;
|
|
40
43
|
const props = await context.adb.getProperties(deviceId);
|
|
41
44
|
return {
|
|
42
45
|
deviceId,
|
|
@@ -50,19 +53,55 @@ export async function handleAdbDeviceTool(input, context) {
|
|
|
50
53
|
allProperties: props,
|
|
51
54
|
};
|
|
52
55
|
}
|
|
56
|
+
case "health-check": {
|
|
57
|
+
const env = await context.environment.detect();
|
|
58
|
+
let adbServerRunning = false;
|
|
59
|
+
let connectedDevices = 0;
|
|
60
|
+
const warnings = [];
|
|
61
|
+
const errors = [];
|
|
62
|
+
if (!env.isValid) {
|
|
63
|
+
errors.push(...env.issues);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
// Test adb server
|
|
67
|
+
try {
|
|
68
|
+
const devices = await context.adb.getDevices();
|
|
69
|
+
adbServerRunning = true;
|
|
70
|
+
connectedDevices = devices.length;
|
|
71
|
+
if (devices.length === 0) {
|
|
72
|
+
warnings.push("No devices connected. Start an emulator or connect a USB device.");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
errors.push("adb server not responding. Run 'adb kill-server && adb start-server'");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
healthy: errors.length === 0,
|
|
81
|
+
environment: {
|
|
82
|
+
sdkPath: env.sdkPath,
|
|
83
|
+
adbPath: env.adbPath,
|
|
84
|
+
platform: env.platform,
|
|
85
|
+
},
|
|
86
|
+
adbServerRunning,
|
|
87
|
+
connectedDevices,
|
|
88
|
+
warnings,
|
|
89
|
+
errors,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
53
92
|
default:
|
|
54
93
|
throw new Error(`Unknown operation: ${input.operation}`);
|
|
55
94
|
}
|
|
56
95
|
}
|
|
57
96
|
export const adbDeviceToolDefinition = {
|
|
58
97
|
name: "adb-device",
|
|
59
|
-
description: "Manage device connections. Operations: list, select, wait, properties.",
|
|
98
|
+
description: "Manage device connections. Operations: list, select, wait, properties, health-check.",
|
|
60
99
|
inputSchema: {
|
|
61
100
|
type: "object",
|
|
62
101
|
properties: {
|
|
63
102
|
operation: {
|
|
64
103
|
type: "string",
|
|
65
|
-
enum: ["list", "select", "wait", "properties"],
|
|
104
|
+
enum: ["list", "select", "wait", "properties", "health-check"],
|
|
66
105
|
},
|
|
67
106
|
deviceId: { type: "string", description: "Device ID for select/wait/properties" },
|
|
68
107
|
},
|
package/dist/tools/adb-logcat.js
CHANGED
|
@@ -9,7 +9,8 @@ export const adbLogcatInputSchema = z.object({
|
|
|
9
9
|
since: z.string().optional(),
|
|
10
10
|
});
|
|
11
11
|
export async function handleAdbLogcatTool(input, context) {
|
|
12
|
-
const
|
|
12
|
+
const device = await context.deviceState.ensureDevice(context.adb);
|
|
13
|
+
const deviceId = device.id;
|
|
13
14
|
// Build filter string
|
|
14
15
|
let filter = "";
|
|
15
16
|
if (input.rawFilter) {
|
|
@@ -55,7 +56,7 @@ export async function handleAdbLogcatTool(input, context) {
|
|
|
55
56
|
}
|
|
56
57
|
export const adbLogcatToolDefinition = {
|
|
57
58
|
name: "adb-logcat",
|
|
58
|
-
description: "Read device logs. Returns summary with logId for full output.",
|
|
59
|
+
description: "Read device logs. Auto-selects device if only one connected. Returns summary with logId for full output.",
|
|
59
60
|
inputSchema: {
|
|
60
61
|
type: "object",
|
|
61
62
|
properties: {
|
package/dist/tools/adb-shell.js
CHANGED
|
@@ -4,7 +4,8 @@ export const adbShellInputSchema = z.object({
|
|
|
4
4
|
timeout: z.number().optional(),
|
|
5
5
|
});
|
|
6
6
|
export async function handleAdbShellTool(input, context) {
|
|
7
|
-
const
|
|
7
|
+
const device = await context.deviceState.ensureDevice(context.adb);
|
|
8
|
+
const deviceId = device.id;
|
|
8
9
|
const result = await context.adb.shell(deviceId, input.command, input.timeout);
|
|
9
10
|
return {
|
|
10
11
|
stdout: result.stdout,
|
|
@@ -15,7 +16,7 @@ export async function handleAdbShellTool(input, context) {
|
|
|
15
16
|
}
|
|
16
17
|
export const adbShellToolDefinition = {
|
|
17
18
|
name: "adb-shell",
|
|
18
|
-
description: "Execute shell commands with safety guards. Dangerous commands are blocked.",
|
|
19
|
+
description: "Execute shell commands with safety guards. Auto-selects device if only one connected. Dangerous commands are blocked.",
|
|
19
20
|
inputSchema: {
|
|
20
21
|
type: "object",
|
|
21
22
|
properties: {
|
|
@@ -3,8 +3,8 @@ import { ServerContext } from "../server.js";
|
|
|
3
3
|
export declare const emulatorDeviceInputSchema: z.ZodObject<{
|
|
4
4
|
operation: z.ZodEnum<{
|
|
5
5
|
kill: "kill";
|
|
6
|
-
list: "list";
|
|
7
6
|
create: "create";
|
|
7
|
+
list: "list";
|
|
8
8
|
start: "start";
|
|
9
9
|
wipe: "wipe";
|
|
10
10
|
"snapshot-save": "snapshot-save";
|
|
@@ -5,8 +5,8 @@ export declare const gradleGetDetailsInputSchema: z.ZodObject<{
|
|
|
5
5
|
detailType: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
|
|
6
6
|
all: "all";
|
|
7
7
|
tasks: "tasks";
|
|
8
|
-
logs: "logs";
|
|
9
8
|
errors: "errors";
|
|
9
|
+
logs: "logs";
|
|
10
10
|
}>>>;
|
|
11
11
|
}, z.core.$strip>;
|
|
12
12
|
export type GradleGetDetailsInput = z.infer<typeof gradleGetDetailsInputSchema>;
|
package/dist/tools/ui.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { ServerContext } from "../server.js";
|
|
3
|
+
import { UiConfig } from "../types/index.js";
|
|
3
4
|
export declare const uiInputSchema: z.ZodObject<{
|
|
4
5
|
operation: z.ZodEnum<{
|
|
5
6
|
find: "find";
|
|
@@ -8,21 +9,27 @@ export declare const uiInputSchema: z.ZodObject<{
|
|
|
8
9
|
input: "input";
|
|
9
10
|
screenshot: "screenshot";
|
|
10
11
|
"accessibility-check": "accessibility-check";
|
|
12
|
+
"visual-snapshot": "visual-snapshot";
|
|
11
13
|
}>;
|
|
12
14
|
selector: z.ZodOptional<z.ZodObject<{
|
|
13
15
|
resourceId: z.ZodOptional<z.ZodString>;
|
|
14
16
|
text: z.ZodOptional<z.ZodString>;
|
|
15
17
|
textContains: z.ZodOptional<z.ZodString>;
|
|
16
18
|
className: z.ZodOptional<z.ZodString>;
|
|
19
|
+
nearestTo: z.ZodOptional<z.ZodString>;
|
|
17
20
|
}, z.core.$strip>>;
|
|
18
21
|
x: z.ZodOptional<z.ZodNumber>;
|
|
19
22
|
y: z.ZodOptional<z.ZodNumber>;
|
|
20
23
|
elementIndex: z.ZodOptional<z.ZodNumber>;
|
|
21
24
|
text: z.ZodOptional<z.ZodString>;
|
|
22
25
|
localPath: z.ZodOptional<z.ZodString>;
|
|
26
|
+
inline: z.ZodOptional<z.ZodBoolean>;
|
|
27
|
+
debug: z.ZodOptional<z.ZodBoolean>;
|
|
28
|
+
gridCell: z.ZodOptional<z.ZodNumber>;
|
|
29
|
+
gridPosition: z.ZodOptional<z.ZodNumber>;
|
|
23
30
|
}, z.core.$strip>;
|
|
24
31
|
export type UiInput = z.infer<typeof uiInputSchema>;
|
|
25
|
-
export declare function handleUiTool(input: UiInput, context: ServerContext): Promise<Record<string, unknown>>;
|
|
32
|
+
export declare function handleUiTool(input: UiInput, context: ServerContext, uiConfig?: UiConfig): Promise<Record<string, unknown>>;
|
|
26
33
|
export declare const uiToolDefinition: {
|
|
27
34
|
name: string;
|
|
28
35
|
description: string;
|
|
@@ -48,6 +55,10 @@ export declare const uiToolDefinition: {
|
|
|
48
55
|
className: {
|
|
49
56
|
type: string;
|
|
50
57
|
};
|
|
58
|
+
nearestTo: {
|
|
59
|
+
type: string;
|
|
60
|
+
description: string;
|
|
61
|
+
};
|
|
51
62
|
};
|
|
52
63
|
description: string;
|
|
53
64
|
};
|
|
@@ -71,6 +82,26 @@ export declare const uiToolDefinition: {
|
|
|
71
82
|
type: string;
|
|
72
83
|
description: string;
|
|
73
84
|
};
|
|
85
|
+
inline: {
|
|
86
|
+
type: string;
|
|
87
|
+
description: string;
|
|
88
|
+
};
|
|
89
|
+
debug: {
|
|
90
|
+
type: string;
|
|
91
|
+
description: string;
|
|
92
|
+
};
|
|
93
|
+
gridCell: {
|
|
94
|
+
type: string;
|
|
95
|
+
minimum: number;
|
|
96
|
+
maximum: number;
|
|
97
|
+
description: string;
|
|
98
|
+
};
|
|
99
|
+
gridPosition: {
|
|
100
|
+
type: string;
|
|
101
|
+
minimum: number;
|
|
102
|
+
maximum: number;
|
|
103
|
+
description: string;
|
|
104
|
+
};
|
|
74
105
|
};
|
|
75
106
|
required: string[];
|
|
76
107
|
};
|