@@ -997,3 +997,102 @@ fn mcp_news_search_tool_call_returns_clusters() {
997997 assert_eq ! ( clusters[ 0 ] [ "items" ] [ 0 ] [ "paywall" ] , true ) ;
998998 assert_eq ! ( clusters[ 1 ] [ "items" ] . as_array( ) . unwrap( ) . len( ) , 2 ) ;
999999}
1000+
1001+ #[ test]
1002+ fn mcp_tool_call_error_returns_json_rpc_error_and_keeps_server_alive ( ) {
1003+ let server = MockServer :: start ( ) ;
1004+
1005+ // Mock search endpoint to return a 500 error, simulating a backend failure.
1006+ let _search = server. mock ( |when, then| {
1007+ when. method ( GET ) . path ( "/search" ) ;
1008+ then. status ( 500 ) . body ( "Internal Server Error" ) ;
1009+ } ) ;
1010+
1011+ // Mock news endpoints so kagi_news succeeds — proving the server survived.
1012+ let _latest = server. mock ( |when, then| {
1013+ when. method ( GET ) . path ( "/api/batches/latest" ) ;
1014+ then. status ( 200 )
1015+ . header ( "content-type" , "application/json" )
1016+ . json_body ( news_latest_batch ( ) ) ;
1017+ } ) ;
1018+ let _metadata = server. mock ( |when, then| {
1019+ when. method ( GET ) . path ( "/api/categories/metadata" ) ;
1020+ then. status ( 200 )
1021+ . header ( "content-type" , "application/json" )
1022+ . json_body ( news_category_metadata ( ) ) ;
1023+ } ) ;
1024+ let _categories = server. mock ( |when, then| {
1025+ when. method ( GET ) . path ( "/api/batches/batch-1/categories" ) ;
1026+ then. status ( 200 )
1027+ . header ( "content-type" , "application/json" )
1028+ . json_body ( news_batch_categories ( ) ) ;
1029+ } ) ;
1030+ let _stories = server. mock ( |when, then| {
1031+ when. method ( GET )
1032+ . path ( "/api/batches/batch-1/categories/category-1/stories" ) ;
1033+ then. status ( 200 )
1034+ . header ( "content-type" , "application/json" )
1035+ . json_body ( news_stories ( ) ) ;
1036+ } ) ;
1037+
1038+ let tempdir = TempDir :: new ( ) . expect ( "tempdir" ) ;
1039+ let env = session_env ( & server) ;
1040+
1041+ // Send a search tool call (will fail) followed by a news tool call (should succeed).
1042+ let failing_request = json ! ( {
1043+ "jsonrpc" : "2.0" ,
1044+ "id" : 1 ,
1045+ "method" : "tools/call" ,
1046+ "params" : {
1047+ "name" : "kagi_search" ,
1048+ "arguments" : { "query" : "test" }
1049+ }
1050+ } ) ;
1051+ let succeeding_request = json ! ( {
1052+ "jsonrpc" : "2.0" ,
1053+ "id" : 2 ,
1054+ "method" : "tools/call" ,
1055+ "params" : {
1056+ "name" : "kagi_news" ,
1057+ "arguments" : { "category" : "tech" , "lang" : "en" , "limit" : 3 }
1058+ }
1059+ } ) ;
1060+
1061+ let stdin = format ! (
1062+ "{}\n {}\n " ,
1063+ serde_json:: to_string( & failing_request) . unwrap( ) ,
1064+ serde_json:: to_string( & succeeding_request) . unwrap( ) ,
1065+ ) ;
1066+
1067+ let output = run_kagi_with_stdin ( & [ "mcp" ] , & stdin, & env_refs ( & env) , tempdir. path ( ) ) ;
1068+
1069+ assert_success ( & output) ;
1070+ let stdout = String :: from_utf8_lossy ( & output. stdout ) ;
1071+ let responses: Vec < Value > = stdout
1072+ . lines ( )
1073+ . map ( |line| serde_json:: from_str ( line) . expect ( "each line is valid JSON" ) )
1074+ . collect ( ) ;
1075+
1076+ assert_eq ! ( responses. len( ) , 2 , "expected two JSON-RPC responses" ) ;
1077+
1078+ // First response: the failed tool call should be a JSON-RPC error, not a crash.
1079+ let error_resp = & responses[ 0 ] ;
1080+ assert_eq ! ( error_resp[ "id" ] , 1 ) ;
1081+ assert ! (
1082+ error_resp. get( "error" ) . is_some( ) ,
1083+ "expected JSON-RPC error for failed tool call, got: {error_resp}"
1084+ ) ;
1085+ assert_eq ! ( error_resp[ "error" ] [ "code" ] , -32000 ) ;
1086+ assert ! (
1087+ !error_resp[ "error" ] [ "message" ] . as_str( ) . unwrap( ) . is_empty( ) ,
1088+ "error message should be non-empty"
1089+ ) ;
1090+
1091+ // Second response: the server stayed alive and processed the next request.
1092+ let success_resp = & responses[ 1 ] ;
1093+ assert_eq ! ( success_resp[ "id" ] , 2 ) ;
1094+ assert ! (
1095+ success_resp. get( "result" ) . is_some( ) ,
1096+ "expected successful result for second tool call, got: {success_resp}"
1097+ ) ;
1098+ }
0 commit comments