mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-24 02:39:19 -05:00
feat: Enhance group management with server tool configuration (#250)
This commit is contained in:
@@ -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<GroupFormData>({
|
||||
name: '',
|
||||
description: '',
|
||||
servers: []
|
||||
servers: [] as IGroupServerConfig[]
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
@@ -66,18 +66,21 @@ const AddGroupForm = ({ onAdd, onCancel }: AddGroupFormProps) => {
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-lg shadow-lg max-w-md w-full">
|
||||
<div className="p-6">
|
||||
<div className="bg-white rounded-lg shadow-lg max-w-2xl w-full max-h-[90vh] flex flex-col">
|
||||
<div className="p-6 flex-shrink-0">
|
||||
<h2 className="text-xl font-semibold text-gray-800 mb-4">{t('groups.addNew')}</h2>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-red-100 text-red-700 rounded">
|
||||
<div className="mb-4 p-3 bg-red-100 text-red-700 rounded-md border border-gray-200">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-4">
|
||||
<form onSubmit={handleSubmit} className="flex flex-col flex-1 min-h-0">
|
||||
<div className="flex-1 overflow-y-auto px-6">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="name">
|
||||
{t('groups.name')} *
|
||||
</label>
|
||||
@@ -87,36 +90,38 @@ const AddGroupForm = ({ onAdd, onCancel }: AddGroupFormProps) => {
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline form-input"
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-2 text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder={t('groups.namePlaceholder')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ToggleGroup
|
||||
className="mb-6"
|
||||
label={t('groups.servers')}
|
||||
noOptionsText={t('groups.noServerOptions')}
|
||||
values={formData.servers}
|
||||
options={availableServers.map(server => ({
|
||||
value: server.name,
|
||||
label: server.name
|
||||
}))}
|
||||
<div>
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2">
|
||||
{t('groups.configureTools')}
|
||||
</label>
|
||||
<ServerToolConfig
|
||||
servers={availableServers}
|
||||
value={formData.servers as IGroupServerConfig[]}
|
||||
onChange={(servers) => setFormData(prev => ({ ...prev, servers }))}
|
||||
className="border border-gray-200 rounded-lg p-4 bg-gray-50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-3">
|
||||
<div className="flex justify-end space-x-3 p-6 pt-4 border-t border-gray-200 flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="px-4 py-2 text-gray-600 hover:text-gray-800 btn-secondary"
|
||||
className="px-4 py-2 text-gray-600 hover:text-gray-800 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50 btn-primary"
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 disabled:opacity-50 transition-colors"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? t('common.submitting') : t('common.create')}
|
||||
@@ -125,7 +130,6 @@ const AddGroupForm = ({ onAdd, onCancel }: AddGroupFormProps) => {
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,18 +71,21 @@ const EditGroupForm = ({ group, onEdit, onCancel }: EditGroupFormProps) => {
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-lg shadow-lg max-w-md w-full">
|
||||
<div className="p-6">
|
||||
<div className="bg-white rounded-lg shadow-lg max-w-2xl w-full max-h-[90vh] flex flex-col">
|
||||
<div className="p-6 flex-shrink-0">
|
||||
<h2 className="text-xl font-semibold text-gray-800 mb-4">{t('groups.edit')}</h2>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-red-100 text-red-700 rounded">
|
||||
<div className="mb-4 p-3 bg-red-100 text-red-700 rounded-md border border-gray-200">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-4">
|
||||
<form onSubmit={handleSubmit} className="flex flex-col flex-1 min-h-0">
|
||||
<div className="flex-1 overflow-y-auto px-6">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="name">
|
||||
{t('groups.name')} *
|
||||
</label>
|
||||
@@ -92,36 +95,38 @@ const EditGroupForm = ({ group, onEdit, onCancel }: EditGroupFormProps) => {
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline form-input"
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-2 text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder={t('groups.namePlaceholder')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ToggleGroup
|
||||
className="mb-6"
|
||||
label={t('groups.servers')}
|
||||
noOptionsText={t('groups.noServerOptions')}
|
||||
values={formData.servers}
|
||||
options={availableServers.map(server => ({
|
||||
value: server.name,
|
||||
label: server.name
|
||||
}))}
|
||||
<div>
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2">
|
||||
{t('groups.configureTools')}
|
||||
</label>
|
||||
<ServerToolConfig
|
||||
servers={availableServers}
|
||||
value={formData.servers as IGroupServerConfig[]}
|
||||
onChange={(servers) => setFormData(prev => ({ ...prev, servers }))}
|
||||
className="border border-gray-200 rounded-lg p-4 bg-gray-50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-3">
|
||||
<div className="flex justify-end space-x-3 p-6 pt-4 border-t border-gray-200 flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="px-4 py-2 text-gray-600 hover:text-gray-800 btn-secondary"
|
||||
className="px-4 py-2 text-gray-600 hover:text-gray-800 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50 btn-primary"
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 disabled:opacity-50 transition-colors"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? t('common.submitting') : t('common.save')}
|
||||
@@ -130,7 +135,6 @@ const EditGroupForm = ({ group, onEdit, onCancel }: EditGroupFormProps) => {
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string | null>(null)
|
||||
const dropdownRef = useRef<HTMLDivElement>(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 (
|
||||
<div className="bg-white shadow rounded-lg p-6 ">
|
||||
@@ -186,19 +204,69 @@ const GroupCard = ({
|
||||
{groupServers.length === 0 ? (
|
||||
<p className="text-gray-500 italic">{t('groups.noServers')}</p>
|
||||
) : (
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{groupServers.map(server => (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{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 (
|
||||
<div key={server.name} className="relative">
|
||||
<div
|
||||
key={server.name}
|
||||
className="inline-flex items-center px-3 py-1 bg-gray-50 rounded"
|
||||
className="flex items-center space-x-2 bg-gray-50 rounded-lg px-3 py-2 cursor-pointer hover:bg-gray-100 transition-colors"
|
||||
onClick={handleServerClick}
|
||||
>
|
||||
<span className="font-medium text-gray-700 text-sm">{server.name}</span>
|
||||
<span className={`ml-2 inline-block h-2 w-2 rounded-full ${server.status === 'connected' ? 'bg-green-500' :
|
||||
<span className={`inline-block h-2 w-2 rounded-full ${server.status === 'connected' ? 'bg-green-500' :
|
||||
server.status === 'connecting' ? 'bg-yellow-500' : 'bg-red-500'
|
||||
}`}></span>
|
||||
{toolCount > 0 && (
|
||||
<span className="text-xs text-blue-600 bg-blue-100 px-2 py-0.5 rounded flex items-center gap-1">
|
||||
<Wrench size={12} />
|
||||
{toolCount}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="absolute top-full left-0 mt-1 bg-white shadow-lg rounded-md border border-gray-200 p-3 z-10 min-w-[300px] max-w-[400px]">
|
||||
<div className="text-gray-600 text-xs mb-2">
|
||||
{hasToolRestrictions ? t('groups.selectedTools') : t('groups.allTools')}:
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{getToolsList().map((toolName, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-block bg-gray-100 text-gray-700 px-2 py-1 rounded text-xs"
|
||||
>
|
||||
{toolName}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
317
frontend/src/components/ServerToolConfig.tsx
Normal file
317
frontend/src/components/ServerToolConfig.tsx
Normal file
@@ -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<ServerToolConfigProps> = ({
|
||||
servers,
|
||||
value,
|
||||
onChange,
|
||||
className
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [expandedServers, setExpandedServers] = useState<Set<string>>(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<string>();
|
||||
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 (
|
||||
<div className={cn("space-y-4", className)}>
|
||||
<div className="space-y-3">
|
||||
{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 (
|
||||
<div key={server.name} className="border border-gray-200 rounded-lg hover:border-gray-300 hover:bg-gray-50 transition-colors">
|
||||
<div
|
||||
className="flex items-center justify-between p-3 cursor-pointer rounded-lg transition-colors"
|
||||
onClick={() => toggleServerExpanded(server.name)}
|
||||
>
|
||||
<div
|
||||
className="flex items-center space-x-3"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleServer(server.name);
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSelected || isPartiallySelected}
|
||||
onChange={() => toggleServer(server.name)}
|
||||
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
||||
/>
|
||||
<span className="font-medium text-gray-900 cursor-pointer select-none">
|
||||
{server.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
{serverConfig && serverConfig.tools !== 'all' && Array.isArray(serverConfig.tools) && (
|
||||
<span className="text-sm text-green-600">
|
||||
({t('groups.toolsSelected')} {serverConfig.tools.length}/{serverTools.length})
|
||||
</span>
|
||||
)}
|
||||
{serverConfig && serverConfig.tools === 'all' && (
|
||||
<span className="text-sm text-green-600">
|
||||
({t('groups.allTools')} {serverTools.length}/{serverTools.length})
|
||||
</span>
|
||||
)}
|
||||
|
||||
{serverTools.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
className="p-1 text-gray-400 hover:text-gray-600 transition-colors"
|
||||
>
|
||||
<svg
|
||||
className={cn("w-5 h-5 transition-transform", isExpanded && "rotate-180")}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isExpanded && serverTools.length > 0 && (
|
||||
<div className="border-t border-gray-200 bg-gray-50 p-3">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
{t('groups.toolSelection')}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const isAllSelected = serverConfig?.tools === 'all';
|
||||
if (isAllSelected || (Array.isArray(serverConfig?.tools) && serverConfig.tools.length === serverTools.length)) {
|
||||
// If all tools are selected, deselect all (remove server) but keep expanded
|
||||
updateServerTools(server.name, [], true);
|
||||
} else {
|
||||
// Select all tools (add server if not present)
|
||||
updateServerTools(server.name, 'all');
|
||||
// Don't auto-expand - let user control expansion manually
|
||||
}
|
||||
}}
|
||||
className="text-sm text-blue-600 hover:text-blue-800 transition-colors"
|
||||
>
|
||||
{(serverConfig?.tools === 'all' ||
|
||||
(Array.isArray(serverConfig?.tools) && serverConfig.tools.length === serverTools.length))
|
||||
? t('groups.selectNone')
|
||||
: t('groups.selectAll')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-2 max-h-32 overflow-y-auto">
|
||||
{serverTools.map(tool => {
|
||||
const toolName = tool.name.replace(`${server.name}-`, '');
|
||||
const isToolChecked = isToolSelected(server.name, toolName);
|
||||
|
||||
return (
|
||||
<label key={tool.name} className="flex items-center space-x-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isToolChecked}
|
||||
onChange={() => toggleTool(server.name, toolName)}
|
||||
className="w-3 h-3 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-gray-700">
|
||||
{toolName}
|
||||
</span>
|
||||
{tool.description && (
|
||||
<span className="text-gray-400 text-xs truncate">
|
||||
{tool.description}
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{availableServers.length === 0 && (
|
||||
<p className="text-gray-500 text-sm">{t('groups.noServerOptions')}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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`), {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -272,7 +272,14 @@
|
||||
"noGroups": "暂无可用分组。创建一个新分组以开始使用。",
|
||||
"noServers": "此分组中没有服务器。",
|
||||
"noServerOptions": "没有可用的服务器",
|
||||
"serverCount": "{{count}} 台服务器"
|
||||
"serverCount": "{{count}} 台服务器",
|
||||
"toolSelection": "工具选择",
|
||||
"toolsSelected": "选择",
|
||||
"allTools": "全部",
|
||||
"selectedTools": "选中的工具",
|
||||
"selectAll": "全选",
|
||||
"selectNone": "全不选",
|
||||
"configureTools": "配置工具"
|
||||
},
|
||||
"market": {
|
||||
"title": "服务器市场",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>): 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>): 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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user