@@ -30,7 +30,7 @@ import { isServiceError } from '@/lib/utils';
3030import { NotConfiguredErrorBanner } from '@/features/chat/components/notConfiguredErrorBanner' ;
3131import { McpServerIconContext , McpServerIconMap , McpToolNameContext , McpToolNameMap } from '../../mcpDisplayMetadataContext' ;
3232import { McpReconnectContext } from '../../mcpReconnectContext' ;
33- import { McpAuthRequiredData , useMcpReconnectController } from './useMcpReconnectController' ;
33+ import { McpAuthRequiredData , McpServerLoadFailureData , useMcpReconnectController } from './useMcpReconnectController' ;
3434import { McpReconnectBanner } from './mcpReconnectBanner' ;
3535import { ToolApprovalProvider } from '../../toolApprovalContext' ;
3636import useCaptureEvent from '@/hooks/useCaptureEvent' ;
@@ -122,19 +122,7 @@ export const ChatThread = ({
122122 return map ;
123123 } ) ;
124124
125- const [ failedMcpServers , setFailedMcpServers ] = useState < string [ ] > ( ( ) => {
126- const names : string [ ] = [ ] ;
127- initialMessages ?. forEach ( ( message ) => {
128- message . parts
129- . filter ( ( part ) => part . type === 'data-mcp-failed-server' )
130- . forEach ( ( part ) => {
131- if ( ! names . includes ( part . data . serverName ) ) {
132- names . push ( part . data . serverName ) ;
133- }
134- } ) ;
135- } ) ;
136- return names ;
137- } ) ;
125+ const [ failedMcpServers , setFailedMcpServers ] = useState < McpServerLoadFailureData [ ] > ( [ ] ) ;
138126 const [ isFailedMcpBannerVisible , setIsFailedMcpBannerVisible ] = useState ( false ) ;
139127
140128 const { selectedLanguageModel } = useSelectedLanguageModel ( ) ;
@@ -150,6 +138,37 @@ export const ChatThread = ({
150138 useEffect ( ( ) => { modelRef . current = selectedLanguageModel ; } , [ selectedLanguageModel ] ) ;
151139 useEffect ( ( ) => { disabledMcpRef . current = disabledMcpServerIds ; } , [ disabledMcpServerIds ] ) ;
152140
141+ const forceDisableMcpServer = useCallback ( ( serverId : string ) => {
142+ if ( disabledMcpRef . current . includes ( serverId ) ) {
143+ return ;
144+ }
145+
146+ const nextDisabledServerIds = [ ...disabledMcpRef . current , serverId ] ;
147+ disabledMcpRef . current = nextDisabledServerIds ;
148+ onDisabledMcpServerIdsChange ( nextDisabledServerIds ) ;
149+ } , [ onDisabledMcpServerIdsChange ] ) ;
150+
151+ const reenableMcpServer = useCallback ( ( serverId : string ) => {
152+ if ( ! disabledMcpRef . current . includes ( serverId ) ) {
153+ return ;
154+ }
155+
156+ const nextDisabledServerIds = disabledMcpRef . current . filter ( ( id ) => id !== serverId ) ;
157+ disabledMcpRef . current = nextDisabledServerIds ;
158+ onDisabledMcpServerIdsChange ( nextDisabledServerIds ) ;
159+ } , [ onDisabledMcpServerIdsChange ] ) ;
160+
161+ const registerFailedMcpServer = useCallback ( ( server : McpServerLoadFailureData ) => {
162+ setFailedMcpServers ( ( prev ) => {
163+ if ( prev . some ( ( candidate ) => candidate . serverId === server . serverId ) ) {
164+ return prev ;
165+ }
166+ return [ ...prev , server ] ;
167+ } ) ;
168+ setIsFailedMcpBannerVisible ( true ) ;
169+ forceDisableMcpServer ( server . serverId ) ;
170+ } , [ forceDisableMcpServer ] ) ;
171+
153172 const getTransportBody = useCallback ( ( ) => ( {
154173 selectedSearchScopes : searchScopesRef . current ,
155174 languageModel : modelRef . current ,
@@ -160,6 +179,7 @@ export const ChatThread = ({
160179 // messages and status), so transient auth-required events received in
161180 // onData are forwarded to it through a ref.
162181 const onMcpAuthRequiredRef = useRef < ( ( data : McpAuthRequiredData ) => void ) | null > ( null ) ;
182+ const onMcpServerLoadFailedRef = useRef < ( ( data : McpServerLoadFailureData ) => void ) | null > ( null ) ;
163183
164184 // Transport with dynamic body, resolved on every request, including auto-resends
165185 // triggered by sendAutomaticallyWhen after tool approval.
@@ -203,13 +223,8 @@ export const ChatThread = ({
203223 } ) ) ;
204224 }
205225 if ( dataPart . type === 'data-mcp-failed-server' ) {
206- setFailedMcpServers ( ( prev ) => {
207- if ( prev . includes ( dataPart . data . serverName ) ) {
208- return prev ;
209- }
210- return [ ...prev , dataPart . data . serverName ] ;
211- } ) ;
212- setIsFailedMcpBannerVisible ( true ) ;
226+ registerFailedMcpServer ( dataPart . data ) ;
227+ onMcpServerLoadFailedRef . current ?.( dataPart . data ) ;
213228 }
214229 if ( dataPart . type === 'data-mcp-auth-required' ) {
215230 onMcpAuthRequiredRef . current ?.( dataPart . data ) ;
@@ -290,6 +305,7 @@ export const ChatThread = ({
290305 const {
291306 contextValue : mcpReconnectContextValue ,
292307 onAuthRequired : onMcpAuthRequired ,
308+ onServerLoadFailed : onMcpServerLoadFailed ,
293309 } = useMcpReconnectController ( {
294310 status,
295311 messages,
@@ -302,7 +318,30 @@ export const ChatThread = ({
302318
303319 useEffect ( ( ) => {
304320 onMcpAuthRequiredRef . current = onMcpAuthRequired ;
305- } , [ onMcpAuthRequired ] ) ;
321+ onMcpServerLoadFailedRef . current = onMcpServerLoadFailed ;
322+ } , [ onMcpAuthRequired , onMcpServerLoadFailed ] ) ;
323+
324+ const handledLoadReconnectsRef = useRef ( new Set < string > ( ) ) ;
325+ useEffect ( ( ) => {
326+ for ( const state of Object . values ( mcpReconnectContextValue . reconnectStates ) ) {
327+ if ( state . source !== 'tool-load' ) {
328+ continue ;
329+ }
330+
331+ if ( state . status === 'reconnected' ) {
332+ if ( handledLoadReconnectsRef . current . has ( state . serverId ) ) {
333+ continue ;
334+ }
335+ handledLoadReconnectsRef . current . add ( state . serverId ) ;
336+ setFailedMcpServers ( ( prev ) => prev . filter ( ( server ) => server . serverId !== state . serverId ) ) ;
337+ reenableMcpServer ( state . serverId ) ;
338+ continue ;
339+ }
340+
341+ handledLoadReconnectsRef . current . delete ( state . serverId ) ;
342+ registerFailedMcpServer ( { serverId : state . serverId , serverName : state . serverName } ) ;
343+ }
344+ } , [ mcpReconnectContextValue . reconnectStates , reenableMcpServer , registerFailedMcpServer ] ) ;
306345
307346 // When the chat is finished, refresh the page to update the chat history.
308347 const prevStatus = usePrevious ( status ) ;
@@ -439,7 +478,7 @@ export const ChatThread = ({
439478 />
440479 ) }
441480 < McpFailedServersBanner
442- serverNames = { failedMcpServers }
481+ servers = { failedMcpServers }
443482 isVisible = { isFailedMcpBannerVisible }
444483 onClose = { ( ) => setIsFailedMcpBannerVisible ( false ) }
445484 />
@@ -546,6 +585,7 @@ export const ChatThread = ({
546585 isContextSelectorOpen = { isContextSelectorOpen }
547586 onContextSelectorOpenChanged = { setIsContextSelectorOpen }
548587 disabledMcpServerIds = { disabledMcpServerIds }
588+ unavailableMcpServerIds = { failedMcpServers . map ( ( server ) => server . serverId ) }
549589 onDisabledMcpServerIdsChange = { onDisabledMcpServerIdsChange }
550590 isAuthenticated = { isAuthenticated }
551591 />
0 commit comments