Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/command.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Client } from 'webdriver';
import {
isAndroidUiautomator2DriverSession,
isRemoteDriverSession,
isXCUITestDriverSession,
} from './session-store.js';
import type { DriverInstance } from './session-store.js';
Expand Down Expand Up @@ -129,6 +130,8 @@ export async function getCurrentContext(
return await driver.getCurrentContext();
} else if (isXCUITestDriverSession(driver)) {
return await driver.getCurrentContext();
} else if (isRemoteDriverSession(driver)) {
return String(await (driver as Client).getAppiumContext());
}
throw new Error('getCurrentContext is not supported');
}
Expand All @@ -144,6 +147,9 @@ export async function getContexts(driver: DriverInstance): Promise<string[]> {
return await driver.getContexts();
} else if (isXCUITestDriverSession(driver)) {
return (await driver.getContexts()) as string[];
} else if (isRemoteDriverSession(driver)) {
const contexts = await (driver as Client).getAppiumContexts();
return contexts.map((c) => (typeof c === 'string' ? c : String(c)));
}
throw new Error('getContexts is not supported');
}
Expand All @@ -162,6 +168,11 @@ export async function setContext(
return await driver.setContext(name);
} else if (isXCUITestDriverSession(driver)) {
return await driver.setContext(name || null);
} else if (isRemoteDriverSession(driver)) {
if (name == null || name === '') {
throw new Error('Context name is required');
}
return await (driver as Client).switchAppiumContext(name);
}
throw new Error('setContext is not supported');
}
Expand Down
22 changes: 14 additions & 8 deletions src/session-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,12 @@ export const PLATFORM = {
};

/**
* Determine whether the provided driver represents a remote driver session.
* Determine whether the provided driver represents a remote Appium session
* (i.e. a `Client` obtained via `WebDriver.newSession`) rather than an
* in-process `AndroidUiautomator2Driver` or `XCUITestDriver`.
*
* This checks for the presence of a string-valued `sessionId` property on the
* driver object, which indicates a remote/WebDriver session.
*
* @param driver - The driver instance to inspect (may be a Client, AndroidUiautomator2Driver, XCUITestDriver, or null).
* @returns `true` if `driver` is non-null and has a string `sessionId`; otherwise `false`.
* @param driver - The driver instance to inspect.
* @returns `true` if `driver` is non-null and not an embedded Appium driver.
*/
export function isRemoteDriverSession(driver: NullableDriverInstance): boolean {
if (driver) {
Expand Down Expand Up @@ -311,11 +310,18 @@ export const getPlatformName = (driver: any): string => {
return PLATFORM.ios;
}

if ((driver as Client).isAndroid) {
const client = driver as Client;
if (client.isAndroid) {
return PLATFORM.android;
} else if ((driver as Client).isIOS) {
}
if (client.isIOS) {
return PLATFORM.ios;
}

const session = listSessions().find((s) => s.sessionId === client.sessionId);
if (session && session.platform) {
return session.platform;
}

throw new Error('Unknown driver type');
};
24 changes: 24 additions & 0 deletions src/tests/session-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -495,4 +495,28 @@ describe('getPlatformName', () => {
const unknown = { isAndroid: false, isIOS: false } as any;
expect(() => getPlatformName(unknown)).toThrow('Unknown driver type');
});

test('falls back to session platformName for remote sessions', () => {
const client = {
isAndroid: false,
isIOS: false,
sessionId: 'session-remote-android',
} as any;
setSession(client, 'session-remote-android', {
platformName: 'Android',
});
expect(getPlatformName(client)).toBe(PLATFORM.android);
});

test('falls back to appium:platformName when platformName is absent', () => {
const client = {
isAndroid: false,
isIOS: false,
sessionId: 'session-remote-ios-cap',
} as any;
setSession(client, 'session-remote-ios-cap', {
'appium:platformName': 'iOS',
});
expect(getPlatformName(client)).toBe(PLATFORM.ios);
});
});
33 changes: 30 additions & 3 deletions src/tools/app-management/list-apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,23 @@ import {
} from '../../ui/mcp-ui-utils.js';
import type { AndroidUiautomator2Driver } from 'appium-uiautomator2-driver';
import type { XCUITestDriver } from 'appium-xcuitest-driver';
import { execute } from '../../command.js';

const execAsync = promisify(exec);

/** Extract package ids from the `mobile: listApps` result (map or legacy array). */
function androidListAppsPackageIds(
result: Record<string, unknown> | string[] | null | undefined
): string[] {
if (result == null) {
return [];
}
if (Array.isArray(result)) {
return result;
}
return Object.keys(result);
}

function normalizeListAppsResult(
result: Record<string, Record<string, unknown> | undefined>
): { packageName: string; appName: string }[] {
Expand All @@ -40,12 +54,25 @@ export async function listAppsFromDevice(
throw new Error('No driver found');
}

const platform = getPlatformName(driver);

if (isRemoteDriverSession(driver)) {
throw new Error('listApps is not yet implemented for the remote driver');
if (platform === PLATFORM.android) {
const result = await execute(driver, 'mobile: listApps', {});
const ids = androidListAppsPackageIds(result);
return ids.map((packageName) => ({ packageName, appName: packageName }));
}
if (platform === PLATFORM.ios) {
const result = await execute(driver, 'mobile: listApps', {
applicationType,
});
return normalizeListAppsResult(
(result as Record<string, Record<string, unknown> | undefined>) || {}
);
}
throw new Error(`listApps is not implemented for platform: ${platform}`);
}

const platform = getPlatformName(driver);

if (platform === PLATFORM.ios && isXCUITestDriverSession(driver)) {
const xcuiDriver = driver as XCUITestDriver;
if (xcuiDriver.isSimulator()) {
Expand Down
12 changes: 1 addition & 11 deletions src/tools/context/context.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import type { ContentResult, FastMCP } from 'fastmcp';
import { z } from 'zod';
import {
getDriver,
isRemoteDriverSession,
setCurrentContext,
} from '../../session-store.js';
import { getDriver, setCurrentContext } from '../../session-store.js';
import { getContexts, getCurrentContext, setContext } from '../../command.js';
import {
createUIResource,
Expand Down Expand Up @@ -48,12 +44,6 @@ export default function context(server: FastMCP): void {
throw new Error('No driver found. Please create a session first.');
}

if (isRemoteDriverSession(driver)) {
throw new Error(
'Context management is not yet implemented for the remote driver'
);
}

const [currentContext, availableContexts] = await Promise.all([
getCurrentContext(driver).catch(() => null),
getContexts(driver).catch(() => [] as string[]),
Expand Down
Loading