mobile-mcp-server 1.0.6 → 1.1.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/dist/config/env.d.ts +0 -1
- package/dist/config/env.js +0 -2
- package/dist/config/paths.d.ts +0 -3
- package/dist/config/paths.js +6 -12
- package/dist/logs/2026-02-10.log +1 -0
- package/dist/prompts/connection.prompts.d.ts +6 -0
- package/dist/prompts/connection.prompts.js +42 -0
- package/dist/server.d.ts +2 -10
- package/dist/server.js +25 -43
- package/dist/server_http.js +60 -0
- package/dist/tools/apk.tools.js +10 -26
- package/dist/tools/appium.tools.js +0 -4
- package/dist/tools/device.tools.js +15 -25
- package/dist/tools/evidence.tools.js +12 -24
- package/dist/tools/ios.tools.d.ts +5 -0
- package/dist/tools/ios.tools.js +123 -0
- package/dist/utils/exec.d.ts +0 -13
- package/dist/utils/exec.js +1 -25
- package/package.json +20 -17
- package/README.md +0 -43
- package/dist/bin.d.ts +0 -2
- package/dist/bin.js +0 -31
- package/dist/prompts/mobile.prompts.d.ts +0 -8
- package/dist/prompts/mobile.prompts.js +0 -96
- package/dist/resources/mobile.resources.d.ts +0 -9
- package/dist/resources/mobile.resources.js +0 -66
- package/dist/server.http.js +0 -94
- package/dist/tools/smart.tools.d.ts +0 -5
- package/dist/tools/smart.tools.js +0 -127
- package/dist/tools/utility.tools.d.ts +0 -5
- package/dist/tools/utility.tools.js +0 -25
- package/dist/utils/validator.d.ts +0 -9
- package/dist/utils/validator.js +0 -34
- package/proxy_client.js +0 -140
- /package/dist/{server.http.d.ts → server_http.d.ts} +0 -0
package/dist/config/env.d.ts
CHANGED
package/dist/config/env.js
CHANGED
|
@@ -11,5 +11,3 @@ export const SCREENSHOT_TIMEOUT = 10000;
|
|
|
11
11
|
export const APPIUM_HOST = process.env.APPIUM_HOST || "127.0.0.1";
|
|
12
12
|
export const APPIUM_PORT = parseInt(process.env.APPIUM_PORT || "4723", 10);
|
|
13
13
|
export const LOG_LEVEL = process.env.LOG_LEVEL || "info";
|
|
14
|
-
// Security
|
|
15
|
-
export const MCP_API_KEY = process.env.MCP_API_KEY || "hash2026";
|
package/dist/config/paths.d.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
export declare const ANDROID_HOME: string;
|
|
2
2
|
export declare const ADB_PATH: string;
|
|
3
|
-
export declare const EMULATOR_PATH: string;
|
|
4
|
-
export declare const ARTIFACTS_ROOT: string;
|
|
5
3
|
export declare const LOG_DIR: string;
|
|
6
4
|
export declare const SCREENSHOT_DIR: string;
|
|
7
|
-
export declare const APK_DIR: string;
|
|
8
5
|
export declare const DOWNLOADS_DIR: string;
|
package/dist/config/paths.js
CHANGED
|
@@ -7,16 +7,10 @@ import { dirname } from "path";
|
|
|
7
7
|
const __filename = fileURLToPath(import.meta.url);
|
|
8
8
|
const __dirname = dirname(__filename);
|
|
9
9
|
// Android SDK paths
|
|
10
|
-
export const ANDROID_HOME = process.env.ANDROID_HOME || "
|
|
10
|
+
export const ANDROID_HOME = process.env.ANDROID_HOME || "D:\\Android";
|
|
11
11
|
export const ADB_PATH = process.env.ADB_PATH || join(ANDROID_HOME, "platform-tools", "adb.exe");
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
export const
|
|
15
|
-
export const
|
|
16
|
-
export const
|
|
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;
|
|
12
|
+
// Project directories
|
|
13
|
+
const PROJECT_ROOT = join(__dirname, "..");
|
|
14
|
+
export const LOG_DIR = join(PROJECT_ROOT, "logs");
|
|
15
|
+
export const SCREENSHOT_DIR = join(PROJECT_ROOT, "screenshots");
|
|
16
|
+
export const DOWNLOADS_DIR = join(PROJECT_ROOT, "downloads");
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"timestamp":"2026-02-10T13:08:39.593Z","level":"info","message":"TOOL: adb_devices","data":{"input":{},"success":true,"error":null}}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export function registerConnectionPrompts(server) {
|
|
2
|
+
server.prompt("connect_mobile_wifi", "Workflow to connect a physical Android device via WiFi", async () => ({
|
|
3
|
+
messages: [{
|
|
4
|
+
role: "user",
|
|
5
|
+
content: {
|
|
6
|
+
type: "text",
|
|
7
|
+
text: `Connect to my Android device via WiFi.
|
|
8
|
+
|
|
9
|
+
**Prerequisites**
|
|
10
|
+
1. Connect phone and computer to the same WiFi.
|
|
11
|
+
2. Enable "Wireless Debugging" on the phone.
|
|
12
|
+
3. Obtain the device IP address (e.g., 192.168.1.50).
|
|
13
|
+
|
|
14
|
+
**Execution Steps**
|
|
15
|
+
1. Request the Device IP if missing.
|
|
16
|
+
2. Call \`adb_connect_wifi({ ip: "IP_ADDRESS" })\`.
|
|
17
|
+
3. Verify connection with \`check_device_online\`.
|
|
18
|
+
|
|
19
|
+
**Example**
|
|
20
|
+
User: "Connect to 192.168.1.45"
|
|
21
|
+
Assistant: Calls \`adb_connect_wifi("192.168.1.45")\`
|
|
22
|
+
`
|
|
23
|
+
}
|
|
24
|
+
}]
|
|
25
|
+
}));
|
|
26
|
+
server.prompt("setup_physical_device", "Guide to enable Developer Options and USB Debugging", async () => ({
|
|
27
|
+
messages: [{
|
|
28
|
+
role: "user",
|
|
29
|
+
content: {
|
|
30
|
+
type: "text",
|
|
31
|
+
text: `Set up Android device for automation.
|
|
32
|
+
|
|
33
|
+
1. Open **Settings > About Phone**.
|
|
34
|
+
2. Tap **Build Number** 7 times to enable Developer Options.
|
|
35
|
+
3. Open **Settings > System > Developer Options**.
|
|
36
|
+
4. Enable **USB Debugging**.
|
|
37
|
+
5. Enable **Wireless Debugging** to view the IP address.
|
|
38
|
+
`
|
|
39
|
+
}
|
|
40
|
+
}]
|
|
41
|
+
}));
|
|
42
|
+
}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,10 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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;
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
export {};
|
package/dist/server.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
/**
|
|
2
3
|
* Mobile Automation MCP Server
|
|
3
4
|
* Entry point for the MCP server that provides mobile automation tools
|
|
@@ -10,51 +11,32 @@ import { registerApkTools } from "./tools/apk.tools.js";
|
|
|
10
11
|
import { registerAppTools } from "./tools/app.tools.js";
|
|
11
12
|
import { registerEvidenceTools } from "./tools/evidence.tools.js";
|
|
12
13
|
import { registerAppiumTools } from "./tools/appium.tools.js";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
// Import control plane registration functions
|
|
16
|
-
import { registerMobileResources } from "./resources/mobile.resources.js";
|
|
17
|
-
import { registerMobilePrompts } from "./prompts/mobile.prompts.js";
|
|
14
|
+
import { registerIosTools } from "./tools/ios.tools.js";
|
|
15
|
+
import { registerConnectionPrompts } from "./prompts/connection.prompts.js";
|
|
18
16
|
import { SERVER_NAME, SERVER_VERSION } from "./config/env.js";
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
17
|
+
// Create server instance
|
|
18
|
+
const server = new McpServer({
|
|
19
|
+
name: SERVER_NAME,
|
|
20
|
+
version: SERVER_VERSION,
|
|
21
|
+
});
|
|
22
|
+
// Register all tool domains
|
|
23
|
+
// Device Tools: adb_devices, check_device_online, get_device_info
|
|
24
|
+
registerDeviceTools(server);
|
|
25
|
+
// APK Tools: install_apk, uninstall_app, clear_app_data, list_packages, download_apk
|
|
26
|
+
registerApkTools(server);
|
|
27
|
+
// App Tools: launch_app, launch_activity, force_stop_app, open_deep_link, open_url_in_browser
|
|
28
|
+
registerAppTools(server);
|
|
29
|
+
// Evidence Tools: take_screenshot, get_logcat, get_page_source
|
|
30
|
+
registerEvidenceTools(server);
|
|
31
|
+
// Appium Tools: tap, type_text, swipe, press_key, long_press
|
|
32
|
+
registerAppiumTools(server);
|
|
33
|
+
// iOS Tools: list_devices, boot_device, install_app, launch_app, screenshot
|
|
34
|
+
registerIosTools(server);
|
|
35
|
+
// Prompts: connect_mobile_wifi, setup_physical_device
|
|
36
|
+
registerConnectionPrompts(server);
|
|
37
|
+
// Start the server with stdio transport
|
|
51
38
|
async function main() {
|
|
52
|
-
const server = createMcpServer();
|
|
53
39
|
const transport = new StdioServerTransport();
|
|
54
40
|
await server.connect(transport);
|
|
55
41
|
}
|
|
56
|
-
|
|
57
|
-
import { fileURLToPath } from "url";
|
|
58
|
-
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
59
|
-
main().catch(console.error);
|
|
60
|
-
}
|
|
42
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import cors from "cors";
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
5
|
+
// Import tool domain registration functions
|
|
6
|
+
import { registerDeviceTools } from "./tools/device.tools.js";
|
|
7
|
+
import { registerApkTools } from "./tools/apk.tools.js";
|
|
8
|
+
import { registerAppTools } from "./tools/app.tools.js";
|
|
9
|
+
import { registerEvidenceTools } from "./tools/evidence.tools.js";
|
|
10
|
+
import { registerAppiumTools } from "./tools/appium.tools.js";
|
|
11
|
+
import { SERVER_NAME, SERVER_VERSION } from "./config/env.js";
|
|
12
|
+
const app = express();
|
|
13
|
+
const PORT = 7000;
|
|
14
|
+
app.use(cors());
|
|
15
|
+
// app.use(express.json()); // Optional, handled by transport usually
|
|
16
|
+
// Create server instance
|
|
17
|
+
const server = new McpServer({
|
|
18
|
+
name: SERVER_NAME,
|
|
19
|
+
version: SERVER_VERSION,
|
|
20
|
+
});
|
|
21
|
+
// Register all tool domains
|
|
22
|
+
registerDeviceTools(server);
|
|
23
|
+
registerApkTools(server);
|
|
24
|
+
registerAppTools(server);
|
|
25
|
+
registerEvidenceTools(server);
|
|
26
|
+
registerAppiumTools(server);
|
|
27
|
+
// Store transports mapping sessionId -> transport
|
|
28
|
+
const transports = new Map();
|
|
29
|
+
// SSE Endpoint
|
|
30
|
+
app.get("/mcp", async (req, res) => {
|
|
31
|
+
console.log("New SSE connection attempt");
|
|
32
|
+
const transport = new SSEServerTransport("/message", res);
|
|
33
|
+
// SDK generates sessionId
|
|
34
|
+
const sessionId = transport.sessionId;
|
|
35
|
+
transports.set(sessionId, transport);
|
|
36
|
+
req.on("close", () => {
|
|
37
|
+
console.log(`SSE connection closed: ${sessionId}`);
|
|
38
|
+
transports.delete(sessionId);
|
|
39
|
+
});
|
|
40
|
+
await server.connect(transport);
|
|
41
|
+
});
|
|
42
|
+
// Message Endpoint (POST)
|
|
43
|
+
app.post("/message", async (req, res) => {
|
|
44
|
+
const sessionId = req.query.sessionId;
|
|
45
|
+
if (!sessionId) {
|
|
46
|
+
res.status(400).send("Missing sessionId");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const transport = transports.get(sessionId);
|
|
50
|
+
if (!transport) {
|
|
51
|
+
res.status(404).send("Session not found");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
await transport.handlePostMessage(req, res);
|
|
55
|
+
});
|
|
56
|
+
// Start Server
|
|
57
|
+
app.listen(PORT, "0.0.0.0", () => {
|
|
58
|
+
console.log(`Mobile MCP Server (HTTP/SSE) running on http://0.0.0.0:${PORT}`);
|
|
59
|
+
console.log(`SSE Endpoint: http://localhost:${PORT}/mcp`);
|
|
60
|
+
});
|
package/dist/tools/apk.tools.js
CHANGED
|
@@ -6,10 +6,9 @@ import { z } from "zod";
|
|
|
6
6
|
import { adb } from "../utils/exec.js";
|
|
7
7
|
import { logToolExecution } from "../utils/logger.js";
|
|
8
8
|
import { APK_INSTALL_TIMEOUT } from "../config/env.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { dirname } from "path";
|
|
9
|
+
import { SCREENSHOT_DIR } from "../config/paths.js";
|
|
10
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
11
|
+
import { join } from "path";
|
|
13
12
|
/**
|
|
14
13
|
* Register all APK management tools
|
|
15
14
|
*/
|
|
@@ -20,17 +19,6 @@ export function registerApkTools(server) {
|
|
|
20
19
|
device_id: z.string().optional().describe("Target device ID (optional if only one device)"),
|
|
21
20
|
reinstall: z.boolean().optional().describe("Reinstall the app, keeping data (default: false)"),
|
|
22
21
|
}, async ({ apk_path, device_id, reinstall }) => {
|
|
23
|
-
// Validate that we are only installing from allowed directories is good practice,
|
|
24
|
-
// but for now we just need to ensure the path is safe if we were reading it.
|
|
25
|
-
// Since ADB installs it, we just pass the path.
|
|
26
|
-
// However, the user said "Tools must accept explicit paths, validate allowed directories".
|
|
27
|
-
// So we should strictly check if the APK is in our APK_DIR?
|
|
28
|
-
// The prompt says "Tools must accept explicit paths", which usually implies input validation.
|
|
29
|
-
// I will add a check to warn or block if it's not in ARTIFACTS_ROOT, but for flexibility
|
|
30
|
-
// (maybe installing a system apk?) I might leave it open or just warn.
|
|
31
|
-
// Given "Strict File System Rules", I'll enforce it must be within ARTIFACTS_ROOT.
|
|
32
|
-
// For now, let's trust the user's explicit path but ensure it exists?
|
|
33
|
-
// The `adb install` will fail if it doesn't exist.
|
|
34
22
|
const deviceFlag = device_id ? `-s ${device_id}` : "";
|
|
35
23
|
const reinstallFlag = reinstall ? "-r" : "";
|
|
36
24
|
const result = await adb(`${deviceFlag} install ${reinstallFlag} "${apk_path}"`, { timeout: APK_INSTALL_TIMEOUT });
|
|
@@ -126,14 +114,10 @@ export function registerApkTools(server) {
|
|
|
126
114
|
install: z.boolean().optional().describe("Install APK after download (default: true)"),
|
|
127
115
|
device_id: z.string().optional().describe("Target device ID for installation"),
|
|
128
116
|
}, async ({ url, filename, install = true, device_id }) => {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
// but it might not. Safest to keep it?
|
|
134
|
-
// But strict rules say D:\Automation\artifacts\apks exists.
|
|
135
|
-
// I'll keep the logic but use the validated path.
|
|
136
|
-
await mkdir(dirname(finalApkPath), { recursive: true });
|
|
117
|
+
const downloadsDir = join(SCREENSHOT_DIR, "..", "downloads");
|
|
118
|
+
await mkdir(downloadsDir, { recursive: true });
|
|
119
|
+
const apkFilename = filename || `downloaded_${Date.now()}.apk`;
|
|
120
|
+
const apkPath = join(downloadsDir, apkFilename);
|
|
137
121
|
try {
|
|
138
122
|
const response = await fetch(url);
|
|
139
123
|
if (!response.ok) {
|
|
@@ -149,15 +133,15 @@ export function registerApkTools(server) {
|
|
|
149
133
|
};
|
|
150
134
|
}
|
|
151
135
|
const arrayBuffer = await response.arrayBuffer();
|
|
152
|
-
await writeFile(
|
|
136
|
+
await writeFile(apkPath, Buffer.from(arrayBuffer));
|
|
153
137
|
let installResult = null;
|
|
154
138
|
if (install) {
|
|
155
139
|
const deviceFlag = device_id ? `-s ${device_id}` : "";
|
|
156
|
-
installResult = await adb(`${deviceFlag} install -r "${
|
|
140
|
+
installResult = await adb(`${deviceFlag} install -r "${apkPath}"`, { timeout: APK_INSTALL_TIMEOUT });
|
|
157
141
|
}
|
|
158
142
|
const output = {
|
|
159
143
|
success: true,
|
|
160
|
-
downloaded_to:
|
|
144
|
+
downloaded_to: apkPath,
|
|
161
145
|
file_size_mb: (arrayBuffer.byteLength / (1024 * 1024)).toFixed(2),
|
|
162
146
|
installed: install ? (installResult?.success && installResult?.stdout?.includes("Success")) : false,
|
|
163
147
|
install_output: install ? installResult?.stdout?.trim() : null,
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Appium UI Automation Tools Domain
|
|
3
3
|
* Tools for direct UI interaction (tap, type, swipe, etc.)
|
|
4
|
-
*
|
|
5
|
-
* NOTE: This module currently uses ADB commands (`input tap`, `input swipe`)
|
|
6
|
-
* to simulate Appium interactions. This provides a lightweight, dependency-free
|
|
7
|
-
* alternative to running a full Appium node for basic coordinate-based automation.
|
|
8
4
|
*/
|
|
9
5
|
import { z } from "zod";
|
|
10
6
|
import { adb } from "../utils/exec.js";
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Tools for device discovery and status checking
|
|
4
4
|
*/
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { adb
|
|
6
|
+
import { adb } from "../utils/exec.js";
|
|
7
7
|
import { logToolExecution } from "../utils/logger.js";
|
|
8
8
|
/**
|
|
9
9
|
* Register all device tools
|
|
@@ -77,32 +77,22 @@ export function registerDeviceTools(server) {
|
|
|
77
77
|
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
|
|
78
78
|
};
|
|
79
79
|
});
|
|
80
|
-
// Tool:
|
|
81
|
-
server.tool("
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
// Tool: adb_connect_wifi
|
|
81
|
+
server.tool("adb_connect_wifi", "Connect to an Android device via WiFi (requires IP address)", {
|
|
82
|
+
ip: z.string().describe("IP address of the device (e.g., 192.168.1.50)"),
|
|
83
|
+
port: z.number().optional().describe("Port number (default: 5555)"),
|
|
84
|
+
}, async ({ ip, port = 5555 }) => {
|
|
85
|
+
const result = await adb(`connect ${ip}:${port}`);
|
|
86
|
+
// Check output for success
|
|
87
|
+
const success = result.success && (result.stdout.includes("connected to") || result.stdout.includes("already connected"));
|
|
84
88
|
const output = {
|
|
85
|
-
success:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
await logToolExecution("list_avds", {}, output);
|
|
91
|
-
return {
|
|
92
|
-
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
|
|
93
|
-
};
|
|
94
|
-
});
|
|
95
|
-
// Tool: start_emulator
|
|
96
|
-
server.tool("start_emulator", "Start a specific Android emulator (AVD) in the background", {
|
|
97
|
-
avd_name: z.string().describe("Name of the AVD to start (get from list_avds)"),
|
|
98
|
-
}, async ({ avd_name }) => {
|
|
99
|
-
await startEmulatorBackground(avd_name);
|
|
100
|
-
const output = {
|
|
101
|
-
success: true,
|
|
102
|
-
message: `Emulator ${avd_name} starting in background...`,
|
|
103
|
-
avd_name: avd_name,
|
|
89
|
+
success: success,
|
|
90
|
+
action: "adb_connect",
|
|
91
|
+
target: `${ip}:${port}`,
|
|
92
|
+
message: result.stdout.trim(),
|
|
93
|
+
error: result.error
|
|
104
94
|
};
|
|
105
|
-
await logToolExecution("
|
|
95
|
+
await logToolExecution("adb_connect_wifi", { ip, port }, output);
|
|
106
96
|
return {
|
|
107
97
|
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
|
|
108
98
|
};
|
|
@@ -5,10 +5,9 @@
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { adb } from "../utils/exec.js";
|
|
7
7
|
import { logToolExecution } from "../utils/logger.js";
|
|
8
|
-
import { SCREENSHOT_DIR
|
|
9
|
-
import { validateArtifactPath } from "../utils/validator.js";
|
|
8
|
+
import { SCREENSHOT_DIR } from "../config/paths.js";
|
|
10
9
|
import { writeFile, mkdir, readFile } from "fs/promises";
|
|
11
|
-
import { dirname } from "path";
|
|
10
|
+
import { join, dirname } from "path";
|
|
12
11
|
/**
|
|
13
12
|
* Register all evidence capture tools
|
|
14
13
|
*/
|
|
@@ -16,21 +15,12 @@ export function registerEvidenceTools(server) {
|
|
|
16
15
|
// Tool: take_screenshot
|
|
17
16
|
server.tool("take_screenshot", "Capture a screenshot from the device and display it inline (also saves to file)", {
|
|
18
17
|
filename: z.string().optional().describe("Screenshot filename (default: timestamp.png)"),
|
|
19
|
-
output_path: z.string().optional().describe("Full path to save (overrides default screenshots dir
|
|
18
|
+
output_path: z.string().optional().describe("Full path to save (overrides default screenshots dir)"),
|
|
20
19
|
device_id: z.string().optional().describe("Target device ID"),
|
|
21
20
|
}, async ({ filename, output_path, device_id }) => {
|
|
22
21
|
const deviceFlag = device_id ? `-s ${device_id}` : "";
|
|
23
22
|
const defaultFilename = `screenshot_${Date.now()}.png`;
|
|
24
|
-
|
|
25
|
-
// If filename is provided, it must be within SCREENSHOT_DIR.
|
|
26
|
-
// We use validateArtifactPath which handles both absolute and relative-to-root logic.
|
|
27
|
-
let finalPath;
|
|
28
|
-
if (output_path) {
|
|
29
|
-
finalPath = validateArtifactPath(output_path, SCREENSHOT_DIR);
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
finalPath = validateArtifactPath(filename || defaultFilename, SCREENSHOT_DIR);
|
|
33
|
-
}
|
|
23
|
+
const finalPath = output_path || join(SCREENSHOT_DIR, filename || defaultFilename);
|
|
34
24
|
await mkdir(dirname(finalPath), { recursive: true });
|
|
35
25
|
const tempPath = "/sdcard/mcp_screenshot.png";
|
|
36
26
|
await adb(`${deviceFlag} shell screencap -p ${tempPath}`);
|
|
@@ -68,20 +58,19 @@ export function registerEvidenceTools(server) {
|
|
|
68
58
|
device_id: z.string().optional().describe("Target device ID"),
|
|
69
59
|
filter: z.string().optional().describe("Filter by tag or priority (e.g., 'MyApp:D *:S')"),
|
|
70
60
|
lines: z.number().optional().describe("Number of recent lines to fetch (default: 100)"),
|
|
71
|
-
save_to: z.string().optional().describe("Optional path to save logs to file
|
|
61
|
+
save_to: z.string().optional().describe("Optional path to save logs to file"),
|
|
72
62
|
}, async ({ device_id, filter, lines = 100, save_to }) => {
|
|
73
63
|
const deviceFlag = device_id ? `-s ${device_id}` : "";
|
|
74
64
|
const filterStr = filter ? ` ${filter}` : "";
|
|
75
65
|
const result = await adb(`${deviceFlag} logcat -d -t ${lines}${filterStr}`, { maxBuffer: 5 * 1024 * 1024 });
|
|
76
66
|
if (save_to && result.success) {
|
|
77
|
-
|
|
78
|
-
await
|
|
79
|
-
await writeFile(finalPath, result.stdout);
|
|
67
|
+
await mkdir(dirname(save_to), { recursive: true });
|
|
68
|
+
await writeFile(save_to, result.stdout);
|
|
80
69
|
}
|
|
81
70
|
const output = {
|
|
82
71
|
success: result.success,
|
|
83
72
|
lines_count: result.stdout.split("\n").length,
|
|
84
|
-
saved_to: save_to
|
|
73
|
+
saved_to: save_to || null,
|
|
85
74
|
logs: result.stdout,
|
|
86
75
|
error: result.error,
|
|
87
76
|
};
|
|
@@ -93,20 +82,19 @@ export function registerEvidenceTools(server) {
|
|
|
93
82
|
// Tool: get_page_source
|
|
94
83
|
server.tool("get_page_source", "Dump the current UI hierarchy (XML) from the device", {
|
|
95
84
|
device_id: z.string().optional().describe("Target device ID"),
|
|
96
|
-
save_to: z.string().optional().describe("Optional path to save XML to file
|
|
85
|
+
save_to: z.string().optional().describe("Optional path to save XML to file"),
|
|
97
86
|
}, async ({ device_id, save_to }) => {
|
|
98
87
|
const deviceFlag = device_id ? `-s ${device_id}` : "";
|
|
99
88
|
await adb(`${deviceFlag} shell uiautomator dump /sdcard/ui_dump.xml`);
|
|
100
89
|
const result = await adb(`${deviceFlag} shell cat /sdcard/ui_dump.xml`);
|
|
101
90
|
await adb(`${deviceFlag} shell rm /sdcard/ui_dump.xml`);
|
|
102
91
|
if (save_to && result.success) {
|
|
103
|
-
|
|
104
|
-
await
|
|
105
|
-
await writeFile(finalPath, result.stdout);
|
|
92
|
+
await mkdir(dirname(save_to), { recursive: true });
|
|
93
|
+
await writeFile(save_to, result.stdout);
|
|
106
94
|
}
|
|
107
95
|
const output = {
|
|
108
96
|
success: result.success,
|
|
109
|
-
saved_to: save_to
|
|
97
|
+
saved_to: save_to || null,
|
|
110
98
|
xml: result.stdout,
|
|
111
99
|
error: result.error,
|
|
112
100
|
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* iOS Simulator Tools Domain
|
|
3
|
+
* Tools for managing iOS Simulators via 'xcrun simctl'
|
|
4
|
+
* Priority: Mac First Strategy
|
|
5
|
+
*/
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { run } from "../utils/exec.js";
|
|
8
|
+
import { logToolExecution } from "../utils/logger.js";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import { SCREENSHOT_DIR } from "../config/paths.js";
|
|
11
|
+
/**
|
|
12
|
+
* Register all iOS Simulator tools
|
|
13
|
+
*/
|
|
14
|
+
export function registerIosTools(server) {
|
|
15
|
+
// Tool: ios_list_devices
|
|
16
|
+
server.tool("ios_list_devices", "List all available iOS Simulators", {
|
|
17
|
+
state: z.enum(["shutdown", "booted", "all"]).optional().describe("Filter by state"),
|
|
18
|
+
}, async ({ state = "all" }) => {
|
|
19
|
+
const result = await run("xcrun simctl list devices --json");
|
|
20
|
+
if (!result.success) {
|
|
21
|
+
return {
|
|
22
|
+
content: [{ type: "text", text: `Error listing devices: ${result.stderr}` }],
|
|
23
|
+
isError: true,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const data = JSON.parse(result.stdout);
|
|
28
|
+
let devices = [];
|
|
29
|
+
for (const runtime in data.devices) {
|
|
30
|
+
devices = devices.concat(data.devices[runtime]);
|
|
31
|
+
}
|
|
32
|
+
if (state !== "all") {
|
|
33
|
+
devices = devices.filter((d) => d.state.toLowerCase() === state.toLowerCase());
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
content: [{ type: "text", text: JSON.stringify(devices, null, 2) }],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
return {
|
|
41
|
+
content: [{ type: "text", text: `Error parsing JSON: ${e}` }],
|
|
42
|
+
isError: true,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
// Tool: ios_boot_device
|
|
47
|
+
server.tool("ios_boot_device", "Boot an iOS Simulator", {
|
|
48
|
+
udid: z.string().describe("Device UDID"),
|
|
49
|
+
}, async ({ udid }) => {
|
|
50
|
+
const result = await run(`xcrun simctl boot ${udid}`);
|
|
51
|
+
// If already booted, simctl returns error but that's fine for us usually
|
|
52
|
+
// We can check `result.stderr` for "Unable to boot device in current state: Booted"
|
|
53
|
+
const output = {
|
|
54
|
+
success: result.success || result.stderr.includes("Booted"),
|
|
55
|
+
action: "ios_boot_device",
|
|
56
|
+
udid,
|
|
57
|
+
error: result.success ? null : result.stderr
|
|
58
|
+
};
|
|
59
|
+
await logToolExecution("ios_boot_device", { udid }, output);
|
|
60
|
+
// Open Simulator.app to see the UI
|
|
61
|
+
await run(`open -a Simulator`);
|
|
62
|
+
return {
|
|
63
|
+
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
// Tool: ios_install_app
|
|
67
|
+
server.tool("ios_install_app", "Install an .app bundle to a simulator", {
|
|
68
|
+
udid: z.string().describe("Device UDID"),
|
|
69
|
+
app_path: z.string().describe("Absolute path to .app bundle"),
|
|
70
|
+
}, async ({ udid, app_path }) => {
|
|
71
|
+
const result = await run(`xcrun simctl install ${udid} "${app_path}"`);
|
|
72
|
+
const output = {
|
|
73
|
+
success: result.success,
|
|
74
|
+
action: "ios_install_app",
|
|
75
|
+
udid,
|
|
76
|
+
app_path,
|
|
77
|
+
error: result.error
|
|
78
|
+
};
|
|
79
|
+
await logToolExecution("ios_install_app", { udid, app_path }, output);
|
|
80
|
+
return {
|
|
81
|
+
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
// Tool: ios_launch_app
|
|
85
|
+
server.tool("ios_launch_app", "Launch an installed app on simulator", {
|
|
86
|
+
udid: z.string().describe("Device UDID"),
|
|
87
|
+
bundle_id: z.string().describe("App Bundle ID (e.g. com.hashhealth.ios)"),
|
|
88
|
+
}, async ({ udid, bundle_id }) => {
|
|
89
|
+
const result = await run(`xcrun simctl launch ${udid} ${bundle_id}`);
|
|
90
|
+
const output = {
|
|
91
|
+
success: result.success,
|
|
92
|
+
action: "ios_launch_app",
|
|
93
|
+
udid,
|
|
94
|
+
bundle_id,
|
|
95
|
+
pid: result.stdout.trim(), // capture PID
|
|
96
|
+
error: result.error
|
|
97
|
+
};
|
|
98
|
+
await logToolExecution("ios_launch_app", { udid, bundle_id }, output);
|
|
99
|
+
return {
|
|
100
|
+
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
// Tool: ios_screenshot
|
|
104
|
+
server.tool("ios_screenshot", "Take a screenshot of the simulator", {
|
|
105
|
+
udid: z.string().describe("Device UDID"),
|
|
106
|
+
filename: z.string().optional().describe("Output filename (default: timestamp)"),
|
|
107
|
+
}, async ({ udid, filename }) => {
|
|
108
|
+
const name = filename || `ios_${Date.now()}.png`;
|
|
109
|
+
const path = join(SCREENSHOT_DIR, name);
|
|
110
|
+
const result = await run(`xcrun simctl io ${udid} screenshot "${path}"`);
|
|
111
|
+
const output = {
|
|
112
|
+
success: result.success,
|
|
113
|
+
action: "ios_screenshot",
|
|
114
|
+
udid,
|
|
115
|
+
path,
|
|
116
|
+
error: result.error
|
|
117
|
+
};
|
|
118
|
+
await logToolExecution("ios_screenshot", { udid, filename }, output);
|
|
119
|
+
return {
|
|
120
|
+
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
}
|
package/dist/utils/exec.d.ts
CHANGED
|
@@ -18,16 +18,3 @@ export declare function run(command: string, options?: CommandOptions): Promise<
|
|
|
18
18
|
* Run an ADB command using configured path
|
|
19
19
|
*/
|
|
20
20
|
export declare function adb(args: string, options?: CommandOptions): Promise<CommandResult>;
|
|
21
|
-
/**
|
|
22
|
-
* Run a command in the background (detached)
|
|
23
|
-
* Useful for starting the emulator
|
|
24
|
-
*/
|
|
25
|
-
export declare function spawnBackground(command: string, args: string[]): Promise<void>;
|
|
26
|
-
/**
|
|
27
|
-
* Run an emulator command using configured path
|
|
28
|
-
*/
|
|
29
|
-
export declare function emulator(args: string, options?: CommandOptions): Promise<CommandResult>;
|
|
30
|
-
/**
|
|
31
|
-
* Start the emulator in the background
|
|
32
|
-
*/
|
|
33
|
-
export declare function startEmulatorBackground(avdName: string): Promise<void>;
|