feat: Enhance group management with server tool configuration (#250)

This commit is contained in:
samanhappy
2025-07-29 17:31:05 +08:00
committed by GitHub
parent 5bb2715094
commit a6cea2ad3f
14 changed files with 827 additions and 152 deletions

View File

@@ -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',
});
}
};

View File

@@ -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);

View File

@@ -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;
}
};

View File

@@ -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) => {

View File

@@ -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;