mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-04-23 14:01:32 +00:00
refactor(settings): move MCP server management into provider module
Extract MCP server settings out of the settings controller and agents tab into a dedicated frontend MCP module. The settings UI now delegates MCP rendering and behavior to a single module that only needs the selected provider and current projects. Changes: - Add `src/components/mcp` as the single frontend MCP module - Move MCP server list rendering into `McpServers` - Move MCP add/edit modal into `McpServerFormModal` - Move MCP API/state logic into `useMcpServers` - Move MCP form state/validation logic into `useMcpServerForm` - Add provider-specific MCP constants, types, and formatting helpers - Use the unified `/api/providers/:provider/mcp/servers` API for all providers - Support MCP management for Claude, Cursor, Codex, and Gemini - Remove old settings-owned Claude/Codex MCP modal components - Remove old provider-specific `McpServersContent` branching from settings - Strip MCP server state, fetch, save, delete, and modal ownership from `useSettingsController` - Simplify agents settings props so MCP only receives `selectedProvider` and `currentProjects` - Keep Claude working-directory unsupported while preserving cwd support for Cursor, Codex, and Gemini - Add progressive MCP loading: - render user/global scope first - load project/local scopes in the background - append project results as they resolve - cache MCP lists briefly to avoid slow tab-switch refetches - ignore stale async responses after provider switches Verification: - `npx eslint src/components/mcp` - `npm run typecheck` - `npm run build:client`
This commit is contained in:
231
src/components/mcp/view/McpServers.tsx
Normal file
231
src/components/mcp/view/McpServers.tsx
Normal file
@@ -0,0 +1,231 @@
|
||||
import { Edit3, ExternalLink, Globe, Lock, Plus, Server, Terminal, Trash2, Users, Zap } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { McpProject, McpProvider, McpScope, ProviderMcpServer } from '../types';
|
||||
import { IS_PLATFORM } from '../../../constants/config';
|
||||
import { Badge, Button } from '../../../shared/view/ui';
|
||||
import { MCP_PROVIDER_BUTTON_CLASSES, MCP_PROVIDER_NAMES } from '../constants';
|
||||
import { useMcpServers } from '../hooks/useMcpServers';
|
||||
import { maskSecret } from '../utils/mcpFormatting';
|
||||
|
||||
import McpServerFormModal from './modals/McpServerFormModal';
|
||||
|
||||
type McpServersProps = {
|
||||
selectedProvider: McpProvider;
|
||||
currentProjects: McpProject[];
|
||||
};
|
||||
|
||||
const getTransportIcon = (transport: string | undefined) => {
|
||||
if (transport === 'stdio') {
|
||||
return <Terminal className="h-4 w-4" />;
|
||||
}
|
||||
|
||||
if (transport === 'sse') {
|
||||
return <Zap className="h-4 w-4" />;
|
||||
}
|
||||
|
||||
if (transport === 'http') {
|
||||
return <Globe className="h-4 w-4" />;
|
||||
}
|
||||
|
||||
return <Server className="h-4 w-4" />;
|
||||
};
|
||||
|
||||
const getScopeLabel = (scope: McpScope): string => {
|
||||
if (scope === 'user') {
|
||||
return 'user';
|
||||
}
|
||||
|
||||
if (scope === 'local') {
|
||||
return 'local';
|
||||
}
|
||||
|
||||
return 'project';
|
||||
};
|
||||
|
||||
const getServerKey = (server: ProviderMcpServer): string => (
|
||||
`${server.provider}:${server.scope}:${server.workspacePath || 'global'}:${server.name}`
|
||||
);
|
||||
|
||||
function ConfigLine({ label, children }: { label: string; children: string }) {
|
||||
if (!children) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{label}:{' '}
|
||||
<code className="rounded bg-muted px-1 text-xs">{children}</code>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TeamMcpFeatureCard() {
|
||||
return (
|
||||
<div className="rounded-xl border border-dashed border-border/60 bg-muted/20 p-5">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-muted/60 text-muted-foreground">
|
||||
<Users className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="text-sm font-medium text-foreground">Team MCP Configs</h4>
|
||||
<Lock className="h-3 w-3 text-muted-foreground/60" />
|
||||
</div>
|
||||
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">
|
||||
Share MCP server configurations across your team. Everyone stays in sync automatically.
|
||||
</p>
|
||||
<a
|
||||
href="https://cloudcli.ai"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mt-3 inline-flex items-center gap-1 text-xs font-medium text-primary transition-colors hover:underline"
|
||||
>
|
||||
Available with CloudCLI Pro
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function McpServers({ selectedProvider, currentProjects }: McpServersProps) {
|
||||
const { t } = useTranslation('settings');
|
||||
const {
|
||||
servers,
|
||||
isLoading,
|
||||
isLoadingProjectScopes,
|
||||
loadError,
|
||||
deleteError,
|
||||
saveStatus,
|
||||
isFormOpen,
|
||||
editingServer,
|
||||
openForm,
|
||||
closeForm,
|
||||
submitForm,
|
||||
deleteServer,
|
||||
} = useMcpServers({ selectedProvider, currentProjects });
|
||||
|
||||
const providerName = MCP_PROVIDER_NAMES[selectedProvider];
|
||||
const description = t(`mcpServers.description.${selectedProvider}`, {
|
||||
defaultValue: `Model Context Protocol servers provide additional tools and data sources to ${providerName}`,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Server className="h-5 w-5 text-purple-500" />
|
||||
<h3 className="text-lg font-medium text-foreground">{t('mcpServers.title')}</h3>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<Button onClick={() => openForm()} className={MCP_PROVIDER_BUTTON_CLASSES[selectedProvider]} size="sm">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{t('mcpServers.addButton')}
|
||||
</Button>
|
||||
{saveStatus === 'success' && (
|
||||
<span className="animate-in fade-in text-xs text-muted-foreground">{t('saveStatus.success')}</span>
|
||||
)}
|
||||
{isLoadingProjectScopes && (
|
||||
<span className="animate-in fade-in text-xs text-muted-foreground">Refreshing project scopes...</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(loadError || deleteError) && (
|
||||
<div className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700 dark:border-red-800/60 dark:bg-red-900/20 dark:text-red-200">
|
||||
{deleteError || loadError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
{isLoading && servers.length === 0 && (
|
||||
<div className="py-8 text-center text-muted-foreground">Loading MCP servers...</div>
|
||||
)}
|
||||
|
||||
{servers.map((server) => (
|
||||
<div key={getServerKey(server)} className="rounded-lg border border-border bg-card/50 p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="mb-2 flex flex-wrap items-center gap-2">
|
||||
{getTransportIcon(server.transport)}
|
||||
<span className="font-medium text-foreground">{server.name}</span>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{server.transport || 'stdio'}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{getScopeLabel(server.scope)}
|
||||
</Badge>
|
||||
{server.projectDisplayName && (
|
||||
<Badge variant="outline" className="max-w-full truncate text-xs">
|
||||
{server.projectDisplayName}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 text-sm text-muted-foreground">
|
||||
<ConfigLine label={t('mcpServers.config.command')}>{server.command || ''}</ConfigLine>
|
||||
<ConfigLine label={t('mcpServers.config.url')}>{server.url || ''}</ConfigLine>
|
||||
<ConfigLine label={t('mcpServers.config.args')}>{(server.args || []).join(' ')}</ConfigLine>
|
||||
<ConfigLine label="Cwd">{server.cwd || ''}</ConfigLine>
|
||||
{server.env && Object.keys(server.env).length > 0 && (
|
||||
<ConfigLine label={t('mcpServers.config.environment')}>
|
||||
{Object.entries(server.env).map(([key, value]) => `${key}=${maskSecret(value)}`).join(', ')}
|
||||
</ConfigLine>
|
||||
)}
|
||||
{server.envVars && server.envVars.length > 0 && (
|
||||
<ConfigLine label="Env Vars">{server.envVars.join(', ')}</ConfigLine>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ml-4 flex items-center gap-2">
|
||||
<Button
|
||||
onClick={() => openForm(server)}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
title={t('mcpServers.actions.edit')}
|
||||
>
|
||||
<Edit3 className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => deleteServer(server)}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-red-600 hover:text-red-700"
|
||||
title={t('mcpServers.actions.delete')}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{!isLoading && !isLoadingProjectScopes && servers.length === 0 && (
|
||||
<div className="py-8 text-center text-muted-foreground">{t('mcpServers.empty')}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedProvider === 'codex' && (
|
||||
<div className="rounded-lg border border-border bg-muted/50 p-4">
|
||||
<h4 className="mb-2 font-medium text-foreground">{t('mcpServers.help.title')}</h4>
|
||||
<p className="text-sm text-muted-foreground">{t('mcpServers.help.description')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedProvider === 'claude' && !IS_PLATFORM && <TeamMcpFeatureCard />}
|
||||
|
||||
<McpServerFormModal
|
||||
provider={selectedProvider}
|
||||
isOpen={isFormOpen}
|
||||
editingServer={editingServer}
|
||||
currentProjects={currentProjects}
|
||||
onClose={closeForm}
|
||||
onSubmit={submitForm}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
394
src/components/mcp/view/modals/McpServerFormModal.tsx
Normal file
394
src/components/mcp/view/modals/McpServerFormModal.tsx
Normal file
@@ -0,0 +1,394 @@
|
||||
import { FolderOpen, Globe, X } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Button, Input } from '../../../../shared/view/ui';
|
||||
import {
|
||||
MCP_PROVIDER_NAMES,
|
||||
MCP_SUPPORTED_SCOPES,
|
||||
MCP_SUPPORTED_TRANSPORTS,
|
||||
MCP_SUPPORTS_WORKING_DIRECTORY,
|
||||
} from '../../constants';
|
||||
import { useMcpServerForm } from '../../hooks/useMcpServerForm';
|
||||
import { formatKeyValueLines, parseKeyValueLines, parseListLines } from '../../utils/mcpFormatting';
|
||||
import type { McpFormState, McpProject, McpProvider, McpScope, ProviderMcpServer } from '../../types';
|
||||
|
||||
type McpServerFormModalProps = {
|
||||
provider: McpProvider;
|
||||
isOpen: boolean;
|
||||
editingServer: ProviderMcpServer | null;
|
||||
currentProjects: McpProject[];
|
||||
onClose: () => void;
|
||||
onSubmit: (formData: McpFormState, editingServer: ProviderMcpServer | null) => Promise<void>;
|
||||
};
|
||||
|
||||
const getScopeLabel = (scope: McpScope): string => {
|
||||
if (scope === 'user') {
|
||||
return 'User (Global)';
|
||||
}
|
||||
|
||||
if (scope === 'local') {
|
||||
return 'Claude Local';
|
||||
}
|
||||
|
||||
return 'Project';
|
||||
};
|
||||
|
||||
const getScopeDescription = (scope: McpScope): string => {
|
||||
if (scope === 'user') {
|
||||
return 'Available across all projects on your machine';
|
||||
}
|
||||
|
||||
if (scope === 'local') {
|
||||
return 'Stored in Claude user settings for the selected project';
|
||||
}
|
||||
|
||||
return 'Stored in the selected project workspace';
|
||||
};
|
||||
|
||||
export default function McpServerFormModal({
|
||||
provider,
|
||||
isOpen,
|
||||
editingServer,
|
||||
currentProjects,
|
||||
onClose,
|
||||
onSubmit,
|
||||
}: McpServerFormModalProps) {
|
||||
const { t } = useTranslation('settings');
|
||||
const {
|
||||
formData,
|
||||
projectOptions,
|
||||
isEditing,
|
||||
isSubmitting,
|
||||
jsonValidationError,
|
||||
canSubmit,
|
||||
updateForm,
|
||||
updateScope,
|
||||
updateTransport,
|
||||
updateJsonInput,
|
||||
handleSubmit,
|
||||
} = useMcpServerForm({
|
||||
provider,
|
||||
isOpen,
|
||||
editingServer,
|
||||
currentProjects,
|
||||
onSubmit,
|
||||
});
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const providerName = MCP_PROVIDER_NAMES[provider];
|
||||
const showProjectSelector = formData.scope !== 'user';
|
||||
const supportsHttpHeaders = formData.transport === 'http' || formData.transport === 'sse';
|
||||
const supportsWorkingDirectory = MCP_SUPPORTS_WORKING_DIRECTORY[provider];
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[110] flex items-center justify-center bg-black/50 p-4">
|
||||
<div className="max-h-[90vh] w-full max-w-2xl overflow-y-auto rounded-lg border border-border bg-background">
|
||||
<div className="flex items-center justify-between border-b border-border p-4">
|
||||
<h3 className="text-lg font-medium text-foreground">
|
||||
{isEditing ? t('mcpForm.title.edit') : t('mcpForm.title.add')}
|
||||
</h3>
|
||||
<Button variant="ghost" size="sm" onClick={onClose}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4 p-4">
|
||||
{!isEditing && (
|
||||
<div className="mb-4 flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateForm('importMode', 'form')}
|
||||
className={`rounded-lg px-4 py-2 font-medium transition-colors ${
|
||||
formData.importMode === 'form'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
{t('mcpForm.importMode.form')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateForm('importMode', 'json')}
|
||||
className={`rounded-lg px-4 py-2 font-medium transition-colors ${
|
||||
formData.importMode === 'json'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
{t('mcpForm.importMode.json')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isEditing && (
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-900/50">
|
||||
<label className="mb-2 block text-sm font-medium text-foreground">
|
||||
{t('mcpForm.scope.label')}
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
{formData.scope === 'user' ? <Globe className="h-4 w-4" /> : <FolderOpen className="h-4 w-4" />}
|
||||
<span className="text-sm">{getScopeLabel(formData.scope)}</span>
|
||||
{formData.workspacePath && (
|
||||
<span className="truncate text-xs text-muted-foreground">- {formData.workspacePath}</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-muted-foreground">{t('mcpForm.scope.cannotChange')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isEditing && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-foreground">
|
||||
{t('mcpForm.scope.label')} *
|
||||
</label>
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
{MCP_SUPPORTED_SCOPES[provider].map((scope) => (
|
||||
<button
|
||||
key={scope}
|
||||
type="button"
|
||||
onClick={() => updateScope(scope)}
|
||||
className={`rounded-lg px-4 py-2 font-medium transition-colors ${
|
||||
formData.scope === scope
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
{scope === 'user' ? <Globe className="h-4 w-4" /> : <FolderOpen className="h-4 w-4" />}
|
||||
<span>{getScopeLabel(scope)}</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-muted-foreground">{getScopeDescription(formData.scope)}</p>
|
||||
</div>
|
||||
|
||||
{showProjectSelector && (
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-foreground">
|
||||
{t('mcpForm.fields.selectProject')} *
|
||||
</label>
|
||||
<select
|
||||
value={formData.workspacePath}
|
||||
onChange={(event) => updateForm('workspacePath', 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"
|
||||
required
|
||||
>
|
||||
<option value="">{t('mcpForm.fields.selectProject')}</option>
|
||||
{projectOptions.map((project) => (
|
||||
<option key={project.value} value={project.value}>
|
||||
{project.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{formData.workspacePath && (
|
||||
<p className="mt-1 truncate text-xs text-muted-foreground">
|
||||
{t('mcpForm.projectPath', { path: formData.workspacePath })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div className={formData.importMode === 'json' ? 'md:col-span-2' : ''}>
|
||||
<label className="mb-2 block text-sm font-medium text-foreground">
|
||||
{t('mcpForm.fields.serverName')} *
|
||||
</label>
|
||||
<Input
|
||||
value={formData.name}
|
||||
onChange={(event) => updateForm('name', event.target.value)}
|
||||
placeholder={t('mcpForm.placeholders.serverName')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{formData.importMode === 'form' && (
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-foreground">
|
||||
{t('mcpForm.fields.transportType')} *
|
||||
</label>
|
||||
<select
|
||||
value={formData.transport}
|
||||
onChange={(event) => updateTransport(event.target.value as McpFormState['transport'])}
|
||||
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"
|
||||
>
|
||||
{MCP_SUPPORTED_TRANSPORTS[provider].map((transport) => (
|
||||
<option key={transport} value={transport}>
|
||||
{transport === 'sse' ? 'SSE' : transport.toUpperCase()}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{formData.importMode === 'json' && (
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-foreground">
|
||||
{t('mcpForm.fields.jsonConfig')} *
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.jsonInput}
|
||||
onChange={(event) => updateJsonInput(event.target.value)}
|
||||
className={`w-full border px-3 py-2 ${
|
||||
jsonValidationError ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
|
||||
} rounded-lg bg-gray-50 font-mono text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-800 dark:text-gray-100`}
|
||||
rows={8}
|
||||
placeholder={'{\n "type": "stdio",\n "command": "npx",\n "args": ["@upstash/context7-mcp"]\n}'}
|
||||
required
|
||||
/>
|
||||
{jsonValidationError && (
|
||||
<p className="mt-1 text-xs text-red-500">{jsonValidationError}</p>
|
||||
)}
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
{t('mcpForm.validation.jsonHelp')}
|
||||
<br />
|
||||
- stdio: {`{"type":"stdio","command":"npx","args":["@upstash/context7-mcp"]}`}
|
||||
<br />
|
||||
- http/sse: {`{"type":"http","url":"https://api.example.com/mcp"}`}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{formData.importMode === 'form' && formData.transport === 'stdio' && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-foreground">
|
||||
{t('mcpForm.fields.command')} *
|
||||
</label>
|
||||
<Input
|
||||
value={formData.command}
|
||||
onChange={(event) => updateForm('command', event.target.value)}
|
||||
placeholder="npx @my-org/mcp-server"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-foreground">
|
||||
{t('mcpForm.fields.arguments')}
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.args.join('\n')}
|
||||
onChange={(event) => updateForm('args', parseListLines(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"
|
||||
rows={3}
|
||||
placeholder="--port 3000"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{supportsWorkingDirectory && (
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-foreground">
|
||||
Working Directory
|
||||
</label>
|
||||
<Input
|
||||
value={formData.cwd}
|
||||
onChange={(event) => updateForm('cwd', event.target.value)}
|
||||
placeholder="."
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{formData.importMode === 'form' && formData.transport !== 'stdio' && (
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-foreground">
|
||||
{t('mcpForm.fields.url')} *
|
||||
</label>
|
||||
<Input
|
||||
value={formData.url}
|
||||
onChange={(event) => updateForm('url', event.target.value)}
|
||||
placeholder="https://api.example.com/mcp"
|
||||
type="url"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{formData.importMode === 'form' && (
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-foreground">
|
||||
{t('mcpForm.fields.envVars')}
|
||||
</label>
|
||||
<textarea
|
||||
value={formatKeyValueLines(formData.env)}
|
||||
onChange={(event) => updateForm('env', parseKeyValueLines(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"
|
||||
rows={3}
|
||||
placeholder="API_KEY=your-key DEBUG=true"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{formData.importMode === 'form' && supportsHttpHeaders && (
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-foreground">
|
||||
{t('mcpForm.fields.headers')}
|
||||
</label>
|
||||
<textarea
|
||||
value={formatKeyValueLines(formData.headers)}
|
||||
onChange={(event) => updateForm('headers', parseKeyValueLines(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"
|
||||
rows={3}
|
||||
placeholder="Authorization=Bearer token X-API-Key=your-key"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{provider === 'codex' && formData.importMode === 'form' && formData.transport === 'stdio' && (
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-foreground">
|
||||
Environment Variable Names
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.envVars.join('\n')}
|
||||
onChange={(event) => updateForm('envVars', parseListLines(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"
|
||||
rows={3}
|
||||
placeholder="GITHUB_TOKEN API_KEY"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{provider === 'codex' && formData.importMode === 'form' && formData.transport === 'http' && (
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-foreground">
|
||||
Bearer Token Environment Variable
|
||||
</label>
|
||||
<Input
|
||||
value={formData.bearerTokenEnvVar}
|
||||
onChange={(event) => updateForm('bearerTokenEnvVar', event.target.value)}
|
||||
placeholder="MCP_TOKEN"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
<Button type="button" variant="outline" onClick={onClose}>
|
||||
{t('mcpForm.actions.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting || !canSubmit}
|
||||
className="bg-purple-600 text-white hover:bg-purple-700 disabled:opacity-50"
|
||||
>
|
||||
{isSubmitting
|
||||
? t('mcpForm.actions.saving')
|
||||
: isEditing
|
||||
? t('mcpForm.actions.updateServer')
|
||||
: `${t('mcpForm.actions.addServer')} to ${providerName}`}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user