1- //! Embedded HTTP bridge for MCP protocol.
2- //!
3- //! Exposes the capability system over HTTP so the external TypeScript
4- //! MCP server (`data-studio-mcp`) can invoke tools and list capabilities.
5- //!
6- //! Only binds to 127.0.0.1 — not reachable from other machines.
7-
81use std:: path:: { Path , PathBuf } ;
92use std:: sync:: Arc ;
3+ use std:: sync:: Mutex ;
104
115use axum:: extract:: State ;
126use axum:: routing:: { get, post} ;
137use axum:: Json ;
148use data_studio_agent:: capabilities:: registry;
15- use data_studio_agent:: capabilities:: types:: Capability ;
9+ use data_studio_agent:: capabilities:: types:: { Capability , RiskLevel } ;
1610use serde:: { Deserialize , Serialize } ;
1711use serde_json:: { json, Value } ;
18- use std:: sync:: Mutex ;
1912use tauri:: { AppHandle , Manager } ;
2013use tokio:: net:: TcpListener ;
2114use tokio:: sync:: oneshot;
@@ -55,10 +48,16 @@ fn default_auto_start() -> bool {
5548impl McpConfig {
5649 pub fn load ( app_data_dir : & Path ) -> Self {
5750 let path = app_data_dir. join ( "mcp-config.json" ) ;
58- std:: fs:: read_to_string ( path)
59- . ok ( )
60- . and_then ( |s| serde_json:: from_str ( & s) . ok ( ) )
61- . unwrap_or_default ( )
51+ match std:: fs:: read_to_string ( & path) {
52+ Ok ( s) => match serde_json:: from_str ( & s) {
53+ Ok ( cfg) => cfg,
54+ Err ( e) => {
55+ log:: warn!( "Failed to parse mcp-config.json (corrupt?): {}. Using defaults." , e) ;
56+ McpConfig :: default ( )
57+ }
58+ } ,
59+ Err ( _) => McpConfig :: default ( ) ,
60+ }
6261 }
6362
6463 pub fn save ( & self , app_data_dir : & Path ) -> Result < ( ) , String > {
@@ -122,7 +121,6 @@ impl InvokeResponse {
122121struct BridgeState {
123122 handle : AppHandle ,
124123 app_name : & ' static str ,
125- app_version : & ' static str ,
126124 app_data_dir : PathBuf ,
127125}
128126
@@ -147,16 +145,39 @@ async fn handle_tools(
147145 "metadata" : metadata,
148146 "connections" : [ ] ,
149147 } ) ;
150-
151148 Json ( result)
152149}
153150
154151async fn handle_invoke (
155- State ( state) : State < Arc < BridgeState > > ,
156152 Json ( payload) : Json < InvokeRequest > ,
157153) -> Json < InvokeResponse > {
154+ // Reject destructive and elevated capabilities on the bridge
155+ let cap = match registry:: registry ( ) . get ( & payload. name ) {
156+ Some ( c) => c,
157+ None => {
158+ return Json ( InvokeResponse :: error (
159+ 404 ,
160+ format ! ( "Unknown capability: {}" , payload. name) ,
161+ ) )
162+ }
163+ } ;
164+
165+ match cap. risk_level {
166+ RiskLevel :: Safe => { }
167+ RiskLevel :: Elevated | RiskLevel :: Destructive => {
168+ let level_str = serde_json:: to_string ( & cap. risk_level ) . unwrap_or_default ( ) ;
169+ return Json ( InvokeResponse :: error (
170+ 403 ,
171+ format ! (
172+ "Capability '{}' requires {} permission and is not allowed through the MCP bridge" ,
173+ payload. name, level_str
174+ ) ,
175+ ) ) ;
176+ }
177+ }
178+
158179 let config = match payload. connection_id {
159- Some ( ref id) => match resolve_connection ( & state . handle , id) . await {
180+ Some ( ref id) => match resolve_connection ( id) . await {
160181 Ok ( cfg) => Some ( cfg) ,
161182 Err ( e) => return Json ( InvokeResponse :: error ( 400 , e) ) ,
162183 } ,
@@ -178,7 +199,7 @@ async fn handle_health(
178199 Json ( json ! ( {
179200 "status" : "ok" ,
180201 "app" : state. app_name,
181- "version" : state. app_version ,
202+ "version" : state. handle . package_info ( ) . version . to_string ( ) ,
182203 "port" : get_actual_port( & state. app_data_dir) . unwrap_or( 0 ) ,
183204 } ) )
184205}
@@ -191,19 +212,24 @@ fn get_default_port() -> u16 {
191212 9121
192213}
193214
194- fn port_available ( port : u16 ) -> bool {
195- std:: net:: TcpListener :: bind ( std:: net:: SocketAddrV4 :: new (
196- std:: net:: Ipv4Addr :: LOCALHOST ,
197- port,
198- ) )
199- . is_ok ( )
200- }
201-
202215fn get_actual_port ( app_data_dir : & Path ) -> Option < u16 > {
203216 let path = app_data_dir. join ( "mcp-port" ) ;
204- std:: fs:: read_to_string ( path)
217+ let port = std:: fs:: read_to_string ( & path)
205218 . ok ( )
206- . and_then ( |s| s. trim ( ) . parse :: < u16 > ( ) . ok ( ) )
219+ . and_then ( |s| s. trim ( ) . parse :: < u16 > ( ) . ok ( ) ) ?;
220+
221+ let addr = std:: net:: SocketAddrV4 :: new ( std:: net:: Ipv4Addr :: LOCALHOST , port) ;
222+ if std:: net:: TcpStream :: connect_timeout (
223+ & std:: net:: SocketAddr :: V4 ( addr) ,
224+ std:: time:: Duration :: from_millis ( 200 ) ,
225+ )
226+ . is_ok ( )
227+ {
228+ Some ( port)
229+ } else {
230+ let _ = std:: fs:: remove_file ( & path) ;
231+ None
232+ }
207233}
208234
209235async fn write_port_file ( app_data_dir : & Path , port : u16 ) -> Result < ( ) , String > {
@@ -222,55 +248,97 @@ async fn remove_port_file(app_data_dir: &Path) {
222248 let _ = tokio:: fs:: remove_file ( path) . await ;
223249}
224250
251+ async fn send_shutdown ( handle : & AppHandle ) {
252+ let server_handle: tauri:: State < ' _ , McpServerHandle > = handle. state ( ) ;
253+ let old_tx = {
254+ let mut tx = server_handle. shutdown_tx . lock ( ) . unwrap ( ) ;
255+ tx. take ( )
256+ } ;
257+ if let Some ( sender) = old_tx {
258+ let _ = sender. send ( ( ) ) ;
259+ }
260+ }
261+
225262pub async fn start (
226263 handle : AppHandle ,
227264 app_data_dir : PathBuf ,
228265 preferred_port : u16 ,
229266 shutdown_rx : oneshot:: Receiver < ( ) > ,
230267) -> Result < u16 , String > {
231- let port = if port_available ( preferred_port) {
232- preferred_port
233- } else {
234- log:: warn!(
235- "MCP bridge port {} is in use, picking random port" ,
236- preferred_port
237- ) ;
238- portpicker:: pick_unused_port ( ) . ok_or ( "no port available" ) ?
268+ let port = match TcpListener :: bind ( format ! ( "127.0.0.1:{}" , preferred_port) ) . await {
269+ Ok ( listener) => {
270+ let state = Arc :: new ( BridgeState {
271+ handle : handle. clone ( ) ,
272+ app_name : "sqlkit" ,
273+ app_data_dir : app_data_dir. clone ( ) ,
274+ } ) ;
275+
276+ let app = axum:: Router :: new ( )
277+ . route ( "/tools" , post ( handle_tools) )
278+ . route ( "/invoke" , post ( handle_invoke) )
279+ . route ( "/health" , get ( handle_health) )
280+ . with_state ( state) ;
281+
282+ write_port_file ( & app_data_dir, preferred_port) . await ?;
283+
284+ let data_dir = app_data_dir. clone ( ) ;
285+ tokio:: spawn ( async move {
286+ log:: info!( "MCP bridge listening on 127.0.0.1:{}" , preferred_port) ;
287+ axum:: serve ( listener, app)
288+ . with_graceful_shutdown ( async {
289+ shutdown_rx. await . ok ( ) ;
290+ log:: info!( "MCP bridge shutting down" ) ;
291+ } )
292+ . await
293+ . ok ( ) ;
294+ let _ = remove_port_file ( & data_dir) . await ;
295+ } ) ;
296+
297+ Ok ( preferred_port)
298+ }
299+ Err ( _) => {
300+ log:: warn!(
301+ "MCP bridge port {} is in use, picking random port" ,
302+ preferred_port
303+ ) ;
304+ let random_port =
305+ portpicker:: pick_unused_port ( ) . ok_or ( "no port available on localhost" ) ?;
306+ let listener = TcpListener :: bind ( format ! ( "127.0.0.1:{}" , random_port) )
307+ . await
308+ . map_err ( |e| format ! ( "Failed to bind bridge: {}" , e) ) ?;
309+
310+ let state = Arc :: new ( BridgeState {
311+ handle : handle. clone ( ) ,
312+ app_name : "sqlkit" ,
313+ app_data_dir : app_data_dir. clone ( ) ,
314+ } ) ;
315+
316+ let app = axum:: Router :: new ( )
317+ . route ( "/tools" , post ( handle_tools) )
318+ . route ( "/invoke" , post ( handle_invoke) )
319+ . route ( "/health" , get ( handle_health) )
320+ . with_state ( state) ;
321+
322+ write_port_file ( & app_data_dir, random_port) . await ?;
323+
324+ let data_dir = app_data_dir. clone ( ) ;
325+ tokio:: spawn ( async move {
326+ log:: info!( "MCP bridge listening on 127.0.0.1:{}" , random_port) ;
327+ axum:: serve ( listener, app)
328+ . with_graceful_shutdown ( async {
329+ shutdown_rx. await . ok ( ) ;
330+ log:: info!( "MCP bridge shutting down" ) ;
331+ } )
332+ . await
333+ . ok ( ) ;
334+ let _ = remove_port_file ( & data_dir) . await ;
335+ } ) ;
336+
337+ Ok ( random_port)
338+ }
239339 } ;
240340
241- let listener = TcpListener :: bind ( format ! ( "127.0.0.1:{}" , port) )
242- . await
243- . map_err ( |e| format ! ( "Failed to bind bridge: {}" , e) ) ?;
244-
245- let state = Arc :: new ( BridgeState {
246- handle : handle. clone ( ) ,
247- app_name : "sqlkit" ,
248- app_version : env ! ( "CARGO_PKG_VERSION" ) ,
249- app_data_dir : app_data_dir. clone ( ) ,
250- } ) ;
251-
252- let app = axum:: Router :: new ( )
253- . route ( "/tools" , post ( handle_tools) )
254- . route ( "/invoke" , post ( handle_invoke) )
255- . route ( "/health" , get ( handle_health) )
256- . with_state ( state) ;
257-
258- let data_dir = app_data_dir. clone ( ) ;
259-
260- tokio:: spawn ( async move {
261- log:: info!( "MCP bridge listening on 127.0.0.1:{}" , port) ;
262- axum:: serve ( listener, app)
263- . with_graceful_shutdown ( async {
264- shutdown_rx. await . ok ( ) ;
265- log:: info!( "MCP bridge shutting down" ) ;
266- } )
267- . await
268- . ok ( ) ;
269- let _ = remove_port_file ( & data_dir) . await ;
270- } ) ;
271-
272- write_port_file ( & app_data_dir, port) . await ?;
273- Ok ( port)
341+ port
274342}
275343
276344// ---------------------------------------------------------------------------
@@ -295,7 +363,10 @@ fn to_metadata(cap: &Capability) -> Value {
295363 } )
296364}
297365
298- async fn resolve_connection ( handle : & AppHandle , connection_id : & str ) -> Result < Value , String > {
366+ async fn resolve_connection ( connection_id : & str ) -> Result < Value , String > {
367+ let handle = crate :: APP_HANDLE
368+ . get ( )
369+ . ok_or_else ( || "AppHandle not initialized" . to_string ( ) ) ?;
299370 use tauri:: State ;
300371 let state: State < ' _ , crate :: state:: AppState > = handle. state ( ) ;
301372 let conns = state. connections . read ( ) . await ;
@@ -344,19 +415,14 @@ pub async fn save_mcp_config(
344415 let config = McpConfig { port, auto_start } ;
345416 config. save ( & app_data_dir) ?;
346417
418+ send_shutdown ( & app) . await ;
419+
347420 if auto_start {
348- let server_handle: tauri:: State < ' _ , McpServerHandle > = app. state ( ) ;
349- let old_tx = {
350- let mut tx = server_handle. shutdown_tx . lock ( ) . unwrap ( ) ;
351- tx. take ( )
352- } ;
353- if let Some ( sender) = old_tx {
354- let _ = sender. send ( ( ) ) ;
355- tokio:: time:: sleep ( std:: time:: Duration :: from_millis ( 200 ) ) . await ;
356- }
421+ tokio:: time:: sleep ( std:: time:: Duration :: from_millis ( 200 ) ) . await ;
357422
358423 let ( new_shutdown_tx, new_shutdown_rx) = oneshot:: channel ( ) ;
359424 {
425+ let server_handle: tauri:: State < ' _ , McpServerHandle > = app. state ( ) ;
360426 let mut tx = server_handle. shutdown_tx . lock ( ) . unwrap ( ) ;
361427 * tx = Some ( new_shutdown_tx) ;
362428 }
0 commit comments