@smbcloud / cli / commits / 8bce545

Use ControlKit

Seto Elkahfi committed Jul 30, 2026 at 17:01 UTC 8bce545036df165ef03cde5656c5714f2fda41b3
2 files changed +80 -80
crates/xcrs/src/mcp.rs
+68 -68
@@ -41,9 +41,9 @@ pub struct SimulatorTargetArgs {
41 /// Simulator UDID.
42 #[serde(default)]
43 pub simulator_udid: Option<String>,
44 - /// Local DeviceKit JSON-RPC port. Defaults to 12004.
44 + /// Local ControlKit JSON-RPC port. Defaults to 12004.
45 #[serde(default)]
46 - pub devicekit_port: Option<u16>,
46 + pub controlkit_port: Option<u16>,
47 }
48
49 #[derive(Debug, Deserialize, JsonSchema)]
@@ -54,9 +54,9 @@ pub struct SimulatorAppArgs {
54 /// Simulator UDID.
55 #[serde(default)]
56 pub simulator_udid: Option<String>,
57 - /// Local DeviceKit JSON-RPC port. Defaults to 12004.
57 + /// Local ControlKit JSON-RPC port. Defaults to 12004.
58 #[serde(default)]
59 - pub devicekit_port: Option<u16>,
59 + pub controlkit_port: Option<u16>,
60 /// App bundle identifier.
61 pub bundle_id: String,
62 }
@@ -69,9 +69,9 @@ pub struct SimulatorOpenUrlArgs {
69 /// Simulator UDID.
70 #[serde(default)]
71 pub simulator_udid: Option<String>,
72 - /// Local DeviceKit JSON-RPC port. Defaults to 12004.
72 + /// Local ControlKit JSON-RPC port. Defaults to 12004.
73 #[serde(default)]
74 - pub devicekit_port: Option<u16>,
74 + pub controlkit_port: Option<u16>,
75 /// HTTP(S) URL or custom URL scheme.
76 pub url: String,
77 }
@@ -84,9 +84,9 @@ pub struct SimulatorTapArgs {
84 /// Simulator UDID.
85 #[serde(default)]
86 pub simulator_udid: Option<String>,
87 - /// Local DeviceKit JSON-RPC port. Defaults to 12004.
87 + /// Local ControlKit JSON-RPC port. Defaults to 12004.
88 #[serde(default)]
89 - pub devicekit_port: Option<u16>,
89 + pub controlkit_port: Option<u16>,
90 /// Horizontal screen coordinate.
91 pub x: f32,
92 /// Vertical screen coordinate.
@@ -101,9 +101,9 @@ pub struct SimulatorTextArgs {
101 /// Simulator UDID.
102 #[serde(default)]
103 pub simulator_udid: Option<String>,
104 - /// Local DeviceKit JSON-RPC port. Defaults to 12004.
104 + /// Local ControlKit JSON-RPC port. Defaults to 12004.
105 #[serde(default)]
106 - pub devicekit_port: Option<u16>,
106 + pub controlkit_port: Option<u16>,
107 /// Text to type into the focused field.
108 pub text: String,
109 }
@@ -116,9 +116,9 @@ pub struct SimulatorSwipeArgs {
116 /// Simulator UDID.
117 #[serde(default)]
118 pub simulator_udid: Option<String>,
119 - /// Local DeviceKit JSON-RPC port. Defaults to 12004.
119 + /// Local ControlKit JSON-RPC port. Defaults to 12004.
120 #[serde(default)]
121 - pub devicekit_port: Option<u16>,
121 + pub controlkit_port: Option<u16>,
122 /// Swipe start horizontal coordinate.
123 pub x1: i32,
124 /// Swipe start vertical coordinate.
@@ -137,24 +137,24 @@ pub struct SimulatorOrientationArgs {
137 /// Simulator UDID.
138 #[serde(default)]
139 pub simulator_udid: Option<String>,
140 - /// Local DeviceKit JSON-RPC port. Defaults to 12004.
140 + /// Local ControlKit JSON-RPC port. Defaults to 12004.
141 #[serde(default)]
142 - pub devicekit_port: Option<u16>,
142 + pub controlkit_port: Option<u16>,
143 /// Desired orientation: PORTRAIT or LANDSCAPE.
144 pub orientation: String,
145 }
146
147 #[derive(Debug, Deserialize, JsonSchema)]
148 -pub struct SimulatorDeviceKitArgs {
148 +pub struct SimulatorControlKitArgs {
149 /// Exact simulator name.
150 #[serde(default)]
151 pub simulator_name: Option<String>,
152 /// Simulator UDID.
153 #[serde(default)]
154 pub simulator_udid: Option<String>,
155 - /// Local DeviceKit JSON-RPC port. Defaults to 12004.
155 + /// Local ControlKit JSON-RPC port. Defaults to 12004.
156 #[serde(default)]
157 - pub devicekit_port: Option<u16>,
157 + pub controlkit_port: Option<u16>,
158 }
159
160 #[derive(Debug, Deserialize, JsonSchema)]
@@ -165,9 +165,9 @@ pub struct SimulatorButtonArgs {
165 /// Simulator UDID.
166 #[serde(default)]
167 pub simulator_udid: Option<String>,
168 - /// Local DeviceKit JSON-RPC port. Defaults to 12004.
168 + /// Local ControlKit JSON-RPC port. Defaults to 12004.
169 #[serde(default)]
170 - pub devicekit_port: Option<u16>,
170 + pub controlkit_port: Option<u16>,
171 /// tvOS button: up, down, left, right, select, menu, home, or playPause.
172 pub button: String,
173 }
@@ -228,23 +228,23 @@ macro_rules! xcrs_mcp_tools {
228 }
229 }
230
231 - fn devicekit_from_target(
231 + fn controlkit_from_target(
232 simulator_name: &Option<String>,
233 simulator_udid: &Option<String>,
234 - devicekit_port: Option<u16>,
234 + controlkit_port: Option<u16>,
235 ) -> ::std::result::Result<
236 - ($crate::Simulator, $crate::DeviceKit),
236 + ($crate::Simulator, $crate::ControlKit),
237 ::rmcp::model::ErrorData,
238 > {
239 let target = $crate::mcp::SimulatorTargetArgs {
240 simulator_name: simulator_name.clone(),
241 simulator_udid: simulator_udid.clone(),
242 - devicekit_port,
242 + controlkit_port,
243 };
244 let simulator = Self::simulator_from_target(&target)?;
245 Ok((
246 simulator,
247 - $crate::DeviceKit::new(devicekit_port.unwrap_or(12004)),
247 + $crate::ControlKit::new(controlkit_port.unwrap_or(12004)),
248 ))
249 }
250
@@ -379,7 +379,7 @@ macro_rules! xcrs_mcp_tools {
379 let target = $crate::mcp::SimulatorTargetArgs {
380 simulator_name: args.simulator_name,
381 simulator_udid: args.simulator_udid,
382 - devicekit_port: args.devicekit_port,
382 + controlkit_port: args.controlkit_port,
383 };
384 let simulator = Self::simulator_from_target(&target)?;
385 $crate::XcodeCommandLineTools::new()
@@ -414,7 +414,7 @@ macro_rules! xcrs_mcp_tools {
414 let target = $crate::mcp::SimulatorTargetArgs {
415 simulator_name: args.simulator_name,
416 simulator_udid: args.simulator_udid,
417 - devicekit_port: args.devicekit_port,
417 + controlkit_port: args.controlkit_port,
418 };
419 let simulator = Self::simulator_from_target(&target)?;
420 $crate::XcodeCommandLineTools::new()
@@ -449,7 +449,7 @@ macro_rules! xcrs_mcp_tools {
449 let target = $crate::mcp::SimulatorTargetArgs {
450 simulator_name: args.simulator_name,
451 simulator_udid: args.simulator_udid,
452 - devicekit_port: args.devicekit_port,
452 + controlkit_port: args.controlkit_port,
453 };
454 let simulator = Self::simulator_from_target(&target)?;
455 $crate::XcodeCommandLineTools::new()
@@ -468,25 +468,25 @@ macro_rules! xcrs_mcp_tools {
468
469 #[::rmcp::tool(
470 name = $simulator_ui_dump_name,
471 - description = "Return the accessibility UI hierarchy from the foreground iOS app through DeviceKit."
471 + description = "Return the accessibility UI hierarchy from the foreground iOS app through ControlKit."
472 )]
473 async fn simulator_ui_dump(
474 &self,
475 ::rmcp::handler::server::wrapper::Parameters(
476 args,
477 ): ::rmcp::handler::server::wrapper::Parameters<
478 - $crate::mcp::SimulatorDeviceKitArgs,
478 + $crate::mcp::SimulatorControlKitArgs,
479 >,
480 ) -> ::std::result::Result<
481 ::rmcp::model::CallToolResult,
482 ::rmcp::model::ErrorData,
483 > {
484 - let (simulator, devicekit) = Self::devicekit_from_target(
484 + let (simulator, controlkit) = Self::controlkit_from_target(
485 &args.simulator_name,
486 &args.simulator_udid,
487 - args.devicekit_port,
487 + args.controlkit_port,
488 )?;
489 - let result = devicekit
489 + let result = controlkit
490 .call("device.dump.ui", ::serde_json::json!({ "format": "json" }))
491 .await
492 .map_err(|error| {
@@ -502,31 +502,31 @@ macro_rules! xcrs_mcp_tools {
502
503 #[::rmcp::tool(
504 name = $simulator_list_elements_name,
505 - description = "List actionable accessibility elements and coordinates from the foreground iOS app through DeviceKit."
505 + description = "List actionable accessibility elements and coordinates from the foreground iOS app through ControlKit."
506 )]
507 async fn simulator_list_elements(
508 &self,
509 ::rmcp::handler::server::wrapper::Parameters(
510 args,
511 ): ::rmcp::handler::server::wrapper::Parameters<
512 - $crate::mcp::SimulatorDeviceKitArgs,
512 + $crate::mcp::SimulatorControlKitArgs,
513 >,
514 ) -> ::std::result::Result<
515 ::rmcp::model::CallToolResult,
516 ::rmcp::model::ErrorData,
517 > {
518 - let (simulator, devicekit) = Self::devicekit_from_target(
518 + let (simulator, controlkit) = Self::controlkit_from_target(
519 &args.simulator_name,
520 &args.simulator_udid,
521 - args.devicekit_port,
521 + args.controlkit_port,
522 )?;
523 - let ui = devicekit
523 + let ui = controlkit
524 .call("device.dump.ui", ::serde_json::json!({ "format": "json" }))
525 .await
526 .map_err(|error| {
527 ::rmcp::model::ErrorData::internal_error(error.to_string(), None)
528 })?;
529 - let elements = $crate::extract_devicekit_elements(&ui);
529 + let elements = $crate::extract_controlkit_elements(&ui);
530 Ok(::rmcp::model::CallToolResult::success(vec![
531 ::rmcp::model::ContentBlock::json(&::serde_json::json!({
532 "simulator": simulator,
@@ -537,7 +537,7 @@ macro_rules! xcrs_mcp_tools {
537
538 #[::rmcp::tool(
539 name = $simulator_tap_name,
540 - description = "Tap the iOS simulator screen at the given coordinates through DeviceKit."
540 + description = "Tap the iOS simulator screen at the given coordinates through ControlKit."
541 )]
542 async fn simulator_tap(
543 &self,
@@ -550,12 +550,12 @@ macro_rules! xcrs_mcp_tools {
550 ::rmcp::model::CallToolResult,
551 ::rmcp::model::ErrorData,
552 > {
553 - let (simulator, devicekit) = Self::devicekit_from_target(
553 + let (simulator, controlkit) = Self::controlkit_from_target(
554 &args.simulator_name,
555 &args.simulator_udid,
556 - args.devicekit_port,
556 + args.controlkit_port,
557 )?;
558 - devicekit
558 + controlkit
559 .call(
560 "device.io.tap",
561 ::serde_json::json!({ "x": args.x, "y": args.y }),
@@ -574,7 +574,7 @@ macro_rules! xcrs_mcp_tools {
574
575 #[::rmcp::tool(
576 name = $simulator_text_name,
577 - description = "Type text into the focused iOS simulator field through DeviceKit."
577 + description = "Type text into the focused iOS simulator field through ControlKit."
578 )]
579 async fn simulator_text(
580 &self,
@@ -587,12 +587,12 @@ macro_rules! xcrs_mcp_tools {
587 ::rmcp::model::CallToolResult,
588 ::rmcp::model::ErrorData,
589 > {
590 - let (simulator, devicekit) = Self::devicekit_from_target(
590 + let (simulator, controlkit) = Self::controlkit_from_target(
591 &args.simulator_name,
592 &args.simulator_udid,
593 - args.devicekit_port,
593 + args.controlkit_port,
594 )?;
595 - devicekit
595 + controlkit
596 .call("device.io.text", ::serde_json::json!({ "text": args.text }))
597 .await
598 .map_err(|error| {
@@ -608,7 +608,7 @@ macro_rules! xcrs_mcp_tools {
608
609 #[::rmcp::tool(
610 name = $simulator_swipe_name,
611 - description = "Swipe between two screen coordinates on an iOS simulator through DeviceKit."
611 + description = "Swipe between two screen coordinates on an iOS simulator through ControlKit."
612 )]
613 async fn simulator_swipe(
614 &self,
@@ -621,12 +621,12 @@ macro_rules! xcrs_mcp_tools {
621 ::rmcp::model::CallToolResult,
622 ::rmcp::model::ErrorData,
623 > {
624 - let (simulator, devicekit) = Self::devicekit_from_target(
624 + let (simulator, controlkit) = Self::controlkit_from_target(
625 &args.simulator_name,
626 &args.simulator_udid,
627 - args.devicekit_port,
627 + args.controlkit_port,
628 )?;
629 - devicekit
629 + controlkit
630 .call(
631 "device.io.swipe",
632 ::serde_json::json!({
@@ -650,25 +650,25 @@ macro_rules! xcrs_mcp_tools {
650
651 #[::rmcp::tool(
652 name = $simulator_home_name,
653 - description = "Press the iOS simulator Home button through DeviceKit."
653 + description = "Press the iOS simulator Home button through ControlKit."
654 )]
655 async fn simulator_home(
656 &self,
657 ::rmcp::handler::server::wrapper::Parameters(
658 args,
659 ): ::rmcp::handler::server::wrapper::Parameters<
660 - $crate::mcp::SimulatorDeviceKitArgs,
660 + $crate::mcp::SimulatorControlKitArgs,
661 >,
662 ) -> ::std::result::Result<
663 ::rmcp::model::CallToolResult,
664 ::rmcp::model::ErrorData,
665 > {
666 - let (simulator, devicekit) = Self::devicekit_from_target(
666 + let (simulator, controlkit) = Self::controlkit_from_target(
667 &args.simulator_name,
668 &args.simulator_udid,
669 - args.devicekit_port,
669 + args.controlkit_port,
670 )?;
671 - devicekit
671 + controlkit
672 .call("device.io.button", ::serde_json::json!({ "button": "home" }))
673 .await
674 .map_err(|error| {
@@ -684,7 +684,7 @@ macro_rules! xcrs_mcp_tools {
684
685 #[::rmcp::tool(
686 name = $simulator_button_name,
687 - description = "Press a DeviceKit remote button on an iOS or tvOS simulator. tvOS supports up, down, left, right, select, menu, home, and playPause."
687 + description = "Press a ControlKit remote button on an iOS or tvOS simulator. tvOS supports up, down, left, right, select, menu, home, and playPause."
688 )]
689 async fn simulator_button(
690 &self,
@@ -697,10 +697,10 @@ macro_rules! xcrs_mcp_tools {
697 ::rmcp::model::CallToolResult,
698 ::rmcp::model::ErrorData,
699 > {
700 - let (simulator, devicekit) = Self::devicekit_from_target(
700 + let (simulator, controlkit) = Self::controlkit_from_target(
701 &args.simulator_name,
702 &args.simulator_udid,
703 - args.devicekit_port,
703 + args.controlkit_port,
704 )?;
705 let supported_buttons = [
706 "up",
@@ -718,7 +718,7 @@ macro_rules! xcrs_mcp_tools {
718 None,
719 ));
720 }
721 - devicekit
721 + controlkit
722 .call(
723 "device.io.button",
724 ::serde_json::json!({ "button": args.button }),
@@ -737,25 +737,25 @@ macro_rules! xcrs_mcp_tools {
737
738 #[::rmcp::tool(
739 name = $simulator_orientation_get_name,
740 - description = "Read the current iOS simulator orientation through DeviceKit."
740 + description = "Read the current iOS simulator orientation through ControlKit."
741 )]
742 async fn simulator_orientation_get(
743 &self,
744 ::rmcp::handler::server::wrapper::Parameters(
745 args,
746 ): ::rmcp::handler::server::wrapper::Parameters<
747 - $crate::mcp::SimulatorDeviceKitArgs,
747 + $crate::mcp::SimulatorControlKitArgs,
748 >,
749 ) -> ::std::result::Result<
750 ::rmcp::model::CallToolResult,
751 ::rmcp::model::ErrorData,
752 > {
753 - let (simulator, devicekit) = Self::devicekit_from_target(
753 + let (simulator, controlkit) = Self::controlkit_from_target(
754 &args.simulator_name,
755 &args.simulator_udid,
756 - args.devicekit_port,
756 + args.controlkit_port,
757 )?;
758 - let result = devicekit
758 + let result = controlkit
759 .call("device.io.orientation.get", ::serde_json::json!({}))
760 .await
761 .map_err(|error| {
@@ -771,7 +771,7 @@ macro_rules! xcrs_mcp_tools {
771
772 #[::rmcp::tool(
773 name = $simulator_orientation_set_name,
774 - description = "Set the iOS simulator orientation to PORTRAIT or LANDSCAPE through DeviceKit."
774 + description = "Set the iOS simulator orientation to PORTRAIT or LANDSCAPE through ControlKit."
775 )]
776 async fn simulator_orientation_set(
777 &self,
@@ -784,10 +784,10 @@ macro_rules! xcrs_mcp_tools {
784 ::rmcp::model::CallToolResult,
785 ::rmcp::model::ErrorData,
786 > {
787 - let (simulator, devicekit) = Self::devicekit_from_target(
787 + let (simulator, controlkit) = Self::controlkit_from_target(
788 &args.simulator_name,
789 &args.simulator_udid,
790 - args.devicekit_port,
790 + args.controlkit_port,
791 )?;
792 let orientation = args.orientation.to_uppercase();
793 if orientation != "PORTRAIT" && orientation != "LANDSCAPE" {
@@ -796,7 +796,7 @@ macro_rules! xcrs_mcp_tools {
796 None,
797 ));
798 }
799 - devicekit
799 + controlkit
800 .call(
801 "device.io.orientation.set",
802 ::serde_json::json!({ "orientation": orientation }),
crates/xcrs/src/xcrs.rs
+12 -12
@@ -16,13 +16,13 @@ pub fn encode_base64(data: impl AsRef<[u8]>) -> String {
16 STANDARD.encode(data)
17 }
18
19 -pub fn extract_devicekit_elements(root: &serde_json::Value) -> Vec<serde_json::Value> {
19 +pub fn extract_controlkit_elements(root: &serde_json::Value) -> Vec<serde_json::Value> {
20 let mut elements = Vec::new();
21 - collect_devicekit_elements(root, &mut elements);
21 + collect_controlkit_elements(root, &mut elements);
22 elements
23 }
24
25 -fn collect_devicekit_elements(element: &serde_json::Value, elements: &mut Vec<serde_json::Value>) {
25 +fn collect_controlkit_elements(element: &serde_json::Value, elements: &mut Vec<serde_json::Value>) {
26 let object = match element.as_object() {
27 Some(object) => object,
28 None => return,
@@ -71,24 +71,24 @@ fn collect_devicekit_elements(element: &serde_json::Value, elements: &mut Vec<se
71
72 if let Some(children) = object.get("children").and_then(serde_json::Value::as_array) {
73 for child in children {
74 - collect_devicekit_elements(child, elements);
74 + collect_controlkit_elements(child, elements);
75 }
76 }
77 }
78
79 #[derive(Debug, Clone)]
80 -pub struct DeviceKit {
80 +pub struct ControlKit {
81 client: reqwest::Client,
82 base_url: String,
83 }
84
85 -impl Default for DeviceKit {
85 +impl Default for ControlKit {
86 fn default() -> Self {
87 Self::new(12004)
88 }
89 }
90
91 -impl DeviceKit {
91 +impl ControlKit {
92 pub fn new(port: u16) -> Self {
93 Self {
94 client: reqwest::Client::new(),
@@ -109,25 +109,25 @@ impl DeviceKit {
109 }))
110 .send()
111 .await
112 - .with_context(|| format!("failed to connect to DeviceKit at {}", self.base_url))?;
112 + .with_context(|| format!("failed to connect to ControlKit at {}", self.base_url))?;
113
114 let status = response.status();
115 let body: serde_json::Value = response
116 .json()
117 .await
118 - .context("failed to decode DeviceKit JSON-RPC response")?;
118 + .context("failed to decode ControlKit JSON-RPC response")?;
119
120 if !status.is_success() {
121 - return Err(anyhow!("DeviceKit returned HTTP {}: {}", status, body));
121 + return Err(anyhow!("ControlKit returned HTTP {}: {}", status, body));
122 }
123
124 if let Some(error) = body.get("error") {
125 - return Err(anyhow!("DeviceKit method '{method}' failed: {error}"));
125 + return Err(anyhow!("ControlKit method '{method}' failed: {error}"));
126 }
127
128 body.get("result")
129 .cloned()
130 - .ok_or_else(|| anyhow!("DeviceKit response for '{method}' did not contain a result"))
130 + .ok_or_else(|| anyhow!("ControlKit response for '{method}' did not contain a result"))
131 }
132 }
133