@mobilenext/mobile-mcp 0.0.38 → 0.0.40

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
@@ -28,7 +28,7 @@ This server allows Agents and LLMs to interact with native iOS/Android applicati
28
28
  <a href="https://github.com/mobile-next/mobile-mcp/wiki">
29
29
  <img src="https://img.shields.io/badge/documentation-wiki-blue" alt="wiki" />
30
30
  </a>
31
- <a href="http://mobilenexthq.com/join-slack">
31
+ <a href="https://mobilenexthq.com/join-slack">
32
32
  <img src="https://img.shields.io/badge/join-Slack-blueviolet?logo=slack&style=flat" alt="join on Slack" />
33
33
  </a>
34
34
  </h4>
package/lib/android.js CHANGED
@@ -399,9 +399,9 @@ class AndroidRobot {
399
399
  async tap(x, y) {
400
400
  this.adb("shell", "input", "tap", `${x}`, `${y}`);
401
401
  }
402
- async longPress(x, y) {
402
+ async longPress(x, y, duration) {
403
403
  // a long press is a swipe with no movement and a long duration
404
- this.adb("shell", "input", "swipe", `${x}`, `${y}`, `${x}`, `${y}`, "500");
404
+ this.adb("shell", "input", "swipe", `${x}`, `${y}`, `${x}`, `${y}`, `${duration}`);
405
405
  }
406
406
  async doubleTap(x, y) {
407
407
  await this.tap(x, y);
package/lib/ios.js CHANGED
@@ -145,9 +145,9 @@ class IosRobot {
145
145
  const wda = await this.wda();
146
146
  await wda.doubleTap(x, y);
147
147
  }
148
- async longPress(x, y) {
148
+ async longPress(x, y, duration) {
149
149
  const wda = await this.wda();
150
- await wda.longPress(x, y);
150
+ await wda.longPress(x, y, duration);
151
151
  }
152
152
  async getElementsOnScreen() {
153
153
  const wda = await this.wda();
@@ -192,9 +192,9 @@ class Simctl {
192
192
  const wda = await this.wda();
193
193
  await wda.doubleTap(x, y);
194
194
  }
195
- async longPress(x, y) {
195
+ async longPress(x, y, duration) {
196
196
  const wda = await this.wda();
197
- return wda.longPress(x, y);
197
+ return wda.longPress(x, y, duration);
198
198
  }
199
199
  async pressButton(button) {
200
200
  const wda = await this.wda();
@@ -109,8 +109,8 @@ class MobileDevice {
109
109
  await this.tap(x, y);
110
110
  await this.tap(x, y);
111
111
  }
112
- async longPress(x, y) {
113
- this.runCommand(["io", "longpress", `${x},${y}`]);
112
+ async longPress(x, y, duration) {
113
+ this.runCommand(["io", "longpress", `${x},${y}`, "--duration", `${duration}`]);
114
114
  }
115
115
  async getElementsOnScreen() {
116
116
  const response = JSON.parse(this.runCommand(["dump", "ui"]));
package/lib/server.js CHANGED
@@ -39,11 +39,12 @@ const createMcpServer = () => {
39
39
  return "unknown";
40
40
  }
41
41
  };
42
- const tool = (name, title, description, paramsSchema, cb) => {
42
+ const tool = (name, title, description, paramsSchema, annotations, cb) => {
43
43
  server.registerTool(name, {
44
44
  title,
45
45
  description,
46
46
  inputSchema: paramsSchema,
47
+ annotations,
47
48
  }, (async (args, _extra) => {
48
49
  try {
49
50
  (0, logger_1.trace)(`Invoking ${name} with args: ${JSON.stringify(args)}`);
@@ -157,7 +158,7 @@ const createMcpServer = () => {
157
158
  };
158
159
  tool("mobile_list_available_devices", "List 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.", {
159
160
  noParams
160
- }, async ({}) => {
161
+ }, { readOnlyHint: true }, async ({}) => {
161
162
  // from today onward, we must have mobilecli working
162
163
  ensureMobilecliAvailable();
163
164
  const iosManager = new ios_1.IosManager();
@@ -215,7 +216,7 @@ const createMcpServer = () => {
215
216
  });
216
217
  tool("mobile_list_apps", "List Apps", "List all the installed apps on the device", {
217
218
  device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.")
218
- }, async ({ device }) => {
219
+ }, { readOnlyHint: true }, async ({ device }) => {
219
220
  const robot = getRobotFromDevice(device);
220
221
  const result = await robot.listApps();
221
222
  return `Found these apps on device: ${result.map(app => `${app.appName} (${app.packageName})`).join(", ")}`;
@@ -223,7 +224,7 @@ const createMcpServer = () => {
223
224
  tool("mobile_launch_app", "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.", {
224
225
  device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
225
226
  packageName: zod_1.z.string().describe("The package name of the app to launch"),
226
- }, async ({ device, packageName }) => {
227
+ }, { destructiveHint: true }, async ({ device, packageName }) => {
227
228
  const robot = getRobotFromDevice(device);
228
229
  await robot.launchApp(packageName);
229
230
  return `Launched app ${packageName}`;
@@ -231,7 +232,7 @@ const createMcpServer = () => {
231
232
  tool("mobile_terminate_app", "Terminate App", "Stop and terminate an app on mobile device", {
232
233
  device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
233
234
  packageName: zod_1.z.string().describe("The package name of the app to terminate"),
234
- }, async ({ device, packageName }) => {
235
+ }, { destructiveHint: true }, async ({ device, packageName }) => {
235
236
  const robot = getRobotFromDevice(device);
236
237
  await robot.terminateApp(packageName);
237
238
  return `Terminated app ${packageName}`;
@@ -239,7 +240,7 @@ const createMcpServer = () => {
239
240
  tool("mobile_install_app", "Install App", "Install an app on mobile device", {
240
241
  device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
241
242
  path: zod_1.z.string().describe("The path to the app file to install. For iOS simulators, provide a .zip file or a .app directory. For Android provide an .apk file. For iOS real devices provide an .ipa file"),
242
- }, async ({ device, path }) => {
243
+ }, { destructiveHint: true }, async ({ device, path }) => {
243
244
  const robot = getRobotFromDevice(device);
244
245
  await robot.installApp(path);
245
246
  return `Installed app from ${path}`;
@@ -247,14 +248,14 @@ const createMcpServer = () => {
247
248
  tool("mobile_uninstall_app", "Uninstall App", "Uninstall an app from mobile device", {
248
249
  device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
249
250
  bundle_id: zod_1.z.string().describe("Bundle identifier (iOS) or package name (Android) of the app to be uninstalled"),
250
- }, async ({ device, bundle_id }) => {
251
+ }, { destructiveHint: true }, async ({ device, bundle_id }) => {
251
252
  const robot = getRobotFromDevice(device);
252
253
  await robot.uninstallApp(bundle_id);
253
254
  return `Uninstalled app ${bundle_id}`;
254
255
  });
255
256
  tool("mobile_get_screen_size", "Get Screen Size", "Get the screen size of the mobile device in pixels", {
256
257
  device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.")
257
- }, async ({ device }) => {
258
+ }, { readOnlyHint: true }, async ({ device }) => {
258
259
  const robot = getRobotFromDevice(device);
259
260
  const screenSize = await robot.getScreenSize();
260
261
  return `Screen size is ${screenSize.width}x${screenSize.height} pixels`;
@@ -263,7 +264,7 @@ const createMcpServer = () => {
263
264
  device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
264
265
  x: zod_1.z.number().describe("The x coordinate to click on the screen, in pixels"),
265
266
  y: zod_1.z.number().describe("The y coordinate to click on the screen, in pixels"),
266
- }, async ({ device, x, y }) => {
267
+ }, { destructiveHint: true }, async ({ device, x, y }) => {
267
268
  const robot = getRobotFromDevice(device);
268
269
  await robot.tap(x, y);
269
270
  return `Clicked on screen at coordinates: ${x}, ${y}`;
@@ -272,7 +273,7 @@ const createMcpServer = () => {
272
273
  device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
273
274
  x: zod_1.z.number().describe("The x coordinate to double-tap, in pixels"),
274
275
  y: zod_1.z.number().describe("The y coordinate to double-tap, in pixels"),
275
- }, async ({ device, x, y }) => {
276
+ }, { destructiveHint: true }, async ({ device, x, y }) => {
276
277
  const robot = getRobotFromDevice(device);
277
278
  await robot.doubleTap(x, y);
278
279
  return `Double-tapped on screen at coordinates: ${x}, ${y}`;
@@ -281,14 +282,16 @@ const createMcpServer = () => {
281
282
  device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
282
283
  x: zod_1.z.number().describe("The x coordinate to long press on the screen, in pixels"),
283
284
  y: zod_1.z.number().describe("The y coordinate to long press on the screen, in pixels"),
284
- }, async ({ device, x, y }) => {
285
+ duration: zod_1.z.number().min(1).max(10000).optional().describe("Duration of the long press in milliseconds. Defaults to 500ms."),
286
+ }, { destructiveHint: true }, async ({ device, x, y, duration }) => {
285
287
  const robot = getRobotFromDevice(device);
286
- await robot.longPress(x, y);
287
- return `Long pressed on screen at coordinates: ${x}, ${y}`;
288
+ const pressDuration = duration ?? 500;
289
+ await robot.longPress(x, y, pressDuration);
290
+ return `Long pressed on screen at coordinates: ${x}, ${y} for ${pressDuration}ms`;
288
291
  });
289
292
  tool("mobile_list_elements_on_screen", "List Screen Elements", "List elements on screen and their coordinates, with display text or accessibility label. Do not cache this result.", {
290
293
  device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.")
291
- }, async ({ device }) => {
294
+ }, { readOnlyHint: true }, async ({ device }) => {
292
295
  const robot = getRobotFromDevice(device);
293
296
  const elements = await robot.getElementsOnScreen();
294
297
  const result = elements.map(element => {
@@ -316,7 +319,7 @@ const createMcpServer = () => {
316
319
  tool("mobile_press_button", "Press Button", "Press a button on device", {
317
320
  device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
318
321
  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)"),
319
- }, async ({ device, button }) => {
322
+ }, { destructiveHint: true }, async ({ device, button }) => {
320
323
  const robot = getRobotFromDevice(device);
321
324
  await robot.pressButton(button);
322
325
  return `Pressed the button: ${button}`;
@@ -324,7 +327,7 @@ const createMcpServer = () => {
324
327
  tool("mobile_open_url", "Open URL", "Open a URL in browser on device", {
325
328
  device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
326
329
  url: zod_1.z.string().describe("The URL to open"),
327
- }, async ({ device, url }) => {
330
+ }, { destructiveHint: true }, async ({ device, url }) => {
328
331
  const robot = getRobotFromDevice(device);
329
332
  await robot.openUrl(url);
330
333
  return `Opened URL: ${url}`;
@@ -335,7 +338,7 @@ const createMcpServer = () => {
335
338
  x: zod_1.z.number().optional().describe("The x coordinate to start the swipe from, in pixels. If not provided, uses center of screen"),
336
339
  y: zod_1.z.number().optional().describe("The y coordinate to start the swipe from, in pixels. If not provided, uses center of screen"),
337
340
  distance: zod_1.z.number().optional().describe("The distance to swipe in pixels. Defaults to 400 pixels for iOS or 30% of screen dimension for Android"),
338
- }, async ({ device, direction, x, y, distance }) => {
341
+ }, { destructiveHint: true }, async ({ device, direction, x, y, distance }) => {
339
342
  const robot = getRobotFromDevice(device);
340
343
  if (x !== undefined && y !== undefined) {
341
344
  // Use coordinate-based swipe
@@ -353,7 +356,7 @@ const createMcpServer = () => {
353
356
  device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
354
357
  text: zod_1.z.string().describe("The text to type"),
355
358
  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."),
356
- }, async ({ device, text, submit }) => {
359
+ }, { destructiveHint: true }, async ({ device, text, submit }) => {
357
360
  const robot = getRobotFromDevice(device);
358
361
  await robot.sendKeys(text);
359
362
  if (submit) {
@@ -364,7 +367,7 @@ const createMcpServer = () => {
364
367
  tool("mobile_save_screenshot", "Save Screenshot", "Save a screenshot of the mobile device to a file", {
365
368
  device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
366
369
  saveTo: zod_1.z.string().describe("The path to save the screenshot to"),
367
- }, async ({ device, saveTo }) => {
370
+ }, { destructiveHint: true }, async ({ device, saveTo }) => {
368
371
  const robot = getRobotFromDevice(device);
369
372
  const screenshot = await robot.getScreenshot();
370
373
  node_fs_1.default.writeFileSync(saveTo, screenshot);
@@ -375,7 +378,10 @@ const createMcpServer = () => {
375
378
  description: "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.",
376
379
  inputSchema: {
377
380
  device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.")
378
- }
381
+ },
382
+ annotations: {
383
+ readOnlyHint: true,
384
+ },
379
385
  }, async ({ device }) => {
380
386
  try {
381
387
  const robot = getRobotFromDevice(device);
@@ -423,14 +429,14 @@ const createMcpServer = () => {
423
429
  tool("mobile_set_orientation", "Set Orientation", "Change the screen orientation of the device", {
424
430
  device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you."),
425
431
  orientation: zod_1.z.enum(["portrait", "landscape"]).describe("The desired orientation"),
426
- }, async ({ device, orientation }) => {
432
+ }, { destructiveHint: true }, async ({ device, orientation }) => {
427
433
  const robot = getRobotFromDevice(device);
428
434
  await robot.setOrientation(orientation);
429
435
  return `Changed device orientation to ${orientation}`;
430
436
  });
431
437
  tool("mobile_get_orientation", "Get Orientation", "Get the current screen orientation of the device", {
432
438
  device: zod_1.z.string().describe("The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.")
433
- }, async ({ device }) => {
439
+ }, { readOnlyHint: true }, async ({ device }) => {
434
440
  const robot = getRobotFromDevice(device);
435
441
  const orientation = await robot.getOrientation();
436
442
  return `Current device orientation is ${orientation}`;
@@ -172,7 +172,7 @@ class WebDriverAgent {
172
172
  });
173
173
  });
174
174
  }
175
- async longPress(x, y) {
175
+ async longPress(x, y, duration) {
176
176
  await this.withinSession(async (sessionUrl) => {
177
177
  const url = `${sessionUrl}/actions`;
178
178
  await fetch(url, {
@@ -189,7 +189,7 @@ class WebDriverAgent {
189
189
  actions: [
190
190
  { type: "pointerMove", duration: 0, x, y },
191
191
  { type: "pointerDown", button: 0 },
192
- { type: "pause", duration: 500 },
192
+ { type: "pause", duration },
193
193
  { type: "pointerUp", button: 0 }
194
194
  ]
195
195
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mobilenext/mobile-mcp",
3
3
  "mcpName": "io.github.mobile-next/mobile-mcp",
4
- "version": "0.0.38",
4
+ "version": "0.0.40",
5
5
  "description": "Mobile MCP",
6
6
  "repository": {
7
7
  "type": "git",
@@ -24,7 +24,7 @@
24
24
  "lib"
25
25
  ],
26
26
  "dependencies": {
27
- "@modelcontextprotocol/sdk": "1.24.2",
27
+ "@modelcontextprotocol/sdk": "1.25.2",
28
28
  "commander": "14.0.0",
29
29
  "express": "5.1.0",
30
30
  "fast-xml-parser": "5.2.5",
@@ -32,7 +32,7 @@
32
32
  "zod-to-json-schema": "3.25.0"
33
33
  },
34
34
  "optionalDependencies": {
35
- "@mobilenext/mobilecli": "0.0.46"
35
+ "@mobilenext/mobilecli": "0.0.49"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@eslint/eslintrc": "^3.2.0",