preflight-ios-mcp 1.0.0

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.
@@ -0,0 +1,130 @@
1
+ import { z } from 'zod';
2
+ import { execSimctl, listDevices, resolveDevice } from '../helpers/simctl.js';
3
+ import { execFile } from 'node:child_process';
4
+ import { promisify } from 'node:util';
5
+ const execFileAsync = promisify(execFile);
6
+ // --- list_devices ---
7
+ export const listDevicesParams = {
8
+ filter: z.enum(['available', 'booted', 'all']).optional().describe('Filter devices (default: all)'),
9
+ };
10
+ export async function handleListDevices(args) {
11
+ const filter = (args.filter || 'all');
12
+ const devices = await listDevices(filter);
13
+ const lines = devices.map(d => {
14
+ const runtime = d.runtime
15
+ .replace(/^com\.apple\.CoreSimulator\.SimRuntime\./, '')
16
+ .replace(/-/g, ' ');
17
+ return `${d.name} | ${d.udid} | ${d.state} | ${runtime}`;
18
+ });
19
+ return {
20
+ content: [{
21
+ type: 'text',
22
+ text: devices.length === 0
23
+ ? `No ${filter === 'all' ? '' : filter + ' '}devices found.`
24
+ : `Found ${devices.length} device(s):\n\nName | UDID | State | Runtime\n---|---|---|---\n${lines.join('\n')}`,
25
+ }],
26
+ };
27
+ }
28
+ // --- boot ---
29
+ export const bootParams = {
30
+ deviceId: z.string().describe('Device UDID or name to boot'),
31
+ waitForBoot: z.boolean().optional().describe('Wait until device is fully booted before returning (default: true)'),
32
+ };
33
+ export async function handleBoot(args) {
34
+ const devices = await listDevices();
35
+ const match = devices.find(d => d.name.toLowerCase() === args.deviceId.toLowerCase() ||
36
+ d.udid === args.deviceId);
37
+ if (!match) {
38
+ const available = devices.filter(d => d.isAvailable && d.runtime.includes('iOS'));
39
+ const suggestions = available.slice(0, 5).map(d => ` - ${d.name}`).join('\n');
40
+ throw new Error(`Device "${args.deviceId}" not found.\n\nAvailable iOS devices:\n${suggestions}`);
41
+ }
42
+ if (match.state === 'Booted') {
43
+ return { content: [{ type: 'text', text: `${match.name} is already booted.` }] };
44
+ }
45
+ await execSimctl(['boot', match.udid], 'tool:boot');
46
+ // Open Simulator.app so the window appears
47
+ try {
48
+ await execFileAsync('open', ['-a', 'Simulator']);
49
+ }
50
+ catch { /* not critical */ }
51
+ // Wait for boot to complete (default: true)
52
+ const shouldWait = args.waitForBoot !== false;
53
+ if (shouldWait) {
54
+ const maxWaitMs = 60000;
55
+ const startTime = Date.now();
56
+ let booted = false;
57
+ while (Date.now() - startTime < maxWaitMs) {
58
+ await new Promise(r => setTimeout(r, 2000));
59
+ const updated = await listDevices('booted');
60
+ if (updated.some(d => d.udid === match.udid)) {
61
+ booted = true;
62
+ break;
63
+ }
64
+ }
65
+ if (!booted) {
66
+ return {
67
+ content: [{
68
+ type: 'text',
69
+ text: `Booted ${match.name} but it didn't reach Booted state within 60s. It may still be starting up.`,
70
+ }],
71
+ };
72
+ }
73
+ return {
74
+ content: [{
75
+ type: 'text',
76
+ text: `Booted ${match.name} (${match.udid}) — ready. Simulator.app is open.`,
77
+ }],
78
+ };
79
+ }
80
+ return {
81
+ content: [{
82
+ type: 'text',
83
+ text: `Booted ${match.name} (${match.udid}). Simulator.app should be opening.`,
84
+ }],
85
+ };
86
+ }
87
+ // --- shutdown ---
88
+ export const shutdownParams = {
89
+ deviceId: z.string().optional().describe('Device UDID, name, or "booted" (default: booted)'),
90
+ };
91
+ export async function handleShutdown(args) {
92
+ const device = await resolveDevice(args.deviceId);
93
+ await execSimctl(['shutdown', device], 'tool:shutdown');
94
+ return { content: [{ type: 'text', text: `Device shut down successfully.` }] };
95
+ }
96
+ // --- erase ---
97
+ export const eraseParams = {
98
+ deviceId: z.string().describe('Device UDID or name to erase (factory reset)'),
99
+ };
100
+ export async function handleErase(args) {
101
+ const device = await resolveDevice(args.deviceId);
102
+ await execSimctl(['erase', device], 'tool:erase');
103
+ return { content: [{ type: 'text', text: `Device erased (factory reset) successfully.` }] };
104
+ }
105
+ // --- open_url ---
106
+ export const openUrlParams = {
107
+ url: z.string().describe('URL or deep link to open (e.g., "https://example.com" or "myapp://path")'),
108
+ deviceId: z.string().optional().describe('Device UDID, name, or "booted" (default: booted)'),
109
+ };
110
+ export async function handleOpenUrl(args) {
111
+ const device = await resolveDevice(args.deviceId);
112
+ await execSimctl(['openurl', device, args.url], 'tool:openUrl');
113
+ return { content: [{ type: 'text', text: `Opened URL: ${args.url}` }] };
114
+ }
115
+ // --- open_simulator ---
116
+ export const openSimulatorParams = {};
117
+ export async function handleOpenSimulator() {
118
+ await execFileAsync('open', ['-a', 'Simulator']);
119
+ return { content: [{ type: 'text', text: 'Simulator.app opened.' }] };
120
+ }
121
+ // --- get_booted_sim_id ---
122
+ export const getBootedSimIdParams = {};
123
+ export async function handleGetBootedSimId() {
124
+ const devices = await listDevices('booted');
125
+ if (devices.length === 0) {
126
+ return { content: [{ type: 'text', text: 'No booted simulator found.' }] };
127
+ }
128
+ const d = devices[0];
129
+ return { content: [{ type: 'text', text: d.udid }] };
130
+ }
@@ -0,0 +1,101 @@
1
+ import { z } from 'zod';
2
+ export declare const tapParams: {
3
+ x: z.ZodNumber;
4
+ y: z.ZodNumber;
5
+ duration: z.ZodOptional<z.ZodNumber>;
6
+ deviceId: z.ZodOptional<z.ZodString>;
7
+ };
8
+ export declare function handleTap(args: {
9
+ x: number;
10
+ y: number;
11
+ duration?: number;
12
+ deviceId?: string;
13
+ }): Promise<{
14
+ content: {
15
+ type: "text";
16
+ text: string;
17
+ }[];
18
+ }>;
19
+ export declare const swipeParams: {
20
+ startX: z.ZodNumber;
21
+ startY: z.ZodNumber;
22
+ endX: z.ZodNumber;
23
+ endY: z.ZodNumber;
24
+ durationMs: z.ZodOptional<z.ZodNumber>;
25
+ delta: z.ZodOptional<z.ZodNumber>;
26
+ deviceId: z.ZodOptional<z.ZodString>;
27
+ };
28
+ export declare function handleSwipe(args: {
29
+ startX: number;
30
+ startY: number;
31
+ endX: number;
32
+ endY: number;
33
+ durationMs?: number;
34
+ delta?: number;
35
+ deviceId?: string;
36
+ }): Promise<{
37
+ content: {
38
+ type: "text";
39
+ text: string;
40
+ }[];
41
+ }>;
42
+ export declare const longPressParams: {
43
+ x: z.ZodNumber;
44
+ y: z.ZodNumber;
45
+ durationMs: z.ZodOptional<z.ZodNumber>;
46
+ deviceId: z.ZodOptional<z.ZodString>;
47
+ };
48
+ export declare function handleLongPress(args: {
49
+ x: number;
50
+ y: number;
51
+ durationMs?: number;
52
+ deviceId?: string;
53
+ }): Promise<{
54
+ content: {
55
+ type: "text";
56
+ text: string;
57
+ }[];
58
+ }>;
59
+ export declare const describePointParams: {
60
+ x: z.ZodNumber;
61
+ y: z.ZodNumber;
62
+ deviceId: z.ZodOptional<z.ZodString>;
63
+ };
64
+ export declare function handleDescribePoint(args: {
65
+ x: number;
66
+ y: number;
67
+ deviceId?: string;
68
+ }): Promise<{
69
+ content: {
70
+ type: "text";
71
+ text: string;
72
+ }[];
73
+ }>;
74
+ export declare const typeTextParams: {
75
+ text: z.ZodString;
76
+ deviceId: z.ZodOptional<z.ZodString>;
77
+ };
78
+ export declare function handleTypeText(args: {
79
+ text: string;
80
+ deviceId?: string;
81
+ }): Promise<{
82
+ content: {
83
+ type: "text";
84
+ text: string;
85
+ }[];
86
+ }>;
87
+ export declare const pressKeyParams: {
88
+ key: z.ZodString;
89
+ modifiers: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
90
+ deviceId: z.ZodOptional<z.ZodString>;
91
+ };
92
+ export declare function handlePressKey(args: {
93
+ key: string;
94
+ modifiers?: string[];
95
+ deviceId?: string;
96
+ }): Promise<{
97
+ content: {
98
+ type: "text";
99
+ text: string;
100
+ }[];
101
+ }>;
@@ -0,0 +1,159 @@
1
+ import { z } from 'zod';
2
+ import { resolveDevice } from '../helpers/simctl.js';
3
+ import { getScreenMapping, simToMac } from '../helpers/coordinate-mapper.js';
4
+ import * as applescript from '../helpers/applescript.js';
5
+ import * as idb from '../helpers/idb.js';
6
+ // --- tap ---
7
+ export const tapParams = {
8
+ x: z.number().describe('X coordinate in simulator screen points'),
9
+ y: z.number().describe('Y coordinate in simulator screen points'),
10
+ duration: z.number().optional().describe('Press duration in seconds (decimal allowed, e.g. 0.5). Default: normal tap'),
11
+ deviceId: z.string().optional().describe('Device UDID, name, or "booted" (default: booted)'),
12
+ };
13
+ export async function handleTap(args) {
14
+ const device = await resolveDevice(args.deviceId);
15
+ if (await idb.checkIdbAvailable()) {
16
+ await idb.idbTap(args.x, args.y, device, args.duration);
17
+ return {
18
+ content: [{
19
+ type: 'text',
20
+ text: `Tapped at (${args.x}, ${args.y})${args.duration ? ` for ${args.duration}s` : ''} via idb [cursor-free]`,
21
+ }],
22
+ };
23
+ }
24
+ // CGEvent fallback: map sim coords → macOS screen coords
25
+ const mapping = await getScreenMapping(device);
26
+ const { macX, macY } = simToMac(args.x, args.y, mapping);
27
+ if (args.duration && args.duration > 0.3) {
28
+ await applescript.longPress(macX, macY, args.duration * 1000);
29
+ }
30
+ else {
31
+ await applescript.tap(macX, macY);
32
+ }
33
+ return {
34
+ content: [{
35
+ type: 'text',
36
+ text: `Tapped at (${args.x}, ${args.y})${args.duration ? ` for ${args.duration}s` : ''} [CGEvent fallback]`,
37
+ }],
38
+ };
39
+ }
40
+ // --- swipe ---
41
+ export const swipeParams = {
42
+ startX: z.number().describe('Start X in simulator screen points. Use 1 to trigger iOS left-edge-swipe-back gesture (iOS recognizes edge touches within ~20pt of edge)'),
43
+ startY: z.number().describe('Start Y in simulator screen points'),
44
+ endX: z.number().describe('End X in simulator screen points'),
45
+ endY: z.number().describe('End Y in simulator screen points'),
46
+ durationMs: z.number().optional().describe('Swipe duration in milliseconds (default: 300). Use 400-600ms for edge-swipe-back'),
47
+ delta: z.number().optional().describe('Step size in pixels between each touch point (idb only, default: device decides)'),
48
+ deviceId: z.string().optional().describe('Device UDID, name, or "booted" (default: booted)'),
49
+ };
50
+ export async function handleSwipe(args) {
51
+ const device = await resolveDevice(args.deviceId);
52
+ const duration = args.durationMs || 300;
53
+ if (await idb.checkIdbAvailable()) {
54
+ await idb.idbSwipe(args.startX, args.startY, args.endX, args.endY, duration, device, args.delta);
55
+ return {
56
+ content: [{
57
+ type: 'text',
58
+ text: `Swiped from (${args.startX},${args.startY}) to (${args.endX},${args.endY}) via idb [cursor-free]`,
59
+ }],
60
+ };
61
+ }
62
+ // CGEvent fallback
63
+ const mapping = await getScreenMapping(device);
64
+ const from = simToMac(args.startX, args.startY, mapping);
65
+ const to = simToMac(args.endX, args.endY, mapping);
66
+ await applescript.swipe(from.macX, from.macY, to.macX, to.macY, duration);
67
+ return {
68
+ content: [{
69
+ type: 'text',
70
+ text: `Swiped from (${args.startX},${args.startY}) to (${args.endX},${args.endY}) [CGEvent fallback]`,
71
+ }],
72
+ };
73
+ }
74
+ // --- long_press ---
75
+ export const longPressParams = {
76
+ x: z.number().describe('X coordinate in simulator screen points'),
77
+ y: z.number().describe('Y coordinate in simulator screen points'),
78
+ durationMs: z.number().optional().describe('Press duration in milliseconds (default: 1000)'),
79
+ deviceId: z.string().optional().describe('Device UDID, name, or "booted" (default: booted)'),
80
+ };
81
+ export async function handleLongPress(args) {
82
+ const device = await resolveDevice(args.deviceId);
83
+ const duration = args.durationMs || 1000;
84
+ if (await idb.checkIdbAvailable()) {
85
+ await idb.idbLongPress(args.x, args.y, duration, device);
86
+ return {
87
+ content: [{
88
+ type: 'text',
89
+ text: `Long pressed at (${args.x}, ${args.y}) for ${duration}ms via idb [cursor-free]`,
90
+ }],
91
+ };
92
+ }
93
+ // CGEvent fallback
94
+ const mapping = await getScreenMapping(device);
95
+ const { macX, macY } = simToMac(args.x, args.y, mapping);
96
+ await applescript.longPress(macX, macY, duration);
97
+ return {
98
+ content: [{
99
+ type: 'text',
100
+ text: `Long pressed at (${args.x}, ${args.y}) for ${duration}ms [CGEvent fallback]`,
101
+ }],
102
+ };
103
+ }
104
+ // --- describe_point ---
105
+ export const describePointParams = {
106
+ x: z.number().describe('X coordinate in simulator screen points'),
107
+ y: z.number().describe('Y coordinate in simulator screen points'),
108
+ deviceId: z.string().optional().describe('Device UDID, name, or "booted" (default: booted)'),
109
+ };
110
+ export async function handleDescribePoint(args) {
111
+ const device = await resolveDevice(args.deviceId);
112
+ if (await idb.checkIdbAvailable()) {
113
+ const output = await idb.idbDescribePoint(args.x, args.y, device);
114
+ return {
115
+ content: [{
116
+ type: 'text',
117
+ text: `Accessibility element at (${args.x}, ${args.y}):\n\n${output}`,
118
+ }],
119
+ };
120
+ }
121
+ return {
122
+ content: [{
123
+ type: 'text',
124
+ text: 'describe_point requires idb. Install: brew tap facebook/fb && brew install idb-companion && pip3 install fb-idb',
125
+ }],
126
+ };
127
+ }
128
+ // --- type_text ---
129
+ export const typeTextParams = {
130
+ text: z.string().describe('Text to type into the currently focused field'),
131
+ deviceId: z.string().optional().describe('Device UDID, name, or "booted" (default: booted)'),
132
+ };
133
+ export async function handleTypeText(args) {
134
+ await resolveDevice(args.deviceId); // validate device is booted
135
+ await applescript.typeText(args.text);
136
+ return {
137
+ content: [{
138
+ type: 'text',
139
+ text: `Typed "${args.text.slice(0, 100)}${args.text.length > 100 ? '...' : ''}"`,
140
+ }],
141
+ };
142
+ }
143
+ // --- press_key ---
144
+ export const pressKeyParams = {
145
+ key: z.string().describe('Key name: return, escape, delete, tab, space, up, down, left, right, home, end, pageup, pagedown, f1-f12'),
146
+ modifiers: z.array(z.string()).optional().describe('Modifier keys: command, shift, option, control'),
147
+ deviceId: z.string().optional().describe('Device UDID, name, or "booted" (default: booted)'),
148
+ };
149
+ export async function handlePressKey(args) {
150
+ await resolveDevice(args.deviceId);
151
+ await applescript.pressKey(args.key, args.modifiers || []);
152
+ const modStr = args.modifiers?.length ? `${args.modifiers.join('+')}+` : '';
153
+ return {
154
+ content: [{
155
+ type: 'text',
156
+ text: `Pressed ${modStr}${args.key}`,
157
+ }],
158
+ };
159
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Playwright-inspired tools for iOS Simulator.
3
+ * Adds structured accessibility snapshots, element waiting, and element search
4
+ * to match the workflow patterns of Playwright MCP for web automation.
5
+ */
6
+ import { z } from 'zod';
7
+ export declare const snapshotParams: {
8
+ deviceId: z.ZodOptional<z.ZodString>;
9
+ };
10
+ export declare function handleSnapshot(args: {
11
+ deviceId?: string;
12
+ }): Promise<{
13
+ content: {
14
+ type: "text";
15
+ text: string;
16
+ }[];
17
+ }>;
18
+ export declare const waitForElementParams: {
19
+ label: z.ZodOptional<z.ZodString>;
20
+ role: z.ZodOptional<z.ZodString>;
21
+ text: z.ZodOptional<z.ZodString>;
22
+ timeoutMs: z.ZodOptional<z.ZodNumber>;
23
+ pollIntervalMs: z.ZodOptional<z.ZodNumber>;
24
+ deviceId: z.ZodOptional<z.ZodString>;
25
+ };
26
+ export declare function handleWaitForElement(args: {
27
+ label?: string;
28
+ role?: string;
29
+ text?: string;
30
+ timeoutMs?: number;
31
+ pollIntervalMs?: number;
32
+ deviceId?: string;
33
+ }): Promise<{
34
+ content: {
35
+ type: "text";
36
+ text: string;
37
+ }[];
38
+ isError: boolean;
39
+ } | {
40
+ content: {
41
+ type: "text";
42
+ text: string;
43
+ }[];
44
+ isError?: undefined;
45
+ }>;
46
+ export declare const elementExistsParams: {
47
+ label: z.ZodOptional<z.ZodString>;
48
+ role: z.ZodOptional<z.ZodString>;
49
+ text: z.ZodOptional<z.ZodString>;
50
+ deviceId: z.ZodOptional<z.ZodString>;
51
+ };
52
+ export declare function handleElementExists(args: {
53
+ label?: string;
54
+ role?: string;
55
+ text?: string;
56
+ deviceId?: string;
57
+ }): Promise<{
58
+ content: {
59
+ type: "text";
60
+ text: string;
61
+ }[];
62
+ isError: boolean;
63
+ } | {
64
+ content: {
65
+ type: "text";
66
+ text: string;
67
+ }[];
68
+ isError?: undefined;
69
+ }>;
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Playwright-inspired tools for iOS Simulator.
3
+ * Adds structured accessibility snapshots, element waiting, and element search
4
+ * to match the workflow patterns of Playwright MCP for web automation.
5
+ */
6
+ import { z } from 'zod';
7
+ import { resolveDevice } from '../helpers/simctl.js';
8
+ import * as idb from '../helpers/idb.js';
9
+ import * as logger from '../helpers/logger.js';
10
+ // --- snapshot (like Playwright's browser_snapshot) ---
11
+ // Returns a structured, LLM-friendly accessibility tree.
12
+ // Preferred over screenshots for understanding page structure.
13
+ export const snapshotParams = {
14
+ deviceId: z.string().optional().describe('Device (default: booted)'),
15
+ };
16
+ export async function handleSnapshot(args) {
17
+ const device = await resolveDevice(args.deviceId);
18
+ if (!(await idb.checkIdbAvailable())) {
19
+ return {
20
+ content: [{
21
+ type: 'text',
22
+ text: 'Snapshot requires idb for the full iOS accessibility tree. Install: brew tap facebook/fb && brew install idb-companion && pip3 install fb-idb\n\nUse simulator_accessibility_audit as a fallback (AppleScript-based, less detail).',
23
+ }],
24
+ };
25
+ }
26
+ const raw = await idb.idbDescribeAll(device);
27
+ // Parse idb output into structured format
28
+ try {
29
+ const elements = JSON.parse(raw);
30
+ if (Array.isArray(elements)) {
31
+ const formatted = formatAccessibilityTree(elements, 0);
32
+ return {
33
+ content: [{
34
+ type: 'text',
35
+ text: `iOS Accessibility Snapshot (${elements.length} root elements):\n\n${formatted}\n\n---\nUse coordinates from the snapshot to interact: simulator_tap, simulator_swipe, etc.`,
36
+ }],
37
+ };
38
+ }
39
+ }
40
+ catch { /* raw output isn't JSON, return as-is */ }
41
+ // Fallback: return raw idb output
42
+ const lines = raw.split('\n').filter((l) => l.trim());
43
+ return {
44
+ content: [{
45
+ type: 'text',
46
+ text: `iOS Accessibility Snapshot (${lines.length} elements):\n\n${raw}\n\n---\nUse coordinates from the snapshot to interact: simulator_tap, simulator_swipe, etc.`,
47
+ }],
48
+ };
49
+ }
50
+ /**
51
+ * Format parsed accessibility elements into a readable tree.
52
+ */
53
+ function formatAccessibilityTree(elements, depth) {
54
+ const indent = ' '.repeat(depth);
55
+ const lines = [];
56
+ for (const el of elements) {
57
+ const role = el.role || el.AXRole || '?';
58
+ const label = el.AXLabel || el.label || el.description || '';
59
+ const value = el.AXValue || el.value || '';
60
+ const frame = el.frame || el.AXFrame;
61
+ let line = `${indent}${role}`;
62
+ if (label)
63
+ line += ` "${label}"`;
64
+ if (value && value !== label)
65
+ line += ` val="${value}"`;
66
+ if (frame) {
67
+ if (typeof frame === 'object') {
68
+ line += ` @(${Math.round(frame.x)},${Math.round(frame.y)}) ${Math.round(frame.width)}x${Math.round(frame.height)}`;
69
+ }
70
+ else if (typeof frame === 'string') {
71
+ line += ` ${frame}`;
72
+ }
73
+ }
74
+ lines.push(line);
75
+ // Recurse into children
76
+ const children = el.children || el.AXChildren;
77
+ if (Array.isArray(children) && children.length > 0) {
78
+ lines.push(formatAccessibilityTree(children, depth + 1));
79
+ }
80
+ }
81
+ return lines.join('\n');
82
+ }
83
+ // --- wait_for_element (like Playwright's browser_wait_for) ---
84
+ // Polls the accessibility tree until an element matching the criteria appears.
85
+ export const waitForElementParams = {
86
+ label: z.string().optional().describe('Wait for element with this accessibility label (case-insensitive partial match)'),
87
+ role: z.string().optional().describe('Wait for element with this role (e.g., "Button", "TextField", "StaticText")'),
88
+ text: z.string().optional().describe('Wait for element containing this text in label or value'),
89
+ timeoutMs: z.number().optional().describe('Max wait time in milliseconds (default: 10000)'),
90
+ pollIntervalMs: z.number().optional().describe('How often to check in milliseconds (default: 500)'),
91
+ deviceId: z.string().optional().describe('Device (default: booted)'),
92
+ };
93
+ export async function handleWaitForElement(args) {
94
+ if (!args.label && !args.role && !args.text) {
95
+ return {
96
+ content: [{
97
+ type: 'text',
98
+ text: 'Provide at least one search criteria: label, role, or text.',
99
+ }],
100
+ isError: true,
101
+ };
102
+ }
103
+ const device = await resolveDevice(args.deviceId);
104
+ if (!(await idb.checkIdbAvailable())) {
105
+ return {
106
+ content: [{
107
+ type: 'text',
108
+ text: 'wait_for_element requires idb. Install: brew tap facebook/fb && brew install idb-companion && pip3 install fb-idb',
109
+ }],
110
+ };
111
+ }
112
+ const timeout = args.timeoutMs || 10000;
113
+ const interval = args.pollIntervalMs || 500;
114
+ const startTime = Date.now();
115
+ while (Date.now() - startTime < timeout) {
116
+ try {
117
+ const raw = await idb.idbDescribeAll(device);
118
+ const searchStr = raw.toLowerCase();
119
+ let found = true;
120
+ if (args.label && !searchStr.includes(args.label.toLowerCase()))
121
+ found = false;
122
+ if (args.role && !searchStr.includes(args.role.toLowerCase()))
123
+ found = false;
124
+ if (args.text && !searchStr.includes(args.text.toLowerCase()))
125
+ found = false;
126
+ if (found) {
127
+ const elapsed = Date.now() - startTime;
128
+ return {
129
+ content: [{
130
+ type: 'text',
131
+ text: `Element found after ${elapsed}ms. Criteria: ${JSON.stringify({ label: args.label, role: args.role, text: args.text })}`,
132
+ }],
133
+ };
134
+ }
135
+ }
136
+ catch (err) {
137
+ logger.debug('tool:waitForElement', `Poll error: ${err.message}`);
138
+ }
139
+ await new Promise(resolve => setTimeout(resolve, interval));
140
+ }
141
+ return {
142
+ content: [{
143
+ type: 'text',
144
+ text: `Timeout after ${timeout}ms. Element not found. Criteria: ${JSON.stringify({ label: args.label, role: args.role, text: args.text })}`,
145
+ }],
146
+ isError: true,
147
+ };
148
+ }
149
+ // --- element_exists ---
150
+ // Quick boolean check: does an element matching criteria exist on screen right now?
151
+ export const elementExistsParams = {
152
+ label: z.string().optional().describe('Search for element with this accessibility label (case-insensitive partial match)'),
153
+ role: z.string().optional().describe('Search for element with this role'),
154
+ text: z.string().optional().describe('Search for element containing this text'),
155
+ deviceId: z.string().optional().describe('Device (default: booted)'),
156
+ };
157
+ export async function handleElementExists(args) {
158
+ if (!args.label && !args.role && !args.text) {
159
+ return {
160
+ content: [{
161
+ type: 'text',
162
+ text: 'Provide at least one search criteria: label, role, or text.',
163
+ }],
164
+ isError: true,
165
+ };
166
+ }
167
+ const device = await resolveDevice(args.deviceId);
168
+ if (!(await idb.checkIdbAvailable())) {
169
+ return {
170
+ content: [{
171
+ type: 'text',
172
+ text: 'element_exists requires idb. Install: brew tap facebook/fb && brew install idb-companion && pip3 install fb-idb',
173
+ }],
174
+ };
175
+ }
176
+ try {
177
+ const raw = await idb.idbDescribeAll(device);
178
+ const searchStr = raw.toLowerCase();
179
+ let found = true;
180
+ if (args.label && !searchStr.includes(args.label.toLowerCase()))
181
+ found = false;
182
+ if (args.role && !searchStr.includes(args.role.toLowerCase()))
183
+ found = false;
184
+ if (args.text && !searchStr.includes(args.text.toLowerCase()))
185
+ found = false;
186
+ return {
187
+ content: [{
188
+ type: 'text',
189
+ text: found
190
+ ? `true — Element exists. Criteria: ${JSON.stringify({ label: args.label, role: args.role, text: args.text })}`
191
+ : `false — Element not found. Criteria: ${JSON.stringify({ label: args.label, role: args.role, text: args.text })}`,
192
+ }],
193
+ };
194
+ }
195
+ catch (err) {
196
+ return {
197
+ content: [{
198
+ type: 'text',
199
+ text: `Error checking element: ${err.message}`,
200
+ }],
201
+ isError: true,
202
+ };
203
+ }
204
+ }
@@ -0,0 +1,27 @@
1
+ import { z } from 'zod';
2
+ export declare const screenshotParams: {
3
+ deviceId: z.ZodOptional<z.ZodString>;
4
+ format: z.ZodOptional<z.ZodEnum<["png", "jpeg"]>>;
5
+ display: z.ZodOptional<z.ZodEnum<["internal", "external"]>>;
6
+ mask: z.ZodOptional<z.ZodEnum<["ignored", "alpha", "black"]>>;
7
+ savePath: z.ZodOptional<z.ZodString>;
8
+ };
9
+ export declare function handleScreenshot(args: {
10
+ deviceId?: string;
11
+ format?: string;
12
+ display?: string;
13
+ mask?: string;
14
+ savePath?: string;
15
+ }): Promise<{
16
+ content: ({
17
+ type: "image";
18
+ data: string;
19
+ mimeType: string;
20
+ text?: undefined;
21
+ } | {
22
+ type: "text";
23
+ text: string;
24
+ data?: undefined;
25
+ mimeType?: undefined;
26
+ })[];
27
+ }>;