feat: add installation configuration support with pythonIndexUrl in settings (#67)

This commit is contained in:
samanhappy
2025-05-10 21:33:35 +08:00
committed by GitHub
parent 7c43ca359e
commit 7af3c8a2ba
9 changed files with 183 additions and 15 deletions

View File

@@ -34,7 +34,6 @@ RUN if [ "$INSTALL_EXT" = "true" ]; then \
fi fi
RUN uv tool install mcp-server-fetch RUN uv tool install mcp-server-fetch
ENV UV_PYTHON_INSTALL_MIRROR="http://mirrors.aliyun.com/pypi/simple/"
WORKDIR /app WORKDIR /app

View File

@@ -18,6 +18,5 @@ if [ -n "$HTTPS_PROXY" ]; then
fi fi
echo "Using REQUEST_TIMEOUT: $REQUEST_TIMEOUT" echo "Using REQUEST_TIMEOUT: $REQUEST_TIMEOUT"
echo "Using UV_PYTHON_INSTALL_MIRROR: $UV_PYTHON_INSTALL_MIRROR"
exec "$@" exec "$@"

View File

@@ -9,9 +9,14 @@ interface RoutingConfig {
enableGroupNameRoute: boolean; enableGroupNameRoute: boolean;
} }
interface InstallConfig {
pythonIndexUrl: string;
}
interface SystemSettings { interface SystemSettings {
systemConfig?: { systemConfig?: {
routing?: RoutingConfig; routing?: RoutingConfig;
install?: InstallConfig;
}; };
} }
@@ -24,6 +29,10 @@ export const useSettingsData = () => {
enableGroupNameRoute: true, enableGroupNameRoute: true,
}); });
const [installConfig, setInstallConfig] = useState<InstallConfig>({
pythonIndexUrl: '',
});
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [refreshKey, setRefreshKey] = useState(0); const [refreshKey, setRefreshKey] = useState(0);
@@ -58,6 +67,12 @@ export const useSettingsData = () => {
enableGroupNameRoute: data.data.systemConfig.routing.enableGroupNameRoute ?? true, enableGroupNameRoute: data.data.systemConfig.routing.enableGroupNameRoute ?? true,
}); });
} }
if (data.success && data.data?.systemConfig?.install) {
setInstallConfig({
pythonIndexUrl: data.data.systemConfig.install.pythonIndexUrl || '',
});
}
} catch (error) { } catch (error) {
console.error('Failed to fetch settings:', error); console.error('Failed to fetch settings:', error);
setError(error instanceof Error ? error.message : 'Failed to fetch settings'); setError(error instanceof Error ? error.message : 'Failed to fetch settings');
@@ -114,6 +129,53 @@ export const useSettingsData = () => {
} }
}; };
// Update install configuration
const updateInstallConfig = async (key: keyof InstallConfig, value: string) => {
setLoading(true);
setError(null);
try {
const token = localStorage.getItem('mcphub_token');
const response = await fetch('/api/system-config', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'x-auth-token': token || '',
},
body: JSON.stringify({
install: {
[key]: value,
},
}),
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
if (data.success) {
setInstallConfig({
...installConfig,
[key]: value,
});
showToast(t('settings.systemConfigUpdated'));
return true;
} else {
showToast(t('errors.failedToUpdateSystemConfig'));
return false;
}
} catch (error) {
console.error('Failed to update system config:', error);
setError(error instanceof Error ? error.message : 'Failed to update system config');
showToast(t('errors.failedToUpdateSystemConfig'));
return false;
} 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();
@@ -121,11 +183,13 @@ export const useSettingsData = () => {
return { return {
routingConfig, routingConfig,
installConfig,
loading, loading,
error, error,
setError, setError,
triggerRefresh, triggerRefresh,
fetchSettings, fetchSettings,
updateRoutingConfig, updateRoutingConfig,
updateInstallConfig,
}; };
}; };

View File

@@ -148,7 +148,8 @@
"account": "Account Settings", "account": "Account Settings",
"password": "Change Password", "password": "Change Password",
"appearance": "Appearance", "appearance": "Appearance",
"routeConfig": "Route Configuration" "routeConfig": "Route Configuration",
"installConfig": "Installation Configuration"
}, },
"market": { "market": {
"title": "Server Market - (Data from mcpm.sh)" "title": "Server Market - (Data from mcpm.sh)"
@@ -243,6 +244,10 @@
"enableGlobalRouteDescription": "Allow connections to /sse endpoint without specifying a group ID", "enableGlobalRouteDescription": "Allow connections to /sse endpoint without specifying a group ID",
"enableGroupNameRoute": "Enable Group Name Route", "enableGroupNameRoute": "Enable Group Name Route",
"enableGroupNameRouteDescription": "Allow connections to /sse endpoint using group names instead of just group IDs", "enableGroupNameRouteDescription": "Allow connections to /sse endpoint using group names instead of just group IDs",
"pythonIndexUrl": "Python Package Repository URL",
"pythonIndexUrlDescription": "Set UV_DEFAULT_INDEX environment variable for Python package installation",
"pythonIndexUrlPlaceholder": "e.g. https://pypi.org/simple",
"installConfig": "Installation Configuration",
"systemConfigUpdated": "System configuration updated successfully" "systemConfigUpdated": "System configuration updated successfully"
} }
} }

View File

@@ -146,7 +146,8 @@
"account": "账户设置", "account": "账户设置",
"password": "修改密码", "password": "修改密码",
"appearance": "外观", "appearance": "外观",
"routeConfig": "路由配置" "routeConfig": "路由配置",
"installConfig": "安装配置"
}, },
"groups": { "groups": {
"title": "分组管理" "title": "分组管理"
@@ -241,9 +242,13 @@
}, },
"settings": { "settings": {
"enableGlobalRoute": "启用全局路由", "enableGlobalRoute": "启用全局路由",
"enableGlobalRouteDescription": "允许不指定组 ID 就连接到 /sse 端点", "enableGlobalRouteDescription": "允许不指定组 ID 就连接到 /sse 端点",
"enableGroupNameRoute": "启用组名路由", "enableGroupNameRoute": "启用组名路由",
"enableGroupNameRouteDescription": "允许使用组名称而非分组 ID 连接到 /sse 端点", "enableGroupNameRouteDescription": "允许使用组名而不仅仅是组 ID 连接到 /sse 端点",
"pythonIndexUrl": "Python 包仓库地址",
"pythonIndexUrlDescription": "设置 UV_DEFAULT_INDEX 环境变量,用于 Python 包安装",
"pythonIndexUrlPlaceholder": "例如: https://mirrors.aliyun.com/pypi/simple",
"installConfig": "安装配置",
"systemConfigUpdated": "系统配置更新成功" "systemConfigUpdated": "系统配置更新成功"
} }
} }

View File

@@ -17,18 +17,34 @@ const SettingsPage: React.FC = () => {
setCurrentLanguage(i18n.language); setCurrentLanguage(i18n.language);
}, [i18n.language]); }, [i18n.language]);
const [installConfig, setInstallConfig] = useState<{
pythonIndexUrl: string;
}>({
pythonIndexUrl: '',
});
const { const {
routingConfig, routingConfig,
installConfig: savedInstallConfig,
loading, loading,
updateRoutingConfig updateRoutingConfig,
updateInstallConfig
} = useSettingsData(); } = useSettingsData();
// Update local installConfig when savedInstallConfig changes
useEffect(() => {
if (savedInstallConfig) {
setInstallConfig(savedInstallConfig);
}
}, [savedInstallConfig]);
const [sectionsVisible, setSectionsVisible] = useState({ const [sectionsVisible, setSectionsVisible] = useState({
routingConfig: false, routingConfig: false,
installConfig: false,
password: false password: false
}); });
const toggleSection = (section: 'routingConfig' | 'password') => { const toggleSection = (section: 'routingConfig' | 'installConfig' | 'password') => {
setSectionsVisible(prev => ({ setSectionsVisible(prev => ({
...prev, ...prev,
[section]: !prev[section] [section]: !prev[section]
@@ -39,6 +55,17 @@ const SettingsPage: React.FC = () => {
await updateRoutingConfig(key, value); await updateRoutingConfig(key, value);
}; };
const handleInstallConfigChange = (value: string) => {
setInstallConfig({
...installConfig,
pythonIndexUrl: value
});
};
const saveInstallConfig = async () => {
await updateInstallConfig('pythonIndexUrl', installConfig.pythonIndexUrl);
};
const handlePasswordChangeSuccess = () => { const handlePasswordChangeSuccess = () => {
setTimeout(() => { setTimeout(() => {
navigate('/'); navigate('/');
@@ -124,6 +151,47 @@ const SettingsPage: React.FC = () => {
)} )}
</div> </div>
{/* Installation Configuration Settings */}
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6">
<div
className="flex justify-between items-center cursor-pointer"
onClick={() => toggleSection('installConfig')}
>
<h2 className="font-semibold text-gray-800">{t('settings.installConfig')}</h2>
<span className="text-gray-500">
{sectionsVisible.installConfig ? '▼' : '►'}
</span>
</div>
{sectionsVisible.installConfig && (
<div className="space-y-4 mt-4">
<div className="p-3 bg-gray-50 rounded-md">
<div className="mb-2">
<h3 className="font-medium text-gray-700">{t('settings.pythonIndexUrl')}</h3>
<p className="text-sm text-gray-500">{t('settings.pythonIndexUrlDescription')}</p>
</div>
<div className="flex items-center gap-3">
<input
type="text"
value={installConfig.pythonIndexUrl}
onChange={(e) => handleInstallConfigChange(e.target.value)}
placeholder={t('settings.pythonIndexUrlPlaceholder')}
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"
disabled={loading}
/>
<button
onClick={saveInstallConfig}
disabled={loading}
className="mt-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium disabled:opacity-50"
>
{t('common.save')}
</button>
</div>
</div>
</div>
)}
</div>
{/* Change Password */} {/* Change Password */}
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6"> <div className="bg-white shadow rounded-lg py-4 px-6 mb-6">
<div <div

View File

@@ -247,9 +247,10 @@ export const toggleServer = async (req: Request, res: Response): Promise<void> =
export const updateSystemConfig = (req: Request, res: Response): void => { export const updateSystemConfig = (req: Request, res: Response): void => {
try { try {
const { routing } = req.body; const { routing, install } = req.body;
if (!routing || (typeof routing.enableGlobalRoute !== 'boolean' && typeof routing.enableGroupNameRoute !== 'boolean')) { if ((!routing || (typeof routing.enableGlobalRoute !== 'boolean' && typeof routing.enableGroupNameRoute !== 'boolean'))
&& (!install || typeof install.pythonIndexUrl !== 'string')) {
res.status(400).json({ res.status(400).json({
success: false, success: false,
message: 'Invalid system configuration provided', message: 'Invalid system configuration provided',
@@ -263,6 +264,9 @@ export const updateSystemConfig = (req: Request, res: Response): void => {
routing: { routing: {
enableGlobalRoute: true, enableGlobalRoute: true,
enableGroupNameRoute: true enableGroupNameRoute: true
},
install: {
pythonIndexUrl: ''
} }
}; };
} }
@@ -274,6 +278,13 @@ export const updateSystemConfig = (req: Request, res: Response): void => {
}; };
} }
if (!settings.systemConfig.install) {
settings.systemConfig.install = {
pythonIndexUrl: ''
};
}
if (routing) {
if (typeof routing.enableGlobalRoute === 'boolean') { if (typeof routing.enableGlobalRoute === 'boolean') {
settings.systemConfig.routing.enableGlobalRoute = routing.enableGlobalRoute; settings.systemConfig.routing.enableGlobalRoute = routing.enableGlobalRoute;
} }
@@ -281,6 +292,13 @@ export const updateSystemConfig = (req: Request, res: Response): void => {
if (typeof routing.enableGroupNameRoute === 'boolean') { if (typeof routing.enableGroupNameRoute === 'boolean') {
settings.systemConfig.routing.enableGroupNameRoute = routing.enableGroupNameRoute; settings.systemConfig.routing.enableGroupNameRoute = routing.enableGroupNameRoute;
} }
}
if (install) {
if (typeof install.pythonIndexUrl === 'string') {
settings.systemConfig.install.pythonIndexUrl = install.pythonIndexUrl;
}
}
if (saveSettings(settings)) { if (saveSettings(settings)) {
res.json({ res.json({

View File

@@ -79,6 +79,13 @@ export const initializeClientsFromSettings = (isInit: boolean): ServerInfo[] =>
} else if (conf.command && conf.args) { } else if (conf.command && conf.args) {
const env: Record<string, string> = conf.env || {}; const env: Record<string, string> = conf.env || {};
env['PATH'] = expandEnvVars(process.env.PATH as string) || ''; env['PATH'] = expandEnvVars(process.env.PATH as string) || '';
// Add UV_DEFAULT_INDEX from settings if available (for Python packages)
const settings = loadSettings();
if (settings.systemConfig?.install?.pythonIndexUrl && conf.command === 'uvx') {
env['UV_DEFAULT_INDEX'] = settings.systemConfig.install.pythonIndexUrl;
}
transport = new StdioClientTransport({ transport = new StdioClientTransport({
command: conf.command, command: conf.command,
args: conf.args, args: conf.args,

View File

@@ -83,6 +83,9 @@ export interface McpSettings {
enableGlobalRoute?: boolean; // Controls whether the /sse endpoint without group is enabled enableGlobalRoute?: boolean; // Controls whether the /sse endpoint without group is enabled
enableGroupNameRoute?: boolean; // Controls whether group routing by name is allowed enableGroupNameRoute?: boolean; // Controls whether group routing by name is allowed
}; };
install?: {
pythonIndexUrl?: string; // Python package repository URL (UV_DEFAULT_INDEX)
};
// Add other system configuration sections here in the future // Add other system configuration sections here in the future
}; };
} }