mobile-debug-mcp 0.24.8 → 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/README.md +1 -1
- package/dist/interact/index.js +240 -4
- package/dist/observe/ios.js +126 -3
- package/dist/server/common.js +2 -1
- package/dist/server/tool-definitions.js +55 -0
- package/dist/server/tool-handlers.js +17 -0
- package/dist/server-core.js +1 -1
- package/dist/utils/android/utils.js +134 -3
- package/docs/CHANGELOG.md +9 -0
- package/docs/ROADMAP.md +406 -0
- package/docs/rfcs/001-state-verification.md +452 -0
- 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 +5 -0
- package/docs/tools/interact.md +25 -0
- package/docs/tools/observe.md +3 -1
- package/package.json +1 -1
- package/src/interact/index.ts +272 -4
- package/src/observe/index.ts +6 -0
- package/src/observe/ios.ts +129 -4
- package/src/server/common.ts +2 -1
- package/src/server/tool-definitions.ts +55 -0
- package/src/server/tool-handlers.ts +18 -0
- package/src/server-core.ts +1 -1
- package/src/types.ts +67 -1
- package/src/utils/android/utils.ts +126 -4
- package/test/unit/observe/state_extraction.test.ts +90 -0
- package/test/unit/server/response_shapes.test.ts +40 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Mobile Debug Tools
|
|
2
2
|
|
|
3
|
-
A minimal, secure MCP server for AI-assisted mobile development. Build, install, and inspect Android/iOS apps from an MCP-compatible client.
|
|
3
|
+
A minimal, secure MCP server for AI-assisted mobile development. Build, install, interact and inspect Android/iOS apps from an MCP-compatible client.
|
|
4
4
|
|
|
5
5
|
> **Support:**
|
|
6
6
|
> * KMP
|
package/dist/interact/index.js
CHANGED
|
@@ -34,6 +34,43 @@ export class ToolsInteract {
|
|
|
34
34
|
return null;
|
|
35
35
|
return normalized;
|
|
36
36
|
}
|
|
37
|
+
static _matchesSelector(el, selector) {
|
|
38
|
+
if (!selector)
|
|
39
|
+
return false;
|
|
40
|
+
const normalize = ToolsInteract._normalize;
|
|
41
|
+
const containsFlag = !!selector.contains;
|
|
42
|
+
const text = normalize(el.text ?? el.label ?? el.value ?? '');
|
|
43
|
+
const resourceId = normalize(el.resourceId ?? el.resourceID ?? el.id ?? '');
|
|
44
|
+
const accessibilityId = normalize(el.contentDescription ?? el.contentDesc ?? el.accessibilityLabel ?? el.label ?? '');
|
|
45
|
+
if (selector.text !== undefined && selector.text !== null) {
|
|
46
|
+
const q = normalize(selector.text);
|
|
47
|
+
if (containsFlag ? !text.includes(q) : text !== q)
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
if (selector.resource_id !== undefined && selector.resource_id !== null) {
|
|
51
|
+
const q = normalize(selector.resource_id);
|
|
52
|
+
if (containsFlag ? !resourceId.includes(q) : resourceId !== q)
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
if (selector.accessibility_id !== undefined && selector.accessibility_id !== null) {
|
|
56
|
+
const q = normalize(selector.accessibility_id);
|
|
57
|
+
if (containsFlag ? !accessibilityId.includes(q) : accessibilityId !== q)
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
static _findFirstMatchingElement(elements, selector) {
|
|
63
|
+
if (!selector)
|
|
64
|
+
return null;
|
|
65
|
+
for (let i = 0; i < elements.length; i++) {
|
|
66
|
+
const el = elements[i];
|
|
67
|
+
if (!el)
|
|
68
|
+
continue;
|
|
69
|
+
if (ToolsInteract._matchesSelector(el, selector))
|
|
70
|
+
return { el, idx: i };
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
37
74
|
static _isVisibleElement(el) {
|
|
38
75
|
const bounds = ToolsInteract._normalizeBounds(el.bounds);
|
|
39
76
|
return !!el.visible && !!bounds && bounds[2] > bounds[0] && bounds[3] > bounds[1];
|
|
@@ -68,7 +105,13 @@ export class ToolsInteract {
|
|
|
68
105
|
class: el.type ?? el.class ?? null,
|
|
69
106
|
bounds,
|
|
70
107
|
index,
|
|
71
|
-
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
|
|
72
115
|
};
|
|
73
116
|
}
|
|
74
117
|
static _rememberResolvedElement(elementId, context) {
|
|
@@ -100,7 +143,13 @@ export class ToolsInteract {
|
|
|
100
143
|
accessibility_id: element.contentDescription ?? element.contentDesc ?? element.accessibilityLabel ?? element.label ?? null,
|
|
101
144
|
class: element.type ?? element.class ?? null,
|
|
102
145
|
bounds: ToolsInteract._normalizeBounds(element.bounds),
|
|
103
|
-
index
|
|
146
|
+
index,
|
|
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
|
|
104
153
|
};
|
|
105
154
|
}
|
|
106
155
|
static _actionFailure(actionId, timestamp, actionType, selector, resolved, failureCode, retryable, uiFingerprintBefore, uiFingerprintAfter) {
|
|
@@ -504,6 +553,11 @@ export class ToolsInteract {
|
|
|
504
553
|
bounds: boundsObj,
|
|
505
554
|
clickable: !!best.clickable,
|
|
506
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,
|
|
507
561
|
tapCoordinates,
|
|
508
562
|
telemetry: {
|
|
509
563
|
matchedIndex: best?._index ?? null,
|
|
@@ -901,7 +955,13 @@ export class ToolsInteract {
|
|
|
901
955
|
accessibility_id: result.element.accessibility_id ?? null,
|
|
902
956
|
class: result.element.class ?? null,
|
|
903
957
|
bounds: result.element.bounds ?? null,
|
|
904
|
-
index: typeof result.element.index === 'number' ? result.element.index : null
|
|
958
|
+
index: typeof result.element.index === 'number' ? result.element.index : 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
|
|
905
965
|
},
|
|
906
966
|
observed: {
|
|
907
967
|
status: result.status,
|
|
@@ -915,7 +975,13 @@ export class ToolsInteract {
|
|
|
915
975
|
accessibility_id: result.element.accessibility_id ?? null,
|
|
916
976
|
class: result.element.class ?? null,
|
|
917
977
|
bounds: result.element.bounds ?? null,
|
|
918
|
-
index: typeof result.element.index === 'number' ? result.element.index : null
|
|
978
|
+
index: typeof result.element.index === 'number' ? result.element.index : 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
|
|
919
985
|
}
|
|
920
986
|
},
|
|
921
987
|
reason: 'selector is visible'
|
|
@@ -939,6 +1005,176 @@ export class ToolsInteract {
|
|
|
939
1005
|
retryable: errorCode === 'TIMEOUT'
|
|
940
1006
|
};
|
|
941
1007
|
}
|
|
1008
|
+
static async expectStateHandler({ selector, element_id, property, expected, platform, deviceId }) {
|
|
1009
|
+
const tree = await ToolsObserve.getUITreeHandler({ platform, deviceId });
|
|
1010
|
+
const elements = Array.isArray(tree?.elements) ? tree.elements : [];
|
|
1011
|
+
const treePlatform = tree?.device?.platform === 'ios' ? 'ios' : (platform || 'android');
|
|
1012
|
+
const treeDeviceId = tree?.device?.id || deviceId;
|
|
1013
|
+
let matched = null;
|
|
1014
|
+
if (element_id) {
|
|
1015
|
+
const resolved = ToolsInteract._resolvedUiElements.get(element_id);
|
|
1016
|
+
if (resolved) {
|
|
1017
|
+
const current = ToolsInteract._findCurrentResolvedElement(elements, treePlatform, treeDeviceId, resolved);
|
|
1018
|
+
if (current)
|
|
1019
|
+
matched = { el: current.el, idx: current.index };
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
if (!matched && selector) {
|
|
1023
|
+
matched = ToolsInteract._findFirstMatchingElement(elements, selector);
|
|
1024
|
+
}
|
|
1025
|
+
if (!matched) {
|
|
1026
|
+
return {
|
|
1027
|
+
success: false,
|
|
1028
|
+
selector,
|
|
1029
|
+
element_id: element_id ?? null,
|
|
1030
|
+
expected_state: { property, expected },
|
|
1031
|
+
reason: 'element not found',
|
|
1032
|
+
failure_code: 'ELEMENT_NOT_FOUND',
|
|
1033
|
+
retryable: true
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
const resolvedElement = ToolsInteract._resolvedTargetFromElement(ToolsInteract._computeElementId(treePlatform, treeDeviceId, matched.el, matched.idx), matched.el, matched.idx);
|
|
1037
|
+
const observedState = matched.el.state ?? null;
|
|
1038
|
+
const actual = observedState?.[property] ?? null;
|
|
1039
|
+
const compareBoolean = (value) => typeof value === 'boolean' ? value : null;
|
|
1040
|
+
const compareString = (value) => typeof value === 'string' ? value : null;
|
|
1041
|
+
const compareNumber = (value) => typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
1042
|
+
let success = false;
|
|
1043
|
+
let reason = '';
|
|
1044
|
+
let rawValue = null;
|
|
1045
|
+
let observedValue = actual;
|
|
1046
|
+
switch (property) {
|
|
1047
|
+
case 'checked':
|
|
1048
|
+
case 'focused':
|
|
1049
|
+
case 'expanded':
|
|
1050
|
+
case 'enabled': {
|
|
1051
|
+
const expectedBool = compareBoolean(expected);
|
|
1052
|
+
const actualBool = compareBoolean(actual);
|
|
1053
|
+
if (expectedBool === null) {
|
|
1054
|
+
reason = `expected ${property} must be boolean`;
|
|
1055
|
+
}
|
|
1056
|
+
else if (actualBool === null) {
|
|
1057
|
+
reason = `${property} state unavailable`;
|
|
1058
|
+
}
|
|
1059
|
+
else {
|
|
1060
|
+
rawValue = actualBool;
|
|
1061
|
+
success = actualBool === expectedBool;
|
|
1062
|
+
reason = success ? `${property} matches expected value` : `expected ${property}=${expectedBool} but observed ${actualBool}`;
|
|
1063
|
+
}
|
|
1064
|
+
observedValue = actualBool;
|
|
1065
|
+
break;
|
|
1066
|
+
}
|
|
1067
|
+
case 'value':
|
|
1068
|
+
case 'raw_value': {
|
|
1069
|
+
const expectedNumber = compareNumber(expected);
|
|
1070
|
+
const actualNumber = compareNumber(actual);
|
|
1071
|
+
if (expectedNumber !== null && actualNumber !== null) {
|
|
1072
|
+
success = actualNumber === expectedNumber;
|
|
1073
|
+
rawValue = actualNumber;
|
|
1074
|
+
observedValue = actualNumber;
|
|
1075
|
+
reason = success ? 'value matches expected value' : `expected value=${expectedNumber} but observed ${actualNumber}`;
|
|
1076
|
+
break;
|
|
1077
|
+
}
|
|
1078
|
+
const expectedString = typeof expected === 'string' ? expected : null;
|
|
1079
|
+
const actualString = compareString(actual);
|
|
1080
|
+
if (expectedString !== null && actualString !== null) {
|
|
1081
|
+
success = actualString === expectedString;
|
|
1082
|
+
rawValue = actualString;
|
|
1083
|
+
observedValue = actualString;
|
|
1084
|
+
reason = success ? 'value matches expected value' : `expected value=${expectedString} but observed ${actualString}`;
|
|
1085
|
+
}
|
|
1086
|
+
else {
|
|
1087
|
+
reason = 'value state unavailable';
|
|
1088
|
+
}
|
|
1089
|
+
break;
|
|
1090
|
+
}
|
|
1091
|
+
case 'selected': {
|
|
1092
|
+
const expectedBool = typeof expected === 'boolean' ? expected : null;
|
|
1093
|
+
const expectedString = typeof expected === 'string'
|
|
1094
|
+
? expected
|
|
1095
|
+
: expected && typeof expected === 'object'
|
|
1096
|
+
? String(expected.id ?? expected.label ?? '')
|
|
1097
|
+
: null;
|
|
1098
|
+
if (!observedState || observedState.selected === undefined || observedState.selected === null) {
|
|
1099
|
+
reason = 'selected state unavailable';
|
|
1100
|
+
break;
|
|
1101
|
+
}
|
|
1102
|
+
if (expectedBool !== null) {
|
|
1103
|
+
const actualBool = typeof observedState.selected === 'boolean' ? observedState.selected : null;
|
|
1104
|
+
if (actualBool === null) {
|
|
1105
|
+
reason = 'selected state is not boolean';
|
|
1106
|
+
break;
|
|
1107
|
+
}
|
|
1108
|
+
rawValue = actualBool;
|
|
1109
|
+
observedValue = actualBool;
|
|
1110
|
+
success = actualBool === expectedBool;
|
|
1111
|
+
reason = success ? 'selected matches expected value' : `expected selected=${expectedBool} but observed ${actualBool}`;
|
|
1112
|
+
break;
|
|
1113
|
+
}
|
|
1114
|
+
const actualSelected = typeof observedState.selected === 'object' && observedState.selected !== null
|
|
1115
|
+
? String(observedState.selected.id ?? observedState.selected.label ?? '')
|
|
1116
|
+
: String(observedState.selected);
|
|
1117
|
+
const actualString = actualSelected.trim();
|
|
1118
|
+
if (!expectedString) {
|
|
1119
|
+
reason = 'expected selected must be boolean, string, or object with id/label';
|
|
1120
|
+
break;
|
|
1121
|
+
}
|
|
1122
|
+
rawValue = actualString;
|
|
1123
|
+
observedValue = actualString;
|
|
1124
|
+
success = actualString === expectedString;
|
|
1125
|
+
reason = success ? 'selected matches expected value' : `expected selected=${expectedString} but observed ${actualString}`;
|
|
1126
|
+
break;
|
|
1127
|
+
}
|
|
1128
|
+
case 'text_value': {
|
|
1129
|
+
const expectedString = typeof expected === 'string' ? expected : null;
|
|
1130
|
+
const actualString = compareString(actual);
|
|
1131
|
+
if (!expectedString) {
|
|
1132
|
+
reason = 'expected text_value must be string';
|
|
1133
|
+
}
|
|
1134
|
+
else if (!actualString) {
|
|
1135
|
+
reason = 'text_value state unavailable';
|
|
1136
|
+
}
|
|
1137
|
+
else {
|
|
1138
|
+
success = actualString === expectedString;
|
|
1139
|
+
rawValue = actualString;
|
|
1140
|
+
observedValue = actualString;
|
|
1141
|
+
reason = success ? 'text_value matches expected value' : `expected text_value=${expectedString} but observed ${actualString}`;
|
|
1142
|
+
}
|
|
1143
|
+
break;
|
|
1144
|
+
}
|
|
1145
|
+
default: {
|
|
1146
|
+
if (actual !== null && actual !== undefined) {
|
|
1147
|
+
success = actual === expected;
|
|
1148
|
+
observedValue = actual;
|
|
1149
|
+
rawValue = typeof actual === 'string' || typeof actual === 'number' || typeof actual === 'boolean' ? actual : null;
|
|
1150
|
+
reason = success ? `${property} matches expected value` : `expected ${property} to match but observed ${String(actual)}`;
|
|
1151
|
+
}
|
|
1152
|
+
else {
|
|
1153
|
+
reason = `unsupported or unavailable state property: ${property}`;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
if (!success && !reason) {
|
|
1158
|
+
reason = `${property} did not match expected value`;
|
|
1159
|
+
}
|
|
1160
|
+
return {
|
|
1161
|
+
success,
|
|
1162
|
+
selector,
|
|
1163
|
+
element_id: element_id ?? resolvedElement.elementId,
|
|
1164
|
+
expected_state: { property, expected },
|
|
1165
|
+
element: {
|
|
1166
|
+
...resolvedElement,
|
|
1167
|
+
state: observedState
|
|
1168
|
+
},
|
|
1169
|
+
observed_state: {
|
|
1170
|
+
property,
|
|
1171
|
+
value: observedValue,
|
|
1172
|
+
...(rawValue !== null ? { raw_value: rawValue } : {})
|
|
1173
|
+
},
|
|
1174
|
+
reason,
|
|
1175
|
+
...(success ? {} : { failure_code: 'UNKNOWN', retryable: false })
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
942
1178
|
static async waitForUICore({ type = 'ui', query, timeoutMs = 30000, pollIntervalMs = 300, includeSnapshotOnFailure = true, match = 'present', stability_ms = 700, observationDelayMs = 0, platform, deviceId }) {
|
|
943
1179
|
const start = Date.now();
|
|
944
1180
|
const deadline = start + (timeoutMs || 0);
|
package/dist/observe/ios.js
CHANGED
|
@@ -37,7 +37,118 @@ function getCenter(bounds) {
|
|
|
37
37
|
const [x1, y1, x2, y2] = bounds;
|
|
38
38
|
return [Math.floor((x1 + x2) / 2), Math.floor((y1 + y2) / 2)];
|
|
39
39
|
}
|
|
40
|
-
function
|
|
40
|
+
function parseIOSNumber(value) {
|
|
41
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
42
|
+
return value;
|
|
43
|
+
if (typeof value !== 'string')
|
|
44
|
+
return null;
|
|
45
|
+
const parsed = Number(value);
|
|
46
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
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
|
+
}
|
|
107
|
+
function isIOSAdjustable(node, type, traits) {
|
|
108
|
+
return /slider|adjustable|stepper|progress/i.test(type) || traits.some((trait) => /adjustable|slider|progress/i.test(trait));
|
|
109
|
+
}
|
|
110
|
+
function extractIOSState(node, type, label, value, traits) {
|
|
111
|
+
const state = {};
|
|
112
|
+
const normalizedTraits = traits.map((trait) => String(trait).toLowerCase());
|
|
113
|
+
if (normalizedTraits.some((trait) => /selected/.test(trait))) {
|
|
114
|
+
state.selected = label || value || true;
|
|
115
|
+
}
|
|
116
|
+
if (normalizedTraits.some((trait) => /focused/.test(trait))) {
|
|
117
|
+
state.focused = true;
|
|
118
|
+
}
|
|
119
|
+
if (normalizedTraits.some((trait) => /enabled/.test(trait))) {
|
|
120
|
+
state.enabled = true;
|
|
121
|
+
}
|
|
122
|
+
if (normalizedTraits.some((trait) => /disabled/.test(trait))) {
|
|
123
|
+
state.enabled = false;
|
|
124
|
+
}
|
|
125
|
+
if (value && /textfield|search|text/i.test(type)) {
|
|
126
|
+
state.text_value = value;
|
|
127
|
+
}
|
|
128
|
+
if (isIOSAdjustable(node, type, traits)) {
|
|
129
|
+
const rawValue = parseIOSNumber(value);
|
|
130
|
+
if (rawValue !== null) {
|
|
131
|
+
state.raw_value = rawValue;
|
|
132
|
+
state.value = rawValue >= 0 && rawValue <= 1 ? Math.round(rawValue * 100) : rawValue;
|
|
133
|
+
}
|
|
134
|
+
else if (value) {
|
|
135
|
+
state.raw_value = value;
|
|
136
|
+
state.value = value;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else if (value) {
|
|
140
|
+
const numericValue = parseIOSNumber(value);
|
|
141
|
+
if (numericValue !== null) {
|
|
142
|
+
state.value = numericValue;
|
|
143
|
+
state.raw_value = numericValue;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
state.value = value;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return Object.keys(state).length > 0 ? state : null;
|
|
150
|
+
}
|
|
151
|
+
export function traverseIDBNode(node, elements, parentIndex = -1, depth = 0) {
|
|
41
152
|
if (!node)
|
|
42
153
|
return -1;
|
|
43
154
|
let currentIndex = -1;
|
|
@@ -46,6 +157,12 @@ function traverseIDBNode(node, elements, parentIndex = -1, depth = 0) {
|
|
|
46
157
|
const value = node.AXValue || null;
|
|
47
158
|
const frame = node.AXFrame || node.frame;
|
|
48
159
|
const traits = node.AXTraits || [];
|
|
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);
|
|
49
166
|
const clickable = traits.includes("UIAccessibilityTraitButton") || type === "Button" || type === "Cell";
|
|
50
167
|
const isUseful = clickable || (label && label.length > 0) || (value && value.length > 0) || type === "Application" || type === "Window";
|
|
51
168
|
if (isUseful) {
|
|
@@ -54,13 +171,19 @@ function traverseIDBNode(node, elements, parentIndex = -1, depth = 0) {
|
|
|
54
171
|
text: label,
|
|
55
172
|
contentDescription: value,
|
|
56
173
|
type: type,
|
|
57
|
-
resourceId:
|
|
174
|
+
resourceId: stableId,
|
|
58
175
|
clickable: clickable,
|
|
59
176
|
enabled: true,
|
|
60
177
|
visible: true,
|
|
61
178
|
bounds: bounds,
|
|
62
179
|
center: getCenter(bounds),
|
|
63
|
-
depth: depth
|
|
180
|
+
depth: depth,
|
|
181
|
+
state,
|
|
182
|
+
stable_id: stableId,
|
|
183
|
+
role,
|
|
184
|
+
test_tag: stableId,
|
|
185
|
+
selector,
|
|
186
|
+
semantic
|
|
64
187
|
};
|
|
65
188
|
if (parentIndex !== -1) {
|
|
66
189
|
element.parentId = parentIndex;
|
package/dist/server/common.js
CHANGED
|
@@ -79,7 +79,8 @@ export function normalizeResolvedTarget(value = null) {
|
|
|
79
79
|
accessibility_id: value.accessibility_id ?? null,
|
|
80
80
|
class: value.class ?? null,
|
|
81
81
|
bounds: value.bounds ?? null,
|
|
82
|
-
index: value.index ?? null
|
|
82
|
+
index: value.index ?? null,
|
|
83
|
+
state: value.state ?? null
|
|
83
84
|
};
|
|
84
85
|
}
|
|
85
86
|
export function inferGenericFailure(message) {
|
|
@@ -468,6 +468,61 @@ Failure Handling:
|
|
|
468
468
|
required: ['selector']
|
|
469
469
|
}
|
|
470
470
|
},
|
|
471
|
+
{
|
|
472
|
+
name: 'expect_state',
|
|
473
|
+
description: `Purpose:
|
|
474
|
+
Verify a readable UI state property on the currently visible element.
|
|
475
|
+
|
|
476
|
+
Inputs:
|
|
477
|
+
- selector or element_id
|
|
478
|
+
- property
|
|
479
|
+
- expected
|
|
480
|
+
- platform/deviceId (optional)
|
|
481
|
+
|
|
482
|
+
Supported properties:
|
|
483
|
+
- checked, selected, focused, expanded, enabled, text_value, value, raw_value
|
|
484
|
+
|
|
485
|
+
Verification Guidance:
|
|
486
|
+
- Use this when the UI element is visible but its state must also be confirmed
|
|
487
|
+
- Prefer the canonical property names above
|
|
488
|
+
- The tool compares the normalized readable state and returns the observed value when available
|
|
489
|
+
|
|
490
|
+
Constraints:
|
|
491
|
+
- Returns structured success/failure only
|
|
492
|
+
- Does not infer a state when the property is unavailable
|
|
493
|
+
|
|
494
|
+
Failure Handling:
|
|
495
|
+
- ELEMENT_NOT_FOUND → re-resolve the element or wait for UI stabilization
|
|
496
|
+
- UNKNOWN → capture a snapshot and stop`,
|
|
497
|
+
inputSchema: {
|
|
498
|
+
type: 'object',
|
|
499
|
+
properties: {
|
|
500
|
+
selector: {
|
|
501
|
+
type: 'object',
|
|
502
|
+
properties: {
|
|
503
|
+
text: { type: 'string' },
|
|
504
|
+
resource_id: { type: 'string' },
|
|
505
|
+
accessibility_id: { type: 'string' },
|
|
506
|
+
contains: { type: 'boolean', default: false }
|
|
507
|
+
}
|
|
508
|
+
},
|
|
509
|
+
element_id: { type: 'string', description: 'Optional previously resolved element identifier.' },
|
|
510
|
+
property: { type: 'string', description: 'Readable state property to verify.' },
|
|
511
|
+
expected: {
|
|
512
|
+
description: 'Expected normalized state value.',
|
|
513
|
+
oneOf: [
|
|
514
|
+
{ type: 'boolean' },
|
|
515
|
+
{ type: 'number' },
|
|
516
|
+
{ type: 'string' },
|
|
517
|
+
{ type: 'object' }
|
|
518
|
+
]
|
|
519
|
+
},
|
|
520
|
+
platform: { type: 'string', enum: ['android', 'ios'], description: 'Optional platform override' },
|
|
521
|
+
deviceId: { type: 'string', description: 'Optional device serial/udid' }
|
|
522
|
+
},
|
|
523
|
+
required: ['property', 'expected']
|
|
524
|
+
}
|
|
525
|
+
},
|
|
471
526
|
{
|
|
472
527
|
name: 'wait_for_ui',
|
|
473
528
|
description: `Purpose:
|
|
@@ -208,6 +208,22 @@ async function handleExpectElementVisible(args) {
|
|
|
208
208
|
const res = await ToolsInteract.expectElementVisibleHandler({ selector, element_id, timeout_ms, poll_interval_ms, platform, deviceId });
|
|
209
209
|
return wrapResponse(res);
|
|
210
210
|
}
|
|
211
|
+
async function handleExpectState(args) {
|
|
212
|
+
const selector = getObjectArg(args, 'selector');
|
|
213
|
+
const element_id = getStringArg(args, 'element_id');
|
|
214
|
+
const property = requireStringArg(args, 'property');
|
|
215
|
+
const platform = getStringArg(args, 'platform');
|
|
216
|
+
const deviceId = getStringArg(args, 'deviceId');
|
|
217
|
+
if (!selector && !element_id) {
|
|
218
|
+
throw new Error('Missing selector or element_id argument');
|
|
219
|
+
}
|
|
220
|
+
if (!Object.prototype.hasOwnProperty.call(args, 'expected')) {
|
|
221
|
+
throw new Error('Missing expected argument');
|
|
222
|
+
}
|
|
223
|
+
const expected = args.expected;
|
|
224
|
+
const res = await ToolsInteract.expectStateHandler({ selector: selector ?? undefined, element_id: element_id ?? undefined, property, expected, platform, deviceId });
|
|
225
|
+
return wrapResponse(res);
|
|
226
|
+
}
|
|
211
227
|
async function handleWaitForUI(args) {
|
|
212
228
|
const selector = getObjectArg(args, 'selector');
|
|
213
229
|
const condition = getStringArg(args, 'condition') ?? 'exists';
|
|
@@ -395,6 +411,7 @@ export const toolHandlers = {
|
|
|
395
411
|
wait_for_screen_change: handleWaitForScreenChange,
|
|
396
412
|
expect_screen: handleExpectScreen,
|
|
397
413
|
expect_element_visible: handleExpectElementVisible,
|
|
414
|
+
expect_state: handleExpectState,
|
|
398
415
|
wait_for_ui: handleWaitForUI,
|
|
399
416
|
find_element: handleFindElement,
|
|
400
417
|
tap: handleTap,
|
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.
|
|
9
|
+
version: '0.25.1'
|
|
10
10
|
};
|
|
11
11
|
export function createServer() {
|
|
12
12
|
const server = new Server(serverInfo, {
|