diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 825e4e7..b52b461 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -275,6 +275,34 @@ const Dashboard = () => { } } + const handleServerToggle = async (server: Server, enabled: boolean) => { + try { + const token = localStorage.getItem('mcphub_token'); + const response = await fetch(`/api/servers/${server.name}/toggle`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-auth-token': token || '' + }, + body: JSON.stringify({ enabled }), + }); + + const result = await response.json(); + + if (!response.ok) { + console.error('Failed to toggle server:', result); + setError(t('server.toggleError', { serverName: server.name })); + return; + } + + // Update the UI immediately to reflect the change + setRefreshKey(prevKey => prevKey + 1); + } catch (err) { + console.error('Error toggling server:', err); + setError(err instanceof Error ? err.message : String(err)); + } + }; + const handleLogout = () => { logout() navigate('/login') @@ -333,6 +361,7 @@ const Dashboard = () => { server={server} onRemove={handleServerRemove} onEdit={handleServerEdit} + onToggle={handleServerToggle} /> ))} diff --git a/frontend/src/components/ServerCard.tsx b/frontend/src/components/ServerCard.tsx index 46d6e55..7f79181 100644 --- a/frontend/src/components/ServerCard.tsx +++ b/frontend/src/components/ServerCard.tsx @@ -10,12 +10,14 @@ interface ServerCardProps { server: Server onRemove: (serverName: string) => void onEdit: (server: Server) => void + onToggle?: (server: Server, enabled: boolean) => void } -const ServerCard = ({ server, onRemove, onEdit }: ServerCardProps) => { +const ServerCard = ({ server, onRemove, onEdit, onToggle }: ServerCardProps) => { const { t } = useTranslation() const [isExpanded, setIsExpanded] = useState(false) const [showDeleteDialog, setShowDeleteDialog] = useState(false) + const [isToggling, setIsToggling] = useState(false) const handleRemove = (e: React.MouseEvent) => { e.stopPropagation() @@ -27,6 +29,19 @@ const ServerCard = ({ server, onRemove, onEdit }: ServerCardProps) => { onEdit(server) } + const handleToggle = async (e: React.MouseEvent) => { + e.stopPropagation() + if (isToggling || !onToggle) return + + setIsToggling(true) + try { + // Toggle the server's enabled status + await onToggle(server, !(server.enabled !== false)) + } finally { + setIsToggling(false) + } + } + const handleConfirmDelete = () => { onRemove(server.name) setShowDeleteDialog(false) @@ -55,6 +70,26 @@ const ServerCard = ({ server, onRemove, onEdit }: ServerCardProps) => { > {t('server.delete')} +
+ +
diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 9469208..e27410c 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -3,7 +3,7 @@ "title": "MCP Hub Dashboard", "error": "Error", "closeButton": "Close", - "noServers": "No MCP servers available", + "noServers": "No servers found. Add a new server to get started.", "loading": "Loading...", "logout": "Logout", "profile": "Profile", @@ -24,7 +24,14 @@ "passwordsNotMatch": "New password and confirmation do not match", "changePasswordSuccess": "Password changed successfully", "changePasswordError": "Failed to change password", - "changePassword": "Change Password" + "changePassword": "Change Password", + "confirmNewPassword": "Confirm New Password", + "loginButton": "Login", + "changePasswordTitle": "Change Password", + "changePasswordButton": "Change Password", + "passwordsMustMatch": "Passwords must match", + "changeSuccess": "Password changed successfully", + "invalidCredentials": "Invalid username or password" }, "server": { "addServer": "Add Server", @@ -33,25 +40,34 @@ "delete": "Delete", "confirmDelete": "Are you sure you want to delete this server?", "status": "Status", - "tools": "Tools", - "name": "Server Name", - "url": "Server URL", + "tools": "Available Tools", + "name": "Name", + "url": "URL", "apiKey": "API Key", - "save": "Save Changes", + "save": "Save", "cancel": "Cancel", - "invalidConfig": "Could not find configuration data for {{serverName}}", + "invalidConfig": "Failed to get configuration for '{{serverName}}'", "addError": "Failed to add server", "editError": "Failed to edit server {{serverName}}", "deleteError": "Failed to delete server {{serverName}}", - "updateError": "Failed to update server", + "updateError": "Failed to update server '{{serverName}}'", "editTitle": "Edit Server: {{serverName}}", - "type": "Server Type", + "type": "Type", "command": "Command", "arguments": "Arguments", "envVars": "Environment Variables", - "key": "key", - "value": "value", - "remove": "Remove" + "key": "Key", + "value": "Value", + "remove": "Remove", + "deleteTitle": "Delete Server", + "deleteConfirm": "Are you sure you want to delete {{serverName}}?", + "deleteWarning": "This action cannot be undone.", + "enable": "Enable", + "disable": "Disable", + "toggleError": "Failed to toggle server: {{serverName}}", + "invalidData": "Invalid server data", + "alreadyExists": "Server '{{serverName}}' already exists", + "notFound": "Server '{{serverName}}' not found" }, "status": { "online": "Online", @@ -59,16 +75,17 @@ "connecting": "Connecting" }, "errors": { - "general": "Something went wrong", - "network": "Network connection error. Please check your internet connection", - "serverConnection": "Unable to connect to the server. Please check if the server is running", - "serverAdd": "Failed to add server. Please check the server status", - "serverUpdate": "Failed to edit server {{serverName}}. Please check the server status", - "serverFetch": "Failed to retrieve server data. Please try again later", - "initialStartup": "The server might be starting up. Please wait a moment as this process can take some time on first launch..." + "general": "An error occurred", + "network": "Network connection error. Please check your internet connection.", + "serverConnection": "Could not connect to server. Please try again later.", + "serverAdd": "Failed to add server.", + "serverUpdate": "Failed to update server '{{serverName}}'.", + "serverFetch": "Failed to fetch servers data.", + "initialStartup": "Server is starting up. Please wait..." }, "common": { "save": "Save", - "cancel": "Cancel" + "cancel": "Cancel", + "processing": "Processing..." } } \ No newline at end of file diff --git a/frontend/src/locales/zh.json b/frontend/src/locales/zh.json index e858d22..ef1e09e 100644 --- a/frontend/src/locales/zh.json +++ b/frontend/src/locales/zh.json @@ -3,7 +3,7 @@ "title": "MCP Hub 控制面板", "error": "错误", "closeButton": "关闭", - "noServers": "没有可用的 MCP 服务器", + "noServers": "未找到服务器。添加新服务器以开始使用。", "loading": "加载中...", "logout": "退出登录", "profile": "个人资料", @@ -14,44 +14,60 @@ "loginTitle": "登录 MCP Hub", "username": "用户名", "password": "密码", - "loggingIn": "登录中...", + "loggingIn": "正在登录...", "emptyFields": "用户名和密码不能为空", "loginFailed": "登录失败,请检查用户名和密码", - "loginError": "登录过程中出现错误", + "loginError": "登录时发生错误", "currentPassword": "当前密码", "newPassword": "新密码", "confirmPassword": "确认密码", - "passwordsNotMatch": "新密码与确认密码不一致", + "passwordsNotMatch": "新密码和确认密码不匹配", "changePasswordSuccess": "密码修改成功", - "changePasswordError": "修改密码失败", - "changePassword": "修改密码" + "changePasswordError": "密码修改失败", + "changePassword": "修改密码", + "confirmNewPassword": "确认新密码", + "loginButton": "登录", + "changePasswordTitle": "修改密码", + "changePasswordButton": "修改密码", + "passwordsMustMatch": "密码必须匹配", + "changeSuccess": "密码修改成功", + "invalidCredentials": "用户名或密码无效" }, "server": { "addServer": "添加服务器", "add": "添加", "edit": "编辑", "delete": "删除", - "confirmDelete": "您确定要删除此服务器吗?", + "confirmDelete": "确定要删除此服务器吗?", "status": "状态", - "tools": "工具", - "name": "服务器名称", - "url": "服务器 URL", + "tools": "可用工具", + "name": "名称", + "url": "URL", "apiKey": "API 密钥", - "save": "保存更改", + "save": "保存", "cancel": "取消", + "invalidConfig": "获取 '{{serverName}}' 的配置失败", "addError": "添加服务器失败", "editError": "编辑服务器 {{serverName}} 失败", - "invalidConfig": "无法找到 {{serverName}} 的配置数据", "deleteError": "删除服务器 {{serverName}} 失败", - "updateError": "更新服务器失败", + "updateError": "更新服务器 '{{serverName}}' 失败", "editTitle": "编辑服务器: {{serverName}}", - "type": "服务器类型", + "type": "类型", "command": "命令", "arguments": "参数", "envVars": "环境变量", "key": "键", "value": "值", - "remove": "移除" + "remove": "移除", + "deleteTitle": "删除服务器", + "deleteConfirm": "您确定要删除 {{serverName}} 吗?", + "deleteWarning": "此操作无法撤销。", + "enable": "启用", + "disable": "禁用", + "toggleError": "切换服务器 {{serverName}} 状态失败", + "invalidData": "无效的服务器数据", + "alreadyExists": "服务器 '{{serverName}}' 已存在", + "notFound": "未找到服务器 '{{serverName}}'" }, "status": { "online": "在线", @@ -60,15 +76,16 @@ }, "errors": { "general": "发生错误", - "network": "网络连接错误,请检查您的互联网连接", - "serverConnection": "无法连接到服务器,请检查服务器是否正在运行", - "serverAdd": "添加服务器失败,请检查服务器状态", - "serverUpdate": "编辑服务器 {{serverName}} 失败,请检查服务器状态", - "serverFetch": "获取服务器数据失败,请稍后重试", - "initialStartup": "服务器可能正在启动中。首次启动可能需要一些时间,请耐心等候..." + "network": "网络连接错误。请检查您的互联网连接。", + "serverConnection": "无法连接到服务器。请稍后再试。", + "serverAdd": "添加服务器失败。", + "serverUpdate": "更新服务器 '{{serverName}}' 失败。", + "serverFetch": "获取服务器数据失败。", + "initialStartup": "服务器正在启动。请稍候..." }, "common": { "save": "保存", - "cancel": "取消" + "cancel": "取消", + "processing": "处理中..." } } \ No newline at end of file diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index ecf0193..c5129fd 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -30,6 +30,7 @@ export interface Server { status: ServerStatus; tools?: Tool[]; config?: ServerConfig; + enabled?: boolean; } // 环境变量类型 diff --git a/src/controllers/serverController.ts b/src/controllers/serverController.ts index 8dd5f69..552d03c 100644 --- a/src/controllers/serverController.ts +++ b/src/controllers/serverController.ts @@ -6,6 +6,7 @@ import { removeServer, updateMcpServer, recreateMcpServer, + toggleServerStatus, } from '../services/mcpService.js'; import { loadSettings } from '../config/index.js'; @@ -207,3 +208,46 @@ export const getServerConfig = (req: Request, res: Response): void => { }); } }; + +export const toggleServer = async (req: Request, res: Response): Promise => { + try { + const { name } = req.params; + const { enabled } = req.body; + + if (!name) { + res.status(400).json({ + success: false, + message: 'Server name is required', + }); + return; + } + + if (typeof enabled !== 'boolean') { + res.status(400).json({ + success: false, + message: 'Enabled status must be a boolean', + }); + return; + } + + const result = await toggleServerStatus(name, enabled); + + if (result.success) { + recreateMcpServer(); + res.json({ + success: true, + message: result.message || `Server ${enabled ? 'enabled' : 'disabled'} successfully`, + }); + } else { + res.status(404).json({ + success: false, + message: result.message || 'Server not found or failed to toggle status', + }); + } + } catch (error) { + res.status(500).json({ + success: false, + message: 'Internal server error', + }); + } +}; diff --git a/src/routes/index.ts b/src/routes/index.ts index 2eca564..5e1c6ec 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -6,6 +6,7 @@ import { createServer, updateServer, deleteServer, + toggleServer, } from '../controllers/serverController.js'; import { login, @@ -24,6 +25,7 @@ export const initRoutes = (app: express.Application): void => { router.post('/servers', createServer); router.put('/servers/:name', updateServer); router.delete('/servers/:name', deleteServer); + router.post('/servers/:name/toggle', toggleServer); // Auth routes (these will NOT be protected by auth middleware) app.post('/auth/login', [ diff --git a/src/services/mcpService.ts b/src/services/mcpService.ts index f4e7f5f..c41479e 100644 --- a/src/services/mcpService.ts +++ b/src/services/mcpService.ts @@ -44,12 +44,28 @@ export const initializeClientsFromSettings = (): ServerInfo[] => { serverInfos = []; for (const [name, conf] of Object.entries(settings.mcpServers)) { + // Skip disabled servers + if (conf.enabled === false) { + console.log(`Skipping disabled server: ${name}`); + serverInfos.push({ + name, + status: 'disconnected', + tools: [], + createTime: Date.now(), + enabled: false + }); + continue; + } + // Check if server is already connected const existingServer = existingServerInfos.find( (s) => s.name === name && s.status === 'connected', ); if (existingServer) { - serverInfos.push(existingServer); + serverInfos.push({ + ...existingServer, + enabled: conf.enabled === undefined ? true : conf.enabled + }); console.log(`Server '${name}' is already connected.`); continue; } @@ -160,12 +176,18 @@ export const registerAllTools = async (server: McpServer, forceInit: boolean): P // Get all server information export const getServersInfo = (): Omit[] => { - return serverInfos.map(({ name, status, tools, createTime }) => ({ - name, - status, - tools, - createTime, - })); + const settings = loadSettings(); + return serverInfos.map(({ name, status, tools, createTime }) => { + const serverConfig = settings.mcpServers[name]; + const enabled = serverConfig ? (serverConfig.enabled !== false) : true; + return { + name, + status, + tools, + createTime, + enabled, + }; + }); }; // Get server information by name @@ -252,6 +274,51 @@ export const updateMcpServer = async ( } }; +// Toggle server enabled status +export const toggleServerStatus = async ( + name: string, + enabled: boolean +): Promise<{ success: boolean; message?: string }> => { + try { + const settings = loadSettings(); + if (!settings.mcpServers[name]) { + return { success: false, message: 'Server not found' }; + } + + // Update the enabled status in settings + settings.mcpServers[name].enabled = enabled; + + if (!saveSettings(settings)) { + return { success: false, message: 'Failed to save settings' }; + } + + // If disabling, disconnect the server and remove from active servers + if (!enabled) { + const serverInfo = serverInfos.find((serverInfo) => serverInfo.name === name); + if (serverInfo && serverInfo.client && serverInfo.transport) { + serverInfo.client.close(); + serverInfo.transport.close(); + console.log(`Closed client and transport for server: ${name}`); + } + + // Update the server info to show as disconnected and disabled + const index = serverInfos.findIndex(s => s.name === name); + if (index !== -1) { + serverInfos[index] = { + ...serverInfos[index], + status: 'disconnected', + enabled: false, + }; + } + } + + return { success: true, message: `Server ${enabled ? 'enabled' : 'disabled'} successfully` }; + } catch (error) { + console.error(`Failed to toggle server status: ${name}`, error); + return { success: false, message: 'Failed to toggle server status' }; + } +}; + // Create McpServer instance export const createMcpServer = (name: string, version: string): McpServer => { return new McpServer({ name, version }); diff --git a/src/types/index.ts b/src/types/index.ts index 82cade1..47ec1c6 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -23,6 +23,7 @@ export interface ServerConfig { command?: string; // Command to execute for stdio-based servers args?: string[]; // Arguments for the command env?: Record; // Environment variables + enabled?: boolean; // Flag to enable/disable the server } // Information about a server's status and tools @@ -33,6 +34,7 @@ export interface ServerInfo { client?: Client; // Client instance for communication transport?: SSEClientTransport | StdioClientTransport; // Transport mechanism used createTime: number; // Timestamp of when the server was created + enabled?: boolean; // Flag to indicate if the server is enabled } // Details about a tool available on the server