preflight-ios-mcp 1.0.0 → 2.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.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  The most comprehensive MCP (Model Context Protocol) server for iOS Simulator automation. Gives AI agents like Claude, ChatGPT, Cursor, Windsurf, and any MCP-compatible tool full control over iOS Simulators — tap, swipe, type, read accessibility trees, inspect app data, capture screenshots, record video, manage devices, and debug apps in real time.
4
4
 
5
- **57 tools** across 10 categories. Zero cursor interference — works silently in the background while you use your Mac.
5
+ **82 tools** across 14 categories. Zero cursor interference — works silently in the background while you use your Mac.
6
6
 
7
7
  Inspired by [Playwright MCP](https://github.com/anthropics/mcp-server-playwright) for web automation — Preflight brings the same structured accessibility-first approach to iOS.
8
8
 
@@ -15,7 +15,7 @@ Inspired by [Playwright MCP](https://github.com/anthropics/mcp-server-playwright
15
15
  - **AI-optimized** — Images compressed for minimal token usage. Video → key frames (most AI models can't view video files).
16
16
  - **Accessibility-first** — Like Playwright's `browser_snapshot`, use `simulator_snapshot` to understand the screen without vision models.
17
17
  - **Cursor-free** — Touch injection via idb (IndigoHID) — your Mac cursor stays put.
18
- - **57 tools** — From basic tap/swipe to Dynamic Type testing, biometric enrollment, crash log analysis.
18
+ - **82 tools** — From basic tap/swipe to StoreKit testing, network conditioning, memory profiling, and crash log analysis.
19
19
 
20
20
  ## Quick Start
21
21
 
@@ -265,7 +265,7 @@ Set the `PATH` environment variable to include idb's location for cursor-free to
265
265
  | `simulator_record_video` | Start screen recording. On stop, key frames are extracted as images for AI chat. |
266
266
  | `simulator_stop_recording` | Stop recording. Returns key frames inline (no disk clutter). Optional `savePath` to keep the video. |
267
267
 
268
- ### Advanced Debugging & Testing (14 tools)
268
+ ### Advanced Debugging & Testing (18 tools)
269
269
 
270
270
  | Tool | Description |
271
271
  |------|-------------|
@@ -279,16 +279,66 @@ Set the `PATH` environment variable to include idb's location for cursor-free to
279
279
  | `simulator_verbose_logging` | Enable/disable verbose device logging for deep debugging. |
280
280
  | `simulator_install_app_data` | Install .xcappdata packages to restore test data snapshots. |
281
281
  | `simulator_get_env` | Read environment variables from the running simulator. |
282
- | `simulator_biometric` | Set Face ID / Touch ID enrollment state for auth testing. |
282
+ | `simulator_biometric` | Enroll, unenroll, match, or fail Face ID / Touch ID for auth testing. |
283
283
  | `simulator_network_status` | Get network configuration — DNS, interfaces, connectivity status. |
284
284
  | `simulator_defaults_read` | Read UserDefaults from inside the simulator (inspect app prefs, feature flags). |
285
285
  | `simulator_defaults_write` | Write UserDefaults inside the simulator (set flags, inject test config). |
286
+ | `simulator_rotate` | Rotate the simulator left or right. |
287
+ | `simulator_notify_post` | Post a Darwin notification to trigger system events. |
288
+ | `simulator_set_locale` | Set device locale for internationalization testing. |
289
+ | `simulator_trigger_siri` | Invoke Siri for voice command testing. |
290
+
291
+ ### Accessibility Settings (4 tools)
292
+
293
+ | Tool | Description |
294
+ |------|-------------|
295
+ | `simulator_set_reduce_motion` | Toggle Reduce Motion accessibility setting (via defaults write + notification). |
296
+ | `simulator_set_smart_invert` | Toggle Smart Invert Colors (via defaults write + notification). |
297
+ | `simulator_set_bold_text` | Toggle Bold Text (via defaults write + notification). |
298
+ | `simulator_set_reduce_transparency` | Toggle Reduce Transparency (via defaults write + notification). |
299
+
300
+ ### Device Creation & Management (4 tools)
301
+
302
+ | Tool | Description |
303
+ |------|-------------|
304
+ | `simulator_create_device` | Create a new simulator with device type and runtime. |
305
+ | `simulator_delete_device` | Permanently delete a simulator device. |
306
+ | `simulator_rename_device` | Rename an existing device. |
307
+ | `simulator_clone_device` | Clone a device with all its state. |
308
+
309
+ ### StoreKit Testing (6 tools) — Xcode 14-16
310
+
311
+ | Tool | Description |
312
+ |------|-------------|
313
+ | `simulator_storekit_config` | Enable or disable StoreKit test mode. |
314
+ | `simulator_storekit_transactions` | List all StoreKit test transactions. |
315
+ | `simulator_storekit_delete_transactions` | Clear all test transactions. |
316
+ | `simulator_storekit_manage_subscription` | Expire or force-renew a subscription. |
317
+ | `simulator_storekit_manage_transaction` | Refund, approve, or decline ask-to-buy transactions. |
318
+ | `simulator_storekit_reset_eligibility` | Reset introductory offer eligibility for all products. |
319
+
320
+ ### Network Testing (2 tools)
321
+
322
+ | Tool | Description |
323
+ |------|-------------|
324
+ | `simulator_network_condition` | Apply network throttling with presets (3G, LTE, Edge, WiFi, 100% loss) or custom bandwidth/latency/loss. |
325
+ | `simulator_network_capture` | Capture network activity summary — active connections, DNS, interfaces. |
326
+
327
+ ### Debugging & Profiling (5 tools)
328
+
329
+ | Tool | Description |
330
+ |------|-------------|
331
+ | `simulator_leak_check` | Check a running app for memory leaks via Apple's `leaks` tool. |
332
+ | `simulator_heap_info` | Dump heap allocation summary — object counts by class, total memory. |
333
+ | `simulator_vmmap` | Show virtual memory map — regions, sizes, permissions. |
334
+ | `simulator_sample_process` | Sample a process for CPU hotspot detection and hang analysis. |
335
+ | `simulator_thermal_state` | Simulate thermal pressure state changes (nominal, fair, serious, critical). |
286
336
 
287
337
  ## Architecture
288
338
 
289
339
  ```
290
340
  src/
291
- ├── index.ts # MCP server entry, 57 tool registrations
341
+ ├── index.ts # MCP server entry, 82 tool registrations
292
342
  ├── helpers/
293
343
  │ ├── idb.ts # Facebook idb CLI wrapper (cursor-free touch)
294
344
  │ ├── simctl.ts # xcrun simctl command wrapper
@@ -304,7 +354,10 @@ src/
304
354
  ├── system.ts # Location, push, clipboard, media, permissions
305
355
  ├── ui.ts # Appearance, status bar, video recording, navigate back
306
356
  ├── debug.ts # Logs, files, crash reports, accessibility
307
- ├── advanced.ts # Dynamic Type, keychain, iCloud, biometric, defaults
357
+ ├── advanced.ts # Dynamic Type, keychain, iCloud, biometric, defaults, accessibility, rotation, locale
358
+ ├── storekit.ts # StoreKit testing — transactions, subscriptions, eligibility
359
+ ├── network.ts # Network conditioning (dnctl/pfctl) and capture
360
+ ├── profiling.ts # Memory profiling — leaks, heap, vmmap, sample, thermal
308
361
  └── playwright.ts # Snapshot, wait_for_element, element_exists
309
362
  ```
310
363
 
@@ -345,11 +398,14 @@ xcodebuild -project MCPDemo.xcodeproj -scheme MCPDemo \
345
398
  build
346
399
  ```
347
400
 
348
- The demo app has 4 tabs exercising every tool category:
401
+ The demo app has 7 tabs exercising every tool category:
349
402
  - **Interactions**: Buttons, text fields, long-press zones, navigation stack, scrollable lists
350
403
  - **Location**: Live GPS display for testing `simulator_set_location`
351
404
  - **Notifications**: Push notification display for testing `simulator_send_push`
352
405
  - **Settings**: Clipboard, file I/O, accessibility toggles, UserDefaults
406
+ - **StoreKit**: Mock purchases and subscriptions for testing StoreKit tools
407
+ - **Network**: Connection monitoring and latency testing for network conditioning
408
+ - **Debug**: Memory/CPU stress tests, thermal state, accessibility settings observer, biometric auth
353
409
 
354
410
  ## Configuration
355
411
 
@@ -1,5 +1,8 @@
1
- import { execSimctlBuffer, runAppleScript } from './simctl.js';
1
+ import { execSimctl, runAppleScript } from './simctl.js';
2
2
  import * as logger from './logger.js';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { readFile, unlink } from 'node:fs/promises';
3
6
  const TITLE_BAR_HEIGHT = 28;
4
7
  /**
5
8
  * Get the Simulator.app front window position and size using AppleScript.
@@ -33,8 +36,11 @@ end tell`;
33
36
  * Returns { pointWidth, pointHeight, scaleFactor }.
34
37
  */
35
38
  export async function getDeviceScreenDimensions(device) {
36
- // Take a screenshot and get its pixel dimensions
37
- const { stdout } = await execSimctlBuffer(['io', device, 'screenshot', '--type=png', '-'], 'coordinate-mapper');
39
+ // Take a screenshot and get its pixel dimensions (temp file to avoid stdout piping issues)
40
+ const tmpPath = join(tmpdir(), `simscr-dim-${Date.now()}.png`);
41
+ await execSimctl(['io', device, 'screenshot', '--type=png', tmpPath], 'coordinate-mapper');
42
+ const stdout = await readFile(tmpPath);
43
+ await unlink(tmpPath).catch(() => { });
38
44
  // Parse PNG header to get dimensions (IHDR chunk at offset 16)
39
45
  // PNG signature (8 bytes) + IHDR length (4 bytes) + "IHDR" (4 bytes) + width (4 bytes) + height (4 bytes)
40
46
  if (stdout.length < 24) {
package/dist/index.js CHANGED
@@ -4,20 +4,23 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
4
4
  import * as logger from './helpers/logger.js';
5
5
  // Tool imports
6
6
  import { screenshotParams, handleScreenshot } from './tools/screenshot.js';
7
- import { listDevicesParams, handleListDevices, bootParams, handleBoot, shutdownParams, handleShutdown, eraseParams, handleErase, openUrlParams, handleOpenUrl, openSimulatorParams, handleOpenSimulator, getBootedSimIdParams, handleGetBootedSimId, } from './tools/device.js';
7
+ import { listDevicesParams, handleListDevices, bootParams, handleBoot, shutdownParams, handleShutdown, eraseParams, handleErase, openUrlParams, handleOpenUrl, openSimulatorParams, handleOpenSimulator, getBootedSimIdParams, handleGetBootedSimId, createDeviceParams, handleCreateDevice, deleteDeviceParams, handleDeleteDevice, renameDeviceParams, handleRenameDevice, cloneDeviceParams, handleCloneDevice, } from './tools/device.js';
8
8
  import { listAppsParams, handleListApps, appInfoParams, handleAppInfo, launchAppParams, handleLaunchApp, terminateAppParams, handleTerminateApp, installAppParams, handleInstallApp, uninstallAppParams, handleUninstallApp, } from './tools/app.js';
9
9
  import { tapParams, handleTap, swipeParams, handleSwipe, longPressParams, handleLongPress, describePointParams, handleDescribePoint, typeTextParams, handleTypeText, pressKeyParams, handlePressKey, } from './tools/interaction.js';
10
10
  import { setLocationParams, handleSetLocation, sendPushParams, handleSendPush, setClipboardParams, handleSetClipboard, getClipboardParams, handleGetClipboard, addMediaParams, handleAddMedia, grantPermissionParams, handleGrantPermission, } from './tools/system.js';
11
11
  import { setAppearanceParams, handleSetAppearance, overrideStatusBarParams, handleOverrideStatusBar, recordVideoParams, handleRecordVideo, stopRecordingParams, handleStopRecording, navigateBackParams, handleNavigateBack, } from './tools/ui.js';
12
12
  import { getLogsParams, handleGetLogs, streamLogsParams, handleStreamLogs, getAppContainerParams, handleGetAppContainer, listAppFilesParams, handleListAppFiles, readAppFileParams, handleReadAppFile, getCrashLogsParams, handleGetCrashLogs, diagnoseParams, handleDiagnose, accessibilityAuditParams, handleAccessibilityAudit, getScreenInfoParams, handleGetScreenInfo, } from './tools/debug.js';
13
- import { icloudSyncParams, handleIcloudSync, keychainParams, handleKeychain, contentSizeParams, handleContentSize, increaseContrastParams, handleIncreaseContrast, locationScenarioParams, handleLocationScenario, locationRouteParams, handleLocationRoute, verboseLoggingParams, handleVerboseLogging, installAppDataParams, handleInstallAppData, getEnvParams, handleGetEnv, memoryWarningParams, handleMemoryWarning, biometricParams, handleBiometric, networkStatusParams, handleNetworkStatus, defaultsReadParams, handleDefaultsRead, defaultsWriteParams, handleDefaultsWrite, } from './tools/advanced.js';
13
+ import { icloudSyncParams, handleIcloudSync, keychainParams, handleKeychain, contentSizeParams, handleContentSize, increaseContrastParams, handleIncreaseContrast, locationScenarioParams, handleLocationScenario, locationRouteParams, handleLocationRoute, verboseLoggingParams, handleVerboseLogging, installAppDataParams, handleInstallAppData, getEnvParams, handleGetEnv, memoryWarningParams, handleMemoryWarning, biometricParams, handleBiometric, networkStatusParams, handleNetworkStatus, defaultsReadParams, handleDefaultsRead, defaultsWriteParams, handleDefaultsWrite, reduceMotionParams, handleReduceMotion, smartInvertParams, handleSmartInvert, boldTextParams, handleBoldText, reduceTransparencyParams, handleReduceTransparency, rotateParams, handleRotate, notifyPostParams, handleNotifyPost, setLocaleParams, handleSetLocale, triggerSiriParams, handleTriggerSiri, } from './tools/advanced.js';
14
14
  import { snapshotParams, handleSnapshot, waitForElementParams, handleWaitForElement, elementExistsParams, handleElementExists, } from './tools/playwright.js';
15
+ import { storekitConfigParams, handleStorekitConfig, storekitTransactionsParams, handleStorekitTransactions, storekitDeleteTransactionsParams, handleStorekitDeleteTransactions, storekitManageSubscriptionParams, handleStorekitManageSubscription, storekitManageTransactionParams, handleStorekitManageTransaction, storekitResetEligibilityParams, handleStorekitResetEligibility, } from './tools/storekit.js';
16
+ import { networkConditionParams, handleNetworkCondition, networkCaptureParams, handleNetworkCapture, } from './tools/network.js';
17
+ import { leakCheckParams, handleLeakCheck, heapInfoParams, handleHeapInfo, vmmapParams, handleVmmap, sampleProcessParams, handleSampleProcess, thermalStateParams, handleThermalState, } from './tools/profiling.js';
15
18
  // Support tool filtering via environment variable
16
19
  const filteredTools = new Set((process.env.PREFLIGHT_FILTERED_TOOLS || process.env.IOS_SIMULATOR_MCP_FILTERED_TOOLS || '')
17
20
  .split(',').map(s => s.trim()).filter(Boolean));
18
21
  const server = new McpServer({
19
22
  name: 'preflight-mcp',
20
- version: '1.0.0',
23
+ version: '2.0.0',
21
24
  });
22
25
  // Register a tool, skipping if it's in the filtered list
23
26
  function registerTool(name, description, params, handler) {
@@ -109,7 +112,7 @@ registerTool('simulator_verbose_logging', 'Enable or disable verbose device logg
109
112
  registerTool('simulator_install_app_data', 'Install an .xcappdata package to replace the current app container contents. Useful for restoring test data snapshots.', installAppDataParams, wrapHandler('simulator_install_app_data', handleInstallAppData));
110
113
  registerTool('simulator_get_env', 'Read an environment variable from the running simulator device (e.g., HOME, TMPDIR, PATH).', getEnvParams, wrapHandler('simulator_get_env', handleGetEnv));
111
114
  registerTool('simulator_memory_warning', 'Trigger a simulated memory warning. Apps will receive didReceiveMemoryWarning and can be tested for proper memory cleanup.', memoryWarningParams, wrapHandler('simulator_memory_warning', handleMemoryWarning));
112
- registerTool('simulator_biometric', 'Set Face ID / Touch ID enrollment state. Test biometric authentication flows.', biometricParams, wrapHandler('simulator_biometric', handleBiometric));
115
+ registerTool('simulator_biometric', 'Control Face ID / Touch ID: enroll, unenroll, trigger matching (success), or trigger failure. Test biometric authentication flows end-to-end.', biometricParams, wrapHandler('simulator_biometric', handleBiometric));
113
116
  registerTool('simulator_network_status', 'Get the current network configuration inside the simulator — interfaces, IP addresses, DNS config.', networkStatusParams, wrapHandler('simulator_network_status', handleNetworkStatus));
114
117
  registerTool('simulator_defaults_read', 'Read UserDefaults values from inside the simulator. Inspect app preferences, feature flags, and configuration.', defaultsReadParams, wrapHandler('simulator_defaults_read', handleDefaultsRead));
115
118
  registerTool('simulator_defaults_write', 'Write UserDefaults values inside the simulator. Set feature flags, change app configuration, or inject test data.', defaultsWriteParams, wrapHandler('simulator_defaults_write', handleDefaultsWrite));
@@ -117,6 +120,36 @@ registerTool('simulator_defaults_write', 'Write UserDefaults values inside the s
117
120
  registerTool('simulator_snapshot', 'Capture a structured accessibility snapshot of the current screen — like Playwright\'s browser_snapshot. Returns roles, labels, values, and positions. PREFERRED over screenshots for understanding UI structure and targeting interactions. No vision model needed.', snapshotParams, wrapHandler('simulator_snapshot', handleSnapshot));
118
121
  registerTool('simulator_wait_for_element', 'Wait for an accessibility element to appear on screen. Polls until the element matching your criteria (label, role, or text) appears, or times out. Like Playwright\'s browser_wait_for.', waitForElementParams, wrapHandler('simulator_wait_for_element', handleWaitForElement));
119
122
  registerTool('simulator_element_exists', 'Quick check: does an element matching your criteria exist on screen right now? Returns true/false. Useful for conditional logic.', elementExistsParams, wrapHandler('simulator_element_exists', handleElementExists));
123
+ // ========== StoreKit Testing Tools ==========
124
+ registerTool('simulator_storekit_config', 'Enable or disable StoreKit testing on the simulator. Required before using other StoreKit tools.', storekitConfigParams, wrapHandler('simulator_storekit_config', handleStorekitConfig));
125
+ registerTool('simulator_storekit_transactions', 'List all StoreKit test transactions (purchases, subscriptions, etc.).', storekitTransactionsParams, wrapHandler('simulator_storekit_transactions', handleStorekitTransactions));
126
+ registerTool('simulator_storekit_delete_transactions', 'Delete all StoreKit test transactions. Clears purchase history for fresh testing.', storekitDeleteTransactionsParams, wrapHandler('simulator_storekit_delete_transactions', handleStorekitDeleteTransactions));
127
+ registerTool('simulator_storekit_manage_subscription', 'Expire or force-renew a StoreKit test subscription by transaction ID.', storekitManageSubscriptionParams, wrapHandler('simulator_storekit_manage_subscription', handleStorekitManageSubscription));
128
+ registerTool('simulator_storekit_manage_transaction', 'Refund, approve, or decline ask-to-buy for a StoreKit test transaction.', storekitManageTransactionParams, wrapHandler('simulator_storekit_manage_transaction', handleStorekitManageTransaction));
129
+ registerTool('simulator_storekit_reset_eligibility', 'Reset introductory offer eligibility for all StoreKit products. Allows re-testing intro pricing.', storekitResetEligibilityParams, wrapHandler('simulator_storekit_reset_eligibility', handleStorekitResetEligibility));
130
+ // ========== Network Testing Tools ==========
131
+ registerTool('simulator_network_condition', 'Apply network throttling: bandwidth limits, latency, and packet loss. Presets: 3G, LTE, Edge, WiFi, WiFi-lossy, 100%-loss (offline). Simulator shares host network stack, so conditioning affects all simulator traffic.', networkConditionParams, wrapHandler('simulator_network_condition', handleNetworkCondition));
132
+ registerTool('simulator_network_capture', 'Capture a snapshot of network activity: active TCP connections, DNS resolution, host interfaces, and any active network conditioning.', networkCaptureParams, wrapHandler('simulator_network_capture', handleNetworkCapture));
133
+ // ========== Debugging & Profiling Tools ==========
134
+ registerTool('simulator_leak_check', 'Check a running app for memory leaks using Apple\'s leaks tool. Returns leak count, leaked bytes, and details.', leakCheckParams, wrapHandler('simulator_leak_check', handleLeakCheck));
135
+ registerTool('simulator_heap_info', 'Dump heap allocation summary for a running app. Shows object counts by class and total memory usage.', heapInfoParams, wrapHandler('simulator_heap_info', handleHeapInfo));
136
+ registerTool('simulator_vmmap', 'Show virtual memory map for a running app. Displays memory regions, sizes, and permissions. Useful for diagnosing memory issues.', vmmapParams, wrapHandler('simulator_vmmap', handleVmmap));
137
+ registerTool('simulator_sample_process', 'Sample a running app\'s CPU activity for a few seconds. Returns call stack tree — useful for finding performance hotspots and hangs.', sampleProcessParams, wrapHandler('simulator_sample_process', handleSampleProcess));
138
+ registerTool('simulator_thermal_state', 'Simulate thermal pressure changes (nominal, fair, serious, critical). Test how your app responds to device overheating.', thermalStateParams, wrapHandler('simulator_thermal_state', handleThermalState));
139
+ // ========== Accessibility Settings Tools ==========
140
+ registerTool('simulator_set_reduce_motion', 'Enable or disable the Reduce Motion accessibility setting. Test animations and transitions with motion sensitivity.', reduceMotionParams, wrapHandler('simulator_set_reduce_motion', handleReduceMotion));
141
+ registerTool('simulator_set_smart_invert', 'Enable or disable Smart Invert Colors. Test how your app handles inverted color schemes.', smartInvertParams, wrapHandler('simulator_set_smart_invert', handleSmartInvert));
142
+ registerTool('simulator_set_bold_text', 'Enable or disable Bold Text accessibility setting. Test text rendering with bold system fonts.', boldTextParams, wrapHandler('simulator_set_bold_text', handleBoldText));
143
+ registerTool('simulator_set_reduce_transparency', 'Enable or disable Reduce Transparency. Test UI readability without blur/vibrancy effects.', reduceTransparencyParams, wrapHandler('simulator_set_reduce_transparency', handleReduceTransparency));
144
+ // ========== Additional Device Management ==========
145
+ registerTool('simulator_create_device', 'Create a new simulator device. Specify name and device type; auto-detects latest iOS runtime if not provided.', createDeviceParams, wrapHandler('simulator_create_device', handleCreateDevice));
146
+ registerTool('simulator_delete_device', 'Permanently delete a simulator device by UDID or name.', deleteDeviceParams, wrapHandler('simulator_delete_device', handleDeleteDevice));
147
+ registerTool('simulator_rename_device', 'Rename a simulator device.', renameDeviceParams, wrapHandler('simulator_rename_device', handleRenameDevice));
148
+ registerTool('simulator_clone_device', 'Clone a simulator device with all its current state — apps, data, settings.', cloneDeviceParams, wrapHandler('simulator_clone_device', handleCloneDevice));
149
+ registerTool('simulator_rotate', 'Rotate the simulator device left or right. Uses Cmd+Arrow keyboard shortcut in Simulator.app.', rotateParams, wrapHandler('simulator_rotate', handleRotate));
150
+ registerTool('simulator_notify_post', 'Post a Darwin notification inside the simulator. Useful for triggering system events or testing notification observers.', notifyPostParams, wrapHandler('simulator_notify_post', handleNotifyPost));
151
+ registerTool('simulator_set_locale', 'Set the device locale and language for internationalization testing. Requires device reboot to take effect.', setLocaleParams, wrapHandler('simulator_set_locale', handleSetLocale));
152
+ registerTool('simulator_trigger_siri', 'Invoke Siri on the simulator. Use simulator_type_text to enter a query if text input is available.', triggerSiriParams, wrapHandler('simulator_trigger_siri', handleTriggerSiri));
120
153
  // ========== Start Server ==========
121
154
  async function main() {
122
155
  logger.info('server', 'Preflight MCP server starting...');
@@ -144,11 +144,13 @@ export declare function handleMemoryWarning(args: {
144
144
  }[];
145
145
  }>;
146
146
  export declare const biometricParams: {
147
- enrolled: z.ZodBoolean;
147
+ action: z.ZodOptional<z.ZodEnum<["enroll", "unenroll", "match", "fail"]>>;
148
+ enrolled: z.ZodOptional<z.ZodBoolean>;
148
149
  deviceId: z.ZodOptional<z.ZodString>;
149
150
  };
150
151
  export declare function handleBiometric(args: {
151
- enrolled: boolean;
152
+ action?: string;
153
+ enrolled?: boolean;
152
154
  deviceId?: string;
153
155
  }): Promise<{
154
156
  content: {
@@ -201,3 +203,107 @@ export declare function handleDefaultsWrite(args: {
201
203
  text: string;
202
204
  }[];
203
205
  }>;
206
+ export declare const reduceMotionParams: {
207
+ enabled: z.ZodBoolean;
208
+ deviceId: z.ZodOptional<z.ZodString>;
209
+ };
210
+ export declare function handleReduceMotion(args: {
211
+ enabled: boolean;
212
+ deviceId?: string;
213
+ }): Promise<{
214
+ content: {
215
+ type: "text";
216
+ text: string;
217
+ }[];
218
+ }>;
219
+ export declare const smartInvertParams: {
220
+ enabled: z.ZodBoolean;
221
+ deviceId: z.ZodOptional<z.ZodString>;
222
+ };
223
+ export declare function handleSmartInvert(args: {
224
+ enabled: boolean;
225
+ deviceId?: string;
226
+ }): Promise<{
227
+ content: {
228
+ type: "text";
229
+ text: string;
230
+ }[];
231
+ }>;
232
+ export declare const boldTextParams: {
233
+ enabled: z.ZodBoolean;
234
+ deviceId: z.ZodOptional<z.ZodString>;
235
+ };
236
+ export declare function handleBoldText(args: {
237
+ enabled: boolean;
238
+ deviceId?: string;
239
+ }): Promise<{
240
+ content: {
241
+ type: "text";
242
+ text: string;
243
+ }[];
244
+ }>;
245
+ export declare const reduceTransparencyParams: {
246
+ enabled: z.ZodBoolean;
247
+ deviceId: z.ZodOptional<z.ZodString>;
248
+ };
249
+ export declare function handleReduceTransparency(args: {
250
+ enabled: boolean;
251
+ deviceId?: string;
252
+ }): Promise<{
253
+ content: {
254
+ type: "text";
255
+ text: string;
256
+ }[];
257
+ }>;
258
+ export declare const rotateParams: {
259
+ direction: z.ZodEnum<["left", "right"]>;
260
+ deviceId: z.ZodOptional<z.ZodString>;
261
+ };
262
+ export declare function handleRotate(args: {
263
+ direction: 'left' | 'right';
264
+ deviceId?: string;
265
+ }): Promise<{
266
+ content: {
267
+ type: "text";
268
+ text: string;
269
+ }[];
270
+ }>;
271
+ export declare const notifyPostParams: {
272
+ notification: z.ZodString;
273
+ deviceId: z.ZodOptional<z.ZodString>;
274
+ };
275
+ export declare function handleNotifyPost(args: {
276
+ notification: string;
277
+ deviceId?: string;
278
+ }): Promise<{
279
+ content: {
280
+ type: "text";
281
+ text: string;
282
+ }[];
283
+ }>;
284
+ export declare const setLocaleParams: {
285
+ locale: z.ZodString;
286
+ language: z.ZodOptional<z.ZodString>;
287
+ deviceId: z.ZodOptional<z.ZodString>;
288
+ };
289
+ export declare function handleSetLocale(args: {
290
+ locale: string;
291
+ language?: string;
292
+ deviceId?: string;
293
+ }): Promise<{
294
+ content: {
295
+ type: "text";
296
+ text: string;
297
+ }[];
298
+ }>;
299
+ export declare const triggerSiriParams: {
300
+ deviceId: z.ZodOptional<z.ZodString>;
301
+ };
302
+ export declare function handleTriggerSiri(args: {
303
+ deviceId?: string;
304
+ }): Promise<{
305
+ content: {
306
+ type: "text";
307
+ text: string;
308
+ }[];
309
+ }>;
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { execSimctl, resolveDevice } from '../helpers/simctl.js';
2
+ import { execSimctl, resolveDevice, runAppleScript } from '../helpers/simctl.js';
3
3
  // --- icloud_sync ---
4
4
  export const icloudSyncParams = {
5
5
  deviceId: z.string().optional().describe('Device (default: booted)'),
@@ -93,7 +93,19 @@ export async function handleLocationRoute(args) {
93
93
  if (args.waypoints.length < 2) {
94
94
  return { content: [{ type: 'text', text: 'At least 2 waypoints required.' }] };
95
95
  }
96
- const waypointData = args.waypoints.map(w => `${w.lat},${w.lng}`).join('\n') + '\n';
96
+ // Generate proper GPX XML for simctl location start (incremental timestamps for movement)
97
+ const baseTime = Date.now();
98
+ const gpxWaypoints = args.waypoints.map((w, i) => ` <trkpt lat="${w.lat}" lon="${w.lng}"><time>${new Date(baseTime + i * 1000).toISOString()}</time></trkpt>`).join('\n');
99
+ const gpxData = `<?xml version="1.0" encoding="UTF-8"?>
100
+ <gpx version="1.1" creator="preflight-mcp">
101
+ <trk>
102
+ <name>Preflight Route</name>
103
+ <trkseg>
104
+ ${gpxWaypoints}
105
+ </trkseg>
106
+ </trk>
107
+ </gpx>
108
+ `;
97
109
  const cmdArgs = ['simctl', 'location', device, 'start'];
98
110
  if (args.speed)
99
111
  cmdArgs.push('--speed=' + args.speed);
@@ -109,7 +121,7 @@ export async function handleLocationRoute(args) {
109
121
  else
110
122
  reject(new Error(stderr || `exit code ${code}`));
111
123
  });
112
- child.stdin.write(waypointData);
124
+ child.stdin.write(gpxData);
113
125
  child.stdin.end();
114
126
  });
115
127
  return {
@@ -184,25 +196,41 @@ export async function handleMemoryWarning(args) {
184
196
  }
185
197
  // --- biometric_enrollment ---
186
198
  export const biometricParams = {
187
- enrolled: z.boolean().describe('Whether Face ID / Touch ID is enrolled'),
199
+ action: z.enum(['enroll', 'unenroll', 'match', 'fail']).optional().describe('"enroll" to enable biometrics, "unenroll" to disable, "match" to simulate successful auth, "fail" to simulate failed auth'),
200
+ enrolled: z.boolean().optional().describe('(Deprecated: use action instead) Whether Face ID / Touch ID is enrolled'),
188
201
  deviceId: z.string().optional().describe('Device (default: booted)'),
189
202
  };
190
203
  export async function handleBiometric(args) {
191
204
  const device = await resolveDevice(args.deviceId);
192
- // Use notifyutil to toggle biometric enrollment
205
+ // Backward compat: map enrolled boolean to action
206
+ let action = args.action;
207
+ if (!action && args.enrolled !== undefined) {
208
+ action = args.enrolled ? 'enroll' : 'unenroll';
209
+ }
210
+ if (!action) {
211
+ return { content: [{ type: 'text', text: 'Provide action ("enroll", "unenroll", "match", or "fail") or enrolled (true/false).' }] };
212
+ }
193
213
  try {
194
- const val = args.enrolled ? '1' : '0';
195
- await execSimctl(['spawn', device, 'notifyutil', '-s', 'com.apple.BiometricKit.enrollmentChanged', val], 'tool:biometric');
196
- return {
197
- content: [{
198
- type: 'text',
199
- text: `Biometric enrollment set to ${args.enrolled ? 'enrolled' : 'not enrolled'}.`,
200
- }],
201
- };
214
+ switch (action) {
215
+ case 'enroll':
216
+ await execSimctl(['spawn', device, 'notifyutil', '-s', 'com.apple.BiometricKit.enrollmentChanged', '1'], 'tool:biometric');
217
+ return { content: [{ type: 'text', text: 'Biometric enrollment enabled (Face ID / Touch ID enrolled).' }] };
218
+ case 'unenroll':
219
+ await execSimctl(['spawn', device, 'notifyutil', '-s', 'com.apple.BiometricKit.enrollmentChanged', '0'], 'tool:biometric');
220
+ return { content: [{ type: 'text', text: 'Biometric enrollment disabled.' }] };
221
+ case 'match':
222
+ await execSimctl(['spawn', device, 'notifyutil', '-p', 'com.apple.BiometricKit_Sim.fingerTouch.match'], 'tool:biometric');
223
+ return { content: [{ type: 'text', text: 'Biometric match triggered — app should receive successful authentication.' }] };
224
+ case 'fail':
225
+ await execSimctl(['spawn', device, 'notifyutil', '-p', 'com.apple.BiometricKit_Sim.fingerTouch.nomatch'], 'tool:biometric');
226
+ return { content: [{ type: 'text', text: 'Biometric failure triggered — app should receive authentication failure.' }] };
227
+ default:
228
+ return { content: [{ type: 'text', text: `Unknown action: ${action}` }] };
229
+ }
202
230
  }
203
231
  catch (err) {
204
232
  const e = err;
205
- return { content: [{ type: 'text', text: `Biometric enrollment change failed: ${e.message}. Use Simulator > Features > Face ID/Touch ID menu.` }] };
233
+ return { content: [{ type: 'text', text: `Biometric action failed: ${e.message}. Use Simulator > Features > Face ID/Touch ID menu as alternative.` }] };
206
234
  }
207
235
  }
208
236
  // --- network_status (read/display network configuration) ---
@@ -273,3 +301,114 @@ export async function handleDefaultsWrite(args) {
273
301
  await execSimctl(['spawn', device, 'defaults', 'write', args.domain, args.key, typeFlag, args.value], 'tool:defaultsWrite');
274
302
  return { content: [{ type: 'text', text: `Set ${args.domain}.${args.key} = ${args.value} (${args.type || 'string'})` }] };
275
303
  }
304
+ // --- reduce_motion ---
305
+ export const reduceMotionParams = {
306
+ enabled: z.boolean().describe('Enable or disable Reduce Motion accessibility setting'),
307
+ deviceId: z.string().optional().describe('Device (default: booted)'),
308
+ };
309
+ export async function handleReduceMotion(args) {
310
+ const device = await resolveDevice(args.deviceId);
311
+ await execSimctl(['spawn', device, 'defaults', 'write', 'com.apple.Accessibility', 'ReduceMotionEnabled', '-bool', args.enabled ? 'true' : 'false'], 'tool:reduceMotion');
312
+ await execSimctl(['spawn', device, 'notifyutil', '-p', 'com.apple.accessibility.reduceMotionStatusDidChange'], 'tool:reduceMotion');
313
+ return { content: [{ type: 'text', text: `Reduce Motion ${args.enabled ? 'enabled' : 'disabled'}.` }] };
314
+ }
315
+ // --- smart_invert ---
316
+ export const smartInvertParams = {
317
+ enabled: z.boolean().describe('Enable or disable Smart Invert Colors'),
318
+ deviceId: z.string().optional().describe('Device (default: booted)'),
319
+ };
320
+ export async function handleSmartInvert(args) {
321
+ const device = await resolveDevice(args.deviceId);
322
+ await execSimctl(['spawn', device, 'defaults', 'write', 'com.apple.Accessibility', 'AXSSystemUIProcessAppSmartInvertEnabledPreference', '-bool', args.enabled ? 'true' : 'false'], 'tool:smartInvert');
323
+ await execSimctl(['spawn', device, 'notifyutil', '-p', 'com.apple.accessibility.invertColorsStatusDidChange'], 'tool:smartInvert');
324
+ return { content: [{ type: 'text', text: `Smart Invert ${args.enabled ? 'enabled' : 'disabled'}.` }] };
325
+ }
326
+ // --- bold_text ---
327
+ export const boldTextParams = {
328
+ enabled: z.boolean().describe('Enable or disable Bold Text'),
329
+ deviceId: z.string().optional().describe('Device (default: booted)'),
330
+ };
331
+ export async function handleBoldText(args) {
332
+ const device = await resolveDevice(args.deviceId);
333
+ await execSimctl(['spawn', device, 'defaults', 'write', 'com.apple.Accessibility', 'BoldTextEnabled', '-bool', args.enabled ? 'true' : 'false'], 'tool:boldText');
334
+ await execSimctl(['spawn', device, 'notifyutil', '-p', 'com.apple.accessibility.boldTextStatusDidChange'], 'tool:boldText');
335
+ return { content: [{ type: 'text', text: `Bold Text ${args.enabled ? 'enabled' : 'disabled'}.` }] };
336
+ }
337
+ // --- reduce_transparency ---
338
+ export const reduceTransparencyParams = {
339
+ enabled: z.boolean().describe('Enable or disable Reduce Transparency'),
340
+ deviceId: z.string().optional().describe('Device (default: booted)'),
341
+ };
342
+ export async function handleReduceTransparency(args) {
343
+ const device = await resolveDevice(args.deviceId);
344
+ await execSimctl(['spawn', device, 'defaults', 'write', 'com.apple.Accessibility', 'EnhancedBackgroundContrastEnabled', '-bool', args.enabled ? 'true' : 'false'], 'tool:reduceTransparency');
345
+ await execSimctl(['spawn', device, 'notifyutil', '-p', 'com.apple.accessibility.reduceTransparencyStatusDidChange'], 'tool:reduceTransparency');
346
+ return { content: [{ type: 'text', text: `Reduce Transparency ${args.enabled ? 'enabled' : 'disabled'}.` }] };
347
+ }
348
+ // --- rotate ---
349
+ export const rotateParams = {
350
+ direction: z.enum(['left', 'right']).describe('Rotation direction'),
351
+ deviceId: z.string().optional().describe('Device (default: booted)'),
352
+ };
353
+ export async function handleRotate(args) {
354
+ await resolveDevice(args.deviceId);
355
+ // Cmd+Left/Right arrow in Simulator.app rotates the device
356
+ const keyCode = args.direction === 'left' ? 123 : 124; // left=123, right=124
357
+ const script = `
358
+ tell application "Simulator" to activate
359
+ delay 0.3
360
+ tell application "System Events"
361
+ key code ${keyCode} using command down
362
+ end tell`;
363
+ await runAppleScript(script, 'tool:rotate');
364
+ return { content: [{ type: 'text', text: `Device rotated ${args.direction}.` }] };
365
+ }
366
+ // --- notify_post ---
367
+ export const notifyPostParams = {
368
+ notification: z.string().describe('Darwin notification name to post (e.g., "com.apple.MobileDataSettingsUpdated")'),
369
+ deviceId: z.string().optional().describe('Device (default: booted)'),
370
+ };
371
+ export async function handleNotifyPost(args) {
372
+ const device = await resolveDevice(args.deviceId);
373
+ await execSimctl(['spawn', device, 'notifyutil', '-p', args.notification], 'tool:notifyPost');
374
+ return { content: [{ type: 'text', text: `Darwin notification posted: ${args.notification}` }] };
375
+ }
376
+ // --- set_locale ---
377
+ export const setLocaleParams = {
378
+ locale: z.string().describe('Locale identifier (e.g., "en_US", "ja_JP", "fr_FR", "de_DE")'),
379
+ language: z.string().optional().describe('Language identifier (e.g., "en", "ja", "fr"). If omitted, derived from locale'),
380
+ deviceId: z.string().optional().describe('Device (default: booted)'),
381
+ };
382
+ export async function handleSetLocale(args) {
383
+ const device = await resolveDevice(args.deviceId);
384
+ const lang = args.language || args.locale.split('_')[0];
385
+ await execSimctl(['spawn', device, 'defaults', 'write', '.GlobalPreferences', 'AppleLocale', '-string', args.locale], 'tool:setLocale');
386
+ await execSimctl(['spawn', device, 'defaults', 'write', '.GlobalPreferences', 'AppleLanguages', '-array', lang], 'tool:setLocale');
387
+ return {
388
+ content: [{
389
+ type: 'text',
390
+ text: `Locale set to "${args.locale}", language to "${lang}". Reboot the simulator for changes to take effect (use simulator_shutdown then simulator_boot).`,
391
+ }],
392
+ };
393
+ }
394
+ // --- trigger_siri ---
395
+ export const triggerSiriParams = {
396
+ deviceId: z.string().optional().describe('Device (default: booted)'),
397
+ };
398
+ export async function handleTriggerSiri(args) {
399
+ await resolveDevice(args.deviceId);
400
+ const script = `
401
+ tell application "Simulator" to activate
402
+ delay 0.3
403
+ tell application "System Events"
404
+ key code 49 using command down
405
+ end tell`;
406
+ try {
407
+ await runAppleScript(script, 'tool:triggerSiri');
408
+ return { content: [{ type: 'text', text: 'Siri invoked (Cmd+Space). Use simulator_type_text to enter a query if text input is available.' }] };
409
+ }
410
+ catch (err) {
411
+ const e = err;
412
+ return { content: [{ type: 'text', text: `Failed to trigger Siri: ${e.message}` }] };
413
+ }
414
+ }
@@ -36,8 +36,24 @@ export async function handleGetLogs(args) {
36
36
  if (args.messageContains)
37
37
  predicateParts.push(`eventMessage CONTAINS "${esc(args.messageContains)}"`);
38
38
  const cmdArgs = ['simctl', 'spawn', device, 'log', 'show'];
39
- if (args.level)
40
- cmdArgs.push('--level', args.level);
39
+ // log show uses --debug/--info flags, not --level
40
+ if (args.level) {
41
+ switch (args.level) {
42
+ case 'debug':
43
+ cmdArgs.push('--debug');
44
+ break;
45
+ case 'info':
46
+ cmdArgs.push('--info');
47
+ break;
48
+ case 'error':
49
+ predicateParts.push('(messageType == error OR messageType == fault)');
50
+ break;
51
+ case 'fault':
52
+ predicateParts.push('messageType == fault');
53
+ break;
54
+ // 'default' needs no flag — it's the default behavior
55
+ }
56
+ }
41
57
  cmdArgs.push('--last', args.since || '1m');
42
58
  cmdArgs.push('--style', 'compact');
43
59
  if (predicateParts.length > 0) {
@@ -131,7 +147,25 @@ export async function handleStreamLogs(args) {
131
147
  while (buffer.length > maxLines)
132
148
  buffer.shift();
133
149
  });
150
+ child.on('error', (err) => {
151
+ buffer.push(`[error] Stream process error: ${err.message}`);
152
+ });
153
+ child.on('exit', (code, signal) => {
154
+ buffer.push(`[info] Stream process exited (code=${code}, signal=${signal})`);
155
+ });
134
156
  activeStreams.set(streamKey, { process: child, buffer, maxLines });
157
+ // Brief check to verify the process didn't die immediately
158
+ await new Promise(resolve => setTimeout(resolve, 200));
159
+ if (child.exitCode !== null) {
160
+ const output = buffer.join('\n');
161
+ activeStreams.delete(streamKey);
162
+ return {
163
+ content: [{
164
+ type: 'text',
165
+ text: `Log stream failed to start (exit code ${child.exitCode}):\n${output}`,
166
+ }],
167
+ };
168
+ }
135
169
  return {
136
170
  content: [{
137
171
  type: 'text',