mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-30 21:49:13 -05:00
Merge pull request #8 from samanhappy/toggle
feat: add server toggle functionality
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -51,6 +51,8 @@
|
||||
"envVars": "环境变量",
|
||||
"key": "键",
|
||||
"value": "值",
|
||||
"enable": "启用",
|
||||
"disable": "禁用",
|
||||
"remove": "移除"
|
||||
},
|
||||
"status": {
|
||||
@@ -68,6 +70,7 @@
|
||||
"initialStartup": "服务器可能正在启动中。首次启动可能需要一些时间,请耐心等候..."
|
||||
},
|
||||
"common": {
|
||||
"processing": "处理中...",
|
||||
"save": "保存",
|
||||
"cancel": "取消"
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ export interface Server {
|
||||
status: ServerStatus;
|
||||
tools?: Tool[];
|
||||
config?: ServerConfig;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
// 环境变量类型
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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', [
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user