Skip to main content

rmcp_display/
lib.rs

1use display_info::DisplayInfo;
2use rmcp::{
3    handler::server::{router::tool::ToolRouter, ServerHandler, wrapper::Parameters},
4    model::*,
5    ErrorData as McpError,
6};
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9
10/// Parameters for get_display_at_point
11#[derive(Debug, Serialize, Deserialize, JsonSchema)]
12pub struct PointParams {
13    #[schemars(description = "X coordinate on screen")]
14    pub x: i32,
15    #[schemars(description = "Y coordinate on screen")]
16    pub y: i32,
17}
18
19/// Parameters for get_display_by_name
20#[derive(Debug, Serialize, Deserialize, JsonSchema)]
21pub struct NameParams {
22    #[schemars(description = "Display name to search for")]
23    pub name: String,
24}
25
26#[derive(Debug)]
27pub struct DisplayServer {
28    pub tool_router: ToolRouter<Self>,
29}
30
31impl Default for DisplayServer {
32    fn default() -> Self {
33        Self::new()
34    }
35}
36
37impl DisplayServer {
38    pub fn new() -> Self {
39        Self {
40            tool_router: Self::tool_router(),
41        }
42    }
43
44    fn format_single_display(d: &DisplayInfo) -> String {
45        let mut result = String::new();
46
47        // Header with name and primary indicator
48        let primary = if d.is_primary { " (primary)" } else { "" };
49        result.push_str(&format!(
50            "{}{}\n",
51            if d.friendly_name.is_empty() { &d.name } else { &d.friendly_name },
52            primary
53        ));
54
55        // Resolution and position
56        result.push_str(&format!("  Resolution: {}x{}\n", d.width, d.height));
57        result.push_str(&format!("  Position: ({}, {})\n", d.x, d.y));
58
59        // Physical size if available
60        if d.width_mm > 0 && d.height_mm > 0 {
61            let diag_mm = ((d.width_mm.pow(2) + d.height_mm.pow(2)) as f32).sqrt();
62            let diag_inches = diag_mm / 25.4;
63            result.push_str(&format!(
64                "  Physical: {}mm x {}mm (~{:.1}\")\n",
65                d.width_mm, d.height_mm, diag_inches
66            ));
67        }
68
69        // Refresh rate
70        if d.frequency > 0.0 {
71            result.push_str(&format!("  Refresh: {:.0}Hz\n", d.frequency));
72        }
73
74        // Scale factor
75        if d.scale_factor != 1.0 {
76            result.push_str(&format!("  Scale: {:.0}%\n", d.scale_factor * 100.0));
77        }
78
79        // Rotation
80        if d.rotation != 0.0 {
81            result.push_str(&format!("  Rotation: {}°\n", d.rotation as i32));
82        }
83
84        result
85    }
86
87    fn format_display_info(displays: &[DisplayInfo]) -> String {
88        let mut result = String::from("Display Information:\n\n");
89
90        if displays.is_empty() {
91            result.push_str("No displays detected.\n");
92            return result;
93        }
94
95        for (i, d) in displays.iter().enumerate() {
96            result.push_str(&format!("Display {}: ", i + 1));
97            result.push_str(&Self::format_single_display(d));
98            result.push('\n');
99        }
100
101        result.push_str(&format!("Total displays: {}\n", displays.len()));
102        result
103    }
104}
105
106#[rmcp::tool_router]
107impl DisplayServer {
108    #[rmcp::tool(description = "Get display/monitor information (connected displays, resolutions, physical sizes)")]
109    pub async fn get_display_info(&self) -> Result<CallToolResult, McpError> {
110        let displays = DisplayInfo::all()
111            .map_err(|e| McpError::internal_error(format!("Failed to get display info: {}", e), None))?;
112
113        let formatted = Self::format_display_info(&displays);
114
115        Ok(CallToolResult::success(vec![Content::text(formatted)]))
116    }
117
118    #[rmcp::tool(description = "Get display info at specific screen coordinates (useful for determining which monitor contains a point)")]
119    pub async fn get_display_at_point(
120        &self,
121        Parameters(params): Parameters<PointParams>,
122    ) -> Result<CallToolResult, McpError> {
123        let display = DisplayInfo::from_point(params.x, params.y)
124            .map_err(|e| McpError::internal_error(format!("Failed to get display at ({}, {}): {}", params.x, params.y, e), None))?;
125
126        let formatted = format!(
127            "Display at ({}, {}):\n{}",
128            params.x, params.y,
129            Self::format_single_display(&display)
130        );
131
132        Ok(CallToolResult::success(vec![Content::text(formatted)]))
133    }
134
135    #[rmcp::tool(description = "Get display info by name")]
136    pub async fn get_display_by_name(
137        &self,
138        Parameters(params): Parameters<NameParams>,
139    ) -> Result<CallToolResult, McpError> {
140        let display = DisplayInfo::from_name(&params.name)
141            .map_err(|e| McpError::internal_error(format!("Failed to get display '{}': {}", params.name, e), None))?;
142
143        let formatted = Self::format_single_display(&display);
144
145        Ok(CallToolResult::success(vec![Content::text(formatted)]))
146    }
147}
148
149#[rmcp::tool_handler]
150impl ServerHandler for DisplayServer {
151    fn get_info(&self) -> ServerInfo {
152        ServerInfo {
153            protocol_version: ProtocolVersion::V_2024_11_05,
154            capabilities: ServerCapabilities::builder()
155                .enable_tools()
156                .build(),
157            server_info: Implementation::from_build_env(),
158            instructions: Some("Cross-platform display/monitor information server".into()),
159        }
160    }
161}