mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-04-19 03:51:31 +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:
@@ -1,6 +1,7 @@
|
||||
import type { AgentCategoryContentSectionProps } from '../types';
|
||||
import { McpServers } from '../../../../../mcp';
|
||||
|
||||
import AccountContent from './content/AccountContent';
|
||||
import McpServersContent from './content/McpServersContent';
|
||||
import PermissionsContent from './content/PermissionsContent';
|
||||
|
||||
export default function AgentCategoryContentSection({
|
||||
@@ -13,18 +14,8 @@ export default function AgentCategoryContentSection({
|
||||
onCursorPermissionsChange,
|
||||
codexPermissionMode,
|
||||
onCodexPermissionModeChange,
|
||||
mcpServers,
|
||||
cursorMcpServers,
|
||||
codexMcpServers,
|
||||
deleteError,
|
||||
onOpenMcpForm,
|
||||
onDeleteMcpServer,
|
||||
onOpenCodexMcpForm,
|
||||
onDeleteCodexMcpServer,
|
||||
projects,
|
||||
}: AgentCategoryContentSectionProps) {
|
||||
// Cursor MCP add/edit/delete was previously a placeholder and is intentionally preserved.
|
||||
const noopCursorMcpAction = () => {};
|
||||
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto p-3 md:p-4">
|
||||
{selectedCategory === 'account' && (
|
||||
@@ -79,35 +70,10 @@ export default function AgentCategoryContentSection({
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedCategory === 'mcp' && selectedAgent === 'claude' && (
|
||||
<McpServersContent
|
||||
agent="claude"
|
||||
servers={mcpServers}
|
||||
onAdd={() => onOpenMcpForm()}
|
||||
onEdit={(server) => onOpenMcpForm(server)}
|
||||
onDelete={onDeleteMcpServer}
|
||||
deleteError={deleteError}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedCategory === 'mcp' && selectedAgent === 'cursor' && (
|
||||
<McpServersContent
|
||||
agent="cursor"
|
||||
servers={cursorMcpServers}
|
||||
onAdd={noopCursorMcpAction}
|
||||
onEdit={noopCursorMcpAction}
|
||||
onDelete={noopCursorMcpAction}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedCategory === 'mcp' && selectedAgent === 'codex' && (
|
||||
<McpServersContent
|
||||
agent="codex"
|
||||
servers={codexMcpServers}
|
||||
onAdd={() => onOpenCodexMcpForm()}
|
||||
onEdit={(server) => onOpenCodexMcpForm(server)}
|
||||
onDelete={(serverId) => onDeleteCodexMcpServer(serverId)}
|
||||
deleteError={deleteError}
|
||||
{selectedCategory === 'mcp' && (
|
||||
<McpServers
|
||||
selectedProvider={selectedAgent}
|
||||
currentProjects={projects}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,352 +0,0 @@
|
||||
import { Edit3, Globe, Plus, Server, Terminal, Trash2, Users, Zap } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Badge, Button } from '../../../../../../../shared/view/ui';
|
||||
import { IS_PLATFORM } from '../../../../../../../constants/config';
|
||||
import PremiumFeatureCard from '../../../../PremiumFeatureCard';
|
||||
import type { McpServer } from '../../../../../types/types';
|
||||
|
||||
const getTransportIcon = (type: string | undefined) => {
|
||||
if (type === 'stdio') {
|
||||
return <Terminal className="h-4 w-4" />;
|
||||
}
|
||||
|
||||
if (type === 'sse') {
|
||||
return <Zap className="h-4 w-4" />;
|
||||
}
|
||||
|
||||
if (type === 'http') {
|
||||
return <Globe className="h-4 w-4" />;
|
||||
}
|
||||
|
||||
return <Server className="h-4 w-4" />;
|
||||
};
|
||||
|
||||
const maskSecret = (value: unknown): string => {
|
||||
const normalizedValue = String(value ?? '');
|
||||
if (normalizedValue.length <= 4) {
|
||||
return '****';
|
||||
}
|
||||
|
||||
return `${normalizedValue.slice(0, 2)}****${normalizedValue.slice(-2)}`;
|
||||
};
|
||||
|
||||
type ClaudeMcpServersProps = {
|
||||
agent: 'claude';
|
||||
servers: McpServer[];
|
||||
onAdd: () => void;
|
||||
onEdit: (server: McpServer) => void;
|
||||
onDelete: (serverId: string, scope?: string) => void;
|
||||
deleteError?: string | null;
|
||||
};
|
||||
|
||||
function ClaudeMcpServers({
|
||||
servers,
|
||||
onAdd,
|
||||
onEdit,
|
||||
onDelete,
|
||||
deleteError,
|
||||
}: Omit<ClaudeMcpServersProps, 'agent'>) {
|
||||
const { t } = useTranslation('settings');
|
||||
|
||||
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">{t('mcpServers.description.claude')}</p>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Button onClick={onAdd} className="bg-purple-600 text-white hover:bg-purple-700" size="sm">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{t('mcpServers.addButton')}
|
||||
</Button>
|
||||
</div>
|
||||
{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}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
{servers.map((server) => {
|
||||
const serverId = server.id || server.name;
|
||||
|
||||
return (
|
||||
<div key={serverId} className="rounded-lg border border-border bg-card/50 p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
{getTransportIcon(server.type)}
|
||||
<span className="font-medium text-foreground">{server.name}</span>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{server.type || 'stdio'}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{server.scope === 'local'
|
||||
? t('mcpServers.scope.local')
|
||||
: server.scope === 'user'
|
||||
? t('mcpServers.scope.user')
|
||||
: server.scope}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 text-sm text-muted-foreground">
|
||||
{server.type === 'stdio' && server.config?.command && (
|
||||
<div>
|
||||
{t('mcpServers.config.command')}:{' '}
|
||||
<code className="rounded bg-muted px-1 text-xs">{server.config.command}</code>
|
||||
</div>
|
||||
)}
|
||||
{(server.type === 'sse' || server.type === 'http') && server.config?.url && (
|
||||
<div>
|
||||
{t('mcpServers.config.url')}:{' '}
|
||||
<code className="rounded bg-muted px-1 text-xs">{server.config.url}</code>
|
||||
</div>
|
||||
)}
|
||||
{server.config?.args && server.config.args.length > 0 && (
|
||||
<div>
|
||||
{t('mcpServers.config.args')}:{' '}
|
||||
<code className="rounded bg-muted px-1 text-xs">{server.config.args.join(' ')}</code>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="ml-4 flex items-center gap-2">
|
||||
<Button
|
||||
onClick={() => onEdit(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={() => onDelete(serverId, server.scope)}
|
||||
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>
|
||||
);
|
||||
})}
|
||||
{servers.length === 0 && (
|
||||
<div className="py-8 text-center text-muted-foreground">{t('mcpServers.empty')}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!IS_PLATFORM && (
|
||||
<PremiumFeatureCard
|
||||
icon={<Users className="h-5 w-5" />}
|
||||
title="Team MCP Configs"
|
||||
description="Share MCP server configurations across your team. Everyone stays in sync automatically."
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type CursorMcpServersProps = {
|
||||
agent: 'cursor';
|
||||
servers: McpServer[];
|
||||
onAdd: () => void;
|
||||
onEdit: (server: McpServer) => void;
|
||||
onDelete: (serverId: string) => void;
|
||||
};
|
||||
|
||||
function CursorMcpServers({ servers, onAdd, onEdit, onDelete }: Omit<CursorMcpServersProps, 'agent'>) {
|
||||
const { t } = useTranslation('settings');
|
||||
|
||||
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">{t('mcpServers.description.cursor')}</p>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Button onClick={onAdd} className="bg-purple-600 text-white hover:bg-purple-700" size="sm">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{t('mcpServers.addButton')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{servers.map((server) => {
|
||||
const serverId = server.id || server.name;
|
||||
|
||||
return (
|
||||
<div key={serverId} className="rounded-lg border border-border bg-card/50 p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<Terminal className="h-4 w-4" />
|
||||
<span className="font-medium text-foreground">{server.name}</span>
|
||||
<Badge variant="outline" className="text-xs">stdio</Badge>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{server.config?.command && (
|
||||
<div>
|
||||
{t('mcpServers.config.command')}:{' '}
|
||||
<code className="rounded bg-muted px-1 text-xs">{server.config.command}</code>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-4 flex items-center gap-2">
|
||||
<Button
|
||||
onClick={() => onEdit(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={() => onDelete(serverId)}
|
||||
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>
|
||||
);
|
||||
})}
|
||||
{servers.length === 0 && (
|
||||
<div className="py-8 text-center text-muted-foreground">{t('mcpServers.empty')}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type CodexMcpServersProps = {
|
||||
agent: 'codex';
|
||||
servers: McpServer[];
|
||||
onAdd: () => void;
|
||||
onEdit: (server: McpServer) => void;
|
||||
onDelete: (serverId: string) => void;
|
||||
deleteError?: string | null;
|
||||
};
|
||||
|
||||
function CodexMcpServers({ servers, onAdd, onEdit, onDelete, deleteError }: Omit<CodexMcpServersProps, 'agent'>) {
|
||||
const { t } = useTranslation('settings');
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Server className="h-5 w-5 text-muted-foreground" />
|
||||
<h3 className="text-lg font-medium text-foreground">{t('mcpServers.title')}</h3>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{t('mcpServers.description.codex')}</p>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Button onClick={onAdd} className="bg-gray-800 text-white hover:bg-gray-900 dark:bg-gray-700 dark:hover:bg-gray-600" size="sm">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{t('mcpServers.addButton')}
|
||||
</Button>
|
||||
</div>
|
||||
{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}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
{servers.map((server) => (
|
||||
<div key={server.name} className="rounded-lg border border-border bg-card/50 p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<Terminal className="h-4 w-4" />
|
||||
<span className="font-medium text-foreground">{server.name}</span>
|
||||
<Badge variant="outline" className="text-xs">stdio</Badge>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 text-sm text-muted-foreground">
|
||||
{server.config?.command && (
|
||||
<div>
|
||||
{t('mcpServers.config.command')}:{' '}
|
||||
<code className="rounded bg-muted px-1 text-xs">{server.config.command}</code>
|
||||
</div>
|
||||
)}
|
||||
{server.config?.args && server.config.args.length > 0 && (
|
||||
<div>
|
||||
{t('mcpServers.config.args')}:{' '}
|
||||
<code className="rounded bg-muted px-1 text-xs">{server.config.args.join(' ')}</code>
|
||||
</div>
|
||||
)}
|
||||
{server.config?.env && Object.keys(server.config.env).length > 0 && (
|
||||
<div>
|
||||
{t('mcpServers.config.environment')}:{' '}
|
||||
<code className="rounded bg-muted px-1 text-xs">
|
||||
{Object.entries(server.config.env).map(([key, value]) => `${key}=${maskSecret(value)}`).join(', ')}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ml-4 flex items-center gap-2">
|
||||
<Button
|
||||
onClick={() => onEdit(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={() => onDelete(server.name)}
|
||||
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>
|
||||
))}
|
||||
{servers.length === 0 && (
|
||||
<div className="py-8 text-center text-muted-foreground">{t('mcpServers.empty')}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type McpServersContentProps = ClaudeMcpServersProps | CursorMcpServersProps | CodexMcpServersProps;
|
||||
|
||||
export default function McpServersContent(props: McpServersContentProps) {
|
||||
if (props.agent === 'claude') {
|
||||
return <ClaudeMcpServers {...props} />;
|
||||
}
|
||||
|
||||
if (props.agent === 'cursor') {
|
||||
return <CursorMcpServers {...props} />;
|
||||
}
|
||||
|
||||
return <CodexMcpServers {...props} />;
|
||||
}
|
||||
Reference in New Issue
Block a user