@smbcloud / cli / commits / 383dcca

Rename Apple MCP tools to match agent tool-selection conventions

The Apple-platform MCP tools were named `smb_simulator_*` even though most drive the app through a ControlKit runner on a simulator *or* a physical device *or* macOS. That wrong scope signal, divergence from the verbs coding agents already know (XcodeBuildMCP's `describe_ui`/`tap`/`screenshot`), and a per-call `simulator_name`/`simulator_udid`/`host`/`controlkit_port` matrix with no selection step made Codex, Copilot, and Claude Code pick the wrong tools. - Rename host-agnostic actions to bare ecosystem verbs: `describe_ui`, `list_elements`, `tap`, `type_text`, `swipe`, `button`, `orientation_get/set`, `launch_app`, `terminate_app`, `capabilities`, `click` (macOS), `gesture` (visionOS). Keep truly simulator-only tools honest: `list_simulators`, `find_simulator`, `boot_and_install`, `open_url`. - Add `use_target` so a simulator or device is selected once and later actions need no target arguments; missing target fields fall back to it. - Consolidate near-duplicates: merge `device_screenshot` into a single auto-routing `screenshot`, fold `press_home` into `button`, and `watchos_tap` into `tap` (~25 tools down to 19). - Rewrite descriptions to be intent-first with honest host scope and use-when hints, dropping "ControlKit"/"simulator" jargon from the user-facing surface. - Update docs/mcp.md and docs/controlkit.md with the new names and a select-a-target-then-act workflow. Both the `smb_*` and standalone `xcrs_*` surfaces are renamed in lockstep. Release Notes: - Renamed the Apple-platform MCP tools to clearer, host-agnostic names (e.g. `smb_tap`, `smb_describe_ui`, `smb_screenshot`) and added `smb_use_target` to select a simulator or device once for subsequent actions. This is a breaking change for any scripts using the previous `smb_simulator_*` tool names. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Seto Elkahfi committed Jul 31, 2026 at 13:23 UTC 383dccab6417eb42225eae3e3ade4cf97af08ea8
4 files changed +317 -244
crates/cli/src/mcp_xcrs.rs
+19 -21
@@ -2,25 +2,23 @@ use super::SmbMcpServer;
2
3 xcrs::xcrs_mcp_tools!(
4 SmbMcpServer,
5 - "smb_simulator_list",
6 - "smb_simulator_find",
7 - "smb_ios_app_test",
8 - "smb_simulator_screenshot",
9 - "smb_simulator_launch_app",
10 - "smb_simulator_terminate_app",
11 - "smb_simulator_open_url",
12 - "smb_simulator_ui_dump",
13 - "smb_simulator_list_elements",
14 - "smb_simulator_tap",
15 - "smb_simulator_type_text",
16 - "smb_simulator_swipe",
17 - "smb_simulator_press_home",
18 - "smb_simulator_press_button",
19 - "smb_simulator_orientation_get",
20 - "smb_simulator_orientation_set",
21 - "smb_controlkit_capabilities",
22 - "smb_macos_click",
23 - "smb_visionos_spatial_tap",
24 - "smb_watchos_tap",
25 - "smb_device_screenshot"
5 + "smb_list_simulators",
6 + "smb_find_simulator",
7 + "smb_boot_and_install",
8 + "smb_screenshot",
9 + "smb_launch_app",
10 + "smb_terminate_app",
11 + "smb_open_url",
12 + "smb_describe_ui",
13 + "smb_list_elements",
14 + "smb_tap",
15 + "smb_type_text",
16 + "smb_swipe",
17 + "smb_button",
18 + "smb_orientation_get",
19 + "smb_orientation_set",
20 + "smb_capabilities",
21 + "smb_click",
22 + "smb_gesture",
23 + "smb_use_target"
24 );
crates/xcrs/src/mcp.rs
+242 -169
@@ -17,10 +17,49 @@ pub struct SimulatorFindArgs {
17 }
18
19 #[derive(Debug, Deserialize, JsonSchema)]
20 -pub struct DeviceScreenshotArgs {
21 - /// Physical device identifier known to CoreDevice: UDID, ECID, serial
22 - /// number, user-provided name, or DNS name.
23 - pub device: String,
20 +pub struct ScreenshotArgs {
21 + /// Simulator name of the target to capture. Omit to use the target selected
22 + /// with the use-target tool.
23 + #[serde(default)]
24 + pub simulator_name: Option<String>,
25 + /// Simulator UDID of the target to capture.
26 + #[serde(default)]
27 + pub simulator_udid: Option<String>,
28 + /// Physical device identifier known to CoreDevice (UDID, ECID, serial
29 + /// number, user-provided name, or DNS name). Provide this to capture a
30 + /// paired physical device instead of a simulator.
31 + #[serde(default)]
32 + pub device: Option<String>,
33 +}
34 +
35 +/// A target chosen once with the use-target tool and reused by later actions so
36 +/// they no longer need per-call target arguments.
37 +#[derive(Debug, Clone, Default, Deserialize, JsonSchema, serde::Serialize)]
38 +pub struct StoredTarget {
39 + #[serde(default)]
40 + pub simulator_name: Option<String>,
41 + #[serde(default)]
42 + pub simulator_udid: Option<String>,
43 + #[serde(default)]
44 + pub host: Option<String>,
45 + #[serde(default)]
46 + pub controlkit_port: Option<u16>,
47 +}
48 +
49 +#[derive(Debug, Deserialize, JsonSchema)]
50 +pub struct UseTargetArgs {
51 + /// Exact simulator name to remember as the active target.
52 + #[serde(default)]
53 + pub simulator_name: Option<String>,
54 + /// Simulator UDID to remember as the active target.
55 + #[serde(default)]
56 + pub simulator_udid: Option<String>,
57 + /// ControlKit host of a physical device or remote runner to remember.
58 + #[serde(default)]
59 + pub host: Option<String>,
60 + /// Local ControlKit JSON-RPC port to remember. Defaults to 12004.
61 + #[serde(default)]
62 + pub controlkit_port: Option<u16>,
63 }
64
65 #[derive(Debug, Deserialize, JsonSchema)]
@@ -261,23 +300,67 @@ macro_rules! xcrs_mcp_tools {
300 $simulator_tap_name:literal,
301 $simulator_text_name:literal,
302 $simulator_swipe_name:literal,
264 - $simulator_home_name:literal,
303 $simulator_button_name:literal,
304 $simulator_orientation_get_name:literal,
305 $simulator_orientation_set_name:literal,
306 $controlkit_capabilities_name:literal,
307 $macos_click_name:literal,
308 $visionos_spatial_tap_name:literal,
271 - $watchos_tap_name:literal,
272 - $device_screenshot_name:literal
309 + $use_target_name:literal
310 ) => {
311 #[::rmcp::tool_router(router = xcrs_tool_router, vis = "pub(crate)")]
312 impl $server {
313 + fn selected_target_slot(
314 + ) -> &'static ::std::sync::Mutex<Option<$crate::mcp::StoredTarget>> {
315 + static SLOT: ::std::sync::Mutex<Option<$crate::mcp::StoredTarget>> =
316 + ::std::sync::Mutex::new(None);
317 + &SLOT
318 + }
319 +
320 + fn stored_target() -> Option<$crate::mcp::StoredTarget> {
321 + Self::selected_target_slot()
322 + .lock()
323 + .ok()
324 + .and_then(|slot| slot.clone())
325 + }
326 +
327 + /// Fill missing target arguments from the target selected with the
328 + /// use-target tool so per-call target fields become optional.
329 + fn resolve_target_fields(
330 + simulator_name: &Option<String>,
331 + simulator_udid: &Option<String>,
332 + host: &Option<String>,
333 + controlkit_port: Option<u16>,
334 + ) -> (Option<String>, Option<String>, Option<String>, Option<u16>) {
335 + if simulator_name.is_none() && simulator_udid.is_none() && host.is_none() {
336 + if let Some(stored) = Self::stored_target() {
337 + return (
338 + stored.simulator_name,
339 + stored.simulator_udid,
340 + stored.host,
341 + controlkit_port.or(stored.controlkit_port),
342 + );
343 + }
344 + }
345 + (
346 + simulator_name.clone(),
347 + simulator_udid.clone(),
348 + host.clone(),
349 + controlkit_port,
350 + )
351 + }
352 +
353 fn simulator_from_target(
354 target: &$crate::mcp::SimulatorTargetArgs,
355 ) -> ::std::result::Result<$crate::Simulator, ::rmcp::model::ErrorData> {
356 let tools = $crate::XcodeCommandLineTools::new();
280 - match (&target.simulator_udid, &target.simulator_name) {
357 + let (simulator_name, simulator_udid, _host, _port) = Self::resolve_target_fields(
358 + &target.simulator_name,
359 + &target.simulator_udid,
360 + &None,
361 + target.controlkit_port,
362 + );
363 + match (&simulator_udid, &simulator_name) {
364 (Some(udid), _) => tools
365 .simctl()
366 .find_simulator_by_udid(udid)
@@ -291,7 +374,7 @@ macro_rules! xcrs_mcp_tools {
374 ::rmcp::model::ErrorData::invalid_request(error.to_string(), None)
375 }),
376 (None, None) => Err(::rmcp::model::ErrorData::invalid_request(
294 - "Provide either simulator_name or simulator_udid.",
377 + "No target. Pass simulator_name or simulator_udid, or select one first with the use-target tool.",
378 None,
379 )),
380 }
@@ -306,15 +389,22 @@ macro_rules! xcrs_mcp_tools {
389 (Option<$crate::Simulator>, $crate::ControlKit),
390 ::rmcp::model::ErrorData,
391 > {
392 + let (simulator_name, simulator_udid, host, controlkit_port) =
393 + Self::resolve_target_fields(
394 + simulator_name,
395 + simulator_udid,
396 + host,
397 + controlkit_port,
398 + );
399 if let Some(host) = host {
400 return Ok((
401 None,
312 - $crate::ControlKit::with_host(host, controlkit_port.unwrap_or(12004)),
402 + $crate::ControlKit::with_host(&host, controlkit_port.unwrap_or(12004)),
403 ));
404 }
405 let target = $crate::mcp::SimulatorTargetArgs {
316 - simulator_name: simulator_name.clone(),
317 - simulator_udid: simulator_udid.clone(),
406 + simulator_name,
407 + simulator_udid,
408 controlkit_port,
409 };
410 let simulator = Self::simulator_from_target(&target)?;
@@ -337,7 +427,14 @@ macro_rules! xcrs_mcp_tools {
427 ($crate::ControlKit, Option<$crate::Simulator>),
428 ::rmcp::model::ErrorData,
429 > {
340 - let simulator = match (&endpoint.simulator_udid, &endpoint.simulator_name) {
430 + let (simulator_name, simulator_udid, host, controlkit_port) =
431 + Self::resolve_target_fields(
432 + &endpoint.simulator_name,
433 + &endpoint.simulator_udid,
434 + &endpoint.host,
435 + endpoint.controlkit_port,
436 + );
437 + let simulator = match (&simulator_udid, &simulator_name) {
438 (Some(udid), _) => Some(
439 $crate::XcodeCommandLineTools::new()
440 .simctl()
@@ -362,11 +459,11 @@ macro_rules! xcrs_mcp_tools {
459 ),
460 (None, None) => None,
461 };
365 - let host = endpoint.host.as_deref().unwrap_or("127.0.0.1");
462 + let host = host.unwrap_or_else(|| "127.0.0.1".to_string());
463 Ok((
464 $crate::ControlKit::with_host(
368 - host,
369 - endpoint.controlkit_port.unwrap_or(12004),
465 + &host,
466 + controlkit_port.unwrap_or(12004),
467 ),
468 simulator,
469 ))
@@ -383,9 +480,55 @@ macro_rules! xcrs_mcp_tools {
480 }
481 }
482
483 + #[::rmcp::tool(
484 + name = $use_target_name,
485 + description = "Select the Apple target (simulator or physical/remote device) that later UI actions use by default, so you don't repeat target arguments on every call. Pass a simulator_name/simulator_udid for a simulator, or a host (plus optional controlkit_port) for a physical device or remote runner. Call this once after picking a target with the list-simulators tool; individual actions can still override it with their own target arguments."
486 + )]
487 + async fn use_target(
488 + &self,
489 + ::rmcp::handler::server::wrapper::Parameters(
490 + args,
491 + ): ::rmcp::handler::server::wrapper::Parameters<
492 + $crate::mcp::UseTargetArgs,
493 + >,
494 + ) -> ::std::result::Result<
495 + ::rmcp::model::CallToolResult,
496 + ::rmcp::model::ErrorData,
497 + > {
498 + if args.simulator_name.is_none()
499 + && args.simulator_udid.is_none()
500 + && args.host.is_none()
501 + {
502 + return Err(::rmcp::model::ErrorData::invalid_request(
503 + "Provide a simulator_name/simulator_udid or a host to select as the active target.",
504 + None,
505 + ));
506 + }
507 + let stored = $crate::mcp::StoredTarget {
508 + simulator_name: args.simulator_name,
509 + simulator_udid: args.simulator_udid,
510 + host: args.host,
511 + controlkit_port: args.controlkit_port,
512 + };
513 + match Self::selected_target_slot().lock() {
514 + Ok(mut slot) => *slot = Some(stored.clone()),
515 + Err(_) => {
516 + return Err(::rmcp::model::ErrorData::internal_error(
517 + "Failed to store the selected target.",
518 + None,
519 + ))
520 + }
521 + }
522 + Ok(::rmcp::model::CallToolResult::success(vec![
523 + ::rmcp::model::ContentBlock::json(&::serde_json::json!({
524 + "selected": stored,
525 + }))?,
526 + ]))
527 + }
528 +
529 #[::rmcp::tool(
530 name = $controlkit_capabilities_name,
388 - description = "Read the platform and supported capabilities from a ControlKit runner. Use a simulator name/UDID for iOS, tvOS, watchOS, or visionOS; omit them for a local macOS runner."
531 + description = "Report the platform and supported input capabilities of the active Apple target. Use this to discover whether the target supports taps, spatial taps, buttons, or orientation before driving it. Works on a simulator, a physical/remote device, or the local Mac."
532 )]
533 async fn controlkit_capabilities(
534 &self,
@@ -415,7 +558,7 @@ macro_rules! xcrs_mcp_tools {
558
559 #[::rmcp::tool(
560 name = $macos_click_name,
418 - description = "Click a macOS ControlKit runner at screen coordinates. Omit simulator_name and simulator_udid for the local Mac."
561 + description = "Click the running macOS app at screen coordinates. Use this for macOS targets; use the tap tool for iOS/tvOS/watchOS and the gesture tool for visionOS."
562 )]
563 async fn macos_click(
564 &self,
@@ -449,7 +592,7 @@ macro_rules! xcrs_mcp_tools {
592
593 #[::rmcp::tool(
594 name = $visionos_spatial_tap_name,
452 - description = "Perform a spatial tap on a visionOS ControlKit runner."
595 + description = "Perform a spatial tap on the running visionOS app at screen coordinates. Use this for visionOS targets; use the tap tool for iOS/tvOS/watchOS and the click tool for macOS."
596 )]
597 async fn visionos_spatial_tap(
598 &self,
@@ -481,43 +624,9 @@ macro_rules! xcrs_mcp_tools {
624 ]))
625 }
626
484 - #[::rmcp::tool(
485 - name = $watchos_tap_name,
486 - description = "Tap a watchOS ControlKit runner at screen coordinates."
487 - )]
488 - async fn watchos_tap(
489 - &self,
490 - ::rmcp::handler::server::wrapper::Parameters(
491 - args,
492 - ): ::rmcp::handler::server::wrapper::Parameters<
493 - $crate::mcp::ControlKitCoordinateArgs,
494 - >,
495 - ) -> ::std::result::Result<
496 - ::rmcp::model::CallToolResult,
497 - ::rmcp::model::ErrorData,
498 - > {
499 - let endpoint = Self::endpoint_from_coordinate(&args);
500 - let (controlkit, _) = Self::controlkit_from_endpoint(&endpoint)?;
501 - controlkit
502 - .call(
503 - "device.io.tap",
504 - ::serde_json::json!({ "x": args.x, "y": args.y }),
505 - )
506 - .await
507 - .map_err(|error| {
508 - ::rmcp::model::ErrorData::internal_error(error.to_string(), None)
509 - })?;
510 - Ok(::rmcp::model::CallToolResult::success(vec![
511 - ::rmcp::model::ContentBlock::text(format!(
512 - "Tapped ({}, {}).",
513 - args.x, args.y
514 - )),
515 - ]))
516 - }
517 -
627 #[::rmcp::tool(
628 name = $simulator_list_name,
520 - description = "List all Apple simulator devices known to Xcode, including their platform, runtime, UDID, availability, and current state."
629 + description = "List every Apple simulator known to Xcode with its platform, runtime, UDID, availability, and current state. Start here to pick a simulator, then remember it with the use-target tool."
630 )]
631 async fn simulator_list(
632 &self,
@@ -538,7 +647,7 @@ macro_rules! xcrs_mcp_tools {
647
648 #[::rmcp::tool(
649 name = $simulator_find_name,
541 - description = "Find an Apple simulator by its exact name and return its details as JSON."
650 + description = "Find an Apple simulator by its exact name and return its details as JSON. Use this to resolve a UDID before selecting it with the use-target tool."
651 )]
652 async fn simulator_find(
653 &self,
@@ -564,7 +673,7 @@ macro_rules! xcrs_mcp_tools {
673
674 #[::rmcp::tool(
675 name = $ios_app_test_name,
567 - description = "Boot an iOS simulator, install an .app bundle, optionally terminate the bundle, launch it, and return its app container path."
676 + description = "Boot an iOS simulator, install an .app bundle, optionally terminate it first, launch it, and return its app container path. Simulator-only end-to-end setup step; use launch-app/terminate-app for an already-installed app."
677 )]
678 async fn ios_app_test(
679 &self,
@@ -602,53 +711,40 @@ macro_rules! xcrs_mcp_tools {
711
712 #[::rmcp::tool(
713 name = $simulator_screenshot_name,
605 - description = "Capture a PNG screenshot from an Apple simulator."
714 + description = "Capture a PNG screenshot of the running app. Targets a simulator (via simulator_name/simulator_udid or the selected target) or a paired physical device (via the device identifier). Use describe-ui instead when you need element coordinates rather than pixels."
715 )]
716 async fn simulator_screenshot(
717 &self,
718 ::rmcp::handler::server::wrapper::Parameters(
719 args,
720 ): ::rmcp::handler::server::wrapper::Parameters<
612 - $crate::mcp::SimulatorTargetArgs,
721 + $crate::mcp::ScreenshotArgs,
722 >,
723 ) -> ::std::result::Result<
724 ::rmcp::model::CallToolResult,
725 ::rmcp::model::ErrorData,
726 > {
618 - let simulator = Self::simulator_from_target(&args)?;
619 - let screenshot = $crate::XcodeCommandLineTools::new()
620 - .simctl()
621 - .screenshot(&simulator.udid)
622 - .map_err(|error| {
623 - ::rmcp::model::ErrorData::internal_error(error.to_string(), None)
624 - })?;
625 - let encoded = $crate::encode_base64(screenshot);
626 - Ok(::rmcp::model::CallToolResult::success(vec![
627 - ::rmcp::model::ContentBlock::image(encoded, "image/png"),
628 - ]))
629 - }
630 -
631 - #[::rmcp::tool(
632 - name = $device_screenshot_name,
633 - description = "Capture a PNG screenshot from a physical iOS, tvOS, watchOS, or visionOS device paired with this Mac. Accepts a UDID, ECID, serial number, user-provided name, or DNS name."
634 - )]
635 - async fn device_screenshot(
636 - &self,
637 - ::rmcp::handler::server::wrapper::Parameters(
638 - args,
639 - ): ::rmcp::handler::server::wrapper::Parameters<
640 - $crate::mcp::DeviceScreenshotArgs,
641 - >,
642 - ) -> ::std::result::Result<
643 - ::rmcp::model::CallToolResult,
644 - ::rmcp::model::ErrorData,
645 - > {
646 - let screenshot = $crate::XcodeCommandLineTools::new()
647 - .devicectl()
648 - .screenshot(&args.device)
649 - .map_err(|error| {
650 - ::rmcp::model::ErrorData::internal_error(error.to_string(), None)
651 - })?;
727 + let screenshot = if let Some(device) = &args.device {
728 + $crate::XcodeCommandLineTools::new()
729 + .devicectl()
730 + .screenshot(device)
731 + .map_err(|error| {
732 + ::rmcp::model::ErrorData::internal_error(error.to_string(), None)
733 + })?
734 + } else {
735 + let target = $crate::mcp::SimulatorTargetArgs {
736 + simulator_name: args.simulator_name.clone(),
737 + simulator_udid: args.simulator_udid.clone(),
738 + controlkit_port: None,
739 + };
740 + let simulator = Self::simulator_from_target(&target)?;
741 + $crate::XcodeCommandLineTools::new()
742 + .simctl()
743 + .screenshot(&simulator.udid)
744 + .map_err(|error| {
745 + ::rmcp::model::ErrorData::internal_error(error.to_string(), None)
746 + })?
747 + };
748 let encoded = $crate::encode_base64(screenshot);
749 Ok(::rmcp::model::CallToolResult::success(vec![
750 ::rmcp::model::ContentBlock::image(encoded, "image/png"),
@@ -657,7 +753,7 @@ macro_rules! xcrs_mcp_tools {
753
754 #[::rmcp::tool(
755 name = $simulator_launch_app_name,
660 - description = "Launch an installed app on an Apple simulator by bundle identifier."
756 + description = "Launch an installed app by bundle identifier on the active target. Works on a simulator or a physical/remote device."
757 )]
758 async fn simulator_launch_app(
759 &self,
@@ -670,9 +766,16 @@ macro_rules! xcrs_mcp_tools {
766 ::rmcp::model::CallToolResult,
767 ::rmcp::model::ErrorData,
768 > {
673 - if let Some(host) = &args.host {
769 + let (simulator_name, simulator_udid, host, controlkit_port) =
770 + Self::resolve_target_fields(
771 + &args.simulator_name,
772 + &args.simulator_udid,
773 + &args.host,
774 + args.controlkit_port,
775 + );
776 + if let Some(host) = &host {
777 let controlkit =
675 - $crate::ControlKit::with_host(host, args.controlkit_port.unwrap_or(12004));
778 + $crate::ControlKit::with_host(host, controlkit_port.unwrap_or(12004));
779 controlkit
780 .call(
781 "device.apps.launch",
@@ -690,9 +793,9 @@ macro_rules! xcrs_mcp_tools {
793 ]));
794 }
795 let target = $crate::mcp::SimulatorTargetArgs {
693 - simulator_name: args.simulator_name,
694 - simulator_udid: args.simulator_udid,
695 - controlkit_port: args.controlkit_port,
796 + simulator_name,
797 + simulator_udid,
798 + controlkit_port,
799 };
800 let simulator = Self::simulator_from_target(&target)?;
801 $crate::XcodeCommandLineTools::new()
@@ -711,7 +814,7 @@ macro_rules! xcrs_mcp_tools {
814
815 #[::rmcp::tool(
816 name = $simulator_terminate_app_name,
714 - description = "Terminate an installed app on an Apple simulator by bundle identifier."
817 + description = "Terminate an installed app by bundle identifier on the active target. Works on a simulator or a physical/remote device."
818 )]
819 async fn simulator_terminate_app(
820 &self,
@@ -724,9 +827,16 @@ macro_rules! xcrs_mcp_tools {
827 ::rmcp::model::CallToolResult,
828 ::rmcp::model::ErrorData,
829 > {
727 - if let Some(host) = &args.host {
830 + let (simulator_name, simulator_udid, host, controlkit_port) =
831 + Self::resolve_target_fields(
832 + &args.simulator_name,
833 + &args.simulator_udid,
834 + &args.host,
835 + args.controlkit_port,
836 + );
837 + if let Some(host) = &host {
838 let controlkit =
729 - $crate::ControlKit::with_host(host, args.controlkit_port.unwrap_or(12004));
839 + $crate::ControlKit::with_host(host, controlkit_port.unwrap_or(12004));
840 controlkit
841 .call(
842 "device.apps.terminate",
@@ -744,9 +854,9 @@ macro_rules! xcrs_mcp_tools {
854 ]));
855 }
856 let target = $crate::mcp::SimulatorTargetArgs {
747 - simulator_name: args.simulator_name,
748 - simulator_udid: args.simulator_udid,
749 - controlkit_port: args.controlkit_port,
857 + simulator_name,
858 + simulator_udid,
859 + controlkit_port,
860 };
861 let simulator = Self::simulator_from_target(&target)?;
862 $crate::XcodeCommandLineTools::new()
@@ -765,7 +875,7 @@ macro_rules! xcrs_mcp_tools {
875
876 #[::rmcp::tool(
877 name = $simulator_open_url_name,
768 - description = "Open a URL or custom URL scheme on an Apple simulator."
878 + description = "Open a URL or custom URL scheme on the active simulator target (simulator only)."
879 )]
880 async fn simulator_open_url(
881 &self,
@@ -800,7 +910,7 @@ macro_rules! xcrs_mcp_tools {
910
911 #[::rmcp::tool(
912 name = $simulator_ui_dump_name,
803 - description = "Return the accessibility UI hierarchy from the foreground Apple app through ControlKit."
913 + description = "Return the full accessibility hierarchy of the foreground app as JSON. Use this first to see what is on screen before tapping or typing. Works on a simulator or a physical/remote device. Prefer list-elements when you only need actionable elements and their tap coordinates."
914 )]
915 async fn simulator_ui_dump(
916 &self,
@@ -835,7 +945,7 @@ macro_rules! xcrs_mcp_tools {
945
946 #[::rmcp::tool(
947 name = $simulator_list_elements_name,
838 - description = "List actionable accessibility elements and coordinates from the foreground Apple app through ControlKit."
948 + description = "List just the actionable accessibility elements of the foreground app with their labels and tap coordinates. Use this to decide where to tap. Works on a simulator or a physical/remote device. Use describe-ui when you need the full hierarchy."
949 )]
950 async fn simulator_list_elements(
951 &self,
@@ -871,7 +981,7 @@ macro_rules! xcrs_mcp_tools {
981
982 #[::rmcp::tool(
983 name = $simulator_tap_name,
874 - description = "Tap an Apple simulator screen at the given coordinates through ControlKit."
984 + description = "Tap the running app at screen coordinates. Use this for iOS, tvOS, and watchOS targets, on a simulator or a physical/remote device. Use the click tool for macOS and the gesture tool for visionOS. Read coordinates from list-elements or describe-ui first."
985 )]
986 async fn simulator_tap(
987 &self,
@@ -909,7 +1019,7 @@ macro_rules! xcrs_mcp_tools {
1019
1020 #[::rmcp::tool(
1021 name = $simulator_text_name,
912 - description = "Type text into the focused Apple simulator field through ControlKit."
1022 + description = "Type text into the focused field of the running app. Works on a simulator or a physical/remote device. Tap the field first to focus it."
1023 )]
1024 async fn simulator_text(
1025 &self,
@@ -944,7 +1054,7 @@ macro_rules! xcrs_mcp_tools {
1054
1055 #[::rmcp::tool(
1056 name = $simulator_swipe_name,
947 - description = "Swipe between two screen coordinates on an Apple simulator through ControlKit."
1057 + description = "Swipe between two screen coordinates in the running app, e.g. to scroll. Works on a simulator or a physical/remote device."
1058 )]
1059 async fn simulator_swipe(
1060 &self,
@@ -985,44 +1095,9 @@ macro_rules! xcrs_mcp_tools {
1095 ]))
1096 }
1097
988 - #[::rmcp::tool(
989 - name = $simulator_home_name,
990 - description = "Press the iOS Home button through ControlKit."
991 - )]
992 - async fn simulator_home(
993 - &self,
994 - ::rmcp::handler::server::wrapper::Parameters(
995 - args,
996 - ): ::rmcp::handler::server::wrapper::Parameters<
997 - $crate::mcp::SimulatorControlKitArgs,
998 - >,
999 - ) -> ::std::result::Result<
1000 - ::rmcp::model::CallToolResult,
1001 - ::rmcp::model::ErrorData,
1002 - > {
1003 - let (simulator, controlkit) = Self::controlkit_from_target(
1004 - &args.simulator_name,
1005 - &args.simulator_udid,
1006 - &args.host,
1007 - args.controlkit_port,
1008 - )?;
1009 - controlkit
1010 - .call("device.io.button", ::serde_json::json!({ "button": "home" }))
1011 - .await
1012 - .map_err(|error| {
1013 - ::rmcp::model::ErrorData::internal_error(error.to_string(), None)
1014 - })?;
1015 - Ok(::rmcp::model::CallToolResult::success(vec![
1016 - ::rmcp::model::ContentBlock::text(format!(
1017 - "Pressed Home on {}.",
1018 - Self::target_label(&simulator, &args.host)
1019 - )),
1020 - ]))
1021 - }
1022 -
1098 #[::rmcp::tool(
1099 name = $simulator_button_name,
1025 - description = "Press a ControlKit remote button on an iOS or tvOS simulator. tvOS supports up, down, left, right, select, menu, home, and playPause."
1100 + description = "Press a hardware or remote button on the running target: home (iOS), or up, down, left, right, select, menu, playPause (tvOS remote). Works on a simulator or a physical/remote device. Use this to press Home instead of a dedicated tool."
1101 )]
1102 async fn simulator_button(
1103 &self,
@@ -1076,7 +1151,7 @@ macro_rules! xcrs_mcp_tools {
1151
1152 #[::rmcp::tool(
1153 name = $simulator_orientation_get_name,
1079 - description = "Read the current iOS simulator orientation through ControlKit."
1154 + description = "Read the current screen orientation of the running target. Works on a simulator or a physical/remote device."
1155 )]
1156 async fn simulator_orientation_get(
1157 &self,
@@ -1111,7 +1186,7 @@ macro_rules! xcrs_mcp_tools {
1186
1187 #[::rmcp::tool(
1188 name = $simulator_orientation_set_name,
1114 - description = "Set the iOS simulator orientation to PORTRAIT or LANDSCAPE through ControlKit."
1189 + description = "Set the screen orientation of the running target to PORTRAIT or LANDSCAPE. Works on a simulator or a physical/remote device."
1190 )]
1191 async fn simulator_orientation_set(
1192 &self,
@@ -1159,27 +1234,25 @@ macro_rules! xcrs_mcp_tools {
1234
1235 xcrs_mcp_tools!(
1236 XcrsMcpServer,
1162 - "xcrs_simulator_list",
1163 - "xcrs_simulator_find",
1164 - "xcrs_ios_app_test",
1165 - "xcrs_simulator_screenshot",
1166 - "xcrs_simulator_launch_app",
1167 - "xcrs_simulator_terminate_app",
1168 - "xcrs_simulator_open_url",
1169 - "xcrs_simulator_ui_dump",
1170 - "xcrs_simulator_list_elements",
1171 - "xcrs_simulator_tap",
1172 - "xcrs_simulator_type_text",
1173 - "xcrs_simulator_swipe",
1174 - "xcrs_simulator_press_home",
1175 - "xcrs_simulator_press_button",
1176 - "xcrs_simulator_orientation_get",
1177 - "xcrs_simulator_orientation_set",
1178 - "xcrs_controlkit_capabilities",
1179 - "xcrs_macos_click",
1180 - "xcrs_visionos_spatial_tap",
1181 - "xcrs_watchos_tap",
1182 - "xcrs_device_screenshot"
1237 + "xcrs_list_simulators",
1238 + "xcrs_find_simulator",
1239 + "xcrs_boot_and_install",
1240 + "xcrs_screenshot",
1241 + "xcrs_launch_app",
1242 + "xcrs_terminate_app",
1243 + "xcrs_open_url",
1244 + "xcrs_describe_ui",
1245 + "xcrs_list_elements",
1246 + "xcrs_tap",
1247 + "xcrs_type_text",
1248 + "xcrs_swipe",
1249 + "xcrs_button",
1250 + "xcrs_orientation_get",
1251 + "xcrs_orientation_set",
1252 + "xcrs_capabilities",
1253 + "xcrs_click",
1254 + "xcrs_gesture",
1255 + "xcrs_use_target"
1256 );
1257
1258 #[rmcp::tool_handler(router = Self::xcrs_tool_router())]
docs/controlkit.md
+15 -14
@@ -99,23 +99,24 @@ smb --mcp
99
100 The runner tools accept either `simulator_name` or `simulator_udid` for a
101 simulator, or `host` (plus `controlkit_port`, defaulting to `12004`) for a
102 -physical device or any other remote runner. The standalone `xcrs --mcp`
103 -server exposes the same tools with the `xcrs_` prefix.
102 +physical device or any other remote runner. Call `smb_use_target` once to
103 +remember the target so later tools need no target arguments. The standalone
104 +`xcrs --mcp` server exposes the same tools with the `xcrs_` prefix.
105
106 | Tool | Runner | Purpose |
107 | --- | --- | --- |
107 -| `smb_controlkit_capabilities` | All platforms | Read platform and capability information. |
108 -| `smb_macos_click` | macOS | Click at screen coordinates. |
109 -| `smb_visionos_spatial_tap` | visionOS | Perform a spatial tap. |
110 -| `smb_watchos_tap` | watchOS | Perform a touch tap. |
111 -| `smb_simulator_tap` | iOS / tvOS, simulator or physical | Perform a touch tap. |
112 -| `smb_simulator_type_text` | iOS / tvOS, simulator or physical | Type into the focused field. |
113 -| `smb_simulator_swipe` | iOS / tvOS, simulator or physical | Swipe between coordinates. |
114 -| `smb_simulator_press_button` | iOS / tvOS, simulator or physical | Press a Home or tvOS remote button. |
115 -| `smb_simulator_launch_app`/`terminate_app` | 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`. |
116 -| `smb_simulator_screenshot` | Simulator only | Capture a PNG screenshot via `simctl`. |
117 -| `smb_device_screenshot` | Physical device only | Capture a PNG screenshot via `devicectl device capture screenshot` — works independently of the ControlKit runner, so it succeeds even if the RPC server isn't running. |
118 -| `smb_simulator_ui_dump` | All UI-test runners | Read the accessibility hierarchy. |
108 +| `smb_use_target` | All platforms | Remember the active simulator or device for later tools. |
109 +| `smb_capabilities` | All platforms | Read platform and capability information. |
110 +| `smb_click` | macOS | Click at screen coordinates. |
111 +| `smb_gesture` | visionOS | Perform a spatial tap. |
112 +| `smb_tap` | iOS / tvOS / watchOS, simulator or physical | Perform a touch tap. |
113 +| `smb_type_text` | iOS / tvOS, simulator or physical | Type into the focused field. |
114 +| `smb_swipe` | iOS / tvOS, simulator or physical | Swipe between coordinates. |
115 +| `smb_button` | iOS / tvOS, simulator or physical | Press a Home or tvOS remote button. |
116 +| `smb_launch_app`/`terminate_app` | 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`. |
117 +| `smb_screenshot` | Simulator or physical | Capture a PNG screenshot. Uses `simctl` for a simulator, or `devicectl device capture screenshot` when a `device` identifier is given — the device path works even if the ControlKit runner isn't running. |
118 +| `smb_describe_ui` | All UI-test runners | Read the accessibility hierarchy. |
119 +| `smb_list_elements` | All UI-test runners | Read only actionable elements with tap coordinates. |
120
121 For a local macOS runner, omit simulator fields and pass `host` and
122 `controlkit_port` when needed:
docs/mcp.md
+41 -40
@@ -251,49 +251,50 @@ users; it belongs to a project.
251 | `auth_app_update` | `id`, `name`/`support_email` (at least one) | The updated Auth app. |
252 | `auth_app_delete` | `id` | Confirmation. **Destructive and irreversible.** |
253
254 -### ControlKit runners
255 -
256 -The `smb` MCP server also exposes local Xcode simulator and ControlKit runner
257 -tools. Simulator arguments accept either `simulator_name` or `simulator_udid`;
258 -ControlKit arguments default to `127.0.0.1:12004`. For a local macOS runner,
259 -omit the simulator fields and use the optional `host` and `controlkit_port`
260 -fields.
261 -
262 -The same `host` field also targets a **physical** iOS/tvOS/watchOS/visionOS
263 -device running a ControlKit runner reachable over the network (see
264 -[ControlKit runners](./controlkit.md)) — omit `simulator_name`/`simulator_udid`
265 -and pass the device's host and the runner's listen port instead. This applies
266 -to `smb_simulator_tap`, `smb_simulator_type_text`, `smb_simulator_swipe`,
267 -`smb_simulator_press_button`, `smb_simulator_press_home`,
268 -`smb_simulator_orientation_get`/`set`, `smb_simulator_ui_dump`,
269 -`smb_simulator_list_elements`, and `smb_simulator_launch_app`/`terminate_app`
270 -(the latter two route through the runner's `device.apps.launch`/`terminate`
271 -RPC methods instead of `simctl` when `host` is given).
254 +### Apple platform automation
255 +
256 +The `smb` MCP server also exposes tools for driving Apple apps under test. Most
257 +of the action tools are **host-agnostic**: the same tool works against an Xcode
258 +simulator *or* a physical iOS/tvOS/watchOS/visionOS device *or* the local Mac,
259 +because they talk to an on-target [ControlKit runner](./controlkit.md).
260 +
261 +**Recommended flow — select a target, then act:**
262 +
263 +1. `smb_list_simulators` (or `smb_find_simulator`) to pick a simulator, or note
264 + a device's `host`.
265 +2. `smb_use_target` once with the chosen `simulator_name`/`simulator_udid` (a
266 + simulator) or `host` + optional `controlkit_port` (a device/remote runner).
267 + The server remembers it, so later action tools need **no** target arguments.
268 +3. `smb_describe_ui` / `smb_list_elements` to see the screen, then
269 + `smb_tap` / `smb_type_text` / `smb_swipe` / `smb_button` and
270 + `smb_screenshot` to drive and verify it.
271 +
272 +Any action tool can still override the selected target inline with its own
273 +`simulator_name`/`simulator_udid` or `host` fields. Simulator arguments accept
274 +either a name or a UDID; ControlKit arguments default to `127.0.0.1:12004`.
275
276 | Tool | Arguments | Returns |
277 | --- | --- | --- |
275 -| `smb_simulator_list` | _(none)_ | All Apple simulator devices known to Xcode. |
276 -| `smb_simulator_find` | `name` | A simulator's details. |
277 -| `smb_controlkit_capabilities` | simulator target or optional `host`, `controlkit_port` | Runner platform and capabilities. |
278 -| `smb_macos_click` | `x`, `y`, plus optional endpoint fields | A pointer click on a macOS runner. |
279 -| `smb_visionos_spatial_tap` | `x`, `y`, plus optional endpoint fields | A spatial tap on a visionOS runner. |
280 -| `smb_watchos_tap` | `x`, `y`, plus optional endpoint fields | A touch tap on a watchOS runner. |
281 -| `smb_simulator_tap` | simulator target or `host`, `x`, `y` | A touch tap through ControlKit. |
282 -| `smb_simulator_type_text` | simulator target or `host`, `text` | Text input through ControlKit. |
283 -| `smb_simulator_swipe` | simulator target or `host`, `x1`, `y1`, `x2`, `y2` | A swipe through ControlKit. |
284 -| `smb_simulator_press_button` | simulator target or `host`, `button` | A supported remote-button press. |
285 -| `smb_simulator_press_home` | simulator target or `host` | An iOS Home-button press. |
286 -| `smb_simulator_orientation_get`/`set` | simulator target or `host`; `orientation` for `set` | Current or updated iOS orientation. |
287 -| `smb_simulator_launch_app`/`terminate_app` | simulator target or `host`, `bundle_id` | App lifecycle confirmation. |
288 -| `smb_simulator_open_url` | simulator target, `url` | URL-open confirmation. |
289 -| `smb_simulator_ui_dump` | simulator target or `host` | Accessibility UI hierarchy. |
290 -| `smb_simulator_list_elements` | simulator target or `host` | Actionable accessibility elements. |
291 -| `smb_simulator_screenshot` | simulator target | A PNG image. |
292 -| `smb_device_screenshot` | `device` (UDID, ECID, serial number, name, or DNS name) | A PNG image captured from a physical device via `devicectl`. |
293 -| `smb_ios_app_test` | simulator target, `app_path`, `bundle_id` | Installed/launched app details. |
294 -
295 -The standalone `xcrs --mcp` server exposes the same runner surface with the
296 -`xcrs_` prefix instead of `smb_`.
278 +| `smb_use_target` | `simulator_name`/`simulator_udid`, or `host` + optional `controlkit_port` | The remembered active target. |
279 +| `smb_list_simulators` | _(none)_ | All Apple simulator devices known to Xcode. |
280 +| `smb_find_simulator` | `name` | A simulator's details (resolve a UDID). |
281 +| `smb_capabilities` | optional target | Target platform and supported input capabilities. |
282 +| `smb_describe_ui` | optional target | Full accessibility hierarchy of the foreground app. |
283 +| `smb_list_elements` | optional target | Actionable accessibility elements with tap coordinates. |
284 +| `smb_tap` | optional target, `x`, `y` | A touch tap (iOS/tvOS/watchOS). |
285 +| `smb_click` | optional endpoint, `x`, `y` | A pointer click (macOS). |
286 +| `smb_gesture` | optional endpoint, `x`, `y` | A spatial tap (visionOS). |
287 +| `smb_type_text` | optional target, `text` | Text typed into the focused field. |
288 +| `smb_swipe` | optional target, `x1`, `y1`, `x2`, `y2` | A swipe. |
289 +| `smb_button` | optional target, `button` | A hardware/remote button press (e.g. `home`). |
290 +| `smb_orientation_get`/`set` | optional target; `orientation` for `set` | Current or updated screen orientation. |
291 +| `smb_launch_app`/`terminate_app` | optional target, `bundle_id` | App lifecycle confirmation (routes through the runner's `device.apps.launch`/`terminate` RPC on a device, `simctl` on a simulator). |
292 +| `smb_open_url` | simulator target, `url` | URL-open confirmation (simulator only). |
293 +| `smb_screenshot` | simulator target, or `device` (UDID/ECID/serial/name/DNS) | A PNG image (`simctl` for a simulator, `devicectl` for a physical device). |
294 +| `smb_boot_and_install` | simulator target, `app_path`, `bundle_id` | Boots a simulator, installs and launches an app (simulator only). |
295 +
296 +The standalone `xcrs --mcp` server exposes the same surface with the `xcrs_`
297 +prefix instead of `smb_`.
298
299 ## Safety: tools run without confirmation
300