@@ -535,6 +535,240 @@ async fn test_stdio_transport() -> Result<()> {
535535 Ok ( ( ) )
536536}
537537
538+ #[ test( tokio:: test) ]
539+ async fn test_tool_list_notification ( ) -> Result < ( ) > {
540+ // Create a temporary directory for this test to avoid loading existing components
541+ let temp_dir = tempfile:: tempdir ( ) ?;
542+ let plugin_dir_arg = format ! ( "--plugin-dir={}" , temp_dir. path( ) . display( ) ) ;
543+
544+ // Get the path to the built binary
545+ let binary_path = std:: env:: current_dir ( )
546+ . context ( "Failed to get current directory" ) ?
547+ . join ( "target/debug/wassette" ) ;
548+
549+ // Start the server with stdio transport (disable logs to avoid stdout pollution)
550+ let mut child = tokio:: process:: Command :: new ( & binary_path)
551+ . args ( [ "serve" , & plugin_dir_arg] )
552+ . env ( "RUST_LOG" , "off" )
553+ . stdin ( Stdio :: piped ( ) )
554+ . stdout ( Stdio :: piped ( ) )
555+ . stderr ( Stdio :: piped ( ) )
556+ . spawn ( )
557+ . context ( "Failed to start wassette with stdio transport" ) ?;
558+
559+ let stdin = child. stdin . take ( ) . context ( "Failed to get stdin handle" ) ?;
560+ let stdout = child. stdout . take ( ) . context ( "Failed to get stdout handle" ) ?;
561+ let stderr = child. stderr . take ( ) . context ( "Failed to get stderr handle" ) ?;
562+
563+ let mut stdin = stdin;
564+ let mut stdout = BufReader :: new ( stdout) ;
565+ let mut stderr = BufReader :: new ( stderr) ;
566+
567+ // Give the server time to start
568+ tokio:: time:: sleep ( Duration :: from_millis ( 1000 ) ) . await ;
569+
570+ // Check if the process is still running
571+ if let Ok ( Some ( status) ) = child. try_wait ( ) {
572+ let mut stderr_output = String :: new ( ) ;
573+ let _ = stderr. read_line ( & mut stderr_output) . await ;
574+ return Err ( anyhow:: anyhow!(
575+ "Server process exited with status: {:?}, stderr: {}" ,
576+ status,
577+ stderr_output
578+ ) ) ;
579+ }
580+
581+ // Send MCP initialize request
582+ let initialize_request = r#"{"jsonrpc": "2.0", "method": "initialize", "params": {"protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "test-client", "version": "1.0.0"}}, "id": 1}
583+ "# ;
584+
585+ stdin. write_all ( initialize_request. as_bytes ( ) ) . await ?;
586+ stdin. flush ( ) . await ?;
587+
588+ // Read and verify initialize response
589+ let mut response_line = String :: new ( ) ;
590+ match tokio:: time:: timeout (
591+ Duration :: from_secs ( 10 ) ,
592+ stdout. read_line ( & mut response_line) ,
593+ )
594+ . await
595+ {
596+ Ok ( Ok ( _) ) => { }
597+ Ok ( Err ( e) ) => {
598+ return Err ( anyhow:: anyhow!( "Failed to read initialize response: {}" , e) ) ;
599+ }
600+ Err ( _) => {
601+ let mut stderr_output = String :: new ( ) ;
602+ let _ =
603+ tokio:: time:: timeout ( Duration :: from_secs ( 1 ) , stderr. read_line ( & mut stderr_output) )
604+ . await ;
605+ return Err ( anyhow:: anyhow!(
606+ "Timeout waiting for initialize response. Stderr: {}" ,
607+ stderr_output
608+ ) ) ;
609+ }
610+ }
611+
612+ let response: serde_json:: Value =
613+ serde_json:: from_str ( & response_line) . context ( "Failed to parse initialize response" ) ?;
614+
615+ assert_eq ! ( response[ "jsonrpc" ] , "2.0" ) ;
616+ assert_eq ! ( response[ "id" ] , 1 ) ;
617+ assert ! ( response[ "result" ] . is_object( ) ) ;
618+
619+ // Send initialized notification (required by MCP protocol)
620+ let initialized_notification = r#"{"jsonrpc": "2.0", "method": "notifications/initialized", "params": {}}
621+ "# ;
622+
623+ stdin. write_all ( initialized_notification. as_bytes ( ) ) . await ?;
624+ stdin. flush ( ) . await ?;
625+
626+ // Step 1: Send initial list_tools request to get baseline tool count
627+ let list_tools_request = r#"{"jsonrpc": "2.0", "method": "tools/list", "params": {}, "id": 2}
628+ "# ;
629+
630+ stdin. write_all ( list_tools_request. as_bytes ( ) ) . await ?;
631+ stdin. flush ( ) . await ?;
632+
633+ // Read initial tools list response
634+ let mut tools_response_line = String :: new ( ) ;
635+ tokio:: time:: timeout (
636+ Duration :: from_secs ( 10 ) ,
637+ stdout. read_line ( & mut tools_response_line) ,
638+ )
639+ . await
640+ . context ( "Timeout waiting for initial tools/list response" ) ?
641+ . context ( "Failed to read initial tools/list response" ) ?;
642+
643+ let initial_tools_response: serde_json:: Value = serde_json:: from_str ( & tools_response_line)
644+ . context ( "Failed to parse initial tools/list response" ) ?;
645+
646+ assert_eq ! ( initial_tools_response[ "jsonrpc" ] , "2.0" ) ;
647+ assert_eq ! ( initial_tools_response[ "id" ] , 2 ) ;
648+ assert ! ( initial_tools_response[ "result" ] . is_object( ) ) ;
649+ assert ! ( initial_tools_response[ "result" ] [ "tools" ] . is_array( ) ) ;
650+
651+ let initial_tools = & initial_tools_response[ "result" ] [ "tools" ]
652+ . as_array ( )
653+ . unwrap ( ) ;
654+ let initial_tool_count = initial_tools. len ( ) ;
655+ println ! ( "Initial tool count: {}" , initial_tool_count) ;
656+
657+ // Build a component to load
658+ let component_path = build_fetch_component ( ) . await ?;
659+
660+ // Step 2: Load a component using the load-component tool
661+ let load_component_request = format ! (
662+ r#"{{"jsonrpc": "2.0", "method": "tools/call", "params": {{"name": "load-component", "arguments": {{"path": "file://{}"}}}}, "id": 3}}
663+ "# ,
664+ component_path. to_str( ) . unwrap( )
665+ ) ;
666+
667+ stdin. write_all ( load_component_request. as_bytes ( ) ) . await ?;
668+ stdin. flush ( ) . await ?;
669+
670+ // Read the tool list change notification first (this is what we're testing!)
671+ let mut notification_line = String :: new ( ) ;
672+ tokio:: time:: timeout (
673+ Duration :: from_secs ( 15 ) ,
674+ stdout. read_line ( & mut notification_line) ,
675+ )
676+ . await
677+ . context ( "Timeout waiting for tool list change notification" ) ?
678+ . context ( "Failed to read tool list change notification" ) ?;
679+
680+ let notification: serde_json:: Value = serde_json:: from_str ( & notification_line)
681+ . context ( "Failed to parse tool list change notification" ) ?;
682+
683+ // Verify we received a tools/list_changed notification
684+ assert_eq ! ( notification[ "jsonrpc" ] , "2.0" ) ;
685+ assert_eq ! ( notification[ "method" ] , "notifications/tools/list_changed" ) ;
686+ println ! ( "✓ Received tools/list_changed notification as expected" ) ;
687+
688+ // Read the actual load-component response
689+ let mut load_response_line = String :: new ( ) ;
690+ tokio:: time:: timeout (
691+ Duration :: from_secs ( 15 ) ,
692+ stdout. read_line ( & mut load_response_line) ,
693+ )
694+ . await
695+ . context ( "Timeout waiting for load-component response" ) ?
696+ . context ( "Failed to read load-component response" ) ?;
697+
698+ let load_response: serde_json:: Value = serde_json:: from_str ( & load_response_line)
699+ . context ( "Failed to parse load-component response" ) ?;
700+
701+ assert_eq ! ( load_response[ "jsonrpc" ] , "2.0" ) ;
702+ assert_eq ! ( load_response[ "id" ] , 3 ) ;
703+
704+ // Check if the load succeeded
705+ if load_response[ "error" ] . is_object ( ) {
706+ panic ! ( "Failed to load component: {}" , load_response[ "error" ] ) ;
707+ }
708+ assert ! ( load_response[ "result" ] . is_object( ) ) ;
709+ println ! ( "✓ Component loaded successfully" ) ;
710+
711+ // Step 3: Send another list_tools request to verify tools were added
712+ let list_tools_request_after = r#"{"jsonrpc": "2.0", "method": "tools/list", "params": {}, "id": 4}
713+ "# ;
714+
715+ stdin. write_all ( list_tools_request_after. as_bytes ( ) ) . await ?;
716+ stdin. flush ( ) . await ?;
717+
718+ // Read updated tools list response
719+ let mut updated_tools_response_line = String :: new ( ) ;
720+ tokio:: time:: timeout (
721+ Duration :: from_secs ( 10 ) ,
722+ stdout. read_line ( & mut updated_tools_response_line) ,
723+ )
724+ . await
725+ . context ( "Timeout waiting for updated tools/list response" ) ?
726+ . context ( "Failed to read updated tools/list response" ) ?;
727+
728+ let updated_tools_response: serde_json:: Value =
729+ serde_json:: from_str ( & updated_tools_response_line)
730+ . context ( "Failed to parse updated tools/list response" ) ?;
731+
732+ assert_eq ! ( updated_tools_response[ "jsonrpc" ] , "2.0" ) ;
733+ assert_eq ! ( updated_tools_response[ "id" ] , 4 ) ;
734+ assert ! ( updated_tools_response[ "result" ] . is_object( ) ) ;
735+ assert ! ( updated_tools_response[ "result" ] [ "tools" ] . is_array( ) ) ;
736+
737+ let updated_tools = & updated_tools_response[ "result" ] [ "tools" ]
738+ . as_array ( )
739+ . unwrap ( ) ;
740+ let updated_tool_count = updated_tools. len ( ) ;
741+ println ! ( "Updated tool count: {}" , updated_tool_count) ;
742+
743+ // Verify that the tool count increased after loading the component
744+ assert ! (
745+ updated_tool_count > initial_tool_count,
746+ "Tool count should have increased from {} to {}, but it didn't" ,
747+ initial_tool_count,
748+ updated_tool_count
749+ ) ;
750+ println ! ( "✓ Tool count increased as expected after loading component" ) ;
751+
752+ // Verify that the new tools from the component are present
753+ let updated_tool_names: Vec < String > = updated_tools
754+ . iter ( )
755+ . map ( |tool| tool[ "name" ] . as_str ( ) . unwrap_or ( "" ) . to_string ( ) )
756+ . collect ( ) ;
757+
758+ // The fetch component should add a "fetch" tool
759+ assert ! (
760+ updated_tool_names. contains( & "fetch" . to_string( ) ) ,
761+ "Expected 'fetch' tool from loaded component, but found tools: {:?}" ,
762+ updated_tool_names
763+ ) ;
764+ println ! ( "✓ New tools from loaded component are present in the list" ) ;
765+
766+ // Clean up
767+ child. kill ( ) . await . ok ( ) ;
768+
769+ Ok ( ( ) )
770+ }
771+
538772#[ test( tokio:: test) ]
539773async fn test_http_transport ( ) -> Result < ( ) > {
540774 // Use a random available port to avoid conflicts
0 commit comments