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