Skip to content

Commit 9387365

Browse files
Added support to download and install wda for simulators only.
Co-authored-by: SrinivasanTarget <[email protected]>
1 parent e2011ff commit 9387365

File tree

5 files changed

+318
-9
lines changed

5 files changed

+318
-9
lines changed

src/tools/boot-simulator.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,15 @@ export default function bootSimulator(server: any): void {
6767
content: [
6868
{
6969
type: 'text',
70-
text: `✅ Simulator booted successfully!\n\nDevice: ${simulator.name}\nUDID: ${udid}\niOS Version: ${simulator.platform || 'Unknown'}\nBoot Time: ${bootDuration} seconds\n\n🚀 The simulator is now ready for session creation. You can use the create_session tool to start an Appium session.`,
70+
text: `${JSON.stringify(
71+
{
72+
instruction:
73+
'You can now use the install_wda tool to install WDA on the simulator.',
74+
status: 'Simulator booted successfully!',
75+
},
76+
null,
77+
2
78+
)}`,
7179
},
7280
],
7381
};

src/tools/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import selectPlatform from './select-platform.js';
99
import selectDevice from './select-device.js';
1010
import bootSimulator from './boot-simulator.js';
1111
import setupWDA from './setup-wda.js';
12+
import installWDA from './install-wda.js';
1213
import generateTest from './generate-tests.js';
1314
import scroll from './scroll.js';
1415
import scrollToElement from './scroll-to-element.js';
@@ -28,6 +29,7 @@ export default function registerTools(server: FastMCP): void {
2829
selectDevice(server);
2930
bootSimulator(server);
3031
setupWDA(server);
32+
installWDA(server);
3133
createSession(server);
3234
deleteSession(server);
3335
createCloudSession(server);

src/tools/install-wda.ts

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
/**
2+
* Tool to install and launch WebDriverAgent (WDA) on a booted iOS simulator
3+
*/
4+
import { z } from 'zod';
5+
import { exec } from 'child_process';
6+
import { promisify } from 'util';
7+
import path from 'path';
8+
import fs from 'fs';
9+
import os from 'os';
10+
11+
const execAsync = promisify(exec);
12+
13+
function cachePath(folder: string): string {
14+
return path.join(os.homedir(), '.cache', 'appium-mcp', folder);
15+
}
16+
17+
async function getLatestWDAVersion(): Promise<string> {
18+
// Scan the cache directory to find the latest version
19+
const wdaCacheDir = cachePath('wda');
20+
if (!fs.existsSync(wdaCacheDir)) {
21+
throw new Error('No WDA cache found. Please run setup_wda first.');
22+
}
23+
24+
const versions = fs
25+
.readdirSync(wdaCacheDir)
26+
.filter(dir => fs.statSync(path.join(wdaCacheDir, dir)).isDirectory())
27+
.sort((a, b) => {
28+
// Simple version comparison - you might want to use semver for more complex versions
29+
return b.localeCompare(a, undefined, { numeric: true });
30+
});
31+
32+
if (versions.length === 0) {
33+
throw new Error(
34+
'No WDA versions found in cache. Please run setup_wda first.'
35+
);
36+
}
37+
38+
return versions[0];
39+
}
40+
41+
async function getBootedSimulators(): Promise<string[]> {
42+
try {
43+
const { stdout } = await execAsync('xcrun simctl list devices --json');
44+
const data = JSON.parse(stdout);
45+
const bootedSimulators: string[] = [];
46+
47+
for (const [runtime, devices] of Object.entries(data.devices)) {
48+
if (Array.isArray(devices)) {
49+
for (const device of devices as any[]) {
50+
if (device.state === 'Booted') {
51+
bootedSimulators.push(device.udid);
52+
}
53+
}
54+
}
55+
}
56+
57+
return bootedSimulators;
58+
} catch (error) {
59+
throw new Error(`Failed to list simulators: ${error}`);
60+
}
61+
}
62+
63+
async function installAppOnSimulator(
64+
appPath: string,
65+
simulatorUdid: string
66+
): Promise<void> {
67+
try {
68+
await execAsync(`xcrun simctl install "${simulatorUdid}" "${appPath}"`);
69+
} catch (error) {
70+
throw new Error(
71+
`Failed to install app on simulator ${simulatorUdid}: ${error}`
72+
);
73+
}
74+
}
75+
76+
async function launchAppOnSimulator(
77+
bundleId: string,
78+
simulatorUdid: string
79+
): Promise<void> {
80+
try {
81+
await execAsync(`xcrun simctl launch "${simulatorUdid}" "${bundleId}"`);
82+
} catch (error) {
83+
throw new Error(
84+
`Failed to launch app on simulator ${simulatorUdid}: ${error}`
85+
);
86+
}
87+
}
88+
89+
async function getAppBundleId(appPath: string): Promise<string> {
90+
try {
91+
const { stdout } = await execAsync(
92+
`/usr/libexec/PlistBuddy -c "Print CFBundleIdentifier" "${path.join(appPath, 'Info.plist')}"`
93+
);
94+
return stdout.trim();
95+
} catch (error) {
96+
throw new Error(`Failed to get bundle ID for app at ${appPath}: ${error}`);
97+
}
98+
}
99+
100+
async function isWDAInstalled(simulatorUdid: string): Promise<boolean> {
101+
try {
102+
const { stdout } = await execAsync(
103+
`xcrun simctl listapps "${simulatorUdid}" --json`
104+
);
105+
const data = JSON.parse(stdout);
106+
107+
// Check if any app has a bundle ID that looks like WDA
108+
for (const [bundleId, appInfo] of Object.entries(data)) {
109+
if (
110+
bundleId.includes('WebDriverAgentRunner') ||
111+
(appInfo as any)?.CFBundleName?.includes('WebDriverAgent')
112+
) {
113+
return true;
114+
}
115+
}
116+
return false;
117+
} catch (error) {
118+
// If we can't check, assume it's not installed
119+
return false;
120+
}
121+
}
122+
123+
async function isWDARunning(simulatorUdid: string): Promise<boolean> {
124+
try {
125+
const { stdout } = await execAsync(
126+
`xcrun simctl listapps "${simulatorUdid}" --json`
127+
);
128+
const data = JSON.parse(stdout);
129+
130+
// Check if WDA is running
131+
for (const [bundleId, appInfo] of Object.entries(data)) {
132+
if (
133+
bundleId.includes('WebDriverAgentRunner') &&
134+
(appInfo as any)?.ApplicationType === 'User'
135+
) {
136+
return true;
137+
}
138+
}
139+
return false;
140+
} catch (error) {
141+
return false;
142+
}
143+
}
144+
145+
export default function installWDA(server: any): void {
146+
server.addTool({
147+
name: 'install_wda',
148+
description:
149+
'Install and launch the WebDriverAgent (WDA) app on a booted iOS simulator using the app path from setup_wda. This tool requires WDA to be already set up using setup_wda and at least one simulator to be booted.',
150+
parameters: z.object({
151+
simulatorUdid: z
152+
.string()
153+
.optional()
154+
.describe(
155+
'The UDID of the simulator to install WDA on. If not provided, will use the first booted simulator found.'
156+
),
157+
appPath: z
158+
.string()
159+
.optional()
160+
.describe(
161+
'The path to the WDA app bundle (.app file) that be generated by setup_wda tool. If not provided, will try to find the latest cached WDA app.'
162+
),
163+
}),
164+
annotations: {
165+
readOnlyHint: false,
166+
openWorldHint: false,
167+
},
168+
execute: async (args: any, context: any): Promise<any> => {
169+
try {
170+
const { simulatorUdid, appPath: providedAppPath } = args;
171+
172+
// Verify it's a macOS system
173+
if (process.platform !== 'darwin') {
174+
throw new Error(
175+
'WDA installation is only supported on macOS systems'
176+
);
177+
}
178+
179+
// Determine WDA app path
180+
let appPath: string;
181+
if (providedAppPath) {
182+
appPath = providedAppPath;
183+
} else {
184+
// Try to find the latest cached WDA app
185+
const version = await getLatestWDAVersion();
186+
const extractDir = cachePath(`wda/${version}/extracted`);
187+
appPath = path.join(extractDir, 'WebDriverAgentRunner-Runner.app');
188+
}
189+
190+
// Verify WDA app exists
191+
if (!fs.existsSync(appPath)) {
192+
throw new Error(
193+
`WDA app not found at ${appPath}. Please run setup_wda first to download and cache WDA, or provide a valid appPath.`
194+
);
195+
}
196+
197+
// Get booted simulators
198+
const bootedSimulators = await getBootedSimulators();
199+
if (bootedSimulators.length === 0) {
200+
throw new Error(
201+
'No booted simulators found. Please boot a simulator first using boot_simulator tool.'
202+
);
203+
}
204+
205+
// Determine target simulator
206+
const targetSimulator = simulatorUdid || bootedSimulators[0];
207+
208+
if (!bootedSimulators.includes(targetSimulator)) {
209+
throw new Error(
210+
`Simulator ${targetSimulator} is not booted. Available booted simulators: ${bootedSimulators.join(', ')}`
211+
);
212+
}
213+
214+
console.log(
215+
`Installing WDA from ${appPath} on simulator ${targetSimulator}...`
216+
);
217+
218+
// Check if WDA is already installed and running
219+
const isInstalled = await isWDAInstalled(targetSimulator);
220+
const isRunning = await isWDARunning(targetSimulator);
221+
222+
if (isRunning) {
223+
return {
224+
content: [
225+
{
226+
type: 'text',
227+
text: `✅ WebDriverAgent is already running on simulator ${targetSimulator}!\n\nSimulator: ${targetSimulator}\nApp Path: ${appPath}\nStatus: Running\n\n🚀 WDA is ready to accept connections from Appium.`,
228+
},
229+
],
230+
};
231+
}
232+
233+
// Install the app (only if not already installed)
234+
if (!isInstalled) {
235+
await installAppOnSimulator(appPath, targetSimulator);
236+
console.log('WDA app installed successfully');
237+
} else {
238+
console.log('WDA app already installed, skipping installation');
239+
}
240+
241+
// Get bundle ID and launch the app
242+
const bundleId = await getAppBundleId(appPath);
243+
console.log(`Launching WDA with bundle ID: ${bundleId}`);
244+
await launchAppOnSimulator(bundleId, targetSimulator);
245+
246+
return {
247+
content: [
248+
{
249+
type: 'text',
250+
text: `✅ WebDriverAgent installed and launched successfully!\n\nSimulator: ${targetSimulator}\nBundle ID: ${bundleId}\nApp Path: ${appPath}\nInstallation: ${isInstalled ? 'Skipped (already installed)' : 'Completed'}\n\n🚀 WDA is now running on the simulator and ready to accept connections from Appium.\n\nNote: The WDA app should be visible on the simulator screen. You can now create an Appium session.`,
251+
},
252+
],
253+
};
254+
} catch (error: any) {
255+
console.error('Error installing WDA:', error);
256+
throw new Error(`Failed to install WebDriverAgent: ${error.message}`);
257+
}
258+
},
259+
});
260+
}

src/tools/select-device.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,11 @@ export function clearSelectedDevice(): void {
3131
export default function selectDevice(server: any): void {
3232
server.addTool({
3333
name: 'select_device',
34-
description:
35-
'REQUIRED when multiple devices are found: Ask the user to select which specific device they want to use from the available devices. This tool lists all available devices and allows selection by UDID. You MUST use this tool when select_platform returns multiple devices before calling create_session.',
34+
description: `REQUIRED when multiple devices are found: Ask the user to select which specific device they want to use from the available devices.
35+
This tool lists all available devices and allows selection by UDID.
36+
You MUST use this tool when select_platform returns multiple devices before calling create_session for android.
37+
You MUST use this tool when select_platform returns multiple devices before calling boot_simulator for ios if the user has selected simulator device type.
38+
`,
3639
parameters: z.object({
3740
platform: z
3841
.enum(['ios', 'android'])
@@ -84,7 +87,19 @@ export default function selectDevice(server: any): void {
8487
content: [
8588
{
8689
type: 'text',
87-
text: `✅ Device selected: ${deviceUdid}\n\n🚀 You can now create a session using the create_session tool with:\n• platform='android'\n• capabilities: { "appium:udid": "${deviceUdid}" }`,
90+
text: JSON.stringify(
91+
{
92+
message: `✅ Device selected: ${deviceUdid}`,
93+
instructions:
94+
'🚀 You can now create a session using the create_session tool with:',
95+
platform: 'android',
96+
capabilities: {
97+
'appium:udid': deviceUdid,
98+
},
99+
},
100+
null,
101+
2
102+
),
88103
},
89104
],
90105
};
@@ -142,7 +157,19 @@ export default function selectDevice(server: any): void {
142157
content: [
143158
{
144159
type: 'text',
145-
text: `✅ Device selected: ${selectedDevice.name} (${deviceUdid})\n\n🚀 You can now create a session using the create_session tool with:\n• platform='ios'\n• capabilities: { "appium:udid": "${deviceUdid}" }`,
160+
text: JSON.stringify(
161+
{
162+
message: `✅ Device selected: ${selectedDevice.name} (${deviceUdid})`,
163+
instructions:
164+
'🚀 You can now call the setup_wda tool to setup WDA on the simulator.',
165+
platform: 'ios',
166+
capabilities: {
167+
'appium:udid': deviceUdid,
168+
},
169+
},
170+
null,
171+
2
172+
),
146173
},
147174
],
148175
};

src/tools/setup-wda.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ export default function setupWDA(server: any): void {
139139
const extractDir = path.join(versionCacheDir, 'extracted');
140140
const zipPath = path.join(
141141
versionCacheDir,
142-
'WebDriverAgentRunner-Runner.zip'
142+
`WebDriverAgentRunner-Build-Sim-${archStr}.zip`
143143
);
144144
const appPath = path.join(
145145
extractDir,
@@ -165,8 +165,8 @@ export default function setupWDA(server: any): void {
165165
fs.mkdirSync(versionCacheDir, { recursive: true });
166166
fs.mkdirSync(extractDir, { recursive: true });
167167

168-
// Download URL
169-
const downloadUrl = `https://github.com/appium/WebDriverAgent/releases/download/v${wdaVersion}/WebDriverAgentRunner-Runner.zip`;
168+
// Download URL - use architecture-specific filename
169+
const downloadUrl = `https://github.com/appium/WebDriverAgent/releases/download/v${wdaVersion}/WebDriverAgentRunner-Build-Sim-${archStr}.zip`;
170170

171171
console.log(
172172
`Downloading prebuilt WDA v${wdaVersion} for ${platform} simulator...`
@@ -190,7 +190,19 @@ export default function setupWDA(server: any): void {
190190
content: [
191191
{
192192
type: 'text',
193-
text: `✅ WebDriverAgent downloaded and set up successfully!\n\nVersion: ${wdaVersion}\nPlatform: ${platform} (simulator only)\nArchitecture: ${archStr}\nLocation: ${appPath}\nCache: ~/.cache/appium-mcp/wda/${wdaVersion}\nDownload Time: ${duration} seconds\n\n🚀 WDA is now ready! Your next Appium session will start much faster without needing to build WDA from source.\n\nNote: This cached version will be reused on subsequent runs.`,
193+
text: `${JSON.stringify(
194+
{
195+
version: wdaVersion,
196+
platform: platform,
197+
architecture: archStr,
198+
wdaAppPath: appPath,
199+
wdaCachePath: `~/.cache/appium-mcp/wda/${wdaVersion}`,
200+
simulatorOnly: true,
201+
ready: true,
202+
},
203+
null,
204+
2
205+
)}`,
194206
},
195207
],
196208
};

0 commit comments

Comments
 (0)