mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-24 02:39:19 -05:00
feat: integrate offcial mcp server registry (#374)
This commit is contained in:
@@ -1,17 +1,23 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Server, EnvVar, ServerFormData } from '@/types'
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Server, EnvVar, ServerFormData } from '@/types';
|
||||
|
||||
interface ServerFormProps {
|
||||
onSubmit: (payload: any) => void
|
||||
onCancel: () => void
|
||||
initialData?: Server | null
|
||||
modalTitle: string
|
||||
formError?: string | null
|
||||
onSubmit: (payload: any) => void;
|
||||
onCancel: () => void;
|
||||
initialData?: Server | null;
|
||||
modalTitle: string;
|
||||
formError?: string | null;
|
||||
}
|
||||
|
||||
const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formError = null }: ServerFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const ServerForm = ({
|
||||
onSubmit,
|
||||
onCancel,
|
||||
initialData = null,
|
||||
modalTitle,
|
||||
formError = null,
|
||||
}: ServerFormProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Determine the initial server type from the initialData
|
||||
const getInitialServerType = () => {
|
||||
@@ -26,7 +32,19 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
}
|
||||
};
|
||||
|
||||
const [serverType, setServerType] = useState<'stdio' | 'sse' | 'streamable-http' | 'openapi'>(getInitialServerType());
|
||||
const getInitialServerEnvVars = (data: Server | null): EnvVar[] => {
|
||||
if (!data || !data.config || !data.config.env) return [];
|
||||
|
||||
return Object.entries(data.config.env).map(([key, value]) => ({
|
||||
key,
|
||||
value,
|
||||
description: '', // You can set a default description if needed
|
||||
}));
|
||||
};
|
||||
|
||||
const [serverType, setServerType] = useState<'stdio' | 'sse' | 'streamable-http' | 'openapi'>(
|
||||
getInitialServerType(),
|
||||
);
|
||||
|
||||
const [formData, setFormData] = useState<ServerFormData>({
|
||||
name: (initialData && initialData.name) || '',
|
||||
@@ -40,149 +58,178 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
: '',
|
||||
args: (initialData && initialData.config && initialData.config.args) || [],
|
||||
type: getInitialServerType(), // Initialize the type field
|
||||
env: [],
|
||||
env: getInitialServerEnvVars(initialData),
|
||||
headers: [],
|
||||
options: {
|
||||
timeout: (initialData && initialData.config && initialData.config.options && initialData.config.options.timeout) || 60000,
|
||||
resetTimeoutOnProgress: (initialData && initialData.config && initialData.config.options && initialData.config.options.resetTimeoutOnProgress) || false,
|
||||
maxTotalTimeout: (initialData && initialData.config && initialData.config.options && initialData.config.options.maxTotalTimeout) || undefined,
|
||||
timeout:
|
||||
(initialData &&
|
||||
initialData.config &&
|
||||
initialData.config.options &&
|
||||
initialData.config.options.timeout) ||
|
||||
60000,
|
||||
resetTimeoutOnProgress:
|
||||
(initialData &&
|
||||
initialData.config &&
|
||||
initialData.config.options &&
|
||||
initialData.config.options.resetTimeoutOnProgress) ||
|
||||
false,
|
||||
maxTotalTimeout:
|
||||
(initialData &&
|
||||
initialData.config &&
|
||||
initialData.config.options &&
|
||||
initialData.config.options.maxTotalTimeout) ||
|
||||
undefined,
|
||||
},
|
||||
// OpenAPI configuration initialization
|
||||
openapi: initialData && initialData.config && initialData.config.openapi ? {
|
||||
url: initialData.config.openapi.url || '',
|
||||
schema: initialData.config.openapi.schema ? JSON.stringify(initialData.config.openapi.schema, null, 2) : '',
|
||||
inputMode: initialData.config.openapi.url ? 'url' : (initialData.config.openapi.schema ? 'schema' : 'url'),
|
||||
version: initialData.config.openapi.version || '3.1.0',
|
||||
securityType: initialData.config.openapi.security?.type || 'none',
|
||||
// API Key initialization
|
||||
apiKeyName: initialData.config.openapi.security?.apiKey?.name || '',
|
||||
apiKeyIn: initialData.config.openapi.security?.apiKey?.in || 'header',
|
||||
apiKeyValue: initialData.config.openapi.security?.apiKey?.value || '',
|
||||
// HTTP auth initialization
|
||||
httpScheme: initialData.config.openapi.security?.http?.scheme || 'bearer',
|
||||
httpCredentials: initialData.config.openapi.security?.http?.credentials || '',
|
||||
// OAuth2 initialization
|
||||
oauth2Token: initialData.config.openapi.security?.oauth2?.token || '',
|
||||
// OpenID Connect initialization
|
||||
openIdConnectUrl: initialData.config.openapi.security?.openIdConnect?.url || '',
|
||||
openIdConnectToken: initialData.config.openapi.security?.openIdConnect?.token || '',
|
||||
// Passthrough headers initialization
|
||||
passthroughHeaders: initialData.config.openapi.passthroughHeaders ? initialData.config.openapi.passthroughHeaders.join(', ') : '',
|
||||
} : {
|
||||
inputMode: 'url',
|
||||
url: '',
|
||||
schema: '',
|
||||
version: '3.1.0',
|
||||
securityType: 'none',
|
||||
passthroughHeaders: '',
|
||||
}
|
||||
})
|
||||
openapi:
|
||||
initialData && initialData.config && initialData.config.openapi
|
||||
? {
|
||||
url: initialData.config.openapi.url || '',
|
||||
schema: initialData.config.openapi.schema
|
||||
? JSON.stringify(initialData.config.openapi.schema, null, 2)
|
||||
: '',
|
||||
inputMode: initialData.config.openapi.url
|
||||
? 'url'
|
||||
: initialData.config.openapi.schema
|
||||
? 'schema'
|
||||
: 'url',
|
||||
version: initialData.config.openapi.version || '3.1.0',
|
||||
securityType: initialData.config.openapi.security?.type || 'none',
|
||||
// API Key initialization
|
||||
apiKeyName: initialData.config.openapi.security?.apiKey?.name || '',
|
||||
apiKeyIn: initialData.config.openapi.security?.apiKey?.in || 'header',
|
||||
apiKeyValue: initialData.config.openapi.security?.apiKey?.value || '',
|
||||
// HTTP auth initialization
|
||||
httpScheme: initialData.config.openapi.security?.http?.scheme || 'bearer',
|
||||
httpCredentials: initialData.config.openapi.security?.http?.credentials || '',
|
||||
// OAuth2 initialization
|
||||
oauth2Token: initialData.config.openapi.security?.oauth2?.token || '',
|
||||
// OpenID Connect initialization
|
||||
openIdConnectUrl: initialData.config.openapi.security?.openIdConnect?.url || '',
|
||||
openIdConnectToken: initialData.config.openapi.security?.openIdConnect?.token || '',
|
||||
// Passthrough headers initialization
|
||||
passthroughHeaders: initialData.config.openapi.passthroughHeaders
|
||||
? initialData.config.openapi.passthroughHeaders.join(', ')
|
||||
: '',
|
||||
}
|
||||
: {
|
||||
inputMode: 'url',
|
||||
url: '',
|
||||
schema: '',
|
||||
version: '3.1.0',
|
||||
securityType: 'none',
|
||||
passthroughHeaders: '',
|
||||
},
|
||||
});
|
||||
|
||||
const [envVars, setEnvVars] = useState<EnvVar[]>(
|
||||
initialData && initialData.config && initialData.config.env
|
||||
? Object.entries(initialData.config.env).map(([key, value]) => ({ key, value }))
|
||||
: [],
|
||||
)
|
||||
);
|
||||
|
||||
const [headerVars, setHeaderVars] = useState<EnvVar[]>(
|
||||
initialData && initialData.config && initialData.config.headers
|
||||
? Object.entries(initialData.config.headers).map(([key, value]) => ({ key, value }))
|
||||
: [],
|
||||
)
|
||||
);
|
||||
|
||||
const [isRequestOptionsExpanded, setIsRequestOptionsExpanded] = useState<boolean>(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const isEdit = !!initialData
|
||||
const [isRequestOptionsExpanded, setIsRequestOptionsExpanded] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const isEdit = !!initialData;
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target
|
||||
setFormData({ ...formData, [name]: value })
|
||||
}
|
||||
const { name, value } = e.target;
|
||||
setFormData({ ...formData, [name]: value });
|
||||
};
|
||||
|
||||
// Transform space-separated arguments string into array
|
||||
const handleArgsChange = (value: string) => {
|
||||
const args = value.split(' ').filter((arg) => arg.trim() !== '')
|
||||
setFormData({ ...formData, arguments: value, args })
|
||||
}
|
||||
const args = value.split(' ').filter((arg) => arg.trim() !== '');
|
||||
setFormData({ ...formData, arguments: value, args });
|
||||
};
|
||||
|
||||
const updateServerType = (type: 'stdio' | 'sse' | 'streamable-http' | 'openapi') => {
|
||||
setServerType(type);
|
||||
setFormData(prev => ({ ...prev, type }));
|
||||
}
|
||||
setFormData((prev) => ({ ...prev, type }));
|
||||
};
|
||||
|
||||
const handleEnvVarChange = (index: number, field: 'key' | 'value', value: string) => {
|
||||
const newEnvVars = [...envVars]
|
||||
newEnvVars[index][field] = value
|
||||
setEnvVars(newEnvVars)
|
||||
}
|
||||
const newEnvVars = [...envVars];
|
||||
newEnvVars[index][field] = value;
|
||||
setEnvVars(newEnvVars);
|
||||
};
|
||||
|
||||
const addEnvVar = () => {
|
||||
setEnvVars([...envVars, { key: '', value: '' }])
|
||||
}
|
||||
setEnvVars([...envVars, { key: '', value: '' }]);
|
||||
};
|
||||
|
||||
const removeEnvVar = (index: number) => {
|
||||
const newEnvVars = [...envVars]
|
||||
newEnvVars.splice(index, 1)
|
||||
setEnvVars(newEnvVars)
|
||||
}
|
||||
const newEnvVars = [...envVars];
|
||||
newEnvVars.splice(index, 1);
|
||||
setEnvVars(newEnvVars);
|
||||
};
|
||||
|
||||
const handleHeaderVarChange = (index: number, field: 'key' | 'value', value: string) => {
|
||||
const newHeaderVars = [...headerVars]
|
||||
newHeaderVars[index][field] = value
|
||||
setHeaderVars(newHeaderVars)
|
||||
}
|
||||
const newHeaderVars = [...headerVars];
|
||||
newHeaderVars[index][field] = value;
|
||||
setHeaderVars(newHeaderVars);
|
||||
};
|
||||
|
||||
const addHeaderVar = () => {
|
||||
setHeaderVars([...headerVars, { key: '', value: '' }])
|
||||
}
|
||||
setHeaderVars([...headerVars, { key: '', value: '' }]);
|
||||
};
|
||||
|
||||
const removeHeaderVar = (index: number) => {
|
||||
const newHeaderVars = [...headerVars]
|
||||
newHeaderVars.splice(index, 1)
|
||||
setHeaderVars(newHeaderVars)
|
||||
}
|
||||
const newHeaderVars = [...headerVars];
|
||||
newHeaderVars.splice(index, 1);
|
||||
setHeaderVars(newHeaderVars);
|
||||
};
|
||||
|
||||
// Handle options changes
|
||||
const handleOptionsChange = (field: 'timeout' | 'resetTimeoutOnProgress' | 'maxTotalTimeout', value: number | boolean | undefined) => {
|
||||
setFormData(prev => ({
|
||||
const handleOptionsChange = (
|
||||
field: 'timeout' | 'resetTimeoutOnProgress' | 'maxTotalTimeout',
|
||||
value: number | boolean | undefined,
|
||||
) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
options: {
|
||||
...prev.options,
|
||||
[field]: value
|
||||
}
|
||||
}))
|
||||
}
|
||||
[field]: value,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
// Submit handler for server configuration
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError(null)
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const env: Record<string, string> = {}
|
||||
const env: Record<string, string> = {};
|
||||
envVars.forEach(({ key, value }) => {
|
||||
if (key.trim()) {
|
||||
env[key.trim()] = value
|
||||
env[key.trim()] = value;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const headers: Record<string, string> = {}
|
||||
const headers: Record<string, string> = {};
|
||||
headerVars.forEach(({ key, value }) => {
|
||||
if (key.trim()) {
|
||||
headers[key.trim()] = value
|
||||
headers[key.trim()] = value;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// Prepare options object, only include defined values
|
||||
const options: any = {}
|
||||
const options: any = {};
|
||||
if (formData.options?.timeout && formData.options.timeout !== 60000) {
|
||||
options.timeout = formData.options.timeout
|
||||
options.timeout = formData.options.timeout;
|
||||
}
|
||||
if (formData.options?.resetTimeoutOnProgress) {
|
||||
options.resetTimeoutOnProgress = formData.options.resetTimeoutOnProgress
|
||||
options.resetTimeoutOnProgress = formData.options.resetTimeoutOnProgress;
|
||||
}
|
||||
if (formData.options?.maxTotalTimeout) {
|
||||
options.maxTotalTimeout = formData.options.maxTotalTimeout
|
||||
options.maxTotalTimeout = formData.options.maxTotalTimeout;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
@@ -191,85 +238,87 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
type: serverType, // Always include the type
|
||||
...(serverType === 'openapi'
|
||||
? {
|
||||
openapi: (() => {
|
||||
const openapi: any = {
|
||||
version: formData.openapi?.version || '3.1.0'
|
||||
};
|
||||
|
||||
// Add URL or schema based on input mode
|
||||
if (formData.openapi?.inputMode === 'url') {
|
||||
openapi.url = formData.openapi?.url || '';
|
||||
} else if (formData.openapi?.inputMode === 'schema' && formData.openapi?.schema) {
|
||||
try {
|
||||
openapi.schema = JSON.parse(formData.openapi.schema);
|
||||
} catch (e) {
|
||||
throw new Error('Invalid JSON schema format');
|
||||
}
|
||||
}
|
||||
|
||||
// Add security configuration if provided
|
||||
if (formData.openapi?.securityType && formData.openapi.securityType !== 'none') {
|
||||
openapi.security = {
|
||||
type: formData.openapi.securityType,
|
||||
...(formData.openapi.securityType === 'apiKey' && {
|
||||
apiKey: {
|
||||
name: formData.openapi.apiKeyName || '',
|
||||
in: formData.openapi.apiKeyIn || 'header',
|
||||
value: formData.openapi.apiKeyValue || ''
|
||||
}
|
||||
}),
|
||||
...(formData.openapi.securityType === 'http' && {
|
||||
http: {
|
||||
scheme: formData.openapi.httpScheme || 'bearer',
|
||||
credentials: formData.openapi.httpCredentials || ''
|
||||
}
|
||||
}),
|
||||
...(formData.openapi.securityType === 'oauth2' && {
|
||||
oauth2: {
|
||||
token: formData.openapi.oauth2Token || ''
|
||||
}
|
||||
}),
|
||||
...(formData.openapi.securityType === 'openIdConnect' && {
|
||||
openIdConnect: {
|
||||
url: formData.openapi.openIdConnectUrl || '',
|
||||
token: formData.openapi.openIdConnectToken || ''
|
||||
}
|
||||
})
|
||||
openapi: (() => {
|
||||
const openapi: any = {
|
||||
version: formData.openapi?.version || '3.1.0',
|
||||
};
|
||||
}
|
||||
|
||||
// Add passthrough headers if provided
|
||||
if (formData.openapi?.passthroughHeaders && formData.openapi.passthroughHeaders.trim()) {
|
||||
openapi.passthroughHeaders = formData.openapi.passthroughHeaders
|
||||
.split(',')
|
||||
.map(header => header.trim())
|
||||
.filter(header => header.length > 0);
|
||||
}
|
||||
// Add URL or schema based on input mode
|
||||
if (formData.openapi?.inputMode === 'url') {
|
||||
openapi.url = formData.openapi?.url || '';
|
||||
} else if (formData.openapi?.inputMode === 'schema' && formData.openapi?.schema) {
|
||||
try {
|
||||
openapi.schema = JSON.parse(formData.openapi.schema);
|
||||
} catch (e) {
|
||||
throw new Error('Invalid JSON schema format');
|
||||
}
|
||||
}
|
||||
|
||||
return openapi;
|
||||
})(),
|
||||
...(Object.keys(headers).length > 0 ? { headers } : {})
|
||||
}
|
||||
// Add security configuration if provided
|
||||
if (formData.openapi?.securityType && formData.openapi.securityType !== 'none') {
|
||||
openapi.security = {
|
||||
type: formData.openapi.securityType,
|
||||
...(formData.openapi.securityType === 'apiKey' && {
|
||||
apiKey: {
|
||||
name: formData.openapi.apiKeyName || '',
|
||||
in: formData.openapi.apiKeyIn || 'header',
|
||||
value: formData.openapi.apiKeyValue || '',
|
||||
},
|
||||
}),
|
||||
...(formData.openapi.securityType === 'http' && {
|
||||
http: {
|
||||
scheme: formData.openapi.httpScheme || 'bearer',
|
||||
credentials: formData.openapi.httpCredentials || '',
|
||||
},
|
||||
}),
|
||||
...(formData.openapi.securityType === 'oauth2' && {
|
||||
oauth2: {
|
||||
token: formData.openapi.oauth2Token || '',
|
||||
},
|
||||
}),
|
||||
...(formData.openapi.securityType === 'openIdConnect' && {
|
||||
openIdConnect: {
|
||||
url: formData.openapi.openIdConnectUrl || '',
|
||||
token: formData.openapi.openIdConnectToken || '',
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
// Add passthrough headers if provided
|
||||
if (
|
||||
formData.openapi?.passthroughHeaders &&
|
||||
formData.openapi.passthroughHeaders.trim()
|
||||
) {
|
||||
openapi.passthroughHeaders = formData.openapi.passthroughHeaders
|
||||
.split(',')
|
||||
.map((header) => header.trim())
|
||||
.filter((header) => header.length > 0);
|
||||
}
|
||||
|
||||
return openapi;
|
||||
})(),
|
||||
...(Object.keys(headers).length > 0 ? { headers } : {}),
|
||||
}
|
||||
: serverType === 'sse' || serverType === 'streamable-http'
|
||||
? {
|
||||
url: formData.url,
|
||||
...(Object.keys(headers).length > 0 ? { headers } : {})
|
||||
}
|
||||
url: formData.url,
|
||||
...(Object.keys(headers).length > 0 ? { headers } : {}),
|
||||
}
|
||||
: {
|
||||
command: formData.command,
|
||||
args: formData.args,
|
||||
env: Object.keys(env).length > 0 ? env : undefined,
|
||||
}
|
||||
),
|
||||
...(Object.keys(options).length > 0 ? { options } : {})
|
||||
}
|
||||
}
|
||||
command: formData.command,
|
||||
args: formData.args,
|
||||
env: Object.keys(env).length > 0 ? env : undefined,
|
||||
}),
|
||||
...(Object.keys(options).length > 0 ? { options } : {}),
|
||||
},
|
||||
};
|
||||
|
||||
onSubmit(payload)
|
||||
onSubmit(payload);
|
||||
} catch (err) {
|
||||
setError(`Error: ${err instanceof Error ? err.message : String(err)}`)
|
||||
setError(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white shadow rounded-lg p-6 w-full max-w-xl max-h-screen overflow-y-auto">
|
||||
@@ -281,9 +330,7 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
</div>
|
||||
|
||||
{(error || formError) && (
|
||||
<div className="bg-red-50 text-red-700 p-3 rounded mb-4">
|
||||
{formError || error}
|
||||
</div>
|
||||
<div className="bg-red-50 text-red-700 p-3 rounded mb-4">{formError || error}</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
@@ -373,10 +420,12 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
name="inputMode"
|
||||
value="url"
|
||||
checked={formData.openapi?.inputMode === 'url'}
|
||||
onChange={() => setFormData(prev => ({
|
||||
...prev,
|
||||
openapi: { ...prev.openapi!, inputMode: 'url' }
|
||||
}))}
|
||||
onChange={() =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
openapi: { ...prev.openapi!, inputMode: 'url' },
|
||||
}))
|
||||
}
|
||||
className="mr-1"
|
||||
/>
|
||||
<label htmlFor="input-mode-url">{t('server.openapi.inputModeUrl')}</label>
|
||||
@@ -388,10 +437,12 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
name="inputMode"
|
||||
value="schema"
|
||||
checked={formData.openapi?.inputMode === 'schema'}
|
||||
onChange={() => setFormData(prev => ({
|
||||
...prev,
|
||||
openapi: { ...prev.openapi!, inputMode: 'schema' }
|
||||
}))}
|
||||
onChange={() =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
openapi: { ...prev.openapi!, inputMode: 'schema' },
|
||||
}))
|
||||
}
|
||||
className="mr-1"
|
||||
/>
|
||||
<label htmlFor="input-mode-schema">{t('server.openapi.inputModeSchema')}</label>
|
||||
@@ -410,10 +461,12 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
name="openapi-url"
|
||||
id="openapi-url"
|
||||
value={formData.openapi?.url || ''}
|
||||
onChange={(e) => setFormData(prev => ({
|
||||
...prev,
|
||||
openapi: { ...prev.openapi!, url: e.target.value }
|
||||
}))}
|
||||
onChange={(e) =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
openapi: { ...prev.openapi!, url: e.target.value },
|
||||
}))
|
||||
}
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline form-input"
|
||||
placeholder="e.g.: https://api.example.com/openapi.json"
|
||||
required={serverType === 'openapi' && formData.openapi?.inputMode === 'url'}
|
||||
@@ -424,7 +477,10 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
{/* Schema Input */}
|
||||
{formData.openapi?.inputMode === 'schema' && (
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="openapi-schema">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="openapi-schema"
|
||||
>
|
||||
{t('server.openapi.schema')}
|
||||
</label>
|
||||
<textarea
|
||||
@@ -432,10 +488,12 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
id="openapi-schema"
|
||||
rows={10}
|
||||
value={formData.openapi?.schema || ''}
|
||||
onChange={(e) => setFormData(prev => ({
|
||||
...prev,
|
||||
openapi: { ...prev.openapi!, schema: e.target.value }
|
||||
}))}
|
||||
onChange={(e) =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
openapi: { ...prev.openapi!, schema: e.target.value },
|
||||
}))
|
||||
}
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline font-mono text-sm"
|
||||
placeholder={`{
|
||||
"openapi": "3.1.0",
|
||||
@@ -465,14 +523,16 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
</label>
|
||||
<select
|
||||
value={formData.openapi?.securityType || 'none'}
|
||||
onChange={(e) => setFormData(prev => ({
|
||||
...prev,
|
||||
openapi: {
|
||||
...prev.openapi,
|
||||
securityType: e.target.value as any,
|
||||
url: prev.openapi?.url || ''
|
||||
}
|
||||
}))}
|
||||
onChange={(e) =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
openapi: {
|
||||
...prev.openapi,
|
||||
securityType: e.target.value as any,
|
||||
url: prev.openapi?.url || '',
|
||||
},
|
||||
}))
|
||||
}
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline form-input"
|
||||
>
|
||||
<option value="none">{t('server.openapi.securityNone')}</option>
|
||||
@@ -486,29 +546,47 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
{/* API Key Configuration */}
|
||||
{formData.openapi?.securityType === 'apiKey' && (
|
||||
<div className="mb-4 p-4 border border-gray-200 rounded bg-gray-50">
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-3">{t('server.openapi.apiKeyConfig')}</h4>
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-3">
|
||||
{t('server.openapi.apiKeyConfig')}
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">{t('server.openapi.apiKeyName')}</label>
|
||||
<label className="block text-xs text-gray-600 mb-1">
|
||||
{t('server.openapi.apiKeyName')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.openapi?.apiKeyName || ''}
|
||||
onChange={(e) => setFormData(prev => ({
|
||||
...prev,
|
||||
openapi: { ...prev.openapi, apiKeyName: e.target.value, url: prev.openapi?.url || '' }
|
||||
}))}
|
||||
onChange={(e) =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
openapi: {
|
||||
...prev.openapi,
|
||||
apiKeyName: e.target.value,
|
||||
url: prev.openapi?.url || '',
|
||||
},
|
||||
}))
|
||||
}
|
||||
className="w-full border rounded px-2 py-1 text-sm form-input focus:outline-none"
|
||||
placeholder="Authorization"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">{t('server.openapi.apiKeyIn')}</label>
|
||||
<label className="block text-xs text-gray-600 mb-1">
|
||||
{t('server.openapi.apiKeyIn')}
|
||||
</label>
|
||||
<select
|
||||
value={formData.openapi?.apiKeyIn || 'header'}
|
||||
onChange={(e) => setFormData(prev => ({
|
||||
...prev,
|
||||
openapi: { ...prev.openapi, apiKeyIn: e.target.value as any, url: prev.openapi?.url || '' }
|
||||
}))}
|
||||
onChange={(e) =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
openapi: {
|
||||
...prev.openapi,
|
||||
apiKeyIn: e.target.value as any,
|
||||
url: prev.openapi?.url || '',
|
||||
},
|
||||
}))
|
||||
}
|
||||
className="w-full border rounded px-2 py-1 text-sm focus:outline-none form-input"
|
||||
>
|
||||
<option value="header">{t('server.openapi.apiKeyInHeader')}</option>
|
||||
@@ -517,14 +595,22 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">{t('server.openapi.apiKeyValue')}</label>
|
||||
<label className="block text-xs text-gray-600 mb-1">
|
||||
{t('server.openapi.apiKeyValue')}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={formData.openapi?.apiKeyValue || ''}
|
||||
onChange={(e) => setFormData(prev => ({
|
||||
...prev,
|
||||
openapi: { ...prev.openapi, apiKeyValue: e.target.value, url: prev.openapi?.url || '' }
|
||||
}))}
|
||||
onChange={(e) =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
openapi: {
|
||||
...prev.openapi,
|
||||
apiKeyValue: e.target.value,
|
||||
url: prev.openapi?.url || '',
|
||||
},
|
||||
}))
|
||||
}
|
||||
className="w-full border rounded px-2 py-1 text-sm focus:outline-none form-input"
|
||||
placeholder="your-api-key"
|
||||
/>
|
||||
@@ -536,16 +622,26 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
{/* HTTP Authentication Configuration */}
|
||||
{formData.openapi?.securityType === 'http' && (
|
||||
<div className="mb-4 p-4 border border-gray-200 rounded bg-gray-50">
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-3">{t('server.openapi.httpAuthConfig')}</h4>
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-3">
|
||||
{t('server.openapi.httpAuthConfig')}
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">{t('server.openapi.httpScheme')}</label>
|
||||
<label className="block text-xs text-gray-600 mb-1">
|
||||
{t('server.openapi.httpScheme')}
|
||||
</label>
|
||||
<select
|
||||
value={formData.openapi?.httpScheme || 'bearer'}
|
||||
onChange={(e) => setFormData(prev => ({
|
||||
...prev,
|
||||
openapi: { ...prev.openapi, httpScheme: e.target.value as any, url: prev.openapi?.url || '' }
|
||||
}))}
|
||||
onChange={(e) =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
openapi: {
|
||||
...prev.openapi,
|
||||
httpScheme: e.target.value as any,
|
||||
url: prev.openapi?.url || '',
|
||||
},
|
||||
}))
|
||||
}
|
||||
className="w-full border rounded px-2 py-1 text-sm focus:outline-none form-input"
|
||||
>
|
||||
<option value="basic">{t('server.openapi.httpSchemeBasic')}</option>
|
||||
@@ -554,16 +650,28 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">{t('server.openapi.httpCredentials')}</label>
|
||||
<label className="block text-xs text-gray-600 mb-1">
|
||||
{t('server.openapi.httpCredentials')}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={formData.openapi?.httpCredentials || ''}
|
||||
onChange={(e) => setFormData(prev => ({
|
||||
...prev,
|
||||
openapi: { ...prev.openapi, httpCredentials: e.target.value, url: prev.openapi?.url || '' }
|
||||
}))}
|
||||
onChange={(e) =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
openapi: {
|
||||
...prev.openapi,
|
||||
httpCredentials: e.target.value,
|
||||
url: prev.openapi?.url || '',
|
||||
},
|
||||
}))
|
||||
}
|
||||
className="w-full border rounded px-2 py-1 text-sm focus:outline-none form-input"
|
||||
placeholder={formData.openapi?.httpScheme === 'basic' ? 'base64-encoded-credentials' : 'bearer-token'}
|
||||
placeholder={
|
||||
formData.openapi?.httpScheme === 'basic'
|
||||
? 'base64-encoded-credentials'
|
||||
: 'bearer-token'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -573,17 +681,27 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
{/* OAuth2 Configuration */}
|
||||
{formData.openapi?.securityType === 'oauth2' && (
|
||||
<div className="mb-4 p-4 border border-gray-200 rounded bg-gray-50">
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-3">{t('server.openapi.oauth2Config')}</h4>
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-3">
|
||||
{t('server.openapi.oauth2Config')}
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">{t('server.openapi.oauth2Token')}</label>
|
||||
<label className="block text-xs text-gray-600 mb-1">
|
||||
{t('server.openapi.oauth2Token')}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={formData.openapi?.oauth2Token || ''}
|
||||
onChange={(e) => setFormData(prev => ({
|
||||
...prev,
|
||||
openapi: { ...prev.openapi, oauth2Token: e.target.value, url: prev.openapi?.url || '' }
|
||||
}))}
|
||||
onChange={(e) =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
openapi: {
|
||||
...prev.openapi,
|
||||
oauth2Token: e.target.value,
|
||||
url: prev.openapi?.url || '',
|
||||
},
|
||||
}))
|
||||
}
|
||||
className="w-full border rounded px-2 py-1 text-sm focus:outline-none form-input"
|
||||
placeholder="access-token"
|
||||
/>
|
||||
@@ -595,30 +713,48 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
{/* OpenID Connect Configuration */}
|
||||
{formData.openapi?.securityType === 'openIdConnect' && (
|
||||
<div className="mb-4 p-4 border border-gray-200 rounded bg-gray-50">
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-3">{t('server.openapi.openIdConnectConfig')}</h4>
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-3">
|
||||
{t('server.openapi.openIdConnectConfig')}
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">{t('server.openapi.openIdConnectUrl')}</label>
|
||||
<label className="block text-xs text-gray-600 mb-1">
|
||||
{t('server.openapi.openIdConnectUrl')}
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
value={formData.openapi?.openIdConnectUrl || ''}
|
||||
onChange={(e) => setFormData(prev => ({
|
||||
...prev,
|
||||
openapi: { ...prev.openapi, openIdConnectUrl: e.target.value, url: prev.openapi?.url || '' }
|
||||
}))}
|
||||
onChange={(e) =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
openapi: {
|
||||
...prev.openapi,
|
||||
openIdConnectUrl: e.target.value,
|
||||
url: prev.openapi?.url || '',
|
||||
},
|
||||
}))
|
||||
}
|
||||
className="w-full border rounded px-2 py-1 text-sm focus:outline-none form-input"
|
||||
placeholder="https://example.com/.well-known/openid_configuration"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">{t('server.openapi.openIdConnectToken')}</label>
|
||||
<label className="block text-xs text-gray-600 mb-1">
|
||||
{t('server.openapi.openIdConnectToken')}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={formData.openapi?.openIdConnectToken || ''}
|
||||
onChange={(e) => setFormData(prev => ({
|
||||
...prev,
|
||||
openapi: { ...prev.openapi, openIdConnectToken: e.target.value, url: prev.openapi?.url || '' }
|
||||
}))}
|
||||
onChange={(e) =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
openapi: {
|
||||
...prev.openapi,
|
||||
openIdConnectToken: e.target.value,
|
||||
url: prev.openapi?.url || '',
|
||||
},
|
||||
}))
|
||||
}
|
||||
className="w-full border rounded px-2 py-1 text-sm focus:outline-none form-input"
|
||||
placeholder="id-token"
|
||||
/>
|
||||
@@ -635,14 +771,22 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
<input
|
||||
type="text"
|
||||
value={formData.openapi?.passthroughHeaders || ''}
|
||||
onChange={(e) => setFormData(prev => ({
|
||||
...prev,
|
||||
openapi: { ...prev.openapi, passthroughHeaders: e.target.value, url: prev.openapi?.url || '' }
|
||||
}))}
|
||||
onChange={(e) =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
openapi: {
|
||||
...prev.openapi,
|
||||
passthroughHeaders: e.target.value,
|
||||
url: prev.openapi?.url || '',
|
||||
},
|
||||
}))
|
||||
}
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline form-input"
|
||||
placeholder="Authorization, X-API-Key, X-Custom-Header"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">{t('server.openapi.passthroughHeadersHelp')}</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
{t('server.openapi.passthroughHeadersHelp')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
@@ -701,7 +845,11 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
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 form-input"
|
||||
placeholder={serverType === 'streamable-http' ? "e.g.: http://localhost:3000/mcp" : "e.g.: http://localhost:3000/sse"}
|
||||
placeholder={
|
||||
serverType === 'streamable-http'
|
||||
? 'e.g.: http://localhost:3000/mcp'
|
||||
: 'e.g.: http://localhost:3000/sse'
|
||||
}
|
||||
required={serverType === 'sse' || serverType === 'streamable-http'}
|
||||
/>
|
||||
</div>
|
||||
@@ -837,23 +985,26 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
<label className="text-gray-700 text-sm font-bold">
|
||||
{t('server.requestOptions')}
|
||||
</label>
|
||||
<span className="text-gray-500 text-sm">
|
||||
{isRequestOptionsExpanded ? '▼' : '▶'}
|
||||
</span>
|
||||
<span className="text-gray-500 text-sm">{isRequestOptionsExpanded ? '▼' : '▶'}</span>
|
||||
</div>
|
||||
|
||||
{isRequestOptionsExpanded && (
|
||||
<div className="border border-gray-200 rounded-b p-4 bg-gray-50 border-t-0">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-gray-600 text-sm font-medium mb-1" htmlFor="timeout">
|
||||
<label
|
||||
className="block text-gray-600 text-sm font-medium mb-1"
|
||||
htmlFor="timeout"
|
||||
>
|
||||
{t('server.timeout')}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="timeout"
|
||||
value={formData.options?.timeout || 60000}
|
||||
onChange={(e) => handleOptionsChange('timeout', parseInt(e.target.value) || 60000)}
|
||||
onChange={(e) =>
|
||||
handleOptionsChange('timeout', parseInt(e.target.value) || 60000)
|
||||
}
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline form-input"
|
||||
placeholder="30000"
|
||||
min="1000"
|
||||
@@ -863,19 +1014,29 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-gray-600 text-sm font-medium mb-1" htmlFor="maxTotalTimeout">
|
||||
<label
|
||||
className="block text-gray-600 text-sm font-medium mb-1"
|
||||
htmlFor="maxTotalTimeout"
|
||||
>
|
||||
{t('server.maxTotalTimeout')}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="maxTotalTimeout"
|
||||
value={formData.options?.maxTotalTimeout || ''}
|
||||
onChange={(e) => handleOptionsChange('maxTotalTimeout', e.target.value ? parseInt(e.target.value) : undefined)}
|
||||
onChange={(e) =>
|
||||
handleOptionsChange(
|
||||
'maxTotalTimeout',
|
||||
e.target.value ? parseInt(e.target.value) : undefined,
|
||||
)
|
||||
}
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline form-input"
|
||||
placeholder="Optional"
|
||||
min="1000"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">{t('server.maxTotalTimeoutDescription')}</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
{t('server.maxTotalTimeoutDescription')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -884,10 +1045,14 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.options?.resetTimeoutOnProgress || false}
|
||||
onChange={(e) => handleOptionsChange('resetTimeoutOnProgress', e.target.checked)}
|
||||
onChange={(e) =>
|
||||
handleOptionsChange('resetTimeoutOnProgress', e.target.checked)
|
||||
}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-gray-600 text-sm">{t('server.resetTimeoutOnProgress')}</span>
|
||||
<span className="text-gray-600 text-sm">
|
||||
{t('server.resetTimeoutOnProgress')}
|
||||
</span>
|
||||
</label>
|
||||
<p className="text-xs text-gray-500 mt-1 ml-6">
|
||||
{t('server.resetTimeoutOnProgressDescription')}
|
||||
@@ -915,7 +1080,7 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default ServerForm
|
||||
export default ServerForm;
|
||||
|
||||
Reference in New Issue
Block a user