@nbakka/mcp-appium 2.0.7 → 2.0.8
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/server.js +52 -53
- package/package.json +1 -1
package/lib/server.js
CHANGED
|
@@ -213,75 +213,74 @@ await new Promise(resolve => setTimeout(resolve, 5000));
|
|
|
213
213
|
const orientation = await robot.getOrientation();
|
|
214
214
|
return `Current device orientation is ${orientation}`;
|
|
215
215
|
});
|
|
216
|
-
|
|
217
216
|
tool(
|
|
218
|
-
"
|
|
219
|
-
"
|
|
220
|
-
{},
|
|
221
|
-
async () => {
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
headers: { "Content-Type": "application/json" },
|
|
225
|
-
});
|
|
217
|
+
"tap_by_text",
|
|
218
|
+
"tap an element by passing the text visible on screen",
|
|
219
|
+
{ text: "string" },
|
|
220
|
+
async ({ text }) => {
|
|
221
|
+
const { execSync } = require("child_process");
|
|
222
|
+
const { XMLParser } = require("fast-xml-parser");
|
|
226
223
|
|
|
227
|
-
if (!
|
|
228
|
-
throw new Error(`Failed to get sessions: ${response.statusText}`);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const json = await response.json();
|
|
224
|
+
if (!text) throw new Error("Input text is required");
|
|
232
225
|
|
|
233
|
-
|
|
234
|
-
|
|
226
|
+
// 1. Dump UI Automator XML to stdout
|
|
227
|
+
let dump;
|
|
228
|
+
try {
|
|
229
|
+
dump = execSync("adb shell uiautomator dump /dev/tty", { maxBuffer: 10 * 1024 * 1024 }).toString();
|
|
230
|
+
} catch (e) {
|
|
231
|
+
throw new Error("Failed to dump UI Automator XML via adb");
|
|
235
232
|
}
|
|
236
233
|
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
234
|
+
// 2. Parse XML
|
|
235
|
+
const parser = new XMLParser({
|
|
236
|
+
ignoreAttributes: false,
|
|
237
|
+
attributeNamePrefix: "",
|
|
238
|
+
});
|
|
239
|
+
const xmlObj = parser.parse(dump);
|
|
242
240
|
|
|
241
|
+
// 3. Recursive traversal to find matching element by text/content-desc/hint
|
|
242
|
+
function findElement(node) {
|
|
243
|
+
if (!node) return null;
|
|
243
244
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
"Click an element identified by text using path",
|
|
247
|
-
{
|
|
248
|
-
sessionId: zod_1.z.string().describe("Appium session ID"),
|
|
249
|
-
text: zod_1.z.string().describe("Visible text of the element to click"),
|
|
250
|
-
},
|
|
251
|
-
async ({ sessionId, text }) => {
|
|
252
|
-
const xpath = `//*[@text="${text}"]`;
|
|
253
|
-
const clickUrl = `http://localhost:4723/session/${sessionId}/element`;
|
|
245
|
+
const matchText = node.text || node["content-desc"] || node.hint || "";
|
|
246
|
+
if (matchText === text) return node;
|
|
254
247
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
248
|
+
if (node.node) {
|
|
249
|
+
if (Array.isArray(node.node)) {
|
|
250
|
+
for (const child of node.node) {
|
|
251
|
+
const found = findElement(child);
|
|
252
|
+
if (found) return found;
|
|
253
|
+
}
|
|
254
|
+
} else {
|
|
255
|
+
return findElement(node.node);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
261
258
|
|
|
262
|
-
|
|
263
|
-
throw new Error(`Failed to find element: ${findResponse.statusText}`);
|
|
259
|
+
return null;
|
|
264
260
|
}
|
|
265
261
|
|
|
266
|
-
const
|
|
267
|
-
if (!
|
|
268
|
-
throw new Error(`Element with text "${text}" not found`);
|
|
269
|
-
}
|
|
262
|
+
const element = findElement(xmlObj.hierarchy.node);
|
|
263
|
+
if (!element) throw new Error(`Element with text "${text}" not found`);
|
|
270
264
|
|
|
271
|
-
|
|
265
|
+
// 4. Parse bounds: format "[left,top][right,bottom]"
|
|
266
|
+
const bounds = element.bounds;
|
|
267
|
+
if (!bounds) throw new Error("Element bounds not found");
|
|
272
268
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const clickResponse = await fetch(clickElementUrl, { method: "POST" });
|
|
269
|
+
const coords = bounds.match(/\d+/g).map(Number);
|
|
270
|
+
if (coords.length !== 4) throw new Error("Invalid bounds format");
|
|
276
271
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
272
|
+
const [left, top, right, bottom] = coords;
|
|
273
|
+
const x = Math.floor((left + right) / 2);
|
|
274
|
+
const y = Math.floor((top + bottom) / 2);
|
|
280
275
|
|
|
281
|
-
//
|
|
282
|
-
|
|
276
|
+
// 5. Tap via adb
|
|
277
|
+
try {
|
|
278
|
+
execSync(`adb shell input tap ${x} ${y}`);
|
|
279
|
+
} catch (e) {
|
|
280
|
+
throw new Error("Failed to perform tap via adb");
|
|
281
|
+
}
|
|
283
282
|
|
|
284
|
-
return `
|
|
283
|
+
return `Tapped element with text "${text}" at (${x},${y})`;
|
|
285
284
|
}
|
|
286
285
|
);
|
|
287
286
|
// async check for latest agent version
|