1use serde::{Deserialize, Serialize};
11use std::path::PathBuf;
12use thiserror::Error;
13
14pub fn default_socket_path() -> PathBuf {
16 let runtime_dir = std::env::var("XDG_RUNTIME_DIR")
17 .map(PathBuf::from)
18 .unwrap_or_else(|_| std::env::temp_dir());
19 runtime_dir.join("egui-mcp.sock")
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct NodeInfo {
25 pub id: u64,
27 pub role: String,
29 pub label: Option<String>,
31 pub value: Option<String>,
33 pub bounds: Option<Rect>,
35 pub children: Vec<u64>,
37 pub toggled: Option<bool>,
39 pub disabled: bool,
41 pub focused: bool,
43}
44
45#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
47pub struct Rect {
48 pub x: f32,
49 pub y: f32,
50 pub width: f32,
51 pub height: f32,
52}
53
54#[derive(Debug, Clone, Default, Serialize, Deserialize)]
56pub struct UiTree {
57 pub roots: Vec<u64>,
59 pub nodes: Vec<NodeInfo>,
61}
62
63#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
65pub enum MouseButton {
66 Left,
67 Right,
68 Middle,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct LogEntry {
74 pub level: String,
76 pub target: String,
78 pub message: String,
80 pub timestamp_ms: u64,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct FrameStats {
87 pub fps: f32,
89 pub frame_time_ms: f32,
91 pub frame_time_min_ms: f32,
93 pub frame_time_max_ms: f32,
95 pub sample_count: usize,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct PerfReport {
102 pub duration_ms: u64,
104 pub total_frames: usize,
106 pub avg_fps: f32,
108 pub avg_frame_time_ms: f32,
110 pub min_frame_time_ms: f32,
112 pub max_frame_time_ms: f32,
114 pub p95_frame_time_ms: f32,
116 pub p99_frame_time_ms: f32,
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
125#[serde(tag = "type")]
126pub enum Request {
127 Ping,
129
130 TakeScreenshot,
132
133 TakeScreenshotRegion {
135 x: f32,
137 y: f32,
139 width: f32,
141 height: f32,
143 },
144
145 ClickAt {
147 x: f32,
149 y: f32,
151 button: MouseButton,
153 },
154
155 KeyboardInput {
157 key: String,
159 },
160
161 Scroll {
163 x: f32,
165 y: f32,
167 delta_x: f32,
169 delta_y: f32,
171 },
172
173 MoveMouse {
175 x: f32,
177 y: f32,
179 },
180
181 Drag {
183 start_x: f32,
185 start_y: f32,
187 end_x: f32,
189 end_y: f32,
191 button: MouseButton,
193 },
194
195 DoubleClick {
197 x: f32,
199 y: f32,
201 button: MouseButton,
203 },
204
205 HighlightElement {
207 x: f32,
209 y: f32,
211 width: f32,
213 height: f32,
215 color: [u8; 4],
217 duration_ms: u64,
219 },
220
221 ClearHighlights,
223
224 GetLogs {
226 level: Option<String>,
229 limit: Option<usize>,
231 },
232
233 ClearLogs,
235
236 GetFrameStats,
238
239 StartPerfRecording {
241 duration_ms: u64,
243 },
244
245 GetPerfReport,
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize)]
251#[serde(tag = "type")]
252pub enum Response {
253 Pong,
255
256 Screenshot {
258 data: String,
260 format: String,
262 },
263
264 Success,
266
267 Error { message: String },
269
270 Logs {
272 entries: Vec<LogEntry>,
274 },
275
276 FrameStatsResponse {
278 stats: FrameStats,
280 },
281
282 PerfReportResponse {
284 report: Option<PerfReport>,
286 },
287}
288
289#[derive(Debug, Error)]
291pub enum ProtocolError {
292 #[error("IO error: {0}")]
293 Io(#[from] std::io::Error),
294 #[error("JSON error: {0}")]
295 Json(#[from] serde_json::Error),
296 #[error("Connection closed")]
297 ConnectionClosed,
298 #[error("Message too large: {0} bytes")]
299 MessageTooLarge(usize),
300}
301
302pub const MAX_MESSAGE_SIZE: usize = 1024 * 1024;
304
305pub async fn read_message<R: tokio::io::AsyncReadExt + Unpin>(
307 reader: &mut R,
308) -> Result<Vec<u8>, ProtocolError> {
309 let mut len_buf = [0u8; 4];
310 match reader.read_exact(&mut len_buf).await {
311 Ok(_) => {}
312 Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
313 return Err(ProtocolError::ConnectionClosed);
314 }
315 Err(e) => return Err(e.into()),
316 }
317
318 let len = u32::from_be_bytes(len_buf) as usize;
319 if len > MAX_MESSAGE_SIZE {
320 return Err(ProtocolError::MessageTooLarge(len));
321 }
322
323 let mut buf = vec![0u8; len];
324 reader.read_exact(&mut buf).await?;
325 Ok(buf)
326}
327
328pub async fn write_message<W: tokio::io::AsyncWriteExt + Unpin>(
330 writer: &mut W,
331 data: &[u8],
332) -> Result<(), ProtocolError> {
333 if data.len() > MAX_MESSAGE_SIZE {
334 return Err(ProtocolError::MessageTooLarge(data.len()));
335 }
336
337 let len = (data.len() as u32).to_be_bytes();
338 writer.write_all(&len).await?;
339 writer.write_all(data).await?;
340 writer.flush().await?;
341 Ok(())
342}
343
344pub async fn read_request<R: tokio::io::AsyncReadExt + Unpin>(
346 reader: &mut R,
347) -> Result<Request, ProtocolError> {
348 let data = read_message(reader).await?;
349 let request = serde_json::from_slice(&data)?;
350 Ok(request)
351}
352
353pub async fn write_response<W: tokio::io::AsyncWriteExt + Unpin>(
355 writer: &mut W,
356 response: &Response,
357) -> Result<(), ProtocolError> {
358 let data = serde_json::to_vec(response)?;
359 write_message(writer, &data).await
360}
361
362pub async fn read_response<R: tokio::io::AsyncReadExt + Unpin>(
364 reader: &mut R,
365) -> Result<Response, ProtocolError> {
366 let data = read_message(reader).await?;
367 let response = serde_json::from_slice(&data)?;
368 Ok(response)
369}
370
371pub async fn write_request<W: tokio::io::AsyncWriteExt + Unpin>(
373 writer: &mut W,
374 request: &Request,
375) -> Result<(), ProtocolError> {
376 let data = serde_json::to_vec(request)?;
377 write_message(writer, &data).await
378}
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383
384 #[test]
385 fn test_serialize_request() {
386 let req = Request::Ping;
387 let json = serde_json::to_string(&req).unwrap();
388 assert!(json.contains("Ping"));
389 }
390
391 #[test]
392 fn test_serialize_response() {
393 let resp = Response::Pong;
394 let json = serde_json::to_string(&resp).unwrap();
395 assert!(json.contains("Pong"));
396 }
397
398 #[test]
399 fn test_default_socket_path() {
400 let path = default_socket_path();
401 assert!(path.to_string_lossy().contains("egui-mcp.sock"));
402 }
403
404 #[test]
405 fn test_click_at_request() {
406 let req = Request::ClickAt {
407 x: 100.0,
408 y: 200.0,
409 button: MouseButton::Left,
410 };
411 let json = serde_json::to_string(&req).unwrap();
412 assert!(json.contains("ClickAt"));
413 assert!(json.contains("100"));
414 }
415
416 #[test]
417 fn test_keyboard_input_request() {
418 let req = Request::KeyboardInput {
419 key: "Enter".to_string(),
420 };
421 let json = serde_json::to_string(&req).unwrap();
422 assert!(json.contains("KeyboardInput"));
423 assert!(json.contains("Enter"));
424 }
425}