Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,50 @@ pub struct ComputerUseLinux {
desktop_size: Arc<Mutex<Option<(u32, u32)>>>,
}

fn sanitize_unsigned_integer_formats(value: &mut serde_json::Value) {
let serde_json::Value::Object(object) = value else {
return;
};

let has_unsigned_format = matches!(
object.get("format").and_then(serde_json::Value::as_str),
Some("uint" | "uint8" | "uint16" | "uint32" | "uint64" | "usize")
);
if has_unsigned_format {
object.remove("format");
}

for nested in object.values_mut() {
match nested {
serde_json::Value::Object(_) => sanitize_unsigned_integer_formats(nested),
serde_json::Value::Array(items) => {
for item in items {
sanitize_unsigned_integer_formats(item);
}
}
_ => {}
}
}
}
Comment on lines +69 to +93

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The recursion logic in sanitize_unsigned_integer_formats can be simplified and made more robust by matching directly on the serde_json::Value enum, similar to how it is done in the test helper collect_unsigned_integer_formats. This avoids the nested match inside the loop and ensures that any nested array-of-arrays or other structures are correctly traversed.

fn sanitize_unsigned_integer_formats(value: &mut serde_json::Value) {
    match value {
        serde_json::Value::Object(object) => {
            let has_unsigned_format = matches!(
                object.get("format").and_then(serde_json::Value::as_str),
                Some("uint" | "uint8" | "uint16" | "uint32" | "uint64" | "usize")
            );
            if has_unsigned_format {
                object.remove("format");
            }
            for nested in object.values_mut() {
                sanitize_unsigned_integer_formats(nested);
            }
        }
        serde_json::Value::Array(items) => {
            for item in items {
                sanitize_unsigned_integer_formats(item);
            }
        }
        _ => {}
    }
}


impl ComputerUseLinux {
fn mcp_tool_router(&self) -> rmcp::handler::server::router::tool::ToolRouter<Self> {
let mut router = Self::tool_router();
for route in router.map.values_mut() {
let input_schema = Arc::make_mut(&mut route.attr.input_schema);
for value in input_schema.values_mut() {
sanitize_unsigned_integer_formats(value);
}
if let Some(output_schema) = route.attr.output_schema.as_mut() {
for value in Arc::make_mut(output_schema).values_mut() {
sanitize_unsigned_integer_formats(value);
}
}
}
router
}
}

#[tool_router]
impl ComputerUseLinux {
#[tool(
Expand Down Expand Up @@ -1287,6 +1331,7 @@ impl ComputerUseLinux {
}

#[tool_handler(
router = self.mcp_tool_router(),
name = "computer-use-linux",
// NOTE: keep in lockstep with Cargo.toml + package.json on every release.
// The rmcp tool_handler macro only accepts a string literal here, so this
Expand Down Expand Up @@ -3743,6 +3788,49 @@ mod tests {
use crate::atspi_tree::{AccessibilityAction, Bounds};
use crate::windows::{WindowBounds, GNOME_SHELL_EXTENSION_BACKEND};

#[test]
fn exported_tool_schemas_omit_unsigned_integer_formats() {
let tools = ComputerUseLinux::default().mcp_tool_router().list_all();
let value = serde_json::to_value(tools).unwrap();
let mut unsupported = Vec::new();
collect_unsigned_integer_formats(&value, "$", &mut unsupported);

assert!(
unsupported.is_empty(),
"unsupported unsigned integer formats: {unsupported:?}"
);
}

fn collect_unsigned_integer_formats(
value: &serde_json::Value,
path: &str,
unsupported: &mut Vec<String>,
) {
match value {
serde_json::Value::Object(object) => {
if matches!(
object.get("format").and_then(serde_json::Value::as_str),
Some("uint" | "uint8" | "uint16" | "uint32" | "uint64" | "usize")
) {
unsupported.push(path.to_string());
}
for (key, nested) in object {
collect_unsigned_integer_formats(nested, &format!("{path}/{key}"), unsupported);
}
}
serde_json::Value::Array(items) => {
for (index, nested) in items.iter().enumerate() {
collect_unsigned_integer_formats(
nested,
&format!("{path}/{index}"),
unsupported,
);
}
}
_ => {}
}
}

fn node(index: u32, bounds: Option<Bounds>) -> AccessibilityNode {
node_with_actions(index, bounds, Vec::new())
}
Expand Down
28 changes: 24 additions & 4 deletions src/windowing/backends/hyprland.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,18 +83,30 @@ pub(crate) fn parse_hyprland_clients(json: &str) -> Result<Vec<WindowInfo>> {

pub fn activate_window(window_id: u64) -> Result<()> {
let address = format!("address:0x{window_id:x}");
let output = hyprctl_output(&["dispatch", "focuswindow", &address])
let lua_dispatch = lua_focus_dispatch(&address);
let lua_output = hyprctl_output(&["dispatch", &lua_dispatch])
.with_context(|| format!("failed to run Hyprland Lua focus dispatcher for {address}"))?;
if lua_output.status.success() {
return Ok(());
}

let legacy_output = hyprctl_output(&["dispatch", "focuswindow", &address])
.with_context(|| format!("failed to run hyprctl dispatch focuswindow {address}"))?;
if output.status.success() {
if legacy_output.status.success() {
Ok(())
} else {
bail!(
"hyprctl dispatch focuswindow {address} failed: {}",
String::from_utf8_lossy(&output.stderr).trim()
"Hyprland window focus failed for {address}; Lua dispatcher: {}; legacy dispatcher: {}",
String::from_utf8_lossy(&lua_output.stderr).trim(),
String::from_utf8_lossy(&legacy_output.stderr).trim()
);
}
}

fn lua_focus_dispatch(address: &str) -> String {
format!("hl.dsp.focus({{ window = \"{address}\" }})")
}

fn hyprctl_output(args: &[&str]) -> std::io::Result<std::process::Output> {
let mut command = Command::new("hyprctl");
let has_signature = std::env::var("HYPRLAND_INSTANCE_SIGNATURE")
Expand Down Expand Up @@ -259,6 +271,14 @@ mod tests {
use super::*;
use std::time::Duration;

#[test]
fn builds_hyprland_055_lua_focus_dispatch() {
assert_eq!(
lua_focus_dispatch("address:0x1234abcd"),
"hl.dsp.focus({ window = \"address:0x1234abcd\" })"
);
}

#[test]
fn selects_wayland_matching_hyprland_instance_before_newer_nonmatch() {
let older_match = HyprlandInstanceCandidate {
Expand Down
Loading