This commit is contained in:
Junyan Qin
2025-11-04 17:09:28 +08:00
parent 0666778fea
commit 3ee7736361
5 changed files with 253 additions and 442 deletions
@@ -1,4 +1,4 @@
import { MCPServer, MCPServerConfig } from '@/app/infra/entities/api'; import { MCPServer } from '@/app/infra/entities/api';
export class MCPCardVO { export class MCPCardVO {
name: string; name: string;
@@ -7,20 +7,24 @@ export class MCPCardVO {
status: 'connected' | 'disconnected' | 'error' | 'disabled'; status: 'connected' | 'disconnected' | 'error' | 'disabled';
tools: number; tools: number;
error?: string; error?: string;
config: MCPServerConfig;
constructor(data: MCPServer) { constructor(data: MCPServer) {
this.name = data.name; this.name = data.name;
this.mode = data.mode; this.mode = data.mode;
this.enable = data.enable; this.enable = data.enable;
this.status = // Determine status from runtime_info
(data.status as string) === 'enabled' ? 'connected' : data.status; if (!data.runtime_info) {
this.tools = Array.isArray(data.tools) this.status = 'disconnected';
? data.tools.length this.tools = 0;
: data.tools || 0; } else if (data.runtime_info.connected) {
this.error = data.error; this.status = 'connected';
this.config = data.config; this.tools = data.runtime_info.tool_count || 0;
} else {
this.status = 'error';
this.tools = 0;
this.error = data.runtime_info.error_message;
}
} }
getStatusColor(): string { getStatusColor(): string {
@@ -9,41 +9,24 @@ import { httpClient } from '@/app/infra/http/HttpClient';
export default function MCPComponent({ export default function MCPComponent({
onEditServer, onEditServer,
toolsCountCache = {},
}: { }: {
askInstallServer?: (githubURL: string) => void; askInstallServer?: (githubURL: string) => void;
onEditServer?: (serverName: string) => void; onEditServer?: (serverName: string) => void;
toolsCountCache?: Record<string, number>;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [installedServers, setInstalledServers] = useState<MCPCardVO[]>([]); const [installedServers, setInstalledServers] = useState<MCPCardVO[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
useEffect(() => { useEffect(() => {
initData(); fetchInstalledServers();
}, []); }, []);
useEffect(() => {
fetchInstalledServers();
}, [toolsCountCache]);
function initData() {
fetchInstalledServers();
}
function fetchInstalledServers() { function fetchInstalledServers() {
setLoading(true); setLoading(true);
httpClient httpClient
.getMCPServers() .getMCPServers()
.then((resp) => { .then((resp) => {
const servers = resp.servers.map((server) => { const servers = resp.servers.map((server) => new MCPCardVO(server));
const vo = new MCPCardVO(server);
if (toolsCountCache[server.name] !== undefined) {
vo.tools = toolsCountCache[server.name];
}
return vo;
});
setInstalledServers(servers); setInstalledServers(servers);
setLoading(false); setLoading(false);
}) })
@@ -1,12 +1,11 @@
import { MCPCardVO } from '@/app/home/plugins/mcp-server/MCPCardVO'; import { MCPCardVO } from '@/app/home/plugins/mcp-server/MCPCardVO';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { httpClient } from '@/app/infra/http/HttpClient'; import { httpClient } from '@/app/infra/http/HttpClient';
import { Badge } from '@/components/ui/badge';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { RefreshCcw, Wrench } from 'lucide-react'; import { RefreshCcw, Wrench, Ban, AlertCircle } from 'lucide-react';
export default function MCPCardComponent({ export default function MCPCardComponent({
cardVO, cardVO,
@@ -23,19 +22,12 @@ export default function MCPCardComponent({
const [testing, setTesting] = useState(false); const [testing, setTesting] = useState(false);
const [toolsCount, setToolsCount] = useState(cardVO.tools); const [toolsCount, setToolsCount] = useState(cardVO.tools);
const [status, setStatus] = useState(cardVO.status); const [status, setStatus] = useState(cardVO.status);
const [error, setError] = useState(cardVO.error);
useEffect(() => { useEffect(() => {
console.log(`[MCPCard ${cardVO.name}] Status updated:`, {
status: cardVO.status,
tools: cardVO.tools,
error: cardVO.error,
});
setStatus(cardVO.status); setStatus(cardVO.status);
setError(cardVO.error);
setToolsCount(cardVO.tools); setToolsCount(cardVO.tools);
setEnabled(cardVO.enable); setEnabled(cardVO.enable);
}, [cardVO.name, cardVO.status, cardVO.error, cardVO.tools, cardVO.enable]); }, [cardVO.status, cardVO.tools, cardVO.enable]);
function handleEnable(checked: boolean) { function handleEnable(checked: boolean) {
setSwitchEnable(false); setSwitchEnable(false);
@@ -54,65 +46,28 @@ export default function MCPCardComponent({
} }
function handleTest(e: React.MouseEvent) { function handleTest(e: React.MouseEvent) {
e.stopPropagation(); // 阻止事件冒泡 e.stopPropagation();
setTesting(true); setTesting(true);
httpClient httpClient
.testMCPServer(cardVO.name) .testMCPServer(cardVO.name)
.then((resp) => { .then((resp) => {
const taskId = resp.task_id; const taskId = resp.task_id;
// 监控任务状态
const interval = setInterval(() => { const interval = setInterval(() => {
httpClient.getAsyncTask(taskId).then((taskResp) => { httpClient.getAsyncTask(taskId).then((taskResp) => {
if (taskResp.runtime.done) { if (taskResp.runtime.done) {
clearInterval(interval); clearInterval(interval);
setTesting(false);
if (taskResp.runtime.exception) { if (taskResp.runtime.exception) {
toast.error(t('mcp.testFailed') + taskResp.runtime.exception); toast.error(t('mcp.testFailed') + taskResp.runtime.exception);
} else { } else {
// 解析测试结果获取工具数量 toast.success(t('mcp.testSuccess'));
try {
const rawResult = taskResp.runtime.result as
| string
| {
status?: string;
tools_count?: number;
tools_names_lists?: string[];
error?: string;
}
| undefined;
if (rawResult) {
let result: {
status?: string;
tools_count?: number;
tools_names_lists?: string[];
error?: string;
};
if (typeof rawResult === 'string') {
result = JSON.parse(rawResult.replace(/'/g, '"'));
} else {
result = rawResult;
}
if (result.tools_count !== undefined) {
setToolsCount(result.tools_count);
toast.success(
t('mcp.testSuccess') +
` - ${result.tools_count} ${t('mcp.toolsFound')}`,
);
} else {
toast.success(t('mcp.testSuccess'));
}
} else {
toast.success(t('mcp.testSuccess'));
}
} catch (parseError) {
console.error('Failed to parse test result:', parseError);
toast.success(t('mcp.testSuccess'));
}
onRefresh();
} }
setTesting(false);
// Refresh to get updated runtime_info
onRefresh();
} }
}); });
}, 1000); }, 1000);
@@ -141,28 +96,37 @@ export default function MCPCardComponent({
<div className="w-full h-full flex flex-col items-start justify-between gap-[0.6rem]"> <div className="w-full h-full flex flex-col items-start justify-between gap-[0.6rem]">
<div className="flex flex-col items-start justify-start"> <div className="flex flex-col items-start justify-start">
<div className="flex flex-col items-start justify-start"> <div className="text-[1.2rem] text-black dark:text-[#f0f0f0] font-medium">
<div className="flex flex-row items-center justify-start gap-[0.4rem]"> {cardVO.name}
<div className="text-[1.2rem] text-black dark:text-[#f0f0f0] font-medium">
{cardVO.name}
</div>
</div>
</div> </div>
{error && (
<div className="text-[0.7rem] text-red-500 dark:text-red-400 line-clamp-2 mt-1">
{error}
</div>
)}
</div> </div>
<div className="w-full flex flex-row items-start justify-start gap-[0.6rem]"> <div className="w-full flex flex-row items-start justify-start gap-[0.6rem]">
<div className="flex h-full flex-row items-center justify-center gap-[0.4rem]"> {!enabled ? (
<Wrench className="w-5 h-5" /> // 未启用 - 橙色
<div className="text-base text-black dark:text-[#f0f0f0] font-medium"> <div className="flex flex-row items-center gap-[0.4rem]">
{t('mcp.toolCount', { count: toolsCount })} <Ban className="w-4 h-4 text-orange-500 dark:text-orange-400" />
<div className="text-sm text-orange-500 dark:text-orange-400 font-medium">
{t('mcp.statusDisabled')}
</div>
</div> </div>
</div> ) : status === 'connected' ? (
// 连接成功 - 显示工具数量
<div className="flex h-full flex-row items-center justify-center gap-[0.4rem]">
<Wrench className="w-5 h-5" />
<div className="text-base text-black dark:text-[#f0f0f0] font-medium">
{t('mcp.toolCount', { count: toolsCount })}
</div>
</div>
) : (
// 连接失败 - 红色
<div className="flex flex-row items-center gap-[0.4rem]">
<AlertCircle className="w-4 h-4 text-red-500 dark:text-red-400" />
<div className="text-sm text-red-500 dark:text-red-400 font-medium">
{t('mcp.connectionFailed')}
</div>
</div>
)}
</div> </div>
</div> </div>
@@ -13,6 +13,12 @@ import {
DialogTitle, DialogTitle,
DialogFooter, DialogFooter,
} from '@/components/ui/dialog'; } from '@/components/ui/dialog';
import {
Card,
CardHeader,
CardTitle,
CardDescription,
} from '@/components/ui/card';
import { import {
Form, Form,
FormControl, FormControl,
@@ -32,6 +38,98 @@ import {
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { httpClient } from '@/app/infra/http/HttpClient'; import { httpClient } from '@/app/infra/http/HttpClient';
import {
MCPServerRuntimeInfo,
MCPTool,
MCPServer,
} from '@/app/infra/entities/api';
// Status Display Component - 只在测试中或连接失败时使用
function StatusDisplay({
testing,
runtimeInfo,
t,
}: {
testing: boolean;
runtimeInfo: MCPServerRuntimeInfo;
t: (key: string) => string;
}) {
if (testing) {
return (
<div className="flex items-center gap-2 text-blue-600">
<svg
className="w-5 h-5 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
<span className="font-medium">{t('mcp.testing')}</span>
</div>
);
}
// 连接失败
return (
<div className="space-y-1">
<div className="flex items-center gap-2 text-red-600">
<svg
className="w-5 h-5"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span className="font-medium">{t('mcp.connectionFailed')}</span>
</div>
{runtimeInfo.error_message && (
<div className="text-sm text-red-500 pl-7">
{runtimeInfo.error_message}
</div>
)}
</div>
);
}
// Tools List Component
function ToolsList({ tools }: { tools: MCPTool[] }) {
return (
<div className="space-y-2 max-h-[300px] overflow-y-auto">
{tools.map((tool, index) => (
<Card key={index} className="py-3 shadow-none">
<CardHeader>
<CardTitle className="text-sm">{tool.name}</CardTitle>
{tool.description && (
<CardDescription className="text-xs">
{tool.description}
</CardDescription>
)}
</CardHeader>
</Card>
))}
</div>
);
}
const getFormSchema = (t: (key: string) => string) => const getFormSchema = (t: (key: string) => string) =>
z.object({ z.object({
@@ -72,7 +170,6 @@ interface MCPFormDialogProps {
isEditMode?: boolean; isEditMode?: boolean;
onSuccess?: () => void; onSuccess?: () => void;
onDelete?: () => void; onDelete?: () => void;
onUpdateToolsCache?: (serverName: string, toolsCount: number) => void;
} }
export default function MCPFormDialog({ export default function MCPFormDialog({
@@ -82,7 +179,6 @@ export default function MCPFormDialog({
isEditMode = false, isEditMode = false,
onSuccess, onSuccess,
onDelete, onDelete,
onUpdateToolsCache,
}: MCPFormDialogProps) { }: MCPFormDialogProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const formSchema = getFormSchema(t); const formSchema = getFormSchema(t);
@@ -102,11 +198,9 @@ export default function MCPFormDialog({
{ key: string; type: 'string' | 'number' | 'boolean'; value: string }[] { key: string; type: 'string' | 'number' | 'boolean'; value: string }[]
>([]); >([]);
const [mcpTesting, setMcpTesting] = useState(false); const [mcpTesting, setMcpTesting] = useState(false);
const [mcpTestStatus, setMcpTestStatus] = useState< const [runtimeInfo, setRuntimeInfo] = useState<MCPServerRuntimeInfo | null>(
'idle' | 'testing' | 'success' | 'failed' null,
>('idle'); );
const [mcpToolNames, setMcpToolNames] = useState<string[]>([]);
const [mcpTestError, setMcpTestError] = useState<string>('');
// Load server data when editing // Load server data when editing
useEffect(() => { useEffect(() => {
@@ -116,9 +210,7 @@ export default function MCPFormDialog({
// Reset form when creating new server // Reset form when creating new server
form.reset(); form.reset();
setExtraArgs([]); setExtraArgs([]);
setMcpTestStatus('idle'); setRuntimeInfo(null);
setMcpToolNames([]);
setMcpTestError('');
} }
}, [open, isEditMode, serverName]); }, [open, isEditMode, serverName]);
@@ -145,88 +237,11 @@ export default function MCPFormDialog({
form.setValue('extra_args', headers); form.setValue('extra_args', headers);
} }
setMcpTestStatus('testing'); // Set runtime_info from server data
setMcpToolNames([]); if (server.runtime_info) {
setMcpTestError(''); setRuntimeInfo(server.runtime_info);
} else {
try { setRuntimeInfo(null);
const res = await httpClient.testMCPServer(server.name);
if (res.task_id) {
const taskId = res.task_id;
const interval = setInterval(() => {
httpClient
.getAsyncTask(taskId)
.then((taskResp) => {
if (taskResp.runtime && taskResp.runtime.done) {
clearInterval(interval);
if (taskResp.runtime.exception) {
setMcpTestStatus('failed');
setMcpToolNames([]);
setMcpTestError(taskResp.runtime.exception || '未知错误');
} else if (taskResp.runtime.result) {
try {
let result: {
status?: string;
tools_count?: number;
tools_names_lists?: string[];
error?: string;
};
const rawResult: unknown = taskResp.runtime.result;
if (typeof rawResult === 'string') {
result = JSON.parse(rawResult.replace(/'/g, '"'));
} else {
result = rawResult as typeof result;
}
if (
result.tools_names_lists &&
result.tools_names_lists.length > 0
) {
setMcpTestStatus('success');
setMcpToolNames(result.tools_names_lists);
// Update tools cache
if (onUpdateToolsCache && serverName) {
onUpdateToolsCache(
serverName,
result.tools_names_lists.length,
);
}
} else {
setMcpTestStatus('failed');
setMcpToolNames([]);
setMcpTestError('未找到任何工具');
}
} catch (parseError) {
setMcpTestStatus('failed');
setMcpToolNames([]);
setMcpTestError('解析测试结果失败');
}
} else {
setMcpTestStatus('failed');
setMcpToolNames([]);
setMcpTestError('测试未返回结果');
}
}
})
.catch((err) => {
clearInterval(interval);
setMcpTestStatus('failed');
setMcpToolNames([]);
setMcpTestError(err.message || '获取任务状态失败');
});
}, 1000);
} else {
setMcpTestStatus('failed');
setMcpToolNames([]);
setMcpTestError('未获取到任务ID');
}
} catch (error) {
setMcpTestStatus('failed');
setMcpToolNames([]);
setMcpTestError((error as Error).message || '测试连接时发生错误');
} }
} catch (error) { } catch (error) {
console.error('Failed to load server:', error); console.error('Failed to load server:', error);
@@ -235,167 +250,103 @@ export default function MCPFormDialog({
} }
async function handleFormSubmit(value: z.infer<typeof formSchema>) { async function handleFormSubmit(value: z.infer<typeof formSchema>) {
const extraArgsObj: Record<string, string | number | boolean> = {}; // Convert extra_args to headers - all values must be strings according to MCPServerExtraArgsSSE
value.extra_args?.forEach( const headers: Record<string, string> = {};
(arg: { key: string; type: string; value: string }) => { value.extra_args?.forEach((arg) => {
if (arg.type === 'number') { // Convert all values to strings to match MCPServerExtraArgsSSE.headers type
extraArgsObj[arg.key] = Number(arg.value); headers[arg.key] = String(arg.value);
} else if (arg.type === 'boolean') { });
extraArgsObj[arg.key] = arg.value === 'true';
} else {
extraArgsObj[arg.key] = arg.value;
}
},
);
try { try {
const serverConfig = { const serverConfig: Omit<
MCPServer,
'uuid' | 'created_at' | 'updated_at' | 'runtime_info'
> = {
name: value.name, name: value.name,
mode: 'sse' as const, mode: 'sse' as const,
enable: true, enable: true,
url: value.url, extra_args: {
headers: extraArgsObj as Record<string, string>, url: value.url,
timeout: value.timeout, headers: headers,
ssereadtimeout: value.ssereadtimeout, timeout: value.timeout,
ssereadtimeout: value.ssereadtimeout,
},
}; };
if (isEditMode && serverName) { if (isEditMode && serverName) {
await httpClient.updateMCPServer(serverName, serverConfig); await httpClient.updateMCPServer(serverName, serverConfig);
toast.success(t('mcp.updateSuccess')); toast.success(t('mcp.updateSuccess'));
} else { } else {
await httpClient.createMCPServer({ await httpClient.createMCPServer(serverConfig);
extra_args: {
url: value.url,
headers: extraArgsObj as Record<string, string>,
timeout: value.timeout,
ssereadtimeout: value.ssereadtimeout,
},
name: value.name,
mode: 'sse' as const,
enable: true,
});
toast.success(t('mcp.createSuccess')); toast.success(t('mcp.createSuccess'));
} }
onOpenChange(false); handleDialogClose(false);
form.reset(); onSuccess?.();
setExtraArgs([]);
if (onSuccess) {
onSuccess();
}
} catch (error) { } catch (error) {
console.error('Failed to save MCP server:', error); console.error('Failed to save MCP server:', error);
toast.error(isEditMode ? t('mcp.updateFailed') : t('mcp.createFailed')); toast.error(isEditMode ? t('mcp.updateFailed') : t('mcp.createFailed'));
} }
} }
function testMcp() { async function testMcp() {
const serverName = form.getValues('name');
setMcpTesting(true); setMcpTesting(true);
const extraArgsObj: Record<string, string | number | boolean> = {};
form
.getValues('extra_args')
?.forEach((arg: { key: string; type: string; value: string }) => {
if (arg.type === 'number') {
extraArgsObj[arg.key] = Number(arg.value);
} else if (arg.type === 'boolean') {
extraArgsObj[arg.key] = arg.value === 'true';
} else {
extraArgsObj[arg.key] = arg.value;
}
});
httpClient
.testMCPServer(form.getValues('name'))
.then((res) => {
if (res.task_id) {
const taskId = res.task_id;
const interval = setInterval(() => { try {
httpClient const { task_id } = await httpClient.testMCPServer(serverName);
.getAsyncTask(taskId) if (!task_id) {
.then((taskResp) => { throw new Error(t('mcp.noTaskId'));
if (taskResp.runtime && taskResp.runtime.done) { }
clearInterval(interval);
setMcpTesting(false);
if (taskResp.runtime.exception) { const interval = setInterval(async () => {
toast.error( try {
t('mcp.testError') + const taskResp = await httpClient.getAsyncTask(task_id);
': ' +
(taskResp.runtime.exception || t('mcp.unknownError')),
);
} else if (taskResp.runtime.result) {
try {
let result: {
status?: string;
tools_count?: number;
tools_names_lists?: string[];
error?: string;
};
const rawResult: unknown = taskResp.runtime.result; if (taskResp.runtime?.done) {
if (typeof rawResult === 'string') { clearInterval(interval);
result = JSON.parse(rawResult.replace(/'/g, '"')); setMcpTesting(false);
} else {
result = rawResult as typeof result;
}
if ( if (taskResp.runtime.exception) {
result.tools_names_lists && const errorMsg =
result.tools_names_lists.length > 0 taskResp.runtime.exception || t('mcp.unknownError');
) { toast.error(`${t('mcp.testError')}: ${errorMsg}`);
toast.success( setRuntimeInfo({
t('mcp.testSuccess') + connected: false,
' - ' + error_message: errorMsg,
result.tools_names_lists.length + tool_count: 0,
' ' + tools: [],
t('mcp.toolsFound'),
);
} else {
toast.error(
t('mcp.testError') + ': ' + t('mcp.noToolsFound'),
);
}
} catch (parseError) {
console.error('Failed to parse test result:', parseError);
toast.error(
t('mcp.testError') + ': ' + t('mcp.parseResultFailed'),
);
}
} else {
toast.error(
t('mcp.testError') + ': ' + t('mcp.noResultReturned'),
);
}
}
})
.catch((err) => {
console.error('获取测试任务状态失败:', err);
clearInterval(interval);
setMcpTesting(false);
toast.error(
t('mcp.testError') +
': ' +
(err.message || t('mcp.getTaskFailed')),
);
}); });
}, 1000); } else if (taskResp.runtime.result) {
} else { await loadServerForEdit(serverName);
toast.success(t('mcp.testSuccess'));
} else {
toast.error(
`${t('mcp.testError')}: ${t('mcp.noResultReturned')}`,
);
}
}
} catch (err) {
clearInterval(interval);
setMcpTesting(false); setMcpTesting(false);
toast.error(t('mcp.testError') + ': ' + t('mcp.noTaskId')); const errorMsg = (err as Error).message || t('mcp.getTaskFailed');
toast.error(`${t('mcp.testError')}: ${errorMsg}`);
} }
}) }, 1000);
.catch((err) => { } catch (err) {
console.error('启动测试失败:', err); setMcpTesting(false);
setMcpTesting(false); const errorMsg = (err as Error).message || t('mcp.unknownError');
toast.error( toast.error(`${t('mcp.testError')}: ${errorMsg}`);
t('mcp.testError') + ': ' + (err.message || t('mcp.unknownError')), }
);
});
} }
const addExtraArg = () => { const addExtraArg = () => {
setExtraArgs([...extraArgs, { key: '', type: 'string', value: '' }]); const newArgs = [
...extraArgs,
{ key: '', type: 'string' as const, value: '' },
];
setExtraArgs(newArgs);
form.setValue('extra_args', newArgs);
}; };
const removeExtraArg = (index: number) => { const removeExtraArg = (index: number) => {
@@ -410,25 +361,22 @@ export default function MCPFormDialog({
value: string, value: string,
) => { ) => {
const newArgs = [...extraArgs]; const newArgs = [...extraArgs];
newArgs[index] = { newArgs[index] = { ...newArgs[index], [field]: value };
...newArgs[index],
[field]: value,
};
setExtraArgs(newArgs); setExtraArgs(newArgs);
form.setValue('extra_args', newArgs); form.setValue('extra_args', newArgs);
}; };
const handleDialogClose = (open: boolean) => {
onOpenChange(open);
if (!open) {
form.reset();
setExtraArgs([]);
setRuntimeInfo(null);
}
};
return ( return (
<Dialog <Dialog open={open} onOpenChange={handleDialogClose}>
open={open}
onOpenChange={(open) => {
onOpenChange(open);
if (!open) {
form.reset();
setExtraArgs([]);
}
}}
>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
@@ -436,97 +384,25 @@ export default function MCPFormDialog({
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
{isEditMode && ( {isEditMode && runtimeInfo && (
<div className="mb-4 p-3 rounded-lg border"> <div className="mb-4 space-y-3">
{mcpTestStatus === 'testing' && ( {/* 测试中或连接失败时显示状态 */}
<div className="flex items-center gap-2 text-blue-600"> {(mcpTesting || !runtimeInfo.connected) && (
<svg <div className="p-3 rounded-lg border">
className="w-5 h-5 animate-spin" <StatusDisplay
xmlns="http://www.w3.org/2000/svg" testing={mcpTesting}
fill="none" runtimeInfo={runtimeInfo}
viewBox="0 0 24 24" t={t}
> />
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<span className="font-medium">{t('mcp.testing')}</span>
</div> </div>
)} )}
{mcpTestStatus === 'success' && ( {/* 连接成功时只显示工具列表 */}
<div className="space-y-2"> {!mcpTesting &&
<div className="flex items-center gap-2 text-green-600"> runtimeInfo.connected &&
<svg runtimeInfo.tools?.length > 0 && (
className="w-5 h-5" <ToolsList tools={runtimeInfo.tools} />
xmlns="http://www.w3.org/2000/svg" )}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span className="font-medium">
{t('mcp.connectionSuccess')} - {mcpToolNames.length}{' '}
{t('mcp.toolsFound')}
</span>
</div>
<div className="flex flex-wrap gap-1 mt-2">
{mcpToolNames.map((toolName, index) => (
<span
key={index}
className="px-2 py-1 bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 text-xs rounded-md"
>
{toolName}
</span>
))}
</div>
</div>
)}
{mcpTestStatus === 'failed' && (
<div className="space-y-1">
<div className="flex items-center gap-2 text-red-600">
<svg
className="w-5 h-5"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span className="font-medium">
{t('mcp.connectionFailed')}
</span>
</div>
{mcpTestError && (
<div className="text-sm text-red-500 pl-7">
{mcpTestError}
</div>
)}
</div>
)}
</div> </div>
)} )}
@@ -695,11 +571,7 @@ export default function MCPFormDialog({
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
onClick={() => { onClick={() => handleDialogClose(false)}
onOpenChange(false);
form.reset();
setExtraArgs([]);
}}
> >
{t('common.cancel')} {t('common.cancel')}
</Button> </Button>
-12
View File
@@ -70,11 +70,6 @@ export default function PluginConfigPage() {
const [isEditMode, setIsEditMode] = useState(false); const [isEditMode, setIsEditMode] = useState(false);
const [refreshKey, setRefreshKey] = useState(0); const [refreshKey, setRefreshKey] = useState(0);
// 缓存每个服务器测试后的工具数量
const [serverToolsCache, setServerToolsCache] = useState<
Record<string, number>
>({});
useEffect(() => { useEffect(() => {
const fetchPluginSystemStatus = async () => { const fetchPluginSystemStatus = async () => {
try { try {
@@ -404,7 +399,6 @@ export default function PluginConfigPage() {
setIsEditMode(true); setIsEditMode(true);
setMcpSSEModalOpen(true); setMcpSSEModalOpen(true);
}} }}
toolsCountCache={serverToolsCache}
/> />
</TabsContent> </TabsContent>
</Tabs> </Tabs>
@@ -496,12 +490,6 @@ export default function PluginConfigPage() {
onDelete={() => { onDelete={() => {
setShowDeleteConfirmModal(true); setShowDeleteConfirmModal(true);
}} }}
onUpdateToolsCache={(serverName, toolsCount) => {
setServerToolsCache((prev) => ({
...prev,
[serverName]: toolsCount,
}));
}}
/> />
<MCPDeleteConfirmDialog <MCPDeleteConfirmDialog