mobile-mcp-server 1.0.1

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 ADDED
@@ -0,0 +1,43 @@
1
+ # Mobile MCP Server
2
+
3
+ A Model Context Protocol (MCP) server for controlling Android devices and emulators.
4
+
5
+ ## Quick Start (Local)
6
+ 1. `npm install`
7
+ 2. `npm run build`
8
+ 3. `node dist/server.http.js`
9
+
10
+ ## Connecting from another PC (e.g. Claude Desktop)
11
+ If your Claude Desktop is on a different machine than the Android execution node:
12
+
13
+ ### 1. Networking (Tailscale 추천)
14
+ Since devices on different subnets (e.g., `192.168.1.x` and `192.168.31.x`) cannot communicate directly:
15
+ - Install **Tailscale** on both machines.
16
+ - Use the Tailscale IP (e.g., `100.x.x.x`) of the server machine.
17
+
18
+ ### 2. Client Setup
19
+ 1. Copy `proxy_client.js` from this repo to your client machine.
20
+ 2. Update your `claude_desktop_config.json`:
21
+
22
+ ```json
23
+ {
24
+ "mcpServers": {
25
+ "mobile-mcp-http": {
26
+ "command": "node",
27
+ "args": [
28
+ "C:\\path\\to\\your\\proxy_client.js"
29
+ ],
30
+ "env": {
31
+ "MCP_SERVER_URL": "http://<SERVER_IP>:7000/mcp",
32
+ "MCP_API_KEY": "hash2026"
33
+ }
34
+ }
35
+ }
36
+ }
37
+ ```
38
+
39
+ ## Features
40
+ - ADB Toolset (click, type, shell, devices)
41
+ - App Management (install, launch, list)
42
+ - Automated Prompts (smoke test, launch-only)
43
+ - Hardware Control (start emulator)
package/dist/bin.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/bin.js ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ import { fileURLToPath } from 'url';
3
+ import path from 'path';
4
+ // Get current directory
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+ const args = process.argv.slice(2);
8
+ /**
9
+ * Universal Entry Point for Mobile MCP
10
+ *
11
+ * Usage:
12
+ * npx mobile-mcp -> Runs local Stdio server (for PC)
13
+ * npx mobile-mcp --remote -> Runs Proxy client (for Laptop)
14
+ */
15
+ async function start() {
16
+ if (args.includes('--remote')) {
17
+ console.error('[Mobile MCP] Starting in REMOTE mode (Proxy Client)...');
18
+ // Point to the compiled proxy logic
19
+ const proxyPath = path.resolve(__dirname, '../proxy_client.js');
20
+ import(proxyPath);
21
+ }
22
+ else {
23
+ // Standard Local Stdio mode
24
+ const serverPath = path.resolve(__dirname, '../server.js');
25
+ import(serverPath);
26
+ }
27
+ }
28
+ start().catch(err => {
29
+ console.error('[Mobile MCP] Fatal Error:', err);
30
+ process.exit(1);
31
+ });
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Environment and runtime configurations
3
+ */
4
+ export declare const SERVER_NAME: string;
5
+ export declare const SERVER_VERSION: string;
6
+ export declare const DEFAULT_COMMAND_TIMEOUT: number;
7
+ export declare const APK_INSTALL_TIMEOUT: number;
8
+ export declare const SCREENSHOT_TIMEOUT: number;
9
+ export declare const APPIUM_HOST: string;
10
+ export declare const APPIUM_PORT: number;
11
+ export type LogLevel = "debug" | "info" | "warn" | "error";
12
+ export declare const LOG_LEVEL: LogLevel;
13
+ export declare const MCP_API_KEY: string;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Environment and runtime configurations
3
+ */
4
+ export const SERVER_NAME = "mobile-automation-server";
5
+ export const SERVER_VERSION = "1.0.0";
6
+ // Timeouts (in milliseconds)
7
+ export const DEFAULT_COMMAND_TIMEOUT = 30000;
8
+ export const APK_INSTALL_TIMEOUT = 120000;
9
+ export const SCREENSHOT_TIMEOUT = 10000;
10
+ // Appium configuration (for future use)
11
+ export const APPIUM_HOST = process.env.APPIUM_HOST || "127.0.0.1";
12
+ export const APPIUM_PORT = parseInt(process.env.APPIUM_PORT || "4723", 10);
13
+ export const LOG_LEVEL = process.env.LOG_LEVEL || "info";
14
+ // Security
15
+ export const MCP_API_KEY = process.env.MCP_API_KEY || "hash2026";
@@ -0,0 +1,8 @@
1
+ export declare const ANDROID_HOME: string;
2
+ export declare const ADB_PATH: string;
3
+ export declare const EMULATOR_PATH: string;
4
+ export declare const ARTIFACTS_ROOT: string;
5
+ export declare const LOG_DIR: string;
6
+ export declare const SCREENSHOT_DIR: string;
7
+ export declare const APK_DIR: string;
8
+ export declare const DOWNLOADS_DIR: string;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Centralized path configurations
3
+ */
4
+ import { join } from "path";
5
+ import { fileURLToPath } from "url";
6
+ import { dirname } from "path";
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+ // Android SDK paths
10
+ export const ANDROID_HOME = process.env.ANDROID_HOME || "E:\\Android\\Sdk";
11
+ export const ADB_PATH = process.env.ADB_PATH || join(ANDROID_HOME, "platform-tools", "adb.exe");
12
+ export const EMULATOR_PATH = process.env.EMULATOR_PATH || join(ANDROID_HOME, "emulator", "emulator.exe");
13
+ // Strict Artifact Paths (LOCKED by User Constraints)
14
+ export const ARTIFACTS_ROOT = "D:\\Automation\\artifacts";
15
+ export const LOG_DIR = join(ARTIFACTS_ROOT, "logs");
16
+ export const SCREENSHOT_DIR = join(ARTIFACTS_ROOT, "shots"); // Strictly "shots", not "screenshots"
17
+ export const APK_DIR = join(ARTIFACTS_ROOT, "apks");
18
+ // Map "downloads" to apks directory for now, or a temp dir if needed,
19
+ // but user only gave us 3 allowlisted buckets. Let's force downloads to be treated as APKs if they are APKs.
20
+ // or just put them in a temp folder inside artifacts if generic.
21
+ // For now, I will add a strict DOWNLOADS_DIR that maps to apks for consistency with user constraints on APKs.
22
+ export const DOWNLOADS_DIR = APK_DIR;
@@ -0,0 +1,8 @@
1
+ {"timestamp":"2026-02-09T14:04:19.170Z","level":"info","message":"TOOL: adb_devices","data":{"input":{},"success":true,"error":null}}
2
+ {"timestamp":"2026-02-09T14:10:39.712Z","level":"info","message":"TOOL: tap","data":{"input":{"x":200,"y":450},"success":true,"error":null}}
3
+ {"timestamp":"2026-02-09T14:10:47.878Z","level":"info","message":"TOOL: type_text","data":{"input":{"text":"vigneshwaran@pxlbrain.com"},"success":true,"error":null}}
4
+ {"timestamp":"2026-02-09T14:17:22.975Z","level":"info","message":"TOOL: uninstall_app","data":{"input":{"package_name":"com.hashhealth"},"success":true,"error":null}}
5
+ {"timestamp":"2026-02-09T14:19:57.351Z","level":"info","message":"TOOL: install_apk","data":{"input":{"apk_path":"D:\\MCP\\mobile-mcp-server\\downloads\\downloaded_1770637108340.apk"},"success":false,"error":"Command failed: \"D:\\Android\\platform-tools\\adb.exe\" install \"D:\\MCP\\mobile-mcp-server\\downloads\\downloaded_1770637108340.apk\"\nadb.exe: failed to install D:\\MCP\\mobile-mcp-server\\downloads\\downloaded_1770637108340.apk: Failure [INSTALL_PARSE_FAILED_NOT_APK: Failed to parse /data/app/vmdl715720077.tmp/base.apk: Failed to load asset path /data/app/vmdl715720077.tmp/base.apk]\r\n\r\n"}}
6
+ {"timestamp":"2026-02-09T14:29:15.278Z","level":"info","message":"TOOL: adb_devices","data":{"input":{},"success":true,"error":null}}
7
+ {"timestamp":"2026-02-09T14:31:41.399Z","level":"info","message":"TOOL: adb_devices","data":{"input":{},"success":true,"error":null}}
8
+ {"timestamp":"2026-02-09T14:32:41.069Z","level":"info","message":"TOOL: install_apk","data":{"input":{"apk_path":"C:\\Users\\Vigneshwaran G\\Downloads\\application-16cd461a-950b-4e50-9c7c-367848657e8a.apk","device_id":"emulator-5554"},"success":true,"error":null}}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Mobile Prompts Domain - Enhanced for Industry Level Standards
3
+ */
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ /**
6
+ * Register all mobile prompts
7
+ */
8
+ export declare function registerMobilePrompts(server: McpServer): void;
@@ -0,0 +1,96 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Register all mobile prompts
4
+ */
5
+ export function registerMobilePrompts(server) {
6
+ // Prompt: install_only
7
+ server.prompt("install_only", {
8
+ apk_name: z.string().optional().describe("Name of the APK file to install (must be in artifacts/apks)"),
9
+ }, ({ apk_name }) => ({
10
+ messages: [{
11
+ role: "user",
12
+ content: {
13
+ type: "text",
14
+ text: `
15
+ ## SYSTEM ROLE
16
+ You are the **Mobile Automation Engine**. Execute deterministic installation workflows.
17
+
18
+ ## TASK: INSTALL_ONLY_FLOW
19
+ 1. **Check Device**: Call \`adb_devices\`.
20
+ - IF empty -> Call \`list_avds\` -> Call \`start_emulator\` -> **Wait 30s** (\`wait\`).
21
+ 2. **Install**: Call \`install_apk\` with "D:\\Automation\\artifacts\\apks\\${apk_name || "<latest_apk>.apk"}"
22
+ 3. **Verify**: Call \`list_packages\` to confirm.
23
+ `
24
+ }
25
+ }]
26
+ }));
27
+ // Prompt: mobile_smoke_test (CRITICAL)
28
+ server.prompt("mobile_smoke_test", {
29
+ apk_name: z.string().optional().describe("Name of the APK file to install"),
30
+ }, ({ apk_name }) => ({
31
+ messages: [{
32
+ role: "user",
33
+ content: {
34
+ type: "text",
35
+ text: `
36
+ ## SYSTEM ROLE
37
+ You are the **Mobile Automation Engine**. You orchestrate the full "Day 1" smoke test.
38
+
39
+ ## TASK: MOBILE_SMOKE_TEST
40
+ Install -> Launch -> Verify.
41
+
42
+ ### PHASE 1: PREPARATION
43
+ 1. **Check Device**: Call \`adb_devices\`.
44
+ - IF no device found:
45
+ - Call \`list_avds\`.
46
+ - Call \`start_emulator\` with the first available AVD (or "Medium_Phone_API_36.1").
47
+ - **Wait 30s** (\`wait\`) for boot, then call \`adb_devices\` again.
48
+
49
+ ### PHASE 2: INSTALLATION
50
+ 2. **Install APK**: Call \`install_apk\` with "D:\\Automation\\artifacts\\apks\\${apk_name || "<latest>"}"
51
+
52
+ ### PHASE 3: EXECUTION
53
+ 3. **Launch App**: Call \`launch_app\` using package "com.hashhealth".
54
+ 4. **Stabilize (Smart)**: Call \`wait_for_text\` with "Hash Health" or "Login".
55
+
56
+ ### PHASE 4: EVIDENCE & ASSERTION
57
+ 5. **Smart Click**: Call \`smart_click\` with action text like "Get Started".
58
+ 6. **Evidence**: Call \`take_screenshot\`.
59
+ `
60
+ }
61
+ }]
62
+ }));
63
+ // Prompt: launch_and_check (AUTO-LAUNCH EMULATOR)
64
+ server.prompt("launch_and_check", {}, () => ({
65
+ messages: [{
66
+ role: "user",
67
+ content: {
68
+ type: "text",
69
+ text: `
70
+ ## SYSTEM ROLE
71
+ You are the **Mobile Automation Engine**.
72
+
73
+ ## TASK: LAUNCH_AND_CHECK
74
+ This is an **Automated Action**. If the emulator is off, you MUST turn it on.
75
+
76
+ ### EXECUTION PLAN
77
+ 1. **Environment Check**:
78
+ - Call \`adb_devices\`.
79
+ - IF empty:
80
+ - Call \`list_avds\`.
81
+ - Call \`start_emulator\` (using "Medium_Phone_API_36.1" as priority).
82
+ - **Wait 35s** (\`wait\`) for full Android boot.
83
+ - Call \`adb_devices\` again to confirm.
84
+
85
+ 2. **Launch**:
86
+ - Call \`launch_app\` with package "com.hashhealth".
87
+
88
+ 3. **Verify (Smart)**:
89
+ - Call \`wait_for_text\` with key app text from "mobile://screens/home".
90
+ - Call \`take_screenshot\`.
91
+ - Assert focus on "com.hashhealth".
92
+ `
93
+ }
94
+ }]
95
+ }));
96
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Mobile Resources Domain
3
+ * Static configuration and ground-truth context
4
+ */
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ /**
7
+ * Register all mobile resources
8
+ */
9
+ export declare function registerMobileResources(server: McpServer): void;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Register all mobile resources
3
+ */
4
+ export function registerMobileResources(server) {
5
+ // Resource: App Config
6
+ server.resource("app_config", "mobile://app/config", async (uri) => ({
7
+ contents: [{
8
+ uri: uri.href,
9
+ text: JSON.stringify({
10
+ package: "com.hashhealth",
11
+ launch_activity: ".MainActivity",
12
+ timeouts: {
13
+ install: 30000,
14
+ launch: 5000
15
+ }
16
+ }, null, 2)
17
+ }]
18
+ }));
19
+ // Resource: Device Config
20
+ server.resource("device_config", "mobile://device/config", async (uri) => ({
21
+ contents: [{
22
+ uri: uri.href,
23
+ text: JSON.stringify({
24
+ default_emulator: "Medium_Phone_API_36.1",
25
+ adb_serial: "emulator-5554"
26
+ }, null, 2)
27
+ }]
28
+ }));
29
+ // Resource: Home Screen Map (Refined for Orchestration)
30
+ server.resource("home_screen", "mobile://screens/home", async (uri) => ({
31
+ contents: [{
32
+ uri: uri.href,
33
+ text: JSON.stringify({
34
+ screen: "home",
35
+ description: "Main dashboard after login",
36
+ selectors: {
37
+ get_started: "Get Started",
38
+ login_button: "Login",
39
+ signup_link: "Sign Up",
40
+ skip_intro: "Skip",
41
+ main_nav: "com.hashhealth:id/nav_view"
42
+ },
43
+ anchors: {
44
+ toolbar: { x: 540, y: 120 },
45
+ primary_cta: { x: 540, y: 1750 }
46
+ }
47
+ }, null, 2)
48
+ }]
49
+ }));
50
+ // Resource: App State Map
51
+ server.resource("app_states", "mobile://app/states", async (uri) => ({
52
+ contents: [{
53
+ uri: uri.href,
54
+ text: JSON.stringify({
55
+ initial_load: {
56
+ expected_text: "Welcome to Hash Health",
57
+ next_action: "smart_click('Get Started')"
58
+ },
59
+ login_screen: {
60
+ expected_text: "Sign In",
61
+ fields: ["email", "password"]
62
+ }
63
+ }, null, 2)
64
+ }]
65
+ }));
66
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Mobile Automation MCP Server
3
+ * Entry point for the MCP server that provides mobile automation tools
4
+ */
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ /**
7
+ * Creates and configures the MCP Server instance
8
+ * Registers all tools, resources, and prompts
9
+ */
10
+ export declare function createMcpServer(): McpServer;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Mobile Automation MCP Server — Dual HTTP Transport
3
+ * Using Vanilla Express for precise control over SSE and Stateless endpoints.
4
+ */
5
+ import express from "express";
6
+ import cors from "cors";
7
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
8
+ import { createMcpServer } from "./server.js";
9
+ import { MCP_API_KEY } from "./config/env.js";
10
+ import crypto from "crypto";
11
+ const PORT = 7000;
12
+ const app = express();
13
+ app.use(cors());
14
+ app.use(express.json());
15
+ if (!MCP_API_KEY) {
16
+ console.warn("WARNING: MCP_API_KEY is not set. Authentication will fail.");
17
+ }
18
+ // ── 1. Stateful Setup (for /mcp/sse) ──────────────────────────────────────
19
+ const statefulTransport = new StreamableHTTPServerTransport({
20
+ sessionIdGenerator: () => crypto.randomUUID(),
21
+ });
22
+ const statefulServer = createMcpServer();
23
+ statefulServer.connect(statefulTransport).catch(console.error);
24
+ /**
25
+ * Authentication Helper
26
+ */
27
+ function isAuthorized(req) {
28
+ const apiKey = req.headers["x-mcp-key"] || req.query.apiKey;
29
+ return !!(apiKey && apiKey === MCP_API_KEY);
30
+ }
31
+ /**
32
+ * JSON-RPC Error Formatter
33
+ */
34
+ function sendJsonRpcError(res, code, message, id = null) {
35
+ res.status(200).json({
36
+ jsonrpc: "2.0",
37
+ error: { code, message },
38
+ id: id
39
+ });
40
+ }
41
+ // ── ENDPOINT: /mcp/sse (Stateful SSE for Remote Laptop) ───────────────────
42
+ app.get("/mcp/sse", async (req, res) => {
43
+ if (!isAuthorized(req)) {
44
+ res.status(401).end("Unauthorized");
45
+ return;
46
+ }
47
+ await statefulTransport.handleRequest(req, res);
48
+ });
49
+ app.post("/mcp/sse", async (req, res) => {
50
+ if (!isAuthorized(req)) {
51
+ sendJsonRpcError(res, -32000, "Unauthorized", req.body?.id);
52
+ return;
53
+ }
54
+ await statefulTransport.handleRequest(req, res, req.body);
55
+ });
56
+ // ── ENDPOINT: /mcp (Stateless POST for Local Proxy Client) ────────────────
57
+ app.post("/mcp", async (req, res) => {
58
+ const requestId = req.body?.id;
59
+ if (!isAuthorized(req)) {
60
+ sendJsonRpcError(res, -32000, "Unauthorized", requestId);
61
+ return;
62
+ }
63
+ try {
64
+ const statelessTransport = new StreamableHTTPServerTransport({
65
+ sessionIdGenerator: undefined,
66
+ });
67
+ const statelessServer = createMcpServer();
68
+ await statelessServer.connect(statelessTransport);
69
+ // Handle the stateless JSON-RPC request
70
+ await statelessTransport.handleRequest(req, res, req.body);
71
+ res.on("close", () => {
72
+ statelessTransport.close();
73
+ statelessServer.close();
74
+ });
75
+ }
76
+ catch (error) {
77
+ console.error("Stateless Message Error:", error);
78
+ if (!res.headersSent) {
79
+ sendJsonRpcError(res, -32603, error.message || "Internal Server Error", requestId);
80
+ }
81
+ }
82
+ });
83
+ // ── Cleanup Helper ──
84
+ app.delete("/mcp/sse", async (req, res) => {
85
+ await statefulTransport.handleRequest(req, res);
86
+ });
87
+ app.listen(PORT, "0.0.0.0", () => {
88
+ console.log(`HTTP MCP server running on http://0.0.0.0:${PORT}`);
89
+ console.log(`- Local PC Proxy: http://localhost:7000/mcp`);
90
+ console.log(`- Remote Laptop: http://PC_IP:7000/mcp/sse`);
91
+ });
92
+ process.on("SIGINT", () => {
93
+ process.exit(0);
94
+ });
package/dist/server.js ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Mobile Automation MCP Server
3
+ * Entry point for the MCP server that provides mobile automation tools
4
+ */
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+ // Import tool domain registration functions
8
+ import { registerDeviceTools } from "./tools/device.tools.js";
9
+ import { registerApkTools } from "./tools/apk.tools.js";
10
+ import { registerAppTools } from "./tools/app.tools.js";
11
+ import { registerEvidenceTools } from "./tools/evidence.tools.js";
12
+ import { registerAppiumTools } from "./tools/appium.tools.js";
13
+ import { registerSmartTools } from "./tools/smart.tools.js";
14
+ import { registerUtilityTools } from "./tools/utility.tools.js";
15
+ // Import control plane registration functions
16
+ import { registerMobileResources } from "./resources/mobile.resources.js";
17
+ import { registerMobilePrompts } from "./prompts/mobile.prompts.js";
18
+ import { SERVER_NAME, SERVER_VERSION } from "./config/env.js";
19
+ /**
20
+ * Creates and configures the MCP Server instance
21
+ * Registers all tools, resources, and prompts
22
+ */
23
+ export function createMcpServer() {
24
+ // Create server instance
25
+ const server = new McpServer({
26
+ name: SERVER_NAME,
27
+ version: SERVER_VERSION,
28
+ });
29
+ // Register all tool domains
30
+ // Device Tools: adb_devices, check_device_online, get_device_info
31
+ registerDeviceTools(server);
32
+ // APK Tools: install_apk, uninstall_app, clear_app_data, list_packages, download_apk
33
+ registerApkTools(server);
34
+ // App Tools: launch_app, launch_activity, force_stop_app, open_deep_link, open_url_in_browser
35
+ registerAppTools(server);
36
+ // Evidence Tools: take_screenshot, get_logcat, get_page_source
37
+ registerEvidenceTools(server);
38
+ // Appium Tools: tap, type_text, swipe, press_key, long_press
39
+ registerAppiumTools(server);
40
+ // Smart Tools: smart_click, wait_for_text, get_ui_inventory
41
+ registerSmartTools(server);
42
+ // Utility Tools: wait
43
+ registerUtilityTools(server);
44
+ // Register Control Plane (Resources & Prompts)
45
+ registerMobileResources(server);
46
+ registerMobilePrompts(server);
47
+ return server;
48
+ }
49
+ // Start the server with stdio transport if running directly
50
+ // This maintains the original behavior for local stdio usage
51
+ async function main() {
52
+ const server = createMcpServer();
53
+ const transport = new StdioServerTransport();
54
+ await server.connect(transport);
55
+ }
56
+ // Only run main if called directly (not imported)
57
+ import { fileURLToPath } from "url";
58
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
59
+ main().catch(console.error);
60
+ }
@@ -0,0 +1,5 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ /**
3
+ * Register all APK management tools
4
+ */
5
+ export declare function registerApkTools(server: McpServer): void;