@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.
Files changed (2) hide show
  1. package/lib/server.js +52 -53
  2. 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
- "mobile_get_active_session",
219
- "retrieve an active mobile session ID for reuse across tools",
220
- {},
221
- async () => {
222
- const response = await fetch("http://localhost:4723/sessions", {
223
- method: "GET",
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 (!response.ok) {
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
- if (!json.value || json.value.length === 0) {
234
- throw new Error("No active sessions found");
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
- // Return the first active sessionId
238
- return `Active sessionId: ${json.value[0].id}`;
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
- tool(
245
- "mobile_click",
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
- // Find element
256
- const findResponse = await fetch(clickUrl, {
257
- method: "POST",
258
- headers: { "Content-Type": "application/json" },
259
- body: JSON.stringify({ using: "xpath", value: xpath }),
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
- if (!findResponse.ok) {
263
- throw new Error(`Failed to find element: ${findResponse.statusText}`);
259
+ return null;
264
260
  }
265
261
 
266
- const findJson = await findResponse.json();
267
- if (!findJson.value || !findJson.value.elementId) {
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
- const elementId = findJson.value.elementId;
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
- // Click element
274
- const clickElementUrl = `http://localhost:4723/session/${sessionId}/element/${elementId}/click`;
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
- if (!clickResponse.ok) {
278
- throw new Error(`Failed to click element: ${clickResponse.statusText}`);
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
- // Optional wait after click
282
- await new Promise((resolve) => setTimeout(resolve, 2000));
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 `Clicked on element with text: "${text}" in session: ${sessionId}`;
283
+ return `Tapped element with text "${text}" at (${x},${y})`;
285
284
  }
286
285
  );
287
286
  // async check for latest agent version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nbakka/mcp-appium",
3
- "version": "2.0.7",
3
+ "version": "2.0.8",
4
4
  "description": "Appium MCP",
5
5
  "engines": {
6
6
  "node": ">=18"