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
93 changes: 66 additions & 27 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion src/android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,11 @@ export class AndroidRobot implements Robot {
attributeNamePrefix: "",
});

return parser.parse(dump) as UiAutomatorXml;
try {
return parser.parse(dump) as UiAutomatorXml;
} catch {
throw new ActionableError("Failed to parse UIAutomator XML from device");
}
}

private getScreenElementRect(node: UiAutomatorXmlNode): ScreenElementRect {
Expand Down
13 changes: 12 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ const startSseServer = async (port: number) => {
const app = express();
const server = createMcpServer();

const token = process.env.MCP_TOKEN;
if (token) {
app.use((req, res, next) => {
if (req.headers["authorization"] !== `Bearer ${token}`) {
res.status(401).json({ error: "Unauthorized" });
return;
}
next();
});
}

let transport: SSEServerTransport | null = null;

app.post("/mcp", (req, res) => {
Expand Down Expand Up @@ -42,7 +53,7 @@ const startStdioServer = async () => {
error("mobile-mcp server running on stdio");
} catch (err: any) {
console.error("Fatal error in main():", err);
error("Fatal error in main(): " + JSON.stringify(err.stack));
error("Fatal error in main(): " + err.message);
process.exit(1);
}
};
Expand Down
40 changes: 32 additions & 8 deletions src/ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,12 @@ export class IosRobot implements Robot {

public async getIosVersion(): Promise<string> {
const output = await this.ios("info");
const json = JSON.parse(output);
return json.ProductVersion;
try {
const json = JSON.parse(output);
return json.ProductVersion;
} catch {
throw new ActionableError("Failed to parse iOS device info from go-ios");
}
}

private async isTunnelRequired(): Promise<boolean> {
Expand Down Expand Up @@ -245,14 +249,22 @@ export class IosManager {

public getDeviceName(deviceId: string): string {
const output = execFileSync(getGoIosPath(), ["info", "--udid", deviceId]).toString();
const json: InfoCommandOutput = JSON.parse(output);
return json.DeviceName;
try {
const json: InfoCommandOutput = JSON.parse(output);
return json.DeviceName;
} catch {
throw new ActionableError(`Failed to parse device info for ${deviceId} from go-ios`);
}
}

public getDeviceInfo(deviceId: string): InfoCommandOutput {
const output = execFileSync(getGoIosPath(), ["info", "--udid", deviceId]).toString();
const json: InfoCommandOutput = JSON.parse(output);
return json;
try {
const json: InfoCommandOutput = JSON.parse(output);
return json;
} catch {
throw new ActionableError(`Failed to parse device info for ${deviceId} from go-ios`);
}
}

public listDevices(): IosDevice[] {
Expand All @@ -262,7 +274,13 @@ export class IosManager {
}

const output = execFileSync(getGoIosPath(), ["list"]).toString();
const json: ListCommandOutput = JSON.parse(output);
let json: ListCommandOutput;
try {
json = JSON.parse(output);
} catch {
console.error("Failed to parse device list from go-ios");
return [];
}
const devices = json.deviceList.map(device => ({
deviceId: device,
deviceName: this.getDeviceName(device),
Expand All @@ -278,7 +296,13 @@ export class IosManager {
}

const output = execFileSync(getGoIosPath(), ["list"]).toString();
const json: ListCommandOutput = JSON.parse(output);
let json: ListCommandOutput;
try {
json = JSON.parse(output);
} catch {
console.error("Failed to parse device list from go-ios");
return [];
}
const devices = json.deviceList.map(device => {
const info = this.getDeviceInfo(device);
return {
Expand Down
30 changes: 25 additions & 5 deletions src/mobile-device.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Mobilecli } from "./mobilecli";
import { Button, InstalledApp, Orientation, Robot, ScreenElement, ScreenSize, SwipeDirection } from "./robot";
import { ActionableError, Button, InstalledApp, Orientation, Robot, ScreenElement, ScreenSize, SwipeDirection } from "./robot";

interface InstalledAppsResponse {
status: "ok",
Expand Down Expand Up @@ -73,7 +73,12 @@ export class MobileDevice implements Robot {
}

public async getScreenSize(): Promise<ScreenSize> {
const response = JSON.parse(this.runCommand(["device", "info"])) as DeviceInfoResponse;
let response: DeviceInfoResponse;
try {
response = JSON.parse(this.runCommand(["device", "info"])) as DeviceInfoResponse;
} catch {
throw new ActionableError("Failed to parse device info response from mobilecli");
}
if (response.data.device.screenSize) {
return response.data.device.screenSize;
}
Expand Down Expand Up @@ -142,7 +147,12 @@ export class MobileDevice implements Robot {
}

public async listApps(): Promise<InstalledApp[]> {
const response = JSON.parse(this.runCommand(["apps", "list"])) as InstalledAppsResponse;
let response: InstalledAppsResponse;
try {
response = JSON.parse(this.runCommand(["apps", "list"])) as InstalledAppsResponse;
} catch {
throw new ActionableError("Failed to parse app list response from mobilecli");
}
return response.data.map(app => ({
appName: app.appName || app.packageName,
packageName: app.packageName,
Expand Down Expand Up @@ -192,7 +202,12 @@ export class MobileDevice implements Robot {
}

public async getElementsOnScreen(): Promise<ScreenElement[]> {
const response = JSON.parse(this.runCommand(["dump", "ui"])) as DumpUIResponse;
let response: DumpUIResponse;
try {
response = JSON.parse(this.runCommand(["dump", "ui"])) as DumpUIResponse;
} catch {
throw new ActionableError("Failed to parse UI dump response from mobilecli");
}
return response.data.elements.map(element => ({
type: element.type,
label: element.label,
Expand All @@ -210,7 +225,12 @@ export class MobileDevice implements Robot {
}

public async getOrientation(): Promise<Orientation> {
const response = JSON.parse(this.runCommand(["device", "orientation", "get"])) as OrientationResponse;
let response: OrientationResponse;
try {
response = JSON.parse(this.runCommand(["device", "orientation", "get"])) as OrientationResponse;
} catch {
throw new ActionableError("Failed to parse orientation response from mobilecli");
}
return response.data.orientation;
}
}
Loading