5
//! `device_capabilities`, `app_install_launch`, `screen_capture`, `app_launch`,
6
//! `app_terminate`, `url_open`, `ui_describe`, `ui_element_list`, `input_tap`,
7
//! `input_text`, `input_swipe`, `input_button`, `input_click`, `input_spatial_tap`,
8
-//! `orientation_get`, and `orientation_set`. Each name is supplied by the embedder
8
+//! `orientation_get`, `orientation_set`, `posture_get`, and `posture_set`. Each name is supplied by the embedder
9
//! through [`xcrs_mcp_tools`] so the standalone crate and CLI profile share one
10
//! implementation and one public contract.
11
//!
12
+//! `screen_capture` takes a `display` for foldable simulators such as iPhone Duo,
13
+//! which have an outer and an inner display; `posture_get` and `posture_set` read
14
+//! and change the fold state.
15
+//!
16
//! Targets are Apple simulators, Apple ControlKit hosts (physical/remote devices or
17
//! a local macOS runner), or Android devices reachable through adb. A target can be
18
//! selected once with `device_select` and reused by later calls, or passed explicitly
340
}
341
}
342
343
+/// The fold state `posture_set` can request on a foldable simulator.
344
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, JsonSchema)]
345
+#[serde(rename_all = "lowercase")]
346
+pub enum Posture {
347
+ /// Hinge closed: the outer display is live.
348
+ Folded,
349
+ /// Hinge fully open: the inner display is live.
350
+ Unfolded,
351
+}
352
+
353
+impl Posture {
354
+ pub fn as_str(self) -> &'static str {
355
+ match self {
356
+ Self::Folded => "folded",
357
+ Self::Unfolded => "unfolded",
358
+ }
359
+ }
360
+
361
+ /// The Device Hub hinge button that sets this posture.
362
+ pub fn hinge_button(self) -> &'static str {
363
+ match self {
364
+ Self::Folded => "closed",
365
+ Self::Unfolded => "open",
366
+ }
367
+ }
368
+
369
+ /// Whether a runner's `device.hinge.get` status means this posture.
370
+ pub fn matches_hinge_status(self, status: &str) -> bool {
371
+ status
372
+ == match self {
373
+ Self::Folded => "closed",
374
+ Self::Unfolded => "fullyOpen",
375
+ }
376
+ }
377
+}
378
+
379
+/// The display that shows content for a runner hinge status: the outer display
380
+/// when the hinge is closed, the inner display otherwise. `None` when the status
381
+/// is unknown or the device does not have an outer and an inner display.
382
+pub fn live_display<'a>(
383
+ hinge_status: &str,
384
+ displays: &'a [crate::SimDisplay],
385
+) -> Option<&'a crate::SimDisplay> {
386
+ let role = match hinge_status {
387
+ "closed" => "outer",
388
+ "partiallyOpen" | "fullyOpen" => "inner",
389
+ _ => return None,
390
+ };
391
+ displays.iter().find(|display| display.role == role)
392
+}
393
+
394
+#[derive(Debug, Deserialize, JsonSchema)]
395
+pub struct PostureGetArgs {
396
+ /// Exact Apple simulator name. Omit to use the target selected with
397
+ /// `device_select`.
398
+ #[serde(default)]
399
+ pub simulator_name: Option<String>,
400
+ /// Apple simulator UDID. Omit to use the target selected with `device_select`.
401
+ #[serde(default)]
402
+ pub simulator_udid: Option<String>,
403
+ /// ControlKit host of a physical device or remote runner. Omit for a local
404
+ /// simulator.
405
+ #[serde(default)]
406
+ pub host: Option<String>,
407
+ /// Local ControlKit JSON-RPC port. Defaults to 12004.
408
+ #[serde(default)]
409
+ pub controlkit_port: Option<u16>,
410
+}
411
+
412
+#[derive(Debug, Deserialize, JsonSchema)]
413
+pub struct PostureSetArgs {
414
+ /// Exact Apple simulator name. Omit to use the target selected with
415
+ /// `device_select`.
416
+ #[serde(default)]
417
+ pub simulator_name: Option<String>,
418
+ /// Apple simulator UDID. Omit to use the target selected with `device_select`.
419
+ #[serde(default)]
420
+ pub simulator_udid: Option<String>,
421
+ /// Local ControlKit JSON-RPC port. Defaults to 12004.
422
+ #[serde(default)]
423
+ pub controlkit_port: Option<u16>,
424
+ /// `folded` closes the hinge so the outer display is live; `unfolded` opens it
425
+ /// fully so the inner display is live.
426
+ pub posture: Posture,
427
+}
428
+
429
/// Static, hand-maintained capability description for Android targets. Unlike
430
/// Apple's `device.capabilities` ControlKit call, adb has no capability-discovery
431
/// RPC, so this mirrors exactly the actions `AndroidDebugBridge` implements.
559
/// field.
560
#[serde(default)]
561
pub device: Option<String>,
562
+ /// Which display of an Apple simulator to capture: `outer`, `inner`, `all`, or
563
+ /// a screen ID from `device_capabilities`. Omit it to capture every display of
564
+ /// a foldable device (such as iPhone Duo), or the only display of any other.
565
+ /// Only valid for Apple simulators.
566
+ #[serde(default)]
567
+ pub display: Option<String>,
568
/// Android device serial from `adb devices -l`.
569
#[serde(default)]
570
pub android_serial: Option<String>,
571
}
572
573
+/// Capture the requested displays of a simulator as MCP content. A selection of
574
+/// more than one display yields a text label followed by an image per display;
575
+/// a single display yields just the image, as it always has.
576
+pub fn capture_simulator(
577
+ udid: &str,
578
+ display: Option<&str>,
579
+) -> Result<Vec<rmcp::model::ContentBlock>> {
580
+ let tools = crate::XcodeCommandLineTools::new();
581
+ let simctl = tools.simctl();
582
+
583
+ let displays = match simctl.displays(udid) {
584
+ Ok(displays) => displays,
585
+ // Without display metadata only the primary display can be captured.
586
+ Err(_) if display.is_none() => {
587
+ let screenshot = simctl.screenshot(udid, None)?;
588
+ return Ok(vec![rmcp::model::ContentBlock::image(
589
+ crate::encode_base64(screenshot),
590
+ "image/png",
591
+ )]);
592
+ }
593
+ Err(error) => return Err(error),
594
+ };
595
+
596
+ let selected = crate::select_displays(&displays, display)?;
597
+ let labelled = selected.len() > 1;
598
+ let mut content = Vec::new();
599
+ for entry in selected {
600
+ let screenshot = simctl.screenshot(udid, Some(&entry.screen_id.to_string()))?;
601
+ if labelled {
602
+ content.push(rmcp::model::ContentBlock::text(entry.describe()));
603
+ }
604
+ content.push(rmcp::model::ContentBlock::image(
605
+ crate::encode_base64(screenshot),
606
+ "image/png",
607
+ ));
608
+ }
609
+ Ok(content)
610
+}
611
+
612
#[derive(Debug, Deserialize, JsonSchema)]
613
pub struct AppTargetArgs {
614
/// Exact Apple simulator name. Omit to use the target selected with
884
$input_click_name:literal,
885
$input_spatial_tap_name:literal,
886
$orientation_get_name:literal,
752
- $orientation_set_name:literal
887
+ $orientation_set_name:literal,
888
+ $posture_get_name:literal,
889
+ $posture_set_name:literal
890
) => {
891
#[::rmcp::tool_router(router = xcrs_tool_router, vis = "pub(crate)")]
892
impl $server {
1222
.map_err(|error| {
1223
::rmcp::model::ErrorData::internal_error(error.to_string(), None)
1224
})?;
1225
+ let displays = simulator.as_ref().and_then(|simulator| {
1226
+ $crate::XcodeCommandLineTools::new()
1227
+ .simctl()
1228
+ .displays(&simulator.udid)
1229
+ .ok()
1230
+ });
1231
+ let mut payload = ::serde_json::json!({
1232
+ "platform": "apple",
1233
+ "simulator": simulator,
1234
+ "capabilities": result,
1235
+ });
1236
+ if let (Some(displays), Some(object)) = (displays, payload.as_object_mut()) {
1237
+ object.insert("foldable".to_string(), ::serde_json::json!(displays.len() > 1));
1238
+ object.insert("displays".to_string(), ::serde_json::json!(displays));
1239
+ }
1240
Ok(::rmcp::model::CallToolResult::success(vec![
1089
- ::rmcp::model::ContentBlock::json(&::serde_json::json!({
1090
- "platform": "apple",
1091
- "simulator": simulator,
1092
- "capabilities": result,
1093
- }))?,
1241
+ ::rmcp::model::ContentBlock::json(&payload)?,
1242
]))
1243
}
1244
}
1288
name = $screen_capture_name,
1289
title = "Capture screenshot",
1290
annotations(title = "Capture screenshot", read_only_hint = true, idempotent_hint = true),
1143
- description = "Purpose: capture a PNG screenshot of the resolved target's current screen. When to use vs siblings: use this for a pixel image; use ui_describe or ui_element_list instead when you need element coordinates or labels rather than pixels. Behavior: for an Apple simulator, captures through `simctl io screenshot`; for a paired physical Apple device, pass its CoreDevice identifier as `device` to capture through `devicectl` (this bypasses simulator/host/android_serial resolution entirely and is mutually exclusive with them); for an Android device, captures through `adb exec-out screencap`. Prerequisites: the target must be booted/connected and, for simulators, `simctl` must be able to reach it. Failure modes: returns an error if `device` is combined with any other target field, if the resolved target is an Apple ControlKit host (screenshot over ControlKit is not implemented; use `device` or select a simulator instead), or if the underlying capture command fails."
1291
+ description = "Purpose: capture a PNG screenshot of the resolved target's current screen. When to use vs siblings: use this for a pixel image; use ui_describe or ui_element_list instead when you need element coordinates or labels rather than pixels. Behavior: for an Apple simulator, captures through `simctl io screenshot`; a foldable simulator such as iPhone Duo has two displays (outer and inner), and only the one currently in use shows content, so by default every display is captured, each preceded by a text block naming it (for example `screen 1 outer 1398x2034`), and `display` (`outer`, `inner`, `all` or a screen ID) narrows that; for a paired physical Apple device, pass its CoreDevice identifier as `device` to capture through `devicectl` (this bypasses simulator/host/android_serial resolution entirely and is mutually exclusive with them); for an Android device, captures through `adb exec-out screencap`. Prerequisites: the target must be booted/connected and, for simulators, `simctl` must be able to reach it. Failure modes: returns an error if `device` is combined with any other target field, if `display` is given for a physical device or an Android device or names a display the simulator does not have, if the resolved target is an Apple ControlKit host (screenshot over ControlKit is not implemented; use `device` or select a simulator instead), or if the underlying capture command fails."
1292
)]
1293
async fn screen_capture(
1294
&self,
1302
::rmcp::model::ErrorData,
1303
> {
1304
if let Some(device) = &args.device {
1305
+ if args.display.is_some() {
1306
+ return Err(::rmcp::model::ErrorData::invalid_request(
1307
+ "display only applies to Apple simulators, not to a physical device.",
1308
+ None,
1309
+ ));
1310
+ }
1311
if args.simulator_name.is_some()
1312
|| args.simulator_udid.is_some()
1313
|| args.host.is_some()
1339
args.controlkit_port,
1340
args.android_serial,
1341
)?;
1188
- let screenshot = match &target {
1342
+ let content = match &target {
1343
$crate::mcp::SelectedTarget::Android { serial } => {
1344
+ if args.display.is_some() {
1345
+ return Err(::rmcp::model::ErrorData::invalid_request(
1346
+ "display only applies to Apple simulators, not to Android devices.",
1347
+ None,
1348
+ ));
1349
+ }
1350
let device = Self::android_device_for(serial.clone()).await?;
1351
let device_serial = device.serial.clone();
1192
- Self::run_android_blocking(move || {
1352
+ let screenshot = Self::run_android_blocking(move || {
1353
$crate::AndroidDebugBridge::new().screenshot(&device_serial)
1354
})
1355
.await
1356
.map_err(|error| {
1357
::rmcp::model::ErrorData::internal_error(error.to_string(), None)
1198
- })?
1358
+ })?;
1359
+ vec![::rmcp::model::ContentBlock::image(
1360
+ $crate::encode_base64(screenshot),
1361
+ "image/png",
1362
+ )]
1363
}
1364
$crate::mcp::SelectedTarget::AppleSimulator {
1365
simulator_name,
1367
..
1368
} => {
1369
let simulator = Self::simulator_for(simulator_name, simulator_udid)?;
1206
- $crate::XcodeCommandLineTools::new()
1207
- .simctl()
1208
- .screenshot(&simulator.udid)
1370
+ $crate::mcp::capture_simulator(&simulator.udid, args.display.as_deref())
1371
.map_err(|error| {
1372
::rmcp::model::ErrorData::internal_error(error.to_string(), None)
1373
})?
1381
));
1382
}
1383
};
1222
- Ok(::rmcp::model::CallToolResult::success(vec![
1223
- ::rmcp::model::ContentBlock::image(
1224
- $crate::encode_base64(screenshot),
1225
- "image/png",
1226
- ),
1227
- ]))
1384
+ Ok(::rmcp::model::CallToolResult::success(content))
1385
}
1386
1387
#[::rmcp::tool(
2192
)),
2193
]))
2194
}
2195
+
2196
+ #[::rmcp::tool(
2197
+ name = $posture_get_name,
2198
+ title = "Get fold posture",
2199
+ annotations(title = "Get fold posture", read_only_hint = true, idempotent_hint = true),
2200
+ description = "Purpose: report whether a foldable simulator (such as iPhone Duo) is folded or unfolded, and which display is live. When to use vs siblings: use this before screen_capture or ui_element_list on a foldable to know which display shows content; use posture_set to change it. Behavior: calls the runner's ControlKit `device.hinge.get` and returns its status (`closed`, `partiallyOpen`, `fullyOpen`, or `unsupported` on a device without a hinge) and angle in radians (0 closed, pi fully open), plus `live_display`: the outer display when closed, the inner display otherwise. Prerequisites: a reachable ControlKit runner built with hinge support, running on iOS 27.1 or later. Failure modes: errors if no target can be resolved, if the target is Android, or if the runner is unreachable or outdated."
2201
+ )]
2202
+ async fn posture_get(
2203
+ &self,
2204
+ ::rmcp::handler::server::wrapper::Parameters(
2205
+ args,
2206
+ ): ::rmcp::handler::server::wrapper::Parameters<
2207
+ $crate::mcp::PostureGetArgs,
2208
+ >,
2209
+ ) -> ::std::result::Result<
2210
+ ::rmcp::model::CallToolResult,
2211
+ ::rmcp::model::ErrorData,
2212
+ > {
2213
+ let target = Self::dispatch_apple_target(
2214
+ $posture_get_name,
2215
+ args.simulator_name,
2216
+ args.simulator_udid,
2217
+ args.host,
2218
+ args.controlkit_port,
2219
+ )?;
2220
+ let (simulator, controlkit) = Self::controlkit_for_target(&target)?;
2221
+ let hinge = controlkit
2222
+ .call("device.hinge.get", ::serde_json::json!({}))
2223
+ .await
2224
+ .map_err(|error| {
2225
+ ::rmcp::model::ErrorData::internal_error(error.to_string(), None)
2226
+ })?;
2227
+ let status = hinge
2228
+ .get("status")
2229
+ .and_then(::serde_json::Value::as_str)
2230
+ .unwrap_or("unsupported");
2231
+ let live_display = simulator.as_ref().and_then(|simulator| {
2232
+ let displays = $crate::XcodeCommandLineTools::new()
2233
+ .simctl()
2234
+ .displays(&simulator.udid)
2235
+ .ok()?;
2236
+ $crate::mcp::live_display(status, &displays).cloned()
2237
+ });
2238
+ Ok(::rmcp::model::CallToolResult::success(vec![
2239
+ ::rmcp::model::ContentBlock::json(&::serde_json::json!({
2240
+ "simulator": simulator,
2241
+ "hinge": hinge,
2242
+ "live_display": live_display,
2243
+ }))?,
2244
+ ]))
2245
+ }
2246
+
2247
+ #[::rmcp::tool(
2248
+ name = $posture_set_name,
2249
+ title = "Set fold posture",
2250
+ annotations(title = "Set fold posture", read_only_hint = false, destructive_hint = false, idempotent_hint = true),
2251
+ description = "Purpose: fold or unfold a foldable simulator (such as iPhone Duo). When to use vs siblings: use this to switch which display is live; use posture_get to read the current state. Behavior: reads the hinge through ControlKit, and if it is not already in the requested posture presses the matching hinge button (Closed or Open) in the simulator's Device Hub window through macOS accessibility, then polls `device.hinge.get` for up to 5 seconds until the hinge reports the new state. Prerequisites: Device Hub running with the simulator's window open (simctl and the Simulator app have no fold control), the Accessibility permission granted to the app running xcrs, and a ControlKit runner with hinge support. The window is found by the simulator's name, so keep a single Device Hub window per name. Failure modes: errors if the target is not an Apple simulator, if the device reports no hinge, if Device Hub or its window is missing, if Accessibility access is denied, or if the hinge does not change within the timeout."
2252
+ )]
2253
+ async fn posture_set(
2254
+ &self,
2255
+ ::rmcp::handler::server::wrapper::Parameters(
2256
+ args,
2257
+ ): ::rmcp::handler::server::wrapper::Parameters<
2258
+ $crate::mcp::PostureSetArgs,
2259
+ >,
2260
+ ) -> ::std::result::Result<
2261
+ ::rmcp::model::CallToolResult,
2262
+ ::rmcp::model::ErrorData,
2263
+ > {
2264
+ let target = Self::dispatch_apple_target(
2265
+ $posture_set_name,
2266
+ args.simulator_name,
2267
+ args.simulator_udid,
2268
+ None,
2269
+ args.controlkit_port,
2270
+ )?;
2271
+ let (Some(simulator), controlkit) = Self::controlkit_for_target(&target)? else {
2272
+ return Err(::rmcp::model::ErrorData::invalid_request(
2273
+ concat!($posture_set_name, " only works on Apple simulators."),
2274
+ None,
2275
+ ));
2276
+ };
2277
+ let read_status = || async {
2278
+ let hinge = controlkit
2279
+ .call("device.hinge.get", ::serde_json::json!({}))
2280
+ .await
2281
+ .map_err(|error| {
2282
+ ::rmcp::model::ErrorData::internal_error(error.to_string(), None)
2283
+ })?;
2284
+ Ok::<_, ::rmcp::model::ErrorData>(
2285
+ hinge
2286
+ .get("status")
2287
+ .and_then(::serde_json::Value::as_str)
2288
+ .unwrap_or("unsupported")
2289
+ .to_string(),
2290
+ )
2291
+ };
2292
+
2293
+ let current = read_status().await?;
2294
+ if current == "unsupported" {
2295
+ return Err(::rmcp::model::ErrorData::invalid_request(
2296
+ format!("{} has no hinge, so it cannot be folded.", simulator.name),
2297
+ None,
2298
+ ));
2299
+ }
2300
+ if !args.posture.matches_hinge_status(¤t) {
2301
+ $crate::HostInspect::new()
2302
+ .set_posture(&simulator.name, args.posture.hinge_button())
2303
+ .map_err(|error| {
2304
+ ::rmcp::model::ErrorData::internal_error(error.to_string(), None)
2305
+ })?;
2306
+ let deadline = ::std::time::Instant::now() + ::std::time::Duration::from_secs(5);
2307
+ loop {
2308
+ ::tokio::time::sleep(::std::time::Duration::from_millis(250)).await;
2309
+ if args.posture.matches_hinge_status(&read_status().await?) {
2310
+ break;
2311
+ }
2312
+ if ::std::time::Instant::now() >= deadline {
2313
+ return Err(::rmcp::model::ErrorData::internal_error(
2314
+ format!(
2315
+ "Device Hub accepted the {} request but the hinge did not change within 5 seconds.",
2316
+ args.posture.as_str()
2317
+ ),
2318
+ None,
2319
+ ));
2320
+ }
2321
+ }
2322
+ }
2323
+ Ok(::rmcp::model::CallToolResult::success(vec![
2324
+ ::rmcp::model::ContentBlock::text(format!(
2325
+ "Set {} to {}.",
2326
+ simulator.name,
2327
+ args.posture.as_str()
2328
+ )),
2329
+ ]))
2330
+ }
2331
}
2332
};
2333
}
2352
"input_click",
2353
"input_spatial_tap",
2354
"orientation_get",
2062
- "orientation_set"
2355
+ "orientation_set",
2356
+ "posture_get",
2357
+ "posture_set"
2358
);
2359
2360
#[rmcp::tool_handler(router = Self::xcrs_tool_router())]
2393
mod tests {
2394
use super::*;
2395
2396
+ fn duo_displays() -> Vec<crate::SimDisplay> {
2397
+ let display = |screen_id, role: &str, width, height| crate::SimDisplay {
2398
+ screen_id,
2399
+ name: String::new(),
2400
+ role: role.to_string(),
2401
+ width,
2402
+ height,
2403
+ };
2404
+ vec![
2405
+ display(1, "outer", 1398, 2034),
2406
+ display(3, "inner", 2007, 2853),
2407
+ ]
2408
+ }
2409
+
2410
+ #[test]
2411
+ fn live_display_follows_the_hinge_status() {
2412
+ let displays = duo_displays();
2413
+
2414
+ assert_eq!(
2415
+ live_display("closed", &displays).map(|d| d.screen_id),
2416
+ Some(1)
2417
+ );
2418
+ assert_eq!(
2419
+ live_display("fullyOpen", &displays).map(|d| d.screen_id),
2420
+ Some(3)
2421
+ );
2422
+ assert_eq!(
2423
+ live_display("partiallyOpen", &displays).map(|d| d.screen_id),
2424
+ Some(3)
2425
+ );
2426
+ assert!(live_display("unsupported", &displays).is_none());
2427
+ assert!(live_display("closed", &displays[1..]).is_none());
2428
+ }
2429
+
2430
+ #[test]
2431
+ fn postures_map_to_hinge_buttons_and_statuses() {
2432
+ assert_eq!(Posture::Folded.hinge_button(), "closed");
2433
+ assert_eq!(Posture::Unfolded.hinge_button(), "open");
2434
+ assert!(Posture::Folded.matches_hinge_status("closed"));
2435
+ assert!(!Posture::Folded.matches_hinge_status("fullyOpen"));
2436
+ assert!(Posture::Unfolded.matches_hinge_status("fullyOpen"));
2437
+ assert!(!Posture::Unfolded.matches_hinge_status("partiallyOpen"));
2438
+ }
2439
+
2440
+ #[test]
2441
+ fn posture_set_args_accept_folded_and_unfolded_only() {
2442
+ let args: PostureSetArgs =
2443
+ serde_json::from_value(json!({ "posture": "folded" })).expect("folded is valid");
2444
+ assert_eq!(args.posture, Posture::Folded);
2445
+ assert!(serde_json::from_value::<PostureSetArgs>(json!({ "posture": "bent" })).is_err());
2446
+ assert!(serde_json::from_value::<PostureSetArgs>(json!({})).is_err());
2447
+ }
2448
+
2449
+ #[test]
2450
+ fn screen_capture_advertises_the_display_argument() {
2451
+ let tools = XcrsMcpServer::xcrs_tool_router().list_all();
2452
+ let tool = tools
2453
+ .iter()
2454
+ .find(|tool| tool.name == "screen_capture")
2455
+ .expect("screen_capture is exposed");
2456
+ assert!(tool
2457
+ .input_schema
2458
+ .get("properties")
2459
+ .and_then(|p| p.get("display"))
2460
+ .is_some());
2461
+ }
2462
+
2463
#[test]
2464
fn legacy_discovery_probe_receives_method_not_found() {
2465
let response = legacy_discovery_response(
2482
assert!(response.is_none());
2483
}
2484
2123
- /// The 19 canonical tool names shared by the standalone and embedded
2485
+ /// The 21 canonical tool names shared by the standalone and embedded
2486
/// automation servers.
2125
- const CANONICAL_TOOL_NAMES: [&str; 19] = [
2487
+ const CANONICAL_TOOL_NAMES: [&str; 21] = [
2488
"device_list",
2489
"device_select",
2490
"device_capabilities",
2504
"input_spatial_tap",
2505
"orientation_get",
2506
"orientation_set",
2507
+ "posture_get",
2508
+ "posture_set",
2509
];
2510
2511
/// Fully-qualified tool names this crate used before the canonical-tool
2544
/// types it does not own, so they intentionally advertise no
2545
/// `output_schema`. Only `device_select` returns structured content built
2546
/// entirely from types this module owns, so it is the only tool with one.
2183
- const TOOLS_WITHOUT_OUTPUT_SCHEMA: [&str; 18] = [
2547
+ const TOOLS_WITHOUT_OUTPUT_SCHEMA: [&str; 20] = [
2548
"device_list",
2549
"device_capabilities",
2550
"app_install_launch",
2563
"input_spatial_tap",
2564
"orientation_get",
2565
"orientation_set",
2566
+ "posture_get",
2567
+ "posture_set",
2568
];
2569
2570
#[test]
2205
- fn tool_router_exposes_exactly_the_nineteen_canonical_tools() {
2571
+ fn tool_router_exposes_exactly_the_twentyone_canonical_tools() {
2572
let tools = XcrsMcpServer::xcrs_tool_router().list_all();
2573
2574
assert_eq!(