@nbakka/mcp-appium 1.0.27 → 2.0.0

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.
package/lib/ios.js ADDED
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.IosManager = exports.IosRobot = void 0;
7
+ const path_1 = __importDefault(require("path"));
8
+ const os_1 = require("os");
9
+ const crypto_1 = require("crypto");
10
+ const fs_1 = require("fs");
11
+ const child_process_1 = require("child_process");
12
+ const net_1 = require("net");
13
+ const webdriver_agent_1 = require("./webdriver-agent");
14
+ const robot_1 = require("./robot");
15
+ const WDA_PORT = 8100;
16
+ const IOS_TUNNEL_PORT = 60105;
17
+ const getGoIosPath = () => {
18
+ if (process.env.GO_IOS_PATH) {
19
+ return process.env.GO_IOS_PATH;
20
+ }
21
+ // fallback to go-ios in PATH via `npm install -g go-ios`
22
+ return "ios";
23
+ };
24
+ class IosRobot {
25
+ deviceId;
26
+ constructor(deviceId) {
27
+ this.deviceId = deviceId;
28
+ }
29
+ isListeningOnPort(port) {
30
+ return new Promise((resolve, reject) => {
31
+ const client = new net_1.Socket();
32
+ client.connect(port, "localhost", () => {
33
+ client.destroy();
34
+ resolve(true);
35
+ });
36
+ client.on("error", (err) => {
37
+ resolve(false);
38
+ });
39
+ });
40
+ }
41
+ async isTunnelRunning() {
42
+ return await this.isListeningOnPort(IOS_TUNNEL_PORT);
43
+ }
44
+ async isWdaForwardRunning() {
45
+ return await this.isListeningOnPort(WDA_PORT);
46
+ }
47
+ async assertTunnelRunning() {
48
+ if (await this.isTunnelRequired()) {
49
+ if (!(await this.isTunnelRunning())) {
50
+ throw new robot_1.ActionableError("iOS tunnel is not running, please see https://github.com/mobile-next/mobile-mcp/wiki/");
51
+ }
52
+ }
53
+ }
54
+ async wda() {
55
+ await this.assertTunnelRunning();
56
+ if (!(await this.isWdaForwardRunning())) {
57
+ throw new robot_1.ActionableError("Port forwarding to WebDriverAgent is not running (tunnel okay), please see https://github.com/mobile-next/mobile-mcp/wiki/");
58
+ }
59
+ const wda = new webdriver_agent_1.WebDriverAgent("localhost", WDA_PORT);
60
+ if (!(await wda.isRunning())) {
61
+ throw new robot_1.ActionableError("WebDriverAgent is not running on device (tunnel okay, port forwarding okay), please see https://github.com/mobile-next/mobile-mcp/wiki/");
62
+ }
63
+ return wda;
64
+ }
65
+ async ios(...args) {
66
+ return (0, child_process_1.execFileSync)(getGoIosPath(), ["--udid", this.deviceId, ...args], {}).toString();
67
+ }
68
+ async getIosVersion() {
69
+ const output = await this.ios("info");
70
+ const json = JSON.parse(output);
71
+ return json.ProductVersion;
72
+ }
73
+ async isTunnelRequired() {
74
+ const version = await this.getIosVersion();
75
+ const args = version.split(".");
76
+ return parseInt(args[0], 10) >= 17;
77
+ }
78
+ async getScreenSize() {
79
+ const wda = await this.wda();
80
+ return await wda.getScreenSize();
81
+ }
82
+ async swipe(direction) {
83
+ const wda = await this.wda();
84
+ await wda.swipe(direction);
85
+ }
86
+ async listApps() {
87
+ await this.assertTunnelRunning();
88
+ const output = await this.ios("apps", "--all", "--list");
89
+ return output
90
+ .split("\n")
91
+ .map(line => {
92
+ const [packageName, appName] = line.split(" ");
93
+ return {
94
+ packageName,
95
+ appName,
96
+ };
97
+ });
98
+ }
99
+ async launchApp(packageName) {
100
+ await this.assertTunnelRunning();
101
+ await this.ios("launch", packageName);
102
+ }
103
+ async terminateApp(packageName) {
104
+ await this.assertTunnelRunning();
105
+ await this.ios("kill", packageName);
106
+ }
107
+ async openUrl(url) {
108
+ const wda = await this.wda();
109
+ await wda.openUrl(url);
110
+ }
111
+ async sendKeys(text) {
112
+ const wda = await this.wda();
113
+ await wda.sendKeys(text);
114
+ }
115
+ async pressButton(button) {
116
+ const wda = await this.wda();
117
+ await wda.pressButton(button);
118
+ }
119
+ async tap(x, y) {
120
+ const wda = await this.wda();
121
+ await wda.tap(x, y);
122
+ }
123
+ async getElementsOnScreen() {
124
+ const wda = await this.wda();
125
+ return await wda.getElementsOnScreen();
126
+ }
127
+ async getScreenshot() {
128
+ await this.assertTunnelRunning();
129
+ const tmpFilename = path_1.default.join((0, os_1.tmpdir)(), `screenshot-${(0, crypto_1.randomBytes)(8).toString("hex")}.png`);
130
+ await this.ios("screenshot", "--output", tmpFilename);
131
+ const buffer = (0, fs_1.readFileSync)(tmpFilename);
132
+ (0, fs_1.unlinkSync)(tmpFilename);
133
+ return buffer;
134
+ }
135
+ async setOrientation(orientation) {
136
+ const wda = await this.wda();
137
+ await wda.setOrientation(orientation);
138
+ }
139
+ async getOrientation() {
140
+ const wda = await this.wda();
141
+ return await wda.getOrientation();
142
+ }
143
+ }
144
+ exports.IosRobot = IosRobot;
145
+ class IosManager {
146
+ async isGoIosInstalled() {
147
+ try {
148
+ const output = (0, child_process_1.execFileSync)(getGoIosPath(), ["version"], { stdio: ["pipe", "pipe", "ignore"] }).toString();
149
+ const json = JSON.parse(output);
150
+ return json.version !== undefined && (json.version.startsWith("v") || json.version === "local-build");
151
+ }
152
+ catch (error) {
153
+ return false;
154
+ }
155
+ }
156
+ async getDeviceName(deviceId) {
157
+ const output = (0, child_process_1.execFileSync)(getGoIosPath(), ["info", "--udid", deviceId]).toString();
158
+ const json = JSON.parse(output);
159
+ return json.DeviceName;
160
+ }
161
+ async listDevices() {
162
+ if (!(await this.isGoIosInstalled())) {
163
+ console.error("go-ios is not installed, no physical iOS devices can be detected");
164
+ return [];
165
+ }
166
+ const output = (0, child_process_1.execFileSync)(getGoIosPath(), ["list"]).toString();
167
+ const json = JSON.parse(output);
168
+ const devices = json.deviceList.map(async (device) => ({
169
+ deviceId: device,
170
+ deviceName: await this.getDeviceName(device),
171
+ }));
172
+ return Promise.all(devices);
173
+ }
174
+ }
175
+ exports.IosManager = IosManager;
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SimctlManager = exports.Simctl = void 0;
4
+ const child_process_1 = require("child_process");
5
+ const webdriver_agent_1 = require("./webdriver-agent");
6
+ const robot_1 = require("./robot");
7
+ const TIMEOUT = 30000;
8
+ const WDA_PORT = 8100;
9
+ const MAX_BUFFER_SIZE = 1024 * 1024 * 4;
10
+ class Simctl {
11
+ simulatorUuid;
12
+ constructor(simulatorUuid) {
13
+ this.simulatorUuid = simulatorUuid;
14
+ }
15
+ async wda() {
16
+ const wda = new webdriver_agent_1.WebDriverAgent("localhost", WDA_PORT);
17
+ if (!(await wda.isRunning())) {
18
+ throw new robot_1.ActionableError("WebDriverAgent is not running on simulator, please see https://github.com/mobile-next/mobile-mcp/wiki/");
19
+ }
20
+ return wda;
21
+ }
22
+ simctl(...args) {
23
+ return (0, child_process_1.execFileSync)("xcrun", ["simctl", ...args], {
24
+ timeout: TIMEOUT,
25
+ maxBuffer: MAX_BUFFER_SIZE,
26
+ });
27
+ }
28
+ async getScreenshot() {
29
+ return this.simctl("io", this.simulatorUuid, "screenshot", "-");
30
+ }
31
+ async openUrl(url) {
32
+ const wda = await this.wda();
33
+ await wda.openUrl(url);
34
+ // alternative: this.simctl("openurl", this.simulatorUuid, url);
35
+ }
36
+ async launchApp(packageName) {
37
+ this.simctl("launch", this.simulatorUuid, packageName);
38
+ }
39
+ async terminateApp(packageName) {
40
+ this.simctl("terminate", this.simulatorUuid, packageName);
41
+ }
42
+ static parseIOSAppData(inputText) {
43
+ const result = [];
44
+ let ParseState;
45
+ (function (ParseState) {
46
+ ParseState[ParseState["LOOKING_FOR_APP"] = 0] = "LOOKING_FOR_APP";
47
+ ParseState[ParseState["IN_APP"] = 1] = "IN_APP";
48
+ ParseState[ParseState["IN_PROPERTY"] = 2] = "IN_PROPERTY";
49
+ })(ParseState || (ParseState = {}));
50
+ let state = ParseState.LOOKING_FOR_APP;
51
+ let currentApp = {};
52
+ let appIdentifier = "";
53
+ const lines = inputText.split("\n");
54
+ for (let line of lines) {
55
+ line = line.trim();
56
+ if (line === "") {
57
+ continue;
58
+ }
59
+ switch (state) {
60
+ case ParseState.LOOKING_FOR_APP:
61
+ // look for app identifier pattern: "com.example.app" = {
62
+ const appMatch = line.match(/^"?([^"=]+)"?\s*=\s*\{/);
63
+ if (appMatch) {
64
+ appIdentifier = appMatch[1].trim();
65
+ currentApp = {
66
+ CFBundleIdentifier: appIdentifier,
67
+ };
68
+ state = ParseState.IN_APP;
69
+ }
70
+ break;
71
+ case ParseState.IN_APP:
72
+ if (line === "};") {
73
+ result.push(currentApp);
74
+ currentApp = {};
75
+ state = ParseState.LOOKING_FOR_APP;
76
+ }
77
+ else {
78
+ // look for property: PropertyName = Value;
79
+ const propertyMatch = line.match(/^([^=]+)\s*=\s*(.+?);\s*$/);
80
+ if (propertyMatch) {
81
+ const propName = propertyMatch[1].trim();
82
+ let propValue = propertyMatch[2].trim();
83
+ // remove quotes if present (they're optional)
84
+ if (propValue.startsWith('"') && propValue.endsWith('"')) {
85
+ propValue = propValue.substring(1, propValue.length - 1);
86
+ }
87
+ // add property to current app
88
+ currentApp[propName] = propValue;
89
+ }
90
+ else if (line.endsWith("{")) {
91
+ // nested property like GroupContainers = {
92
+ state = ParseState.IN_PROPERTY;
93
+ }
94
+ }
95
+ break;
96
+ case ParseState.IN_PROPERTY:
97
+ if (line === "};") {
98
+ // end of nested property
99
+ state = ParseState.IN_APP;
100
+ }
101
+ // skip content of nested properties, we don't care of those right now
102
+ break;
103
+ }
104
+ }
105
+ return result;
106
+ }
107
+ async listApps() {
108
+ const text = this.simctl("listapps", this.simulatorUuid).toString();
109
+ const apps = Simctl.parseIOSAppData(text);
110
+ return apps.map(app => ({
111
+ packageName: app.CFBundleIdentifier,
112
+ appName: app.CFBundleDisplayName,
113
+ }));
114
+ }
115
+ async getScreenSize() {
116
+ const wda = await this.wda();
117
+ return wda.getScreenSize();
118
+ }
119
+ async sendKeys(keys) {
120
+ const wda = await this.wda();
121
+ return wda.sendKeys(keys);
122
+ }
123
+ async swipe(direction) {
124
+ const wda = await this.wda();
125
+ return wda.swipe(direction);
126
+ }
127
+ async tap(x, y) {
128
+ const wda = await this.wda();
129
+ return wda.tap(x, y);
130
+ }
131
+ async pressButton(button) {
132
+ const wda = await this.wda();
133
+ return wda.pressButton(button);
134
+ }
135
+ async getElementsOnScreen() {
136
+ const wda = await this.wda();
137
+ return wda.getElementsOnScreen();
138
+ }
139
+ async setOrientation(orientation) {
140
+ const wda = await this.wda();
141
+ return wda.setOrientation(orientation);
142
+ }
143
+ async getOrientation() {
144
+ const wda = await this.wda();
145
+ return wda.getOrientation();
146
+ }
147
+ }
148
+ exports.Simctl = Simctl;
149
+ class SimctlManager {
150
+ listSimulators() {
151
+ // detect if this is a mac
152
+ if (process.platform !== "darwin") {
153
+ // don't even try to run xcrun
154
+ return [];
155
+ }
156
+ try {
157
+ const text = (0, child_process_1.execFileSync)("xcrun", ["simctl", "list", "devices", "-j"]).toString();
158
+ const json = JSON.parse(text);
159
+ return Object.values(json.devices).flatMap(device => {
160
+ return device.map(d => {
161
+ return {
162
+ name: d.name,
163
+ uuid: d.udid,
164
+ state: d.state,
165
+ };
166
+ });
167
+ });
168
+ }
169
+ catch (error) {
170
+ console.error("Error listing simulators", error);
171
+ return [];
172
+ }
173
+ }
174
+ listBootedSimulators() {
175
+ return this.listSimulators()
176
+ .filter(simulator => simulator.state === "Booted");
177
+ }
178
+ getSimulator(uuid) {
179
+ return new Simctl(uuid);
180
+ }
181
+ }
182
+ exports.SimctlManager = SimctlManager;
package/lib/logger.js ADDED
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.error = exports.trace = void 0;
4
+ const fs_1 = require("fs");
5
+ const writeLog = (message) => {
6
+ if (process.env.LOG_FILE) {
7
+ const logfile = process.env.LOG_FILE;
8
+ const timestamp = new Date().toISOString();
9
+ const levelStr = "INFO";
10
+ const logMessage = `[${timestamp}] ${levelStr} ${message}`;
11
+ (0, fs_1.appendFileSync)(logfile, logMessage + "\n");
12
+ }
13
+ console.error(message);
14
+ };
15
+ const trace = (message) => {
16
+ writeLog(message);
17
+ };
18
+ exports.trace = trace;
19
+ const error = (message) => {
20
+ writeLog(message);
21
+ };
22
+ exports.error = error;
package/lib/png.js ADDED
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PNG = void 0;
4
+ class PNG {
5
+ buffer;
6
+ constructor(buffer) {
7
+ this.buffer = buffer;
8
+ }
9
+ getDimensions() {
10
+ const pngSignature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
11
+ if (!this.buffer.subarray(0, 8).equals(pngSignature)) {
12
+ throw new Error("Not a valid PNG file");
13
+ }
14
+ const width = this.buffer.readUInt32BE(16);
15
+ const height = this.buffer.readUInt32BE(20);
16
+ return { width, height };
17
+ }
18
+ }
19
+ exports.PNG = PNG;
package/lib/robot.js ADDED
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ActionableError = void 0;
4
+ class ActionableError extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ }
8
+ }
9
+ exports.ActionableError = ActionableError;
package/lib/server.js ADDED
@@ -0,0 +1,226 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createMcpServer = void 0;
4
+ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
5
+ const zod_1 = require("zod");
6
+ const logger_1 = require("./logger");
7
+ const android_1 = require("./android");
8
+ const robot_1 = require("./robot");
9
+ const iphone_simulator_1 = require("./iphone-simulator");
10
+ const ios_1 = require("./ios");
11
+ const png_1 = require("./png");
12
+ const image_utils_1 = require("./image-utils");
13
+ const getAgentVersion = () => {
14
+ const json = require("../package.json");
15
+ return json.version;
16
+ };
17
+ const getLatestAgentVersion = async () => {
18
+ const response = await fetch("https://api.github.com/repos/mobile-next/mobile-mcp/tags?per_page=1");
19
+ const json = await response.json();
20
+ return json[0].name;
21
+ };
22
+ const checkForLatestAgentVersion = async () => {
23
+ try {
24
+ const latestVersion = await getLatestAgentVersion();
25
+ const currentVersion = getAgentVersion();
26
+ if (latestVersion !== currentVersion) {
27
+ (0, logger_1.trace)(`You are running an older version of the agent. Please update to the latest version: ${latestVersion}.`);
28
+ }
29
+ }
30
+ catch (error) {
31
+ // ignore
32
+ }
33
+ };
34
+ const createMcpServer = () => {
35
+ const server = new mcp_js_1.McpServer({
36
+ name: "mobile-mcp",
37
+ version: getAgentVersion(),
38
+ capabilities: {
39
+ resources: {},
40
+ tools: {},
41
+ },
42
+ });
43
+ const tool = (name, description, paramsSchema, cb) => {
44
+ const wrappedCb = async (args) => {
45
+ try {
46
+ (0, logger_1.trace)(`Invoking ${name} with args: ${JSON.stringify(args)}`);
47
+ const response = await cb(args);
48
+ (0, logger_1.trace)(`=> ${response}`);
49
+ return {
50
+ content: [{ type: "text", text: response }],
51
+ };
52
+ }
53
+ catch (error) {
54
+ if (error instanceof robot_1.ActionableError) {
55
+ return {
56
+ content: [{ type: "text", text: `${error.message}. Please fix the issue and try again.` }],
57
+ };
58
+ }
59
+ else {
60
+ // a real exception
61
+ (0, logger_1.trace)(`Tool '${description}' failed: ${error.message} stack: ${error.stack}`);
62
+ return {
63
+ content: [{ type: "text", text: `Error: ${error.message}` }],
64
+ isError: true,
65
+ };
66
+ }
67
+ }
68
+ };
69
+ server.tool(name, description, paramsSchema, args => wrappedCb(args));
70
+ };
71
+ let robot;
72
+ const simulatorManager = new iphone_simulator_1.SimctlManager();
73
+ const requireRobot = () => {
74
+ if (!robot) {
75
+ throw new robot_1.ActionableError("No device selected. Use the mobile_use_device tool to select a device.");
76
+ }
77
+ };
78
+ tool("mobile_list_available_devices", "List all available devices. This includes both physical devices and simulators. If there is more than one device returned, you need to let the user select one of them.", {}, async ({}) => {
79
+ const iosManager = new ios_1.IosManager();
80
+ const androidManager = new android_1.AndroidDeviceManager();
81
+ const devices = simulatorManager.listBootedSimulators();
82
+ const simulatorNames = devices.map(d => d.name);
83
+ const androidDevices = androidManager.getConnectedDevices();
84
+ const iosDevices = await iosManager.listDevices();
85
+ const iosDeviceNames = iosDevices.map(d => d.deviceId);
86
+ const androidTvDevices = androidDevices.filter(d => d.deviceType === "tv").map(d => d.deviceId);
87
+ const androidMobileDevices = androidDevices.filter(d => d.deviceType === "mobile").map(d => d.deviceId);
88
+ const resp = ["Found these devices:"];
89
+ if (simulatorNames.length > 0) {
90
+ resp.push(`iOS simulators: [${simulatorNames.join(".")}]`);
91
+ }
92
+ if (iosDevices.length > 0) {
93
+ resp.push(`iOS devices: [${iosDeviceNames.join(",")}]`);
94
+ }
95
+ if (androidMobileDevices.length > 0) {
96
+ resp.push(`Android devices: [${androidMobileDevices.join(",")}]`);
97
+ }
98
+ if (androidTvDevices.length > 0) {
99
+ resp.push(`Android TV devices: [${androidTvDevices.join(",")}]`);
100
+ }
101
+ return resp.join("\n");
102
+ });
103
+ tool("mobile_use_device", "Select a device to use. This can be a simulator or an Android device. Use the list_available_devices tool to get a list of available devices.", {
104
+ device: zod_1.z.string().describe("The name of the device to select"),
105
+ deviceType: zod_1.z.enum(["simulator", "ios", "android"]).describe("The type of device to select"),
106
+ }, async ({ device, deviceType }) => {
107
+ switch (deviceType) {
108
+ case "simulator":
109
+ robot = simulatorManager.getSimulator(device);
110
+ break;
111
+ case "ios":
112
+ robot = new ios_1.IosRobot(device);
113
+ break;
114
+ case "android":
115
+ robot = new android_1.AndroidRobot(device);
116
+ break;
117
+ }
118
+ return `Selected device: ${device}`;
119
+ });
120
+ tool("mobile_list_apps", "List all the installed apps on the device", {}, async ({}) => {
121
+ requireRobot();
122
+ const result = await robot.listApps();
123
+ return `Found these apps on device: ${result.map(app => `${app.appName} (${app.packageName})`).join(", ")}`;
124
+ });
125
+ tool("mobile_launch_app", "Launch an app on mobile device. Use this to open a specific app. You can find the package name of the app by calling list_apps_on_device.", {
126
+ packageName: zod_1.z.string().describe("The package name of the app to launch"),
127
+ }, async ({ packageName }) => {
128
+ requireRobot();
129
+ await robot.launchApp(packageName);
130
+ return `Launched app ${packageName}`;
131
+ });
132
+ tool("mobile_terminate_app", "Stop and terminate an app on mobile device", {
133
+ packageName: zod_1.z.string().describe("The package name of the app to terminate"),
134
+ }, async ({ packageName }) => {
135
+ requireRobot();
136
+ await robot.terminateApp(packageName);
137
+ return `Terminated app ${packageName}`;
138
+ });
139
+ tool("mobile_get_screen_size", "Get the screen size of the mobile device in pixels", {}, async ({}) => {
140
+ requireRobot();
141
+ const screenSize = await robot.getScreenSize();
142
+ return `Screen size is ${screenSize.width}x${screenSize.height} pixels`;
143
+ });
144
+ tool("mobile_click_on_element_by_text", "Click on the screen element identified by its text", {
145
+ text: zod_1.z.string().describe("The visible text of the element to click"),
146
+ }, async ({ text }) => {
147
+ requireRobot();
148
+ const xpath = `//*[@text="${text}"]`;
149
+ const element = await robot.findElementByXPath(xpath);
150
+ if (!element) {
151
+ throw new Error(`Element with text "${text}" not found`);
152
+ }
153
+ await element.click();
154
+ // Wait for 2 seconds after click
155
+ await new Promise(resolve => setTimeout(resolve, 2000));
156
+ return `Clicked on element with text: "${text}"`;
157
+ });
158
+
159
+ tool("mobile_list_elements_on_screen", "List elements on screen with display text or accessibility label. Do not cache this result.", {}, async ({}) => {
160
+ requireRobot();
161
+ const elements = await robot.getElementsOnScreen();
162
+ const result = elements.map(element => {
163
+ const out = {
164
+ type: element.type,
165
+ text: element.text,
166
+ label: element.label,
167
+ name: element.name,
168
+ value: element.value,
169
+ };
170
+ if (element.focused) {
171
+ out.focused = true;
172
+ }
173
+ return out;
174
+ });
175
+ return `Found these elements on screen: ${JSON.stringify(result)}`;
176
+ });
177
+ tool("mobile_press_button", "Press a button on device", {
178
+ button: zod_1.z.string().describe("The button to press. Supported buttons: BACK (android only), HOME, VOLUME_UP, VOLUME_DOWN, ENTER, DPAD_CENTER (android tv only), DPAD_UP (android tv only), DPAD_DOWN (android tv only), DPAD_LEFT (android tv only), DPAD_RIGHT (android tv only)"),
179
+ }, async ({ button }) => {
180
+ requireRobot();
181
+ await robot.pressButton(button);
182
+ return `Pressed the button: ${button}`;
183
+ });
184
+ tool("mobile_open_url", "Open a URL in browser on device", {
185
+ url: zod_1.z.string().describe("The URL to open"),
186
+ }, async ({ url }) => {
187
+ requireRobot();
188
+ await robot.openUrl(url);
189
+ await new Promise(resolve => setTimeout(resolve, 5000));
190
+ return `Opened URL: ${url}`;
191
+ });
192
+ tool("swipe_on_screen", "Swipe on the screen", {
193
+ direction: zod_1.z.enum(["up", "down"]).describe("The direction to swipe"),
194
+ }, async ({ direction }) => {
195
+ requireRobot();
196
+ await robot.swipe(direction);
197
+ return `Swiped ${direction} on screen`;
198
+ });
199
+ tool("mobile_type_keys", "Type text into the focused element", {
200
+ text: zod_1.z.string().describe("The text to type"),
201
+ submit: zod_1.z.boolean().describe("Whether to submit the text. If true, the text will be submitted as if the user pressed the enter key."),
202
+ }, async ({ text, submit }) => {
203
+ requireRobot();
204
+ await robot.sendKeys(text);
205
+ if (submit) {
206
+ await robot.pressButton("ENTER");
207
+ }
208
+ return `Typed text: ${text}`;
209
+ });
210
+ tool("mobile_set_orientation", "Change the screen orientation of the device", {
211
+ orientation: zod_1.z.enum(["portrait", "landscape"]).describe("The desired orientation"),
212
+ }, async ({ orientation }) => {
213
+ requireRobot();
214
+ await robot.setOrientation(orientation);
215
+ return `Changed device orientation to ${orientation}`;
216
+ });
217
+ tool("mobile_get_orientation", "Get the current screen orientation of the device", {}, async () => {
218
+ requireRobot();
219
+ const orientation = await robot.getOrientation();
220
+ return `Current device orientation is ${orientation}`;
221
+ });
222
+ // async check for latest agent version
223
+ checkForLatestAgentVersion().then();
224
+ return server;
225
+ };
226
+ exports.createMcpServer = createMcpServer;