@nbakka/mcp-appium 2.0.20 → 2.0.22

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.
@@ -0,0 +1,21 @@
1
+ When asked to open buy SRP, use deeplink to open https://housing.com/in/buy/mumbai/<locality name>. For example: if asked to open 'Mira Road East' buy SRP, then open this deeplink: https://housing.com/in/buy/mumbai/mira_road_east
2
+ Homepage deeplink -> https://housing.com/in/buy/real-estate-mumbai
3
+ Project dedicated page deeplink -> https://housing.com/in/buy/projects/page/319874-sunteck-sky-park-by-sunteck-realty-limited-in-mira-road-east
4
+ Filters on the filter screen may initially be offscreen and require scrolling to view.
5
+ Stop execution if any step fails.
6
+ (Remember this properly - Very important) After the execution is successful/completed, do NOT analyze what you found or did; only generate Appium code.
7
+ Print the Java/Appium code. In our framework, we use the following methods:
8
+ - waitForElementToBeVisible("locator_key", Wait.SHORT); locator_key is locator key and wait can be SHORT, MEDIUM, or LONG.
9
+ - scrollDown(n); // n is the number of times to scroll.
10
+ - navigateBack(); // for back action.
11
+ - deepLink.OpenDeepLink("deeplink_key"); // deeplink_key is deeplink name.
12
+ - getText("locator_key"); // to get text of element.
13
+ Using the above, return code that looks like:
14
+ deepLink.OpenDeepLink(deeplink); // deeplink -> https://housing.com
15
+ waitForElementToBeVisible("view_on_map", Wait.LONG); // view_on_map=//android.widget.TextView[@text="View On Map"]; here xpath should be comments so it can be pasted in xpath sheet.
16
+ When giving output, please remove escape characters before double quotes.
17
+ Do similarly for click and sendKeys methods:
18
+ - click("search_select")
19
+ - sendKeys("locator_key", "string_to_be_passed", Wait.LONG)
20
+ Figure out dynamic paths like //android.widget.TextView[@text="View 180 Properties"] here 180 is not constant so create a xpath like //android.widget.TextView[contains(@text,"View") and contains(@text,"Properties")]
21
+ For repeating logic, create loops
@@ -0,0 +1,13 @@
1
+ {
2
+ "type": "service_account",
3
+ "project_id": "gen-lang-client-0998595520",
4
+ "private_key_id": "99b60e861b2815a0b7e4946fa0697d70f157b4a6",
5
+ "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCylqcylIgnlYFv\nSDSNA5w54FBTGKqwhg55bUYZoO3PYvXywgUDUO/q7jSLIYOLzQb+yiSKOBbJKTrs\ng/HP8Qr/vGoLJBXmEmhheh2YEip2uDERTeIaC9ilyWoDgRa8bWnj2DKUVlEAKK29\nK2cl7k3sejS9QNXW7b38ov3qb4uQK4tBy28rlUs2SpElNsMbO+RJOWxSuRqlWXER\nFD2alSmWB8ZB6ugHeOXsx5kKkNzDlS2wDKQJqZNWqga8LpSl+9lENrdi59qKuRgq\n053N5M/WLGJWyg8W2BWXMhZGCak1NcgPanQ/vFLO2mIljowb8mxtNML6rZfLawKp\nbFre03cZAgMBAAECggEAAJTwD4CfXu2kQc4M6cawvBmIeXTTnYaaRnf+Kfg1vh62\n6WcLaCLb3TqQbkGaMZrl4m+bJd4f6ODbAck+QOyqmgGtoMKp1ZmKwIKK1SN+Auv/\nsOw9h9MxDf01fLRvdAaxOR5Lr+MGGYeIZVzeVafMljAhutoauDwHCesoA5sAa+RP\nBzIa7+N6EmGt/FvAdKAvSuuZZsMUHLoOiHeB3knwjqoKJCHVMNWqpeMyqclIjR+O\nIv37jhlj2z7pbR0n6hfjYQLmGRfSRGvmw1hrDV1ije4RXeNs5/13gUokNf4kBhqn\nQ+t4eI6UEa82lgLS7Msmbcyyh2h2TfFG1Ygx6nYZHwKBgQDhUzdMjCtADBvQnSQw\nV+qWPzOJFdYzURgg1zMXUGnPws8vuUMjuCNEu3AUrtjvc5RVdePAIyN6o/j5HLAV\nt6qksoQkUas/daCzuRG9IWFw2eI0rhs+paMJVIUJQWXt60afgv61rsvLfIC/MBi9\ntl62xI8FNDWzPgSVWTXukIhOzwKBgQDK5p/3dr/S+oSk6E/y1bwejNm8b0VgqvwO\nP9OO2kmDEXTGSlXX5Mn1qW7Wg1n+z5NrDkA3IyRtEbILyylUELooGXVKiKPkiJpE\n+Qi+wsVqmThvqO6cYl8FW64/0UH88ZcvDjyDLfUsei4lY0hRjafTshaoqk0SOyq9\n4UOs3ioVlwKBgHYYtHoVWTHHZuivA+Gmopg+5dbqsArTbQ8BW5DTn7G5y/eaZRsa\njrmed/8PKTpPXKZyFH2GrTjBKmP+ajfnvLN3sRSMDXJER4cK78Yt8bFBMXMk8bii\n/dGND/Eq6q6JSsmd0bwNslijl6MdJUqBhCDM4pz6oU6hqatRR5gS/q43AoGBAIsp\nNGwQyS4V3mYQY80kpNq7NhdUpdvQSgIn6pzewG6hyVq63zes1oukQr3j5xSqH+zc\nIFTwyGn6KgiGtfjPZC5ej6CoKOh0fIJz33ies7ISFrAWyFj/6zYMlG12w3CN7mg6\ntmwuWCrCPeYsuwwcQRAj5ACYlTW82OrUlor48RpPAoGALtMf3SEALRuKvpSqDxpe\nw/XkD098Lq4g+uFbFfN0aL1gyuWvwQ4KtIkmMpENWYC/zFVH79AAWSA7gvEEWE0C\n3FP2lI29GgXPhG+EDS3/dvjY4UPcqDVMFYAen4Z4X0FnKa8Yr5rGPuYtZCsglGmh\nigkENMmOUL6T62YvJATeFd8=\n-----END PRIVATE KEY-----\n",
6
+ "client_email": "mcp-sheet-access@gen-lang-client-0998595520.iam.gserviceaccount.com",
7
+ "client_id": "104386077369721430122",
8
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
9
+ "token_uri": "https://oauth2.googleapis.com/token",
10
+ "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
11
+ "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/mcp-sheet-access%40gen-lang-client-0998595520.iam.gserviceaccount.com",
12
+ "universe_domain": "googleapis.com"
13
+ }
package/lib/server.js CHANGED
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createMcpServer = void 0;
4
+ const fs = require('fs').promises;
5
+ const path = require('path');
4
6
  const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
5
7
  const zod_1 = require("zod");
6
8
  const logger_1 = require("./logger");
@@ -10,28 +12,15 @@ const iphone_simulator_1 = require("./iphone-simulator");
10
12
  const ios_1 = require("./ios");
11
13
  const png_1 = require("./png");
12
14
  const image_utils_1 = require("./image-utils");
15
+ const { google } = require('googleapis');
13
16
  const getAgentVersion = () => {
14
17
  const json = require("../package.json");
15
18
  return json.version;
16
19
  };
17
-
18
- const getLatestAgentVersion = async () => {
19
- const response = await fetch("https://api.github.com/repos/mobile-next/mobile-mcp/tags?per_page=1");
20
- const json = await response.json();
21
- return json[0].name;
22
- };
23
- const checkForLatestAgentVersion = async () => {
24
- try {
25
- const latestVersion = await getLatestAgentVersion();
26
- const currentVersion = getAgentVersion();
27
- if (latestVersion !== currentVersion) {
28
- (0, logger_1.trace)(`You are running an older version of the agent. Please update to the latest version: ${latestVersion}.`);
29
- }
30
- }
31
- catch (error) {
32
- // ignore
33
- }
20
+ const selectedSheetContext = {
21
+ sheetName: null,
34
22
  };
23
+
35
24
  const createMcpServer = () => {
36
25
  const server = new mcp_js_1.McpServer({
37
26
  name: "mobile-mcp",
@@ -41,6 +30,7 @@ const createMcpServer = () => {
41
30
  tools: {},
42
31
  },
43
32
  });
33
+
44
34
  const tool = (name, description, paramsSchema, cb) => {
45
35
  const wrappedCb = async (args) => {
46
36
  try {
@@ -76,6 +66,7 @@ const createMcpServer = () => {
76
66
  throw new robot_1.ActionableError("No device selected. Use the mobile_use_device tool to select a device.");
77
67
  }
78
68
  };
69
+
79
70
  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 ({}) => {
80
71
  const iosManager = new ios_1.IosManager();
81
72
  const androidManager = new android_1.AndroidDeviceManager();
@@ -101,6 +92,7 @@ const createMcpServer = () => {
101
92
  }
102
93
  return resp.join("\n");
103
94
  });
95
+
104
96
  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.", {
105
97
  device: zod_1.z.string().describe("The name of the device to select"),
106
98
  deviceType: zod_1.z.enum(["simulator", "ios", "android"]).describe("The type of device to select"),
@@ -118,11 +110,13 @@ const createMcpServer = () => {
118
110
  }
119
111
  return `Selected device: ${device}`;
120
112
  });
113
+
121
114
  tool("mobile_list_apps", "List all the installed apps on the device", {}, async ({}) => {
122
115
  requireRobot();
123
116
  const result = await robot.listApps();
124
117
  return `Found these apps on device: ${result.map(app => `${app.appName} (${app.packageName})`).join(", ")}`;
125
118
  });
119
+
126
120
  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.", {
127
121
  packageName: zod_1.z.string().describe("The package name of the app to launch"),
128
122
  }, async ({ packageName }) => {
@@ -130,6 +124,7 @@ const createMcpServer = () => {
130
124
  await robot.launchApp(packageName);
131
125
  return `Launched app ${packageName}`;
132
126
  });
127
+
133
128
  tool("mobile_terminate_app", "Stop and terminate an app on mobile device", {
134
129
  packageName: zod_1.z.string().describe("The package name of the app to terminate"),
135
130
  }, async ({ packageName }) => {
@@ -137,54 +132,67 @@ const createMcpServer = () => {
137
132
  await robot.terminateApp(packageName);
138
133
  return `Terminated app ${packageName}`;
139
134
  });
135
+
140
136
  tool("mobile_get_screen_size", "Get the screen size of the mobile device in pixels", {}, async ({}) => {
141
137
  requireRobot();
142
138
  const screenSize = await robot.getScreenSize();
143
139
  return `Screen size is ${screenSize.width}x${screenSize.height} pixels`;
144
140
  });
145
- tool(
146
- "mobile_learn_current_app_context",
147
- "Follow this instructions strictly to navigate through the app. This should be the first step after launching the app and before performing any tests.",
148
- {},
149
- async () => {
150
- const context = {
151
- notes: ["when asked to open buy srp use deeeplink to open https://housing.com/in/buy/mumbai/<locality name> For eg: if asked to open 'Mira Road East' buy srp then open following deep link https://housing.com/in/buy/mumbai/mira_road_east", "homepade deeplink-> https://housing.com/in/buy/real-estate-mumbai and project dedicated page deeplink -> https://housing.com/in/buy/projects/page/319874-sunteck-sky-park-by-sunteck-realty-limited-in-mira-road-east",
152
- "Filters on the filter screen may initially be offscreen and require scrolling to view.",
153
- "Stop execution if any step fails", "(Remember this properly - Very important)After the execution is successful/completed, no need to analyze what you found or what you did, only generate appium code",
154
- "print the java/appium code, in our framework we use following methods waitForElementToBeVisible(\"locator_key\", Wait.SHORT); locator_key is locator key and wait can be SHORT, MEDIUM and LONG, scrollDown(n); -> n is number of times to scroll, navigateBack() for back action, deepLink.OpenDeepLink(\"deeplink_key\") -> deeplink_key is deep link name, getText(\"locator_key\") -> to get text of element. Using the above return code that looks like deepLink.OpenDeepLink(deeplink); //deeplink -> https://housing.com \n waitForElementToBeVisible(\"view_on_map\", Wait.LONG); //view_on_map=//android.widget.TextView[@text=\"View On Map\"]; here xpath should be comments so that I can paste this in xpath sheet and when giving output please remove escape characters before double quotes, do similar for click and sendKeys methods click(\"search_select\") and sendKeys(\"locator_key\", \"string_to_be_passed\", Wait.LONG)",
155
-
156
- ],
157
- };
158
141
 
159
- return `App context learned: ${JSON.stringify(context)}`;
160
- }
161
- );
142
+ const fs = require('fs').promises;
143
+ const path = require('path');
162
144
 
145
+ tool(
146
+ "mobile_learn_current_app_context",
147
+ "Follow this instructions strictly to navigate through the app. This should be the first step after launching the app and before performing any tests.",
148
+ {},
149
+ async () => {
150
+ try {
151
+ // Read file from config folder
152
+ const filePath = path.join(__dirname, 'lib', 'app_context.txt');
153
+ const fileContent = await fs.readFile(filePath, 'utf-8');
163
154
 
164
- tool("mobile_list_elements_on_screen", "List elements on screen and their coordinates, with display text or accessibility label. Do not cache this result.", {}, async ({}) => {
165
- requireRobot();
166
- const elements = await robot.getElementsOnScreen();
167
- const result = elements.map(element => {
168
- const out = {
169
- type: element.type,
170
- text: element.text,
171
- label: element.label,
172
- name: element.name,
173
- value: element.value,
174
- coordinates: {
175
- x: element.rect.x,
176
- y: element.rect.y,
177
- width: element.rect.width,
178
- height: element.rect.height,
179
- },
180
- };
181
- if (element.focused) {
182
- out.focused = true;
183
- }
184
- return out;
155
+ // Assuming each note is separated by a newline
156
+ // Trim to remove empty lines, filter out blank lines
157
+ const notes = fileContent
158
+ .split('\n')
159
+ .map(line => line.trim())
160
+ .filter(line => line.length > 0);
161
+
162
+ const context = { notes };
163
+
164
+ return `App context learned: ${JSON.stringify(context)}`;
165
+ } catch (error) {
166
+ // Handle file read errors gracefully
167
+ return `Error reading app context notes: ${error.message}`;
168
+ }
169
+ }
170
+ );
171
+
172
+ tool("mobile_list_elements_on_screen", "List elements on screen and their coordinates, with display text or accessibility label. Do not cache this result.", {}, async ({}) => {
173
+ requireRobot();
174
+ const elements = await robot.getElementsOnScreen();
175
+ const result = elements.map(element => {
176
+ const out = {
177
+ type: element.type,
178
+ text: element.text,
179
+ label: element.label,
180
+ name: element.name,
181
+ value: element.value,
182
+ coordinates: {
183
+ x: element.rect.x,
184
+ y: element.rect.y,
185
+ width: element.rect.width,
186
+ height: element.rect.height,
187
+ },
188
+ };
189
+ if (element.focused) {
190
+ out.focused = true;
191
+ }
192
+ return out;
193
+ });
194
+ return `Found these elements on screen: ${JSON.stringify(result)}`;
185
195
  });
186
- return `Found these elements on screen: ${JSON.stringify(result)}`;
187
- });
188
196
 
189
197
  tool("mobile_press_button", "Press a button on device", {
190
198
  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)"),
@@ -193,14 +201,16 @@ tool("mobile_list_elements_on_screen", "List elements on screen and their coordi
193
201
  await robot.pressButton(button);
194
202
  return `Pressed the button: ${button}`;
195
203
  });
204
+
196
205
  tool("mobile_open_url", "Open a URL in browser on device", {
197
206
  url: zod_1.z.string().describe("The URL to open"),
198
207
  }, async ({ url }) => {
199
208
  requireRobot();
200
209
  await robot.openUrl(url);
201
- await new Promise(resolve => setTimeout(resolve, 5000));
210
+ await new Promise(resolve => setTimeout(resolve, 5000));
202
211
  return `Opened URL: ${url}`;
203
212
  });
213
+
204
214
  tool("swipe_on_screen", "Swipe on the screen", {
205
215
  direction: zod_1.z.enum(["up", "down"]).describe("The direction to swipe, up direction means it will from bottom part of the screen to top part of screen, essentially moving the screen content up and opposite happens for down"),
206
216
  }, async ({ direction }) => {
@@ -208,6 +218,7 @@ await new Promise(resolve => setTimeout(resolve, 5000));
208
218
  await robot.swipe(direction);
209
219
  return `Swiped ${direction} on screen`;
210
220
  });
221
+
211
222
  tool("mobile_type_keys", "Type text into the focused element", {
212
223
  text: zod_1.z.string().describe("The text to type"),
213
224
  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."),
@@ -219,6 +230,7 @@ await new Promise(resolve => setTimeout(resolve, 5000));
219
230
  }
220
231
  return `Typed text: ${text}`;
221
232
  });
233
+
222
234
  tool("mobile_set_orientation", "Change the screen orientation of the device", {
223
235
  orientation: zod_1.z.enum(["portrait", "landscape"]).describe("The desired orientation"),
224
236
  }, async ({ orientation }) => {
@@ -226,44 +238,119 @@ await new Promise(resolve => setTimeout(resolve, 5000));
226
238
  await robot.setOrientation(orientation);
227
239
  return `Changed device orientation to ${orientation}`;
228
240
  });
241
+
229
242
  tool("mobile_get_orientation", "Get the current screen orientation of the device", {}, async () => {
230
243
  requireRobot();
231
244
  const orientation = await robot.getOrientation();
232
245
  return `Current device orientation is ${orientation}`;
233
246
  });
234
- tool(
235
- "mobile_tap_by_text",
236
- "Tap an element on screen by its displayed text or accessibility label using ADB tap",
237
- {
238
- text: zod_1.z.string().describe("The exact text or label of the element to tap"),
239
- },
240
- async ({ text }) => {
241
- if (!text) throw new Error("Input text is required");
242
-
243
- requireRobot(); // ensure robot instance available
244
- const elements = await robot.getElementsOnScreen();
245
-
246
- // Find element by exact match on text or label
247
- const element = elements.find(
248
- el => el.text === text || el.label === text
247
+
248
+ tool(
249
+ "mobile_tap_by_text",
250
+ "Tap an element on screen by its displayed text or accessibility label using ADB tap",
251
+ {
252
+ text: zod_1.z.string().describe("The exact text or label of the element to tap"),
253
+ },
254
+ async ({ text }) => {
255
+ if (!text) throw new Error("Input text is required");
256
+
257
+ requireRobot(); // ensure robot instance available
258
+ const elements = await robot.getElementsOnScreen();
259
+
260
+ // Find element by exact match on text or label
261
+ const element = elements.find(
262
+ el => el.text === text || el.label === text
263
+ );
264
+
265
+ if (!element) throw new Error(`Element with text or label "${text}" not found`);
266
+
267
+ // Calculate center coordinates
268
+ const rect = element.rect;
269
+ const x = Math.floor(rect.x + rect.width / 2);
270
+ const y = Math.floor(rect.y + rect.height / 2);
271
+
272
+ // Execute adb tap
273
+ const { execSync } = require("child_process");
274
+ execSync(`adb shell input tap ${x} ${y}`);
275
+
276
+ return `Tapped element with text/label "${text}" at (${x},${y})`;
277
+ }
249
278
  );
250
279
 
251
- if (!element) throw new Error(`Element with text or label "${text}" not found`);
280
+ tool(
281
+ "select_google_sheet",
282
+ "MUST be called after mobile_learn_current_app_context to set the Google Sheet name to be used by fetch_google_sheet_locators.",
283
+ {
284
+ sheetName: { type: "string", description: "Name of the sheet/tab to fetch from" }
285
+ },
286
+ async ({ sheetName }) => {
287
+ if (!sheetName) {
288
+ return "Error: sheetName is required.";
289
+ }
290
+ selectedSheetContext.sheetName = sheetName.trim();
291
+ return `Sheet name "${selectedSheetContext.sheetName}" saved. Now call fetch_google_sheet_locators to fetch data.`;
292
+ }
293
+ );
252
294
 
253
- // Calculate center coordinates
254
- const rect = element.rect;
255
- const x = Math.floor(rect.x + rect.width / 2);
256
- const y = Math.floor(rect.y + rect.height / 2);
295
+ // Tool 2: fetch locator data using previously selected sheet name
296
+ tool(
297
+ "fetch_google_sheet_locators",
298
+ "MUST be called after select_google_sheet. Fetch locatorName and androidLocator from the sheet set by select_google_sheet. Please call select_google_sheet first.",
299
+ {},
300
+ async () => {
301
+ const sheetName = selectedSheetContext.sheetName;
302
+ if (!sheetName) {
303
+ return "Error: Sheet name not set. Please call select_google_sheet first.";
304
+ }
305
+
306
+ try {
307
+ const fs = require('fs').promises;
308
+ const path = require('path');
309
+ const { google } = require('googleapis');
310
+
311
+ // Load credentials
312
+ const keyFile = path.join(__dirname, 'lib', 'secret.json');
313
+ const credentials = JSON.parse(await fs.readFile(keyFile, 'utf-8'));
314
+
315
+ const auth = new google.auth.GoogleAuth({
316
+ credentials,
317
+ scopes: ['https://www.googleapis.com/auth/spreadsheets.readonly'],
318
+ });
319
+
320
+ const authClient = await auth.getClient();
321
+ const sheets = google.sheets({ version: 'v4', auth: authClient });
322
+ const spreadsheetId = '1UapR81AxaztDUlPGDV-_EwHo2hWXkKCZXl8ALsvIyxA';
323
+
324
+ const range = `${sheetName}!A1:Z1000`;
325
+ const res = await sheets.spreadsheets.values.get({ spreadsheetId, range });
326
+ const rows = res.data.values;
257
327
 
258
- // Execute adb tap
259
- const { execSync } = require("child_process");
260
- execSync(`adb shell input tap ${x} ${y}`);
328
+ if (!rows || rows.length === 0) {
329
+ return `Sheet "${sheetName}" is empty or does not exist.`;
330
+ }
331
+
332
+ const header = rows[0].map(h => h.toString().toLowerCase());
333
+ const locatorNameIdx = header.indexOf('locatorName');
334
+ const androidLocatorIdx = header.findIndex(h => h === 'android xpath' || h === 'androidLocator');
335
+
336
+ if (locatorNameIdx === -1 || androidLocatorIdx === -1) {
337
+ return `Required columns "locatorName" and/or "androidXpath" not found in sheet "${sheetName}".`;
338
+ }
339
+
340
+ const extracted = rows.slice(1)
341
+ .filter(row => row[locatorNameIdx] && row[androidLocatorIdx])
342
+ .map(row => ({
343
+ locatorName: row[locatorNameIdx],
344
+ androidLocator: row[androidLocatorIdx],
345
+ }));
346
+
347
+ return extracted;
348
+ } catch (error) {
349
+ return `Error fetching data from Google Sheets: ${error.message}`;
350
+ }
351
+ }
352
+ );
261
353
 
262
- return `Tapped element with text/label "${text}" at (${x},${y})`;
263
- }
264
- );
265
- // async check for latest agent version
266
- checkForLatestAgentVersion().then();
267
354
  return server;
268
355
  };
269
356
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nbakka/mcp-appium",
3
- "version": "2.0.20",
3
+ "version": "2.0.22",
4
4
  "description": "Appium MCP",
5
5
  "engines": {
6
6
  "node": ">=18"
@@ -20,7 +20,8 @@
20
20
  "dependencies": {
21
21
  "@modelcontextprotocol/sdk": "^1.6.1",
22
22
  "fast-xml-parser": "^5.0.9",
23
- "zod-to-json-schema": "^3.24.4"
23
+ "zod-to-json-schema": "^3.24.4",
24
+ "googleapis": "^39.2.0"
24
25
  },
25
26
  "devDependencies": {
26
27
  "@eslint/eslintrc": "^3.2.0",