1 # ControlKit runners
2
3 ControlKit is the native XCTest runner used by the CLI's Apple-platform MCP
4 tools. The public runner project is
5 [`xcrs-controlkit`](https://github.com/ondeinference/xcrs-controlkit). It uses
6 one Xcode project generated by XcodeGen, with platform-specific targets and
7 schemes:
8
9 | Platform | Scheme | App product |
10 | --- | --- | --- |
11 | iOS / iPadOS | `ControlKit-iOS` | `ControlKit.app` |
12 | tvOS | `ControlKit-tvOS` | `ControlKit.app` |
13 | visionOS | `ControlKit-visionOS` | `ControlKit.app` |
14 | watchOS | `ControlKit-watchOS` | `ControlKit.app` |
15 | macOS | `ControlKit-macOS` | `ControlKit.app` |
16
17 The scheme and target names keep their platform suffixes so Xcode can select
18 the correct SDK, while each built app is named `ControlKit`.
19
20 ## Build a runner
21
22 Install XcodeGen once, clone the runner project, and generate the Xcode
23 project:
24
25 ```sh
26 brew install xcodegen
27 cd <path-to-xcrs-controlkit>/Runners
28 xcodegen generate
29 ```
30
31 Build the simulator runner for the platform you need:
32
33 ```sh
34 xcodebuild -project XCRSControlKitRunner.xcodeproj \
35 -scheme ControlKit-<platform> \
36 -destination 'generic/platform=<platform> Simulator' \
37 build-for-testing
38 ```
39
40 Replace `<platform>` with `iOS`, `tvOS`, `watchOS`, or `visionOS`. macOS is
41 built with `-destination 'platform=macOS'` and runs on the local Mac rather
42 than a simulator.
43
44 ## Start the runner
45
46 Boot a compatible simulator and run the UI-test host:
47
48 ```sh
49 xcodebuild -project XCRSControlKitRunner.xcodeproj \
50 -scheme ControlKit-<platform> \
51 -destination 'id=<simulator-udid>' \
52 test-without-building
53 ```
54
55 The shared XCTest runner starts a local ControlKit JSON-RPC server on
56 `127.0.0.1:12004`. Keep that test process running while calling the MCP tools.
57 The macOS UI-test target includes the network-server entitlement required to
58 bind the local port.
59
60 ## Run on a physical device
61
62 The same runner also works installed on a real iOS, tvOS, watchOS, or
63 visionOS device, not just a simulator. Build and start it against the
64 device's UDID (find it with `xcrun devicectl list devices`) instead of a
65 simulator destination:
66
67 ```sh
68 xcodebuild -project XCRSControlKitRunner.xcodeproj \
69 -scheme ControlKit-<platform> \
70 -destination 'id=<device-udid>' \
71 build-for-testing
72
73 TEST_RUNNER_CONTROLKIT_LISTEN_HOST=0.0.0.0 xcodebuild \
74 -project XCRSControlKitRunner.xcodeproj \
75 -scheme ControlKit-<platform> \
76 -destination 'id=<device-udid>' \
77 test-without-building
78 ```
79
80 The `CONTROLKIT_LISTEN_HOST` environment variable controls what the
81 in-process RPC server binds to (`Shared/ControlKitRPCServer.swift`) — it
82 defaults to `127.0.0.1`, which is loopback-only *on the device itself* and
83 unreachable from your Mac. `xcodebuild` forwards any `TEST_RUNNER_`-prefixed
84 environment variable into the test process with the prefix stripped, so
85 setting `TEST_RUNNER_CONTROLKIT_LISTEN_HOST=0.0.0.0` makes the server bind on
86 every interface, including the one reachable over your LAN. Find the device's
87 address with its Bonjour hostname (`<device-name>.local`, discoverable via
88 `dns-sd -B _airplay._tcp local` for an Apple TV) or its CoreDevice tunnel
89 address (`xcrun devicectl device info details --device <udid>`, under
90 `Tunnel IP Address`).
91
92 ## Connect through the automation MCP profile
93
94 Start either automation server:
95
96 ```sh
97 xcrs --mcp
98 # or
99 smb --mcp --scope automation
100 ```
101
102 The runner tools accept either `simulator_name` or `simulator_udid` for a
103 simulator, or `host` (plus `controlkit_port`, defaulting to `12004`) for a
104 physical device or any other remote runner. Call `device_select` once to
105 remember the target so later tools need no target arguments. Both commands
106 expose the same unprefixed tool names.
107
108 | Tool | Runner | Purpose |
109 | --- | --- | --- |
110 | `device_select` | All platforms | Remember the active simulator or device for later tools. |
111 | `device_capabilities` | All platforms | Read platform and capability information. |
112 | `input_click` | macOS | Click at screen coordinates. |
113 | `input_spatial_tap` | visionOS | Perform a spatial tap. |
114 | `input_tap` | iOS / tvOS / watchOS, simulator or physical | Perform a touch tap. |
115 | `input_text` | iOS / tvOS, simulator or physical | Type into the focused field. |
116 | `input_swipe` | iOS / tvOS, simulator or physical | Swipe between coordinates. |
117 | `input_button` | iOS / tvOS, simulator or physical | Press a Home or tvOS remote button. |
118 | `app_launch`/`app_terminate` | simulator or physical | Launch/terminate an app by bundle ID. On a physical device this calls the runner's `device.apps.launch`/`device.apps.terminate` RPC methods instead of `simctl`. |
119 | `screen_capture` | Simulator or physical | Capture a PNG screenshot. Uses `simctl` for a simulator, or `devicectl device capture screenshot` when a `device` identifier is given. |
120 | `ui_describe` | All UI-test runners | Read the accessibility hierarchy for the supplied `bundle_id`. |
121 | `ui_element_list` | All UI-test runners | Read actionable elements with tap coordinates for the supplied `bundle_id`. |
122 | `ui_tap` | All UI-test runners | Resolve and activate an accessibility element by label, identifier, or value. |
123
124 UI introspection requires a recent ControlKit runner that implements
125 `device.dump.ui`. The `ui_tap` action requires a runner that implements
126 `device.ui.tap`; it resolves and activates the requested element inside the
127 target app, so an MCP client does not need to turn accessibility coordinates
128 into a second action. The label, identifier, or value must match exactly one
129 element. On tvOS, that element must already have focus; use `input_button` to
130 navigate focus before calling `ui_tap`. All three UI tools require the target
131 app's `bundle_id`; this lets XCTest attach to an already-running app instead of
132 accidentally reading the ControlKit host app.
133
134 For a local macOS runner, omit simulator fields and pass `host` and
135 `controlkit_port` when needed:
136
137 ```json
138 {
139 "host": "127.0.0.1",
140 "controlkit_port": 12004,
141 "x": 400,
142 "y": 300
143 }
144 ```
145
146 For a physical device, pass its Bonjour hostname or tunnel address as `host`
147 instead:
148
149 ```json
150 {
151 "host": "<device-name>.local",
152 "controlkit_port": 12004,
153 "button": "select"
154 }
155 ```
156
157 Platform-specific operations intentionally reject unsupported input. For
158 example, visionOS rejects raw touch, watchOS rejects pointer and spatial input,
159 and macOS rejects touch and Home-button requests.
160
161 ## See also
162
163 For the product-level overview of end-to-end testing with xcrs and
164 ControlKit, see [smbcloud.xyz/xcrs-end-to-end-testing](https://smbcloud.xyz/xcrs-end-to-end-testing).