1use crate::{Result, TauriMcpError};
2use crate::tools::{
3 process::ProcessManager,
4 window::WindowManager,
5 input::InputSimulator,
6 debug::DebugTools,
7 ipc::IpcManager,
8};
9use jsonrpc_core::{IoHandler, Params, Value, Error as RpcError};
10use serde::{Deserialize, Serialize};
11use serde_json::json;
12use std::path::PathBuf;
13use std::sync::Arc;
14use tokio::sync::RwLock;
15use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
16use tracing::{debug, error, info};
17
18pub struct TauriMcpServer {
19 process_manager: Arc<RwLock<ProcessManager>>,
20 window_manager: Arc<WindowManager>,
21 input_simulator: Arc<InputSimulator>,
22 debug_tools: Arc<DebugTools>,
23 ipc_manager: Arc<IpcManager>,
24 config: ServerConfig,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct ServerConfig {
29 pub auto_discover: bool,
30 pub session_management: bool,
31 pub event_streaming: bool,
32 pub performance_profiling: bool,
33 pub network_interception: bool,
34}
35
36impl Default for ServerConfig {
37 fn default() -> Self {
38 Self {
39 auto_discover: true,
40 session_management: true,
41 event_streaming: false,
42 performance_profiling: false,
43 network_interception: false,
44 }
45 }
46}
47
48
49impl TauriMcpServer {
50 pub async fn new(config_path: PathBuf) -> Result<Self> {
51 let config = if config_path.exists() {
52 let config_str = tokio::fs::read_to_string(&config_path).await?;
53 toml::from_str(&config_str).map_err(|e| TauriMcpError::ConfigError(e.to_string()))?
54 } else {
55 ServerConfig::default()
56 };
57
58 debug!("Initializing Tauri MCP server with config: {:?}", config);
59
60 Ok(Self {
61 process_manager: Arc::new(RwLock::new(ProcessManager::new())),
62 window_manager: Arc::new(WindowManager::new()),
63 input_simulator: Arc::new(InputSimulator::new()),
64 debug_tools: Arc::new(DebugTools::new()),
65 ipc_manager: Arc::new(IpcManager::new()),
66 config,
67 })
68 }
69
70 pub async fn serve(&self, host: &str, port: u16) -> Result<()> {
71 debug!("Starting MCP server on {}:{}", host, port);
72
73 let mut io = IoHandler::new();
74
75 let server = McpServerImpl {
76 process_manager: Arc::clone(&self.process_manager),
77 window_manager: Arc::clone(&self.window_manager),
78 input_simulator: Arc::clone(&self.input_simulator),
79 debug_tools: Arc::clone(&self.debug_tools),
80 ipc_manager: Arc::clone(&self.ipc_manager),
81 };
82
83 let server_clone = server.clone();
85 io.add_method("initialize", move |params: Params| {
86 let server = server_clone.clone();
87 async move {
88 match params {
89 Params::Map(mut map) => {
90 let protocol_version = map.remove("protocolVersion")
91 .and_then(|v| v.as_str().map(String::from))
92 .unwrap_or_else(|| "1.0".to_string());
93
94 let capabilities = map.remove("capabilities").unwrap_or(Value::Null);
95
96 server.initialize(protocol_version, capabilities)
97 }
98 _ => Err(RpcError::invalid_params("Expected object parameters"))
99 }
100 }
101 });
102
103 let server_clone = server.clone();
104 io.add_method("shutdown", move |_params: Params| {
105 let server = server_clone.clone();
106 async move { server.shutdown() }
107 });
108
109 let server_clone = server.clone();
110 io.add_method("tools/list", move |_params: Params| {
111 let server = server_clone.clone();
112 async move { server.list_tools() }
113 });
114
115 let server_clone = server.clone();
116 io.add_method("tools/call", move |params: Params| {
117 let server = server_clone.clone();
118 async move {
119 match params {
120 Params::Map(map) => server.call_tool(Value::Object(map)),
121 _ => Err(RpcError::invalid_params("Expected object parameters"))
122 }
123 }
124 });
125
126 let tool_methods = vec![
128 ("launch_app", "app_path", "args"),
129 ("stop_app", "process_id", ""),
130 ("get_app_logs", "process_id", "lines"),
131 ("take_screenshot", "process_id", "output_path"),
132 ("get_window_info", "process_id", ""),
133 ("send_keyboard_input", "process_id", "keys"),
134 ("send_mouse_click", "process_id", "x,y,button"),
135 ("execute_js", "process_id", "javascript_code"),
136 ("get_devtools_info", "process_id", ""),
137 ("monitor_resources", "process_id", ""),
138 ("list_ipc_handlers", "process_id", ""),
139 ("call_ipc_command", "process_id", "command_name,args"),
140 ];
141
142 for (method_name, _, _) in tool_methods {
143 let server_clone = server.clone();
144 io.add_method(method_name, move |params: Params| {
145 let server = server_clone.clone();
146 let method_name = method_name.to_string();
147 async move {
148 match params {
149 Params::Map(map) => {
150 server.call_tool(json!({
151 "name": method_name,
152 "arguments": Value::Object(map)
153 }))
154 }
155 _ => Err(RpcError::invalid_params("Expected object parameters"))
156 }
157 }
158 });
159 }
160
161 let stdin = tokio::io::stdin();
162 let stdout = tokio::io::stdout();
163 let mut reader = BufReader::new(stdin);
164 let mut stdout = stdout;
165
166 loop {
169 let mut line = String::new();
170 match reader.read_line(&mut line).await {
171 Ok(0) => {
172 info!("EOF reached, shutting down");
173 break;
174 }
175 Ok(_) => {
176 let line = line.trim();
177 if line.is_empty() {
178 continue;
179 }
180
181 debug!("Received request: {}", line);
182
183 match io.handle_request(&line).await {
184 Some(response) => {
185 debug!("Sending response: {}", response);
186 stdout.write_all(response.as_bytes()).await?;
187 stdout.write_all(b"\n").await?;
188 stdout.flush().await?;
189 }
190 None => {
191 error!("No response generated for request: {}", line);
192 }
193 }
194 }
195 Err(e) => {
196 error!("Error reading from stdin: {}", e);
197 break;
198 }
199 }
200 }
201
202 Ok(())
203 }
204}
205
206#[derive(Clone)]
207struct McpServerImpl {
208 process_manager: Arc<RwLock<ProcessManager>>,
209 window_manager: Arc<WindowManager>,
210 input_simulator: Arc<InputSimulator>,
211 debug_tools: Arc<DebugTools>,
212 ipc_manager: Arc<IpcManager>,
213}
214
215impl McpServerImpl {
216 fn initialize(&self, protocol_version: String, capabilities: Value) -> jsonrpc_core::Result<Value> {
217
218 if protocol_version != "1.0" && protocol_version != "2024-11-05" {
220 return Err(RpcError::invalid_params(format!("Unsupported protocol version: {}", protocol_version)));
221 }
222
223 let _client_capabilities = capabilities;
225
226 Ok(json!({
227 "protocolVersion": protocol_version,
228 "serverInfo": {
229 "name": "tauri-mcp",
230 "version": env!("CARGO_PKG_VERSION"),
231 "description": "MCP server for testing and interacting with Tauri v2 applications"
232 },
233 "capabilities": {
234 "tools": {
235 "listTools": true
236 },
237 "resources": null,
238 "prompts": null,
239 "logging": null
240 }
241 }))
242 }
243
244 fn shutdown(&self) -> jsonrpc_core::Result<Value> {
245 Ok(json!({
247 "status": "shutdown"
248 }))
249 }
250
251 fn launch_app(&self, app_path: String, args: Option<Vec<String>>) -> jsonrpc_core::Result<Value> {
252 let process_manager = Arc::clone(&self.process_manager);
253 let args = args.unwrap_or_default();
254
255 let runtime = tokio::runtime::Handle::current();
256 let result = runtime.block_on(async {
257 let mut manager = process_manager.write().await;
258 manager.launch_app(&app_path, args).await
259 });
260
261 match result {
262 Ok(process_id) => Ok(json!({
263 "process_id": process_id,
264 "status": "launched"
265 })),
266 Err(e) => Err(RpcError::invalid_params(e.to_string())),
267 }
268 }
269
270 fn stop_app(&self, process_id: String) -> jsonrpc_core::Result<Value> {
271 let process_manager = Arc::clone(&self.process_manager);
272
273 let runtime = tokio::runtime::Handle::current();
274 let result = runtime.block_on(async {
275 let mut manager = process_manager.write().await;
276 manager.stop_app(&process_id).await
277 });
278
279 match result {
280 Ok(()) => Ok(json!({
281 "status": "stopped"
282 })),
283 Err(e) => Err(RpcError::invalid_params(e.to_string())),
284 }
285 }
286
287 fn get_app_logs(&self, process_id: String, lines: Option<usize>) -> jsonrpc_core::Result<Value> {
288 let process_manager = Arc::clone(&self.process_manager);
289
290 let runtime = tokio::runtime::Handle::current();
291 let result = runtime.block_on(async {
292 let manager = process_manager.read().await;
293 manager.get_app_logs(&process_id, lines).await
294 });
295
296 match result {
297 Ok(logs) => Ok(json!({
298 "logs": logs
299 })),
300 Err(e) => Err(RpcError::invalid_params(e.to_string())),
301 }
302 }
303
304 fn take_screenshot(&self, process_id: String, output_path: Option<String>) -> jsonrpc_core::Result<Value> {
305 let window_manager = Arc::clone(&self.window_manager);
306 let output_path = output_path.map(PathBuf::from);
307
308 let runtime = tokio::runtime::Handle::current();
309 let result = runtime.block_on(async {
310 window_manager.take_screenshot(&process_id, output_path).await
311 });
312
313 match result {
314 Ok(screenshot_data) => Ok(json!({
315 "screenshot": screenshot_data
316 })),
317 Err(e) => Err(RpcError::invalid_params(e.to_string())),
318 }
319 }
320
321 fn get_window_info(&self, process_id: String) -> jsonrpc_core::Result<Value> {
322 let window_manager = Arc::clone(&self.window_manager);
323
324 let runtime = tokio::runtime::Handle::current();
325 let result = runtime.block_on(async {
326 window_manager.get_window_info(&process_id).await
327 });
328
329 match result {
330 Ok(info) => Ok(info),
331 Err(e) => Err(RpcError::invalid_params(e.to_string())),
332 }
333 }
334
335 fn send_keyboard_input(&self, process_id: String, keys: String) -> jsonrpc_core::Result<Value> {
336 let input_simulator = Arc::clone(&self.input_simulator);
337
338 let runtime = tokio::runtime::Handle::current();
339 let result = runtime.block_on(async {
340 input_simulator.send_keyboard_input(&process_id, &keys).await
341 });
342
343 match result {
344 Ok(()) => Ok(json!({
345 "status": "sent"
346 })),
347 Err(e) => Err(RpcError::invalid_params(e.to_string())),
348 }
349 }
350
351 fn send_mouse_click(&self, process_id: String, x: i32, y: i32, button: Option<String>) -> jsonrpc_core::Result<Value> {
352 let input_simulator = Arc::clone(&self.input_simulator);
353 let button = button.unwrap_or_else(|| "left".to_string());
354
355 let runtime = tokio::runtime::Handle::current();
356 let result = runtime.block_on(async {
357 input_simulator.send_mouse_click(&process_id, x, y, &button).await
358 });
359
360 match result {
361 Ok(()) => Ok(json!({
362 "status": "clicked"
363 })),
364 Err(e) => Err(RpcError::invalid_params(e.to_string())),
365 }
366 }
367
368 fn execute_js(&self, process_id: String, javascript_code: String) -> jsonrpc_core::Result<Value> {
369 let debug_tools = Arc::clone(&self.debug_tools);
370
371 let runtime = tokio::runtime::Handle::current();
372 let result = runtime.block_on(async {
373 debug_tools.execute_js(&process_id, &javascript_code).await
374 });
375
376 match result {
377 Ok(result) => Ok(json!({
378 "result": result
379 })),
380 Err(e) => Err(RpcError::invalid_params(e.to_string())),
381 }
382 }
383
384 fn get_devtools_info(&self, process_id: String) -> jsonrpc_core::Result<Value> {
385 let debug_tools = Arc::clone(&self.debug_tools);
386
387 let runtime = tokio::runtime::Handle::current();
388 let result = runtime.block_on(async {
389 debug_tools.get_devtools_info(&process_id).await
390 });
391
392 match result {
393 Ok(info) => Ok(info),
394 Err(e) => Err(RpcError::invalid_params(e.to_string())),
395 }
396 }
397
398 fn monitor_resources(&self, process_id: String) -> jsonrpc_core::Result<Value> {
399 let process_manager = Arc::clone(&self.process_manager);
400
401 let runtime = tokio::runtime::Handle::current();
402 let result = runtime.block_on(async {
403 let manager = process_manager.read().await;
404 manager.monitor_resources(&process_id).await
405 });
406
407 match result {
408 Ok(resources) => Ok(resources),
409 Err(e) => Err(RpcError::invalid_params(e.to_string())),
410 }
411 }
412
413 fn list_ipc_handlers(&self, process_id: String) -> jsonrpc_core::Result<Value> {
414 let ipc_manager = Arc::clone(&self.ipc_manager);
415
416 let runtime = tokio::runtime::Handle::current();
417 let result = runtime.block_on(async {
418 ipc_manager.list_ipc_handlers(&process_id).await
419 });
420
421 match result {
422 Ok(handlers) => Ok(json!({
423 "handlers": handlers
424 })),
425 Err(e) => Err(RpcError::invalid_params(e.to_string())),
426 }
427 }
428
429 fn call_ipc_command(&self, process_id: String, command_name: String, args: Option<Value>) -> jsonrpc_core::Result<Value> {
430 let ipc_manager = Arc::clone(&self.ipc_manager);
431 let args = args.unwrap_or(Value::Null);
432
433 let runtime = tokio::runtime::Handle::current();
434 let result = runtime.block_on(async {
435 ipc_manager.call_ipc_command(&process_id, &command_name, args).await
436 });
437
438 match result {
439 Ok(result) => Ok(result),
440 Err(e) => Err(RpcError::invalid_params(e.to_string())),
441 }
442 }
443
444 fn list_tools(&self) -> jsonrpc_core::Result<Value> {
445 Ok(json!({
446 "tools": [
447 {
448 "name": "launch_app",
449 "description": "Launch a Tauri application",
450 "inputSchema": {
451 "type": "object",
452 "properties": {
453 "app_path": { "type": "string", "description": "Path to the Tauri application" },
454 "args": { "type": "array", "items": { "type": "string" }, "description": "Optional launch arguments" }
455 },
456 "required": ["app_path"]
457 }
458 },
459 {
460 "name": "stop_app",
461 "description": "Stop a running Tauri application",
462 "inputSchema": {
463 "type": "object",
464 "properties": {
465 "process_id": { "type": "string", "description": "Process ID of the app to stop" }
466 },
467 "required": ["process_id"]
468 }
469 },
470 {
471 "name": "get_app_logs",
472 "description": "Get stdout/stderr logs from a running app",
473 "inputSchema": {
474 "type": "object",
475 "properties": {
476 "process_id": { "type": "string", "description": "Process ID of the app" },
477 "lines": { "type": "number", "description": "Number of recent lines to return" }
478 },
479 "required": ["process_id"]
480 }
481 },
482 {
483 "name": "take_screenshot",
484 "description": "Take a screenshot of the app window",
485 "inputSchema": {
486 "type": "object",
487 "properties": {
488 "process_id": { "type": "string", "description": "Process ID of the app" },
489 "output_path": { "type": "string", "description": "Optional path to save the screenshot" }
490 },
491 "required": ["process_id"]
492 }
493 },
494 {
495 "name": "get_window_info",
496 "description": "Get window dimensions, position, and state",
497 "inputSchema": {
498 "type": "object",
499 "properties": {
500 "process_id": { "type": "string", "description": "Process ID of the app" }
501 },
502 "required": ["process_id"]
503 }
504 },
505 {
506 "name": "send_keyboard_input",
507 "description": "Send keyboard input to the app",
508 "inputSchema": {
509 "type": "object",
510 "properties": {
511 "process_id": { "type": "string", "description": "Process ID of the app" },
512 "keys": { "type": "string", "description": "Keys to send" }
513 },
514 "required": ["process_id", "keys"]
515 }
516 },
517 {
518 "name": "send_mouse_click",
519 "description": "Send mouse click to specific coordinates",
520 "inputSchema": {
521 "type": "object",
522 "properties": {
523 "process_id": { "type": "string", "description": "Process ID of the app" },
524 "x": { "type": "number", "description": "X coordinate" },
525 "y": { "type": "number", "description": "Y coordinate" },
526 "button": { "type": "string", "enum": ["left", "right", "middle"], "description": "Mouse button" }
527 },
528 "required": ["process_id", "x", "y"]
529 }
530 },
531 {
532 "name": "execute_js",
533 "description": "Execute JavaScript in the app's webview",
534 "inputSchema": {
535 "type": "object",
536 "properties": {
537 "process_id": { "type": "string", "description": "Process ID of the app" },
538 "javascript_code": { "type": "string", "description": "JavaScript code to execute" }
539 },
540 "required": ["process_id", "javascript_code"]
541 }
542 },
543 {
544 "name": "get_devtools_info",
545 "description": "Get DevTools connection information",
546 "inputSchema": {
547 "type": "object",
548 "properties": {
549 "process_id": { "type": "string", "description": "Process ID of the app" }
550 },
551 "required": ["process_id"]
552 }
553 },
554 {
555 "name": "monitor_resources",
556 "description": "Monitor CPU, memory, and other resource usage",
557 "inputSchema": {
558 "type": "object",
559 "properties": {
560 "process_id": { "type": "string", "description": "Process ID of the app" }
561 },
562 "required": ["process_id"]
563 }
564 },
565 {
566 "name": "list_ipc_handlers",
567 "description": "List all registered Tauri IPC commands",
568 "inputSchema": {
569 "type": "object",
570 "properties": {
571 "process_id": { "type": "string", "description": "Process ID of the app" }
572 },
573 "required": ["process_id"]
574 }
575 },
576 {
577 "name": "call_ipc_command",
578 "description": "Call a Tauri IPC command",
579 "inputSchema": {
580 "type": "object",
581 "properties": {
582 "process_id": { "type": "string", "description": "Process ID of the app" },
583 "command_name": { "type": "string", "description": "Name of the IPC command" },
584 "args": { "type": "object", "description": "Arguments to pass to the command" }
585 },
586 "required": ["process_id", "command_name"]
587 }
588 }
589 ]
590 }))
591 }
592
593 fn call_tool(&self, params: Value) -> jsonrpc_core::Result<Value> {
594 let tool_name = params.get("name")
595 .and_then(|v| v.as_str())
596 .ok_or_else(|| RpcError::invalid_params("Missing tool name"))?;
597
598 let arguments = params.get("arguments")
599 .cloned()
600 .unwrap_or(json!({}));
601
602 match tool_name {
603 "launch_app" => {
604 let app_path = arguments.get("app_path")
605 .and_then(|v| v.as_str())
606 .ok_or_else(|| RpcError::invalid_params("Missing app_path"))?
607 .to_string();
608
609 let args = arguments.get("args")
610 .and_then(|v| v.as_array())
611 .map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect());
612
613 self.launch_app(app_path, args)
614 },
615 "stop_app" => {
616 let process_id = arguments.get("process_id")
617 .and_then(|v| v.as_str())
618 .ok_or_else(|| RpcError::invalid_params("Missing process_id"))?
619 .to_string();
620
621 self.stop_app(process_id)
622 },
623 "get_app_logs" => {
624 let process_id = arguments.get("process_id")
625 .and_then(|v| v.as_str())
626 .ok_or_else(|| RpcError::invalid_params("Missing process_id"))?
627 .to_string();
628
629 let lines = arguments.get("lines")
630 .and_then(|v| v.as_u64())
631 .map(|n| n as usize);
632
633 self.get_app_logs(process_id, lines)
634 },
635 "take_screenshot" => {
636 let process_id = arguments.get("process_id")
637 .and_then(|v| v.as_str())
638 .ok_or_else(|| RpcError::invalid_params("Missing process_id"))?
639 .to_string();
640
641 let output_path = arguments.get("output_path")
642 .and_then(|v| v.as_str())
643 .map(String::from);
644
645 self.take_screenshot(process_id, output_path)
646 },
647 "get_window_info" => {
648 let process_id = arguments.get("process_id")
649 .and_then(|v| v.as_str())
650 .ok_or_else(|| RpcError::invalid_params("Missing process_id"))?
651 .to_string();
652
653 self.get_window_info(process_id)
654 },
655 "send_keyboard_input" => {
656 let process_id = arguments.get("process_id")
657 .and_then(|v| v.as_str())
658 .ok_or_else(|| RpcError::invalid_params("Missing process_id"))?
659 .to_string();
660
661 let keys = arguments.get("keys")
662 .and_then(|v| v.as_str())
663 .ok_or_else(|| RpcError::invalid_params("Missing keys"))?
664 .to_string();
665
666 self.send_keyboard_input(process_id, keys)
667 },
668 "send_mouse_click" => {
669 let process_id = arguments.get("process_id")
670 .and_then(|v| v.as_str())
671 .ok_or_else(|| RpcError::invalid_params("Missing process_id"))?
672 .to_string();
673
674 let x = arguments.get("x")
675 .and_then(|v| v.as_i64())
676 .ok_or_else(|| RpcError::invalid_params("Missing x coordinate"))? as i32;
677
678 let y = arguments.get("y")
679 .and_then(|v| v.as_i64())
680 .ok_or_else(|| RpcError::invalid_params("Missing y coordinate"))? as i32;
681
682 let button = arguments.get("button")
683 .and_then(|v| v.as_str())
684 .map(String::from);
685
686 self.send_mouse_click(process_id, x, y, button)
687 },
688 "execute_js" => {
689 let process_id = arguments.get("process_id")
690 .and_then(|v| v.as_str())
691 .ok_or_else(|| RpcError::invalid_params("Missing process_id"))?
692 .to_string();
693
694 let javascript_code = arguments.get("javascript_code")
695 .and_then(|v| v.as_str())
696 .ok_or_else(|| RpcError::invalid_params("Missing javascript_code"))?
697 .to_string();
698
699 self.execute_js(process_id, javascript_code)
700 },
701 "get_devtools_info" => {
702 let process_id = arguments.get("process_id")
703 .and_then(|v| v.as_str())
704 .ok_or_else(|| RpcError::invalid_params("Missing process_id"))?
705 .to_string();
706
707 self.get_devtools_info(process_id)
708 },
709 "monitor_resources" => {
710 let process_id = arguments.get("process_id")
711 .and_then(|v| v.as_str())
712 .ok_or_else(|| RpcError::invalid_params("Missing process_id"))?
713 .to_string();
714
715 self.monitor_resources(process_id)
716 },
717 "list_ipc_handlers" => {
718 let process_id = arguments.get("process_id")
719 .and_then(|v| v.as_str())
720 .ok_or_else(|| RpcError::invalid_params("Missing process_id"))?
721 .to_string();
722
723 self.list_ipc_handlers(process_id)
724 },
725 "call_ipc_command" => {
726 let process_id = arguments.get("process_id")
727 .and_then(|v| v.as_str())
728 .ok_or_else(|| RpcError::invalid_params("Missing process_id"))?
729 .to_string();
730
731 let command_name = arguments.get("command_name")
732 .and_then(|v| v.as_str())
733 .ok_or_else(|| RpcError::invalid_params("Missing command_name"))?
734 .to_string();
735
736 let args = arguments.get("args").cloned();
737
738 self.call_ipc_command(process_id, command_name, args)
739 },
740 _ => Err(RpcError::method_not_found())
741 }
742 }
743}