mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-24 02:39:19 -05:00
feat: Add MCP settings export and copy functionality (#367)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: samanhappy <2755122+samanhappy@users.noreply.github.com> Co-authored-by: samanhappy <samanhappy@gmail.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"semi": true,
|
"semi": false,
|
||||||
"trailingComma": "all",
|
"trailingComma": "all",
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"printWidth": 100,
|
"printWidth": 100,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import ToolCard from '@/components/ui/ToolCard'
|
|||||||
import PromptCard from '@/components/ui/PromptCard'
|
import PromptCard from '@/components/ui/PromptCard'
|
||||||
import DeleteDialog from '@/components/ui/DeleteDialog'
|
import DeleteDialog from '@/components/ui/DeleteDialog'
|
||||||
import { useToast } from '@/contexts/ToastContext'
|
import { useToast } from '@/contexts/ToastContext'
|
||||||
|
import { useSettingsData } from '@/hooks/useSettingsData'
|
||||||
|
|
||||||
interface ServerCardProps {
|
interface ServerCardProps {
|
||||||
server: Server
|
server: Server
|
||||||
@@ -39,6 +40,8 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCar
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const { exportMCPSettings } = useSettingsData()
|
||||||
|
|
||||||
const handleRemove = (e: React.MouseEvent) => {
|
const handleRemove = (e: React.MouseEvent) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
setShowDeleteDialog(true)
|
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 = () => {
|
const handleConfirmDelete = () => {
|
||||||
onRemove(server.name)
|
onRemove(server.name)
|
||||||
setShowDeleteDialog(false)
|
setShowDeleteDialog(false)
|
||||||
@@ -111,7 +147,7 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCar
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
showToast(
|
showToast(
|
||||||
t(enabled ? 'tool.enableSuccess' : 'tool.disableSuccess', { name: toolName }),
|
t(enabled ? 'tool.enableSuccess' : 'tool.disableSuccess', { name: toolName }),
|
||||||
'success'
|
'success',
|
||||||
)
|
)
|
||||||
// Trigger refresh to update the tool's state in the UI
|
// Trigger refresh to update the tool's state in the UI
|
||||||
if (onRefresh) {
|
if (onRefresh) {
|
||||||
@@ -133,7 +169,7 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCar
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
showToast(
|
showToast(
|
||||||
t(enabled ? 'tool.enableSuccess' : 'tool.disableSuccess', { name: promptName }),
|
t(enabled ? 'tool.enableSuccess' : 'tool.disableSuccess', { name: promptName }),
|
||||||
'success'
|
'success',
|
||||||
)
|
)
|
||||||
// Trigger refresh to update the prompt's state in the UI
|
// Trigger refresh to update the prompt's state in the UI
|
||||||
if (onRefresh) {
|
if (onRefresh) {
|
||||||
@@ -150,21 +186,33 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCar
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={`bg-white shadow rounded-lg p-6 mb-6 page-card transition-all duration-200 ${server.enabled === false ? 'opacity-60' : ''}`}>
|
<div
|
||||||
|
className={`bg-white shadow rounded-lg p-6 mb-6 page-card transition-all duration-200 ${server.enabled === false ? 'opacity-60' : ''}`}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className="flex justify-between items-center cursor-pointer"
|
className="flex justify-between items-center cursor-pointer"
|
||||||
onClick={() => setIsExpanded(!isExpanded)}
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<h2 className={`text-xl font-semibold ${server.enabled === false ? 'text-gray-600' : 'text-gray-900'}`}>{server.name}</h2>
|
<h2
|
||||||
|
className={`text-xl font-semibold ${server.enabled === false ? 'text-gray-600' : 'text-gray-900'}`}
|
||||||
|
>
|
||||||
|
{server.name}
|
||||||
|
</h2>
|
||||||
<StatusBadge status={server.status} />
|
<StatusBadge status={server.status} />
|
||||||
|
|
||||||
{/* Tool count display */}
|
{/* Tool count display */}
|
||||||
<div className="flex items-center px-2 py-1 bg-blue-50 text-blue-700 rounded-full text-sm btn-primary">
|
<div className="flex items-center px-2 py-1 bg-blue-50 text-blue-700 rounded-full text-sm btn-primary">
|
||||||
<svg className="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
<svg className="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<path fillRule="evenodd" d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z" clipRule="evenodd" />
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span>{server.tools?.length || 0} {t('server.tools')}</span>
|
<span>
|
||||||
|
{server.tools?.length || 0} {t('server.tools')}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Prompt count display */}
|
{/* Prompt count display */}
|
||||||
@@ -173,7 +221,9 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCar
|
|||||||
<path d="M2 5a2 2 0 012-2h7a2 2 0 012 2v4a2 2 0 01-2 2H9l-3 3v-3H4a2 2 0 01-2-2V5z" />
|
<path d="M2 5a2 2 0 012-2h7a2 2 0 012 2v4a2 2 0 01-2 2H9l-3 3v-3H4a2 2 0 01-2-2V5z" />
|
||||||
<path d="M15 7v2a4 4 0 01-4 4H9.828l-1.766 1.767c.28.149.599.233.938.233h2l3 3v-3h2a2 2 0 002-2V9a2 2 0 00-2-2h-1z" />
|
<path d="M15 7v2a4 4 0 01-4 4H9.828l-1.766 1.767c.28.149.599.233.938.233h2l3 3v-3h2a2 2 0 002-2V9a2 2 0 00-2-2h-1z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>{server.prompts?.length || 0} {t('server.prompts')}</span>
|
<span>
|
||||||
|
{server.prompts?.length || 0} {t('server.prompts')}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{server.error && (
|
{server.error && (
|
||||||
@@ -196,19 +246,25 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCar
|
|||||||
maxHeight: '300px',
|
maxHeight: '300px',
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
width: '480px',
|
width: '480px',
|
||||||
transform: 'translateX(50%)'
|
transform: 'translateX(50%)',
|
||||||
}}
|
}}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-center sticky top-0 bg-white py-2 px-4 border-b border-gray-200 z-20 shadow-sm">
|
<div className="flex justify-between items-center sticky top-0 bg-white py-2 px-4 border-b border-gray-200 z-20 shadow-sm">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<h4 className="text-sm font-medium text-red-600">{t('server.errorDetails')}</h4>
|
<h4 className="text-sm font-medium text-red-600">
|
||||||
|
{t('server.errorDetails')}
|
||||||
|
</h4>
|
||||||
<button
|
<button
|
||||||
onClick={copyToClipboard}
|
onClick={copyToClipboard}
|
||||||
className="p-1 text-gray-400 hover:text-gray-600 transition-colors btn-secondary"
|
className="p-1 text-gray-400 hover:text-gray-600 transition-colors btn-secondary"
|
||||||
title={t('common.copy')}
|
title={t('common.copy')}
|
||||||
>
|
>
|
||||||
{copied ? <Check size={14} className="text-green-500" /> : <Copy size={14} />}
|
{copied ? (
|
||||||
|
<Check size={14} className="text-green-500" />
|
||||||
|
) : (
|
||||||
|
<Copy size={14} />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -222,7 +278,9 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCar
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 pt-2">
|
<div className="p-4 pt-2">
|
||||||
<pre className="text-sm text-gray-700 break-words whitespace-pre-wrap">{server.error}</pre>
|
<pre className="text-sm text-gray-700 break-words whitespace-pre-wrap">
|
||||||
|
{server.error}
|
||||||
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -230,6 +288,9 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCar
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
|
<button onClick={handleCopyServerConfig} className={`px-3 py-1 btn-secondary`}>
|
||||||
|
{t('server.copy')}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleEdit}
|
onClick={handleEdit}
|
||||||
className="px-3 py-1 bg-blue-100 text-blue-800 rounded hover:bg-blue-200 text-sm btn-primary"
|
className="px-3 py-1 bg-blue-100 text-blue-800 rounded hover:bg-blue-200 text-sm btn-primary"
|
||||||
@@ -239,20 +300,20 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCar
|
|||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<button
|
<button
|
||||||
onClick={handleToggle}
|
onClick={handleToggle}
|
||||||
className={`px-3 py-1 text-sm rounded transition-colors ${isToggling
|
className={`px-3 py-1 text-sm rounded transition-colors ${
|
||||||
? 'bg-gray-200 text-gray-500'
|
isToggling
|
||||||
: server.enabled !== false
|
? 'bg-gray-200 text-gray-500'
|
||||||
? 'bg-green-100 text-green-800 hover:bg-green-200 btn-secondary'
|
: server.enabled !== false
|
||||||
: 'bg-gray-100 text-gray-800 hover:bg-gray-200 btn-primary'
|
? 'bg-green-100 text-green-800 hover:bg-green-200 btn-secondary'
|
||||||
}`}
|
: 'bg-gray-100 text-gray-800 hover:bg-gray-200 btn-primary'
|
||||||
|
}`}
|
||||||
disabled={isToggling}
|
disabled={isToggling}
|
||||||
>
|
>
|
||||||
{isToggling
|
{isToggling
|
||||||
? t('common.processing')
|
? t('common.processing')
|
||||||
: server.enabled !== false
|
: server.enabled !== false
|
||||||
? t('server.disable')
|
? t('server.disable')
|
||||||
: t('server.enable')
|
: t('server.enable')}
|
||||||
}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -271,10 +332,19 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCar
|
|||||||
<>
|
<>
|
||||||
{server.tools && (
|
{server.tools && (
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<h6 className={`font-medium ${server.enabled === false ? 'text-gray-600' : 'text-gray-900'} mb-4`}>{t('server.tools')}</h6>
|
<h6
|
||||||
|
className={`font-medium ${server.enabled === false ? 'text-gray-600' : 'text-gray-900'} mb-4`}
|
||||||
|
>
|
||||||
|
{t('server.tools')}
|
||||||
|
</h6>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{server.tools.map((tool, index) => (
|
{server.tools.map((tool, index) => (
|
||||||
<ToolCard key={index} server={server.name} tool={tool} onToggle={handleToolToggle} />
|
<ToolCard
|
||||||
|
key={index}
|
||||||
|
server={server.name}
|
||||||
|
tool={tool}
|
||||||
|
onToggle={handleToolToggle}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -282,7 +352,11 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCar
|
|||||||
|
|
||||||
{server.prompts && (
|
{server.prompts && (
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<h6 className={`font-medium ${server.enabled === false ? 'text-gray-600' : 'text-gray-900'} mb-4`}>{t('server.prompts')}</h6>
|
<h6
|
||||||
|
className={`font-medium ${server.enabled === false ? 'text-gray-600' : 'text-gray-900'} mb-4`}
|
||||||
|
>
|
||||||
|
{t('server.prompts')}
|
||||||
|
</h6>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{server.prompts.map((prompt, index) => (
|
{server.prompts.map((prompt, index) => (
|
||||||
<PromptCard
|
<PromptCard
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export const PERMISSIONS = {
|
|||||||
SETTINGS_SMART_ROUTING: 'settings:smart_routing',
|
SETTINGS_SMART_ROUTING: 'settings:smart_routing',
|
||||||
SETTINGS_SKIP_AUTH: 'settings:skip_auth',
|
SETTINGS_SKIP_AUTH: 'settings:skip_auth',
|
||||||
SETTINGS_INSTALL_CONFIG: 'settings:install_config',
|
SETTINGS_INSTALL_CONFIG: 'settings:install_config',
|
||||||
|
SETTINGS_EXPORT_CONFIG: 'settings:export_config',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export default PERMISSIONS;
|
export default PERMISSIONS;
|
||||||
|
|||||||
@@ -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
|
// Fetch settings when the component mounts or refreshKey changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchSettings();
|
fetchSettings();
|
||||||
@@ -454,5 +469,6 @@ export const useSettingsData = () => {
|
|||||||
updateMCPRouterConfig,
|
updateMCPRouterConfig,
|
||||||
updateMCPRouterConfigBatch,
|
updateMCPRouterConfigBatch,
|
||||||
updateNameSeparator,
|
updateNameSeparator,
|
||||||
|
exportMCPSettings,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,54 +1,55 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react'
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom'
|
||||||
import ChangePasswordForm from '@/components/ChangePasswordForm';
|
import ChangePasswordForm from '@/components/ChangePasswordForm'
|
||||||
import { Switch } from '@/components/ui/ToggleGroup';
|
import { Switch } from '@/components/ui/ToggleGroup'
|
||||||
import { useSettingsData } from '@/hooks/useSettingsData';
|
import { useSettingsData } from '@/hooks/useSettingsData'
|
||||||
import { useToast } from '@/contexts/ToastContext';
|
import { useToast } from '@/contexts/ToastContext'
|
||||||
import { generateRandomKey } from '@/utils/key';
|
import { generateRandomKey } from '@/utils/key'
|
||||||
import { PermissionChecker } from '@/components/PermissionChecker';
|
import { PermissionChecker } from '@/components/PermissionChecker'
|
||||||
import { PERMISSIONS } from '@/constants/permissions';
|
import { PERMISSIONS } from '@/constants/permissions'
|
||||||
|
import { Copy, Check, Download } from 'lucide-react'
|
||||||
|
|
||||||
const SettingsPage: React.FC = () => {
|
const SettingsPage: React.FC = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate()
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast()
|
||||||
|
|
||||||
const [installConfig, setInstallConfig] = useState<{
|
const [installConfig, setInstallConfig] = useState<{
|
||||||
pythonIndexUrl: string;
|
pythonIndexUrl: string
|
||||||
npmRegistry: string;
|
npmRegistry: string
|
||||||
baseUrl: string;
|
baseUrl: string
|
||||||
}>({
|
}>({
|
||||||
pythonIndexUrl: '',
|
pythonIndexUrl: '',
|
||||||
npmRegistry: '',
|
npmRegistry: '',
|
||||||
baseUrl: 'http://localhost:3000',
|
baseUrl: 'http://localhost:3000',
|
||||||
});
|
})
|
||||||
|
|
||||||
const [tempSmartRoutingConfig, setTempSmartRoutingConfig] = useState<{
|
const [tempSmartRoutingConfig, setTempSmartRoutingConfig] = useState<{
|
||||||
dbUrl: string;
|
dbUrl: string
|
||||||
openaiApiBaseUrl: string;
|
openaiApiBaseUrl: string
|
||||||
openaiApiKey: string;
|
openaiApiKey: string
|
||||||
openaiApiEmbeddingModel: string;
|
openaiApiEmbeddingModel: string
|
||||||
}>({
|
}>({
|
||||||
dbUrl: '',
|
dbUrl: '',
|
||||||
openaiApiBaseUrl: '',
|
openaiApiBaseUrl: '',
|
||||||
openaiApiKey: '',
|
openaiApiKey: '',
|
||||||
openaiApiEmbeddingModel: '',
|
openaiApiEmbeddingModel: '',
|
||||||
});
|
})
|
||||||
|
|
||||||
const [tempMCPRouterConfig, setTempMCPRouterConfig] = useState<{
|
const [tempMCPRouterConfig, setTempMCPRouterConfig] = useState<{
|
||||||
apiKey: string;
|
apiKey: string
|
||||||
referer: string;
|
referer: string
|
||||||
title: string;
|
title: string
|
||||||
baseUrl: string;
|
baseUrl: string
|
||||||
}>({
|
}>({
|
||||||
apiKey: '',
|
apiKey: '',
|
||||||
referer: 'https://www.mcphubx.com',
|
referer: 'https://www.mcphubx.com',
|
||||||
title: 'MCPHub',
|
title: 'MCPHub',
|
||||||
baseUrl: 'https://api.mcprouter.to/v1',
|
baseUrl: 'https://api.mcprouter.to/v1',
|
||||||
});
|
})
|
||||||
|
|
||||||
const [tempNameSeparator, setTempNameSeparator] = useState<string>('-');
|
const [tempNameSeparator, setTempNameSeparator] = useState<string>('-')
|
||||||
|
|
||||||
const {
|
const {
|
||||||
routingConfig,
|
routingConfig,
|
||||||
@@ -66,14 +67,15 @@ const SettingsPage: React.FC = () => {
|
|||||||
updateSmartRoutingConfigBatch,
|
updateSmartRoutingConfigBatch,
|
||||||
updateMCPRouterConfig,
|
updateMCPRouterConfig,
|
||||||
updateNameSeparator,
|
updateNameSeparator,
|
||||||
} = useSettingsData();
|
exportMCPSettings,
|
||||||
|
} = useSettingsData()
|
||||||
|
|
||||||
// Update local installConfig when savedInstallConfig changes
|
// Update local installConfig when savedInstallConfig changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (savedInstallConfig) {
|
if (savedInstallConfig) {
|
||||||
setInstallConfig(savedInstallConfig);
|
setInstallConfig(savedInstallConfig)
|
||||||
}
|
}
|
||||||
}, [savedInstallConfig]);
|
}, [savedInstallConfig])
|
||||||
|
|
||||||
// Update local tempSmartRoutingConfig when smartRoutingConfig changes
|
// Update local tempSmartRoutingConfig when smartRoutingConfig changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -83,9 +85,9 @@ const SettingsPage: React.FC = () => {
|
|||||||
openaiApiBaseUrl: smartRoutingConfig.openaiApiBaseUrl || '',
|
openaiApiBaseUrl: smartRoutingConfig.openaiApiBaseUrl || '',
|
||||||
openaiApiKey: smartRoutingConfig.openaiApiKey || '',
|
openaiApiKey: smartRoutingConfig.openaiApiKey || '',
|
||||||
openaiApiEmbeddingModel: smartRoutingConfig.openaiApiEmbeddingModel || '',
|
openaiApiEmbeddingModel: smartRoutingConfig.openaiApiEmbeddingModel || '',
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}, [smartRoutingConfig]);
|
}, [smartRoutingConfig])
|
||||||
|
|
||||||
// Update local tempMCPRouterConfig when mcpRouterConfig changes
|
// Update local tempMCPRouterConfig when mcpRouterConfig changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -95,14 +97,14 @@ const SettingsPage: React.FC = () => {
|
|||||||
referer: mcpRouterConfig.referer || 'https://www.mcphubx.com',
|
referer: mcpRouterConfig.referer || 'https://www.mcphubx.com',
|
||||||
title: mcpRouterConfig.title || 'MCPHub',
|
title: mcpRouterConfig.title || 'MCPHub',
|
||||||
baseUrl: mcpRouterConfig.baseUrl || 'https://api.mcprouter.to/v1',
|
baseUrl: mcpRouterConfig.baseUrl || 'https://api.mcprouter.to/v1',
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}, [mcpRouterConfig]);
|
}, [mcpRouterConfig])
|
||||||
|
|
||||||
// Update local tempNameSeparator when nameSeparator changes
|
// Update local tempNameSeparator when nameSeparator changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTempNameSeparator(nameSeparator);
|
setTempNameSeparator(nameSeparator)
|
||||||
}, [nameSeparator]);
|
}, [nameSeparator])
|
||||||
|
|
||||||
const [sectionsVisible, setSectionsVisible] = useState({
|
const [sectionsVisible, setSectionsVisible] = useState({
|
||||||
routingConfig: false,
|
routingConfig: false,
|
||||||
@@ -110,138 +112,244 @@ const SettingsPage: React.FC = () => {
|
|||||||
smartRoutingConfig: false,
|
smartRoutingConfig: false,
|
||||||
mcpRouterConfig: false,
|
mcpRouterConfig: false,
|
||||||
nameSeparator: false,
|
nameSeparator: false,
|
||||||
password: false
|
password: false,
|
||||||
});
|
exportConfig: false,
|
||||||
|
})
|
||||||
|
|
||||||
const toggleSection = (section: 'routingConfig' | 'installConfig' | 'smartRoutingConfig' | 'mcpRouterConfig' | 'nameSeparator' | 'password') => {
|
const toggleSection = (
|
||||||
setSectionsVisible(prev => ({
|
section:
|
||||||
|
| 'routingConfig'
|
||||||
|
| 'installConfig'
|
||||||
|
| 'smartRoutingConfig'
|
||||||
|
| 'mcpRouterConfig'
|
||||||
|
| 'nameSeparator'
|
||||||
|
| 'password'
|
||||||
|
| 'exportConfig',
|
||||||
|
) => {
|
||||||
|
setSectionsVisible((prev) => ({
|
||||||
...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 enableBearerAuth is turned on and there's no key, generate one first
|
||||||
if (key === 'enableBearerAuth' && value === true) {
|
if (key === 'enableBearerAuth' && value === true) {
|
||||||
if (!tempRoutingConfig.bearerAuthKey && !routingConfig.bearerAuthKey) {
|
if (!tempRoutingConfig.bearerAuthKey && !routingConfig.bearerAuthKey) {
|
||||||
const newKey = generateRandomKey();
|
const newKey = generateRandomKey()
|
||||||
handleBearerAuthKeyChange(newKey);
|
handleBearerAuthKeyChange(newKey)
|
||||||
|
|
||||||
// Update both enableBearerAuth and bearerAuthKey in a single call
|
// Update both enableBearerAuth and bearerAuthKey in a single call
|
||||||
const success = await updateRoutingConfigBatch({
|
const success = await updateRoutingConfigBatch({
|
||||||
enableBearerAuth: true,
|
enableBearerAuth: true,
|
||||||
bearerAuthKey: newKey
|
bearerAuthKey: newKey,
|
||||||
});
|
})
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
// Update tempRoutingConfig to reflect the saved values
|
// Update tempRoutingConfig to reflect the saved values
|
||||||
setTempRoutingConfig(prev => ({
|
setTempRoutingConfig((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
bearerAuthKey: newKey
|
bearerAuthKey: newKey,
|
||||||
}));
|
}))
|
||||||
}
|
}
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateRoutingConfig(key, value);
|
await updateRoutingConfig(key, value)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleBearerAuthKeyChange = (value: string) => {
|
const handleBearerAuthKeyChange = (value: string) => {
|
||||||
setTempRoutingConfig(prev => ({
|
setTempRoutingConfig((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
bearerAuthKey: value
|
bearerAuthKey: value,
|
||||||
}));
|
}))
|
||||||
};
|
}
|
||||||
|
|
||||||
const saveBearerAuthKey = async () => {
|
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({
|
setInstallConfig({
|
||||||
...installConfig,
|
...installConfig,
|
||||||
[key]: value
|
[key]: value,
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
const saveInstallConfig = async (key: 'pythonIndexUrl' | 'npmRegistry' | 'baseUrl') => {
|
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({
|
setTempSmartRoutingConfig({
|
||||||
...tempSmartRoutingConfig,
|
...tempSmartRoutingConfig,
|
||||||
[key]: value
|
[key]: value,
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
const saveSmartRoutingConfig = async (key: 'dbUrl' | 'openaiApiBaseUrl' | 'openaiApiKey' | 'openaiApiEmbeddingModel') => {
|
const saveSmartRoutingConfig = async (
|
||||||
await updateSmartRoutingConfig(key, tempSmartRoutingConfig[key]);
|
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({
|
setTempMCPRouterConfig({
|
||||||
...tempMCPRouterConfig,
|
...tempMCPRouterConfig,
|
||||||
[key]: value
|
[key]: value,
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
const saveMCPRouterConfig = async (key: 'apiKey' | 'referer' | 'title' | 'baseUrl') => {
|
const saveMCPRouterConfig = async (key: 'apiKey' | 'referer' | 'title' | 'baseUrl') => {
|
||||||
await updateMCPRouterConfig(key, tempMCPRouterConfig[key]);
|
await updateMCPRouterConfig(key, tempMCPRouterConfig[key])
|
||||||
};
|
}
|
||||||
|
|
||||||
const saveNameSeparator = async () => {
|
const saveNameSeparator = async () => {
|
||||||
await updateNameSeparator(tempNameSeparator);
|
await updateNameSeparator(tempNameSeparator)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleSmartRoutingEnabledChange = async (value: boolean) => {
|
const handleSmartRoutingEnabledChange = async (value: boolean) => {
|
||||||
// If enabling Smart Routing, validate required fields and save any unsaved changes
|
// If enabling Smart Routing, validate required fields and save any unsaved changes
|
||||||
if (value) {
|
if (value) {
|
||||||
const currentDbUrl = tempSmartRoutingConfig.dbUrl || smartRoutingConfig.dbUrl;
|
const currentDbUrl = tempSmartRoutingConfig.dbUrl || smartRoutingConfig.dbUrl
|
||||||
const currentOpenaiApiKey = tempSmartRoutingConfig.openaiApiKey || smartRoutingConfig.openaiApiKey;
|
const currentOpenaiApiKey =
|
||||||
|
tempSmartRoutingConfig.openaiApiKey || smartRoutingConfig.openaiApiKey
|
||||||
|
|
||||||
if (!currentDbUrl || !currentOpenaiApiKey) {
|
if (!currentDbUrl || !currentOpenaiApiKey) {
|
||||||
const missingFields = [];
|
const missingFields = []
|
||||||
if (!currentDbUrl) missingFields.push(t('settings.dbUrl'));
|
if (!currentDbUrl) missingFields.push(t('settings.dbUrl'))
|
||||||
if (!currentOpenaiApiKey) missingFields.push(t('settings.openaiApiKey'));
|
if (!currentOpenaiApiKey) missingFields.push(t('settings.openaiApiKey'))
|
||||||
|
|
||||||
showToast(t('settings.smartRoutingValidationError', {
|
showToast(
|
||||||
fields: missingFields.join(', ')
|
t('settings.smartRoutingValidationError', {
|
||||||
}));
|
fields: missingFields.join(', '),
|
||||||
return;
|
}),
|
||||||
|
)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare updates object with unsaved changes and enabled status
|
// 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
|
// Check for unsaved changes and include them in the batch update
|
||||||
if (tempSmartRoutingConfig.dbUrl !== smartRoutingConfig.dbUrl) {
|
if (tempSmartRoutingConfig.dbUrl !== smartRoutingConfig.dbUrl) {
|
||||||
updates.dbUrl = tempSmartRoutingConfig.dbUrl;
|
updates.dbUrl = tempSmartRoutingConfig.dbUrl
|
||||||
}
|
}
|
||||||
if (tempSmartRoutingConfig.openaiApiBaseUrl !== smartRoutingConfig.openaiApiBaseUrl) {
|
if (tempSmartRoutingConfig.openaiApiBaseUrl !== smartRoutingConfig.openaiApiBaseUrl) {
|
||||||
updates.openaiApiBaseUrl = tempSmartRoutingConfig.openaiApiBaseUrl;
|
updates.openaiApiBaseUrl = tempSmartRoutingConfig.openaiApiBaseUrl
|
||||||
}
|
}
|
||||||
if (tempSmartRoutingConfig.openaiApiKey !== smartRoutingConfig.openaiApiKey) {
|
if (tempSmartRoutingConfig.openaiApiKey !== smartRoutingConfig.openaiApiKey) {
|
||||||
updates.openaiApiKey = tempSmartRoutingConfig.openaiApiKey;
|
updates.openaiApiKey = tempSmartRoutingConfig.openaiApiKey
|
||||||
}
|
}
|
||||||
if (tempSmartRoutingConfig.openaiApiEmbeddingModel !== smartRoutingConfig.openaiApiEmbeddingModel) {
|
if (
|
||||||
updates.openaiApiEmbeddingModel = tempSmartRoutingConfig.openaiApiEmbeddingModel;
|
tempSmartRoutingConfig.openaiApiEmbeddingModel !==
|
||||||
|
smartRoutingConfig.openaiApiEmbeddingModel
|
||||||
|
) {
|
||||||
|
updates.openaiApiEmbeddingModel = tempSmartRoutingConfig.openaiApiEmbeddingModel
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save all changes in a single batch update
|
// Save all changes in a single batch update
|
||||||
await updateSmartRoutingConfigBatch(updates);
|
await updateSmartRoutingConfigBatch(updates)
|
||||||
} else {
|
} else {
|
||||||
// If disabling, just update the enabled status
|
// If disabling, just update the enabled status
|
||||||
await updateSmartRoutingConfig('enabled', value);
|
await updateSmartRoutingConfig('enabled', value)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const handlePasswordChangeSuccess = () => {
|
const handlePasswordChangeSuccess = () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
navigate('/');
|
navigate('/')
|
||||||
}, 2000);
|
}, 2000)
|
||||||
};
|
}
|
||||||
|
|
||||||
|
const [copiedConfig, setCopiedConfig] = useState(false)
|
||||||
|
const [mcpSettingsJson, setMcpSettingsJson] = useState<string>('')
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<div className="container mx-auto">
|
<div className="container mx-auto">
|
||||||
@@ -265,7 +373,9 @@ const SettingsPage: React.FC = () => {
|
|||||||
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-md">
|
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-md">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-medium text-gray-700">{t('settings.enableSmartRouting')}</h3>
|
<h3 className="font-medium text-gray-700">{t('settings.enableSmartRouting')}</h3>
|
||||||
<p className="text-sm text-gray-500">{t('settings.enableSmartRoutingDescription')}</p>
|
<p className="text-sm text-gray-500">
|
||||||
|
{t('settings.enableSmartRoutingDescription')}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
@@ -277,7 +387,8 @@ const SettingsPage: React.FC = () => {
|
|||||||
<div className="p-3 bg-gray-50 rounded-md">
|
<div className="p-3 bg-gray-50 rounded-md">
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<h3 className="font-medium text-gray-700">
|
<h3 className="font-medium text-gray-700">
|
||||||
<span className="text-red-500 px-1">*</span>{t('settings.dbUrl')}
|
<span className="text-red-500 px-1">*</span>
|
||||||
|
{t('settings.dbUrl')}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -302,7 +413,8 @@ const SettingsPage: React.FC = () => {
|
|||||||
<div className="p-3 bg-gray-50 rounded-md">
|
<div className="p-3 bg-gray-50 rounded-md">
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<h3 className="font-medium text-gray-700">
|
<h3 className="font-medium text-gray-700">
|
||||||
<span className="text-red-500 px-1">*</span>{t('settings.openaiApiKey')}
|
<span className="text-red-500 px-1">*</span>
|
||||||
|
{t('settings.openaiApiKey')}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -332,7 +444,9 @@ const SettingsPage: React.FC = () => {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={tempSmartRoutingConfig.openaiApiBaseUrl}
|
value={tempSmartRoutingConfig.openaiApiBaseUrl}
|
||||||
onChange={(e) => handleSmartRoutingConfigChange('openaiApiBaseUrl', e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleSmartRoutingConfigChange('openaiApiBaseUrl', e.target.value)
|
||||||
|
}
|
||||||
placeholder={t('settings.openaiApiBaseUrlPlaceholder')}
|
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"
|
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}
|
disabled={loading}
|
||||||
@@ -349,13 +463,17 @@ const SettingsPage: React.FC = () => {
|
|||||||
|
|
||||||
<div className="p-3 bg-gray-50 rounded-md">
|
<div className="p-3 bg-gray-50 rounded-md">
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<h3 className="font-medium text-gray-700">{t('settings.openaiApiEmbeddingModel')}</h3>
|
<h3 className="font-medium text-gray-700">
|
||||||
|
{t('settings.openaiApiEmbeddingModel')}
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={tempSmartRoutingConfig.openaiApiEmbeddingModel}
|
value={tempSmartRoutingConfig.openaiApiEmbeddingModel}
|
||||||
onChange={(e) => handleSmartRoutingConfigChange('openaiApiEmbeddingModel', e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleSmartRoutingConfigChange('openaiApiEmbeddingModel', e.target.value)
|
||||||
|
}
|
||||||
placeholder={t('settings.openaiApiEmbeddingModelPlaceholder')}
|
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"
|
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}
|
disabled={loading}
|
||||||
@@ -392,7 +510,9 @@ const SettingsPage: React.FC = () => {
|
|||||||
<div className="p-3 bg-gray-50 rounded-md">
|
<div className="p-3 bg-gray-50 rounded-md">
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<h3 className="font-medium text-gray-700">{t('settings.mcpRouterApiKey')}</h3>
|
<h3 className="font-medium text-gray-700">{t('settings.mcpRouterApiKey')}</h3>
|
||||||
<p className="text-sm text-gray-500">{t('settings.mcpRouterApiKeyDescription')}</p>
|
<p className="text-sm text-gray-500">
|
||||||
|
{t('settings.mcpRouterApiKeyDescription')}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<input
|
<input
|
||||||
@@ -416,7 +536,9 @@ const SettingsPage: React.FC = () => {
|
|||||||
<div className="p-3 bg-gray-50 rounded-md">
|
<div className="p-3 bg-gray-50 rounded-md">
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<h3 className="font-medium text-gray-700">{t('settings.mcpRouterBaseUrl')}</h3>
|
<h3 className="font-medium text-gray-700">{t('settings.mcpRouterBaseUrl')}</h3>
|
||||||
<p className="text-sm text-gray-500">{t('settings.mcpRouterBaseUrlDescription')}</p>
|
<p className="text-sm text-gray-500">
|
||||||
|
{t('settings.mcpRouterBaseUrlDescription')}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<input
|
<input
|
||||||
@@ -448,9 +570,7 @@ const SettingsPage: React.FC = () => {
|
|||||||
onClick={() => toggleSection('nameSeparator')}
|
onClick={() => toggleSection('nameSeparator')}
|
||||||
>
|
>
|
||||||
<h2 className="font-semibold text-gray-800">{t('settings.systemSettings')}</h2>
|
<h2 className="font-semibold text-gray-800">{t('settings.systemSettings')}</h2>
|
||||||
<span className="text-gray-500">
|
<span className="text-gray-500">{sectionsVisible.nameSeparator ? '▼' : '►'}</span>
|
||||||
{sectionsVisible.nameSeparator ? '▼' : '►'}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{sectionsVisible.nameSeparator && (
|
{sectionsVisible.nameSeparator && (
|
||||||
@@ -490,9 +610,7 @@ const SettingsPage: React.FC = () => {
|
|||||||
onClick={() => toggleSection('routingConfig')}
|
onClick={() => toggleSection('routingConfig')}
|
||||||
>
|
>
|
||||||
<h2 className="font-semibold text-gray-800">{t('pages.settings.routeConfig')}</h2>
|
<h2 className="font-semibold text-gray-800">{t('pages.settings.routeConfig')}</h2>
|
||||||
<span className="text-gray-500">
|
<span className="text-gray-500">{sectionsVisible.routingConfig ? '▼' : '►'}</span>
|
||||||
{sectionsVisible.routingConfig ? '▼' : '►'}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{sectionsVisible.routingConfig && (
|
{sectionsVisible.routingConfig && (
|
||||||
@@ -505,7 +623,9 @@ const SettingsPage: React.FC = () => {
|
|||||||
<Switch
|
<Switch
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
checked={routingConfig.enableBearerAuth}
|
checked={routingConfig.enableBearerAuth}
|
||||||
onCheckedChange={(checked) => handleRoutingConfigChange('enableBearerAuth', checked)}
|
onCheckedChange={(checked) =>
|
||||||
|
handleRoutingConfigChange('enableBearerAuth', checked)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -538,24 +658,32 @@ const SettingsPage: React.FC = () => {
|
|||||||
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-md">
|
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-md">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-medium text-gray-700">{t('settings.enableGlobalRoute')}</h3>
|
<h3 className="font-medium text-gray-700">{t('settings.enableGlobalRoute')}</h3>
|
||||||
<p className="text-sm text-gray-500">{t('settings.enableGlobalRouteDescription')}</p>
|
<p className="text-sm text-gray-500">
|
||||||
|
{t('settings.enableGlobalRouteDescription')}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
checked={routingConfig.enableGlobalRoute}
|
checked={routingConfig.enableGlobalRoute}
|
||||||
onCheckedChange={(checked) => handleRoutingConfigChange('enableGlobalRoute', checked)}
|
onCheckedChange={(checked) =>
|
||||||
|
handleRoutingConfigChange('enableGlobalRoute', checked)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-md">
|
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-md">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-medium text-gray-700">{t('settings.enableGroupNameRoute')}</h3>
|
<h3 className="font-medium text-gray-700">{t('settings.enableGroupNameRoute')}</h3>
|
||||||
<p className="text-sm text-gray-500">{t('settings.enableGroupNameRouteDescription')}</p>
|
<p className="text-sm text-gray-500">
|
||||||
|
{t('settings.enableGroupNameRouteDescription')}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
checked={routingConfig.enableGroupNameRoute}
|
checked={routingConfig.enableGroupNameRoute}
|
||||||
onCheckedChange={(checked) => handleRoutingConfigChange('enableGroupNameRoute', checked)}
|
onCheckedChange={(checked) =>
|
||||||
|
handleRoutingConfigChange('enableGroupNameRoute', checked)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -572,7 +700,6 @@ const SettingsPage: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</PermissionChecker>
|
</PermissionChecker>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -585,9 +712,7 @@ const SettingsPage: React.FC = () => {
|
|||||||
onClick={() => toggleSection('installConfig')}
|
onClick={() => toggleSection('installConfig')}
|
||||||
>
|
>
|
||||||
<h2 className="font-semibold text-gray-800">{t('settings.installConfig')}</h2>
|
<h2 className="font-semibold text-gray-800">{t('settings.installConfig')}</h2>
|
||||||
<span className="text-gray-500">
|
<span className="text-gray-500">{sectionsVisible.installConfig ? '▼' : '►'}</span>
|
||||||
{sectionsVisible.installConfig ? '▼' : '►'}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{sectionsVisible.installConfig && (
|
{sectionsVisible.installConfig && (
|
||||||
@@ -675,9 +800,7 @@ const SettingsPage: React.FC = () => {
|
|||||||
onClick={() => toggleSection('password')}
|
onClick={() => toggleSection('password')}
|
||||||
>
|
>
|
||||||
<h2 className="font-semibold text-gray-800">{t('auth.changePassword')}</h2>
|
<h2 className="font-semibold text-gray-800">{t('auth.changePassword')}</h2>
|
||||||
<span className="text-gray-500">
|
<span className="text-gray-500">{sectionsVisible.password ? '▼' : '►'}</span>
|
||||||
{sectionsVisible.password ? '▼' : '►'}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{sectionsVisible.password && (
|
{sectionsVisible.password && (
|
||||||
@@ -686,8 +809,61 @@ const SettingsPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div >
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SettingsPage;
|
{/* Export MCP Settings */}
|
||||||
|
<PermissionChecker permissions={PERMISSIONS.SETTINGS_EXPORT_CONFIG}>
|
||||||
|
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6 dashboard-card">
|
||||||
|
<div
|
||||||
|
className="flex justify-between items-center cursor-pointer"
|
||||||
|
onClick={() => toggleSection('exportConfig')}
|
||||||
|
>
|
||||||
|
<h2 className="font-semibold text-gray-800">{t('settings.exportMcpSettings')}</h2>
|
||||||
|
<span className="text-gray-500">{sectionsVisible.exportConfig ? '▼' : '►'}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{sectionsVisible.exportConfig && (
|
||||||
|
<div className="space-y-4 mt-4">
|
||||||
|
<div className="p-3 bg-gray-50 rounded-md">
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="font-medium text-gray-700">{t('settings.mcpSettingsJson')}</h3>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
{t('settings.mcpSettingsJsonDescription')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
onClick={handleCopyConfig}
|
||||||
|
disabled={!mcpSettingsJson}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium disabled:opacity-50 btn-primary"
|
||||||
|
>
|
||||||
|
{copiedConfig ? <Check size={16} /> : <Copy size={16} />}
|
||||||
|
{copiedConfig ? t('common.copied') : t('settings.copyToClipboard')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleDownloadConfig}
|
||||||
|
disabled={!mcpSettingsJson}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md text-sm font-medium disabled:opacity-50 btn-primary"
|
||||||
|
>
|
||||||
|
<Download size={16} />
|
||||||
|
{t('settings.downloadJson')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{mcpSettingsJson && (
|
||||||
|
<div className="mt-3">
|
||||||
|
<pre className="bg-gray-900 text-gray-100 p-4 rounded-md overflow-x-auto text-xs max-h-96">
|
||||||
|
{mcpSettingsJson}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</PermissionChecker>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SettingsPage
|
||||||
|
|||||||
@@ -75,6 +75,7 @@
|
|||||||
"addServer": "Add Server",
|
"addServer": "Add Server",
|
||||||
"add": "Add",
|
"add": "Add",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
|
"copy": "Copy",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"confirmDelete": "Are you sure you want to delete this server?",
|
"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.",
|
"deleteWarning": "Deleting server '{{name}}' will remove it and all its data. This action cannot be undone.",
|
||||||
@@ -124,6 +125,7 @@
|
|||||||
"argumentsPlaceholder": "Enter arguments",
|
"argumentsPlaceholder": "Enter arguments",
|
||||||
"errorDetails": "Error Details",
|
"errorDetails": "Error Details",
|
||||||
"viewErrorDetails": "View error details",
|
"viewErrorDetails": "View error details",
|
||||||
|
"copyConfig": "Copy Configuration",
|
||||||
"confirmVariables": "Confirm Variable Configuration",
|
"confirmVariables": "Confirm Variable Configuration",
|
||||||
"variablesDetected": "Variables detected in configuration. Please confirm these variables are properly configured:",
|
"variablesDetected": "Variables detected in configuration. Please confirm these variables are properly configured:",
|
||||||
"detectedVariables": "Detected Variables",
|
"detectedVariables": "Detected Variables",
|
||||||
@@ -200,6 +202,7 @@
|
|||||||
"copyJson": "Copy JSON",
|
"copyJson": "Copy JSON",
|
||||||
"copySuccess": "Copied to clipboard",
|
"copySuccess": "Copied to clipboard",
|
||||||
"copyFailed": "Copy failed",
|
"copyFailed": "Copy failed",
|
||||||
|
"copied": "Copied",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
@@ -502,7 +505,14 @@
|
|||||||
"systemSettings": "System Settings",
|
"systemSettings": "System Settings",
|
||||||
"nameSeparatorLabel": "Name Separator",
|
"nameSeparatorLabel": "Name Separator",
|
||||||
"nameSeparatorDescription": "Character used to separate server name and tool/prompt name (default: -)",
|
"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": {
|
"dxt": {
|
||||||
"upload": "Upload",
|
"upload": "Upload",
|
||||||
|
|||||||
@@ -75,6 +75,7 @@
|
|||||||
"addServer": "Ajouter un serveur",
|
"addServer": "Ajouter un serveur",
|
||||||
"add": "Ajouter",
|
"add": "Ajouter",
|
||||||
"edit": "Modifier",
|
"edit": "Modifier",
|
||||||
|
"copy": "Copier",
|
||||||
"delete": "Supprimer",
|
"delete": "Supprimer",
|
||||||
"confirmDelete": "Êtes-vous sûr de vouloir supprimer ce serveur ?",
|
"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.",
|
"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",
|
"argumentsPlaceholder": "Entrez les arguments",
|
||||||
"errorDetails": "Détails de l'erreur",
|
"errorDetails": "Détails de l'erreur",
|
||||||
"viewErrorDetails": "Voir les détails de l'erreur",
|
"viewErrorDetails": "Voir les détails de l'erreur",
|
||||||
|
"copyConfig": "Copier la configuration",
|
||||||
"confirmVariables": "Confirmer la configuration des variables",
|
"confirmVariables": "Confirmer la configuration des variables",
|
||||||
"variablesDetected": "Variables détectées dans la configuration. Veuillez confirmer que ces variables sont correctement configurées :",
|
"variablesDetected": "Variables détectées dans la configuration. Veuillez confirmer que ces variables sont correctement configurées :",
|
||||||
"detectedVariables": "Variables détectées",
|
"detectedVariables": "Variables détectées",
|
||||||
@@ -200,6 +202,7 @@
|
|||||||
"copyJson": "Copier le JSON",
|
"copyJson": "Copier le JSON",
|
||||||
"copySuccess": "Copié dans le presse-papiers",
|
"copySuccess": "Copié dans le presse-papiers",
|
||||||
"copyFailed": "Échec de la copie",
|
"copyFailed": "Échec de la copie",
|
||||||
|
"copied": "Copié",
|
||||||
"close": "Fermer",
|
"close": "Fermer",
|
||||||
"confirm": "Confirmer",
|
"confirm": "Confirmer",
|
||||||
"language": "Langue",
|
"language": "Langue",
|
||||||
@@ -502,7 +505,14 @@
|
|||||||
"systemSettings": "Paramètres système",
|
"systemSettings": "Paramètres système",
|
||||||
"nameSeparatorLabel": "Séparateur de noms",
|
"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 : -)",
|
"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": {
|
"dxt": {
|
||||||
"upload": "Télécharger",
|
"upload": "Télécharger",
|
||||||
|
|||||||
@@ -75,6 +75,7 @@
|
|||||||
"addServer": "添加服务器",
|
"addServer": "添加服务器",
|
||||||
"add": "添加",
|
"add": "添加",
|
||||||
"edit": "编辑",
|
"edit": "编辑",
|
||||||
|
"copy": "复制",
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
"confirmDelete": "您确定要删除此服务器吗?",
|
"confirmDelete": "您确定要删除此服务器吗?",
|
||||||
"deleteWarning": "删除服务器 '{{name}}' 将会移除该服务器及其所有数据。此操作无法撤销。",
|
"deleteWarning": "删除服务器 '{{name}}' 将会移除该服务器及其所有数据。此操作无法撤销。",
|
||||||
@@ -124,6 +125,7 @@
|
|||||||
"argumentsPlaceholder": "请输入参数",
|
"argumentsPlaceholder": "请输入参数",
|
||||||
"errorDetails": "错误详情",
|
"errorDetails": "错误详情",
|
||||||
"viewErrorDetails": "查看错误详情",
|
"viewErrorDetails": "查看错误详情",
|
||||||
|
"copyConfig": "复制配置",
|
||||||
"confirmVariables": "确认变量配置",
|
"confirmVariables": "确认变量配置",
|
||||||
"variablesDetected": "检测到配置中包含变量,请确认这些变量是否已正确配置:",
|
"variablesDetected": "检测到配置中包含变量,请确认这些变量是否已正确配置:",
|
||||||
"detectedVariables": "检测到的变量",
|
"detectedVariables": "检测到的变量",
|
||||||
@@ -201,6 +203,7 @@
|
|||||||
"copyJson": "复制JSON",
|
"copyJson": "复制JSON",
|
||||||
"copySuccess": "已复制到剪贴板",
|
"copySuccess": "已复制到剪贴板",
|
||||||
"copyFailed": "复制失败",
|
"copyFailed": "复制失败",
|
||||||
|
"copied": "已复制",
|
||||||
"close": "关闭",
|
"close": "关闭",
|
||||||
"confirm": "确认",
|
"confirm": "确认",
|
||||||
"language": "语言",
|
"language": "语言",
|
||||||
@@ -504,7 +507,14 @@
|
|||||||
"systemSettings": "系统设置",
|
"systemSettings": "系统设置",
|
||||||
"nameSeparatorLabel": "名称分隔符",
|
"nameSeparatorLabel": "名称分隔符",
|
||||||
"nameSeparatorDescription": "用于分隔服务器名称和工具/提示名称(默认:-)",
|
"nameSeparatorDescription": "用于分隔服务器名称和工具/提示名称(默认:-)",
|
||||||
"restartRequired": "配置已保存。为确保所有服务正确加载新设置,建议重启应用。"
|
"restartRequired": "配置已保存。为确保所有服务正确加载新设置,建议重启应用。",
|
||||||
|
"exportMcpSettings": "导出配置",
|
||||||
|
"mcpSettingsJson": "MCP 配置 JSON",
|
||||||
|
"mcpSettingsJsonDescription": "查看、复制或下载当前的 mcp_settings.json 配置,可用于备份或迁移到其他工具",
|
||||||
|
"copyToClipboard": "复制到剪贴板",
|
||||||
|
"downloadJson": "下载 JSON",
|
||||||
|
"exportSuccess": "配置导出成功",
|
||||||
|
"exportError": "获取配置失败"
|
||||||
},
|
},
|
||||||
"dxt": {
|
"dxt": {
|
||||||
"upload": "上传",
|
"upload": "上传",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import config from '../config/index.js';
|
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 { getDataService } from '../services/services.js';
|
||||||
import { DataService } from '../services/dataService.js';
|
import { DataService } from '../services/dataService.js';
|
||||||
import { IUser } from '../types/index.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',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ import {
|
|||||||
} from '../controllers/cloudController.js';
|
} from '../controllers/cloudController.js';
|
||||||
import { login, register, getCurrentUser, changePassword } from '../controllers/authController.js';
|
import { login, register, getCurrentUser, changePassword } from '../controllers/authController.js';
|
||||||
import { getAllLogs, clearLogs, streamLogs } from '../controllers/logController.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 { callTool } from '../controllers/toolController.js';
|
||||||
import { getPrompt } from '../controllers/promptController.js';
|
import { getPrompt } from '../controllers/promptController.js';
|
||||||
import { uploadDxtFile, uploadMiddleware } from '../controllers/dxtController.js';
|
import { uploadDxtFile, uploadMiddleware } from '../controllers/dxtController.js';
|
||||||
@@ -149,6 +149,9 @@ export const initRoutes = (app: express.Application): void => {
|
|||||||
router.delete('/logs', clearLogs);
|
router.delete('/logs', clearLogs);
|
||||||
router.get('/logs/stream', streamLogs);
|
router.get('/logs/stream', streamLogs);
|
||||||
|
|
||||||
|
// MCP settings export route
|
||||||
|
router.get('/mcp-settings/export', getMcpSettingsJson);
|
||||||
|
|
||||||
// Auth routes - move to router instead of app directly
|
// Auth routes - move to router instead of app directly
|
||||||
router.post(
|
router.post(
|
||||||
'/auth/login',
|
'/auth/login',
|
||||||
|
|||||||
139
tests/controllers/configController.test.ts
Normal file
139
tests/controllers/configController.test.ts
Normal file
@@ -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<Request>
|
||||||
|
let mockResponse: Partial<Response>
|
||||||
|
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',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user