feat: add server toggle functionality

This commit is contained in:
samanhappy@qq.com
2025-04-14 22:25:07 +08:00
parent 40d8792294
commit 2e1f73ef64
9 changed files with 264 additions and 50 deletions

View File

@@ -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 = () => { const handleLogout = () => {
logout() logout()
navigate('/login') navigate('/login')
@@ -333,6 +361,7 @@ const Dashboard = () => {
server={server} server={server}
onRemove={handleServerRemove} onRemove={handleServerRemove}
onEdit={handleServerEdit} onEdit={handleServerEdit}
onToggle={handleServerToggle}
/> />
))} ))}
</div> </div>

View File

@@ -10,12 +10,14 @@ interface ServerCardProps {
server: Server server: Server
onRemove: (serverName: string) => void onRemove: (serverName: string) => void
onEdit: (server: Server) => 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 { t } = useTranslation()
const [isExpanded, setIsExpanded] = useState(false) const [isExpanded, setIsExpanded] = useState(false)
const [showDeleteDialog, setShowDeleteDialog] = useState(false) const [showDeleteDialog, setShowDeleteDialog] = useState(false)
const [isToggling, setIsToggling] = useState(false)
const handleRemove = (e: React.MouseEvent) => { const handleRemove = (e: React.MouseEvent) => {
e.stopPropagation() e.stopPropagation()
@@ -27,6 +29,19 @@ const ServerCard = ({ server, onRemove, onEdit }: ServerCardProps) => {
onEdit(server) 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 = () => { const handleConfirmDelete = () => {
onRemove(server.name) onRemove(server.name)
setShowDeleteDialog(false) setShowDeleteDialog(false)
@@ -55,6 +70,26 @@ const ServerCard = ({ server, onRemove, onEdit }: ServerCardProps) => {
> >
{t('server.delete')} {t('server.delete')}
</button> </button>
<div className="flex items-center">
<button
onClick={handleToggle}
className={`px-3 py-1 text-sm rounded transition-colors ${
isToggling
? 'bg-gray-200 text-gray-500'
: server.enabled !== false
? 'bg-green-100 text-green-800 hover:bg-green-200'
: 'bg-gray-100 text-gray-800 hover:bg-gray-200'
}`}
disabled={isToggling}
>
{isToggling
? t('common.processing')
: server.enabled !== false
? t('server.disable')
: t('server.enable')
}
</button>
</div>
<button className="text-gray-400 hover:text-gray-600"> <button className="text-gray-400 hover:text-gray-600">
{isExpanded ? <ChevronDown size={18} /> : <ChevronRight size={18} />} {isExpanded ? <ChevronDown size={18} /> : <ChevronRight size={18} />}
</button> </button>

View File

@@ -3,7 +3,7 @@
"title": "MCP Hub Dashboard", "title": "MCP Hub Dashboard",
"error": "Error", "error": "Error",
"closeButton": "Close", "closeButton": "Close",
"noServers": "No MCP servers available", "noServers": "No servers found. Add a new server to get started.",
"loading": "Loading...", "loading": "Loading...",
"logout": "Logout", "logout": "Logout",
"profile": "Profile", "profile": "Profile",
@@ -24,7 +24,14 @@
"passwordsNotMatch": "New password and confirmation do not match", "passwordsNotMatch": "New password and confirmation do not match",
"changePasswordSuccess": "Password changed successfully", "changePasswordSuccess": "Password changed successfully",
"changePasswordError": "Failed to change password", "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": { "server": {
"addServer": "Add Server", "addServer": "Add Server",
@@ -33,25 +40,34 @@
"delete": "Delete", "delete": "Delete",
"confirmDelete": "Are you sure you want to delete this server?", "confirmDelete": "Are you sure you want to delete this server?",
"status": "Status", "status": "Status",
"tools": "Tools", "tools": "Available Tools",
"name": "Server Name", "name": "Name",
"url": "Server URL", "url": "URL",
"apiKey": "API Key", "apiKey": "API Key",
"save": "Save Changes", "save": "Save",
"cancel": "Cancel", "cancel": "Cancel",
"invalidConfig": "Could not find configuration data for {{serverName}}", "invalidConfig": "Failed to get configuration for '{{serverName}}'",
"addError": "Failed to add server", "addError": "Failed to add server",
"editError": "Failed to edit server {{serverName}}", "editError": "Failed to edit server {{serverName}}",
"deleteError": "Failed to delete server {{serverName}}", "deleteError": "Failed to delete server {{serverName}}",
"updateError": "Failed to update server", "updateError": "Failed to update server '{{serverName}}'",
"editTitle": "Edit Server: {{serverName}}", "editTitle": "Edit Server: {{serverName}}",
"type": "Server Type", "type": "Type",
"command": "Command", "command": "Command",
"arguments": "Arguments", "arguments": "Arguments",
"envVars": "Environment Variables", "envVars": "Environment Variables",
"key": "key", "key": "Key",
"value": "value", "value": "Value",
"remove": "Remove" "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": { "status": {
"online": "Online", "online": "Online",
@@ -59,16 +75,17 @@
"connecting": "Connecting" "connecting": "Connecting"
}, },
"errors": { "errors": {
"general": "Something went wrong", "general": "An error occurred",
"network": "Network connection error. Please check your internet connection", "network": "Network connection error. Please check your internet connection.",
"serverConnection": "Unable to connect to the server. Please check if the server is running", "serverConnection": "Could not connect to server. Please try again later.",
"serverAdd": "Failed to add server. Please check the server status", "serverAdd": "Failed to add server.",
"serverUpdate": "Failed to edit server {{serverName}}. Please check the server status", "serverUpdate": "Failed to update server '{{serverName}}'.",
"serverFetch": "Failed to retrieve server data. Please try again later", "serverFetch": "Failed to fetch servers data.",
"initialStartup": "The server might be starting up. Please wait a moment as this process can take some time on first launch..." "initialStartup": "Server is starting up. Please wait..."
}, },
"common": { "common": {
"save": "Save", "save": "Save",
"cancel": "Cancel" "cancel": "Cancel",
"processing": "Processing..."
} }
} }

View File

@@ -3,7 +3,7 @@
"title": "MCP Hub 控制面板", "title": "MCP Hub 控制面板",
"error": "错误", "error": "错误",
"closeButton": "关闭", "closeButton": "关闭",
"noServers": "没有可用的 MCP 服务器", "noServers": "未找到服务器。添加新服务器以开始使用。",
"loading": "加载中...", "loading": "加载中...",
"logout": "退出登录", "logout": "退出登录",
"profile": "个人资料", "profile": "个人资料",
@@ -14,44 +14,60 @@
"loginTitle": "登录 MCP Hub", "loginTitle": "登录 MCP Hub",
"username": "用户名", "username": "用户名",
"password": "密码", "password": "密码",
"loggingIn": "登录...", "loggingIn": "正在登录...",
"emptyFields": "用户名和密码不能为空", "emptyFields": "用户名和密码不能为空",
"loginFailed": "登录失败,请检查用户名和密码", "loginFailed": "登录失败,请检查用户名和密码",
"loginError": "登录过程中出现错误", "loginError": "登录时发生错误",
"currentPassword": "当前密码", "currentPassword": "当前密码",
"newPassword": "新密码", "newPassword": "新密码",
"confirmPassword": "确认密码", "confirmPassword": "确认密码",
"passwordsNotMatch": "新密码确认密码不一致", "passwordsNotMatch": "新密码确认密码不匹配",
"changePasswordSuccess": "密码修改成功", "changePasswordSuccess": "密码修改成功",
"changePasswordError": "修改密码失败", "changePasswordError": "密码修改失败",
"changePassword": "修改密码" "changePassword": "修改密码",
"confirmNewPassword": "确认新密码",
"loginButton": "登录",
"changePasswordTitle": "修改密码",
"changePasswordButton": "修改密码",
"passwordsMustMatch": "密码必须匹配",
"changeSuccess": "密码修改成功",
"invalidCredentials": "用户名或密码无效"
}, },
"server": { "server": {
"addServer": "添加服务器", "addServer": "添加服务器",
"add": "添加", "add": "添加",
"edit": "编辑", "edit": "编辑",
"delete": "删除", "delete": "删除",
"confirmDelete": "确定要删除此服务器吗?", "confirmDelete": "确定要删除此服务器吗?",
"status": "状态", "status": "状态",
"tools": "工具", "tools": "可用工具",
"name": "服务器名称", "name": "名称",
"url": "服务器 URL", "url": "URL",
"apiKey": "API 密钥", "apiKey": "API 密钥",
"save": "保存更改", "save": "保存",
"cancel": "取消", "cancel": "取消",
"invalidConfig": "获取 '{{serverName}}' 的配置失败",
"addError": "添加服务器失败", "addError": "添加服务器失败",
"editError": "编辑服务器 {{serverName}} 失败", "editError": "编辑服务器 {{serverName}} 失败",
"invalidConfig": "无法找到 {{serverName}} 的配置数据",
"deleteError": "删除服务器 {{serverName}} 失败", "deleteError": "删除服务器 {{serverName}} 失败",
"updateError": "更新服务器失败", "updateError": "更新服务器 '{{serverName}}' 失败",
"editTitle": "编辑服务器: {{serverName}}", "editTitle": "编辑服务器: {{serverName}}",
"type": "服务器类型", "type": "类型",
"command": "命令", "command": "命令",
"arguments": "参数", "arguments": "参数",
"envVars": "环境变量", "envVars": "环境变量",
"key": "键", "key": "键",
"value": "值", "value": "值",
"remove": "移除" "remove": "移除",
"deleteTitle": "删除服务器",
"deleteConfirm": "您确定要删除 {{serverName}} 吗?",
"deleteWarning": "此操作无法撤销。",
"enable": "启用",
"disable": "禁用",
"toggleError": "切换服务器 {{serverName}} 状态失败",
"invalidData": "无效的服务器数据",
"alreadyExists": "服务器 '{{serverName}}' 已存在",
"notFound": "未找到服务器 '{{serverName}}'"
}, },
"status": { "status": {
"online": "在线", "online": "在线",
@@ -60,15 +76,16 @@
}, },
"errors": { "errors": {
"general": "发生错误", "general": "发生错误",
"network": "网络连接错误请检查您的互联网连接", "network": "网络连接错误请检查您的互联网连接",
"serverConnection": "无法连接到服务器,请检查服务器是否正在运行", "serverConnection": "无法连接到服务器。请稍后再试。",
"serverAdd": "添加服务器失败,请检查服务器状态", "serverAdd": "添加服务器失败",
"serverUpdate": "编辑服务器 {{serverName}} 失败,请检查服务器状态", "serverUpdate": "更新服务器 '{{serverName}}' 失败",
"serverFetch": "获取服务器数据失败,请稍后重试", "serverFetch": "获取服务器数据失败",
"initialStartup": "服务器可能正在启动中。首次启动可能需要一些时间,请耐心等候..." "initialStartup": "服务器正在启动。请稍候..."
}, },
"common": { "common": {
"save": "保存", "save": "保存",
"cancel": "取消" "cancel": "取消",
"processing": "处理中..."
} }
} }

View File

@@ -30,6 +30,7 @@ export interface Server {
status: ServerStatus; status: ServerStatus;
tools?: Tool[]; tools?: Tool[];
config?: ServerConfig; config?: ServerConfig;
enabled?: boolean;
} }
// 环境变量类型 // 环境变量类型

View File

@@ -6,6 +6,7 @@ import {
removeServer, removeServer,
updateMcpServer, updateMcpServer,
recreateMcpServer, recreateMcpServer,
toggleServerStatus,
} from '../services/mcpService.js'; } from '../services/mcpService.js';
import { loadSettings } from '../config/index.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<void> => {
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',
});
}
};

View File

@@ -6,6 +6,7 @@ import {
createServer, createServer,
updateServer, updateServer,
deleteServer, deleteServer,
toggleServer,
} from '../controllers/serverController.js'; } from '../controllers/serverController.js';
import { import {
login, login,
@@ -24,6 +25,7 @@ export const initRoutes = (app: express.Application): void => {
router.post('/servers', createServer); router.post('/servers', createServer);
router.put('/servers/:name', updateServer); router.put('/servers/:name', updateServer);
router.delete('/servers/:name', deleteServer); router.delete('/servers/:name', deleteServer);
router.post('/servers/:name/toggle', toggleServer);
// Auth routes (these will NOT be protected by auth middleware) // Auth routes (these will NOT be protected by auth middleware)
app.post('/auth/login', [ app.post('/auth/login', [

View File

@@ -44,12 +44,28 @@ export const initializeClientsFromSettings = (): ServerInfo[] => {
serverInfos = []; serverInfos = [];
for (const [name, conf] of Object.entries(settings.mcpServers)) { 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 // Check if server is already connected
const existingServer = existingServerInfos.find( const existingServer = existingServerInfos.find(
(s) => s.name === name && s.status === 'connected', (s) => s.name === name && s.status === 'connected',
); );
if (existingServer) { if (existingServer) {
serverInfos.push(existingServer); serverInfos.push({
...existingServer,
enabled: conf.enabled === undefined ? true : conf.enabled
});
console.log(`Server '${name}' is already connected.`); console.log(`Server '${name}' is already connected.`);
continue; continue;
} }
@@ -160,12 +176,18 @@ export const registerAllTools = async (server: McpServer, forceInit: boolean): P
// Get all server information // Get all server information
export const getServersInfo = (): Omit<ServerInfo, 'client' | 'transport'>[] => { export const getServersInfo = (): Omit<ServerInfo, 'client' | 'transport'>[] => {
return serverInfos.map(({ name, status, tools, createTime }) => ({ const settings = loadSettings();
name, return serverInfos.map(({ name, status, tools, createTime }) => {
status, const serverConfig = settings.mcpServers[name];
tools, const enabled = serverConfig ? (serverConfig.enabled !== false) : true;
createTime, return {
})); name,
status,
tools,
createTime,
enabled,
};
});
}; };
// Get server information by name // 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 // Create McpServer instance
export const createMcpServer = (name: string, version: string): McpServer => { export const createMcpServer = (name: string, version: string): McpServer => {
return new McpServer({ name, version }); return new McpServer({ name, version });

View File

@@ -23,6 +23,7 @@ export interface ServerConfig {
command?: string; // Command to execute for stdio-based servers command?: string; // Command to execute for stdio-based servers
args?: string[]; // Arguments for the command args?: string[]; // Arguments for the command
env?: Record<string, string>; // Environment variables env?: Record<string, string>; // Environment variables
enabled?: boolean; // Flag to enable/disable the server
} }
// Information about a server's status and tools // Information about a server's status and tools
@@ -33,6 +34,7 @@ export interface ServerInfo {
client?: Client; // Client instance for communication client?: Client; // Client instance for communication
transport?: SSEClientTransport | StdioClientTransport; // Transport mechanism used transport?: SSEClientTransport | StdioClientTransport; // Transport mechanism used
createTime: number; // Timestamp of when the server was created 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 // Details about a tool available on the server