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 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
@@ -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);
@@ -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 traverseIDBNode(node, elements, parentIndex = -1, depth = 0) {
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: node.AXUniqueId || null,
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;
@@ -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,
@@ -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.24.8'
9
+ version: '0.25.1'
10
10
  };
11
11
  export function createServer() {
12
12
  const server = new Server(serverInfo, {