mobile-debug-mcp 0.21.4 → 0.21.5

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.
@@ -4,6 +4,16 @@ export { AndroidInteract, iOSInteract };
4
4
  import { resolveTargetDevice } from '../utils/resolve-device.js';
5
5
  import { ToolsObserve } from '../observe/index.js';
6
6
  export class ToolsInteract {
7
+ static _normalize(s) {
8
+ if (s === null || s === undefined)
9
+ return '';
10
+ try {
11
+ return String(s).toLowerCase().trim();
12
+ }
13
+ catch {
14
+ return '';
15
+ }
16
+ }
7
17
  static async getInteractionService(platform, deviceId) {
8
18
  const effectivePlatform = platform || 'android';
9
19
  const resolved = await resolveTargetDevice({ platform: effectivePlatform, deviceId });
@@ -34,7 +44,7 @@ export class ToolsInteract {
34
44
  // Try to use observe layer to fetch the current UI tree and perform a fast semantic search
35
45
  const start = Date.now();
36
46
  const deadline = start + timeoutMs;
37
- const normalize = (s) => (s === null || s === undefined) ? '' : String(s).toLowerCase().trim();
47
+ const normalize = ToolsInteract._normalize;
38
48
  const q = normalize(query);
39
49
  if (!q)
40
50
  return { found: false, error: 'Empty query' };
@@ -217,15 +227,19 @@ export class ToolsInteract {
217
227
  }
218
228
  static async waitForUIHandler({ selector, condition = 'exists', timeout_ms = 60000, poll_interval_ms = 300, match, retry = { max_attempts: 1, backoff_ms: 0 }, platform, deviceId }) {
219
229
  const overallStart = Date.now();
220
- // Validate selector: require at least one of text, resource_id, or accessibility_id
221
- if (!selector || (typeof selector === 'object' && Object.keys(selector).length === 0)) {
222
- return { status: 'timeout', error: { code: 'INVALID_SELECTOR', message: 'At least one selector field must be provided (text, resource_id, or accessibility_id)' }, metrics: { latency_ms: Date.now() - overallStart, poll_count: 0, attempts: 0 } };
223
- }
224
- const hasText = selector && typeof selector.text === 'string' && selector.text.trim().length > 0;
225
- const hasResId = selector && typeof selector.resource_id === 'string' && selector.resource_id.trim().length > 0;
226
- const hasAccId = selector && typeof selector.accessibility_id === 'string' && selector.accessibility_id.trim().length > 0;
230
+ // Validate selector: require at least one non-empty field (text, resource_id, or accessibility_id)
231
+ const hasText = typeof selector?.text === 'string' && selector.text.trim().length > 0;
232
+ const hasResId = typeof selector?.resource_id === 'string' && selector.resource_id.trim().length > 0;
233
+ const hasAccId = typeof selector?.accessibility_id === 'string' && selector.accessibility_id.trim().length > 0;
227
234
  if (!hasText && !hasResId && !hasAccId) {
228
- return { status: 'timeout', error: { code: 'INVALID_SELECTOR', message: 'Selector must include at least one of: text, resource_id, accessibility_id' }, metrics: { latency_ms: Date.now() - overallStart, poll_count: 0, attempts: 0 } };
235
+ return {
236
+ status: 'timeout',
237
+ error: {
238
+ code: 'INVALID_SELECTOR',
239
+ message: 'Selector must include at least one non-empty field: text, resource_id, or accessibility_id'
240
+ },
241
+ metrics: { latency_ms: Date.now() - overallStart, poll_count: 0, attempts: 0 }
242
+ };
229
243
  }
230
244
  // Validate condition
231
245
  if (!['exists', 'not_exists', 'visible', 'clickable'].includes(condition)) {
@@ -241,11 +255,11 @@ export class ToolsInteract {
241
255
  let attempts = 0;
242
256
  let totalPollCount = 0;
243
257
  // Precompute normalized selector values and helpers (constant across polls)
244
- const normalize = (s) => (s === null || s === undefined) ? '' : String(s).toLowerCase().trim();
245
- const containsFlag = !!selector.contains;
246
- const selText = normalize(selector.text);
247
- const selRid = normalize(selector.resource_id);
248
- const selAid = normalize(selector.accessibility_id);
258
+ const normalize = ToolsInteract._normalize;
259
+ const containsFlag = !!selector?.contains;
260
+ const selText = normalize(selector?.text);
261
+ const selRid = normalize(selector?.resource_id);
262
+ const selAid = normalize(selector?.accessibility_id);
249
263
  try {
250
264
  while (attempts < maxAttempts) {
251
265
  attempts++;
@@ -301,30 +315,31 @@ export class ToolsInteract {
301
315
  }
302
316
  // Evaluate condition
303
317
  const matchedCount = matches.length;
304
- const pickIndexProvided = (match && typeof match.index === 'number');
305
- const pickIndex = pickIndexProvided ? Number(match.index) : 0;
318
+ const pickIndex = (typeof match?.index === 'number') ? match.index : undefined;
306
319
  let chosen = null;
307
- if (matches.length === 0) {
308
- chosen = null;
309
- }
310
- else if (pickIndexProvided) {
311
- // If a specific index is requested but out of bounds, treat as not matched for this poll (deterministic)
312
- if (pickIndex >= 0 && pickIndex < matches.length)
313
- chosen = matches[pickIndex];
314
- else
315
- chosen = null;
320
+ if (matches.length > 0) {
321
+ if (pickIndex !== undefined) {
322
+ // If a specific index is requested but out of bounds, treat as not matched for this poll (deterministic)
323
+ if (pickIndex >= 0 && pickIndex < matches.length)
324
+ chosen = matches[pickIndex];
325
+ else
326
+ chosen = null;
327
+ }
328
+ else {
329
+ chosen = matches[0];
330
+ }
316
331
  }
317
332
  else {
318
- chosen = matches[0];
333
+ chosen = null;
319
334
  }
320
335
  let conditionMet = false;
321
336
  if (condition === 'exists') {
322
337
  // when an index is specified, existence requires that specific index be present
323
- conditionMet = pickIndexProvided ? (chosen !== null) : (matchedCount >= 1);
338
+ conditionMet = (pickIndex !== undefined) ? (chosen !== null) : (matchedCount >= 1);
324
339
  }
325
340
  else if (condition === 'not_exists') {
326
341
  // when an index is specified, not_exists is true if that index is absent
327
- conditionMet = pickIndexProvided ? (chosen === null) : (matchedCount === 0);
342
+ conditionMet = (pickIndex !== undefined) ? (chosen === null) : (matchedCount === 0);
328
343
  }
329
344
  else if (condition === 'visible') {
330
345
  if (chosen) {
package/docs/CHANGELOG.md CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  All notable changes to the **Mobile Debug MCP** project will be documented in this file.
4
4
 
5
+ ## [0.21.5]
6
+ - Fixed incorrect timeout
7
+
5
8
  ## [0.21.4]
6
9
  - updated `wait_for_ui` with better contract and observability
7
10
  - update `get_logs` to get better output
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobile-debug-mcp",
3
- "version": "0.21.4",
3
+ "version": "0.21.5",
4
4
  "description": "MCP server for mobile app debugging (Android + iOS), with focus on security and reliability",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,6 +32,12 @@ interface UiElement {
32
32
 
33
33
  export class ToolsInteract {
34
34
 
35
+ private static _normalize(s: any): string {
36
+ if (s === null || s === undefined) return ''
37
+ try { return String(s).toLowerCase().trim() } catch { return '' }
38
+ }
39
+
40
+
35
41
  private static async getInteractionService(platform?: 'android' | 'ios', deviceId?: string) {
36
42
  const effectivePlatform = platform || 'android'
37
43
  const resolved = await resolveTargetDevice({ platform: effectivePlatform as 'android' | 'ios', deviceId })
@@ -68,7 +74,7 @@ export class ToolsInteract {
68
74
  // Try to use observe layer to fetch the current UI tree and perform a fast semantic search
69
75
  const start = Date.now()
70
76
  const deadline = start + timeoutMs
71
- const normalize = (s: any) => (s === null || s === undefined) ? '' : String(s).toLowerCase().trim()
77
+ const normalize = ToolsInteract._normalize
72
78
 
73
79
  const q = normalize(query)
74
80
  if (!q) return { found: false, error: 'Empty query' }
@@ -221,16 +227,20 @@ export class ToolsInteract {
221
227
  static async waitForUIHandler({ selector, condition = 'exists', timeout_ms = 60000, poll_interval_ms = 300, match, retry = { max_attempts: 1, backoff_ms: 0 }, platform, deviceId }: { selector?: { text?: string, resource_id?: string, accessibility_id?: string, contains?: boolean }, condition?: 'exists'|'not_exists'|'visible'|'clickable', timeout_ms?: number, poll_interval_ms?: number, match?: { index?: number }, retry?: { max_attempts?: number, backoff_ms?: number }, platform?: 'android'|'ios', deviceId?: string }) {
222
228
  const overallStart = Date.now()
223
229
 
224
- // Validate selector: require at least one of text, resource_id, or accessibility_id
225
- if (!selector || (typeof selector === 'object' && Object.keys(selector).length === 0)) {
226
- return { status: 'timeout', error: { code: 'INVALID_SELECTOR', message: 'At least one selector field must be provided (text, resource_id, or accessibility_id)' }, metrics: { latency_ms: Date.now() - overallStart, poll_count: 0, attempts: 0 } }
227
- }
230
+ // Validate selector: require at least one non-empty field (text, resource_id, or accessibility_id)
231
+ const hasText = typeof selector?.text === 'string' && selector.text.trim().length > 0;
232
+ const hasResId = typeof selector?.resource_id === 'string' && selector.resource_id.trim().length > 0;
233
+ const hasAccId = typeof selector?.accessibility_id === 'string' && selector.accessibility_id.trim().length > 0;
228
234
 
229
- const hasText = selector && typeof (selector as any).text === 'string' && (selector as any).text.trim().length > 0
230
- const hasResId = selector && typeof (selector as any).resource_id === 'string' && (selector as any).resource_id.trim().length > 0
231
- const hasAccId = selector && typeof (selector as any).accessibility_id === 'string' && (selector as any).accessibility_id.trim().length > 0
232
235
  if (!hasText && !hasResId && !hasAccId) {
233
- return { status: 'timeout', error: { code: 'INVALID_SELECTOR', message: 'Selector must include at least one of: text, resource_id, accessibility_id' }, metrics: { latency_ms: Date.now() - overallStart, poll_count: 0, attempts: 0 } }
236
+ return {
237
+ status: 'timeout',
238
+ error: {
239
+ code: 'INVALID_SELECTOR',
240
+ message: 'Selector must include at least one non-empty field: text, resource_id, or accessibility_id'
241
+ },
242
+ metrics: { latency_ms: Date.now() - overallStart, poll_count: 0, attempts: 0 }
243
+ };
234
244
  }
235
245
 
236
246
  // Validate condition
@@ -251,11 +261,11 @@ export class ToolsInteract {
251
261
  let totalPollCount = 0
252
262
 
253
263
  // Precompute normalized selector values and helpers (constant across polls)
254
- const normalize = (s: any) => (s === null || s === undefined) ? '' : String(s).toLowerCase().trim()
255
- const containsFlag = !!selector.contains
256
- const selText = normalize((selector as any).text)
257
- const selRid = normalize((selector as any).resource_id)
258
- const selAid = normalize((selector as any).accessibility_id)
264
+ const normalize = ToolsInteract._normalize
265
+ const containsFlag = !!selector?.contains
266
+ const selText = normalize(selector?.text)
267
+ const selRid = normalize(selector?.resource_id)
268
+ const selAid = normalize(selector?.accessibility_id)
259
269
 
260
270
  try {
261
271
  while (attempts < maxAttempts) {
@@ -310,26 +320,27 @@ export class ToolsInteract {
310
320
 
311
321
  // Evaluate condition
312
322
  const matchedCount = matches.length
313
- const pickIndexProvided = (match && typeof (match as any).index === 'number')
314
- const pickIndex: number = pickIndexProvided ? Number((match as any).index) : 0
323
+ const pickIndex = (typeof match?.index === 'number') ? match!.index as number : undefined
315
324
  let chosen: { el: any, idx: number } | null = null
316
- if (matches.length === 0) {
317
- chosen = null
318
- } else if (pickIndexProvided) {
319
- // If a specific index is requested but out of bounds, treat as not matched for this poll (deterministic)
320
- if (pickIndex >= 0 && pickIndex < matches.length) chosen = matches[pickIndex]
321
- else chosen = null
325
+ if (matches.length > 0) {
326
+ if (pickIndex !== undefined) {
327
+ // If a specific index is requested but out of bounds, treat as not matched for this poll (deterministic)
328
+ if (pickIndex >= 0 && pickIndex < matches.length) chosen = matches[pickIndex]
329
+ else chosen = null
330
+ } else {
331
+ chosen = matches[0]
332
+ }
322
333
  } else {
323
- chosen = matches[0]
334
+ chosen = null
324
335
  }
325
336
 
326
337
  let conditionMet = false
327
338
  if (condition === 'exists') {
328
339
  // when an index is specified, existence requires that specific index be present
329
- conditionMet = pickIndexProvided ? (chosen !== null) : (matchedCount >= 1)
340
+ conditionMet = (pickIndex !== undefined) ? (chosen !== null) : (matchedCount >= 1)
330
341
  } else if (condition === 'not_exists') {
331
342
  // when an index is specified, not_exists is true if that index is absent
332
- conditionMet = pickIndexProvided ? (chosen === null) : (matchedCount === 0)
343
+ conditionMet = (pickIndex !== undefined) ? (chosen === null) : (matchedCount === 0)
333
344
  } else if (condition === 'visible') {
334
345
  if (chosen) {
335
346
  const b = chosen.el.bounds