Merge pull request #8 from samanhappy/toggle

feat: add server toggle functionality
This commit is contained in:
samanhappy
2025-04-15 13:07:42 +08:00
committed by GitHub
9 changed files with 201 additions and 11 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 = () => {
logout()
navigate('/login')
@@ -333,6 +361,7 @@ const Dashboard = () => {
server={server}
onRemove={handleServerRemove}
onEdit={handleServerEdit}
onToggle={handleServerToggle}
/>
))}
</div>

View File

@@ -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,19 +29,31 @@ const ServerCard = ({ server, onRemove, onEdit }: ServerCardProps) => {
onEdit(server)
}
const handleToggle = async (e: React.MouseEvent) => {
e.stopPropagation()
if (isToggling || !onToggle) return
setIsToggling(true)
try {
await onToggle(server, !(server.enabled !== false))
} finally {
setIsToggling(false)
}
}
const handleConfirmDelete = () => {
onRemove(server.name)
setShowDeleteDialog(false)
}
return (
<div className="bg-white shadow rounded-lg p-6 mb-6">
<div className={`bg-white shadow rounded-lg p-6 mb-6 ${server.enabled === false ? 'opacity-60' : ''}`}>
<div
className="flex justify-between items-center cursor-pointer"
onClick={() => setIsExpanded(!isExpanded)}
>
<div className="flex items-center space-x-3">
<h2 className="text-xl font-semibold text-gray-900">{server.name}</h2>
<h2 className={`text-xl font-semibold ${server.enabled === false ? 'text-gray-600' : 'text-gray-900'}`}>{server.name}</h2>
<Badge status={server.status} />
</div>
<div className="flex space-x-2">
@@ -55,6 +69,26 @@ const ServerCard = ({ server, onRemove, onEdit }: ServerCardProps) => {
>
{t('server.delete')}
</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">
{isExpanded ? <ChevronDown size={18} /> : <ChevronRight size={18} />}
</button>
@@ -70,7 +104,7 @@ const ServerCard = ({ server, onRemove, onEdit }: ServerCardProps) => {
{isExpanded && server.tools && (
<div className="mt-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">{t('server.tools')}</h3>
<h3 className={`text-lg font-medium ${server.enabled === false ? 'text-gray-600' : 'text-gray-900'} mb-4`}>{t('server.tools')}</h3>
<div className="space-y-4">
{server.tools.map((tool, index) => (
<ToolCard key={index} tool={tool} />

View File

@@ -51,6 +51,8 @@
"envVars": "Environment Variables",
"key": "key",
"value": "value",
"enable": "Enable",
"disable": "Disable",
"remove": "Remove"
},
"status": {
@@ -68,6 +70,7 @@
"initialStartup": "The server might be starting up. Please wait a moment as this process can take some time on first launch..."
},
"common": {
"processing": "Processing...",
"save": "Save",
"cancel": "Cancel"
}

View File

@@ -51,6 +51,8 @@
"envVars": "环境变量",
"key": "键",
"value": "值",
"enable": "启用",
"disable": "禁用",
"remove": "移除"
},
"status": {
@@ -68,6 +70,7 @@
"initialStartup": "服务器可能正在启动中。首次启动可能需要一些时间,请耐心等候..."
},
"common": {
"processing": "处理中...",
"save": "保存",
"cancel": "取消"
}

View File

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

View File

@@ -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<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,
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', [

View File

@@ -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,23 @@ export const registerAllTools = async (server: McpServer, forceInit: boolean): P
// Get all server information
export const getServersInfo = (): Omit<ServerInfo, 'client' | 'transport'>[] => {
return serverInfos.map(({ name, status, tools, createTime }) => ({
name,
status,
tools,
createTime,
}));
const settings = loadSettings();
const infos = serverInfos.map(({ name, status, tools, createTime }) => {
const serverConfig = settings.mcpServers[name];
const enabled = serverConfig ? (serverConfig.enabled !== false) : true;
return {
name,
status,
tools,
createTime,
enabled,
};
});
infos.sort((a, b) => {
if (a.enabled === b.enabled) return 0;
return a.enabled ? -1 : 1;
});
return infos;
};
// Get server information by name
@@ -252,6 +279,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 });

View File

@@ -23,6 +23,7 @@ export interface ServerConfig {
command?: string; // Command to execute for stdio-based servers
args?: string[]; // Arguments for the command
env?: Record<string, string>; // 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