@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.
- package/lib/app_context.txt +21 -0
- package/lib/secret.json +13 -0
- package/lib/server.js +172 -85
- package/package.json +3 -2
|
@@ -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
|
package/lib/secret.json
ADDED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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.
|
|
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",
|