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

View File

@@ -1,4 +1,4 @@
import { MCPServer, MCPServerConfig } from '@/app/infra/entities/api';
import { MCPServer } from '@/app/infra/entities/api';
export class MCPCardVO {
name: string;
@@ -7,20 +7,24 @@ export class MCPCardVO {
status: 'connected' | 'disconnected' | 'error' | 'disabled';
tools: number;
error?: string;
config: MCPServerConfig;
constructor(data: MCPServer) {
this.name = data.name;
this.mode = data.mode;
this.enable = data.enable;
this.status =
(data.status as string) === 'enabled' ? 'connected' : data.status;
this.tools = Array.isArray(data.tools)
? data.tools.length
: data.tools || 0;
this.error = data.error;
this.config = data.config;
// Determine status from runtime_info
if (!data.runtime_info) {
this.status = 'disconnected';
this.tools = 0;
} else if (data.runtime_info.connected) {
this.status = 'connected';
this.tools = data.runtime_info.tool_count || 0;
} else {
this.status = 'error';
this.tools = 0;
this.error = data.runtime_info.error_message;
}
}
getStatusColor(): string {

View File

@@ -9,41 +9,24 @@ import { httpClient } from '@/app/infra/http/HttpClient';
export default function MCPComponent({
onEditServer,
toolsCountCache = {},
}: {
askInstallServer?: (githubURL: string) => void;
onEditServer?: (serverName: string) => void;
toolsCountCache?: Record<string, number>;
}) {
const { t } = useTranslation();
const [installedServers, setInstalledServers] = useState<MCPCardVO[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
initData();
fetchInstalledServers();
}, []);
useEffect(() => {
fetchInstalledServers();
}, [toolsCountCache]);
function initData() {
fetchInstalledServers();
}
function fetchInstalledServers() {
setLoading(true);
httpClient
.getMCPServers()
.then((resp) => {
const servers = resp.servers.map((server) => {
const vo = new MCPCardVO(server);
if (toolsCountCache[server.name] !== undefined) {
vo.tools = toolsCountCache[server.name];
}
return vo;
});
const servers = resp.servers.map((server) => new MCPCardVO(server));
setInstalledServers(servers);
setLoading(false);
})

View File

@@ -1,12 +1,11 @@
import { MCPCardVO } from '@/app/home/plugins/mcp-server/MCPCardVO';
import { useState, useEffect } from 'react';
import { httpClient } from '@/app/infra/http/HttpClient';
import { Badge } from '@/components/ui/badge';
import { Switch } from '@/components/ui/switch';
import { Button } from '@/components/ui/button';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import { RefreshCcw, Wrench } from 'lucide-react';
import { RefreshCcw, Wrench, Ban, AlertCircle } from 'lucide-react';
export default function MCPCardComponent({
cardVO,
@@ -23,19 +22,12 @@ export default function MCPCardComponent({
const [testing, setTesting] = useState(false);
const [toolsCount, setToolsCount] = useState(cardVO.tools);
const [status, setStatus] = useState(cardVO.status);
const [error, setError] = useState(cardVO.error);
useEffect(() => {
console.log(`[MCPCard ${cardVO.name}] Status updated:`, {
status: cardVO.status,
tools: cardVO.tools,
error: cardVO.error,
});
setStatus(cardVO.status);
setError(cardVO.error);
setToolsCount(cardVO.tools);
setEnabled(cardVO.enable);
}, [cardVO.name, cardVO.status, cardVO.error, cardVO.tools, cardVO.enable]);
}, [cardVO.status, cardVO.tools, cardVO.enable]);
function handleEnable(checked: boolean) {
setSwitchEnable(false);
@@ -54,65 +46,28 @@ export default function MCPCardComponent({
}
function handleTest(e: React.MouseEvent) {
e.stopPropagation(); // 阻止事件冒泡
e.stopPropagation();
setTesting(true);
httpClient
.testMCPServer(cardVO.name)
.then((resp) => {
const taskId = resp.task_id;
// 监控任务状态
const interval = setInterval(() => {
httpClient.getAsyncTask(taskId).then((taskResp) => {
if (taskResp.runtime.done) {
clearInterval(interval);
setTesting(false);
if (taskResp.runtime.exception) {
toast.error(t('mcp.testFailed') + taskResp.runtime.exception);
} else {
// 解析测试结果获取工具数量
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();
toast.success(t('mcp.testSuccess'));
}
setTesting(false);
// Refresh to get updated runtime_info
onRefresh();
}
});
}, 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="flex flex-col items-start justify-start">
<div className="flex flex-col items-start justify-start">
<div className="flex flex-row items-center justify-start gap-[0.4rem]">
<div className="text-[1.2rem] text-black dark:text-[#f0f0f0] font-medium">
{cardVO.name}
</div>
</div>
<div className="text-[1.2rem] text-black dark:text-[#f0f0f0] font-medium">
{cardVO.name}
</div>
{error && (
<div className="text-[0.7rem] text-red-500 dark:text-red-400 line-clamp-2 mt-1">
{error}
</div>
)}
</div>
<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]">
<Wrench className="w-5 h-5" />
<div className="text-base text-black dark:text-[#f0f0f0] font-medium">
{t('mcp.toolCount', { count: toolsCount })}
{!enabled ? (
// 未启用 - 橙色
<div className="flex flex-row items-center gap-[0.4rem]">
<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>
) : 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>

View File

@@ -13,6 +13,12 @@ import {
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog';
import {
Card,
CardHeader,
CardTitle,
CardDescription,
} from '@/components/ui/card';
import {
Form,
FormControl,
@@ -32,6 +38,98 @@ import {
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
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) =>
z.object({
@@ -72,7 +170,6 @@ interface MCPFormDialogProps {
isEditMode?: boolean;
onSuccess?: () => void;
onDelete?: () => void;
onUpdateToolsCache?: (serverName: string, toolsCount: number) => void;
}
export default function MCPFormDialog({
@@ -82,7 +179,6 @@ export default function MCPFormDialog({
isEditMode = false,
onSuccess,
onDelete,
onUpdateToolsCache,
}: MCPFormDialogProps) {
const { t } = useTranslation();
const formSchema = getFormSchema(t);
@@ -102,11 +198,9 @@ export default function MCPFormDialog({
{ key: string; type: 'string' | 'number' | 'boolean'; value: string }[]
>([]);
const [mcpTesting, setMcpTesting] = useState(false);
const [mcpTestStatus, setMcpTestStatus] = useState<
'idle' | 'testing' | 'success' | 'failed'
>('idle');
const [mcpToolNames, setMcpToolNames] = useState<string[]>([]);
const [mcpTestError, setMcpTestError] = useState<string>('');
const [runtimeInfo, setRuntimeInfo] = useState<MCPServerRuntimeInfo | null>(
null,
);
// Load server data when editing
useEffect(() => {
@@ -116,9 +210,7 @@ export default function MCPFormDialog({
// Reset form when creating new server
form.reset();
setExtraArgs([]);
setMcpTestStatus('idle');
setMcpToolNames([]);
setMcpTestError('');
setRuntimeInfo(null);
}
}, [open, isEditMode, serverName]);
@@ -145,88 +237,11 @@ export default function MCPFormDialog({
form.setValue('extra_args', headers);
}
setMcpTestStatus('testing');
setMcpToolNames([]);
setMcpTestError('');
try {
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 || '测试连接时发生错误');
// Set runtime_info from server data
if (server.runtime_info) {
setRuntimeInfo(server.runtime_info);
} else {
setRuntimeInfo(null);
}
} catch (error) {
console.error('Failed to load server:', error);
@@ -235,167 +250,103 @@ export default function MCPFormDialog({
}
async function handleFormSubmit(value: z.infer<typeof formSchema>) {
const extraArgsObj: Record<string, string | number | boolean> = {};
value.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;
}
},
);
// Convert extra_args to headers - all values must be strings according to MCPServerExtraArgsSSE
const headers: Record<string, string> = {};
value.extra_args?.forEach((arg) => {
// Convert all values to strings to match MCPServerExtraArgsSSE.headers type
headers[arg.key] = String(arg.value);
});
try {
const serverConfig = {
const serverConfig: Omit<
MCPServer,
'uuid' | 'created_at' | 'updated_at' | 'runtime_info'
> = {
name: value.name,
mode: 'sse' as const,
enable: true,
url: value.url,
headers: extraArgsObj as Record<string, string>,
timeout: value.timeout,
ssereadtimeout: value.ssereadtimeout,
extra_args: {
url: value.url,
headers: headers,
timeout: value.timeout,
ssereadtimeout: value.ssereadtimeout,
},
};
if (isEditMode && serverName) {
await httpClient.updateMCPServer(serverName, serverConfig);
toast.success(t('mcp.updateSuccess'));
} else {
await httpClient.createMCPServer({
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,
});
await httpClient.createMCPServer(serverConfig);
toast.success(t('mcp.createSuccess'));
}
onOpenChange(false);
form.reset();
setExtraArgs([]);
if (onSuccess) {
onSuccess();
}
handleDialogClose(false);
onSuccess?.();
} catch (error) {
console.error('Failed to save MCP server:', error);
toast.error(isEditMode ? t('mcp.updateFailed') : t('mcp.createFailed'));
}
}
function testMcp() {
async function testMcp() {
const serverName = form.getValues('name');
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(() => {
httpClient
.getAsyncTask(taskId)
.then((taskResp) => {
if (taskResp.runtime && taskResp.runtime.done) {
clearInterval(interval);
setMcpTesting(false);
try {
const { task_id } = await httpClient.testMCPServer(serverName);
if (!task_id) {
throw new Error(t('mcp.noTaskId'));
}
if (taskResp.runtime.exception) {
toast.error(
t('mcp.testError') +
': ' +
(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 interval = setInterval(async () => {
try {
const taskResp = await httpClient.getAsyncTask(task_id);
const rawResult: unknown = taskResp.runtime.result;
if (typeof rawResult === 'string') {
result = JSON.parse(rawResult.replace(/'/g, '"'));
} else {
result = rawResult as typeof result;
}
if (taskResp.runtime?.done) {
clearInterval(interval);
setMcpTesting(false);
if (
result.tools_names_lists &&
result.tools_names_lists.length > 0
) {
toast.success(
t('mcp.testSuccess') +
' - ' +
result.tools_names_lists.length +
' ' +
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')),
);
if (taskResp.runtime.exception) {
const errorMsg =
taskResp.runtime.exception || t('mcp.unknownError');
toast.error(`${t('mcp.testError')}: ${errorMsg}`);
setRuntimeInfo({
connected: false,
error_message: errorMsg,
tool_count: 0,
tools: [],
});
}, 1000);
} else {
} else if (taskResp.runtime.result) {
await loadServerForEdit(serverName);
toast.success(t('mcp.testSuccess'));
} else {
toast.error(
`${t('mcp.testError')}: ${t('mcp.noResultReturned')}`,
);
}
}
} catch (err) {
clearInterval(interval);
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}`);
}
})
.catch((err) => {
console.error('启动测试失败:', err);
setMcpTesting(false);
toast.error(
t('mcp.testError') + ': ' + (err.message || t('mcp.unknownError')),
);
});
}, 1000);
} catch (err) {
setMcpTesting(false);
const errorMsg = (err as Error).message || t('mcp.unknownError');
toast.error(`${t('mcp.testError')}: ${errorMsg}`);
}
}
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) => {
@@ -410,25 +361,22 @@ export default function MCPFormDialog({
value: string,
) => {
const newArgs = [...extraArgs];
newArgs[index] = {
...newArgs[index],
[field]: value,
};
newArgs[index] = { ...newArgs[index], [field]: value };
setExtraArgs(newArgs);
form.setValue('extra_args', newArgs);
};
const handleDialogClose = (open: boolean) => {
onOpenChange(open);
if (!open) {
form.reset();
setExtraArgs([]);
setRuntimeInfo(null);
}
};
return (
<Dialog
open={open}
onOpenChange={(open) => {
onOpenChange(open);
if (!open) {
form.reset();
setExtraArgs([]);
}
}}
>
<Dialog open={open} onOpenChange={handleDialogClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>
@@ -436,97 +384,25 @@ export default function MCPFormDialog({
</DialogTitle>
</DialogHeader>
{isEditMode && (
<div className="mb-4 p-3 rounded-lg border">
{mcpTestStatus === 'testing' && (
<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"
></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>
{isEditMode && runtimeInfo && (
<div className="mb-4 space-y-3">
{/* 测试中或连接失败时显示状态 */}
{(mcpTesting || !runtimeInfo.connected) && (
<div className="p-3 rounded-lg border">
<StatusDisplay
testing={mcpTesting}
runtimeInfo={runtimeInfo}
t={t}
/>
</div>
)}
{mcpTestStatus === 'success' && (
<div className="space-y-2">
<div className="flex items-center gap-2 text-green-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="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>
)}
{/* 连接成功时只显示工具列表 */}
{!mcpTesting &&
runtimeInfo.connected &&
runtimeInfo.tools?.length > 0 && (
<ToolsList tools={runtimeInfo.tools} />
)}
</div>
)}
@@ -695,11 +571,7 @@ export default function MCPFormDialog({
<Button
type="button"
variant="outline"
onClick={() => {
onOpenChange(false);
form.reset();
setExtraArgs([]);
}}
onClick={() => handleDialogClose(false)}
>
{t('common.cancel')}
</Button>

View File

@@ -70,11 +70,6 @@ export default function PluginConfigPage() {
const [isEditMode, setIsEditMode] = useState(false);
const [refreshKey, setRefreshKey] = useState(0);
// 缓存每个服务器测试后的工具数量
const [serverToolsCache, setServerToolsCache] = useState<
Record<string, number>
>({});
useEffect(() => {
const fetchPluginSystemStatus = async () => {
try {
@@ -404,7 +399,6 @@ export default function PluginConfigPage() {
setIsEditMode(true);
setMcpSSEModalOpen(true);
}}
toolsCountCache={serverToolsCache}
/>
</TabsContent>
</Tabs>
@@ -496,12 +490,6 @@ export default function PluginConfigPage() {
onDelete={() => {
setShowDeleteConfirmModal(true);
}}
onUpdateToolsCache={(serverName, toolsCount) => {
setServerToolsCache((prev) => ({
...prev,
[serverName]: toolsCount,
}));
}}
/>
<MCPDeleteConfirmDialog