diff --git a/frontend/src/components/AddGroupForm.tsx b/frontend/src/components/AddGroupForm.tsx index f0548c4..98c6c85 100644 --- a/frontend/src/components/AddGroupForm.tsx +++ b/frontend/src/components/AddGroupForm.tsx @@ -2,8 +2,8 @@ import { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { useGroupData } from '@/hooks/useGroupData' import { useServerData } from '@/hooks/useServerData' -import { GroupFormData, Server } from '@/types' -import { ToggleGroup } from './ui/ToggleGroup' +import { GroupFormData, Server, IGroupServerConfig } from '@/types' +import { ServerToolConfig } from './ServerToolConfig' interface AddGroupFormProps { onAdd: () => void @@ -21,7 +21,7 @@ const AddGroupForm = ({ onAdd, onCancel }: AddGroupFormProps) => { const [formData, setFormData] = useState({ name: '', description: '', - servers: [] + servers: [] as IGroupServerConfig[] }) useEffect(() => { @@ -66,64 +66,68 @@ const AddGroupForm = ({ onAdd, onCancel }: AddGroupFormProps) => { return (
-
-
+
+

{t('groups.addNew')}

{error && ( -
+
{error}
)} - -
-
- - -
- - ({ - value: server.name, - label: server.name - }))} - onChange={(servers) => setFormData(prev => ({ ...prev, servers }))} - /> - -
- - -
-
+ +
+
+
+
+ + +
+ +
+ + setFormData(prev => ({ ...prev, servers }))} + className="border border-gray-200 rounded-lg p-4 bg-gray-50" + /> +
+
+
+ +
+ + +
+
) diff --git a/frontend/src/components/EditGroupForm.tsx b/frontend/src/components/EditGroupForm.tsx index d5038b6..3413e8a 100644 --- a/frontend/src/components/EditGroupForm.tsx +++ b/frontend/src/components/EditGroupForm.tsx @@ -1,9 +1,9 @@ import { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' -import { Group, GroupFormData, Server } from '@/types' +import { Group, GroupFormData, Server, IGroupServerConfig } from '@/types' import { useGroupData } from '@/hooks/useGroupData' import { useServerData } from '@/hooks/useServerData' -import { ToggleGroup } from './ui/ToggleGroup' +import { ServerToolConfig } from './ServerToolConfig' interface EditGroupFormProps { group: Group @@ -71,64 +71,68 @@ const EditGroupForm = ({ group, onEdit, onCancel }: EditGroupFormProps) => { return (
-
-
+
+

{t('groups.edit')}

{error && ( -
+
{error}
)} - -
-
- - -
- - ({ - value: server.name, - label: server.name - }))} - onChange={(servers) => setFormData(prev => ({ ...prev, servers }))} - /> - -
- - -
-
+ +
+
+
+
+ + +
+ +
+ + setFormData(prev => ({ ...prev, servers }))} + className="border border-gray-200 rounded-lg p-4 bg-gray-50" + /> +
+
+
+ +
+ + +
+
) diff --git a/frontend/src/components/GroupCard.tsx b/frontend/src/components/GroupCard.tsx index 059c816..165080c 100644 --- a/frontend/src/components/GroupCard.tsx +++ b/frontend/src/components/GroupCard.tsx @@ -1,7 +1,7 @@ import { useState, useRef, useEffect } from 'react' import { useTranslation } from 'react-i18next' -import { Group, Server } from '@/types' -import { Edit, Trash, Copy, Check, Link, FileCode, DropdownIcon } from '@/components/icons/LucideIcons' +import { Group, Server, IGroupServerConfig } from '@/types' +import { Edit, Trash, Copy, Check, Link, FileCode, DropdownIcon, Wrench } from '@/components/icons/LucideIcons' import DeleteDialog from '@/components/ui/DeleteDialog' import { useToast } from '@/contexts/ToastContext' import { useSettingsData } from '@/hooks/useSettingsData' @@ -25,6 +25,7 @@ const GroupCard = ({ const [showDeleteDialog, setShowDeleteDialog] = useState(false) const [copied, setCopied] = useState(false) const [showCopyDropdown, setShowCopyDropdown] = useState(false) + const [expandedServer, setExpandedServer] = useState(null) const dropdownRef = useRef(null) // Close dropdown when clicking outside @@ -108,8 +109,25 @@ const GroupCard = ({ copyToClipboard(JSON.stringify(jsonConfig, null, 2)) } + // Helper function to normalize group servers to get server names + const getServerNames = (servers: string[] | IGroupServerConfig[]): string[] => { + return servers.map(server => typeof server === 'string' ? server : server.name); + }; + + // Helper function to get server configuration + const getServerConfig = (serverName: string): IGroupServerConfig | undefined => { + const server = group.servers.find(s => + typeof s === 'string' ? s === serverName : s.name === serverName + ); + if (typeof server === 'string') { + return { name: server, tools: 'all' }; + } + return server; + }; + // Get servers that belong to this group - const groupServers = servers.filter(server => group.servers.includes(server.name)) + const serverNames = getServerNames(group.servers); + const groupServers = servers.filter(server => serverNames.includes(server.name)); return (
@@ -186,18 +204,68 @@ const GroupCard = ({ {groupServers.length === 0 ? (

{t('groups.noServers')}

) : ( -
- {groupServers.map(server => ( -
- {server.name} - -
- ))} +
+ {groupServers.map(server => { + const serverConfig = getServerConfig(server.name); + const hasToolRestrictions = serverConfig && serverConfig.tools !== 'all' && Array.isArray(serverConfig.tools); + const toolCount = hasToolRestrictions && Array.isArray(serverConfig?.tools) + ? serverConfig.tools.length + : (server.tools?.length || 0); // Show total tool count when all tools are selected + + const isExpanded = expandedServer === server.name; + + // Get tools list for display + const getToolsList = () => { + if (hasToolRestrictions && Array.isArray(serverConfig?.tools)) { + return serverConfig.tools; + } else if (server.tools && server.tools.length > 0) { + return server.tools.map(tool => tool.name); + } + return []; + }; + + const handleServerClick = () => { + setExpandedServer(isExpanded ? null : server.name); + }; + + return ( +
+
+ {server.name} + + {toolCount > 0 && ( + + + {toolCount} + + )} +
+ + {isExpanded && ( +
+
+ {hasToolRestrictions ? t('groups.selectedTools') : t('groups.allTools')}: +
+
+ {getToolsList().map((toolName, index) => ( + + {toolName} + + ))} +
+
+ )} +
+ ); + })}
)}
diff --git a/frontend/src/components/ServerToolConfig.tsx b/frontend/src/components/ServerToolConfig.tsx new file mode 100644 index 0000000..9e879fc --- /dev/null +++ b/frontend/src/components/ServerToolConfig.tsx @@ -0,0 +1,317 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { IGroupServerConfig, Server, Tool } from '@/types'; +import { cn } from '@/utils/cn'; + +interface ServerToolConfigProps { + servers: Server[]; + value: string[] | IGroupServerConfig[]; + onChange: (value: IGroupServerConfig[]) => void; + className?: string; +} + +export const ServerToolConfig: React.FC = ({ + servers, + value, + onChange, + className +}) => { + const { t } = useTranslation(); + const [expandedServers, setExpandedServers] = useState>(new Set()); + + // Normalize current value to IGroupServerConfig[] format + const normalizedValue: IGroupServerConfig[] = React.useMemo(() => { + return value.map(item => { + if (typeof item === 'string') { + return { name: item, tools: 'all' as const }; + } + return { ...item, tools: item.tools || 'all' as const }; + }); + }, [value]); + + // Get available servers (enabled only) + const availableServers = React.useMemo(() => + servers.filter(server => server.enabled !== false), + [servers] + ); + + // Clean up expanded servers when servers are removed from configuration + // But keep servers that were explicitly expanded even if they have no configuration + React.useEffect(() => { + const configuredServerNames = new Set(normalizedValue.map(config => config.name)); + const availableServerNames = new Set(availableServers.map(server => server.name)); + + setExpandedServers(prev => { + const newSet = new Set(); + prev.forEach(serverName => { + // Keep expanded if server is configured OR if server exists and user manually expanded it + if (configuredServerNames.has(serverName) || availableServerNames.has(serverName)) { + newSet.add(serverName); + } + }); + return newSet; + }); + }, [normalizedValue, availableServers]); + + const toggleServer = (serverName: string) => { + const existingIndex = normalizedValue.findIndex(config => config.name === serverName); + + if (existingIndex >= 0) { + // Remove server - this will also remove all its tools + const newValue = normalizedValue.filter(config => config.name !== serverName); + onChange(newValue); + // Don't auto-collapse the server when it's unchecked - let user control expansion manually + } else { + // Add server with all tools by default + const newValue = [...normalizedValue, { name: serverName, tools: 'all' as const }]; + onChange(newValue); + // Don't auto-expand the server when it's checked - let user control expansion manually + } + }; + + const toggleServerExpanded = (serverName: string) => { + setExpandedServers(prev => { + const newSet = new Set(prev); + if (newSet.has(serverName)) { + newSet.delete(serverName); + } else { + newSet.add(serverName); + } + return newSet; + }); + }; + + const updateServerTools = (serverName: string, tools: string[] | 'all', keepExpanded = false) => { + if (Array.isArray(tools) && tools.length === 0) { + // If no tools are selected, remove the server entirely + const newValue = normalizedValue.filter(config => config.name !== serverName); + onChange(newValue); + // Only collapse the server if not explicitly asked to keep it expanded + if (!keepExpanded) { + setExpandedServers(prev => { + const newSet = new Set(prev); + newSet.delete(serverName); + return newSet; + }); + } + } else { + // Update server tools or add server if it doesn't exist + const existingServerIndex = normalizedValue.findIndex(config => config.name === serverName); + + if (existingServerIndex >= 0) { + // Update existing server + const newValue = normalizedValue.map(config => + config.name === serverName ? { ...config, tools } : config + ); + onChange(newValue); + } else { + // Add new server with specified tools + const newValue = [...normalizedValue, { name: serverName, tools }]; + onChange(newValue); + } + } + }; + + const toggleTool = (serverName: string, toolName: string) => { + const server = availableServers.find(s => s.name === serverName); + if (!server) return; + + const allToolNames = server.tools?.map(tool => tool.name.replace(`${serverName}-`, '')) || []; + const serverConfig = normalizedValue.find(config => config.name === serverName); + + if (!serverConfig) { + // Server not selected yet, add it with only this tool + const newValue = [...normalizedValue, { name: serverName, tools: [toolName] }]; + onChange(newValue); + // Don't auto-expand - let user control expansion manually + return; + } + + if (serverConfig.tools === 'all') { + // Switch from 'all' to specific tools, excluding the toggled tool + const newTools = allToolNames.filter(name => name !== toolName); + updateServerTools(serverName, newTools); + // If all tools are deselected, the server will be removed and collapsed in updateServerTools + } else if (Array.isArray(serverConfig.tools)) { + const currentTools = serverConfig.tools; + if (currentTools.includes(toolName)) { + // Remove tool + const newTools = currentTools.filter(name => name !== toolName); + updateServerTools(serverName, newTools); + // If all tools are deselected, the server will be removed and collapsed in updateServerTools + } else { + // Add tool + const newTools = [...currentTools, toolName]; + + // If all tools are selected, switch to 'all' + if (newTools.length === allToolNames.length) { + updateServerTools(serverName, 'all'); + } else { + updateServerTools(serverName, newTools); + } + } + } + }; + + const isServerSelected = (serverName: string) => { + const serverConfig = normalizedValue.find(config => config.name === serverName); + if (!serverConfig) return false; + + // Server is considered "fully selected" if tools is 'all' + return serverConfig.tools === 'all'; + }; + + const isServerPartiallySelected = (serverName: string) => { + const serverConfig = normalizedValue.find(config => config.name === serverName); + if (!serverConfig) return false; + + // Server is partially selected if it has specific tools selected (not 'all' and not empty) + return Array.isArray(serverConfig.tools) && serverConfig.tools.length > 0; + }; + + const isToolSelected = (serverName: string, toolName: string) => { + const serverConfig = normalizedValue.find(config => config.name === serverName); + if (!serverConfig) return false; + + if (serverConfig.tools === 'all') return true; + if (Array.isArray(serverConfig.tools)) { + return serverConfig.tools.includes(toolName); + } + return false; + }; + + const getServerTools = (serverName: string): Tool[] => { + const server = availableServers.find(s => s.name === serverName); + return server?.tools || []; + }; + + return ( +
+
+ {availableServers.map(server => { + const isSelected = isServerSelected(server.name); + const isPartiallySelected = isServerPartiallySelected(server.name); + const isExpanded = expandedServers.has(server.name); + const serverTools = getServerTools(server.name); + const serverConfig = normalizedValue.find(config => config.name === server.name); + + return ( +
+
toggleServerExpanded(server.name)} + > +
{ + e.stopPropagation(); + toggleServer(server.name); + }} + > + toggleServer(server.name)} + className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500" + /> + + {server.name} + +
+ +
+ {serverConfig && serverConfig.tools !== 'all' && Array.isArray(serverConfig.tools) && ( + + ({t('groups.toolsSelected')} {serverConfig.tools.length}/{serverTools.length}) + + )} + {serverConfig && serverConfig.tools === 'all' && ( + + ({t('groups.allTools')} {serverTools.length}/{serverTools.length}) + + )} + + {serverTools.length > 0 && ( + + )} +
+
+ + {isExpanded && serverTools.length > 0 && ( +
+
+ + {t('groups.toolSelection')} + + +
+ +
+ {serverTools.map(tool => { + const toolName = tool.name.replace(`${server.name}-`, ''); + const isToolChecked = isToolSelected(server.name, toolName); + + return ( + + ); + })} +
+
+ )} +
+ ); + })} +
+ + {availableServers.length === 0 && ( +

{t('groups.noServerOptions')}

+ )} +
+ ); +}; diff --git a/frontend/src/components/icons/LucideIcons.tsx b/frontend/src/components/icons/LucideIcons.tsx index 28ad87f..582878f 100644 --- a/frontend/src/components/icons/LucideIcons.tsx +++ b/frontend/src/components/icons/LucideIcons.tsx @@ -16,7 +16,8 @@ import { AlertCircle, Link, FileCode, - ChevronDown as DropdownIcon + ChevronDown as DropdownIcon, + Wrench } from 'lucide-react' export { @@ -37,7 +38,8 @@ export { AlertCircle, Link, FileCode, - DropdownIcon + DropdownIcon, + Wrench } const LucideIcons = { diff --git a/frontend/src/hooks/useGroupData.ts b/frontend/src/hooks/useGroupData.ts index 852aa01..a550b2a 100644 --- a/frontend/src/hooks/useGroupData.ts +++ b/frontend/src/hooks/useGroupData.ts @@ -1,6 +1,6 @@ import { useState, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { Group, ApiResponse } from '@/types'; +import { Group, ApiResponse, IGroupServerConfig } from '@/types'; import { getApiUrl } from '../utils/runtime'; export const useGroupData = () => { @@ -49,7 +49,11 @@ export const useGroupData = () => { }, []); // Create a new group with server associations - const createGroup = async (name: string, description?: string, servers: string[] = []) => { + const createGroup = async ( + name: string, + description?: string, + servers: string[] | IGroupServerConfig[] = [], + ) => { try { const token = localStorage.getItem('mcphub_token'); const response = await fetch(getApiUrl('/groups'), { @@ -79,7 +83,7 @@ export const useGroupData = () => { // Update an existing group with server associations const updateGroup = async ( id: string, - data: { name?: string; description?: string; servers?: string[] }, + data: { name?: string; description?: string; servers?: string[] | IGroupServerConfig[] }, ) => { try { const token = localStorage.getItem('mcphub_token'); @@ -108,7 +112,7 @@ export const useGroupData = () => { }; // Update servers in a group (for batch updates) - const updateGroupServers = async (groupId: string, servers: string[]) => { + const updateGroupServers = async (groupId: string, servers: string[] | IGroupServerConfig[]) => { try { const token = localStorage.getItem('mcphub_token'); const response = await fetch(getApiUrl(`/groups/${groupId}/servers/batch`), { diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 1824d25..85f6db7 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -271,7 +271,14 @@ "noGroups": "No groups available. Create a new group to get started.", "noServers": "No servers in this group.", "noServerOptions": "No servers available", - "serverCount": "{{count}} Servers" + "serverCount": "{{count}} Servers", + "toolSelection": "Tool Selection", + "toolsSelected": "Selected", + "allTools": "All", + "selectedTools": "Selected tools", + "selectAll": "Select All", + "selectNone": "Select None", + "configureTools": "Configure Tools" }, "market": { "title": "Server Market", diff --git a/frontend/src/locales/zh.json b/frontend/src/locales/zh.json index 2a9d0a2..6b9d2ed 100644 --- a/frontend/src/locales/zh.json +++ b/frontend/src/locales/zh.json @@ -272,7 +272,14 @@ "noGroups": "暂无可用分组。创建一个新分组以开始使用。", "noServers": "此分组中没有服务器。", "noServerOptions": "没有可用的服务器", - "serverCount": "{{count}} 台服务器" + "serverCount": "{{count}} 台服务器", + "toolSelection": "工具选择", + "toolsSelected": "选择", + "allTools": "全部", + "selectedTools": "选中的工具", + "selectAll": "全选", + "selectNone": "全不选", + "configureTools": "配置工具" }, "market": { "title": "服务器市场", diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 6b33f06..bcfa03b 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -137,11 +137,17 @@ export interface Server { } // Group types +// Group server configuration - supports tool selection +export interface IGroupServerConfig { + name: string; // Server name + tools?: string[] | 'all'; // Array of specific tool names to include, or 'all' for all tools (default: 'all') +} + export interface Group { id: string; name: string; description?: string; - servers: string[]; + servers: string[] | IGroupServerConfig[]; // Supports both old and new format } // Environment variable types @@ -196,7 +202,7 @@ export interface ServerFormData { export interface GroupFormData { name: string; description: string; - servers: string[]; // Added servers array to include in form data + servers: string[] | IGroupServerConfig[]; // Updated to support new format } // API response types diff --git a/src/controllers/groupController.ts b/src/controllers/groupController.ts index 2fe1b1c..ed4994a 100644 --- a/src/controllers/groupController.ts +++ b/src/controllers/groupController.ts @@ -9,6 +9,9 @@ import { deleteGroup, addServerToGroup, removeServerFromGroup, + getServerConfigInGroup, + getServerConfigsInGroup, + updateServerToolsInGroup, } from '../services/groupService.js'; // Get all groups @@ -153,7 +156,7 @@ export const updateExistingGroup = (req: Request, res: Response): void => { } }; -// Update servers in a group (batch update) +// Update servers in a group (batch update) - supports both string[] and server config format export const updateGroupServersBatch = (req: Request, res: Response): void => { try { const { id } = req.params; @@ -170,11 +173,36 @@ export const updateGroupServersBatch = (req: Request, res: Response): void => { if (!Array.isArray(servers)) { res.status(400).json({ success: false, - message: 'Servers must be an array of server names', + message: 'Servers must be an array of server names or server configurations', }); return; } + // Validate server configurations if provided in new format + for (const server of servers) { + if (typeof server === 'object' && server !== null) { + if (!server.name || typeof server.name !== 'string') { + res.status(400).json({ + success: false, + message: 'Each server configuration must have a valid name', + }); + return; + } + if ( + server.tools && + server.tools !== 'all' && + (!Array.isArray(server.tools) || + !server.tools.every((tool: any) => typeof tool === 'string')) + ) { + res.status(400).json({ + success: false, + message: 'Tools must be "all" or an array of strings', + }); + return; + } + } + } + const updatedGroup = updateGroupServers(id, servers); if (!updatedGroup) { res.status(404).json({ @@ -343,3 +371,112 @@ export const getGroupServers = (req: Request, res: Response): void => { }); } }; + +// Get server configurations in a group (including tool selections) +export const getGroupServerConfigs = (req: Request, res: Response): void => { + try { + const { id } = req.params; + if (!id) { + res.status(400).json({ + success: false, + message: 'Group ID is required', + }); + return; + } + + const serverConfigs = getServerConfigsInGroup(id); + const response: ApiResponse = { + success: true, + data: serverConfigs, + }; + res.json(response); + } catch (error) { + res.status(500).json({ + success: false, + message: 'Failed to get group server configurations', + }); + } +}; + +// Get specific server configuration in a group +export const getGroupServerConfig = (req: Request, res: Response): void => { + try { + const { id, serverName } = req.params; + if (!id || !serverName) { + res.status(400).json({ + success: false, + message: 'Group ID and server name are required', + }); + return; + } + + const serverConfig = getServerConfigInGroup(id, serverName); + if (!serverConfig) { + res.status(404).json({ + success: false, + message: 'Server not found in group', + }); + return; + } + + const response: ApiResponse = { + success: true, + data: serverConfig, + }; + res.json(response); + } catch (error) { + res.status(500).json({ + success: false, + message: 'Failed to get server configuration', + }); + } +}; + +// Update tools for a specific server in a group +export const updateGroupServerTools = (req: Request, res: Response): void => { + try { + const { id, serverName } = req.params; + const { tools } = req.body; + + if (!id || !serverName) { + res.status(400).json({ + success: false, + message: 'Group ID and server name are required', + }); + return; + } + + // Validate tools parameter + if ( + tools !== 'all' && + (!Array.isArray(tools) || !tools.every((tool) => typeof tool === 'string')) + ) { + res.status(400).json({ + success: false, + message: 'Tools must be "all" or an array of strings', + }); + return; + } + + const updatedGroup = updateServerToolsInGroup(id, serverName, tools); + if (!updatedGroup) { + res.status(404).json({ + success: false, + message: 'Group or server not found', + }); + return; + } + + const response: ApiResponse = { + success: true, + data: updatedGroup, + message: 'Server tools updated successfully', + }; + res.json(response); + } 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 1ec0b55..f4789d1 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -22,6 +22,9 @@ import { removeServerFromExistingGroup, getGroupServers, updateGroupServersBatch, + getGroupServerConfigs, + getGroupServerConfig, + updateGroupServerTools, } from '../controllers/groupController.js'; import { getUsers, @@ -72,6 +75,10 @@ export const initRoutes = (app: express.Application): void => { router.get('/groups/:id/servers', getGroupServers); // New route for batch updating servers in a group router.put('/groups/:id/servers/batch', updateGroupServersBatch); + // New routes for server configurations and tool management in groups + router.get('/groups/:id/server-configs', getGroupServerConfigs); + router.get('/groups/:id/server-configs/:serverName', getGroupServerConfig); + router.put('/groups/:id/server-configs/:serverName/tools', updateGroupServerTools); // User management routes (admin only) router.get('/users', getUsers); diff --git a/src/services/groupService.ts b/src/services/groupService.ts index a1e06e5..19781b1 100644 --- a/src/services/groupService.ts +++ b/src/services/groupService.ts @@ -1,9 +1,21 @@ import { v4 as uuidv4 } from 'uuid'; -import { IGroup } from '../types/index.js'; +import { IGroup, IGroupServerConfig } from '../types/index.js'; import { loadSettings, saveSettings } from '../config/index.js'; import { notifyToolChanged } from './mcpService.js'; import { getDataService } from './services.js'; +// Helper function to normalize group servers configuration +const normalizeGroupServers = (servers: string[] | IGroupServerConfig[]): IGroupServerConfig[] => { + return servers.map((server) => { + if (typeof server === 'string') { + // Backward compatibility: string format means all tools + return { name: server, tools: 'all' }; + } + // New format: ensure tools defaults to 'all' if not specified + return { name: server.name, tools: server.tools || 'all' }; + }); +}; + // Get all groups export const getAllGroups = (): IGroup[] => { const settings = loadSettings(); @@ -32,7 +44,7 @@ export const getGroupByIdOrName = (key: string): IGroup | undefined => { export const createGroup = ( name: string, description?: string, - servers: string[] = [], + servers: string[] | IGroupServerConfig[] = [], owner?: string, ): IGroup | null => { try { @@ -44,8 +56,11 @@ export const createGroup = ( return null; } - // Filter out non-existent servers - const validServers = servers.filter((serverName) => settings.mcpServers[serverName]); + // Normalize servers configuration and filter out non-existent servers + const normalizedServers = normalizeGroupServers(servers); + const validServers: IGroupServerConfig[] = normalizedServers.filter( + (serverConfig) => settings.mcpServers[serverConfig.name], + ); const newGroup: IGroup = { id: uuidv4(), @@ -91,9 +106,12 @@ export const updateGroup = (id: string, data: Partial): IGroup | null => return null; } - // If servers array is provided, validate server existence + // If servers array is provided, validate server existence and normalize format if (data.servers) { - data.servers = data.servers.filter((serverName) => settings.mcpServers[serverName]); + const normalizedServers = normalizeGroupServers(data.servers); + data.servers = normalizedServers.filter( + (serverConfig) => settings.mcpServers[serverConfig.name], + ); } const updatedGroup = { @@ -116,7 +134,11 @@ export const updateGroup = (id: string, data: Partial): IGroup | null => }; // Update servers in a group (batch update) -export const updateGroupServers = (groupId: string, servers: string[]): IGroup | null => { +// Update group servers (maintaining backward compatibility) +export const updateGroupServers = ( + groupId: string, + servers: string[] | IGroupServerConfig[], +): IGroup | null => { try { const settings = loadSettings(); if (!settings.groups) { @@ -128,8 +150,11 @@ export const updateGroupServers = (groupId: string, servers: string[]): IGroup | return null; } - // Filter out non-existent servers - const validServers = servers.filter((serverName) => settings.mcpServers[serverName]); + // Normalize and filter out non-existent servers + const normalizedServers = normalizeGroupServers(servers); + const validServers = normalizedServers.filter( + (serverConfig) => settings.mcpServers[serverConfig.name], + ); settings.groups[groupIndex].servers = validServers; @@ -186,10 +211,12 @@ export const addServerToGroup = (groupId: string, serverName: string): IGroup | } const group = settings.groups[groupIndex]; + const normalizedServers = normalizeGroupServers(group.servers); // Add server to group if not already in it - if (!group.servers.includes(serverName)) { - group.servers.push(serverName); + if (!normalizedServers.some((server) => server.name === serverName)) { + normalizedServers.push({ name: serverName, tools: 'all' }); + group.servers = normalizedServers; if (!saveSettings(settings)) { return null; @@ -218,7 +245,8 @@ export const removeServerFromGroup = (groupId: string, serverName: string): IGro } const group = settings.groups[groupIndex]; - group.servers = group.servers.filter((name) => name !== serverName); + const normalizedServers = normalizeGroupServers(group.servers); + group.servers = normalizedServers.filter((server) => server.name !== serverName); if (!saveSettings(settings)) { return null; @@ -234,5 +262,71 @@ export const removeServerFromGroup = (groupId: string, serverName: string): IGro // Get all servers in a group export const getServersInGroup = (groupId: string): string[] => { const group = getGroupByIdOrName(groupId); - return group ? group.servers : []; + if (!group) return []; + const normalizedServers = normalizeGroupServers(group.servers); + return normalizedServers.map((server) => server.name); +}; + +// Get server configuration from group (including tool selection) +export const getServerConfigInGroup = ( + groupId: string, + serverName: string, +): IGroupServerConfig | undefined => { + const group = getGroupByIdOrName(groupId); + if (!group) return undefined; + const normalizedServers = normalizeGroupServers(group.servers); + return normalizedServers.find((server) => server.name === serverName); +}; + +// Get all server configurations in a group +export const getServerConfigsInGroup = (groupId: string): IGroupServerConfig[] => { + const group = getGroupByIdOrName(groupId); + if (!group) return []; + return normalizeGroupServers(group.servers); +}; + +// Update tools selection for a specific server in a group +export const updateServerToolsInGroup = ( + groupId: string, + serverName: string, + tools: string[] | 'all', +): IGroup | null => { + try { + const settings = loadSettings(); + if (!settings.groups) { + return null; + } + + const groupIndex = settings.groups.findIndex((group) => group.id === groupId); + if (groupIndex === -1) { + return null; + } + + // Verify server exists + if (!settings.mcpServers[serverName]) { + return null; + } + + const group = settings.groups[groupIndex]; + const normalizedServers = normalizeGroupServers(group.servers); + + const serverIndex = normalizedServers.findIndex((server) => server.name === serverName); + if (serverIndex === -1) { + return null; // Server not in group + } + + // Update the tools configuration for the server + normalizedServers[serverIndex].tools = tools; + group.servers = normalizedServers; + + if (!saveSettings(settings)) { + return null; + } + + notifyToolChanged(); + return group; + } catch (error) { + console.error(`Failed to update tools for server ${serverName} in group ${groupId}:`, error); + return null; + } }; diff --git a/src/services/mcpService.ts b/src/services/mcpService.ts index a901b94..5e0e603 100644 --- a/src/services/mcpService.ts +++ b/src/services/mcpService.ts @@ -8,7 +8,7 @@ import { ServerInfo, ServerConfig, ToolInfo } from '../types/index.js'; import { loadSettings, saveSettings, expandEnvVars, replaceEnvVars } from '../config/index.js'; import config from '../config/index.js'; import { getGroup } from './sseService.js'; -import { getServersInGroup } from './groupService.js'; +import { getServersInGroup, getServerConfigInGroup } from './groupService.js'; import { saveToolsAsVectorEmbeddings, searchToolsByVector } from './vectorSearchService.js'; import { OpenAPIClient } from '../clients/openapi.js'; import { getDataService } from './services.js'; @@ -823,10 +823,22 @@ Available servers: ${serversList}`; const allTools = []; for (const serverInfo of allServerInfos) { if (serverInfo.tools && serverInfo.tools.length > 0) { - // Filter tools based on server configuration and apply custom descriptions - const enabledTools = filterToolsByConfig(serverInfo.name, serverInfo.tools); + // Filter tools based on server configuration + let enabledTools = filterToolsByConfig(serverInfo.name, serverInfo.tools); - // Apply custom descriptions from configuration + // If this is a group request, apply group-level tool filtering + if (group) { + const serverConfig = getServerConfigInGroup(group, serverInfo.name); + if (serverConfig && serverConfig.tools !== 'all' && Array.isArray(serverConfig.tools)) { + // Filter tools based on group configuration + const allowedToolNames = serverConfig.tools.map( + (toolName) => `${serverInfo.name}-${toolName}`, + ); + enabledTools = enabledTools.filter((tool) => allowedToolNames.includes(tool.name)); + } + } + + // Apply custom descriptions from server configuration const settings = loadSettings(); const serverConfig = settings.mcpServers[serverInfo.name]; const toolsWithCustomDescriptions = enabledTools.map((tool) => { diff --git a/src/types/index.ts b/src/types/index.ts index 7377fa3..55f2fb9 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -17,10 +17,16 @@ export interface IGroup { id: string; // Unique UUID for the group name: string; // Display name of the group description?: string; // Optional description of the group - servers: string[]; // Array of server names that belong to this group + servers: string[] | IGroupServerConfig[]; // Array of server names or server configurations that belong to this group owner?: string; // Owner of the group, defaults to 'admin' user } +// Server configuration within a group - supports tool selection +export interface IGroupServerConfig { + name: string; // Server name + tools?: string[] | 'all'; // Array of specific tool names to include, or 'all' for all tools (default: 'all') +} + // Market server types export interface MarketServerRepository { type: string;