diff --git a/web/app/components/tools/mcp/create-card.spec.tsx b/web/app/components/tools/mcp/create-card.spec.tsx
index a993c0d1a8..9ddee00460 100644
--- a/web/app/components/tools/mcp/create-card.spec.tsx
+++ b/web/app/components/tools/mcp/create-card.spec.tsx
@@ -1,9 +1,8 @@
-/* eslint-disable react/no-unnecessary-use-prefix */
import type { ReactNode } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import * as React from 'react'
-import { describe, expect, it, vi } from 'vitest'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
import NewMCPCard from './create-card'
// Track the mock functions
@@ -41,10 +40,13 @@ vi.mock('./modal', () => ({
},
}))
+// Mutable workspace manager state
+let mockIsCurrentWorkspaceManager = true
+
// Mock the app context
vi.mock('@/context/app-context', () => ({
useAppContext: () => ({
- isCurrentWorkspaceManager: true,
+ isCurrentWorkspaceManager: mockIsCurrentWorkspaceManager,
isCurrentWorkspaceEditor: true,
}),
}))
@@ -83,6 +85,11 @@ describe('NewMCPCard', () => {
handleCreate: vi.fn(),
}
+ beforeEach(() => {
+ mockCreateMCP.mockClear()
+ mockIsCurrentWorkspaceManager = true
+ })
+
describe('Rendering', () => {
it('should render without crashing', () => {
render(, { wrapper: createWrapper() })
@@ -133,19 +140,11 @@ describe('NewMCPCard', () => {
describe('Non-Manager User', () => {
it('should not render card when user is not workspace manager', () => {
- // Override the mock for this test
- vi.doMock('@/context/app-context', () => ({
- useAppContext: () => ({
- isCurrentWorkspaceManager: false,
- isCurrentWorkspaceEditor: false,
- }),
- }))
+ mockIsCurrentWorkspaceManager = false
- // The component should not render
render(, { wrapper: createWrapper() })
- // Since isCurrentWorkspaceManager is mocked as true at module level,
- // the card will still render. This test documents the expected behavior.
- expect(screen.getByText('tools.mcp.create.cardTitle')).toBeInTheDocument()
+
+ expect(screen.queryByText('tools.mcp.create.cardTitle')).not.toBeInTheDocument()
})
})
@@ -166,10 +165,6 @@ describe('NewMCPCard', () => {
})
describe('Modal Interactions', () => {
- beforeEach(() => {
- mockCreateMCP.mockClear()
- })
-
it('should call create function when modal confirms', async () => {
const handleCreate = vi.fn()
render(, { wrapper: createWrapper() })
diff --git a/web/app/components/tools/mcp/hooks/use-mcp-modal-form.ts b/web/app/components/tools/mcp/hooks/use-mcp-modal-form.ts
index c8245d44f5..286e2bf2e8 100644
--- a/web/app/components/tools/mcp/hooks/use-mcp-modal-form.ts
+++ b/web/app/components/tools/mcp/hooks/use-mcp-modal-form.ts
@@ -126,9 +126,19 @@ export const useMCPModalForm = (data?: ToolWithProvider) => {
}
catch (e) {
let errorMessage = 'Failed to fetch remote icon'
- const errorData = await (e as Response).json()
- if (errorData?.code)
- errorMessage = `Upload failed: ${errorData.code}`
+ if (e instanceof Response) {
+ try {
+ const errorData = await e.json()
+ if (errorData?.code)
+ errorMessage = `Upload failed: ${errorData.code}`
+ }
+ catch {
+ // Ignore JSON parsing errors
+ }
+ }
+ else if (e instanceof Error) {
+ errorMessage = e.message
+ }
console.error('Failed to fetch remote icon:', e)
Toast.notify({ type: 'warning', message: errorMessage })
}
diff --git a/web/app/components/tools/mcp/hooks/use-mcp-service-card.spec.ts b/web/app/components/tools/mcp/hooks/use-mcp-service-card.spec.ts
index 35a9e25854..b36f724857 100644
--- a/web/app/components/tools/mcp/hooks/use-mcp-service-card.spec.ts
+++ b/web/app/components/tools/mcp/hooks/use-mcp-service-card.spec.ts
@@ -1,14 +1,28 @@
-/* eslint-disable react/no-unnecessary-use-prefix */
import type { ReactNode } from 'react'
import type { AppDetailResponse } from '@/models/app'
import type { AppSSO } from '@/types/app'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { act, renderHook } from '@testing-library/react'
import * as React from 'react'
-import { describe, expect, it, vi } from 'vitest'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AppModeEnum } from '@/types/app'
import { useMCPServiceCardState } from './use-mcp-service-card'
+// Mutable mock data for MCP server detail
+let mockMCPServerDetailData: {
+ id: string
+ status: string
+ server_code: string
+ description: string
+ parameters: Record
+} | undefined = {
+ id: 'server-123',
+ status: 'active',
+ server_code: 'abc123',
+ description: 'Test server',
+ parameters: {},
+}
+
// Mock service hooks
vi.mock('@/service/use-tools', () => ({
useUpdateMCPServer: () => ({
@@ -18,16 +32,8 @@ vi.mock('@/service/use-tools', () => ({
mutateAsync: vi.fn().mockResolvedValue({}),
isPending: false,
}),
- useMCPServerDetail: (appId: string) => ({
- data: appId
- ? {
- id: 'server-123',
- status: 'active',
- server_code: 'abc123',
- description: 'Test server',
- parameters: {},
- }
- : undefined,
+ useMCPServerDetail: () => ({
+ data: mockMCPServerDetailData,
}),
useInvalidateMCPServerDetail: () => vi.fn(),
}))
@@ -85,6 +91,17 @@ describe('useMCPServiceCardState', () => {
api_base_url: 'https://api.example.com/v1',
} as AppDetailResponse & Partial)
+ beforeEach(() => {
+ // Reset mock data to default (published server)
+ mockMCPServerDetailData = {
+ id: 'server-123',
+ status: 'active',
+ server_code: 'abc123',
+ description: 'Test server',
+ parameters: {},
+ }
+ })
+
describe('Initialization', () => {
it('should initialize with correct default values for basic app', () => {
const appInfo = createMockAppInfo(AppModeEnum.CHAT)
@@ -350,38 +367,26 @@ describe('useMCPServiceCardState', () => {
describe('Unpublished Server', () => {
it('should open modal and return not activated when enabling unpublished server', async () => {
- // Override mock to return no data (unpublished server)
- vi.doMock('@/service/use-tools', () => ({
- useUpdateMCPServer: () => ({
- mutateAsync: vi.fn().mockResolvedValue({}),
- }),
- useRefreshMCPServerCode: () => ({
- mutateAsync: vi.fn().mockResolvedValue({}),
- isPending: false,
- }),
- useMCPServerDetail: () => ({
- data: undefined, // No server data = unpublished
- }),
- useInvalidateMCPServerDetail: () => vi.fn(),
- }))
+ // Set mock to return undefined (unpublished server)
+ mockMCPServerDetailData = undefined
- // For this test, we need to test the scenario where server is not published
- // Since we can't easily change the mock, we verify the behavior with the current mock
const appInfo = createMockAppInfo()
const { result } = renderHook(
() => useMCPServiceCardState(appInfo, false),
{ wrapper: createWrapper() },
)
- // The current mock returns published server, so status change works normally
- // This test documents the expected behavior
+ // Verify server is not published
+ expect(result.current.serverPublished).toBe(false)
+
let statusResult: { activated: boolean } | undefined
await act(async () => {
statusResult = await result.current.handleStatusChange(true)
})
- // With published server, activation succeeds
- expect(statusResult?.activated).toBe(true)
+ // Should open modal and return not activated
+ expect(result.current.showMCPServerModal).toBe(true)
+ expect(statusResult?.activated).toBe(false)
})
})
diff --git a/web/app/components/tools/mcp/mcp-service-card.spec.tsx b/web/app/components/tools/mcp/mcp-service-card.spec.tsx
index 9dfd9ebfe2..25e5d6d570 100644
--- a/web/app/components/tools/mcp/mcp-service-card.spec.tsx
+++ b/web/app/components/tools/mcp/mcp-service-card.spec.tsx
@@ -9,102 +9,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AppModeEnum } from '@/types/app'
import MCPServiceCard from './mcp-service-card'
-// Mutable mock state for dynamic testing
-type MockServerDetail = {
- id: string
- status: string
- server_code: string
- description: string
- parameters?: Record
-}
-
-type MockAppContext = {
- isCurrentWorkspaceManager: boolean
- isCurrentWorkspaceEditor: boolean
-}
-
-type MockWorkflowData = {
- graph: {
- nodes: Array<{
- data: {
- type: string
- variables?: Array<{ variable: string, label: string }>
- }
- }>
- }
-}
-
-type MockBasicAppConfig = {
- updated_at?: string
- user_input_form?: unknown[]
-}
-
-let mockServerDetail: MockServerDetail | undefined = {
- id: 'server-123',
- status: 'active',
- server_code: 'abc123',
- description: 'Test server',
- parameters: {},
-}
-
-let mockAppContext: MockAppContext = {
- isCurrentWorkspaceManager: true,
- isCurrentWorkspaceEditor: true,
-}
-
-let mockWorkflowData: MockWorkflowData = {
- graph: {
- nodes: [
- { data: { type: 'start', variables: [{ variable: 'input', label: 'Input' }] } },
- ],
- },
-}
-
-let mockBasicAppConfig: MockBasicAppConfig = {
- updated_at: '2024-01-01',
- user_input_form: [],
-}
-
-const mockUpdateMCPServer = vi.fn().mockResolvedValue({})
-const mockRefreshMCPServerCode = vi.fn().mockResolvedValue({})
-const mockInvalidateMCPServerDetail = vi.fn()
-
-// Mock service hooks
-vi.mock('@/service/use-tools', () => ({
- useUpdateMCPServer: () => ({
- mutateAsync: mockUpdateMCPServer,
- }),
- useRefreshMCPServerCode: () => ({
- mutateAsync: mockRefreshMCPServerCode,
- isPending: false,
- }),
- useMCPServerDetail: (appId: string) => ({
- data: appId ? mockServerDetail : undefined,
- }),
- useInvalidateMCPServerDetail: () => mockInvalidateMCPServerDetail,
-}))
-
-// Mock workflow hook
-vi.mock('@/service/use-workflow', () => ({
- useAppWorkflow: (appId: string) => ({
- data: appId ? mockWorkflowData : undefined,
- }),
-}))
-
-// Mock app context
-vi.mock('@/context/app-context', () => ({
- useAppContext: () => mockAppContext,
-}))
-
-// Mock apps service
-vi.mock('@/service/apps', () => ({
- fetchAppDetail: vi.fn().mockImplementation(() =>
- Promise.resolve({
- model_config: mockBasicAppConfig,
- }),
- ),
-}))
-
// Mock MCPServerModal
vi.mock('@/app/components/tools/mcp/mcp-server-modal', () => ({
default: ({ show, onHide }: { show: boolean, onHide: () => void }) => {
@@ -133,15 +37,39 @@ vi.mock('@/app/components/base/confirm', () => ({
}))
// Mutable mock handlers for hook
-let mockHandleStatusChange = vi.fn().mockResolvedValue({ activated: true })
-let mockHandleServerModalHide = vi.fn().mockReturnValue({ shouldDeactivate: false })
-let mockHandleGenCode = vi.fn()
-let mockOpenConfirmDelete = vi.fn()
-let mockCloseConfirmDelete = vi.fn()
-let mockOpenServerModal = vi.fn()
+const mockHandleStatusChange = vi.fn().mockResolvedValue({ activated: true })
+const mockHandleServerModalHide = vi.fn().mockReturnValue({ shouldDeactivate: false })
+const mockHandleGenCode = vi.fn()
+const mockOpenConfirmDelete = vi.fn()
+const mockCloseConfirmDelete = vi.fn()
+const mockOpenServerModal = vi.fn()
-// Mutable hook state
-let mockHookState = {
+// Type for mock hook state
+type MockHookState = {
+ genLoading: boolean
+ isLoading: boolean
+ serverPublished: boolean
+ serverActivated: boolean
+ serverURL: string
+ detail: {
+ id: string
+ status: string
+ server_code: string
+ description: string
+ parameters: Record
+ } | undefined
+ isCurrentWorkspaceManager: boolean
+ toggleDisabled: boolean
+ isMinimalState: boolean
+ appUnpublished: boolean
+ missingStartNode: boolean
+ showConfirmDelete: boolean
+ showMCPServerModal: boolean
+ latestParams: Array
+}
+
+// Default hook state factory - creates fresh state for each test
+const createDefaultHookState = (): MockHookState => ({
genLoading: false,
isLoading: false,
serverPublished: true,
@@ -162,9 +90,12 @@ let mockHookState = {
showConfirmDelete: false,
showMCPServerModal: false,
latestParams: [],
-}
+})
-// Mock the hook
+// Mutable hook state - modify this in tests to change component behavior
+let mockHookState = createDefaultHookState()
+
+// Mock the hook - uses mockHookState which can be modified per test
vi.mock('./hooks/use-mcp-service-card', () => ({
useMCPServiceCardState: () => ({
...mockHookState,
@@ -198,63 +129,16 @@ describe('MCPServiceCard', () => {
} as AppDetailResponse & Partial)
beforeEach(() => {
- // Reset all mock states
- mockServerDetail = {
- id: 'server-123',
- status: 'active',
- server_code: 'abc123',
- description: 'Test server',
- parameters: {},
- }
- mockAppContext = {
- isCurrentWorkspaceManager: true,
- isCurrentWorkspaceEditor: true,
- }
- mockWorkflowData = {
- graph: {
- nodes: [
- { data: { type: 'start', variables: [{ variable: 'input', label: 'Input' }] } },
- ],
- },
- }
- mockBasicAppConfig = {
- updated_at: '2024-01-01',
- user_input_form: [],
- }
- mockUpdateMCPServer.mockClear()
- mockRefreshMCPServerCode.mockClear()
- mockInvalidateMCPServerDetail.mockClear()
+ // Reset hook state to defaults before each test
+ mockHookState = createDefaultHookState()
- // Reset hook mocks
- mockHandleStatusChange = vi.fn().mockResolvedValue({ activated: true })
- mockHandleServerModalHide = vi.fn().mockReturnValue({ shouldDeactivate: false })
- mockHandleGenCode = vi.fn()
- mockOpenConfirmDelete = vi.fn()
- mockCloseConfirmDelete = vi.fn()
- mockOpenServerModal = vi.fn()
-
- mockHookState = {
- genLoading: false,
- isLoading: false,
- serverPublished: true,
- serverActivated: true,
- serverURL: 'https://api.example.com/mcp/server/abc123/mcp',
- detail: {
- id: 'server-123',
- status: 'active',
- server_code: 'abc123',
- description: 'Test server',
- parameters: {},
- },
- isCurrentWorkspaceManager: true,
- toggleDisabled: false,
- isMinimalState: false,
- appUnpublished: false,
- missingStartNode: false,
- showConfirmDelete: false,
- showMCPServerModal: false,
- latestParams: [],
- }
+ // Reset all mock function call history
+ mockHandleStatusChange.mockClear().mockResolvedValue({ activated: true })
+ mockHandleServerModalHide.mockClear().mockReturnValue({ shouldDeactivate: false })
+ mockHandleGenCode.mockClear()
+ mockOpenConfirmDelete.mockClear()
+ mockCloseConfirmDelete.mockClear()
+ mockOpenServerModal.mockClear()
})
describe('Rendering', () => {
@@ -502,7 +386,15 @@ describe('MCPServiceCard', () => {
describe('Server Not Published', () => {
beforeEach(() => {
- mockServerDetail = undefined
+ // Modify hookState to simulate unpublished server
+ mockHookState = {
+ ...createDefaultHookState(),
+ serverPublished: false,
+ serverActivated: false,
+ serverURL: '***********',
+ detail: undefined,
+ isMinimalState: true,
+ }
})
it('should show add description button when server is not published', () => {
@@ -541,12 +433,17 @@ describe('MCPServiceCard', () => {
describe('Inactive Server', () => {
beforeEach(() => {
- mockServerDetail = {
- id: 'server-123',
- status: 'inactive',
- server_code: 'abc123',
- description: 'Test server',
- parameters: {},
+ // Modify hookState to simulate inactive server
+ mockHookState = {
+ ...createDefaultHookState(),
+ serverActivated: false,
+ detail: {
+ id: 'server-123',
+ status: 'inactive',
+ server_code: 'abc123',
+ description: 'Test server',
+ parameters: {},
+ },
}
})
@@ -575,9 +472,10 @@ describe('MCPServiceCard', () => {
describe('Non-Manager User', () => {
beforeEach(() => {
- mockAppContext = {
+ // Modify hookState to simulate non-manager user
+ mockHookState = {
+ ...createDefaultHookState(),
isCurrentWorkspaceManager: false,
- isCurrentWorkspaceEditor: true,
}
})
@@ -593,7 +491,7 @@ describe('MCPServiceCard', () => {
describe('Non-Editor User', () => {
it('should show disabled styling for non-editor switch', () => {
mockHookState = {
- ...mockHookState,
+ ...createDefaultHookState(),
toggleDisabled: true,
}
@@ -690,24 +588,31 @@ describe('MCPServiceCard', () => {
})
it('should deactivate switch when modal closes without previous activation', async () => {
- mockServerDetail = undefined // Unpublished server
+ // Simulate unpublished server state
+ mockHookState = {
+ ...createDefaultHookState(),
+ serverPublished: false,
+ serverActivated: false,
+ detail: undefined,
+ showMCPServerModal: true,
+ }
const appInfo = createMockAppInfo()
render(, { wrapper: createWrapper() })
- const switchElement = screen.getByRole('switch')
- fireEvent.click(switchElement)
+ // Modal should be visible
+ const modal = screen.getByTestId('mcp-server-modal')
+ expect(modal).toBeInTheDocument()
+
+ const closeBtn = screen.getByTestId('close-modal-btn')
+ fireEvent.click(closeBtn)
await waitFor(() => {
- const modal = screen.queryByTestId('mcp-server-modal')
- if (modal) {
- expect(modal).toBeInTheDocument()
- const closeBtn = screen.getByTestId('close-modal-btn')
- fireEvent.click(closeBtn)
- }
+ expect(mockHandleServerModalHide).toHaveBeenCalled()
})
// Switch should be off after closing modal without activation
+ const switchElement = screen.getByRole('switch')
expect(switchElement).toBeInTheDocument()
})
})
@@ -715,7 +620,7 @@ describe('MCPServiceCard', () => {
describe('Unpublished App', () => {
it('should show minimal state for unpublished app', () => {
mockHookState = {
- ...mockHookState,
+ ...createDefaultHookState(),
appUnpublished: true,
isMinimalState: true,
}
@@ -728,7 +633,7 @@ describe('MCPServiceCard', () => {
it('should show disabled styling for unpublished app switch', () => {
mockHookState = {
- ...mockHookState,
+ ...createDefaultHookState(),
appUnpublished: true,
toggleDisabled: true,
}
@@ -746,7 +651,7 @@ describe('MCPServiceCard', () => {
describe('Workflow App Without Start Node', () => {
it('should show minimal state for workflow without start node', () => {
mockHookState = {
- ...mockHookState,
+ ...createDefaultHookState(),
missingStartNode: true,
isMinimalState: true,
}
@@ -759,7 +664,7 @@ describe('MCPServiceCard', () => {
it('should show disabled styling for workflow without start node', () => {
mockHookState = {
- ...mockHookState,
+ ...createDefaultHookState(),
missingStartNode: true,
toggleDisabled: true,
}
@@ -777,7 +682,7 @@ describe('MCPServiceCard', () => {
describe('Loading State', () => {
it('should return null when isLoading is true', () => {
mockHookState = {
- ...mockHookState,
+ ...createDefaultHookState(),
isLoading: true,
}
@@ -790,7 +695,7 @@ describe('MCPServiceCard', () => {
it('should render content when isLoading is false', () => {
mockHookState = {
- ...mockHookState,
+ ...createDefaultHookState(),
isLoading: false,
}
@@ -829,10 +734,10 @@ describe('MCPServiceCard', () => {
it('should call handleStatusChange with false when turning off', async () => {
// Start with server activated
mockHookState = {
- ...mockHookState,
+ ...createDefaultHookState(),
serverActivated: true,
}
- mockHandleStatusChange = vi.fn().mockResolvedValue({ activated: false })
+ mockHandleStatusChange.mockResolvedValue({ activated: false })
const appInfo = createMockAppInfo()
render(, { wrapper: createWrapper() })
@@ -850,10 +755,10 @@ describe('MCPServiceCard', () => {
it('should call handleStatusChange with true when turning on', async () => {
// Start with server deactivated
mockHookState = {
- ...mockHookState,
+ ...createDefaultHookState(),
serverActivated: false,
}
- mockHandleStatusChange = vi.fn().mockResolvedValue({ activated: true })
+ mockHandleStatusChange.mockResolvedValue({ activated: true })
const appInfo = createMockAppInfo()
render(, { wrapper: createWrapper() })
@@ -871,12 +776,12 @@ describe('MCPServiceCard', () => {
it('should set local activated to false when handleStatusChange returns activated: false and state is true', async () => {
// Simulate unpublished server scenario where enabling opens modal
mockHookState = {
- ...mockHookState,
+ ...createDefaultHookState(),
serverActivated: false,
serverPublished: false,
}
// Handler returns activated: false (modal opened instead)
- mockHandleStatusChange = vi.fn().mockResolvedValue({ activated: false })
+ mockHandleStatusChange.mockResolvedValue({ activated: false })
const appInfo = createMockAppInfo()
render(, { wrapper: createWrapper() })
@@ -899,11 +804,11 @@ describe('MCPServiceCard', () => {
it('should deactivate when handleServerModalHide returns shouldDeactivate: true', async () => {
// Set up to show modal
mockHookState = {
- ...mockHookState,
+ ...createDefaultHookState(),
showMCPServerModal: true,
serverActivated: false, // Server was not activated
}
- mockHandleServerModalHide = vi.fn().mockReturnValue({ shouldDeactivate: true })
+ mockHandleServerModalHide.mockReturnValue({ shouldDeactivate: true })
const appInfo = createMockAppInfo()
render(, { wrapper: createWrapper() })
@@ -919,11 +824,11 @@ describe('MCPServiceCard', () => {
it('should not deactivate when handleServerModalHide returns shouldDeactivate: false', async () => {
mockHookState = {
- ...mockHookState,
+ ...createDefaultHookState(),
showMCPServerModal: true,
serverActivated: true, // Server was already activated
}
- mockHandleServerModalHide = vi.fn().mockReturnValue({ shouldDeactivate: false })
+ mockHandleServerModalHide.mockReturnValue({ shouldDeactivate: false })
const appInfo = createMockAppInfo()
render(, { wrapper: createWrapper() })
@@ -942,7 +847,7 @@ describe('MCPServiceCard', () => {
it('should call handleGenCode and closeConfirmDelete when confirm is clicked', async () => {
// Set up to show confirm dialog
mockHookState = {
- ...mockHookState,
+ ...createDefaultHookState(),
showConfirmDelete: true,
}
@@ -965,7 +870,7 @@ describe('MCPServiceCard', () => {
it('should call closeConfirmDelete when cancel is clicked', async () => {
mockHookState = {
- ...mockHookState,
+ ...createDefaultHookState(),
showConfirmDelete: true,
}
@@ -984,7 +889,13 @@ describe('MCPServiceCard', () => {
describe('getTooltipContent Function', () => {
it('should show publish tip when app is unpublished', () => {
- mockBasicAppConfig = {} // Unpublished
+ // Modify hookState to simulate unpublished app
+ mockHookState = {
+ ...createDefaultHookState(),
+ appUnpublished: true,
+ toggleDisabled: true,
+ isMinimalState: true,
+ }
const appInfo = createMockAppInfo()
render(, { wrapper: createWrapper() })
@@ -994,10 +905,12 @@ describe('MCPServiceCard', () => {
})
it('should show missing start node tooltip for workflow without start node', () => {
- mockWorkflowData = {
- graph: {
- nodes: [{ data: { type: 'end' } }],
- },
+ // Modify hookState to simulate missing start node
+ mockHookState = {
+ ...createDefaultHookState(),
+ missingStartNode: true,
+ toggleDisabled: true,
+ isMinimalState: true,
}
const appInfo = createMockAppInfo(AppModeEnum.WORKFLOW)
diff --git a/web/app/components/tools/mcp/mcp-service-card.tsx b/web/app/components/tools/mcp/mcp-service-card.tsx
index 64734deb54..4a85fff7c8 100644
--- a/web/app/components/tools/mcp/mcp-service-card.tsx
+++ b/web/app/components/tools/mcp/mcp-service-card.tsx
@@ -187,27 +187,23 @@ const MCPServiceCard: FC = ({
openServerModal,
} = useMCPServiceCardState(appInfo, triggerModeDisabled)
- // Local UI state for optimistic update
- const [activated, setActivated] = useState(serverActivated)
-
- // Sync with server state when it changes
- if (activated !== serverActivated && !showMCPServerModal) {
- setActivated(serverActivated)
- }
+ // Pending status for optimistic updates (null means use server state)
+ const [pendingStatus, setPendingStatus] = useState(null)
+ const activated = pendingStatus ?? serverActivated
const onChangeStatus = async (state: boolean) => {
- setActivated(state)
+ setPendingStatus(state)
const result = await handleStatusChange(state)
if (!result.activated && state) {
- // Server modal was opened instead, keep local state false
- setActivated(false)
+ // Server modal was opened instead, clear pending status
+ setPendingStatus(null)
}
}
const onServerModalHide = () => {
- const result = handleServerModalHide(serverActivated)
- if (result.shouldDeactivate)
- setActivated(false)
+ handleServerModalHide(serverActivated)
+ // Clear pending status when modal closes to sync with server state
+ setPendingStatus(null)
}
const onConfirmRegenerate = () => {