@@ -226,6 +226,8 @@ struct RefreshTestTransportFactory {
226226 tool : Tool ,
227227 list_started : Option < Arc < Notify > > ,
228228 release_list : Option < Arc < Notify > > ,
229+ next_cursor : Option < String > ,
230+ list_requests : Arc < AtomicUsize > ,
229231}
230232
231233impl ServerHandler for RefreshTestTransportFactory {
@@ -238,13 +240,17 @@ impl ServerHandler for RefreshTestTransportFactory {
238240 _request : Option < PaginatedRequestParams > ,
239241 _context : rmcp:: service:: RequestContext < rmcp:: service:: RoleServer > ,
240242 ) -> Result < ListToolsResult , McpError > {
243+ self . list_requests
244+ . fetch_add ( 1 , std:: sync:: atomic:: Ordering :: SeqCst ) ;
241245 if let Some ( list_started) = & self . list_started {
242246 list_started. notify_one ( ) ;
243247 }
244248 if let Some ( release_list) = & self . release_list {
245249 release_list. notified ( ) . await ;
246250 }
247- Ok ( ListToolsResult :: with_all_items ( vec ! [ self . tool. clone( ) ] ) )
251+ let mut result = ListToolsResult :: with_all_items ( vec ! [ self . tool. clone( ) ] ) ;
252+ result. next_cursor = self . next_cursor . clone ( ) ;
253+ Ok ( result)
248254 }
249255}
250256
@@ -346,6 +352,48 @@ impl InProcessTransportFactory for DisconnectingToolsTransportFactory {
346352 }
347353}
348354
355+ #[ tokio:: test]
356+ async fn legacy_tool_catalog_does_not_follow_pagination_cursor ( ) -> anyhow:: Result < ( ) > {
357+ let requests = Arc :: new ( AtomicUsize :: new ( 0 ) ) ;
358+ let client = Arc :: new (
359+ RmcpClient :: new_in_process_client ( Arc :: new ( RefreshTestTransportFactory {
360+ tool : create_test_tool ( "legacy" , "first-page" ) . tool ,
361+ list_started : None ,
362+ release_list : None ,
363+ next_cursor : Some ( "next-page" . to_string ( ) ) ,
364+ list_requests : Arc :: clone ( & requests) ,
365+ } ) )
366+ . await ?,
367+ ) ;
368+ client
369+ . initialize (
370+ InitializeRequestParams :: new (
371+ ClientCapabilities :: default ( ) ,
372+ Implementation :: new ( "codex-test" , "0.0.0-test" ) ,
373+ )
374+ . with_protocol_version ( ProtocolVersion :: V_2025_06_18 ) ,
375+ Some ( Duration :: from_secs ( 5 ) ) ,
376+ Box :: new ( |_, _| async { Err ( anyhow ! ( "unexpected elicitation" ) ) } . boxed ( ) ) ,
377+ )
378+ . await ?;
379+
380+ let tools = list_tools_for_client_uncached (
381+ "legacy" ,
382+ /*is_codex_apps_mcp_server*/ false ,
383+ "test" ,
384+ & client,
385+ Some ( Duration :: from_secs ( 5 ) ) ,
386+ /*server_instructions*/ None ,
387+ )
388+ . await ?;
389+
390+ assert_eq ! ( tools. len( ) , 1 ) ;
391+ assert_eq ! ( tools[ 0 ] . tool. name. as_ref( ) , "first-page" ) ;
392+ assert_eq ! ( requests. load( std:: sync:: atomic:: Ordering :: SeqCst ) , 1 ) ;
393+ client. shutdown ( ) . await ;
394+ Ok ( ( ) )
395+ }
396+
349397async fn create_test_managed_client ( tools : Vec < ToolInfo > ) -> ManagedClient {
350398 ManagedClient {
351399 client : Arc :: new (
@@ -391,6 +439,8 @@ async fn create_test_manager_with_ready_apps_client(
391439 tool : tool. tool . clone ( ) ,
392440 list_started,
393441 release_list,
442+ next_cursor : None ,
443+ list_requests : Arc :: new ( AtomicUsize :: new ( 0 ) ) ,
394444 } ) )
395445 . await ?,
396446 ) ;
@@ -2568,12 +2618,13 @@ fn reusable_server_identity(
25682618 runtime_context : & McpRuntimeContext ,
25692619) -> McpServerConnectionIdentity {
25702620 let server = EffectiveMcpServer :: configured ( config. clone ( ) ) ;
2621+ let resolved_environment = runtime_context. resolve_server_environment ( "docs" , config) ;
25712622 McpServerConnectionIdentity :: new (
25722623 "docs" ,
25732624 & server,
25742625 OAuthCredentialsStoreMode :: default ( ) ,
25752626 AuthKeyringBackendKind :: default ( ) ,
2576- & Ok ( None ) ,
2627+ & resolved_environment ,
25772628 runtime_context,
25782629 /*runtime_auth_provider*/ None ,
25792630 /*auth*/ None ,
@@ -2796,6 +2847,139 @@ async fn reconciliation_reuses_an_unchanged_ready_server() {
27962847 ) ;
27972848}
27982849
2850+ #[ tokio:: test]
2851+ async fn reconciliation_reuses_legacy_stdio_server_with_existing_protocol_marker ( ) {
2852+ let runtime_context = McpRuntimeContext :: new (
2853+ Arc :: new ( codex_exec_server:: EnvironmentManager :: default_for_tests ( ) ) ,
2854+ PathBuf :: from ( "/tmp" ) ,
2855+ ) ;
2856+ let mut config = reusable_server_config ( "http://127.0.0.1:1" ) ;
2857+ config. transport = McpServerTransportConfig :: Stdio {
2858+ command : "legacy-server" . to_string ( ) ,
2859+ args : Vec :: new ( ) ,
2860+ env : Some ( HashMap :: from ( [ (
2861+ "CODEX_MCP_PROTOCOL_VERSION" . to_string ( ) ,
2862+ "1999-01-01" . to_string ( ) ,
2863+ ) ] ) ) ,
2864+ env_vars : Vec :: new ( ) ,
2865+ cwd : None ,
2866+ } ;
2867+ let previous = manager_with_reusable_ready_server (
2868+ & config,
2869+ & runtime_context,
2870+ vec ! [ create_test_tool( "docs" , "search" ) ] ,
2871+ )
2872+ . await ;
2873+
2874+ let reconciled = reconcile_reusable_server ( & previous, config, runtime_context) . await ;
2875+
2876+ assert ! ( previous. shares_test_connection_with( & reconciled, "docs" ) ) ;
2877+ }
2878+
2879+ #[ tokio:: test]
2880+ async fn reconciliation_replaces_connection_when_protocol_mode_changes ( ) {
2881+ let runtime_context = reusable_server_runtime_context ( ) ;
2882+ let config = reusable_server_config ( "http://127.0.0.1:1" ) ;
2883+ let previous = manager_with_reusable_ready_server (
2884+ & config,
2885+ & runtime_context,
2886+ vec ! [ create_test_tool( "docs" , "search" ) ] ,
2887+ )
2888+ . await ;
2889+ let codex_home = tempdir ( ) . expect ( "tempdir" ) ;
2890+ let mut mcp_config = crate :: mcp:: tests:: test_mcp_config ( codex_home. path ( ) . to_path_buf ( ) ) ;
2891+ mcp_config. protocol_mode = codex_rmcp_client:: McpProtocolMode :: V20260728 ;
2892+
2893+ let reconciled = McpConnectionSet :: new (
2894+ Some ( & previous) ,
2895+ McpPublicationGate :: already_published ( ) ,
2896+ McpRuntimeInput {
2897+ config : Arc :: new ( mcp_config) ,
2898+ plugins_available : false ,
2899+ ready_selected_capability_roots : Vec :: new ( ) ,
2900+ mcp_servers : HashMap :: from ( [ (
2901+ "docs" . to_string ( ) ,
2902+ EffectiveMcpServer :: configured ( config) ,
2903+ ) ] ) ,
2904+ submit_id : "refresh" . to_string ( ) ,
2905+ tx_event : None ,
2906+ startup_cancellation_token : CancellationToken :: new ( ) ,
2907+ runtime_context,
2908+ codex_apps_tools_cache : ConnectorRuntimeManager :: default ( ) ,
2909+ tool_catalog_cache : McpToolCatalogCache :: default ( ) ,
2910+ codex_apps_tools_cache_key : ConnectorRuntimeContextKey :: personal (
2911+ /*account_id*/ None , /*chatgpt_user_id*/ None ,
2912+ ) ,
2913+ supports_openai_form_elicitation : false ,
2914+ auth : None ,
2915+ codex_apps_auth_manager : None ,
2916+ elicitation_reviewer : None ,
2917+ elicitation_lifecycle : None ,
2918+ } ,
2919+ ElicitationRequestRouter :: default ( ) ,
2920+ )
2921+ . await ;
2922+
2923+ assert ! ( !previous. shares_test_connection_with( & reconciled, "docs" ) ) ;
2924+ }
2925+
2926+ #[ tokio:: test]
2927+ async fn reconciliation_reuses_legacy_stdio_server_when_modern_protocol_is_enabled ( ) {
2928+ let runtime_context = McpRuntimeContext :: new (
2929+ Arc :: new ( codex_exec_server:: EnvironmentManager :: default_for_tests ( ) ) ,
2930+ PathBuf :: from ( "/tmp" ) ,
2931+ ) ;
2932+ let mut config = reusable_server_config ( "http://127.0.0.1:1" ) ;
2933+ config. transport = McpServerTransportConfig :: Stdio {
2934+ command : "legacy-server" . to_string ( ) ,
2935+ args : Vec :: new ( ) ,
2936+ env : None ,
2937+ env_vars : Vec :: new ( ) ,
2938+ cwd : None ,
2939+ } ;
2940+ let previous = manager_with_reusable_ready_server (
2941+ & config,
2942+ & runtime_context,
2943+ vec ! [ create_test_tool( "docs" , "search" ) ] ,
2944+ )
2945+ . await ;
2946+ let codex_home = tempdir ( ) . expect ( "tempdir" ) ;
2947+ let mut mcp_config = crate :: mcp:: tests:: test_mcp_config ( codex_home. path ( ) . to_path_buf ( ) ) ;
2948+ mcp_config. protocol_mode = codex_rmcp_client:: McpProtocolMode :: V20260728 ;
2949+
2950+ let reconciled = McpConnectionSet :: new (
2951+ Some ( & previous) ,
2952+ McpPublicationGate :: already_published ( ) ,
2953+ McpRuntimeInput {
2954+ config : Arc :: new ( mcp_config) ,
2955+ plugins_available : false ,
2956+ ready_selected_capability_roots : Vec :: new ( ) ,
2957+ mcp_servers : HashMap :: from ( [ (
2958+ "docs" . to_string ( ) ,
2959+ EffectiveMcpServer :: configured ( config) ,
2960+ ) ] ) ,
2961+ submit_id : "refresh" . to_string ( ) ,
2962+ tx_event : None ,
2963+ startup_cancellation_token : CancellationToken :: new ( ) ,
2964+ runtime_context,
2965+ codex_apps_tools_cache : ConnectorRuntimeManager :: default ( ) ,
2966+ tool_catalog_cache : McpToolCatalogCache :: default ( ) ,
2967+ codex_apps_tools_cache_key : ConnectorRuntimeContextKey :: personal (
2968+ /*account_id*/ None , /*chatgpt_user_id*/ None ,
2969+ ) ,
2970+ supports_openai_form_elicitation : false ,
2971+ auth : None ,
2972+ codex_apps_auth_manager : None ,
2973+ elicitation_reviewer : None ,
2974+ elicitation_lifecycle : None ,
2975+ } ,
2976+ ElicitationRequestRouter :: default ( ) ,
2977+ )
2978+ . await ;
2979+
2980+ assert ! ( previous. shares_test_connection_with( & reconciled, "docs" ) ) ;
2981+ }
2982+
27992983#[ tokio:: test]
28002984async fn reconciliation_updates_elicitation_policy_without_restarting_ready_server ( ) {
28012985 let runtime_context = reusable_server_runtime_context ( ) ;
0 commit comments