fix(mcp): form with multiline text handling for args, env, headers, and envVars

This commit is contained in:
Haileyesus
2026-04-16 22:29:34 +03:00
parent 358f47d020
commit 5143a92021
2 changed files with 69 additions and 21 deletions

View File

@@ -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,
}; };
} }

View File

@@ -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&#10;3000" placeholder="--port&#10;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&#10;DEBUG=true" placeholder="API_KEY=your-key&#10;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&#10;X-API-Key=your-key" placeholder="Authorization=Bearer token&#10;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&#10;API_KEY" placeholder="GITHUB_TOKEN&#10;API_KEY"