@@ -10,6 +10,7 @@ use data_studio_agent::capabilities::types::{Capability, RiskLevel};
1010use serde:: { Deserialize , Serialize } ;
1111use serde_json:: { json, Value } ;
1212use tauri:: { AppHandle , Manager } ;
13+ use tauri_plugin_store:: StoreExt ;
1314use tokio:: net:: TcpListener ;
1415use tokio:: sync:: oneshot;
1516
@@ -54,7 +55,10 @@ impl McpConfig {
5455 Ok ( s) => match serde_json:: from_str ( & s) {
5556 Ok ( cfg) => cfg,
5657 Err ( e) => {
57- log:: warn!( "Failed to parse mcp-config.json (corrupt?): {}. Using defaults." , e) ;
58+ log:: warn!(
59+ "Failed to parse mcp-config.json (corrupt?): {}. Using defaults." ,
60+ e
61+ ) ;
5862 McpConfig :: default ( )
5963 }
6064 } ,
@@ -130,9 +134,7 @@ struct BridgeState {
130134// Handlers
131135// ---------------------------------------------------------------------------
132136
133- async fn handle_tools (
134- State ( _state) : State < Arc < BridgeState > > ,
135- ) -> Json < Value > {
137+ async fn handle_tools ( State ( _state) : State < Arc < BridgeState > > ) -> Json < Value > {
136138 let reg = registry:: registry ( ) ;
137139 let caps = reg. agent_tools ( ) ;
138140
@@ -145,14 +147,42 @@ async fn handle_tools(
145147 let result = json ! ( {
146148 "tools" : openai_tools,
147149 "metadata" : metadata,
148- "connections" : [ ] ,
150+ "connections" : list_connections ( ) ,
149151 } ) ;
150152 Json ( result)
151153}
152154
153- async fn handle_invoke (
154- Json ( payload) : Json < InvokeRequest > ,
155- ) -> Json < InvokeResponse > {
155+ /// List saved connections from the store — id/name/db_type only, no credentials.
156+ fn list_connections ( ) -> Value {
157+ let handle = match crate :: APP_HANDLE . get ( ) {
158+ Some ( h) => h,
159+ None => return json ! ( [ ] ) ,
160+ } ;
161+ let store = match handle. store ( ".store.dat" ) {
162+ Ok ( s) => s,
163+ Err ( _) => return json ! ( [ ] ) ,
164+ } ;
165+
166+ let connections = store. get ( "connections" ) . unwrap_or ( json ! ( [ ] ) ) ;
167+ let safe_list: Vec < Value > = connections
168+ . as_array ( )
169+ . map ( |arr| {
170+ arr. iter ( )
171+ . map ( |c| {
172+ json ! ( {
173+ "id" : c. get( "id" ) ,
174+ "name" : c. get( "name" ) ,
175+ "type" : c. get( "db_type" ) ,
176+ } )
177+ } )
178+ . collect ( )
179+ } )
180+ . unwrap_or_default ( ) ;
181+
182+ json ! ( safe_list)
183+ }
184+
185+ async fn handle_invoke ( Json ( payload) : Json < InvokeRequest > ) -> Json < InvokeResponse > {
156186 // Reject destructive and elevated capabilities on the bridge
157187 let cap = match registry:: registry ( ) . get ( & payload. name ) {
158188 Some ( c) => c,
@@ -195,9 +225,7 @@ async fn handle_invoke(
195225 }
196226}
197227
198- async fn handle_health (
199- State ( state) : State < Arc < BridgeState > > ,
200- ) -> Json < Value > {
228+ async fn handle_health ( State ( state) : State < Arc < BridgeState > > ) -> Json < Value > {
201229 Json ( json ! ( {
202230 "status" : "ok" ,
203231 "app" : state. app_name,
@@ -454,3 +482,134 @@ pub async fn save_mcp_config(
454482
455483 Ok ( serde_json:: to_string ( & json ! ( { "status" : "ok" } ) ) . map_err ( |e| e. to_string ( ) ) ?)
456484}
485+
486+ #[ cfg( test) ]
487+ mod tests {
488+ use super :: * ;
489+ use data_studio_agent:: capabilities:: types:: RiskLevel ;
490+
491+ fn init_registry_for_tests ( ) {
492+ // OnceLock set-once: subsequent calls are no-ops, safe to call in every test
493+ data_studio_agent:: capabilities:: registry:: init_registry ( & [
494+ crate :: capabilities:: sqlkit:: register_all,
495+ crate :: capabilities:: sql:: register_sql_tools,
496+ ] ) ;
497+ }
498+
499+ #[ test]
500+ fn test_default_port_is_9121 ( ) {
501+ assert_eq ! ( get_default_port( ) , 9121 ) ;
502+ }
503+
504+ #[ test]
505+ fn test_mcp_config_default ( ) {
506+ let cfg = McpConfig :: default ( ) ;
507+ assert_eq ! ( cfg. port, None ) ;
508+ assert ! ( cfg. auto_start) ;
509+ }
510+
511+ #[ test]
512+ fn test_mcp_config_save_and_load_roundtrip ( ) {
513+ let dir = std:: env:: temp_dir ( ) . join ( format ! (
514+ "sqlkit-mcp-test-{}-config-roundtrip" ,
515+ std:: process:: id( )
516+ ) ) ;
517+ let _ = std:: fs:: remove_dir_all ( & dir) ;
518+ std:: fs:: create_dir_all ( & dir) . unwrap ( ) ;
519+
520+ let cfg = McpConfig {
521+ port : Some ( 9444 ) ,
522+ auto_start : false ,
523+ } ;
524+ cfg. save ( & dir) . unwrap ( ) ;
525+
526+ let loaded = McpConfig :: load ( & dir) ;
527+ assert_eq ! ( loaded. port, Some ( 9444 ) ) ;
528+ assert ! ( !loaded. auto_start) ;
529+ std:: fs:: remove_dir_all ( & dir) . unwrap ( ) ;
530+ }
531+
532+ #[ test]
533+ fn test_mcp_config_load_corrupt_file_uses_default ( ) {
534+ let dir = std:: env:: temp_dir ( ) . join ( format ! (
535+ "sqlkit-mcp-test-{}-config-corrupt" ,
536+ std:: process:: id( )
537+ ) ) ;
538+ let _ = std:: fs:: remove_dir_all ( & dir) ;
539+ std:: fs:: create_dir_all ( & dir) . unwrap ( ) ;
540+ std:: fs:: write ( dir. join ( "mcp-config.json" ) , "{ not valid json" ) . unwrap ( ) ;
541+
542+ let cfg = McpConfig :: load ( & dir) ;
543+ assert_eq ! ( cfg. port, None ) ;
544+ assert ! ( cfg. auto_start) ;
545+ std:: fs:: remove_dir_all ( & dir) . unwrap ( ) ;
546+ }
547+
548+ #[ test]
549+ fn test_list_connections_empty_without_app_handle ( ) {
550+ assert_eq ! ( list_connections( ) , json!( [ ] ) ) ;
551+ }
552+
553+ #[ test]
554+ fn test_invoke_response_ok_serialization ( ) {
555+ let resp = InvokeResponse :: ok ( json ! ( { "rows" : 1 } ) ) ;
556+ let v = serde_json:: to_value ( & resp) . unwrap ( ) ;
557+ assert_eq ! ( v[ "status" ] , 200 ) ;
558+ assert_eq ! ( v[ "data" ] [ "rows" ] , 1 ) ;
559+ assert ! ( v. get( "message" ) . is_none( ) ) ;
560+ }
561+
562+ #[ test]
563+ fn test_invoke_response_error_serialization ( ) {
564+ let resp = InvokeResponse :: error ( 403 , "forbidden" . into ( ) ) ;
565+ let v = serde_json:: to_value ( & resp) . unwrap ( ) ;
566+ assert_eq ! ( v[ "status" ] , 403 ) ;
567+ assert_eq ! ( v[ "message" ] , "forbidden" ) ;
568+ assert ! ( v. get( "data" ) . is_none( ) ) ;
569+ }
570+
571+ #[ test]
572+ fn test_handle_invoke_unknown_capability_returns_404 ( ) {
573+ init_registry_for_tests ( ) ;
574+ let req = InvokeRequest {
575+ name : "definitely__not_a_real_capability" . into ( ) ,
576+ args : json ! ( { } ) ,
577+ connection_id : None ,
578+ } ;
579+
580+ let rt = tokio:: runtime:: Runtime :: new ( ) . unwrap ( ) ;
581+ let resp = rt. block_on ( handle_invoke ( Json ( req) ) ) . 0 ;
582+
583+ assert_eq ! ( resp. status, 404 ) ;
584+ assert ! ( resp. message. unwrap( ) . contains( "Unknown capability" ) ) ;
585+ }
586+
587+ #[ test]
588+ fn test_handle_invoke_rejects_elevated_and_destructive ( ) {
589+ init_registry_for_tests ( ) ;
590+ let tools = registry:: registry ( ) . agent_tools ( ) ;
591+ // Concurrent tests may initialize the global registry (OnceLock) with
592+ // test-only Safe capabilities; only assert when the full app registry is present.
593+ let Some ( risky) = tools
594+ . iter ( )
595+ . find ( |c| !matches ! ( c. risk_level, RiskLevel :: Safe ) )
596+ else {
597+ return ;
598+ } ;
599+
600+ let req = InvokeRequest {
601+ name : risky. name . to_string ( ) ,
602+ args : json ! ( { } ) ,
603+ connection_id : None ,
604+ } ;
605+
606+ let rt = tokio:: runtime:: Runtime :: new ( ) . unwrap ( ) ;
607+ let resp = rt. block_on ( handle_invoke ( Json ( req) ) ) . 0 ;
608+
609+ assert_eq ! ( resp. status, 403 ) ;
610+ assert ! ( resp
611+ . message
612+ . unwrap( )
613+ . contains( "not allowed through the MCP bridge" ) ) ;
614+ }
615+ }
0 commit comments