diff --git a/frontend/src/components/ServerToolConfig.tsx b/frontend/src/components/ServerToolConfig.tsx index 9e879fc..5a2ecb8 100644 --- a/frontend/src/components/ServerToolConfig.tsx +++ b/frontend/src/components/ServerToolConfig.tsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { IGroupServerConfig, Server, Tool } from '@/types'; import { cn } from '@/utils/cn'; +import { useSettingsData } from '@/hooks/useSettingsData'; interface ServerToolConfigProps { servers: Server[]; @@ -17,6 +18,7 @@ export const ServerToolConfig: React.FC = ({ className }) => { const { t } = useTranslation(); + const { nameSeparator } = useSettingsData(); const [expandedServers, setExpandedServers] = useState>(new Set()); // Normalize current value to IGroupServerConfig[] format @@ -116,7 +118,7 @@ export const ServerToolConfig: React.FC = ({ const server = availableServers.find(s => s.name === serverName); if (!server) return; - const allToolNames = server.tools?.map(tool => tool.name.replace(`${serverName}-`, '')) || []; + const allToolNames = server.tools?.map(tool => tool.name.replace(`${serverName}${nameSeparator}`, '')) || []; const serverConfig = normalizedValue.find(config => config.name === serverName); if (!serverConfig) { @@ -279,7 +281,7 @@ export const ServerToolConfig: React.FC = ({
{serverTools.map(tool => { - const toolName = tool.name.replace(`${server.name}-`, ''); + const toolName = tool.name.replace(`${server.name}${nameSeparator}`, ''); const isToolChecked = isToolSelected(server.name, toolName); return ( diff --git a/frontend/src/components/ui/PromptCard.tsx b/frontend/src/components/ui/PromptCard.tsx index ee06bf5..c7ef685 100644 --- a/frontend/src/components/ui/PromptCard.tsx +++ b/frontend/src/components/ui/PromptCard.tsx @@ -4,6 +4,7 @@ import { Prompt } from '@/types' import { ChevronDown, ChevronRight, Play, Loader, Edit, Check } from '@/components/icons/LucideIcons' import { Switch } from './ToggleGroup' import { getPrompt, PromptCallResult } from '@/services/promptService' +import { useSettingsData } from '@/hooks/useSettingsData' import DynamicForm from './DynamicForm' import PromptResult from './PromptResult' @@ -16,6 +17,7 @@ interface PromptCardProps { const PromptCard = ({ prompt, server, onToggle, onDescriptionUpdate }: PromptCardProps) => { const { t } = useTranslation() + const { nameSeparator } = useSettingsData() const [isExpanded, setIsExpanded] = useState(false) const [showRunForm, setShowRunForm] = useState(false) const [isRunning, setIsRunning] = useState(false) @@ -154,7 +156,7 @@ const PromptCard = ({ prompt, server, onToggle, onDescriptionUpdate }: PromptCar >

- {prompt.name.replace(server + '-', '')} + {prompt.name.replace(server + nameSeparator, '')} {prompt.title && ( {prompt.title} @@ -249,7 +251,7 @@ const PromptCard = ({ prompt, server, onToggle, onDescriptionUpdate }: PromptCar onCancel={handleCancelRun} loading={isRunning} storageKey={getStorageKey()} - title={t('prompt.runPromptWithName', { name: prompt.name.replace(server + '-', '') })} + title={t('prompt.runPromptWithName', { name: prompt.name.replace(server + nameSeparator, '') })} /> {/* Prompt Result */} {result && ( diff --git a/frontend/src/components/ui/ToolCard.tsx b/frontend/src/components/ui/ToolCard.tsx index 49c8c6e..3ed0d5c 100644 --- a/frontend/src/components/ui/ToolCard.tsx +++ b/frontend/src/components/ui/ToolCard.tsx @@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next' import { Tool } from '@/types' import { ChevronDown, ChevronRight, Play, Loader, Edit, Check } from '@/components/icons/LucideIcons' import { callTool, ToolCallResult, updateToolDescription } from '@/services/toolService' +import { useSettingsData } from '@/hooks/useSettingsData' import { Switch } from './ToggleGroup' import DynamicForm from './DynamicForm' import ToolResult from './ToolResult' @@ -25,6 +26,7 @@ function isEmptyValue(value: any): boolean { const ToolCard = ({ tool, server, onToggle, onDescriptionUpdate }: ToolCardProps) => { const { t } = useTranslation() + const { nameSeparator } = useSettingsData() const [isExpanded, setIsExpanded] = useState(false) const [showRunForm, setShowRunForm] = useState(false) const [isRunning, setIsRunning] = useState(false) @@ -148,7 +150,7 @@ const ToolCard = ({ tool, server, onToggle, onDescriptionUpdate }: ToolCardProps >

- {tool.name.replace(server + '-', '')} + {tool.name.replace(server + nameSeparator, '')} {isEditingDescription ? ( <> @@ -246,7 +248,7 @@ const ToolCard = ({ tool, server, onToggle, onDescriptionUpdate }: ToolCardProps onCancel={handleCancelRun} loading={isRunning} storageKey={getStorageKey()} - title={t('tool.runToolWithName', { name: tool.name.replace(server + '-', '') })} + title={t('tool.runToolWithName', { name: tool.name.replace(server + nameSeparator, '') })} /> {/* Tool Result */} {result && ( diff --git a/frontend/src/hooks/useSettingsData.ts b/frontend/src/hooks/useSettingsData.ts index 69832b9..48ed8ca 100644 --- a/frontend/src/hooks/useSettingsData.ts +++ b/frontend/src/hooks/useSettingsData.ts @@ -40,6 +40,7 @@ interface SystemSettings { install?: InstallConfig; smartRouting?: SmartRoutingConfig; mcpRouter?: MCPRouterConfig; + nameSeparator?: string; }; } @@ -84,6 +85,8 @@ export const useSettingsData = () => { baseUrl: 'https://api.mcprouter.to/v1', }); + const [nameSeparator, setNameSeparator] = useState('-'); + const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [refreshKey, setRefreshKey] = useState(0); @@ -135,6 +138,9 @@ export const useSettingsData = () => { baseUrl: data.data.systemConfig.mcpRouter.baseUrl || 'https://api.mcprouter.to/v1', }); } + if (data.success && data.data?.systemConfig?.nameSeparator !== undefined) { + setNameSeparator(data.data.systemConfig.nameSeparator); + } } catch (error) { console.error('Failed to fetch settings:', error); setError(error instanceof Error ? error.message : 'Failed to fetch settings'); @@ -384,6 +390,36 @@ export const useSettingsData = () => { } }; + // Update name separator + const updateNameSeparator = async (value: string) => { + setLoading(true); + setError(null); + + try { + const data = await apiPut('/system-config', { + nameSeparator: value, + }); + + if (data.success) { + setNameSeparator(value); + showToast(t('settings.restartRequired'), 'info'); + return true; + } else { + showToast(data.message || t('errors.failedToUpdateSystemConfig')); + return false; + } + } catch (error) { + console.error('Failed to update name separator:', error); + const errorMessage = + error instanceof Error ? error.message : 'Failed to update name separator'; + setError(errorMessage); + showToast(errorMessage); + return false; + } finally { + setLoading(false); + } + }; + // Fetch settings when the component mounts or refreshKey changes useEffect(() => { fetchSettings(); @@ -404,6 +440,7 @@ export const useSettingsData = () => { installConfig, smartRoutingConfig, mcpRouterConfig, + nameSeparator, loading, error, setError, @@ -416,5 +453,6 @@ export const useSettingsData = () => { updateRoutingConfigBatch, updateMCPRouterConfig, updateMCPRouterConfigBatch, + updateNameSeparator, }; }; diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index 17138d5..fd7fd40 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -48,6 +48,8 @@ const SettingsPage: React.FC = () => { baseUrl: 'https://api.mcprouter.to/v1', }); + const [tempNameSeparator, setTempNameSeparator] = useState('-'); + const { routingConfig, tempRoutingConfig, @@ -55,13 +57,15 @@ const SettingsPage: React.FC = () => { installConfig: savedInstallConfig, smartRoutingConfig, mcpRouterConfig, + nameSeparator, loading, updateRoutingConfig, updateRoutingConfigBatch, updateInstallConfig, updateSmartRoutingConfig, updateSmartRoutingConfigBatch, - updateMCPRouterConfig + updateMCPRouterConfig, + updateNameSeparator, } = useSettingsData(); // Update local installConfig when savedInstallConfig changes @@ -95,15 +99,21 @@ const SettingsPage: React.FC = () => { } }, [mcpRouterConfig]); + // Update local tempNameSeparator when nameSeparator changes + useEffect(() => { + setTempNameSeparator(nameSeparator); + }, [nameSeparator]); + const [sectionsVisible, setSectionsVisible] = useState({ routingConfig: false, installConfig: false, smartRoutingConfig: false, mcpRouterConfig: false, + nameSeparator: false, password: false }); - const toggleSection = (section: 'routingConfig' | 'installConfig' | 'smartRoutingConfig' | 'mcpRouterConfig' | 'password') => { + const toggleSection = (section: 'routingConfig' | 'installConfig' | 'smartRoutingConfig' | 'mcpRouterConfig' | 'nameSeparator' | 'password') => { setSectionsVisible(prev => ({ ...prev, [section]: !prev[section] @@ -181,6 +191,10 @@ const SettingsPage: React.FC = () => { await updateMCPRouterConfig(key, tempMCPRouterConfig[key]); }; + const saveNameSeparator = async () => { + await updateNameSeparator(tempNameSeparator); + }; + const handleSmartRoutingEnabledChange = async (value: boolean) => { // If enabling Smart Routing, validate required fields and save any unsaved changes if (value) { @@ -427,6 +441,48 @@ const SettingsPage: React.FC = () => {

+ {/* System Settings */} +
+
toggleSection('nameSeparator')} + > +

{t('settings.systemSettings')}

+ + {sectionsVisible.nameSeparator ? '▼' : '►'} + +
+ + {sectionsVisible.nameSeparator && ( +
+
+
+

{t('settings.nameSeparatorLabel')}

+

{t('settings.nameSeparatorDescription')}

+
+
+ setTempNameSeparator(e.target.value)} + placeholder="-" + className="flex-1 mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm form-input" + disabled={loading} + maxLength={5} + /> + +
+
+
+ )} +
+ {/* Route Configuration Settings */}
=> { return false; } }; + + diff --git a/locales/en.json b/locales/en.json index 18fb4e7..24dab22 100644 --- a/locales/en.json +++ b/locales/en.json @@ -498,7 +498,11 @@ "mcpRouterTitlePlaceholder": "MCPHub", "mcpRouterBaseUrl": "Base URL", "mcpRouterBaseUrlDescription": "Base URL for MCPRouter API", - "mcpRouterBaseUrlPlaceholder": "https://api.mcprouter.to/v1" + "mcpRouterBaseUrlPlaceholder": "https://api.mcprouter.to/v1", + "systemSettings": "System Settings", + "nameSeparatorLabel": "Name Separator", + "nameSeparatorDescription": "Character used to separate server name and tool/prompt name (default: -)", + "restartRequired": "Configuration saved. It is recommended to restart the application to ensure all services load the new settings correctly." }, "dxt": { "upload": "Upload", diff --git a/locales/fr.json b/locales/fr.json index c2ba9f3..3f53bda 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -498,7 +498,11 @@ "mcpRouterTitlePlaceholder": "MCPHub", "mcpRouterBaseUrl": "URL de base", "mcpRouterBaseUrlDescription": "URL de base pour l'API MCPRouter", - "mcpRouterBaseUrlPlaceholder": "https://api.mcprouter.to/v1" + "mcpRouterBaseUrlPlaceholder": "https://api.mcprouter.to/v1", + "systemSettings": "Paramètres système", + "nameSeparatorLabel": "Séparateur de noms", + "nameSeparatorDescription": "Caractère utilisé pour séparer le nom du serveur et le nom de l'outil/prompt (par défaut : -)", + "restartRequired": "Configuration enregistrée. Il est recommandé de redémarrer l'application pour s'assurer que tous les services chargent correctement les nouveaux paramètres." }, "dxt": { "upload": "Télécharger", diff --git a/locales/zh.json b/locales/zh.json index b69218b..6809e51 100644 --- a/locales/zh.json +++ b/locales/zh.json @@ -500,7 +500,11 @@ "mcpRouterTitlePlaceholder": "MCPHub", "mcpRouterBaseUrl": "基础地址", "mcpRouterBaseUrlDescription": "MCPRouter API 的基础地址", - "mcpRouterBaseUrlPlaceholder": "https://api.mcprouter.to/v1" + "mcpRouterBaseUrlPlaceholder": "https://api.mcprouter.to/v1", + "systemSettings": "系统设置", + "nameSeparatorLabel": "名称分隔符", + "nameSeparatorDescription": "用于分隔服务器名称和工具/提示名称(默认:-)", + "restartRequired": "配置已保存。为确保所有服务正确加载新设置,建议重启应用。" }, "dxt": { "upload": "上传", diff --git a/src/config/index.ts b/src/config/index.ts index dd16e35..7b69af4 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -138,3 +138,8 @@ export const expandEnvVars = (value: string): string => { }; export default defaultConfig; + +export function getNameSeparator(): string { + const settings = loadSettings(); + return settings.systemConfig?.nameSeparator || '-'; +} diff --git a/src/controllers/openApiController.ts b/src/controllers/openApiController.ts index 650bcfe..71d6568 100644 --- a/src/controllers/openApiController.ts +++ b/src/controllers/openApiController.ts @@ -7,6 +7,7 @@ import { } from '../services/openApiGeneratorService.js'; import { getServerByName } from '../services/mcpService.js'; import { getGroupByIdOrName } from '../services/groupService.js'; +import { getNameSeparator } from '../config/index.js'; /** * Controller for OpenAPI generation endpoints @@ -177,7 +178,7 @@ export const executeToolViaOpenAPI = async (req: Request, res: Response): Promis if (serverInfo) { // Find the tool in the server's tools list - const fullToolName = `${serverName}-${toolName}`; + const fullToolName = `${serverName}${getNameSeparator()}${toolName}`; const tool = serverInfo.tools.find( (t: any) => t.name === fullToolName || t.name === toolName, ); diff --git a/src/controllers/serverController.ts b/src/controllers/serverController.ts index 7ab3d13..dfee247 100644 --- a/src/controllers/serverController.ts +++ b/src/controllers/serverController.ts @@ -504,7 +504,7 @@ export const updateToolDescription = async (req: Request, res: Response): Promis export const updateSystemConfig = (req: Request, res: Response): void => { try { - const { routing, install, smartRouting, mcpRouter } = req.body; + const { routing, install, smartRouting, mcpRouter, nameSeparator } = req.body; const currentUser = (req as any).user; if ( @@ -528,7 +528,8 @@ export const updateSystemConfig = (req: Request, res: Response): void => { (typeof mcpRouter.apiKey !== 'string' && typeof mcpRouter.referer !== 'string' && typeof mcpRouter.title !== 'string' && - typeof mcpRouter.baseUrl !== 'string')) + typeof mcpRouter.baseUrl !== 'string')) && + (typeof nameSeparator !== 'string') ) { res.status(400).json({ success: false, @@ -710,6 +711,10 @@ export const updateSystemConfig = (req: Request, res: Response): void => { } } + if (typeof nameSeparator === 'string') { + settings.systemConfig.nameSeparator = nameSeparator; + } + if (saveSettings(settings, currentUser)) { res.json({ success: true, diff --git a/src/services/mcpService.ts b/src/services/mcpService.ts index 4c802b6..0042ac6 100644 --- a/src/services/mcpService.ts +++ b/src/services/mcpService.ts @@ -12,7 +12,7 @@ import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; import { ServerInfo, ServerConfig, Tool } from '../types/index.js'; -import { loadSettings, expandEnvVars, replaceEnvVars } from '../config/index.js'; +import { loadSettings, expandEnvVars, replaceEnvVars, getNameSeparator } from '../config/index.js'; import config from '../config/index.js'; import { getGroup } from './sseService.js'; import { getServersInGroup, getServerConfigInGroup } from './groupService.js'; @@ -294,7 +294,7 @@ const callToolWithReconnect = async ( try { const tools = await client.listTools({}, serverInfo.options || {}); serverInfo.tools = tools.tools.map((tool) => ({ - name: `${serverInfo.name}-${tool.name}`, + name: `${serverInfo.name}${getNameSeparator()}${tool.name}`, description: tool.description || '', inputSchema: cleanInputSchema(tool.inputSchema || {}), })); @@ -420,7 +420,7 @@ export const initializeClientsFromSettings = async ( // Convert OpenAPI tools to MCP tool format const openApiTools = openApiClient.getTools(); const mcpTools: Tool[] = openApiTools.map((tool) => ({ - name: `${name}-${tool.name}`, + name: `${name}${getNameSeparator()}${tool.name}`, description: tool.description, inputSchema: cleanInputSchema(tool.inputSchema), })); @@ -507,7 +507,7 @@ export const initializeClientsFromSettings = async ( .then((tools) => { console.log(`Successfully listed ${tools.tools.length} tools for server: ${name}`); serverInfo.tools = tools.tools.map((tool) => ({ - name: `${name}-${tool.name}`, + name: `${name}${getNameSeparator()}${tool.name}`, description: tool.description || '', inputSchema: cleanInputSchema(tool.inputSchema || {}), })); @@ -530,7 +530,7 @@ export const initializeClientsFromSettings = async ( `Successfully listed ${prompts.prompts.length} prompts for server: ${name}`, ); serverInfo.prompts = prompts.prompts.map((prompt) => ({ - name: `${name}-${prompt.name}`, + name: `${name}${getNameSeparator()}${prompt.name}`, title: prompt.title, description: prompt.description, arguments: prompt.arguments, @@ -848,7 +848,7 @@ Available servers: ${serversList}`; if (serverConfig && serverConfig.tools !== 'all' && Array.isArray(serverConfig.tools)) { // Filter tools based on group configuration const allowedToolNames = serverConfig.tools.map( - (toolName) => `${serverInfo.name}-${toolName}`, + (toolName) => `${serverInfo.name}${getNameSeparator()}${toolName}`, ); enabledTools = enabledTools.filter((tool) => allowedToolNames.includes(tool.name)); } @@ -1035,8 +1035,10 @@ export const handleCallToolRequest = async (request: any, extra: any) => { ); // Remove server prefix from tool name if present - const cleanToolName = toolName.startsWith(`${targetServerInfo.name}-`) - ? toolName.replace(`${targetServerInfo.name}-`, '') + const separator = getNameSeparator(); + const prefix = `${targetServerInfo.name}${separator}`; + const cleanToolName = toolName.startsWith(prefix) + ? toolName.substring(prefix.length) : toolName; // Extract passthrough headers from extra or request context @@ -1093,8 +1095,10 @@ export const handleCallToolRequest = async (request: any, extra: any) => { `Invoking tool '${toolName}' on server '${targetServerInfo.name}' with arguments: ${JSON.stringify(finalArgs)}`, ); - toolName = toolName.startsWith(`${targetServerInfo.name}-`) - ? toolName.replace(`${targetServerInfo.name}-`, '') + const separator = getNameSeparator(); + const prefix = `${targetServerInfo.name}${separator}`; + toolName = toolName.startsWith(prefix) + ? toolName.substring(prefix.length) : toolName; const result = await callToolWithReconnect( targetServerInfo, @@ -1121,8 +1125,10 @@ export const handleCallToolRequest = async (request: any, extra: any) => { const openApiClient = serverInfo.openApiClient; // Remove server prefix from tool name if present - const cleanToolName = request.params.name.startsWith(`${serverInfo.name}-`) - ? request.params.name.replace(`${serverInfo.name}-`, '') + const separator = getNameSeparator(); + const prefix = `${serverInfo.name}${separator}`; + const cleanToolName = request.params.name.startsWith(prefix) + ? request.params.name.substring(prefix.length) : request.params.name; console.log( @@ -1179,8 +1185,10 @@ export const handleCallToolRequest = async (request: any, extra: any) => { throw new Error(`Client not found for server: ${serverInfo.name}`); } - request.params.name = request.params.name.startsWith(`${serverInfo.name}-`) - ? request.params.name.replace(`${serverInfo.name}-`, '') + const separator = getNameSeparator(); + const prefix = `${serverInfo.name}${separator}`; + request.params.name = request.params.name.startsWith(prefix) + ? request.params.name.substring(prefix.length) : request.params.name; const result = await callToolWithReconnect( serverInfo, @@ -1223,8 +1231,10 @@ export const handleGetPromptRequest = async (request: any, extra: any) => { } // Remove server prefix from prompt name if present - const cleanPromptName = name.startsWith(`${server.name}-`) - ? name.replace(`${server.name}-`, '') + const separator = getNameSeparator(); + const prefix = `${server.name}${separator}`; + const cleanPromptName = name.startsWith(prefix) + ? name.substring(prefix.length) : name; const promptParams = { diff --git a/src/services/openApiGeneratorService.ts b/src/services/openApiGeneratorService.ts index 3f1c975..21d29fe 100644 --- a/src/services/openApiGeneratorService.ts +++ b/src/services/openApiGeneratorService.ts @@ -2,7 +2,7 @@ import { OpenAPIV3 } from 'openapi-types'; import { Tool } from '../types/index.js'; import { getServersInfo } from './mcpService.js'; import config from '../config/index.js'; -import { loadSettings } from '../config/index.js'; +import { loadSettings, getNameSeparator } from '../config/index.js'; /** * Service for generating OpenAPI 3.x specifications from MCP tools @@ -209,10 +209,11 @@ export async function generateOpenAPISpec( const allowedTools = groupConfig.get(serverInfo.name); if (allowedTools !== 'all') { // Filter tools to only include those specified in the group configuration + const separator = getNameSeparator(); filteredTools = tools.filter( (tool) => Array.isArray(allowedTools) && - allowedTools.includes(tool.name.replace(serverInfo.name + '-', '')), + allowedTools.includes(tool.name.replace(serverInfo.name + separator, '')), ); } } diff --git a/src/types/index.ts b/src/types/index.ts index aa595ba..f21bbb0 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -144,6 +144,7 @@ export interface SystemConfig { title?: string; // Title header for MCPRouter API requests baseUrl?: string; // Base URL for MCPRouter API (default: https://api.mcprouter.to/v1) }; + nameSeparator?: string; // Separator used between server name and tool/prompt name (default: '-') } export interface UserConfig {