@mobilenext/mobile-mcp 0.0.19 → 0.0.21

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/README.md CHANGED
@@ -175,7 +175,7 @@ What you will need to connect MCP with your agent and mobile devices:
175
175
 
176
176
  - [Xcode command line tools](https://developer.apple.com/xcode/resources/)
177
177
  - [Android Platform Tools](https://developer.android.com/tools/releases/platform-tools)
178
- - [node.js](https://nodejs.org/en/download/)
178
+ - [node.js](https://nodejs.org/en/download/) v22+
179
179
  - [MCP](https://modelcontextprotocol.io/introduction) supported foundational models or agents, like [Claude MCP](https://modelcontextprotocol.io/quickstart/server), [OpenAI Agent SDK](https://openai.github.io/openai-agents-python/mcp/), [Copilot Studio](https://www.microsoft.com/en-us/microsoft-copilot/blog/copilot-studio/introducing-model-context-protocol-mcp-in-copilot-studio-simplified-integration-with-ai-apps-and-agents/)
180
180
 
181
181
  ### Simulators, Emulators, and Physical Devices
package/lib/android.js CHANGED
@@ -37,14 +37,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.AndroidDeviceManager = exports.AndroidRobot = void 0;
40
- const path_1 = __importDefault(require("path"));
41
- const child_process_1 = require("child_process");
40
+ const node_path_1 = __importDefault(require("node:path"));
41
+ const node_child_process_1 = require("node:child_process");
42
42
  const xml = __importStar(require("fast-xml-parser"));
43
43
  const robot_1 = require("./robot");
44
44
  const getAdbPath = () => {
45
45
  let executable = "adb";
46
46
  if (process.env.ANDROID_HOME) {
47
- executable = path_1.default.join(process.env.ANDROID_HOME, "platform-tools", "adb");
47
+ executable = node_path_1.default.join(process.env.ANDROID_HOME, "platform-tools", "adb");
48
48
  }
49
49
  return executable;
50
50
  };
@@ -68,7 +68,7 @@ class AndroidRobot {
68
68
  this.deviceId = deviceId;
69
69
  }
70
70
  adb(...args) {
71
- return (0, child_process_1.execFileSync)(getAdbPath(), ["-s", this.deviceId, ...args], {
71
+ return (0, node_child_process_1.execFileSync)(getAdbPath(), ["-s", this.deviceId, ...args], {
72
72
  maxBuffer: MAX_BUFFER_SIZE,
73
73
  timeout: TIMEOUT,
74
74
  });
@@ -94,6 +94,7 @@ class AndroidRobot {
94
94
  return { width, height, scale };
95
95
  }
96
96
  async listApps() {
97
+ // only apps that have a launcher activity are returned
97
98
  return this.adb("shell", "cmd", "package", "query-activities", "-a", "android.intent.action.MAIN", "-c", "android.intent.category.LAUNCHER")
98
99
  .toString()
99
100
  .split("\n")
@@ -106,6 +107,14 @@ class AndroidRobot {
106
107
  appName: packageName,
107
108
  }));
108
109
  }
110
+ async listPackages() {
111
+ return this.adb("shell", "pm", "list", "packages")
112
+ .toString()
113
+ .split("\n")
114
+ .map(line => line.trim())
115
+ .filter(line => line.startsWith("package:"))
116
+ .map(line => line.substring("package:".length));
117
+ }
109
118
  async launchApp(packageName) {
110
119
  this.adb("shell", "monkey", "-p", packageName, "-c", "android.intent.category.LAUNCHER", "1");
111
120
  }
@@ -229,10 +238,37 @@ class AndroidRobot {
229
238
  async openUrl(url) {
230
239
  this.adb("shell", "am", "start", "-a", "android.intent.action.VIEW", "-d", url);
231
240
  }
241
+ isAscii(text) {
242
+ return /^[\x00-\x7F]*$/.test(text);
243
+ }
244
+ async isDeviceKitInstalled() {
245
+ const packages = await this.listPackages();
246
+ return packages.includes("com.mobilenext.devicekit");
247
+ }
232
248
  async sendKeys(text) {
233
- // adb shell requires some escaping
234
- const _text = text.replace(/ /g, "\\ ");
235
- this.adb("shell", "input", "text", _text);
249
+ if (text === "") {
250
+ // bailing early, so we don't run adb shell with empty string.
251
+ // this happens when you prompt with a simple "submit".
252
+ return;
253
+ }
254
+ if (this.isAscii(text)) {
255
+ // adb shell input only supports ascii characters. and
256
+ // some of the keys have to be escaped.
257
+ const _text = text.replace(/ /g, "\\ ");
258
+ this.adb("shell", "input", "text", _text);
259
+ }
260
+ else if (await this.isDeviceKitInstalled()) {
261
+ // try sending over clipboard
262
+ const base64 = Buffer.from(text).toString("base64");
263
+ // send clipboard over and immediately paste it
264
+ this.adb("shell", "am", "broadcast", "-a", "devicekit.clipboard.set", "-e", "encoding", "base64", "-e", "text", base64, "-n", "com.mobilenext.devicekit/.ClipboardBroadcastReceiver");
265
+ this.adb("shell", "input", "keyevent", "KEYCODE_PASTE");
266
+ // clear clipboard when we're done
267
+ this.adb("shell", "am", "broadcast", "-a", "devicekit.clipboard.clear", "-n", "com.mobilenext.devicekit/.ClipboardBroadcastReceiver");
268
+ }
269
+ else {
270
+ throw new robot_1.ActionableError("Non-ASCII text is not supported on Android, please install mobilenext devicekit, see https://github.com/mobile-next/devicekit-android");
271
+ }
236
272
  }
237
273
  async pressButton(button) {
238
274
  if (!BUTTON_MAP[button]) {
@@ -245,8 +281,9 @@ class AndroidRobot {
245
281
  }
246
282
  async setOrientation(orientation) {
247
283
  const orientationValue = orientation === "portrait" ? 0 : 1;
248
- this.adb("shell", "content", "insert", "--uri", "content://settings/system", "--bind", "name:s:user_rotation", "--bind", `value:i:${orientationValue}`);
284
+ // disable auto-rotation prior to setting the orientation
249
285
  this.adb("shell", "settings", "put", "system", "accelerometer_rotation", "0");
286
+ this.adb("shell", "content", "insert", "--uri", "content://settings/system", "--bind", "name:s:user_rotation", "--bind", `value:i:${orientationValue}`);
250
287
  }
251
288
  async getOrientation() {
252
289
  const rotation = this.adb("shell", "settings", "get", "system", "user_rotation").toString().trim();
@@ -297,11 +334,12 @@ class AndroidDeviceManager {
297
334
  }
298
335
  getConnectedDevices() {
299
336
  try {
300
- const names = (0, child_process_1.execFileSync)(getAdbPath(), ["devices"])
337
+ const names = (0, node_child_process_1.execFileSync)(getAdbPath(), ["devices"])
301
338
  .toString()
302
339
  .split("\n")
340
+ .map(line => line.trim())
341
+ .filter(line => line !== "")
303
342
  .filter(line => !line.startsWith("List of devices attached"))
304
- .filter(line => line.trim() !== "")
305
343
  .map(line => line.split("\t")[0]);
306
344
  return names.map(name => ({
307
345
  deviceId: name,
package/lib/ios.js CHANGED
@@ -1,15 +1,8 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  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");
4
+ const node_net_1 = require("node:net");
5
+ const node_child_process_1 = require("node:child_process");
13
6
  const webdriver_agent_1 = require("./webdriver-agent");
14
7
  const robot_1 = require("./robot");
15
8
  const WDA_PORT = 8100;
@@ -28,7 +21,7 @@ class IosRobot {
28
21
  }
29
22
  isListeningOnPort(port) {
30
23
  return new Promise((resolve, reject) => {
31
- const client = new net_1.Socket();
24
+ const client = new node_net_1.Socket();
32
25
  client.connect(port, "localhost", () => {
33
26
  client.destroy();
34
27
  resolve(true);
@@ -63,7 +56,7 @@ class IosRobot {
63
56
  return wda;
64
57
  }
65
58
  async ios(...args) {
66
- return (0, child_process_1.execFileSync)(getGoIosPath(), ["--udid", this.deviceId, ...args], {}).toString();
59
+ return (0, node_child_process_1.execFileSync)(getGoIosPath(), ["--udid", this.deviceId, ...args], {}).toString();
67
60
  }
68
61
  async getIosVersion() {
69
62
  const output = await this.ios("info");
@@ -129,12 +122,16 @@ class IosRobot {
129
122
  return await wda.getElementsOnScreen();
130
123
  }
131
124
  async getScreenshot() {
125
+ const wda = await this.wda();
126
+ return await wda.getScreenshot();
127
+ /* alternative:
132
128
  await this.assertTunnelRunning();
133
- const tmpFilename = path_1.default.join((0, os_1.tmpdir)(), `screenshot-${(0, crypto_1.randomBytes)(8).toString("hex")}.png`);
129
+ const tmpFilename = path.join(tmpdir(), `screenshot-${randomBytes(8).toString("hex")}.png`);
134
130
  await this.ios("screenshot", "--output", tmpFilename);
135
- const buffer = (0, fs_1.readFileSync)(tmpFilename);
136
- (0, fs_1.unlinkSync)(tmpFilename);
131
+ const buffer = readFileSync(tmpFilename);
132
+ unlinkSync(tmpFilename);
137
133
  return buffer;
134
+ */
138
135
  }
139
136
  async setOrientation(orientation) {
140
137
  const wda = await this.wda();
@@ -149,7 +146,7 @@ exports.IosRobot = IosRobot;
149
146
  class IosManager {
150
147
  async isGoIosInstalled() {
151
148
  try {
152
- const output = (0, child_process_1.execFileSync)(getGoIosPath(), ["version"], { stdio: ["pipe", "pipe", "ignore"] }).toString();
149
+ const output = (0, node_child_process_1.execFileSync)(getGoIosPath(), ["version"], { stdio: ["pipe", "pipe", "ignore"] }).toString();
153
150
  const json = JSON.parse(output);
154
151
  return json.version !== undefined && (json.version.startsWith("v") || json.version === "local-build");
155
152
  }
@@ -157,23 +154,23 @@ class IosManager {
157
154
  return false;
158
155
  }
159
156
  }
160
- async getDeviceName(deviceId) {
161
- const output = (0, child_process_1.execFileSync)(getGoIosPath(), ["info", "--udid", deviceId]).toString();
157
+ getDeviceName(deviceId) {
158
+ const output = (0, node_child_process_1.execFileSync)(getGoIosPath(), ["info", "--udid", deviceId]).toString();
162
159
  const json = JSON.parse(output);
163
160
  return json.DeviceName;
164
161
  }
165
- async listDevices() {
166
- if (!(await this.isGoIosInstalled())) {
162
+ listDevices() {
163
+ if (!this.isGoIosInstalled()) {
167
164
  console.error("go-ios is not installed, no physical iOS devices can be detected");
168
165
  return [];
169
166
  }
170
- const output = (0, child_process_1.execFileSync)(getGoIosPath(), ["list"]).toString();
167
+ const output = (0, node_child_process_1.execFileSync)(getGoIosPath(), ["list"]).toString();
171
168
  const json = JSON.parse(output);
172
- const devices = json.deviceList.map(async (device) => ({
169
+ const devices = json.deviceList.map(device => ({
173
170
  deviceId: device,
174
- deviceName: await this.getDeviceName(device),
171
+ deviceName: this.getDeviceName(device),
175
172
  }));
176
- return Promise.all(devices);
173
+ return devices;
177
174
  }
178
175
  }
179
176
  exports.IosManager = IosManager;
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SimctlManager = exports.Simctl = void 0;
4
- const child_process_1 = require("child_process");
4
+ const node_child_process_1 = require("node:child_process");
5
+ const logger_1 = require("./logger");
5
6
  const webdriver_agent_1 = require("./webdriver-agent");
6
7
  const robot_1 = require("./robot");
7
8
  const TIMEOUT = 30000;
@@ -12,21 +13,54 @@ class Simctl {
12
13
  constructor(simulatorUuid) {
13
14
  this.simulatorUuid = simulatorUuid;
14
15
  }
16
+ async isWdaInstalled() {
17
+ const apps = await this.listApps();
18
+ return apps.map(app => app.packageName).includes("com.facebook.WebDriverAgentRunner.xctrunner");
19
+ }
20
+ async startWda() {
21
+ if (!(await this.isWdaInstalled())) {
22
+ // wda is not even installed, won't attempt to start it
23
+ return;
24
+ }
25
+ (0, logger_1.trace)("Starting WebDriverAgent");
26
+ const webdriverPackageName = "com.facebook.WebDriverAgentRunner.xctrunner";
27
+ this.simctl("launch", this.simulatorUuid, webdriverPackageName);
28
+ // now we wait for wda to have a successful status
29
+ const wda = new webdriver_agent_1.WebDriverAgent("localhost", WDA_PORT);
30
+ // wait up to 10 seconds for wda to start
31
+ const timeout = +new Date() + 10 * 1000;
32
+ while (+new Date() < timeout) {
33
+ // cross fingers and see if wda is already running
34
+ if (await wda.isRunning()) {
35
+ (0, logger_1.trace)("WebDriverAgent is now running");
36
+ return;
37
+ }
38
+ // wait 100ms before trying again
39
+ await new Promise(resolve => setTimeout(resolve, 100));
40
+ }
41
+ (0, logger_1.trace)("Could not start WebDriverAgent in time, giving up");
42
+ }
15
43
  async wda() {
16
44
  const wda = new webdriver_agent_1.WebDriverAgent("localhost", WDA_PORT);
17
45
  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/");
46
+ await this.startWda();
47
+ if (!(await wda.isRunning())) {
48
+ throw new robot_1.ActionableError("WebDriverAgent is not running on simulator, please see https://github.com/mobile-next/mobile-mcp/wiki/");
49
+ }
50
+ // was successfully started
19
51
  }
20
52
  return wda;
21
53
  }
22
54
  simctl(...args) {
23
- return (0, child_process_1.execFileSync)("xcrun", ["simctl", ...args], {
55
+ return (0, node_child_process_1.execFileSync)("xcrun", ["simctl", ...args], {
24
56
  timeout: TIMEOUT,
25
57
  maxBuffer: MAX_BUFFER_SIZE,
26
58
  });
27
59
  }
28
60
  async getScreenshot() {
29
- return this.simctl("io", this.simulatorUuid, "screenshot", "-");
61
+ const wda = await this.wda();
62
+ return await wda.getScreenshot();
63
+ // alternative: return this.simctl("io", this.simulatorUuid, "screenshot", "-");
30
64
  }
31
65
  async openUrl(url) {
32
66
  const wda = await this.wda();
@@ -41,7 +75,7 @@ class Simctl {
41
75
  }
42
76
  async listApps() {
43
77
  const text = this.simctl("listapps", this.simulatorUuid).toString();
44
- const result = (0, child_process_1.execFileSync)("plutil", ["-convert", "json", "-o", "-", "-r", "-"], {
78
+ const result = (0, node_child_process_1.execFileSync)("plutil", ["-convert", "json", "-o", "-", "-r", "-"], {
45
79
  input: text,
46
80
  });
47
81
  const output = JSON.parse(result.toString());
@@ -96,7 +130,7 @@ class SimctlManager {
96
130
  return [];
97
131
  }
98
132
  try {
99
- const text = (0, child_process_1.execFileSync)("xcrun", ["simctl", "list", "devices", "-j"]).toString();
133
+ const text = (0, node_child_process_1.execFileSync)("xcrun", ["simctl", "list", "devices", "-j"]).toString();
100
134
  const json = JSON.parse(text);
101
135
  return Object.values(json.devices).flatMap(device => {
102
136
  return device.map(d => {
package/lib/logger.js CHANGED
@@ -1,14 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.error = exports.trace = void 0;
4
- const fs_1 = require("fs");
4
+ const node_fs_1 = require("node:fs");
5
5
  const writeLog = (message) => {
6
6
  if (process.env.LOG_FILE) {
7
7
  const logfile = process.env.LOG_FILE;
8
8
  const timestamp = new Date().toISOString();
9
9
  const levelStr = "INFO";
10
10
  const logMessage = `[${timestamp}] ${levelStr} ${message}`;
11
- (0, fs_1.appendFileSync)(logfile, logMessage + "\n");
11
+ (0, node_fs_1.appendFileSync)(logfile, logMessage + "\n");
12
12
  }
13
13
  console.error(message);
14
14
  };
package/lib/server.js CHANGED
@@ -1,8 +1,14 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.createMcpServer = exports.getAgentVersion = void 0;
4
7
  const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
5
8
  const zod_1 = require("zod");
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ const node_os_1 = __importDefault(require("node:os"));
11
+ const node_crypto_1 = __importDefault(require("node:crypto"));
6
12
  const logger_1 = require("./logger");
7
13
  const android_1 = require("./android");
8
14
  const robot_1 = require("./robot");
@@ -41,6 +47,7 @@ const createMcpServer = () => {
41
47
  tools: {},
42
48
  },
43
49
  });
50
+ // an empty object to satisfy windsurf
44
51
  const noParams = zod_1.z.object({});
45
52
  const tool = (name, description, paramsSchema, cb) => {
46
53
  const wrappedCb = async (args) => {
@@ -48,6 +55,7 @@ const createMcpServer = () => {
48
55
  (0, logger_1.trace)(`Invoking ${name} with args: ${JSON.stringify(args)}`);
49
56
  const response = await cb(args);
50
57
  (0, logger_1.trace)(`=> ${response}`);
58
+ posthog("tool_invoked", {}).then();
51
59
  return {
52
60
  content: [{ type: "text", text: response }],
53
61
  };
@@ -70,6 +78,39 @@ const createMcpServer = () => {
70
78
  };
71
79
  server.tool(name, description, paramsSchema, args => wrappedCb(args));
72
80
  };
81
+ const posthog = async (event, properties) => {
82
+ try {
83
+ const url = "https://us.i.posthog.com/i/v0/e/";
84
+ const api_key = "phc_KHRTZmkDsU7A8EbydEK8s4lJpPoTDyyBhSlwer694cS";
85
+ const name = node_os_1.default.hostname() + process.execPath;
86
+ const distinct_id = node_crypto_1.default.createHash("sha256").update(name).digest("hex");
87
+ const systemProps = {
88
+ Platform: node_os_1.default.platform(),
89
+ Product: "mobile-mcp",
90
+ Version: (0, exports.getAgentVersion)(),
91
+ NodeVersion: process.version,
92
+ };
93
+ await fetch(url, {
94
+ method: "POST",
95
+ headers: {
96
+ "Content-Type": "application/json"
97
+ },
98
+ body: JSON.stringify({
99
+ api_key,
100
+ event,
101
+ properties: {
102
+ ...systemProps,
103
+ ...properties,
104
+ },
105
+ distinct_id,
106
+ })
107
+ });
108
+ }
109
+ catch (err) {
110
+ // ignore
111
+ }
112
+ };
113
+ posthog("launch", {}).then();
73
114
  let robot;
74
115
  const simulatorManager = new iphone_simulator_1.SimctlManager();
75
116
  const requireRobot = () => {
@@ -77,13 +118,44 @@ const createMcpServer = () => {
77
118
  throw new robot_1.ActionableError("No device selected. Use the mobile_use_device tool to select a device.");
78
119
  }
79
120
  };
121
+ tool("mobile_use_default_device", "Use the default device. This is a shortcut for mobile_use_device with deviceType=simulator and device=simulator_name", {
122
+ noParams
123
+ }, async () => {
124
+ const iosManager = new ios_1.IosManager();
125
+ const androidManager = new android_1.AndroidDeviceManager();
126
+ const simulators = simulatorManager.listBootedSimulators();
127
+ const androidDevices = androidManager.getConnectedDevices();
128
+ const iosDevices = iosManager.listDevices();
129
+ const sum = simulators.length + androidDevices.length + iosDevices.length;
130
+ if (sum === 0) {
131
+ throw new robot_1.ActionableError("No devices found. Please connect a device and try again.");
132
+ }
133
+ else if (sum >= 2) {
134
+ throw new robot_1.ActionableError("Multiple devices found. Please use the mobile_list_available_devices tool to list available devices and select one.");
135
+ }
136
+ // only one device connected, let's find it now
137
+ if (simulators.length === 1) {
138
+ robot = simulatorManager.getSimulator(simulators[0].name);
139
+ return `Selected default device: ${simulators[0].name}`;
140
+ }
141
+ else if (androidDevices.length === 1) {
142
+ robot = new android_1.AndroidRobot(androidDevices[0].deviceId);
143
+ return `Selected default device: ${androidDevices[0].deviceId}`;
144
+ }
145
+ else if (iosDevices.length === 1) {
146
+ robot = new ios_1.IosRobot(iosDevices[0].deviceId);
147
+ return `Selected default device: ${iosDevices[0].deviceId}`;
148
+ }
149
+ // how did this happen?
150
+ throw new robot_1.ActionableError("No device selected. Please use the mobile_list_available_devices tool to list available devices and select one.");
151
+ });
80
152
  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.", {
81
153
  noParams
82
154
  }, async ({}) => {
83
155
  const iosManager = new ios_1.IosManager();
84
156
  const androidManager = new android_1.AndroidDeviceManager();
85
- const devices = simulatorManager.listBootedSimulators();
86
- const simulatorNames = devices.map(d => d.name);
157
+ const simulators = simulatorManager.listBootedSimulators();
158
+ const simulatorNames = simulators.map(d => d.name);
87
159
  const androidDevices = androidManager.getConnectedDevices();
88
160
  const iosDevices = await iosManager.listDevices();
89
161
  const iosDeviceNames = iosDevices.map(d => d.deviceId);
@@ -149,7 +221,7 @@ const createMcpServer = () => {
149
221
  const screenSize = await robot.getScreenSize();
150
222
  return `Screen size is ${screenSize.width}x${screenSize.height} pixels`;
151
223
  });
152
- tool("mobile_click_on_screen_at_coordinates", "Click on the screen at given x,y coordinates", {
224
+ tool("mobile_click_on_screen_at_coordinates", "Click on the screen at given x,y coordinates. If clicking on an element, use the list_elements_on_screen tool to find the coordinates.", {
153
225
  x: zod_1.z.number().describe("The x coordinate to click on the screen, in pixels"),
154
226
  y: zod_1.z.number().describe("The y coordinate to click on the screen, in pixels"),
155
227
  }, async ({ x, y }) => {
@@ -228,6 +300,14 @@ const createMcpServer = () => {
228
300
  }
229
301
  return `Typed text: ${text}`;
230
302
  });
303
+ tool("mobile_save_screenshot", "Save a screenshot of the mobile device to a file", {
304
+ saveTo: zod_1.z.string().describe("The path to save the screenshot to"),
305
+ }, async ({ saveTo }) => {
306
+ requireRobot();
307
+ const screenshot = await robot.getScreenshot();
308
+ node_fs_1.default.writeFileSync(saveTo, screenshot);
309
+ return `Screenshot saved to: ${saveTo}`;
310
+ });
231
311
  server.tool("mobile_take_screenshot", "Take a screenshot of the mobile device. Use this to understand what's on screen, if you need to press an element that is available through view hierarchy then you must list elements on screen instead. Do not cache this result.", {
232
312
  noParams
233
313
  }, async ({}) => {
@@ -13,10 +13,11 @@ class WebDriverAgent {
13
13
  const url = `http://${this.host}:${this.port}/status`;
14
14
  try {
15
15
  const response = await fetch(url);
16
- return response.status === 200;
16
+ const json = await response.json();
17
+ return response.status === 200 && json.value?.ready === true;
17
18
  }
18
19
  catch (error) {
19
- console.error(`Failed to connect to WebDriverAgent: ${error}`);
20
+ // console.error(`Failed to connect to WebDriverAgent: ${error}`);
20
21
  return false;
21
22
  }
22
23
  }
@@ -191,6 +192,12 @@ class WebDriverAgent {
191
192
  });
192
193
  });
193
194
  }
195
+ async getScreenshot() {
196
+ const url = `http://${this.host}:${this.port}/screenshot`;
197
+ const response = await fetch(url);
198
+ const json = await response.json();
199
+ return Buffer.from(json.value, "base64");
200
+ }
194
201
  async swipe(direction) {
195
202
  await this.withinSession(async (sessionUrl) => {
196
203
  const screenSize = await this.getScreenSize(sessionUrl);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mobilenext/mobile-mcp",
3
- "version": "0.0.19",
3
+ "version": "0.0.21",
4
4
  "description": "Mobile MCP",
5
5
  "repository": {
6
6
  "type": "git",
@@ -23,8 +23,6 @@
23
23
  ],
24
24
  "dependencies": {
25
25
  "@modelcontextprotocol/sdk": "^1.6.1",
26
- "@types/commander": "^2.12.0",
27
- "@types/express": "^5.0.3",
28
26
  "commander": "^14.0.0",
29
27
  "express": "^5.1.0",
30
28
  "fast-xml-parser": "^5.0.9",
@@ -34,6 +32,8 @@
34
32
  "@eslint/eslintrc": "^3.2.0",
35
33
  "@eslint/js": "^9.19.0",
36
34
  "@stylistic/eslint-plugin": "^3.0.1",
35
+ "@types/commander": "^2.12.0",
36
+ "@types/express": "^5.0.3",
37
37
  "@types/mocha": "^10.0.10",
38
38
  "@types/node": "^22.13.10",
39
39
  "@typescript-eslint/eslint-plugin": "^8.28.0",