tauri_plugin_mcp/
lib.rs

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