Add HTTP headers support and batch update for routing configuration (#151)

This commit is contained in:
samanhappy
2025-05-31 18:36:41 +08:00
committed by GitHub
parent 394945bbc5
commit d2bbadea83
9 changed files with 221 additions and 25 deletions

View File

@@ -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<EnvVar[]>(
@@ -49,6 +50,12 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
: [],
)
const [headerVars, setHeaderVars] = useState<EnvVar[]>(
initialData && initialData.config && initialData.config.headers
? Object.entries(initialData.config.headers).map(([key, value]) => ({ key, value }))
: [],
)
const [error, setError] = useState<string | null>(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<string, string> = {}
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
</div>
{serverType === 'sse' || serverType === 'streamable-http' ? (
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="url">
{t('server.url')}
</label>
<input
type="url"
name="url"
id="url"
value={formData.url}
onChange={handleInputChange}
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
placeholder={serverType === 'streamable-http' ? "e.g.: http://localhost:3000/mcp" : "e.g.: http://localhost:3000/sse"}
required={serverType === 'sse' || serverType === 'streamable-http'}
/>
</div>
<>
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="url">
{t('server.url')}
</label>
<input
type="url"
name="url"
id="url"
value={formData.url}
onChange={handleInputChange}
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
placeholder={serverType === 'streamable-http' ? "e.g.: http://localhost:3000/mcp" : "e.g.: http://localhost:3000/sse"}
required={serverType === 'sse' || serverType === 'streamable-http'}
/>
</div>
<div className="mb-4">
<div className="flex justify-between items-center mb-2">
<label className="block text-gray-700 text-sm font-bold">
{t('server.headers')}
</label>
<button
type="button"
onClick={addHeaderVar}
className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-1 px-2 rounded text-sm flex items-center"
>
+ {t('server.add')}
</button>
</div>
{headerVars.map((headerVar, index) => (
<div key={index} className="flex items-center mb-2">
<div className="flex items-center space-x-2 flex-grow">
<input
type="text"
value={headerVar.key}
onChange={(e) => 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"
/>
<span className="flex items-center">:</span>
<input
type="text"
value={headerVar.value}
onChange={(e) => 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..."
/>
</div>
<button
type="button"
onClick={() => removeHeaderVar(index)}
className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-1 px-2 rounded text-sm flex items-center justify-center min-w-[56px] ml-2"
>
- {t('server.remove')}
</button>
</div>
))}
</div>
</>
) : (
<>
<div className="mb-4">

View File

@@ -325,6 +325,51 @@ export const useSettingsData = () => {
}
};
// Update multiple routing configuration fields at once
const updateRoutingConfigBatch = async (updates: Partial<RoutingConfig>) => {
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,
};
};

View File

@@ -93,6 +93,7 @@
"command": "Command",
"arguments": "Arguments",
"envVars": "Environment Variables",
"headers": "HTTP Headers",
"key": "key",
"value": "value",
"enabled": "Enabled",

View File

@@ -93,6 +93,7 @@
"command": "命令",
"arguments": "参数",
"envVars": "环境变量",
"headers": "HTTP 请求头",
"key": "键",
"value": "值",
"enabled": "已启用",

View File

@@ -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) => {

View File

@@ -76,6 +76,7 @@ export interface ServerConfig {
command?: string;
args?: string[];
env?: Record<string, string>;
headers?: Record<string, string>;
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