mobile-debug-mcp 0.25.0 → 0.25.1
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/dist/interact/index.js +30 -4
- package/dist/observe/ios.js +71 -2
- package/dist/server-core.js +1 -1
- package/dist/utils/android/utils.js +68 -3
- package/docs/CHANGELOG.md +6 -0
- package/docs/ROADMAP.md +19 -1
- package/docs/rfcs/002-richer-element-identity +400 -0
- package/docs/rfcs/003-wait-and-synchronization-reliability +232 -0
- package/docs/specs/mcp-tooling-spec-v1.md +1 -0
- package/docs/tools/observe.md +2 -1
- package/package.json +1 -1
- package/src/interact/index.ts +35 -4
- package/src/observe/index.ts +6 -0
- package/src/observe/ios.ts +69 -3
- package/src/server-core.ts +1 -1
- package/src/types.ts +26 -1
- package/src/utils/android/utils.ts +78 -20
- package/test/unit/observe/state_extraction.test.ts +47 -0
package/dist/interact/index.js
CHANGED
|
@@ -105,7 +105,13 @@ export class ToolsInteract {
|
|
|
105
105
|
class: el.type ?? el.class ?? null,
|
|
106
106
|
bounds,
|
|
107
107
|
index,
|
|
108
|
-
elementId
|
|
108
|
+
elementId,
|
|
109
|
+
state: el.state ?? null,
|
|
110
|
+
stable_id: el.stable_id ?? null,
|
|
111
|
+
role: el.role ?? null,
|
|
112
|
+
test_tag: el.test_tag ?? null,
|
|
113
|
+
selector: el.selector ?? null,
|
|
114
|
+
semantic: el.semantic ?? null
|
|
109
115
|
};
|
|
110
116
|
}
|
|
111
117
|
static _rememberResolvedElement(elementId, context) {
|
|
@@ -138,7 +144,12 @@ export class ToolsInteract {
|
|
|
138
144
|
class: element.type ?? element.class ?? null,
|
|
139
145
|
bounds: ToolsInteract._normalizeBounds(element.bounds),
|
|
140
146
|
index,
|
|
141
|
-
state: element.state ?? null
|
|
147
|
+
state: element.state ?? null,
|
|
148
|
+
stable_id: element.stable_id ?? null,
|
|
149
|
+
role: element.role ?? null,
|
|
150
|
+
test_tag: element.test_tag ?? null,
|
|
151
|
+
selector: element.selector ?? null,
|
|
152
|
+
semantic: element.semantic ?? null
|
|
142
153
|
};
|
|
143
154
|
}
|
|
144
155
|
static _actionFailure(actionId, timestamp, actionType, selector, resolved, failureCode, retryable, uiFingerprintBefore, uiFingerprintAfter) {
|
|
@@ -542,6 +553,11 @@ export class ToolsInteract {
|
|
|
542
553
|
bounds: boundsObj,
|
|
543
554
|
clickable: !!best.clickable,
|
|
544
555
|
enabled: !!best.enabled,
|
|
556
|
+
stable_id: best.stable_id ?? null,
|
|
557
|
+
role: best.role ?? null,
|
|
558
|
+
test_tag: best.test_tag ?? null,
|
|
559
|
+
selector: best.selector ?? null,
|
|
560
|
+
semantic: best.semantic ?? null,
|
|
545
561
|
tapCoordinates,
|
|
546
562
|
telemetry: {
|
|
547
563
|
matchedIndex: best?._index ?? null,
|
|
@@ -940,7 +956,12 @@ export class ToolsInteract {
|
|
|
940
956
|
class: result.element.class ?? null,
|
|
941
957
|
bounds: result.element.bounds ?? null,
|
|
942
958
|
index: typeof result.element.index === 'number' ? result.element.index : null,
|
|
943
|
-
state: result.element.state ?? null
|
|
959
|
+
state: result.element.state ?? null,
|
|
960
|
+
stable_id: result.element.stable_id ?? null,
|
|
961
|
+
role: result.element.role ?? null,
|
|
962
|
+
test_tag: result.element.test_tag ?? null,
|
|
963
|
+
selector: result.element.selector ?? null,
|
|
964
|
+
semantic: result.element.semantic ?? null
|
|
944
965
|
},
|
|
945
966
|
observed: {
|
|
946
967
|
status: result.status,
|
|
@@ -955,7 +976,12 @@ export class ToolsInteract {
|
|
|
955
976
|
class: result.element.class ?? null,
|
|
956
977
|
bounds: result.element.bounds ?? null,
|
|
957
978
|
index: typeof result.element.index === 'number' ? result.element.index : null,
|
|
958
|
-
state: result.element.state ?? null
|
|
979
|
+
state: result.element.state ?? null,
|
|
980
|
+
stable_id: result.element.stable_id ?? null,
|
|
981
|
+
role: result.element.role ?? null,
|
|
982
|
+
test_tag: result.element.test_tag ?? null,
|
|
983
|
+
selector: result.element.selector ?? null,
|
|
984
|
+
semantic: result.element.semantic ?? null
|
|
959
985
|
}
|
|
960
986
|
},
|
|
961
987
|
reason: 'selector is visible'
|
package/dist/observe/ios.js
CHANGED
|
@@ -45,6 +45,65 @@ function parseIOSNumber(value) {
|
|
|
45
45
|
const parsed = Number(value);
|
|
46
46
|
return Number.isFinite(parsed) ? parsed : null;
|
|
47
47
|
}
|
|
48
|
+
function normalizeIOSType(value) {
|
|
49
|
+
return typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
50
|
+
}
|
|
51
|
+
function inferIOSRole(type, traits) {
|
|
52
|
+
if (/slider|adjustable/.test(type) || traits.some((trait) => /adjustable|slider/.test(trait)))
|
|
53
|
+
return 'slider';
|
|
54
|
+
if (/button/.test(type) || traits.some((trait) => /button/.test(trait)))
|
|
55
|
+
return 'button';
|
|
56
|
+
if (/cell/.test(type))
|
|
57
|
+
return 'cell';
|
|
58
|
+
if (/switch/.test(type))
|
|
59
|
+
return 'switch';
|
|
60
|
+
if (/text field|textfield|search field/.test(type))
|
|
61
|
+
return 'text_field';
|
|
62
|
+
if (/image/.test(type))
|
|
63
|
+
return 'image';
|
|
64
|
+
if (/window|application|group|scroll view|collection view/.test(type))
|
|
65
|
+
return 'container';
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
function getIOSStableId(node) {
|
|
69
|
+
const candidates = [node.AXIdentifier, node.accessibilityIdentifier, node.identifier, node.AXUniqueId];
|
|
70
|
+
for (const candidate of candidates) {
|
|
71
|
+
if (typeof candidate === 'string' && candidate.trim().length > 0)
|
|
72
|
+
return candidate;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
function buildIOSSelectorConfidence(source) {
|
|
77
|
+
switch (source) {
|
|
78
|
+
case 'identifier':
|
|
79
|
+
return { score: 1, reason: 'accessibility_identifier' };
|
|
80
|
+
case 'label':
|
|
81
|
+
return { score: 0.9, reason: 'label_match' };
|
|
82
|
+
case 'value':
|
|
83
|
+
return { score: 0.75, reason: 'value_match' };
|
|
84
|
+
case 'type':
|
|
85
|
+
return { score: 0.35, reason: 'type_match' };
|
|
86
|
+
default:
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function buildIOSSelector(type, label, value, stableId) {
|
|
91
|
+
if (stableId)
|
|
92
|
+
return { value: stableId, confidence: buildIOSSelectorConfidence('identifier') };
|
|
93
|
+
if (label)
|
|
94
|
+
return { value: label, confidence: buildIOSSelectorConfidence('label') };
|
|
95
|
+
if (value)
|
|
96
|
+
return { value: value, confidence: buildIOSSelectorConfidence('value') };
|
|
97
|
+
if (type)
|
|
98
|
+
return { value: type, confidence: buildIOSSelectorConfidence('type') };
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
function buildIOSSemantic(type, traits) {
|
|
102
|
+
return {
|
|
103
|
+
is_clickable: traits.includes("UIAccessibilityTraitButton") || /adjustable|slider/.test(type) || type === "Button" || type === "Cell",
|
|
104
|
+
is_container: /window|application|group|scroll view|collection view/.test(type)
|
|
105
|
+
};
|
|
106
|
+
}
|
|
48
107
|
function isIOSAdjustable(node, type, traits) {
|
|
49
108
|
return /slider|adjustable|stepper|progress/i.test(type) || traits.some((trait) => /adjustable|slider|progress/i.test(trait));
|
|
50
109
|
}
|
|
@@ -99,6 +158,11 @@ export function traverseIDBNode(node, elements, parentIndex = -1, depth = 0) {
|
|
|
99
158
|
const frame = node.AXFrame || node.frame;
|
|
100
159
|
const traits = node.AXTraits || [];
|
|
101
160
|
const state = extractIOSState(node, type, label, value, traits);
|
|
161
|
+
const normalizedType = normalizeIOSType(type);
|
|
162
|
+
const stableId = getIOSStableId(node);
|
|
163
|
+
const selector = buildIOSSelector(type, label, value, stableId);
|
|
164
|
+
const semantic = buildIOSSemantic(normalizedType, traits);
|
|
165
|
+
const role = inferIOSRole(normalizedType, traits);
|
|
102
166
|
const clickable = traits.includes("UIAccessibilityTraitButton") || type === "Button" || type === "Cell";
|
|
103
167
|
const isUseful = clickable || (label && label.length > 0) || (value && value.length > 0) || type === "Application" || type === "Window";
|
|
104
168
|
if (isUseful) {
|
|
@@ -107,14 +171,19 @@ export function traverseIDBNode(node, elements, parentIndex = -1, depth = 0) {
|
|
|
107
171
|
text: label,
|
|
108
172
|
contentDescription: value,
|
|
109
173
|
type: type,
|
|
110
|
-
resourceId:
|
|
174
|
+
resourceId: stableId,
|
|
111
175
|
clickable: clickable,
|
|
112
176
|
enabled: true,
|
|
113
177
|
visible: true,
|
|
114
178
|
bounds: bounds,
|
|
115
179
|
center: getCenter(bounds),
|
|
116
180
|
depth: depth,
|
|
117
|
-
state
|
|
181
|
+
state,
|
|
182
|
+
stable_id: stableId,
|
|
183
|
+
role,
|
|
184
|
+
test_tag: stableId,
|
|
185
|
+
selector,
|
|
186
|
+
semantic
|
|
118
187
|
};
|
|
119
188
|
if (parentIndex !== -1) {
|
|
120
189
|
element.parentId = parentIndex;
|
package/dist/server-core.js
CHANGED
|
@@ -6,7 +6,7 @@ import { handleToolCall } from './server/tool-handlers.js';
|
|
|
6
6
|
export { wrapResponse, toolDefinitions, handleToolCall };
|
|
7
7
|
export const serverInfo = {
|
|
8
8
|
name: 'mobile-debug-mcp',
|
|
9
|
-
version: '0.25.
|
|
9
|
+
version: '0.25.1'
|
|
10
10
|
};
|
|
11
11
|
export function createServer() {
|
|
12
12
|
const server = new Server(serverInfo, {
|
|
@@ -356,6 +356,59 @@ function parseNumberAttr(value) {
|
|
|
356
356
|
const parsed = Number(value);
|
|
357
357
|
return Number.isFinite(parsed) ? parsed : null;
|
|
358
358
|
}
|
|
359
|
+
function normalizeClassName(value) {
|
|
360
|
+
return typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
361
|
+
}
|
|
362
|
+
function inferAndroidRole(className) {
|
|
363
|
+
if (/seekbar|slider|progress/.test(className))
|
|
364
|
+
return 'slider';
|
|
365
|
+
if (/switch|toggle/.test(className))
|
|
366
|
+
return 'switch';
|
|
367
|
+
if (/checkbox/.test(className))
|
|
368
|
+
return 'checkbox';
|
|
369
|
+
if (/radiobutton|radio/.test(className))
|
|
370
|
+
return 'radio';
|
|
371
|
+
if (/edittext|textfield|search/.test(className))
|
|
372
|
+
return 'text_field';
|
|
373
|
+
if (/button|fab/.test(className))
|
|
374
|
+
return 'button';
|
|
375
|
+
if (/imageview|icon/.test(className))
|
|
376
|
+
return 'image';
|
|
377
|
+
if (/recyclerview|scroll|layout|viewgroup|frame/.test(className))
|
|
378
|
+
return 'container';
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
function buildAndroidSelectorConfidence(source) {
|
|
382
|
+
switch (source) {
|
|
383
|
+
case 'resource_id':
|
|
384
|
+
return { score: 1, reason: 'resource_id' };
|
|
385
|
+
case 'content_desc':
|
|
386
|
+
return { score: 0.9, reason: 'content_description' };
|
|
387
|
+
case 'text':
|
|
388
|
+
return { score: 0.6, reason: 'text_match' };
|
|
389
|
+
case 'class':
|
|
390
|
+
return { score: 0.35, reason: 'class_match' };
|
|
391
|
+
default:
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
function buildAndroidSelector(text, contentDescription, resourceId, className) {
|
|
396
|
+
if (resourceId)
|
|
397
|
+
return { value: resourceId, confidence: buildAndroidSelectorConfidence('resource_id') };
|
|
398
|
+
if (contentDescription)
|
|
399
|
+
return { value: contentDescription, confidence: buildAndroidSelectorConfidence('content_desc') };
|
|
400
|
+
if (text)
|
|
401
|
+
return { value: text, confidence: buildAndroidSelectorConfidence('text') };
|
|
402
|
+
if (className)
|
|
403
|
+
return { value: className, confidence: buildAndroidSelectorConfidence('class') };
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
function buildAndroidSemantic(clickable, className) {
|
|
407
|
+
return {
|
|
408
|
+
is_clickable: clickable,
|
|
409
|
+
is_container: /recyclerview|scroll|layout|viewgroup|frame/.test(className)
|
|
410
|
+
};
|
|
411
|
+
}
|
|
359
412
|
function isSliderLikeAndroid(node) {
|
|
360
413
|
const className = String(node['@_class'] || '').toLowerCase();
|
|
361
414
|
return /seekbar|slider|range|progress/i.test(className);
|
|
@@ -426,22 +479,34 @@ export function traverseNode(node, elements, parentIndex = -1, depth = 0) {
|
|
|
426
479
|
const text = node['@_text'] || null;
|
|
427
480
|
const contentDescription = node['@_content-desc'] || null;
|
|
428
481
|
const clickable = node['@_clickable'] === 'true';
|
|
482
|
+
const className = String(node['@_class'] || 'unknown');
|
|
429
483
|
const bounds = parseBounds(node['@_bounds'] || '[0,0][0,0]');
|
|
430
484
|
const state = extractAndroidState(node);
|
|
485
|
+
const role = inferAndroidRole(normalizeClassName(className));
|
|
486
|
+
const resourceId = typeof node['@_resource-id'] === 'string' && node['@_resource-id'].trim().length > 0 ? node['@_resource-id'] : null;
|
|
487
|
+
const stableId = resourceId ?? (typeof contentDescription === 'string' && contentDescription.trim().length > 0 ? contentDescription : null);
|
|
488
|
+
const testTag = stableId;
|
|
489
|
+
const selector = buildAndroidSelector(text, contentDescription, resourceId, normalizeClassName(className));
|
|
490
|
+
const semantic = buildAndroidSemantic(clickable, normalizeClassName(className));
|
|
431
491
|
const isUseful = clickable || (text && text.length > 0) || (contentDescription && contentDescription.length > 0);
|
|
432
492
|
if (isUseful) {
|
|
433
493
|
const element = {
|
|
434
494
|
text,
|
|
435
495
|
contentDescription,
|
|
436
|
-
type:
|
|
437
|
-
resourceId
|
|
496
|
+
type: className,
|
|
497
|
+
resourceId,
|
|
438
498
|
clickable,
|
|
439
499
|
enabled: node['@_enabled'] === 'true',
|
|
440
500
|
visible: true,
|
|
441
501
|
bounds,
|
|
442
502
|
center: getCenter(bounds),
|
|
443
503
|
depth,
|
|
444
|
-
state
|
|
504
|
+
state,
|
|
505
|
+
stable_id: stableId,
|
|
506
|
+
role,
|
|
507
|
+
test_tag: testTag,
|
|
508
|
+
selector,
|
|
509
|
+
semantic
|
|
445
510
|
};
|
|
446
511
|
if (parentIndex !== -1) {
|
|
447
512
|
element.parentId = parentIndex;
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the **Mobile Debug MCP** project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.25.1]
|
|
6
|
+
- Platform-native element identity metadata for UI targeting
|
|
7
|
+
- Hierarchy-independent element references
|
|
8
|
+
- Selector confidence metadata for reliability
|
|
9
|
+
- Structured fallback resolution strategy
|
|
10
|
+
|
|
5
11
|
## [0.25.0]
|
|
6
12
|
- Introduces the `expect_state` tool and a standardized state object for UI elements across Android and iOS.
|
|
7
13
|
|
package/docs/ROADMAP.md
CHANGED
|
@@ -26,11 +26,27 @@ Higher task success with fewer retries.
|
|
|
26
26
|
|
|
27
27
|
---
|
|
28
28
|
|
|
29
|
+
# Completed
|
|
30
|
+
|
|
31
|
+
These priorities are done and kept here for history:
|
|
32
|
+
|
|
33
|
+
- Priority 1 — Stronger State Verification
|
|
34
|
+
- Priority 2 — Richer Element Identity
|
|
35
|
+
|
|
36
|
+
Completion notes:
|
|
37
|
+
|
|
38
|
+
- State-aware verification is now implemented and wired through the tool surface.
|
|
39
|
+
- Platform-native element metadata and selector-confidence hints are now part of the runtime contract.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
29
43
|
# Priority 1 — Stronger State Verification
|
|
30
44
|
|
|
31
45
|
## Why first
|
|
32
46
|
Highest leverage improvement.
|
|
33
47
|
|
|
48
|
+
**Status:** Completed
|
|
49
|
+
|
|
34
50
|
Most failures are not “can’t act,” they’re:
|
|
35
51
|
- uncertain state
|
|
36
52
|
- weak verification
|
|
@@ -68,6 +84,8 @@ Blocks or strengthens:
|
|
|
68
84
|
## Why second
|
|
69
85
|
Directly reduces selector brittleness.
|
|
70
86
|
|
|
87
|
+
**Status:** Completed
|
|
88
|
+
|
|
71
89
|
Improves:
|
|
72
90
|
- targeting stability
|
|
73
91
|
- repeatability
|
|
@@ -385,4 +403,4 @@ Still out of scope:
|
|
|
385
403
|
- Recovery planning logic
|
|
386
404
|
- Autonomous retry strategy
|
|
387
405
|
- MCP-level agent orchestration
|
|
388
|
-
- Autonomous recovery hinting (future consideration only)
|
|
406
|
+
- Autonomous recovery hinting (future consideration only)
|