@@ -782,37 +782,50 @@ describe("mcp surface", () => {
782782 test ( "Enter on an unauthorized server opens the browser and copies the link" , async ( ) => {
783783 await withShell ( ( shell ) => {
784784 const opened : string [ ] = [ ] ;
785+ const retried : string [ ] = [ ] ;
785786 openCommandSurface ( shell , "mcp" , {
786787 notify : ( ) => { } ,
787- mcp : { list : ( ) => entries , openAuthURL : ( url ) => opened . push ( url ) } ,
788+ mcp : {
789+ list : ( ) => entries ,
790+ openAuthURL : ( url ) => opened . push ( url ) ,
791+ retryServer : async ( name ) => {
792+ retried . push ( name ) ;
793+ return { ok : true , message : "should not retry" } ;
794+ } ,
795+ } ,
788796 } ) ;
789797 moveOverlaySelection ( shell , 1 ) ;
790798 acceptOverlaySelection ( shell ) ;
791799 expect ( opened ) . toEqual ( [ "https://notion.test/auth" ] ) ;
800+ expect ( retried ) . toEqual ( [ ] ) ;
792801 expect ( shell . statusFlash ) . toContain ( "notion" ) ;
793802 // The echo would quote "notion — needs auth" back forever, moments
794803 // after the operator authorized it.
795804 expect ( shell . streamLog . filter ( ( r ) => r . meta === "overlay" ) ) . toEqual ( [ ] ) ;
796805 } ) ;
797806 } ) ;
798807
799- test ( "Enter on non-auth rows releases the status subscription" , async ( ) => {
800- const nonAuthEntries : readonly McpEntry [ ] = [
808+ test ( "Enter on connecting and connected rows releases the status subscription" , async ( ) => {
809+ const nonActionEntries : readonly McpEntry [ ] = [
801810 { name : "connected" , state : "connected" , toolCount : 1 } ,
802811 { name : "connecting" , state : "connecting" } ,
803- { name : "failed" , state : "failed" , error : "offline" } ,
804812 ] ;
805813
806- for ( const entry of nonAuthEntries ) {
814+ for ( const entry of nonActionEntries ) {
807815 await withShell ( ( shell ) => {
808816 const listeners = new Set < ( ) => void > ( ) ;
809817 let unsubscribeCalls = 0 ;
810818 const opened : string [ ] = [ ] ;
819+ const retried : string [ ] = [ ] ;
811820 openCommandSurface ( shell , "mcp" , {
812821 notify : ( ) => { } ,
813822 mcp : {
814823 list : ( ) => [ entry ] ,
815824 openAuthURL : ( url ) => opened . push ( url ) ,
825+ retryServer : async ( name ) => {
826+ retried . push ( name ) ;
827+ return { ok : true , message : "should not retry" } ;
828+ } ,
816829 subscribe : ( listener ) => {
817830 listeners . add ( listener ) ;
818831 return ( ) => {
@@ -826,12 +839,120 @@ describe("mcp surface", () => {
826839 expect ( listeners . size ) . toBe ( 1 ) ;
827840 acceptOverlaySelection ( shell ) ;
828841 expect ( opened ) . toEqual ( [ ] ) ;
842+ expect ( retried ) . toEqual ( [ ] ) ;
829843 expect ( unsubscribeCalls ) . toBe ( 1 ) ;
830844 expect ( listeners . size ) . toBe ( 0 ) ;
831845 } ) ;
832846 }
833847 } ) ;
834848
849+ test ( "Enter on a failed server retries connect once without adding again" , async ( ) => {
850+ await withShell ( async ( shell ) => {
851+ const added : { name : string ; url : string } [ ] = [ ] ;
852+ const retried : string [ ] = [ ] ;
853+ const opened : string [ ] = [ ] ;
854+ const notes : string [ ] = [ ] ;
855+ let liveEntries : readonly McpEntry [ ] = [
856+ { name : "sentry" , state : "failed" , error : "ECONNREFUSED" } ,
857+ ] ;
858+ openCommandSurface ( shell , "mcp" , {
859+ notify : ( note ) => notes . push ( note ) ,
860+ mcp : {
861+ list : ( ) => liveEntries ,
862+ openAuthURL : ( url ) => opened . push ( url ) ,
863+ addServer : async ( name , url ) => {
864+ added . push ( { name, url } ) ;
865+ return { ok : true , message : "should not add" } ;
866+ } ,
867+ retryServer : async ( name ) => {
868+ retried . push ( name ) ;
869+ liveEntries = [ { name, state : "connecting" } ] ;
870+ return { ok : true , message : `Retrying ${ name } ; connecting now.` } ;
871+ } ,
872+ } ,
873+ } ) ;
874+ acceptOverlaySelection ( shell ) ;
875+ await Promise . resolve ( ) ;
876+ await Promise . resolve ( ) ;
877+
878+ expect ( retried ) . toEqual ( [ "sentry" ] ) ;
879+ expect ( added ) . toEqual ( [ ] ) ;
880+ expect ( opened ) . toEqual ( [ ] ) ;
881+ expect ( notes ) . toEqual ( [ "Retrying sentry; connecting now." ] ) ;
882+ expect ( shell . overlayItems [ 0 ] ) . toBe ( "sentry — connecting" ) ;
883+ } ) ;
884+ } ) ;
885+
886+ test ( "Enter on a failed row without retry still releases the status subscription" , async ( ) => {
887+ await withShell ( ( shell ) => {
888+ const listeners = new Set < ( ) => void > ( ) ;
889+ let unsubscribeCalls = 0 ;
890+ const added : { name : string ; url : string } [ ] = [ ] ;
891+ openCommandSurface ( shell , "mcp" , {
892+ notify : ( ) => { } ,
893+ mcp : {
894+ list : ( ) => [ { name : "sentry" , state : "failed" , error : "offline" } ] ,
895+ openAuthURL : ( ) => { } ,
896+ addServer : async ( name , url ) => {
897+ added . push ( { name, url } ) ;
898+ return { ok : true , message : "should not add" } ;
899+ } ,
900+ subscribe : ( listener ) => {
901+ listeners . add ( listener ) ;
902+ return ( ) => {
903+ unsubscribeCalls += 1 ;
904+ listeners . delete ( listener ) ;
905+ } ;
906+ } ,
907+ } ,
908+ } ) ;
909+
910+ expect ( listeners . size ) . toBe ( 1 ) ;
911+ acceptOverlaySelection ( shell ) ;
912+ expect ( added ) . toEqual ( [ ] ) ;
913+ expect ( unsubscribeCalls ) . toBe ( 1 ) ;
914+ expect ( listeners . size ) . toBe ( 0 ) ;
915+ } ) ;
916+ } ) ;
917+
918+ test ( "Alt+A of a failed name still persists through addServer" , async ( ) => {
919+ await withShell ( async ( shell ) => {
920+ const added : { name : string ; url : string } [ ] = [ ] ;
921+ const retried : string [ ] = [ ] ;
922+ const notes : string [ ] = [ ] ;
923+ openCommandSurface ( shell , "mcp" , {
924+ notify : ( note ) => notes . push ( note ) ,
925+ mcp : {
926+ list : ( ) => [ { name : "sentry" , state : "failed" as const , error : "offline" } ] ,
927+ openAuthURL : ( ) => { } ,
928+ addServer : async ( name , url ) => {
929+ added . push ( { name, url } ) ;
930+ return {
931+ ok : false ,
932+ message : `An MCP server named "${ name } " already exists or is connecting.` ,
933+ } ;
934+ } ,
935+ retryServer : async ( name ) => {
936+ retried . push ( name ) ;
937+ return { ok : true , message : "should not retry" } ;
938+ } ,
939+ } ,
940+ } ) ;
941+
942+ expect ( runOverlayAction ( shell , altKey ( "a" ) ) ) . toBe ( true ) ;
943+ for ( const ch of "sentry" ) runOverlayAction ( shell , charKey ( ch ) ) ;
944+ acceptOverlaySelection ( shell ) ;
945+ for ( const ch of "https://sentry.test/mcp" ) runOverlayAction ( shell , charKey ( ch ) ) ;
946+ acceptOverlaySelection ( shell ) ;
947+ await Promise . resolve ( ) ;
948+ await Promise . resolve ( ) ;
949+
950+ expect ( added ) . toEqual ( [ { name : "sentry" , url : "https://sentry.test/mcp" } ] ) ;
951+ expect ( retried ) . toEqual ( [ ] ) ;
952+ expect ( notes . at ( - 1 ) ) . toContain ( "already exists" ) ;
953+ } ) ;
954+ } ) ;
955+
835956 test ( "Alt+A collects a name and absolute HTTP URL before adding" , async ( ) => {
836957 await withShell ( async ( shell ) => {
837958 const added : { name : string ; url : string } [ ] = [ ] ;
0 commit comments