mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-07-05 21:13:00 +08:00
fix(mcp): form with multiline text handling for args, env, headers, and envVars
This commit is contained in:
@@ -3,7 +3,14 @@ import { useTranslation } from 'react-i18next';
|
|||||||
|
|
||||||
import { DEFAULT_MCP_FORM, MCP_SUPPORTED_SCOPES, MCP_SUPPORTED_TRANSPORTS } from '../constants';
|
import { DEFAULT_MCP_FORM, MCP_SUPPORTED_SCOPES, MCP_SUPPORTED_TRANSPORTS } from '../constants';
|
||||||
import type { McpFormState, McpProject, McpProvider, McpScope, McpTransport, ProviderMcpServer } from '../types';
|
import type { McpFormState, McpProject, McpProvider, McpScope, McpTransport, ProviderMcpServer } from '../types';
|
||||||
import { getErrorMessage, getProjectPath, isMcpTransport } from '../utils/mcpFormatting';
|
import {
|
||||||
|
formatKeyValueLines,
|
||||||
|
getErrorMessage,
|
||||||
|
getProjectPath,
|
||||||
|
isMcpTransport,
|
||||||
|
parseKeyValueLines,
|
||||||
|
parseListLines,
|
||||||
|
} from '../utils/mcpFormatting';
|
||||||
|
|
||||||
type UseMcpServerFormArgs = {
|
type UseMcpServerFormArgs = {
|
||||||
provider: McpProvider;
|
provider: McpProvider;
|
||||||
@@ -13,6 +20,14 @@ type UseMcpServerFormArgs = {
|
|||||||
onSubmit: (formData: McpFormState, editingServer: ProviderMcpServer | null) => Promise<void>;
|
onSubmit: (formData: McpFormState, editingServer: ProviderMcpServer | null) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type MultilineFieldText = {
|
||||||
|
args: string;
|
||||||
|
env: string;
|
||||||
|
headers: string;
|
||||||
|
envVars: string;
|
||||||
|
envHttpHeaders: string;
|
||||||
|
};
|
||||||
|
|
||||||
const cloneDefaultForm = (provider: McpProvider): McpFormState => ({
|
const cloneDefaultForm = (provider: McpProvider): McpFormState => ({
|
||||||
...DEFAULT_MCP_FORM,
|
...DEFAULT_MCP_FORM,
|
||||||
scope: MCP_SUPPORTED_SCOPES[provider][0],
|
scope: MCP_SUPPORTED_SCOPES[provider][0],
|
||||||
@@ -44,6 +59,14 @@ const createFormStateFromServer = (
|
|||||||
envHttpHeaders: server.envHttpHeaders || {},
|
envHttpHeaders: server.envHttpHeaders || {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const createMultilineTextFromForm = (formData: McpFormState): MultilineFieldText => ({
|
||||||
|
args: formData.args.join('\n'),
|
||||||
|
env: formatKeyValueLines(formData.env),
|
||||||
|
headers: formatKeyValueLines(formData.headers),
|
||||||
|
envVars: formData.envVars.join('\n'),
|
||||||
|
envHttpHeaders: formatKeyValueLines(formData.envHttpHeaders),
|
||||||
|
});
|
||||||
|
|
||||||
const normalizeScope = (provider: McpProvider, value: McpScope): McpScope => (
|
const normalizeScope = (provider: McpProvider, value: McpScope): McpScope => (
|
||||||
MCP_SUPPORTED_SCOPES[provider].includes(value) ? value : MCP_SUPPORTED_SCOPES[provider][0]
|
MCP_SUPPORTED_SCOPES[provider].includes(value) ? value : MCP_SUPPORTED_SCOPES[provider][0]
|
||||||
);
|
);
|
||||||
@@ -61,6 +84,9 @@ export function useMcpServerForm({
|
|||||||
}: UseMcpServerFormArgs) {
|
}: UseMcpServerFormArgs) {
|
||||||
const { t } = useTranslation('settings');
|
const { t } = useTranslation('settings');
|
||||||
const [formData, setFormData] = useState<McpFormState>(() => cloneDefaultForm(provider));
|
const [formData, setFormData] = useState<McpFormState>(() => cloneDefaultForm(provider));
|
||||||
|
const [multilineText, setMultilineText] = useState<MultilineFieldText>(() => (
|
||||||
|
createMultilineTextFromForm(cloneDefaultForm(provider))
|
||||||
|
));
|
||||||
const [jsonValidationError, setJsonValidationError] = useState('');
|
const [jsonValidationError, setJsonValidationError] = useState('');
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
@@ -73,11 +99,15 @@ export function useMcpServerForm({
|
|||||||
|
|
||||||
setJsonValidationError('');
|
setJsonValidationError('');
|
||||||
if (editingServer) {
|
if (editingServer) {
|
||||||
setFormData(createFormStateFromServer(provider, editingServer));
|
const nextFormData = createFormStateFromServer(provider, editingServer);
|
||||||
|
setFormData(nextFormData);
|
||||||
|
setMultilineText(createMultilineTextFromForm(nextFormData));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setFormData(cloneDefaultForm(provider));
|
const nextFormData = cloneDefaultForm(provider);
|
||||||
|
setFormData(nextFormData);
|
||||||
|
setMultilineText(createMultilineTextFromForm(nextFormData));
|
||||||
}, [editingServer, isOpen, provider]);
|
}, [editingServer, isOpen, provider]);
|
||||||
|
|
||||||
const projectOptions = useMemo(() => (
|
const projectOptions = useMemo(() => (
|
||||||
@@ -135,6 +165,19 @@ export function useMcpServerForm({
|
|||||||
validateJsonInput(value);
|
validateJsonInput(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateMultilineText = <K extends keyof MultilineFieldText>(key: K, value: MultilineFieldText[K]) => {
|
||||||
|
setMultilineText((prev) => ({ ...prev, [key]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const createSubmitFormData = (): McpFormState => ({
|
||||||
|
...formData,
|
||||||
|
args: parseListLines(multilineText.args),
|
||||||
|
env: parseKeyValueLines(multilineText.env),
|
||||||
|
headers: parseKeyValueLines(multilineText.headers),
|
||||||
|
envVars: parseListLines(multilineText.envVars),
|
||||||
|
envHttpHeaders: parseKeyValueLines(multilineText.envHttpHeaders),
|
||||||
|
});
|
||||||
|
|
||||||
const canSubmit = useMemo(() => {
|
const canSubmit = useMemo(() => {
|
||||||
if (!formData.name.trim()) {
|
if (!formData.name.trim()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -160,7 +203,9 @@ export function useMcpServerForm({
|
|||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await onSubmit(formData, editingServer);
|
// Textareas keep raw strings while editing so users can create blank
|
||||||
|
// lines or partial KEY=value entries without the form rewriting them.
|
||||||
|
await onSubmit(createSubmitFormData(), editingServer);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(`Error: ${getErrorMessage(error)}`);
|
alert(`Error: ${getErrorMessage(error)}`);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -171,6 +216,7 @@ export function useMcpServerForm({
|
|||||||
return {
|
return {
|
||||||
formData,
|
formData,
|
||||||
setFormData,
|
setFormData,
|
||||||
|
multilineText,
|
||||||
projectOptions,
|
projectOptions,
|
||||||
isEditing,
|
isEditing,
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
@@ -180,6 +226,7 @@ export function useMcpServerForm({
|
|||||||
updateScope,
|
updateScope,
|
||||||
updateTransport,
|
updateTransport,
|
||||||
updateJsonInput,
|
updateJsonInput,
|
||||||
|
updateMultilineText,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
MCP_SUPPORTS_WORKING_DIRECTORY,
|
MCP_SUPPORTS_WORKING_DIRECTORY,
|
||||||
} from '../../constants';
|
} from '../../constants';
|
||||||
import { useMcpServerForm } from '../../hooks/useMcpServerForm';
|
import { useMcpServerForm } from '../../hooks/useMcpServerForm';
|
||||||
import { formatKeyValueLines, parseKeyValueLines, parseListLines } from '../../utils/mcpFormatting';
|
|
||||||
import type { McpFormState, McpProject, McpProvider, McpScope, ProviderMcpServer } from '../../types';
|
import type { McpFormState, McpProject, McpProvider, McpScope, ProviderMcpServer } from '../../types';
|
||||||
|
|
||||||
type McpServerFormModalProps = {
|
type McpServerFormModalProps = {
|
||||||
@@ -56,6 +55,7 @@ export default function McpServerFormModal({
|
|||||||
const { t } = useTranslation('settings');
|
const { t } = useTranslation('settings');
|
||||||
const {
|
const {
|
||||||
formData,
|
formData,
|
||||||
|
multilineText,
|
||||||
projectOptions,
|
projectOptions,
|
||||||
isEditing,
|
isEditing,
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
@@ -65,6 +65,7 @@ export default function McpServerFormModal({
|
|||||||
updateScope,
|
updateScope,
|
||||||
updateTransport,
|
updateTransport,
|
||||||
updateJsonInput,
|
updateJsonInput,
|
||||||
|
updateMultilineText,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
} = useMcpServerForm({
|
} = useMcpServerForm({
|
||||||
provider,
|
provider,
|
||||||
@@ -275,8 +276,8 @@ export default function McpServerFormModal({
|
|||||||
{t('mcpForm.fields.arguments')}
|
{t('mcpForm.fields.arguments')}
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={formData.args.join('\n')}
|
value={multilineText.args}
|
||||||
onChange={(event) => updateForm('args', parseListLines(event.target.value))}
|
onChange={(event) => updateMultilineText('args', event.target.value)}
|
||||||
className="w-full rounded-lg border border-gray-300 bg-gray-50 px-3 py-2 text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
|
className="w-full rounded-lg border border-gray-300 bg-gray-50 px-3 py-2 text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
|
||||||
rows={3}
|
rows={3}
|
||||||
placeholder="--port 3000"
|
placeholder="--port 3000"
|
||||||
@@ -285,14 +286,14 @@ export default function McpServerFormModal({
|
|||||||
|
|
||||||
{supportsWorkingDirectory && (
|
{supportsWorkingDirectory && (
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-2 block text-sm font-medium text-foreground">
|
<label className="mb-2 block text-sm font-medium text-foreground">
|
||||||
Working Directory
|
Working Directory
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
value={formData.cwd}
|
value={formData.cwd}
|
||||||
onChange={(event) => updateForm('cwd', event.target.value)}
|
onChange={(event) => updateForm('cwd', event.target.value)}
|
||||||
placeholder="."
|
placeholder="."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -319,8 +320,8 @@ export default function McpServerFormModal({
|
|||||||
{t('mcpForm.fields.envVars')}
|
{t('mcpForm.fields.envVars')}
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={formatKeyValueLines(formData.env)}
|
value={multilineText.env}
|
||||||
onChange={(event) => updateForm('env', parseKeyValueLines(event.target.value))}
|
onChange={(event) => updateMultilineText('env', event.target.value)}
|
||||||
className="w-full rounded-lg border border-gray-300 bg-gray-50 px-3 py-2 text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
|
className="w-full rounded-lg border border-gray-300 bg-gray-50 px-3 py-2 text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
|
||||||
rows={3}
|
rows={3}
|
||||||
placeholder="API_KEY=your-key DEBUG=true"
|
placeholder="API_KEY=your-key DEBUG=true"
|
||||||
@@ -334,8 +335,8 @@ export default function McpServerFormModal({
|
|||||||
{t('mcpForm.fields.headers')}
|
{t('mcpForm.fields.headers')}
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={formatKeyValueLines(formData.headers)}
|
value={multilineText.headers}
|
||||||
onChange={(event) => updateForm('headers', parseKeyValueLines(event.target.value))}
|
onChange={(event) => updateMultilineText('headers', event.target.value)}
|
||||||
className="w-full rounded-lg border border-gray-300 bg-gray-50 px-3 py-2 text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
|
className="w-full rounded-lg border border-gray-300 bg-gray-50 px-3 py-2 text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
|
||||||
rows={3}
|
rows={3}
|
||||||
placeholder="Authorization=Bearer token X-API-Key=your-key"
|
placeholder="Authorization=Bearer token X-API-Key=your-key"
|
||||||
@@ -349,8 +350,8 @@ export default function McpServerFormModal({
|
|||||||
Environment Variable Names
|
Environment Variable Names
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={formData.envVars.join('\n')}
|
value={multilineText.envVars}
|
||||||
onChange={(event) => updateForm('envVars', parseListLines(event.target.value))}
|
onChange={(event) => updateMultilineText('envVars', event.target.value)}
|
||||||
className="w-full rounded-lg border border-gray-300 bg-gray-50 px-3 py-2 text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
|
className="w-full rounded-lg border border-gray-300 bg-gray-50 px-3 py-2 text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
|
||||||
rows={3}
|
rows={3}
|
||||||
placeholder="GITHUB_TOKEN API_KEY"
|
placeholder="GITHUB_TOKEN API_KEY"
|
||||||
|
|||||||
Reference in New Issue
Block a user