diff --git a/frontend/src/components/ServerForm.tsx b/frontend/src/components/ServerForm.tsx index 1df5552..2556cd1 100644 --- a/frontend/src/components/ServerForm.tsx +++ b/frontend/src/components/ServerForm.tsx @@ -40,7 +40,8 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr : '', args: (initialData && initialData.config && initialData.config.args) || [], type: getInitialServerType(), // Initialize the type field - env: [] + env: [], + headers: [] }) const [envVars, setEnvVars] = useState( @@ -49,6 +50,12 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr : [], ) + const [headerVars, setHeaderVars] = useState( + initialData && initialData.config && initialData.config.headers + ? Object.entries(initialData.config.headers).map(([key, value]) => ({ key, value })) + : [], + ) + const [error, setError] = useState(null) const isEdit = !!initialData @@ -84,6 +91,22 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr setEnvVars(newEnvVars) } + const handleHeaderVarChange = (index: number, field: 'key' | 'value', value: string) => { + const newHeaderVars = [...headerVars] + newHeaderVars[index][field] = value + setHeaderVars(newHeaderVars) + } + + const addHeaderVar = () => { + setHeaderVars([...headerVars, { key: '', value: '' }]) + } + + const removeHeaderVar = (index: number) => { + const newHeaderVars = [...headerVars] + newHeaderVars.splice(index, 1) + setHeaderVars(newHeaderVars) + } + // Submit handler for server configuration const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() @@ -97,12 +120,22 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr } }) + const headers: Record = {} + headerVars.forEach(({ key, value }) => { + if (key.trim()) { + headers[key.trim()] = value + } + }) + const payload = { name: formData.name, config: { type: serverType, // Always include the type ...(serverType === 'sse' || serverType === 'streamable-http' - ? { url: formData.url } + ? { + url: formData.url, + ...(Object.keys(headers).length > 0 ? { headers } : {}) + } : { command: formData.command, args: formData.args, @@ -194,21 +227,66 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr {serverType === 'sse' || serverType === 'streamable-http' ? ( -
- - -
+ <> +
+ + +
+ +
+
+ + +
+ {headerVars.map((headerVar, index) => ( +
+
+ handleHeaderVarChange(index, 'key', e.target.value)} + className="shadow appearance-none border rounded py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline w-1/2" + placeholder="Authorization" + /> + : + handleHeaderVarChange(index, 'value', e.target.value)} + className="shadow appearance-none border rounded py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline w-1/2" + placeholder="Bearer token..." + /> +
+ +
+ ))} +
+ ) : ( <>
diff --git a/frontend/src/hooks/useSettingsData.ts b/frontend/src/hooks/useSettingsData.ts index 47565be..9ceb755 100644 --- a/frontend/src/hooks/useSettingsData.ts +++ b/frontend/src/hooks/useSettingsData.ts @@ -325,6 +325,51 @@ export const useSettingsData = () => { } }; + // Update multiple routing configuration fields at once + const updateRoutingConfigBatch = async (updates: Partial) => { + setLoading(true); + setError(null); + + try { + const token = localStorage.getItem('mcphub_token'); + const response = await fetch(getApiUrl('/system-config'), { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'x-auth-token': token || '', + }, + body: JSON.stringify({ + routing: updates, + }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + + const data = await response.json(); + + if (data.success) { + setRoutingConfig({ + ...routingConfig, + ...updates, + }); + showToast(t('settings.systemConfigUpdated')); + return true; + } else { + showToast(t('errors.failedToUpdateRouteConfig')); + return false; + } + } catch (error) { + console.error('Failed to update routing config:', error); + setError(error instanceof Error ? error.message : 'Failed to update routing config'); + showToast(t('errors.failedToUpdateRouteConfig')); + return false; + } finally { + setLoading(false); + } + }; + // Fetch settings when the component mounts or refreshKey changes useEffect(() => { fetchSettings(); @@ -353,5 +398,6 @@ export const useSettingsData = () => { updateInstallConfig, updateSmartRoutingConfig, updateSmartRoutingConfigBatch, + updateRoutingConfigBatch, }; }; diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 49bc2c2..6cdfc4e 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -93,6 +93,7 @@ "command": "Command", "arguments": "Arguments", "envVars": "Environment Variables", + "headers": "HTTP Headers", "key": "key", "value": "value", "enabled": "Enabled", diff --git a/frontend/src/locales/zh.json b/frontend/src/locales/zh.json index a3e74c7..7118f0f 100644 --- a/frontend/src/locales/zh.json +++ b/frontend/src/locales/zh.json @@ -93,6 +93,7 @@ "command": "命令", "arguments": "参数", "envVars": "环境变量", + "headers": "HTTP 请求头", "key": "键", "value": "值", "enabled": "已启用", diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index a89be0e..43500a2 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -46,6 +46,7 @@ const SettingsPage: React.FC = () => { smartRoutingConfig, loading, updateRoutingConfig, + updateRoutingConfigBatch, updateInstallConfig, updateSmartRoutingConfig, updateSmartRoutingConfigBatch @@ -85,16 +86,30 @@ const SettingsPage: React.FC = () => { }; const handleRoutingConfigChange = async (key: 'enableGlobalRoute' | 'enableGroupNameRoute' | 'enableBearerAuth' | 'bearerAuthKey', value: boolean | string) => { - await updateRoutingConfig(key, value); - - // If enableBearerAuth is turned on and there's no key, generate one + // If enableBearerAuth is turned on and there's no key, generate one first if (key === 'enableBearerAuth' && value === true) { - if (!tempRoutingConfig.bearerAuthKey) { + if (!tempRoutingConfig.bearerAuthKey && !routingConfig.bearerAuthKey) { const newKey = generateRandomKey(); handleBearerAuthKeyChange(newKey); - await updateRoutingConfig('bearerAuthKey', newKey); + + // Update both enableBearerAuth and bearerAuthKey in a single call + const success = await updateRoutingConfigBatch({ + enableBearerAuth: true, + bearerAuthKey: newKey + }); + + if (success) { + // Update tempRoutingConfig to reflect the saved values + setTempRoutingConfig(prev => ({ + ...prev, + bearerAuthKey: newKey + })); + } + return; } } + + await updateRoutingConfig(key, value); }; const handleBearerAuthKeyChange = (value: string) => { diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 2755a54..a23901e 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -76,6 +76,7 @@ export interface ServerConfig { command?: string; args?: string[]; env?: Record; + headers?: Record; enabled?: boolean; } @@ -112,6 +113,7 @@ export interface ServerFormData { args?: string[]; // Added explicit args field type?: 'stdio' | 'sse' | 'streamable-http'; // Added type field env: EnvVar[]; + headers: EnvVar[]; } // Group form data types diff --git a/src/controllers/serverController.ts b/src/controllers/serverController.ts index 2864acd..081041b 100644 --- a/src/controllers/serverController.ts +++ b/src/controllers/serverController.ts @@ -88,6 +88,24 @@ export const createServer = async (req: Request, res: Response): Promise = return; } + // Validate headers if provided + if (config.headers && typeof config.headers !== 'object') { + res.status(400).json({ + success: false, + message: 'Headers must be an object', + }); + return; + } + + // Validate that headers are only used with sse and streamable-http types + if (config.headers && config.type === 'stdio') { + res.status(400).json({ + success: false, + message: 'Headers are not supported for stdio server type', + }); + return; + } + const result = await addServer(name, config); if (result.success) { notifyToolChanged(); @@ -187,6 +205,24 @@ export const updateServer = async (req: Request, res: Response): Promise = return; } + // Validate headers if provided + if (config.headers && typeof config.headers !== 'object') { + res.status(400).json({ + success: false, + message: 'Headers must be an object', + }); + return; + } + + // Validate that headers are only used with sse and streamable-http types + if (config.headers && config.type === 'stdio') { + res.status(400).json({ + success: false, + message: 'Headers are not supported for stdio server type', + }); + return; + } + const result = await updateMcpServer(name, config); if (result.success) { notifyToolChanged(); diff --git a/src/services/mcpService.ts b/src/services/mcpService.ts index e55b266..9ad9722 100644 --- a/src/services/mcpService.ts +++ b/src/services/mcpService.ts @@ -89,11 +89,27 @@ export const initializeClientsFromSettings = (isInit: boolean): ServerInfo[] => let transport; if (conf.type === 'streamable-http') { - transport = new StreamableHTTPClientTransport(new URL(conf.url || '')); + const options: any = {}; + if (conf.headers && Object.keys(conf.headers).length > 0) { + options.requestInit = { + headers: conf.headers, + }; + } + transport = new StreamableHTTPClientTransport(new URL(conf.url || ''), options); } else if (conf.url) { // Default to SSE only when 'conf.type' is not specified and 'conf.url' is available - transport = new SSEClientTransport(new URL(conf.url)); - } else if (conf.command && conf.args) { // If type is stdio or if command and args are provided without type + const options: any = {}; + if (conf.headers && Object.keys(conf.headers).length > 0) { + options.eventSourceInit = { + headers: conf.headers, + }; + options.requestInit = { + headers: conf.headers, + }; + } + transport = new SSEClientTransport(new URL(conf.url), options); + } else if (conf.command && conf.args) { + // If type is stdio or if command and args are provided without type const env: Record = { ...(process.env as Record), // Inherit all environment variables from parent process ...replaceEnvVars(conf.env || {}), // Override with configured env vars diff --git a/src/types/index.ts b/src/types/index.ts index a66d0ac..20f4433 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -108,6 +108,7 @@ export interface ServerConfig { command?: string; // Command to execute for stdio-based servers args?: string[]; // Arguments for the command env?: Record; // Environment variables + headers?: Record; // HTTP headers for SSE/streamable-http servers enabled?: boolean; // Flag to enable/disable the server }