diff --git a/core/http/react-ui/package-lock.json b/core/http/react-ui/package-lock.json
index 96e8cc62b..f9c1e5e17 100644
--- a/core/http/react-ui/package-lock.json
+++ b/core/http/react-ui/package-lock.json
@@ -20,7 +20,7 @@
},
"devDependencies": {
"@eslint/js": "^9.27.0",
- "@playwright/test": "^1.52.0",
+ "@playwright/test": "1.52.0",
"@vitejs/plugin-react": "^4.5.2",
"eslint": "^9.27.0",
"eslint-plugin-react-hooks": "^5.2.0",
@@ -1048,13 +1048,12 @@
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
},
"node_modules/@playwright/test": {
- "version": "1.58.2",
- "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
- "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz",
+ "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==",
"dev": true,
- "license": "Apache-2.0",
"dependencies": {
- "playwright": "1.58.2"
+ "playwright": "1.52.0"
},
"bin": {
"playwright": "cli.js"
@@ -3069,13 +3068,12 @@
}
},
"node_modules/playwright": {
- "version": "1.58.2",
- "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
- "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz",
+ "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==",
"dev": true,
- "license": "Apache-2.0",
"dependencies": {
- "playwright-core": "1.58.2"
+ "playwright-core": "1.52.0"
},
"bin": {
"playwright": "cli.js"
@@ -3088,11 +3086,10 @@
}
},
"node_modules/playwright-core": {
- "version": "1.58.2",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
- "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz",
+ "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==",
"dev": true,
- "license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
@@ -3106,7 +3103,6 @@
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
- "license": "MIT",
"optional": true,
"os": [
"darwin"
diff --git a/core/http/react-ui/src/components/UnifiedMCPDropdown.jsx b/core/http/react-ui/src/components/UnifiedMCPDropdown.jsx
new file mode 100644
index 000000000..fe0bedc31
--- /dev/null
+++ b/core/http/react-ui/src/components/UnifiedMCPDropdown.jsx
@@ -0,0 +1,372 @@
+import { useState, useEffect, useRef, useCallback } from 'react'
+import { loadClientMCPServers, addClientMCPServer, removeClientMCPServer } from '../utils/mcpClientStorage'
+
+export default function UnifiedMCPDropdown({
+ // Server MCP props
+ serverMCPAvailable = false,
+ mcpServerList = [],
+ mcpServersLoading = false,
+ selectedServers = [],
+ onToggleServer,
+ onSelectAllServers,
+ onFetchServers,
+ // Client MCP props
+ clientMCPActiveIds = [],
+ onClientToggle,
+ onClientAdded,
+ onClientRemoved,
+ connectionStatuses = {},
+ getConnectedTools,
+ // Prompts props (optional, Chat only)
+ promptsAvailable = false,
+ mcpPromptList = [],
+ mcpPromptsLoading = false,
+ onFetchPrompts,
+ onSelectPrompt,
+ promptArgsDialog = null,
+ promptArgsValues = {},
+ onPromptArgsChange,
+ onPromptArgsSubmit,
+ onPromptArgsCancel,
+ // Resources props (optional, Chat only)
+ resourcesAvailable = false,
+ mcpResourceList = [],
+ mcpResourcesLoading = false,
+ onFetchResources,
+ selectedResources = [],
+ onToggleResource,
+}) {
+ const [open, setOpen] = useState(false)
+ const [activeTab, setActiveTab] = useState(() => serverMCPAvailable ? 'servers' : 'client')
+ const [addDialog, setAddDialog] = useState(false)
+ const [clientServers, setClientServers] = useState(() => loadClientMCPServers())
+ const [url, setUrl] = useState('')
+ const [name, setName] = useState('')
+ const [authToken, setAuthToken] = useState('')
+ const [useProxy, setUseProxy] = useState(true)
+ const ref = useRef(null)
+
+ // Update default tab when serverMCPAvailable changes
+ useEffect(() => {
+ if (!serverMCPAvailable && activeTab === 'servers') {
+ setActiveTab('client')
+ }
+ }, [serverMCPAvailable])
+
+ // Click outside to close
+ useEffect(() => {
+ if (!open) return
+ const handleClick = (e) => {
+ if (ref.current && !ref.current.contains(e.target)) setOpen(false)
+ }
+ document.addEventListener('mousedown', handleClick)
+ return () => document.removeEventListener('mousedown', handleClick)
+ }, [open])
+
+ const handleOpen = useCallback(() => {
+ if (!open) {
+ // Fetch data for default tab
+ if (serverMCPAvailable && activeTab === 'servers' && onFetchServers) onFetchServers()
+ else if (activeTab === 'prompts' && onFetchPrompts) onFetchPrompts()
+ else if (activeTab === 'resources' && onFetchResources) onFetchResources()
+ }
+ setOpen(!open)
+ }, [open, activeTab, serverMCPAvailable, onFetchServers, onFetchPrompts, onFetchResources])
+
+ const switchTab = useCallback((tab) => {
+ setActiveTab(tab)
+ if (tab === 'servers' && onFetchServers) onFetchServers()
+ else if (tab === 'prompts' && onFetchPrompts) onFetchPrompts()
+ else if (tab === 'resources' && onFetchResources) onFetchResources()
+ }, [onFetchServers, onFetchPrompts, onFetchResources])
+
+ const handleAddClient = useCallback(() => {
+ if (!url.trim()) return
+ const headers = {}
+ if (authToken.trim()) {
+ headers.Authorization = `Bearer ${authToken.trim()}`
+ }
+ const server = addClientMCPServer({ name: name.trim() || undefined, url: url.trim(), headers, useProxy })
+ setClientServers(loadClientMCPServers())
+ setUrl('')
+ setName('')
+ setAuthToken('')
+ setUseProxy(true)
+ setAddDialog(false)
+ if (onClientAdded) onClientAdded(server)
+ }, [url, name, authToken, useProxy, onClientAdded])
+
+ const handleRemoveClient = useCallback((id) => {
+ removeClientMCPServer(id)
+ setClientServers(loadClientMCPServers())
+ if (onClientRemoved) onClientRemoved(id)
+ }, [onClientRemoved])
+
+ const totalBadge = (selectedServers?.length || 0) + (clientMCPActiveIds?.length || 0) + (selectedResources?.length || 0)
+
+ const tabs = []
+ if (serverMCPAvailable) tabs.push({ key: 'servers', label: 'Servers' })
+ tabs.push({ key: 'client', label: 'Client' })
+ if (promptsAvailable) tabs.push({ key: 'prompts', label: 'Prompts' })
+ if (resourcesAvailable) tabs.push({ key: 'resources', label: 'Resources' })
+
+ return (
+
+
+ {open && (
+
+ {/* Tab bar */}
+
+ {tabs.map(tab => (
+
+ ))}
+
+
+ {/* Servers tab */}
+ {activeTab === 'servers' && serverMCPAvailable && (
+ mcpServersLoading ? (
+
Loading servers...
+ ) : mcpServerList.length === 0 ? (
+
No MCP servers configured
+ ) : (
+ <>
+
+ MCP Servers
+
+
+ {mcpServerList.map(server => (
+
+ ))}
+ >
+ )
+ )}
+
+ {/* Client tab */}
+ {activeTab === 'client' && (
+ <>
+
+ Client MCP Servers
+
+
+ {addDialog && (
+
+ )}
+ {clientServers.length === 0 && !addDialog ? (
+
No client MCP servers configured
+ ) : (
+ clientServers.map(server => {
+ const status = connectionStatuses[server.id]?.status || 'disconnected'
+ const isActive = clientMCPActiveIds.includes(server.id)
+ const connTools = getConnectedTools?.().find(c => c.serverId === server.id)
+ return (
+
+ )
+ })
+ )}
+ >
+ )}
+
+ {/* Prompts tab */}
+ {activeTab === 'prompts' && promptsAvailable && (
+ <>
+ {promptArgsDialog ? (
+ <>
+
+ {promptArgsDialog.title || promptArgsDialog.name}
+
+ {promptArgsDialog.arguments.map(arg => (
+
+
+ onPromptArgsChange(arg.name, e.target.value)}
+ />
+
+ ))}
+
+
+
+
+ >
+ ) : mcpPromptsLoading ? (
+
Loading prompts...
+ ) : mcpPromptList.length === 0 ? (
+
No MCP prompts available
+ ) : (
+ <>
+
MCP Prompts
+ {mcpPromptList.map(prompt => (
+
onSelectPrompt(prompt)}
+ >
+
+ {prompt.title || prompt.name}
+ {prompt.description && (
+ {prompt.description}
+ )}
+
+
+ ))}
+ >
+ )}
+ >
+ )}
+
+ {/* Resources tab */}
+ {activeTab === 'resources' && resourcesAvailable && (
+ mcpResourcesLoading ? (
+
Loading resources...
+ ) : mcpResourceList.length === 0 ? (
+
No MCP resources available
+ ) : (
+ <>
+
MCP Resources
+ {mcpResourceList.map(resource => (
+
+ ))}
+ >
+ )
+ )}
+
+ )}
+
+
+
+ )
+}
diff --git a/core/http/react-ui/src/pages/Chat.jsx b/core/http/react-ui/src/pages/Chat.jsx
index 071d6830a..84cab9041 100644
--- a/core/http/react-ui/src/pages/Chat.jsx
+++ b/core/http/react-ui/src/pages/Chat.jsx
@@ -8,7 +8,7 @@ import CanvasPanel from '../components/CanvasPanel'
import { fileToBase64, modelsApi, mcpApi } from '../utils/api'
import { useMCPClient } from '../hooks/useMCPClient'
import MCPAppFrame from '../components/MCPAppFrame'
-import ClientMCPDropdown from '../components/ClientMCPDropdown'
+import UnifiedMCPDropdown from '../components/UnifiedMCPDropdown'
import { loadClientMCPServers } from '../utils/mcpClientStorage'
function relativeTime(ts) {
@@ -299,16 +299,13 @@ export default function Chat() {
const [editingName, setEditingName] = useState(null)
const [editName, setEditName] = useState('')
const [mcpAvailable, setMcpAvailable] = useState(false)
- const [mcpServersOpen, setMcpServersOpen] = useState(false)
const [mcpServerList, setMcpServerList] = useState([])
const [mcpServersLoading, setMcpServersLoading] = useState(false)
const [mcpServerCache, setMcpServerCache] = useState({})
- const [mcpPromptsOpen, setMcpPromptsOpen] = useState(false)
const [mcpPromptList, setMcpPromptList] = useState([])
const [mcpPromptsLoading, setMcpPromptsLoading] = useState(false)
const [mcpPromptArgsDialog, setMcpPromptArgsDialog] = useState(null)
const [mcpPromptArgsValues, setMcpPromptArgsValues] = useState({})
- const [mcpResourcesOpen, setMcpResourcesOpen] = useState(false)
const [mcpResourceList, setMcpResourceList] = useState([])
const [mcpResourcesLoading, setMcpResourcesLoading] = useState(false)
const [chatSearch, setChatSearch] = useState('')
@@ -366,18 +363,6 @@ export default function Chat() {
return () => { cancelled = true }
}, [activeChat?.model])
- const mcpDropdownRef = useRef(null)
- useEffect(() => {
- if (!mcpServersOpen) return
- const handleClick = (e) => {
- if (mcpDropdownRef.current && !mcpDropdownRef.current.contains(e.target)) {
- setMcpServersOpen(false)
- }
- }
- document.addEventListener('mousedown', handleClick)
- return () => document.removeEventListener('mousedown', handleClick)
- }, [mcpServersOpen])
-
const fetchMcpServers = useCallback(async () => {
const model = activeChat?.model
if (!model) return
@@ -407,30 +392,6 @@ export default function Chat() {
updateChatSettings(activeChat.id, { mcpServers: next })
}, [activeChat, updateChatSettings])
- const mcpPromptsRef = useRef(null)
- useEffect(() => {
- if (!mcpPromptsOpen) return
- const handleClick = (e) => {
- if (mcpPromptsRef.current && !mcpPromptsRef.current.contains(e.target)) {
- setMcpPromptsOpen(false)
- }
- }
- document.addEventListener('mousedown', handleClick)
- return () => document.removeEventListener('mousedown', handleClick)
- }, [mcpPromptsOpen])
-
- const mcpResourcesRef = useRef(null)
- useEffect(() => {
- if (!mcpResourcesOpen) return
- const handleClick = (e) => {
- if (mcpResourcesRef.current && !mcpResourcesRef.current.contains(e.target)) {
- setMcpResourcesOpen(false)
- }
- }
- document.addEventListener('mousedown', handleClick)
- return () => document.removeEventListener('mousedown', handleClick)
- }, [mcpResourcesOpen])
-
const fetchMcpPrompts = useCallback(async () => {
const model = activeChat?.model
if (!model) return
@@ -478,7 +439,7 @@ export default function Chat() {
} catch (e) {
addMessage(activeChat.id, { role: 'system', content: `Failed to expand prompt: ${e.message}` })
}
- setMcpPromptsOpen(false)
+
}, [activeChat?.model, activeChat?.id, addMessage])
const handleExpandPromptWithArgs = useCallback(async () => {
@@ -497,7 +458,7 @@ export default function Chat() {
}
setMcpPromptArgsDialog(null)
setMcpPromptArgsValues({})
- setMcpPromptsOpen(false)
+
}, [activeChat?.model, activeChat?.id, mcpPromptArgsDialog, mcpPromptArgsValues, addMessage])
const toggleMcpResource = useCallback((uri) => {
@@ -883,6 +844,9 @@ export default function Chat() {
/>
{activeChat.model && (
<>
+ {modelInfo?.backend && (
+ {modelInfo.backend}
+ )}
>
)}
- {mcpAvailable && (
-
-
- {mcpServersOpen && (
-
- {mcpServersLoading ? (
-
Loading servers...
- ) : mcpServerList.length === 0 ? (
-
No MCP servers configured
- ) : (
- <>
-
- MCP Servers
-
-
- {mcpServerList.map(server => (
-
- ))}
- >
- )}
-
- )}
-
- )}
- {mcpAvailable && (
-
-
- {mcpPromptsOpen && (
-
- {mcpPromptsLoading ? (
-
Loading prompts...
- ) : mcpPromptList.length === 0 ? (
-
No MCP prompts available
- ) : (
- <>
-
MCP Prompts
- {mcpPromptList.map(prompt => (
-
handleSelectPrompt(prompt)}
- >
-
- {prompt.title || prompt.name}
- {prompt.description && (
- {prompt.description}
- )}
-
-
- ))}
- >
- )}
-
- )}
- {mcpPromptArgsDialog && (
-
-
- {mcpPromptArgsDialog.title || mcpPromptArgsDialog.name}
-
- {mcpPromptArgsDialog.arguments.map(arg => (
-
-
- setMcpPromptArgsValues(prev => ({ ...prev, [arg.name]: e.target.value }))}
- />
-
- ))}
-
-
-
-
-
- )}
-
- )}
- {mcpAvailable && (
-
-
- {mcpResourcesOpen && (
-
- {mcpResourcesLoading ? (
-
Loading resources...
- ) : mcpResourceList.length === 0 ? (
-
No MCP resources available
- ) : (
- <>
-
MCP Resources
- {mcpResourceList.map(resource => (
-
- ))}
- >
- )}
-
- )}
-
- )}
- {
+ const allNames = mcpServerList.map(s => s.name)
+ const allSelected = allNames.every(n => (activeChat.mcpServers || []).includes(n))
+ updateChatSettings(activeChat.id, { mcpServers: allSelected ? [] : allNames })
+ }}
+ onFetchServers={fetchMcpServers}
+ clientMCPActiveIds={activeChat.clientMCPServers || []}
+ onClientToggle={handleClientMCPToggle}
+ onClientAdded={handleClientMCPServerAdded}
+ onClientRemoved={handleClientMCPServerRemoved}
connectionStatuses={connectionStatuses}
getConnectedTools={getConnectedTools}
+ promptsAvailable={mcpAvailable}
+ mcpPromptList={mcpPromptList}
+ mcpPromptsLoading={mcpPromptsLoading}
+ onFetchPrompts={fetchMcpPrompts}
+ onSelectPrompt={handleSelectPrompt}
+ promptArgsDialog={mcpPromptArgsDialog}
+ promptArgsValues={mcpPromptArgsValues}
+ onPromptArgsChange={(name, value) => setMcpPromptArgsValues(prev => ({ ...prev, [name]: value }))}
+ onPromptArgsSubmit={handleExpandPromptWithArgs}
+ onPromptArgsCancel={() => setMcpPromptArgsDialog(null)}
+ resourcesAvailable={mcpAvailable}
+ mcpResourceList={mcpResourceList}
+ mcpResourcesLoading={mcpResourcesLoading}
+ onFetchResources={fetchMcpResources}
+ selectedResources={activeChat.mcpResources || []}
+ onToggleResource={toggleMcpResource}
/>