Skip to content

Commit f2c12f4

Browse files
Select device when multiple device is located for android
Co-authored-by: SrinivasanTarget <[email protected]>
1 parent a276565 commit f2c12f4

File tree

8 files changed

+9999
-7620
lines changed

8 files changed

+9999
-7620
lines changed

package-lock.json

Lines changed: 9685 additions & 7613 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@
3333
"@modelcontextprotocol/sdk": "^1.11.0",
3434
"@xenova/transformers": "^2.17.2",
3535
"@xmldom/xmldom": "^0.9.8",
36-
"appium-uiautomator2-driver": "^4.2.3",
37-
"appium-xcuitest-driver": "^9.6.0",
36+
"appium-adb": "^12.12.1",
37+
"appium-uiautomator2-driver": "^5.0.5",
38+
"appium-xcuitest-driver": "^10.2.1",
3839
"fast-xml-parser": "^5.2.3",
3940
"fastmcp": "^1.23.2",
4041
"form-data": "^4.0.3",

src/devicemanager/adb-manager.ts

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { ADB } from 'appium-adb';
2+
import { log } from '../locators/logger.js';
3+
/**
4+
* Singleton ADB Manager to prevent multiple ADB instances
5+
* This ensures only one ADB instance per host machine
6+
*/
7+
export class ADBManager {
8+
private static instance: ADBManager;
9+
private adbInstance: ADB | null = null;
10+
private isInitialized = false;
11+
private initializationPromise: Promise<ADB> | null = null;
12+
13+
private constructor() {}
14+
15+
/**
16+
* Get the singleton instance of ADBManager
17+
*/
18+
public static getInstance(): ADBManager {
19+
if (!ADBManager.instance) {
20+
ADBManager.instance = new ADBManager();
21+
}
22+
return ADBManager.instance;
23+
}
24+
25+
/**
26+
* Initialize ADB instance with configuration
27+
* @param options ADB configuration options
28+
* @returns Promise<ADB> The initialized ADB instance
29+
*/
30+
public async initialize(
31+
options: { adbExecTimeout?: number; udid?: string } = {}
32+
): Promise<ADB> {
33+
// If already initialized, return existing instance
34+
if (this.isInitialized && this.adbInstance) {
35+
log.debug(
36+
'ADB instance already initialized, returning existing instance'
37+
);
38+
return this.adbInstance;
39+
}
40+
41+
// If initialization is in progress, wait for it
42+
if (this.initializationPromise) {
43+
log.debug('ADB initialization in progress, waiting for completion');
44+
return await this.initializationPromise;
45+
}
46+
47+
// Start initialization
48+
this.initializationPromise = this._createADBInstance(options);
49+
50+
try {
51+
this.adbInstance = await this.initializationPromise;
52+
this.isInitialized = true;
53+
log.info('ADB instance initialized successfully');
54+
return this.adbInstance;
55+
} catch (error) {
56+
log.error(`Failed to initialize ADB instance: ${error}`);
57+
this.initializationPromise = null;
58+
throw error;
59+
}
60+
}
61+
62+
/**
63+
* Get the current ADB instance
64+
* @returns ADB instance or null if not initialized
65+
*/
66+
public getADBInstance(): ADB | null {
67+
return this.adbInstance;
68+
}
69+
70+
/**
71+
* Check if ADB is initialized
72+
* @returns boolean indicating initialization status
73+
*/
74+
public isADBInitialized(): boolean {
75+
return this.isInitialized && this.adbInstance !== null;
76+
}
77+
78+
/**
79+
* Reset the ADB instance (for testing or cleanup)
80+
*/
81+
public async reset(): Promise<void> {
82+
if (this.adbInstance) {
83+
try {
84+
// Cleanup any existing ADB instance
85+
log.info('Resetting ADB instance');
86+
this.adbInstance = null;
87+
this.isInitialized = false;
88+
this.initializationPromise = null;
89+
} catch (error) {
90+
log.error(`Error resetting ADB instance: ${error}`);
91+
}
92+
}
93+
}
94+
95+
/**
96+
* Create ADB instance with proper error handling
97+
* @param options ADB configuration options
98+
* @returns Promise<ADB> The created ADB instance
99+
*/
100+
private async _createADBInstance(
101+
options: { adbExecTimeout?: number; udid?: string } = {}
102+
): Promise<ADB> {
103+
const defaultOptions = {
104+
adbExecTimeout: 60000,
105+
...options,
106+
};
107+
108+
log.info(
109+
`Creating ADB instance with options: ${JSON.stringify(defaultOptions)}`
110+
);
111+
112+
try {
113+
const adb = await ADB.createADB(defaultOptions);
114+
log.info('ADB instance created successfully');
115+
return adb;
116+
} catch (error) {
117+
log.error(`Failed to create ADB instance: ${error}`);
118+
throw new Error(`ADB initialization failed: ${error}`);
119+
}
120+
}
121+
122+
/**
123+
* Get ADB instance for specific device operations
124+
* This method ensures we reuse the singleton instance
125+
* @param udid Optional device UDID for device-specific operations
126+
* @returns Promise<ADB> The ADB instance
127+
*/
128+
public async getADBForDevice(udid?: string): Promise<ADB> {
129+
if (!this.isADBInitialized()) {
130+
await this.initialize({ udid });
131+
}
132+
133+
if (!this.adbInstance) {
134+
throw new Error('ADB instance not available');
135+
}
136+
137+
return this.adbInstance;
138+
}
139+
}
140+
141+
/**
142+
* Global ADB Manager instance
143+
* Use this throughout the application to access ADB functionality
144+
*/
145+
export const adbManager = ADBManager.getInstance();
146+
147+
/**
148+
* Convenience function to get ADB instance
149+
* @param options ADB configuration options
150+
* @returns Promise<ADB> The ADB instance
151+
*/
152+
export async function getADBInstance(
153+
options: { adbExecTimeout?: number; udid?: string } = {}
154+
): Promise<ADB> {
155+
return await adbManager.getADBForDevice(options.udid);
156+
}
157+
158+
/**
159+
* Convenience function to get existing ADB instance (without initialization)
160+
* @returns ADB instance or null if not initialized
161+
*/
162+
export function getExistingADBInstance(): ADB | null {
163+
return adbManager.getADBInstance();
164+
}

src/locators/logger.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ class Logger {
1010
error(...args: any[]): void {
1111
console.error(...args);
1212
}
13+
14+
debug(...args: any[]): void {
15+
console.debug(...args);
16+
}
1317
}
1418

1519
export const log = new Logger();

src/tools/create-session.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
hasActiveSession,
1111
safeDeleteSession,
1212
} from './sessionStore.js';
13-
13+
import { getSelectedDevice, clearSelectedDevice } from './select-device.js';
1414
// Define capabilities type
1515
interface Capabilities {
1616
platformName: string;
@@ -29,12 +29,12 @@ export default function createSession(server: any): void {
2929
server.addTool({
3030
name: 'create_session',
3131
description:
32-
'Create a new mobile session with Android or iOS device (use select_platform first to choose your platform)',
32+
'Create a new mobile session with Android or iOS device (MUST use select_platform tool first to ask the user which platform they want - DO NOT assume or default to any platform)',
3333
parameters: z.object({
3434
platform: z
3535
.enum(['ios', 'android'])
3636
.describe(
37-
"REQUIRED: Specify the platform - 'android' for Android devices or 'ios' for iOS devices. Use select_platform tool first if you haven't chosen yet."
37+
'REQUIRED: Must match the platform the user explicitly selected via the select_platform tool. DO NOT default to Android or iOS without asking the user first.'
3838
),
3939
capabilities: z
4040
.object({})
@@ -84,13 +84,22 @@ export default function createSession(server: any): void {
8484
// Get platform-specific capabilities from config
8585
const androidCaps = configCapabilities.android || {};
8686

87+
// Get selected device UDID if available
88+
const selectedDeviceUdid = getSelectedDevice();
89+
8790
// Merge custom capabilities with defaults and config capabilities
8891
finalCapabilities = {
8992
...defaultCapabilities,
9093
...androidCaps,
94+
...(selectedDeviceUdid && { 'appium:udid': selectedDeviceUdid }),
9195
...customCapabilities,
9296
};
9397

98+
// Clear selected device after use
99+
if (selectedDeviceUdid) {
100+
clearSelectedDevice();
101+
}
102+
94103
driver = new AndroidUiautomator2Driver();
95104
} else if (platform === 'ios') {
96105
defaultCapabilities = {

src/tools/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import createCloudSession from './create-cloud-session.js';
66
import uploadApp from './upload-app.js';
77
import generateLocators from './locators.js';
88
import selectPlatform from './select-platform.js';
9+
import selectDevice from './select-device.js';
910
import generateTest from './generate-tests.js';
1011
import scroll from './scroll.js';
1112
import scrollToElement from './scroll-to-element.js';
@@ -22,6 +23,7 @@ import listApps from './interactions/listApps.js';
2223

2324
export default function registerTools(server: FastMCP): void {
2425
selectPlatform(server);
26+
selectDevice(server);
2527
createSession(server);
2628
deleteSession(server);
2729
createCloudSession(server);

src/tools/select-device.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/**
2+
* Tool to select a specific device when multiple devices are available
3+
*/
4+
import { ADBManager } from '../devicemanager/adb-manager.js';
5+
import { z } from 'zod';
6+
7+
// Store selected device globally
8+
let selectedDeviceUdid: string | null = null;
9+
10+
export function getSelectedDevice(): string | null {
11+
return selectedDeviceUdid;
12+
}
13+
14+
export function clearSelectedDevice(): void {
15+
selectedDeviceUdid = null;
16+
}
17+
18+
export default function selectDevice(server: any): void {
19+
server.addTool({
20+
name: 'select_device',
21+
description:
22+
'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.',
23+
parameters: z.object({
24+
platform: z
25+
.enum(['ios', 'android'])
26+
.describe(
27+
'The platform to list devices for (must match previously selected platform)'
28+
),
29+
deviceUdid: z
30+
.string()
31+
.optional()
32+
.describe(
33+
'The UDID of the device selected by the user. If not provided, this tool will list available devices for the user to choose from.'
34+
),
35+
}),
36+
annotations: {
37+
readOnlyHint: false,
38+
openWorldHint: false,
39+
},
40+
execute: async (args: any, context: any): Promise<any> => {
41+
try {
42+
const { platform, deviceUdid } = args;
43+
44+
if (platform === 'android') {
45+
const adb = await ADBManager.getInstance().initialize();
46+
const devices = await adb.getConnectedDevices();
47+
48+
if (devices.length === 0) {
49+
throw new Error('No Android devices/emulators found');
50+
}
51+
52+
// If deviceUdid is provided, validate and select it
53+
if (deviceUdid) {
54+
const selectedDevice = devices.find((d) => d.udid === deviceUdid);
55+
if (!selectedDevice) {
56+
throw new Error(
57+
`Device with UDID "${deviceUdid}" not found. Available devices: ${devices.map((d) => d.udid).join(', ')}`
58+
);
59+
}
60+
61+
selectedDeviceUdid = deviceUdid;
62+
console.log(`Device selected: ${deviceUdid}`);
63+
64+
return {
65+
content: [
66+
{
67+
type: 'text',
68+
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}" }`,
69+
},
70+
],
71+
};
72+
}
73+
74+
// If no deviceUdid provided, list available devices
75+
const deviceList = devices
76+
.map((device, index) => ` ${index + 1}. ${device.udid}`)
77+
.join('\n');
78+
79+
return {
80+
content: [
81+
{
82+
type: 'text',
83+
text: `📱 Available Android devices/emulators (${devices.length}):\n${deviceList}\n\n⚠️ IMPORTANT: Please ask the user which device they want to use.\n\nOnce the user selects a device, call this tool again with the deviceUdid parameter set to their chosen device UDID.`,
84+
},
85+
],
86+
};
87+
} else if (platform === 'ios') {
88+
throw new Error('iOS device selection not yet implemented');
89+
} else {
90+
throw new Error(
91+
`Invalid platform: ${platform}. Please choose 'android' or 'ios'.`
92+
);
93+
}
94+
} catch (error: any) {
95+
console.error('Error selecting device:', error);
96+
throw new Error(`Failed to select device: ${error.message}`);
97+
}
98+
},
99+
});
100+
}
101+

0 commit comments

Comments
 (0)