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 +43 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +31 -0
- package/dist/config/env.d.ts +13 -0
- package/dist/config/env.js +15 -0
- package/dist/config/paths.d.ts +8 -0
- package/dist/config/paths.js +22 -0
- package/dist/logs/2026-02-09.log +8 -0
- package/dist/prompts/mobile.prompts.d.ts +8 -0
- package/dist/prompts/mobile.prompts.js +96 -0
- package/dist/resources/mobile.resources.d.ts +9 -0
- package/dist/resources/mobile.resources.js +66 -0
- package/dist/server.d.ts +10 -0
- package/dist/server.http.d.ts +1 -0
- package/dist/server.http.js +94 -0
- package/dist/server.js +60 -0
- package/dist/tools/apk.tools.d.ts +5 -0
- package/dist/tools/apk.tools.js +185 -0
- package/dist/tools/app.tools.d.ts +5 -0
- package/dist/tools/app.tools.js +114 -0
- package/dist/tools/appium.tools.d.ts +5 -0
- package/dist/tools/appium.tools.js +133 -0
- package/dist/tools/device.tools.d.ts +5 -0
- package/dist/tools/device.tools.js +110 -0
- package/dist/tools/evidence.tools.d.ts +5 -0
- package/dist/tools/evidence.tools.js +118 -0
- package/dist/tools/smart.tools.d.ts +5 -0
- package/dist/tools/smart.tools.js +127 -0
- package/dist/tools/utility.tools.d.ts +5 -0
- package/dist/tools/utility.tools.js +25 -0
- package/dist/utils/exec.d.ts +33 -0
- package/dist/utils/exec.js +69 -0
- package/dist/utils/logger.d.ts +18 -0
- package/dist/utils/logger.js +53 -0
- package/dist/utils/validator.d.ts +9 -0
- package/dist/utils/validator.js +34 -0
- package/package.json +41 -0
- package/proxy_client.js +101 -0
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
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,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
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -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
|
+
}
|