diff --git a/.prettierrc b/.prettierrc
index ca8527e..86396de 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -1,7 +1,7 @@
{
- "semi": true,
+ "semi": false,
"trailingComma": "all",
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2
-}
+}
\ No newline at end of file
diff --git a/frontend/src/components/ServerCard.tsx b/frontend/src/components/ServerCard.tsx
index 5a193bb..18437b2 100644
--- a/frontend/src/components/ServerCard.tsx
+++ b/frontend/src/components/ServerCard.tsx
@@ -7,6 +7,7 @@ import ToolCard from '@/components/ui/ToolCard'
import PromptCard from '@/components/ui/PromptCard'
import DeleteDialog from '@/components/ui/DeleteDialog'
import { useToast } from '@/contexts/ToastContext'
+import { useSettingsData } from '@/hooks/useSettingsData'
interface ServerCardProps {
server: Server
@@ -39,6 +40,8 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCar
}
}, [])
+ const { exportMCPSettings } = useSettingsData()
+
const handleRemove = (e: React.MouseEvent) => {
e.stopPropagation()
setShowDeleteDialog(true)
@@ -99,6 +102,39 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCar
}
}
+ const handleCopyServerConfig = async (e: React.MouseEvent) => {
+ e.stopPropagation()
+ try {
+ const result = await exportMCPSettings(server.name)
+ const configJson = JSON.stringify(result.data, null, 2)
+
+ if (navigator.clipboard && window.isSecureContext) {
+ await navigator.clipboard.writeText(configJson)
+ showToast(t('common.copySuccess') || 'Copied to clipboard', 'success')
+ } else {
+ // Fallback for HTTP or unsupported clipboard API
+ const textArea = document.createElement('textarea')
+ textArea.value = configJson
+ textArea.style.position = 'fixed'
+ textArea.style.left = '-9999px'
+ document.body.appendChild(textArea)
+ textArea.focus()
+ textArea.select()
+ try {
+ document.execCommand('copy')
+ showToast(t('common.copySuccess') || 'Copied to clipboard', 'success')
+ } catch (err) {
+ showToast(t('common.copyFailed') || 'Copy failed', 'error')
+ console.error('Copy to clipboard failed:', err)
+ }
+ document.body.removeChild(textArea)
+ }
+ } catch (error) {
+ console.error('Error copying server configuration:', error)
+ showToast(t('common.copyFailed') || 'Copy failed', 'error')
+ }
+ }
+
const handleConfirmDelete = () => {
onRemove(server.name)
setShowDeleteDialog(false)
@@ -111,7 +147,7 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCar
if (result.success) {
showToast(
t(enabled ? 'tool.enableSuccess' : 'tool.disableSuccess', { name: toolName }),
- 'success'
+ 'success',
)
// Trigger refresh to update the tool's state in the UI
if (onRefresh) {
@@ -133,7 +169,7 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCar
if (result.success) {
showToast(
t(enabled ? 'tool.enableSuccess' : 'tool.disableSuccess', { name: promptName }),
- 'success'
+ 'success',
)
// Trigger refresh to update the prompt's state in the UI
if (onRefresh) {
@@ -150,21 +186,33 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCar
return (
<>
-
+
setIsExpanded(!isExpanded)}
>
-
{server.name}
+
+ {server.name}
+
{/* Tool count display */}
-
{server.tools?.length || 0} {t('server.tools')}
+
+ {server.tools?.length || 0} {t('server.tools')}
+
{/* Prompt count display */}
@@ -173,7 +221,9 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCar
-
{server.prompts?.length || 0} {t('server.prompts')}
+
+ {server.prompts?.length || 0} {t('server.prompts')}
+
{server.error && (
@@ -196,19 +246,25 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCar
maxHeight: '300px',
overflowY: 'auto',
width: '480px',
- transform: 'translateX(50%)'
+ transform: 'translateX(50%)',
}}
onClick={(e) => e.stopPropagation()}
>
-
{t('server.errorDetails')}
+
+ {t('server.errorDetails')}
+
-
{server.error}
+
+ {server.error}
+
)}
@@ -230,6 +288,9 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCar
)}
+
{server.tools && (
-
{t('server.tools')}
+
+ {t('server.tools')}
+
{server.tools.map((tool, index) => (
-
+
))}
@@ -282,14 +352,18 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCar
{server.prompts && (
-
{t('server.prompts')}
+
+ {t('server.prompts')}
+
{server.prompts.map((prompt, index) => (
-
))}
@@ -309,4 +383,4 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCar
)
}
-export default ServerCard
\ No newline at end of file
+export default ServerCard
diff --git a/frontend/src/constants/permissions.ts b/frontend/src/constants/permissions.ts
index 5ae964b..a9b496e 100644
--- a/frontend/src/constants/permissions.ts
+++ b/frontend/src/constants/permissions.ts
@@ -4,6 +4,7 @@ export const PERMISSIONS = {
SETTINGS_SMART_ROUTING: 'settings:smart_routing',
SETTINGS_SKIP_AUTH: 'settings:skip_auth',
SETTINGS_INSTALL_CONFIG: 'settings:install_config',
+ SETTINGS_EXPORT_CONFIG: 'settings:export_config',
} as const;
export default PERMISSIONS;
diff --git a/frontend/src/hooks/useSettingsData.ts b/frontend/src/hooks/useSettingsData.ts
index 48ed8ca..8b4342f 100644
--- a/frontend/src/hooks/useSettingsData.ts
+++ b/frontend/src/hooks/useSettingsData.ts
@@ -420,6 +420,21 @@ export const useSettingsData = () => {
}
};
+ const exportMCPSettings = async (serverName?: string) => {
+ setLoading(true);
+ setError(null);
+ try {
+ return await apiGet(`/mcp-settings/export?serverName=${serverName ? serverName : ''}`);
+ } catch (error) {
+ console.error('Failed to export MCP settings:', error);
+ const errorMessage = error instanceof Error ? error.message : 'Failed to export MCP settings';
+ setError(errorMessage);
+ showToast(errorMessage);
+ } finally {
+ setLoading(false);
+ }
+ };
+
// Fetch settings when the component mounts or refreshKey changes
useEffect(() => {
fetchSettings();
@@ -454,5 +469,6 @@ export const useSettingsData = () => {
updateMCPRouterConfig,
updateMCPRouterConfigBatch,
updateNameSeparator,
+ exportMCPSettings,
};
};
diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx
index fd7fd40..387bfd9 100644
--- a/frontend/src/pages/SettingsPage.tsx
+++ b/frontend/src/pages/SettingsPage.tsx
@@ -1,54 +1,55 @@
-import React, { useState, useEffect } from 'react';
-import { useTranslation } from 'react-i18next';
-import { useNavigate } from 'react-router-dom';
-import ChangePasswordForm from '@/components/ChangePasswordForm';
-import { Switch } from '@/components/ui/ToggleGroup';
-import { useSettingsData } from '@/hooks/useSettingsData';
-import { useToast } from '@/contexts/ToastContext';
-import { generateRandomKey } from '@/utils/key';
-import { PermissionChecker } from '@/components/PermissionChecker';
-import { PERMISSIONS } from '@/constants/permissions';
+import React, { useState, useEffect } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useNavigate } from 'react-router-dom'
+import ChangePasswordForm from '@/components/ChangePasswordForm'
+import { Switch } from '@/components/ui/ToggleGroup'
+import { useSettingsData } from '@/hooks/useSettingsData'
+import { useToast } from '@/contexts/ToastContext'
+import { generateRandomKey } from '@/utils/key'
+import { PermissionChecker } from '@/components/PermissionChecker'
+import { PERMISSIONS } from '@/constants/permissions'
+import { Copy, Check, Download } from 'lucide-react'
const SettingsPage: React.FC = () => {
- const { t } = useTranslation();
- const navigate = useNavigate();
- const { showToast } = useToast();
+ const { t } = useTranslation()
+ const navigate = useNavigate()
+ const { showToast } = useToast()
const [installConfig, setInstallConfig] = useState<{
- pythonIndexUrl: string;
- npmRegistry: string;
- baseUrl: string;
+ pythonIndexUrl: string
+ npmRegistry: string
+ baseUrl: string
}>({
pythonIndexUrl: '',
npmRegistry: '',
baseUrl: 'http://localhost:3000',
- });
+ })
const [tempSmartRoutingConfig, setTempSmartRoutingConfig] = useState<{
- dbUrl: string;
- openaiApiBaseUrl: string;
- openaiApiKey: string;
- openaiApiEmbeddingModel: string;
+ dbUrl: string
+ openaiApiBaseUrl: string
+ openaiApiKey: string
+ openaiApiEmbeddingModel: string
}>({
dbUrl: '',
openaiApiBaseUrl: '',
openaiApiKey: '',
openaiApiEmbeddingModel: '',
- });
+ })
const [tempMCPRouterConfig, setTempMCPRouterConfig] = useState<{
- apiKey: string;
- referer: string;
- title: string;
- baseUrl: string;
+ apiKey: string
+ referer: string
+ title: string
+ baseUrl: string
}>({
apiKey: '',
referer: 'https://www.mcphubx.com',
title: 'MCPHub',
baseUrl: 'https://api.mcprouter.to/v1',
- });
+ })
- const [tempNameSeparator, setTempNameSeparator] = useState
('-');
+ const [tempNameSeparator, setTempNameSeparator] = useState('-')
const {
routingConfig,
@@ -66,14 +67,15 @@ const SettingsPage: React.FC = () => {
updateSmartRoutingConfigBatch,
updateMCPRouterConfig,
updateNameSeparator,
- } = useSettingsData();
+ exportMCPSettings,
+ } = useSettingsData()
// Update local installConfig when savedInstallConfig changes
useEffect(() => {
if (savedInstallConfig) {
- setInstallConfig(savedInstallConfig);
+ setInstallConfig(savedInstallConfig)
}
- }, [savedInstallConfig]);
+ }, [savedInstallConfig])
// Update local tempSmartRoutingConfig when smartRoutingConfig changes
useEffect(() => {
@@ -83,9 +85,9 @@ const SettingsPage: React.FC = () => {
openaiApiBaseUrl: smartRoutingConfig.openaiApiBaseUrl || '',
openaiApiKey: smartRoutingConfig.openaiApiKey || '',
openaiApiEmbeddingModel: smartRoutingConfig.openaiApiEmbeddingModel || '',
- });
+ })
}
- }, [smartRoutingConfig]);
+ }, [smartRoutingConfig])
// Update local tempMCPRouterConfig when mcpRouterConfig changes
useEffect(() => {
@@ -95,14 +97,14 @@ const SettingsPage: React.FC = () => {
referer: mcpRouterConfig.referer || 'https://www.mcphubx.com',
title: mcpRouterConfig.title || 'MCPHub',
baseUrl: mcpRouterConfig.baseUrl || 'https://api.mcprouter.to/v1',
- });
+ })
}
- }, [mcpRouterConfig]);
+ }, [mcpRouterConfig])
// Update local tempNameSeparator when nameSeparator changes
useEffect(() => {
- setTempNameSeparator(nameSeparator);
- }, [nameSeparator]);
+ setTempNameSeparator(nameSeparator)
+ }, [nameSeparator])
const [sectionsVisible, setSectionsVisible] = useState({
routingConfig: false,
@@ -110,138 +112,244 @@ const SettingsPage: React.FC = () => {
smartRoutingConfig: false,
mcpRouterConfig: false,
nameSeparator: false,
- password: false
- });
+ password: false,
+ exportConfig: false,
+ })
- const toggleSection = (section: 'routingConfig' | 'installConfig' | 'smartRoutingConfig' | 'mcpRouterConfig' | 'nameSeparator' | 'password') => {
- setSectionsVisible(prev => ({
+ const toggleSection = (
+ section:
+ | 'routingConfig'
+ | 'installConfig'
+ | 'smartRoutingConfig'
+ | 'mcpRouterConfig'
+ | 'nameSeparator'
+ | 'password'
+ | 'exportConfig',
+ ) => {
+ setSectionsVisible((prev) => ({
...prev,
- [section]: !prev[section]
- }));
- };
+ [section]: !prev[section],
+ }))
+ }
- const handleRoutingConfigChange = async (key: 'enableGlobalRoute' | 'enableGroupNameRoute' | 'enableBearerAuth' | 'bearerAuthKey' | 'skipAuth', value: boolean | string) => {
+ const handleRoutingConfigChange = async (
+ key:
+ | 'enableGlobalRoute'
+ | 'enableGroupNameRoute'
+ | 'enableBearerAuth'
+ | 'bearerAuthKey'
+ | 'skipAuth',
+ value: boolean | string,
+ ) => {
// If enableBearerAuth is turned on and there's no key, generate one first
if (key === 'enableBearerAuth' && value === true) {
if (!tempRoutingConfig.bearerAuthKey && !routingConfig.bearerAuthKey) {
- const newKey = generateRandomKey();
- handleBearerAuthKeyChange(newKey);
+ const newKey = generateRandomKey()
+ handleBearerAuthKeyChange(newKey)
// Update both enableBearerAuth and bearerAuthKey in a single call
const success = await updateRoutingConfigBatch({
enableBearerAuth: true,
- bearerAuthKey: newKey
- });
+ bearerAuthKey: newKey,
+ })
if (success) {
// Update tempRoutingConfig to reflect the saved values
- setTempRoutingConfig(prev => ({
+ setTempRoutingConfig((prev) => ({
...prev,
- bearerAuthKey: newKey
- }));
+ bearerAuthKey: newKey,
+ }))
}
- return;
+ return
}
}
- await updateRoutingConfig(key, value);
- };
+ await updateRoutingConfig(key, value)
+ }
const handleBearerAuthKeyChange = (value: string) => {
- setTempRoutingConfig(prev => ({
+ setTempRoutingConfig((prev) => ({
...prev,
- bearerAuthKey: value
- }));
- };
+ bearerAuthKey: value,
+ }))
+ }
const saveBearerAuthKey = async () => {
- await updateRoutingConfig('bearerAuthKey', tempRoutingConfig.bearerAuthKey);
- };
+ await updateRoutingConfig('bearerAuthKey', tempRoutingConfig.bearerAuthKey)
+ }
- const handleInstallConfigChange = (key: 'pythonIndexUrl' | 'npmRegistry' | 'baseUrl', value: string) => {
+ const handleInstallConfigChange = (
+ key: 'pythonIndexUrl' | 'npmRegistry' | 'baseUrl',
+ value: string,
+ ) => {
setInstallConfig({
...installConfig,
- [key]: value
- });
- };
+ [key]: value,
+ })
+ }
const saveInstallConfig = async (key: 'pythonIndexUrl' | 'npmRegistry' | 'baseUrl') => {
- await updateInstallConfig(key, installConfig[key]);
- };
+ await updateInstallConfig(key, installConfig[key])
+ }
- const handleSmartRoutingConfigChange = (key: 'dbUrl' | 'openaiApiBaseUrl' | 'openaiApiKey' | 'openaiApiEmbeddingModel', value: string) => {
+ const handleSmartRoutingConfigChange = (
+ key: 'dbUrl' | 'openaiApiBaseUrl' | 'openaiApiKey' | 'openaiApiEmbeddingModel',
+ value: string,
+ ) => {
setTempSmartRoutingConfig({
...tempSmartRoutingConfig,
- [key]: value
- });
- };
+ [key]: value,
+ })
+ }
- const saveSmartRoutingConfig = async (key: 'dbUrl' | 'openaiApiBaseUrl' | 'openaiApiKey' | 'openaiApiEmbeddingModel') => {
- await updateSmartRoutingConfig(key, tempSmartRoutingConfig[key]);
- };
+ const saveSmartRoutingConfig = async (
+ key: 'dbUrl' | 'openaiApiBaseUrl' | 'openaiApiKey' | 'openaiApiEmbeddingModel',
+ ) => {
+ await updateSmartRoutingConfig(key, tempSmartRoutingConfig[key])
+ }
- const handleMCPRouterConfigChange = (key: 'apiKey' | 'referer' | 'title' | 'baseUrl', value: string) => {
+ const handleMCPRouterConfigChange = (
+ key: 'apiKey' | 'referer' | 'title' | 'baseUrl',
+ value: string,
+ ) => {
setTempMCPRouterConfig({
...tempMCPRouterConfig,
- [key]: value
- });
- };
+ [key]: value,
+ })
+ }
const saveMCPRouterConfig = async (key: 'apiKey' | 'referer' | 'title' | 'baseUrl') => {
- await updateMCPRouterConfig(key, tempMCPRouterConfig[key]);
- };
+ await updateMCPRouterConfig(key, tempMCPRouterConfig[key])
+ }
const saveNameSeparator = async () => {
- await updateNameSeparator(tempNameSeparator);
- };
+ await updateNameSeparator(tempNameSeparator)
+ }
const handleSmartRoutingEnabledChange = async (value: boolean) => {
// If enabling Smart Routing, validate required fields and save any unsaved changes
if (value) {
- const currentDbUrl = tempSmartRoutingConfig.dbUrl || smartRoutingConfig.dbUrl;
- const currentOpenaiApiKey = tempSmartRoutingConfig.openaiApiKey || smartRoutingConfig.openaiApiKey;
+ const currentDbUrl = tempSmartRoutingConfig.dbUrl || smartRoutingConfig.dbUrl
+ const currentOpenaiApiKey =
+ tempSmartRoutingConfig.openaiApiKey || smartRoutingConfig.openaiApiKey
if (!currentDbUrl || !currentOpenaiApiKey) {
- const missingFields = [];
- if (!currentDbUrl) missingFields.push(t('settings.dbUrl'));
- if (!currentOpenaiApiKey) missingFields.push(t('settings.openaiApiKey'));
+ const missingFields = []
+ if (!currentDbUrl) missingFields.push(t('settings.dbUrl'))
+ if (!currentOpenaiApiKey) missingFields.push(t('settings.openaiApiKey'))
- showToast(t('settings.smartRoutingValidationError', {
- fields: missingFields.join(', ')
- }));
- return;
+ showToast(
+ t('settings.smartRoutingValidationError', {
+ fields: missingFields.join(', '),
+ }),
+ )
+ return
}
// Prepare updates object with unsaved changes and enabled status
- const updates: any = { enabled: value };
+ const updates: any = { enabled: value }
// Check for unsaved changes and include them in the batch update
if (tempSmartRoutingConfig.dbUrl !== smartRoutingConfig.dbUrl) {
- updates.dbUrl = tempSmartRoutingConfig.dbUrl;
+ updates.dbUrl = tempSmartRoutingConfig.dbUrl
}
if (tempSmartRoutingConfig.openaiApiBaseUrl !== smartRoutingConfig.openaiApiBaseUrl) {
- updates.openaiApiBaseUrl = tempSmartRoutingConfig.openaiApiBaseUrl;
+ updates.openaiApiBaseUrl = tempSmartRoutingConfig.openaiApiBaseUrl
}
if (tempSmartRoutingConfig.openaiApiKey !== smartRoutingConfig.openaiApiKey) {
- updates.openaiApiKey = tempSmartRoutingConfig.openaiApiKey;
+ updates.openaiApiKey = tempSmartRoutingConfig.openaiApiKey
}
- if (tempSmartRoutingConfig.openaiApiEmbeddingModel !== smartRoutingConfig.openaiApiEmbeddingModel) {
- updates.openaiApiEmbeddingModel = tempSmartRoutingConfig.openaiApiEmbeddingModel;
+ if (
+ tempSmartRoutingConfig.openaiApiEmbeddingModel !==
+ smartRoutingConfig.openaiApiEmbeddingModel
+ ) {
+ updates.openaiApiEmbeddingModel = tempSmartRoutingConfig.openaiApiEmbeddingModel
}
// Save all changes in a single batch update
- await updateSmartRoutingConfigBatch(updates);
+ await updateSmartRoutingConfigBatch(updates)
} else {
// If disabling, just update the enabled status
- await updateSmartRoutingConfig('enabled', value);
+ await updateSmartRoutingConfig('enabled', value)
}
- };
+ }
const handlePasswordChangeSuccess = () => {
setTimeout(() => {
- navigate('/');
- }, 2000);
- };
+ navigate('/')
+ }, 2000)
+ }
+
+ const [copiedConfig, setCopiedConfig] = useState(false)
+ const [mcpSettingsJson, setMcpSettingsJson] = useState('')
+
+ const fetchMcpSettings = async () => {
+ try {
+ const result = await exportMCPSettings()
+ console.log('Fetched MCP settings:', result)
+ const configJson = JSON.stringify(result, null, 2)
+ setMcpSettingsJson(configJson)
+ } catch (error) {
+ console.error('Error fetching MCP settings:', error)
+ showToast(t('settings.exportError') || 'Failed to fetch settings', 'error')
+ }
+ }
+
+ useEffect(() => {
+ if (sectionsVisible.exportConfig && !mcpSettingsJson) {
+ fetchMcpSettings()
+ }
+ }, [sectionsVisible.exportConfig])
+
+ const handleCopyConfig = async () => {
+ if (!mcpSettingsJson) return
+
+ try {
+ if (navigator.clipboard && window.isSecureContext) {
+ await navigator.clipboard.writeText(mcpSettingsJson)
+ setCopiedConfig(true)
+ showToast(t('common.copySuccess') || 'Copied to clipboard', 'success')
+ setTimeout(() => setCopiedConfig(false), 2000)
+ } else {
+ // Fallback for HTTP or unsupported clipboard API
+ const textArea = document.createElement('textarea')
+ textArea.value = mcpSettingsJson
+ textArea.style.position = 'fixed'
+ textArea.style.left = '-9999px'
+ document.body.appendChild(textArea)
+ textArea.focus()
+ textArea.select()
+ try {
+ document.execCommand('copy')
+ setCopiedConfig(true)
+ showToast(t('common.copySuccess') || 'Copied to clipboard', 'success')
+ setTimeout(() => setCopiedConfig(false), 2000)
+ } catch (err) {
+ showToast(t('common.copyFailed') || 'Copy failed', 'error')
+ console.error('Copy to clipboard failed:', err)
+ }
+ document.body.removeChild(textArea)
+ }
+ } catch (error) {
+ console.error('Error copying configuration:', error)
+ showToast(t('common.copyFailed') || 'Copy failed', 'error')
+ }
+ }
+
+ const handleDownloadConfig = () => {
+ if (!mcpSettingsJson) return
+
+ const blob = new Blob([mcpSettingsJson], { type: 'application/json' })
+ const url = URL.createObjectURL(blob)
+ const link = document.createElement('a')
+ link.href = url
+ link.download = 'mcp_settings.json'
+ document.body.appendChild(link)
+ link.click()
+ document.body.removeChild(link)
+ URL.revokeObjectURL(url)
+ showToast(t('settings.exportSuccess') || 'Settings exported successfully', 'success')
+ }
return (
@@ -265,7 +373,9 @@ const SettingsPage: React.FC = () => {
{t('settings.enableSmartRouting')}
-
{t('settings.enableSmartRoutingDescription')}
+
+ {t('settings.enableSmartRoutingDescription')}
+
{
- *{t('settings.dbUrl')}
+ *
+ {t('settings.dbUrl')}
@@ -302,7 +413,8 @@ const SettingsPage: React.FC = () => {
- *{t('settings.openaiApiKey')}
+ *
+ {t('settings.openaiApiKey')}
@@ -332,7 +444,9 @@ const SettingsPage: React.FC = () => {
handleSmartRoutingConfigChange('openaiApiBaseUrl', e.target.value)}
+ onChange={(e) =>
+ handleSmartRoutingConfigChange('openaiApiBaseUrl', e.target.value)
+ }
placeholder={t('settings.openaiApiBaseUrlPlaceholder')}
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}
@@ -349,13 +463,17 @@ const SettingsPage: React.FC = () => {
-
{t('settings.openaiApiEmbeddingModel')}
+
+ {t('settings.openaiApiEmbeddingModel')}
+
handleSmartRoutingConfigChange('openaiApiEmbeddingModel', e.target.value)}
+ onChange={(e) =>
+ handleSmartRoutingConfigChange('openaiApiEmbeddingModel', e.target.value)
+ }
placeholder={t('settings.openaiApiEmbeddingModelPlaceholder')}
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}
@@ -392,7 +510,9 @@ const SettingsPage: React.FC = () => {
{t('settings.mcpRouterApiKey')}
-
{t('settings.mcpRouterApiKeyDescription')}
+
+ {t('settings.mcpRouterApiKeyDescription')}
+
{
{t('settings.mcpRouterBaseUrl')}
-
{t('settings.mcpRouterBaseUrlDescription')}
+
+ {t('settings.mcpRouterBaseUrlDescription')}
+
{
onClick={() => toggleSection('nameSeparator')}
>
{t('settings.systemSettings')}
-
- {sectionsVisible.nameSeparator ? '▼' : '►'}
-
+ {sectionsVisible.nameSeparator ? '▼' : '►'}
{sectionsVisible.nameSeparator && (
@@ -490,9 +610,7 @@ const SettingsPage: React.FC = () => {
onClick={() => toggleSection('routingConfig')}
>
{t('pages.settings.routeConfig')}
-
- {sectionsVisible.routingConfig ? '▼' : '►'}
-
+
{sectionsVisible.routingConfig ? '▼' : '►'}
{sectionsVisible.routingConfig && (
@@ -505,7 +623,9 @@ const SettingsPage: React.FC = () => {
handleRoutingConfigChange('enableBearerAuth', checked)}
+ onCheckedChange={(checked) =>
+ handleRoutingConfigChange('enableBearerAuth', checked)
+ }
/>
@@ -538,24 +658,32 @@ const SettingsPage: React.FC = () => {
{t('settings.enableGlobalRoute')}
-
{t('settings.enableGlobalRouteDescription')}
+
+ {t('settings.enableGlobalRouteDescription')}
+
handleRoutingConfigChange('enableGlobalRoute', checked)}
+ onCheckedChange={(checked) =>
+ handleRoutingConfigChange('enableGlobalRoute', checked)
+ }
/>
{t('settings.enableGroupNameRoute')}
-
{t('settings.enableGroupNameRouteDescription')}
+
+ {t('settings.enableGroupNameRouteDescription')}
+
handleRoutingConfigChange('enableGroupNameRoute', checked)}
+ onCheckedChange={(checked) =>
+ handleRoutingConfigChange('enableGroupNameRoute', checked)
+ }
/>
@@ -572,7 +700,6 @@ const SettingsPage: React.FC = () => {
/>
-
)}
@@ -585,9 +712,7 @@ const SettingsPage: React.FC = () => {
onClick={() => toggleSection('installConfig')}
>
{t('settings.installConfig')}
-
- {sectionsVisible.installConfig ? '▼' : '►'}
-
+
{sectionsVisible.installConfig ? '▼' : '►'}
{sectionsVisible.installConfig && (
@@ -675,9 +800,7 @@ const SettingsPage: React.FC = () => {
onClick={() => toggleSection('password')}
>
{t('auth.changePassword')}
-
- {sectionsVisible.password ? '▼' : '►'}
-
+
{sectionsVisible.password ? '▼' : '►'}
{sectionsVisible.password && (
@@ -686,8 +809,61 @@ const SettingsPage: React.FC = () => {
)}
-
- );
-};
-export default SettingsPage;
\ No newline at end of file
+ {/* Export MCP Settings */}
+
+
+
toggleSection('exportConfig')}
+ >
+
{t('settings.exportMcpSettings')}
+ {sectionsVisible.exportConfig ? '▼' : '►'}
+
+
+ {sectionsVisible.exportConfig && (
+
+
+
+
{t('settings.mcpSettingsJson')}
+
+ {t('settings.mcpSettingsJsonDescription')}
+
+
+
+
+
+ {copiedConfig ? : }
+ {copiedConfig ? t('common.copied') : t('settings.copyToClipboard')}
+
+
+
+ {t('settings.downloadJson')}
+
+
+ {mcpSettingsJson && (
+
+
+ {mcpSettingsJson}
+
+
+ )}
+
+
+
+ )}
+
+
+
+ )
+}
+
+export default SettingsPage
diff --git a/locales/en.json b/locales/en.json
index 24dab22..c6dd6cd 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -75,6 +75,7 @@
"addServer": "Add Server",
"add": "Add",
"edit": "Edit",
+ "copy": "Copy",
"delete": "Delete",
"confirmDelete": "Are you sure you want to delete this server?",
"deleteWarning": "Deleting server '{{name}}' will remove it and all its data. This action cannot be undone.",
@@ -124,6 +125,7 @@
"argumentsPlaceholder": "Enter arguments",
"errorDetails": "Error Details",
"viewErrorDetails": "View error details",
+ "copyConfig": "Copy Configuration",
"confirmVariables": "Confirm Variable Configuration",
"variablesDetected": "Variables detected in configuration. Please confirm these variables are properly configured:",
"detectedVariables": "Detected Variables",
@@ -200,6 +202,7 @@
"copyJson": "Copy JSON",
"copySuccess": "Copied to clipboard",
"copyFailed": "Copy failed",
+ "copied": "Copied",
"close": "Close",
"confirm": "Confirm",
"language": "Language",
@@ -502,7 +505,14 @@
"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."
+ "restartRequired": "Configuration saved. It is recommended to restart the application to ensure all services load the new settings correctly.",
+ "exportMcpSettings": "Export Settings",
+ "mcpSettingsJson": "MCP Settings JSON",
+ "mcpSettingsJsonDescription": "View, copy, or download your current mcp_settings.json configuration for backup or migration to other tools",
+ "copyToClipboard": "Copy to Clipboard",
+ "downloadJson": "Download JSON",
+ "exportSuccess": "Settings exported successfully",
+ "exportError": "Failed to fetch settings"
},
"dxt": {
"upload": "Upload",
diff --git a/locales/fr.json b/locales/fr.json
index 3f53bda..bf09160 100644
--- a/locales/fr.json
+++ b/locales/fr.json
@@ -75,6 +75,7 @@
"addServer": "Ajouter un serveur",
"add": "Ajouter",
"edit": "Modifier",
+ "copy": "Copier",
"delete": "Supprimer",
"confirmDelete": "Êtes-vous sûr de vouloir supprimer ce serveur ?",
"deleteWarning": "La suppression du serveur '{{name}}' le supprimera ainsi que toutes ses données. Cette action est irréversible.",
@@ -124,6 +125,7 @@
"argumentsPlaceholder": "Entrez les arguments",
"errorDetails": "Détails de l'erreur",
"viewErrorDetails": "Voir les détails de l'erreur",
+ "copyConfig": "Copier la configuration",
"confirmVariables": "Confirmer la configuration des variables",
"variablesDetected": "Variables détectées dans la configuration. Veuillez confirmer que ces variables sont correctement configurées :",
"detectedVariables": "Variables détectées",
@@ -200,6 +202,7 @@
"copyJson": "Copier le JSON",
"copySuccess": "Copié dans le presse-papiers",
"copyFailed": "Échec de la copie",
+ "copied": "Copié",
"close": "Fermer",
"confirm": "Confirmer",
"language": "Langue",
@@ -502,7 +505,14 @@
"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."
+ "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.",
+ "exportMcpSettings": "Exporter les paramètres",
+ "mcpSettingsJson": "JSON des paramètres MCP",
+ "mcpSettingsJsonDescription": "Afficher, copier ou télécharger votre configuration mcp_settings.json actuelle pour la sauvegarde ou la migration vers d'autres outils",
+ "copyToClipboard": "Copier dans le presse-papiers",
+ "downloadJson": "Télécharger JSON",
+ "exportSuccess": "Paramètres exportés avec succès",
+ "exportError": "Échec de la récupération des paramètres"
},
"dxt": {
"upload": "Télécharger",
diff --git a/locales/zh.json b/locales/zh.json
index 6809e51..b8e6b98 100644
--- a/locales/zh.json
+++ b/locales/zh.json
@@ -75,6 +75,7 @@
"addServer": "添加服务器",
"add": "添加",
"edit": "编辑",
+ "copy": "复制",
"delete": "删除",
"confirmDelete": "您确定要删除此服务器吗?",
"deleteWarning": "删除服务器 '{{name}}' 将会移除该服务器及其所有数据。此操作无法撤销。",
@@ -124,6 +125,7 @@
"argumentsPlaceholder": "请输入参数",
"errorDetails": "错误详情",
"viewErrorDetails": "查看错误详情",
+ "copyConfig": "复制配置",
"confirmVariables": "确认变量配置",
"variablesDetected": "检测到配置中包含变量,请确认这些变量是否已正确配置:",
"detectedVariables": "检测到的变量",
@@ -201,6 +203,7 @@
"copyJson": "复制JSON",
"copySuccess": "已复制到剪贴板",
"copyFailed": "复制失败",
+ "copied": "已复制",
"close": "关闭",
"confirm": "确认",
"language": "语言",
@@ -504,7 +507,14 @@
"systemSettings": "系统设置",
"nameSeparatorLabel": "名称分隔符",
"nameSeparatorDescription": "用于分隔服务器名称和工具/提示名称(默认:-)",
- "restartRequired": "配置已保存。为确保所有服务正确加载新设置,建议重启应用。"
+ "restartRequired": "配置已保存。为确保所有服务正确加载新设置,建议重启应用。",
+ "exportMcpSettings": "导出配置",
+ "mcpSettingsJson": "MCP 配置 JSON",
+ "mcpSettingsJsonDescription": "查看、复制或下载当前的 mcp_settings.json 配置,可用于备份或迁移到其他工具",
+ "copyToClipboard": "复制到剪贴板",
+ "downloadJson": "下载 JSON",
+ "exportSuccess": "配置导出成功",
+ "exportError": "获取配置失败"
},
"dxt": {
"upload": "上传",
diff --git a/src/controllers/configController.ts b/src/controllers/configController.ts
index abe6ae3..697e270 100644
--- a/src/controllers/configController.ts
+++ b/src/controllers/configController.ts
@@ -1,6 +1,6 @@
import { Request, Response } from 'express';
import config from '../config/index.js';
-import { loadSettings } from '../config/index.js';
+import { loadSettings, loadOriginalSettings } from '../config/index.js';
import { getDataService } from '../services/services.js';
import { DataService } from '../services/dataService.js';
import { IUser } from '../types/index.js';
@@ -72,3 +72,46 @@ export const getPublicConfig = (req: Request, res: Response): void => {
});
}
};
+
+/**
+ * Get MCP settings in JSON format for export/copy
+ * Supports both full settings and individual server configuration
+ */
+export const getMcpSettingsJson = (req: Request, res: Response): void => {
+ try {
+ const { serverName } = req.query;
+ const settings = loadOriginalSettings();
+ if (serverName && typeof serverName === 'string') {
+ // Return individual server configuration
+ const serverConfig = settings.mcpServers[serverName];
+ if (!serverConfig) {
+ res.status(404).json({
+ success: false,
+ message: `Server '${serverName}' not found`,
+ });
+ return;
+ }
+
+ res.json({
+ success: true,
+ data: {
+ mcpServers: {
+ [serverName]: serverConfig,
+ },
+ },
+ });
+ } else {
+ // Return full settings
+ res.json({
+ success: true,
+ data: settings,
+ });
+ }
+ } catch (error) {
+ console.error('Error getting MCP settings JSON:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Failed to get MCP settings',
+ });
+ }
+};
diff --git a/src/routes/index.ts b/src/routes/index.ts
index 2b18340..98b8bab 100644
--- a/src/routes/index.ts
+++ b/src/routes/index.ts
@@ -58,7 +58,7 @@ import {
} from '../controllers/cloudController.js';
import { login, register, getCurrentUser, changePassword } from '../controllers/authController.js';
import { getAllLogs, clearLogs, streamLogs } from '../controllers/logController.js';
-import { getRuntimeConfig, getPublicConfig } from '../controllers/configController.js';
+import { getRuntimeConfig, getPublicConfig, getMcpSettingsJson } from '../controllers/configController.js';
import { callTool } from '../controllers/toolController.js';
import { getPrompt } from '../controllers/promptController.js';
import { uploadDxtFile, uploadMiddleware } from '../controllers/dxtController.js';
@@ -149,6 +149,9 @@ export const initRoutes = (app: express.Application): void => {
router.delete('/logs', clearLogs);
router.get('/logs/stream', streamLogs);
+ // MCP settings export route
+ router.get('/mcp-settings/export', getMcpSettingsJson);
+
// Auth routes - move to router instead of app directly
router.post(
'/auth/login',
diff --git a/tests/controllers/configController.test.ts b/tests/controllers/configController.test.ts
new file mode 100644
index 0000000..af1f369
--- /dev/null
+++ b/tests/controllers/configController.test.ts
@@ -0,0 +1,139 @@
+import { getMcpSettingsJson } from '../../src/controllers/configController.js'
+import * as config from '../../src/config/index.js'
+import { Request, Response } from 'express'
+
+// Mock the config module
+jest.mock('../../src/config/index.js')
+
+describe('ConfigController - getMcpSettingsJson', () => {
+ let mockRequest: Partial
+ let mockResponse: Partial
+ let mockJson: jest.Mock
+ let mockStatus: jest.Mock
+
+ beforeEach(() => {
+ mockJson = jest.fn()
+ mockStatus = jest.fn().mockReturnThis()
+ mockRequest = {
+ query: {},
+ }
+ mockResponse = {
+ json: mockJson,
+ status: mockStatus,
+ }
+
+ // Reset mocks
+ jest.clearAllMocks()
+ })
+
+ describe('Full Settings Export', () => {
+ it('should handle settings without users array', () => {
+ const mockSettings = {
+ mcpServers: {
+ 'test-server': {
+ command: 'test',
+ args: ['--test'],
+ },
+ },
+ }
+
+ ;(config.loadOriginalSettings as jest.Mock).mockReturnValue(mockSettings)
+
+ getMcpSettingsJson(mockRequest as Request, mockResponse as Response)
+
+ expect(mockJson).toHaveBeenCalledWith({
+ success: true,
+ data: {
+ mcpServers: mockSettings.mcpServers,
+ users: undefined,
+ },
+ })
+ })
+ })
+
+ describe('Individual Server Export', () => {
+ it('should return individual server configuration when serverName is specified', () => {
+ const mockSettings = {
+ mcpServers: {
+ 'test-server': {
+ command: 'test',
+ args: ['--test'],
+ env: {
+ TEST_VAR: 'test-value',
+ },
+ },
+ 'another-server': {
+ command: 'another',
+ args: ['--another'],
+ },
+ },
+ users: [
+ {
+ username: 'admin',
+ password: '$2b$10$hashedpassword',
+ isAdmin: true,
+ },
+ ],
+ }
+
+ mockRequest.query = { serverName: 'test-server' }
+ ;(config.loadOriginalSettings as jest.Mock).mockReturnValue(mockSettings)
+
+ getMcpSettingsJson(mockRequest as Request, mockResponse as Response)
+
+ expect(mockJson).toHaveBeenCalledWith({
+ success: true,
+ data: {
+ mcpServers: {
+ 'test-server': {
+ command: 'test',
+ args: ['--test'],
+ env: {
+ TEST_VAR: 'test-value',
+ },
+ },
+ },
+ },
+ })
+ })
+
+ it('should return 404 when server does not exist', () => {
+ const mockSettings = {
+ mcpServers: {
+ 'test-server': {
+ command: 'test',
+ args: ['--test'],
+ },
+ },
+ }
+
+ mockRequest.query = { serverName: 'non-existent-server' }
+ ;(config.loadOriginalSettings as jest.Mock).mockReturnValue(mockSettings)
+
+ getMcpSettingsJson(mockRequest as Request, mockResponse as Response)
+
+ expect(mockStatus).toHaveBeenCalledWith(404)
+ expect(mockJson).toHaveBeenCalledWith({
+ success: false,
+ message: "Server 'non-existent-server' not found",
+ })
+ })
+ })
+
+ describe('Error Handling', () => {
+ it('should handle errors gracefully and return 500', () => {
+ const errorMessage = 'Failed to load settings'
+ ;(config.loadOriginalSettings as jest.Mock).mockImplementation(() => {
+ throw new Error(errorMessage)
+ })
+
+ getMcpSettingsJson(mockRequest as Request, mockResponse as Response)
+
+ expect(mockStatus).toHaveBeenCalledWith(500)
+ expect(mockJson).toHaveBeenCalledWith({
+ success: false,
+ message: 'Failed to get MCP settings',
+ })
+ })
+ })
+})