mcp-android-emulator 1.4.0 → 2.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/CHANGELOG.md +97 -0
- package/LICENSE +21 -21
- package/README.md +555 -542
- package/SECURITY.md +43 -0
- package/dist/adb/runner.d.ts +43 -0
- package/dist/adb/runner.js +87 -0
- package/dist/adb/validators.d.ts +29 -0
- package/dist/adb/validators.js +110 -0
- package/dist/index.d.ts +17 -2
- package/dist/index.js +404 -762
- package/package.json +50 -48
- package/src/adb/runner.ts +107 -0
- package/src/adb/validators.ts +125 -0
- package/src/index.ts +1463 -1893
- package/test/runner.test.ts +94 -0
- package/test/validators.test.ts +199 -0
- package/tsconfig.json +16 -16
package/package.json
CHANGED
|
@@ -1,48 +1,50 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "mcp-android-emulator",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "MCP Server for Android Emulator interaction via ADB - enables AI assistants to control Android devices",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "dist/index.js",
|
|
7
|
-
"bin": {
|
|
8
|
-
"mcp-android-emulator": "./dist/index.js"
|
|
9
|
-
},
|
|
10
|
-
"scripts": {
|
|
11
|
-
"build": "tsc",
|
|
12
|
-
"start": "node dist/index.js",
|
|
13
|
-
"dev": "tsc --watch",
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"
|
|
47
|
-
|
|
48
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-android-emulator",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "MCP Server for Android Emulator interaction via ADB - enables AI assistants to control Android devices",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mcp-android-emulator": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"dev": "tsc --watch",
|
|
14
|
+
"test": "node --test --import tsx test/validators.test.ts test/runner.test.ts",
|
|
15
|
+
"prepublishOnly": "npm run build"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"mcp",
|
|
19
|
+
"model-context-protocol",
|
|
20
|
+
"android",
|
|
21
|
+
"emulator",
|
|
22
|
+
"adb",
|
|
23
|
+
"automation",
|
|
24
|
+
"testing",
|
|
25
|
+
"claude",
|
|
26
|
+
"ai"
|
|
27
|
+
],
|
|
28
|
+
"author": "",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/Anjos2/mcp-android-emulator.git"
|
|
33
|
+
},
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/Anjos2/mcp-android-emulator/issues"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/Anjos2/mcp-android-emulator#readme",
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18.0.0"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@modelcontextprotocol/sdk": "1.29.0",
|
|
43
|
+
"zod": "3.25.76"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "22.19.17",
|
|
47
|
+
"tsx": "4.21.0",
|
|
48
|
+
"typescript": "5.9.3"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runner seguro para comandos adb.
|
|
3
|
+
*
|
|
4
|
+
* Finalidad:
|
|
5
|
+
* Centraliza toda interacción con el binario adb usando execFile (NO exec),
|
|
6
|
+
* lo que elimina la interpretación de metacaracteres shell en el HOST.
|
|
7
|
+
* El host queda protegido aunque se pasen argumentos con ';', '|', '`', etc.
|
|
8
|
+
*
|
|
9
|
+
* Nota sobre adb shell:
|
|
10
|
+
* Cuando se usa 'adb shell', adb concatena los argv con espacios y los
|
|
11
|
+
* entrega al /system/bin/sh del device. El sh del device SÍ reinterpreta
|
|
12
|
+
* metacaracteres. Por lo tanto, la defensa en profundidad exige que los
|
|
13
|
+
* argumentos vengan validados (allowlist) por src/adb/validators.ts antes
|
|
14
|
+
* de llegar aquí.
|
|
15
|
+
*
|
|
16
|
+
* Interrelación:
|
|
17
|
+
* - Usado por src/index.ts (todas las tools migradas).
|
|
18
|
+
* - Complementado por src/adb/validators.ts (allowlists zod).
|
|
19
|
+
* - Config externa: variable de entorno ADB_PATH.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { execFile } from "node:child_process";
|
|
23
|
+
import { promisify } from "node:util";
|
|
24
|
+
|
|
25
|
+
const execFileAsync = promisify(execFile);
|
|
26
|
+
|
|
27
|
+
const ADB_PATH = process.env.ADB_PATH || "adb";
|
|
28
|
+
|
|
29
|
+
export interface RunOptions {
|
|
30
|
+
/** Timeout en ms; default 30s. 0 o negativo = sin timeout. */
|
|
31
|
+
timeoutMs?: number;
|
|
32
|
+
/** Buffer máximo de stdout/stderr en bytes; default 10 MB. */
|
|
33
|
+
maxBufferBytes?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
37
|
+
const DEFAULT_MAX_BUFFER = 10 * 1024 * 1024;
|
|
38
|
+
|
|
39
|
+
function normalizeOpts(opts: RunOptions): { timeout: number; maxBuffer: number } {
|
|
40
|
+
const t = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
41
|
+
return {
|
|
42
|
+
timeout: t > 0 ? t : 0,
|
|
43
|
+
maxBuffer: opts.maxBufferBytes ?? DEFAULT_MAX_BUFFER,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Ejecuta `adb <args...>` sin pasar por shell del host.
|
|
49
|
+
* Cada elemento de args llega como argumento separado al binario adb.
|
|
50
|
+
*/
|
|
51
|
+
export async function runAdb(args: string[], opts: RunOptions = {}): Promise<string> {
|
|
52
|
+
const { timeout, maxBuffer } = normalizeOpts(opts);
|
|
53
|
+
try {
|
|
54
|
+
const { stdout } = await execFileAsync(ADB_PATH, args, { timeout, maxBuffer });
|
|
55
|
+
return stdout.trim();
|
|
56
|
+
} catch (error: unknown) {
|
|
57
|
+
throw wrapAdbError(error);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Ejecuta `adb shell <argv...>`. Los tokens serán re-ensamblados por adb y
|
|
63
|
+
* pasados al sh del device. Los argumentos DEBEN estar validados contra
|
|
64
|
+
* shell metacharacters antes de invocar esta función (ver validators.ts).
|
|
65
|
+
*/
|
|
66
|
+
export async function runAdbShell(argv: string[], opts: RunOptions = {}): Promise<string> {
|
|
67
|
+
if (argv.length === 0) {
|
|
68
|
+
throw new Error("runAdbShell requires at least one argument");
|
|
69
|
+
}
|
|
70
|
+
return runAdb(["shell", ...argv], opts);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Ejecuta `adb exec-out <argv...>`. Útil para obtener bytes binarios sin
|
|
75
|
+
* transformación (screencap, pull de archivos). Los argumentos DEBEN estar
|
|
76
|
+
* validados. Devuelve un Buffer.
|
|
77
|
+
*/
|
|
78
|
+
export async function runAdbExecOutBinary(argv: string[], opts: RunOptions = {}): Promise<Buffer> {
|
|
79
|
+
if (argv.length === 0) {
|
|
80
|
+
throw new Error("runAdbExecOutBinary requires at least one argument");
|
|
81
|
+
}
|
|
82
|
+
const { timeout, maxBuffer } = normalizeOpts(opts);
|
|
83
|
+
return new Promise<Buffer>((resolve, reject) => {
|
|
84
|
+
execFile(
|
|
85
|
+
ADB_PATH,
|
|
86
|
+
["exec-out", ...argv],
|
|
87
|
+
{ timeout, maxBuffer, encoding: "buffer" },
|
|
88
|
+
(error, stdout) => {
|
|
89
|
+
if (error) {
|
|
90
|
+
reject(wrapAdbError(error));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
resolve(stdout as Buffer);
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function wrapAdbError(error: unknown): Error {
|
|
100
|
+
if (error instanceof Error) {
|
|
101
|
+
// @ts-expect-error — stderr no es estándar pero lo añade child_process
|
|
102
|
+
const stderr: string | undefined = error.stderr;
|
|
103
|
+
const msg = stderr?.toString?.().trim() || error.message;
|
|
104
|
+
return new Error(`adb error: ${msg}`);
|
|
105
|
+
}
|
|
106
|
+
return new Error(`adb error: ${String(error)}`);
|
|
107
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validators zod — allowlists estrictas para todo input que eventualmente
|
|
3
|
+
* llegará a `adb shell` (donde el sh del device reinterpreta metacaracteres).
|
|
4
|
+
*
|
|
5
|
+
* Finalidad:
|
|
6
|
+
* Actuar como segunda línea de defensa sobre src/adb/runner.ts. El runner
|
|
7
|
+
* protege el host (no invoca sh local); estos validators protegen el device
|
|
8
|
+
* (bloquean metacaracteres antes de llegar al sh del device).
|
|
9
|
+
*
|
|
10
|
+
* Interrelación:
|
|
11
|
+
* - Usado por los schemas de tool en src/index.ts.
|
|
12
|
+
* - Combinable con z.object() normal de la librería zod.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Regex para metacaracteres shell peligrosos.
|
|
19
|
+
* Usado para rechazo en allowlists que admiten ciertos caracteres
|
|
20
|
+
* pero quieren excluir los más peligrosos explícitamente.
|
|
21
|
+
*/
|
|
22
|
+
export const SHELL_METACHARS = /[;&|`$()<>\\"'\n\r\t\x00-\x1f]/;
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Android package name
|
|
26
|
+
// Formato Java-compatible: segmentos separados por puntos, cada segmento
|
|
27
|
+
// empieza con letra o underscore, continúa con alfanumérico/underscore.
|
|
28
|
+
// Mínimo dos segmentos (no hay package top-level sin punto en Android real).
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
const PACKAGE_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)+$/;
|
|
31
|
+
|
|
32
|
+
export const packageNameSchema = z
|
|
33
|
+
.string()
|
|
34
|
+
.min(3)
|
|
35
|
+
.max(255)
|
|
36
|
+
.regex(PACKAGE_NAME_REGEX, "Invalid Android package name");
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// APK path
|
|
40
|
+
// Rechaza metacaracteres shell y exige extensión .apk.
|
|
41
|
+
// La existencia del archivo se valida en el handler (no aquí, para que
|
|
42
|
+
// los tests puedan usar paths simulados).
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
export const apkPathSchema = z
|
|
45
|
+
.string()
|
|
46
|
+
.min(5)
|
|
47
|
+
.max(4096)
|
|
48
|
+
.refine((p) => p.toLowerCase().endsWith(".apk"), {
|
|
49
|
+
message: "Must be a .apk file",
|
|
50
|
+
})
|
|
51
|
+
.refine((p) => !SHELL_METACHARS.test(p), {
|
|
52
|
+
message: "Path contains disallowed characters",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Android resource-id (e.g. com.app:id/button_login)
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
const RESOURCE_ID_REGEX = /^[a-zA-Z_][a-zA-Z0-9_.]*:[a-zA-Z]+\/[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
59
|
+
|
|
60
|
+
export const resourceIdSchema = z
|
|
61
|
+
.string()
|
|
62
|
+
.min(3)
|
|
63
|
+
.max(512)
|
|
64
|
+
.regex(RESOURCE_ID_REGEX, "Invalid Android resource-id");
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Texto libre del usuario (filter, search, assert)
|
|
68
|
+
// Este texto NO llega al shell: se usa en JavaScript (String.includes,
|
|
69
|
+
// RegExp.source con escape). Aceptamos caracteres amplios pero limitamos
|
|
70
|
+
// longitud y prohibimos control characters para evitar sorpresas.
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
const CONTROL_CHARS = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
73
|
+
|
|
74
|
+
export const freeTextSchema = z
|
|
75
|
+
.string()
|
|
76
|
+
.max(1024)
|
|
77
|
+
.refine((s) => !CONTROL_CHARS.test(s), {
|
|
78
|
+
message: "Text contains control characters",
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Texto que se va a tipear en un input Android (type_text, set_text).
|
|
83
|
+
// Este texto PASA por el shell del device (`input text ...`). Usamos DENY-list
|
|
84
|
+
// (rechazar solo metacaracteres shell y caracteres de control), NO allow-list,
|
|
85
|
+
// para no romper i18n: acentos, ñ, CJK y emoji son válidos en inputs reales
|
|
86
|
+
// de aplicaciones. Mismo nivel de seguridad que una allow-list ASCII porque
|
|
87
|
+
// los shell metachars son el único vector de inyección.
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
export const typeableTextSchema = z
|
|
90
|
+
.string()
|
|
91
|
+
.min(0)
|
|
92
|
+
.max(2048)
|
|
93
|
+
.refine((s) => !SHELL_METACHARS.test(s), {
|
|
94
|
+
message:
|
|
95
|
+
"Text contains shell metacharacters (; & | ` $ ( ) < > \\ \" ' or control chars). Use key events for these.",
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Filter para list_packages / get_logs — usado en JS, no en shell.
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
export const searchFilterSchema = z
|
|
102
|
+
.string()
|
|
103
|
+
.max(256)
|
|
104
|
+
.refine((s) => !CONTROL_CHARS.test(s), {
|
|
105
|
+
message: "Filter contains control characters",
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Conteo numérico sanitizado (lines en get_logs, etc).
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
export const positiveCountSchema = z
|
|
112
|
+
.number()
|
|
113
|
+
.int()
|
|
114
|
+
.positive()
|
|
115
|
+
.max(100_000);
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Coordenadas (seguras por tipo, pero clamp a límites razonables).
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
export const coordinateSchema = z.number().int().min(0).max(100_000);
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// Duración en ms.
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
export const durationMsSchema = z.number().int().nonnegative().max(600_000);
|