tauri_plugin_mcp/
lib.rs

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    // client.waiting().await.unwrap();
42  });
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  // state.client.take().unwrap().waiting().await.unwrap();
75  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(); // TODO: handle error
91  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}