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.
@@ -10,4 +10,3 @@ export declare const APPIUM_HOST: string;
10
10
  export declare const APPIUM_PORT: number;
11
11
  export type LogLevel = "debug" | "info" | "warn" | "error";
12
12
  export declare const LOG_LEVEL: LogLevel;
13
- export declare const MCP_API_KEY: string;
@@ -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";
@@ -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;
@@ -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 || "E:\\Android\\Sdk";
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
- 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;
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,6 @@
1
+ /**
2
+ * Connection Prompts
3
+ * Templates to guide the user through device connection workflows
4
+ */
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ export declare function registerConnectionPrompts(server: McpServer): void;
@@ -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
- * 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;
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 { 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";
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
- * 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
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
- // 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
- }
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
+ });
@@ -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 { DOWNLOADS_DIR } from "../config/paths.js";
10
- import { validateArtifactPath } from "../utils/validator.js";
11
- import { writeFile, mkdir } from "fs/promises";
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
- // FORCE validation of the target path
130
- const safeFilename = filename || `downloaded_${Date.now()}.apk`;
131
- const finalApkPath = validateArtifactPath(safeFilename, DOWNLOADS_DIR);
132
- // We don't need to mkdir logic here if we assume DOWNLOADS_DIR exists,
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(finalApkPath, Buffer.from(arrayBuffer));
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 "${finalApkPath}"`, { timeout: APK_INSTALL_TIMEOUT });
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: finalApkPath,
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, emulator, startEmulatorBackground } from "../utils/exec.js";
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: list_avds
81
- server.tool("list_avds", "List all available Android Virtual Devices (AVDs)", {}, async () => {
82
- const result = await emulator("-list-avds");
83
- const avds = result.stdout.trim().split("\n").filter(line => line.trim());
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: result.success,
86
- avd_count: avds.length,
87
- avds: avds,
88
- error: result.error,
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("start_emulator", { avd_name }, output);
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, LOG_DIR } from "../config/paths.js";
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, must be in artifacts)"),
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
- // Validate the path. If output_path is provided, it must be valid.
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 (must be in artifacts)"),
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
- const finalPath = validateArtifactPath(save_to, LOG_DIR);
78
- await mkdir(dirname(finalPath), { recursive: true });
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 ? validateArtifactPath(save_to, LOG_DIR) : null,
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 (must be in artifacts)"),
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
- const finalPath = validateArtifactPath(save_to, LOG_DIR);
104
- await mkdir(dirname(finalPath), { recursive: true });
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 ? validateArtifactPath(save_to, LOG_DIR) : null,
97
+ saved_to: save_to || null,
110
98
  xml: result.stdout,
111
99
  error: result.error,
112
100
  };
@@ -0,0 +1,5 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ /**
3
+ * Register all iOS Simulator tools
4
+ */
5
+ export declare function registerIosTools(server: McpServer): void;
@@ -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
+ }
@@ -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>;