1use std::process::Stdio;
2
3use rmcp::{
4 model::{CallToolRequestParam, CallToolResult, Tool},
5 service::RunningService,
6 transport::TokioChildProcess,
7 RoleClient,
8 ServiceExt,
9};
10use serde_json::{Map, Value};
11use tauri::{
12 plugin::{self, TauriPlugin},
13 AppHandle,
14 Manager,
15 Runtime,
16 State,
17};
18use tokio::{process::Command, sync::Mutex};
19
20pub struct McpState {
21 pub client: Option<RunningService<RoleClient, ()>>,
22}
23
24#[allow(clippy::missing_panics_doc)]
25pub fn destroy<R: Runtime>(app_handle: &AppHandle<R>) {
26 println!("Destroying MCP plugin");
27
28 tokio::runtime::Runtime::new().unwrap().block_on(async {
29 let state = app_handle.state::<Mutex<McpState>>();
30
31 let mut state = state.lock().await;
32 if state.client.is_none() {
33 println!("MCP plugin not connected, no need to disconnect");
34 return;
35 }
36
37 let client = state.client.take().unwrap();
38 drop(state);
39
40 client.cancel().await.unwrap();
41 });
43
44 println!("MCP plugin destroyed");
45}
46
47#[tauri::command]
48async fn connect_server(state: State<'_, Mutex<McpState>>, command: String, args: Vec<String>) -> Result<(), String> {
49 let mut state = state.lock().await;
50
51 if state.client.is_some() {
52 return Err("Client already connected".to_string());
53 }
54
55 let child_process = TokioChildProcess::new(Command::new(command).args(args).stderr(Stdio::inherit()).stdout(Stdio::inherit())).unwrap();
56
57 let service: RunningService<RoleClient, ()> = ().serve(child_process).await.unwrap();
58
59 state.client = Some(service);
60 drop(state);
61
62 Ok(())
63}
64
65#[tauri::command]
66async fn disconnect_server(state: State<'_, Mutex<McpState>>) -> Result<(), String> {
67 let mut state = state.lock().await;
68 if state.client.is_none() {
69 return Err("Client not connected".to_string());
70 }
71
72 let cancel_result = state.client.take().unwrap().cancel().await;
73 println!("Cancel result: {cancel_result:?}");
74 drop(state);
76
77 println!("Disconnected from MCP server");
78
79 Ok(())
80}
81
82#[tauri::command]
83async fn list_tools(state: State<'_, Mutex<McpState>>) -> Result<Vec<Tool>, String> {
84 let state = state.lock().await;
85 let client = state.client.as_ref();
86 if client.is_none() {
87 return Err("Client not connected".to_string());
88 }
89
90 let list_tools_result = client.unwrap().list_tools(Option::default()).await.unwrap(); let tools = list_tools_result.tools;
92 drop(state);
93
94 Ok(tools)
95}
96
97#[tauri::command]
98async fn call_tool(state: State<'_, Mutex<McpState>>, name: String, args: Option<Map<String, Value>>) -> Result<CallToolResult, String> {
99 println!("Calling tool: {name:?}");
100 println!("Arguments: {args:?}");
101
102 let state = state.lock().await;
103 let client = state.client.as_ref();
104 if client.is_none() {
105 return Err("Client not connected".to_string());
106 }
107
108 let call_tool_result = client.unwrap().call_tool(CallToolRequestParam { name: name.into(), arguments: args }).await.unwrap();
109 drop(state);
110
111 println!("Tool result: {call_tool_result:?}");
112
113 Ok(call_tool_result)
114}
115
116#[derive(Default)]
117pub struct Builder;
118
119impl Builder {
120 #[must_use]
121 pub fn build<R: Runtime>(self) -> TauriPlugin<R> {
122 println!("Building MCP plugin");
123
124 plugin::Builder::new("mcp")
125 .invoke_handler(tauri::generate_handler![connect_server, disconnect_server, list_tools, call_tool])
126 .setup(|app_handle, _| {
127 app_handle.manage(Mutex::new(McpState { client: None }));
128 Ok(())
129 })
130 .on_drop(|app_handle: AppHandle<R>| {
131 destroy(&app_handle);
132 })
133 .build()
134 }
135}