From 3d81df739dc45599c257d8cdefe0996f66ccdeae Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期四, 19 三月 2026 12:18:14 +0800
Subject: [PATCH] #AI.MCP 管理增强

---
 rsf-admin/src/api/ai/mcpMount.js                                                           |    9 
 rsf-admin/src/page/system/aiMcpMount/AiMcpMountToolsPanel.jsx                              |  344 +++++++++++++++++++----
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiMcpMountServiceImpl.java |  108 ++++++
 rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiMcpMountController.java    |    6 
 rsf-admin/src/page/system/aiMcpMount/AiMcpMountList.jsx                                    |  205 ++++++++++----
 rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiMcpConnectivityTestDto.java       |   23 +
 version/db/ai_feature.sql                                                                  |   72 +++++
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiMcpMountService.java          |    3 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/config/AiDefaults.java                  |    3 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/entity/AiMcpMount.java                  |   25 +
 rsf-admin/src/page/system/aiMcpMount/AiMcpMountForm.jsx                                    |   15 +
 11 files changed, 668 insertions(+), 145 deletions(-)

diff --git a/rsf-admin/src/api/ai/mcpMount.js b/rsf-admin/src/api/ai/mcpMount.js
index 399d0c9..bd8a831 100644
--- a/rsf-admin/src/api/ai/mcpMount.js
+++ b/rsf-admin/src/api/ai/mcpMount.js
@@ -9,6 +9,15 @@
     throw new Error(msg || "鑾峰彇宸ュ叿鍒楄〃澶辫触");
 };
 
+export const testMcpConnectivity = async (mountId) => {
+    const res = await request.post(`aiMcpMount/${mountId}/connectivity/test`);
+    const { code, msg, data } = res.data;
+    if (code === 200) {
+        return data;
+    }
+    throw new Error(msg || "杩為�氭�ф祴璇曞け璐�");
+};
+
 export const testMcpTool = async (mountId, payload) => {
     const res = await request.post(`aiMcpMount/${mountId}/tool/test`, payload);
     const { code, msg, data } = res.data;
diff --git a/rsf-admin/src/page/system/aiMcpMount/AiMcpMountForm.jsx b/rsf-admin/src/page/system/aiMcpMount/AiMcpMountForm.jsx
index b88807a..b05ac16 100644
--- a/rsf-admin/src/page/system/aiMcpMount/AiMcpMountForm.jsx
+++ b/rsf-admin/src/page/system/aiMcpMount/AiMcpMountForm.jsx
@@ -87,6 +87,21 @@
         <Grid item xs={12}>
             <TextInput source="memo" label="澶囨敞" fullWidth multiline minRows={3} disabled={readOnly} />
         </Grid>
+        <Grid item xs={12}>
+            <Typography variant="h6">杩愯鎬佷俊鎭�</Typography>
+        </Grid>
+        <Grid item xs={12} md={4}>
+            <TextInput source="healthStatus" label="鍋ュ悍鐘舵��" fullWidth disabled />
+        </Grid>
+        <Grid item xs={12} md={4}>
+            <TextInput source="lastInitElapsedMs" label="鏈�杩戝垵濮嬪寲鑰楁椂(ms)" fullWidth disabled />
+        </Grid>
+        <Grid item xs={12} md={4}>
+            <TextInput source="lastTestTime$" label="鏈�杩戞祴璇曟椂闂�" fullWidth disabled />
+        </Grid>
+        <Grid item xs={12}>
+            <TextInput source="lastTestMessage" label="鏈�杩戞祴璇曠粨鏋�" fullWidth multiline minRows={3} disabled />
+        </Grid>
     </Grid>
 );
 
diff --git a/rsf-admin/src/page/system/aiMcpMount/AiMcpMountList.jsx b/rsf-admin/src/page/system/aiMcpMount/AiMcpMountList.jsx
index a543ed8..9601238 100644
--- a/rsf-admin/src/page/system/aiMcpMount/AiMcpMountList.jsx
+++ b/rsf-admin/src/page/system/aiMcpMount/AiMcpMountList.jsx
@@ -27,10 +27,12 @@
 import DeleteOutlineOutlinedIcon from "@mui/icons-material/DeleteOutlineOutlined";
 import EditOutlinedIcon from "@mui/icons-material/EditOutlined";
 import VisibilityOutlinedIcon from "@mui/icons-material/VisibilityOutlined";
+import PlayCircleOutlineOutlinedIcon from "@mui/icons-material/PlayCircleOutlineOutlined";
 import MyExportButton from "@/page/components/MyExportButton";
 import AiMcpMountForm from "./AiMcpMountForm";
 import AiConfigDialog from "../aiShared/AiConfigDialog";
 import AiMcpMountToolsPanel from "./AiMcpMountToolsPanel";
+import { testMcpConnectivity } from "@/api/ai/mcpMount";
 
 const filters = [
     <SearchInput source="condition" alwaysOn />,
@@ -59,6 +61,7 @@
     requestTimeoutMs: 60000,
     sort: 0,
     status: 1,
+    healthStatus: "NOT_TESTED",
 };
 
 const truncateText = (value, max = 96) => {
@@ -78,9 +81,31 @@
     return record.serverUrl || "--";
 };
 
-const AiMcpMountCards = ({ onView, onEdit, onDelete, deleting }) => {
+const transportGroups = [
+    { key: "BUILTIN", title: "鍐呯疆 MCP", description: "绯荤粺鍐呯疆宸ュ叿鎸傝浇锛岄�傚悎鐩存帴鏆撮湶骞冲彴鑳藉姏銆�" },
+    { key: "SSE_HTTP", title: "杩滅▼ SSE MCP", description: "閫氳繃杩滅▼ MCP Server 鎸傝浇澶栭儴宸ュ叿銆�" },
+    { key: "STDIO", title: "鏈湴 STDIO MCP", description: "閫氳繃鏈湴鍛戒护杩涚▼鎸傝浇澶栭儴 MCP銆�" },
+];
+
+const resolveHealthMeta = (record) => {
+    if (record.healthStatus === "HEALTHY") {
+        return { color: "success", label: "姝e父" };
+    }
+    if (record.healthStatus === "UNHEALTHY") {
+        return { color: "error", label: "澶辫触" };
+    }
+    return { color: "default", label: "鏈祴璇�" };
+};
+
+const AiMcpMountCards = ({ onView, onEdit, onDelete, onConnectivityTest, deleting, testingConnectivityId }) => {
     const { data, isLoading } = useListContext();
     const records = useMemo(() => (Array.isArray(data) ? data : []), [data]);
+    const groupedRecords = useMemo(() => {
+        return transportGroups.map((group) => ({
+            ...group,
+            records: records.filter((item) => item.transportType === group.key),
+        })).filter((group) => group.records.length > 0);
+    }, [records]);
 
     if (isLoading) {
         return (
@@ -105,70 +130,103 @@
 
     return (
         <Box px={2} py={2}>
-            <Grid container spacing={2}>
-                {records.map((record) => (
-                    <Grid item xs={12} md={6} xl={4} key={record.id}>
-                        <Card
-                            variant="outlined"
-                            sx={{
-                                height: "100%",
-                                borderRadius: 3,
-                                boxShadow: "0 8px 24px rgba(15, 23, 42, 0.06)",
-                            }}
-                        >
-                            <CardContent sx={{ pb: 1.5 }}>
-                                <Stack direction="row" justifyContent="space-between" alignItems="flex-start" spacing={1}>
-                                    <Box>
-                                        <Typography variant="h6" sx={{ mb: 0.5 }}>
-                                            {record.name}
-                                        </Typography>
-                                        <Typography variant="body2" color="text.secondary">
-                                            {record.transportType$ || record.transportType || "--"}
-                                        </Typography>
-                                    </Box>
-                                    <Chip
-                                        size="small"
-                                        color={record.statusBool ? "success" : "default"}
-                                        label={record.statusBool ? "鍚敤" : "鍋滅敤"}
-                                    />
-                                </Stack>
-                                <Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap mt={1.5}>
-                                    <Chip size="small" variant="outlined" label={`鎺掑簭 ${record.sort ?? 0}`} />
-                                    <Chip size="small" variant="outlined" label={`${record.requestTimeoutMs ?? "--"} ms`} />
-                                </Stack>
-                                <Divider sx={{ my: 1.5 }} />
-                                <Typography variant="caption" color="text.secondary">鐩爣</Typography>
-                                <Typography variant="body2" sx={{ mt: 0.5, wordBreak: "break-all" }}>
-                                    {truncateText(resolveTargetLabel(record), 120)}
-                                </Typography>
-                                <Typography variant="caption" color="text.secondary" display="block" mt={1.5}>
-                                    澶囨敞
-                                </Typography>
-                                <Typography variant="body2">{truncateText(record.memo)}</Typography>
-                            </CardContent>
-                            <CardActions sx={{ px: 2, pb: 2, pt: 0, justifyContent: "space-between" }}>
-                                <Stack direction="row" spacing={1}>
-                                    <Button size="small" startIcon={<VisibilityOutlinedIcon />} onClick={() => onView(record.id)}>
-                                        璇︽儏
-                                    </Button>
-                                    <Button size="small" startIcon={<EditOutlinedIcon />} onClick={() => onEdit(record.id)}>
-                                        缂栬緫
-                                    </Button>
-                                </Stack>
-                                <Button
-                                    size="small"
-                                    color="error"
-                                    startIcon={<DeleteOutlineOutlinedIcon />}
-                                    onClick={() => onDelete(record)}
-                                    disabled={deleting}
-                                >
-                                    鍒犻櫎
-                                </Button>
-                            </CardActions>
-                        </Card>
-                    </Grid>
+            <Stack spacing={3}>
+                {groupedRecords.map((group) => (
+                    <Box key={group.key}>
+                        <Box mb={1.5}>
+                            <Typography variant="h6">{group.title}</Typography>
+                            <Typography variant="body2" color="text.secondary">
+                                {group.description}
+                            </Typography>
+                        </Box>
+                        <Grid container spacing={2}>
+                            {group.records.map((record) => {
+                                const healthMeta = resolveHealthMeta(record);
+                                return (
+                                    <Grid item xs={12} md={6} xl={4} key={record.id}>
+                                        <Card
+                                            variant="outlined"
+                                            sx={{
+                                                height: "100%",
+                                                borderRadius: 3,
+                                                boxShadow: "0 8px 24px rgba(15, 23, 42, 0.06)",
+                                            }}
+                                        >
+                                            <CardContent sx={{ pb: 1.5 }}>
+                                                <Stack direction="row" justifyContent="space-between" alignItems="flex-start" spacing={1}>
+                                                    <Box>
+                                                        <Typography variant="h6" sx={{ mb: 0.5 }}>
+                                                            {record.name}
+                                                        </Typography>
+                                                        <Typography variant="body2" color="text.secondary">
+                                                            {record.transportType$ || record.transportType || "--"}
+                                                        </Typography>
+                                                    </Box>
+                                                    <Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap justifyContent="flex-end">
+                                                        <Chip
+                                                            size="small"
+                                                            color={record.statusBool ? "success" : "default"}
+                                                            label={record.statusBool ? "鍚敤" : "鍋滅敤"}
+                                                        />
+                                                        <Chip size="small" color={healthMeta.color} label={healthMeta.label} />
+                                                    </Stack>
+                                                </Stack>
+                                                <Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap mt={1.5}>
+                                                    <Chip size="small" variant="outlined" label={`鎺掑簭 ${record.sort ?? 0}`} />
+                                                    <Chip size="small" variant="outlined" label={`${record.requestTimeoutMs ?? "--"} ms`} />
+                                                    <Chip size="small" variant="outlined" label={`Init ${record.lastInitElapsedMs ?? "--"} ms`} />
+                                                </Stack>
+                                                <Divider sx={{ my: 1.5 }} />
+                                                <Typography variant="caption" color="text.secondary">鐩爣</Typography>
+                                                <Typography variant="body2" sx={{ mt: 0.5, wordBreak: "break-all" }}>
+                                                    {truncateText(resolveTargetLabel(record), 120)}
+                                                </Typography>
+                                                <Typography variant="caption" color="text.secondary" display="block" mt={1.5}>
+                                                    鏈�杩戞祴璇�
+                                                </Typography>
+                                                <Typography variant="body2">
+                                                    {record.lastTestTime$ ? `${record.lastTestTime$} 路 ${truncateText(record.lastTestMessage, 72)}` : "灏氭湭鎵ц杩為�氭�ф祴璇�"}
+                                                </Typography>
+                                                <Typography variant="caption" color="text.secondary" display="block" mt={1.5}>
+                                                    澶囨敞
+                                                </Typography>
+                                                <Typography variant="body2">{truncateText(record.memo)}</Typography>
+                                            </CardContent>
+                                            <CardActions sx={{ px: 2, pb: 2, pt: 0, justifyContent: "space-between", alignItems: "flex-start" }}>
+                                                <Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap>
+                                                    <Button size="small" startIcon={<VisibilityOutlinedIcon />} onClick={() => onView(record.id)}>
+                                                        璇︽儏
+                                                    </Button>
+                                                    <Button size="small" startIcon={<EditOutlinedIcon />} onClick={() => onEdit(record.id)}>
+                                                        缂栬緫
+                                                    </Button>
+                                                    <Button
+                                                        size="small"
+                                                        startIcon={<PlayCircleOutlineOutlinedIcon />}
+                                                        onClick={() => onConnectivityTest(record)}
+                                                        disabled={testingConnectivityId === record.id}
+                                                    >
+                                                        {testingConnectivityId === record.id ? "娴嬭瘯涓�..." : "杩為�氭祴璇�"}
+                                                    </Button>
+                                                </Stack>
+                                                <Button
+                                                    size="small"
+                                                    color="error"
+                                                    startIcon={<DeleteOutlineOutlinedIcon />}
+                                                    onClick={() => onDelete(record)}
+                                                    disabled={deleting}
+                                                >
+                                                    鍒犻櫎
+                                                </Button>
+                                            </CardActions>
+                                        </Card>
+                                    </Grid>
+                                );
+                            })}
+                        </Grid>
+                    </Box>
                 ))}
-            </Grid>
+            </Stack>
         </Box>
     );
 };
@@ -178,6 +236,7 @@
     const refresh = useRefresh();
     const [deleteOne, { isPending: deleting }] = useDelete();
     const [dialogState, setDialogState] = useState({ open: false, mode: "create", recordId: null });
+    const [testingConnectivityId, setTestingConnectivityId] = useState(null);
 
     const openDialog = (mode, recordId = null) => setDialogState({ open: true, mode, recordId });
     const closeDialog = () => setDialogState({ open: false, mode: "create", recordId: null });
@@ -199,6 +258,22 @@
                 },
             }
         );
+    };
+
+    const handleConnectivityTest = async (record) => {
+        if (!record?.id) {
+            return;
+        }
+        setTestingConnectivityId(record.id);
+        try {
+            const result = await testMcpConnectivity(record.id);
+            notify(result?.message || "杩為�氭�ф祴璇曞畬鎴�");
+            refresh();
+        } catch (error) {
+            notify(error?.message || "杩為�氭�ф祴璇曞け璐�", { type: "error" });
+        } finally {
+            setTestingConnectivityId(null);
+        }
     };
 
     const dialogTitle = {
@@ -227,7 +302,9 @@
                     onView={(id) => openDialog("show", id)}
                     onEdit={(id) => openDialog("edit", id)}
                     onDelete={handleDelete}
+                    onConnectivityTest={handleConnectivityTest}
                     deleting={deleting}
+                    testingConnectivityId={testingConnectivityId}
                 />
             </List>
             <AiConfigDialog
diff --git a/rsf-admin/src/page/system/aiMcpMount/AiMcpMountToolsPanel.jsx b/rsf-admin/src/page/system/aiMcpMount/AiMcpMountToolsPanel.jsx
index 1901478..2a333e9 100644
--- a/rsf-admin/src/page/system/aiMcpMount/AiMcpMountToolsPanel.jsx
+++ b/rsf-admin/src/page/system/aiMcpMount/AiMcpMountToolsPanel.jsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from "react";
+import React, { useEffect, useMemo, useState } from "react";
 import {
     Accordion,
     AccordionDetails,
@@ -9,8 +9,8 @@
     Card,
     CardContent,
     CircularProgress,
-    Divider,
     Grid,
+    MenuItem,
     Stack,
     TextField,
     Typography,
@@ -19,7 +19,102 @@
 import PreviewOutlinedIcon from "@mui/icons-material/PreviewOutlined";
 import ExpandMoreOutlinedIcon from "@mui/icons-material/ExpandMoreOutlined";
 import { useNotify } from "react-admin";
-import { previewMcpTools, testMcpTool } from "@/api/ai/mcpMount";
+import { previewMcpTools, testMcpConnectivity, testMcpTool } from "@/api/ai/mcpMount";
+
+const parseInputSchema = (inputSchema) => {
+    if (!inputSchema) {
+        return { pretty: "", fields: [], required: [], error: "" };
+    }
+    try {
+        const schema = JSON.parse(inputSchema);
+        const properties = schema?.properties || {};
+        const required = Array.isArray(schema?.required) ? schema.required : [];
+        return {
+            pretty: JSON.stringify(schema, null, 2),
+            required,
+            error: "",
+            fields: Object.entries(properties).map(([name, definition]) => ({
+                name,
+                title: definition?.title || name,
+                description: definition?.description || "",
+                type: definition?.type || "string",
+                enumValues: Array.isArray(definition?.enum) ? definition.enum : [],
+            })),
+        };
+    } catch (error) {
+        return {
+            pretty: inputSchema,
+            fields: [],
+            required: [],
+            error: `Input Schema 瑙f瀽澶辫触: ${error.message}`,
+        };
+    }
+};
+
+const normalizeFieldValue = (field, rawValue) => {
+    if (rawValue === "" || rawValue == null) {
+        return undefined;
+    }
+    if (field.type === "integer") {
+        const parsed = Number.parseInt(rawValue, 10);
+        return Number.isNaN(parsed) ? rawValue : parsed;
+    }
+    if (field.type === "number") {
+        const parsed = Number(rawValue);
+        return Number.isNaN(parsed) ? rawValue : parsed;
+    }
+    if (field.type === "boolean") {
+        return rawValue === true || rawValue === "true";
+    }
+    return rawValue;
+};
+
+const buildInputJson = (schemaInfo, fieldValues) => {
+    if (!schemaInfo?.fields?.length) {
+        return "";
+    }
+    const payload = {};
+    schemaInfo.fields.forEach((field) => {
+        const normalized = normalizeFieldValue(field, fieldValues?.[field.name]);
+        if (normalized !== undefined) {
+            payload[field.name] = normalized;
+        }
+    });
+    return JSON.stringify(payload, null, 2);
+};
+
+const readStructuredValues = (schemaInfo, inputJson) => {
+    if (!schemaInfo?.fields?.length || !inputJson || !inputJson.trim()) {
+        return {};
+    }
+    try {
+        const parsed = JSON.parse(inputJson);
+        if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
+            return {};
+        }
+        const values = {};
+        schemaInfo.fields.forEach((field) => {
+            const value = parsed[field.name];
+            if (value === undefined || value === null) {
+                return;
+            }
+            values[field.name] = typeof value === "boolean" ? String(value) : String(value);
+        });
+        return values;
+    } catch (error) {
+        return {};
+    }
+};
+
+const resolveConnectivitySeverity = (healthStatus) => {
+    if (healthStatus === "HEALTHY") {
+        return "success";
+    }
+    if (healthStatus === "UNHEALTHY") {
+        return "error";
+    }
+    return "info";
+};
 
 const AiMcpMountToolsPanel = ({ mountId }) => {
     const notify = useNotify();
@@ -27,14 +122,26 @@
     const [tools, setTools] = useState([]);
     const [error, setError] = useState("");
     const [inputs, setInputs] = useState({});
+    const [structuredInputs, setStructuredInputs] = useState({});
     const [outputs, setOutputs] = useState({});
     const [testingToolName, setTestingToolName] = useState("");
+    const [testingConnectivity, setTestingConnectivity] = useState(false);
+    const [connectivity, setConnectivity] = useState(null);
+
+    const schemaInfoMap = useMemo(() => {
+        return tools.reduce((result, tool) => {
+            result[tool.name] = parseInputSchema(tool.inputSchema);
+            return result;
+        }, {});
+    }, [tools]);
 
     useEffect(() => {
         if (!mountId) {
             setTools([]);
             setInputs({});
+            setStructuredInputs({});
             setOutputs({});
+            setConnectivity(null);
             setError("");
             return;
         }
@@ -48,10 +155,26 @@
             const data = await previewMcpTools(mountId);
             setTools(data);
             setOutputs({});
+            setInputs({});
+            setStructuredInputs({});
         } catch (requestError) {
             setError(requestError.message || "鑾峰彇宸ュ叿鍒楄〃澶辫触");
         } finally {
             setLoading(false);
+        }
+    };
+
+    const handleConnectivityTest = async () => {
+        setTestingConnectivity(true);
+        try {
+            const result = await testMcpConnectivity(mountId);
+            setConnectivity(result);
+            notify(result?.message || "杩為�氭�ф祴璇曞畬鎴�");
+        } catch (requestError) {
+            const message = requestError.message || "杩為�氭�ф祴璇曞け璐�";
+            notify(message, { type: "error" });
+        } finally {
+            setTestingConnectivity(false);
         }
     };
 
@@ -60,6 +183,28 @@
             ...prev,
             [toolName]: value,
         }));
+        setStructuredInputs((prev) => ({
+            ...prev,
+            [toolName]: readStructuredValues(schemaInfoMap[toolName], value),
+        }));
+    };
+
+    const handleStructuredFieldChange = (toolName, fieldName, value) => {
+        const schemaInfo = schemaInfoMap[toolName];
+        setStructuredInputs((prev) => {
+            const nextToolValues = {
+                ...(prev[toolName] || {}),
+                [fieldName]: value,
+            };
+            setInputs((prevInputs) => ({
+                ...prevInputs,
+                [toolName]: buildInputJson(schemaInfo, nextToolValues),
+            }));
+            return {
+                ...prev,
+                [toolName]: nextToolValues,
+            };
+        });
     };
 
     const handleTest = async (toolName) => {
@@ -106,16 +251,33 @@
                     <Box flex={1}>
                         <Typography variant="h6">宸ュ叿棰勮涓庢祴璇�</Typography>
                         <Typography variant="body2" color="text.secondary">
-                            褰撳墠鎸傝浇瑙f瀽鍑虹殑鍏ㄩ儴宸ュ叿閮芥樉绀哄湪杩欓噷锛屽彲鐩存帴杈撳叆 JSON 鍋氭祴璇曘��
+                            鏀寔杩為�氭�ф祴璇曘�佺粨鏋勫寲 Schema 棰勮鍜屾寜杈撳叆鍙傛暟鑷姩鐢熸垚娴嬭瘯琛ㄥ崟銆�
                         </Typography>
                     </Box>
                 </AccordionSummary>
                 <AccordionDetails>
-                    <Stack direction="row" justifyContent="flex-end" alignItems="center" mb={1.5}>
+                    <Stack direction="row" justifyContent="space-between" alignItems="center" mb={1.5} flexWrap="wrap" useFlexGap>
                         <Button size="small" startIcon={<PreviewOutlinedIcon />} onClick={loadTools} disabled={loading}>
                             鍒锋柊宸ュ叿
                         </Button>
+                        <Button
+                            size="small"
+                            variant="outlined"
+                            startIcon={<PlayCircleOutlineOutlinedIcon />}
+                            onClick={handleConnectivityTest}
+                            disabled={testingConnectivity}
+                        >
+                            {testingConnectivity ? "娴嬭瘯涓�..." : "杩為�氭�ф祴璇�"}
+                        </Button>
                     </Stack>
+                    {!!connectivity && (
+                        <Alert severity={resolveConnectivitySeverity(connectivity.healthStatus)} sx={{ mb: 2 }}>
+                            {connectivity.message}
+                            {connectivity.initElapsedMs != null && ` 路 Init ${connectivity.initElapsedMs} ms`}
+                            {connectivity.toolCount != null && ` 路 Tools ${connectivity.toolCount}`}
+                            {connectivity.testedAt && ` 路 ${connectivity.testedAt}`}
+                        </Alert>
+                    )}
                     {loading && (
                         <Box display="flex" justifyContent="center" py={4}>
                             <CircularProgress size={28} />
@@ -130,71 +292,115 @@
                         <Alert severity="info">褰撳墠鎸傝浇鏈В鏋愬嚭浠讳綍宸ュ叿銆�</Alert>
                     )}
                     <Grid container spacing={2}>
-                        {tools.map((tool) => (
-                            <Grid item xs={12} key={tool.name}>
-                                <Accordion defaultExpanded={false} sx={{ borderRadius: 3, overflow: "hidden" }}>
-                                    <AccordionSummary expandIcon={<ExpandMoreOutlinedIcon />}>
-                                        <Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2} width="100%" pr={1}>
-                                            <Box>
-                                                <Typography variant="subtitle1">{tool.name}</Typography>
-                                                <Typography variant="body2" color="text.secondary">
-                                                    {tool.description || "鏆傛棤鎻忚堪"}
-                                                </Typography>
-                                            </Box>
-                                            <Typography variant="caption" color="text.secondary">
-                                                {tool.returnDirect ? "returnDirect" : "normal"}
-                                            </Typography>
-                                        </Stack>
-                                    </AccordionSummary>
-                                    <AccordionDetails>
-                                        <Card variant="outlined" sx={{ borderRadius: 3 }}>
-                                            <CardContent>
-                                                <TextField
-                                                    label="Input Schema"
-                                                    value={tool.inputSchema || ""}
-                                                    fullWidth
-                                                    multiline
-                                                    minRows={5}
-                                                    maxRows={12}
-                                                    InputProps={{ readOnly: true }}
-                                                />
-                                                <TextField
-                                                    label="娴嬭瘯杈撳叆 JSON"
-                                                    value={inputs[tool.name] || ""}
-                                                    onChange={(event) => handleInputChange(tool.name, event.target.value)}
-                                                    fullWidth
-                                                    multiline
-                                                    minRows={5}
-                                                    maxRows={12}
-                                                    sx={{ mt: 2 }}
-                                                    placeholder='渚嬪锛歿"code":"A01"}'
-                                                />
-                                                <Stack direction="row" justifyContent="flex-end" mt={1.5}>
-                                                    <Button
-                                                        variant="contained"
-                                                        startIcon={<PlayCircleOutlineOutlinedIcon />}
-                                                        onClick={() => handleTest(tool.name)}
-                                                        disabled={testingToolName === tool.name}
-                                                    >
-                                                        {testingToolName === tool.name ? "娴嬭瘯涓�..." : "鎵ц娴嬭瘯"}
-                                                    </Button>
+                        {tools.map((tool) => {
+                            const schemaInfo = schemaInfoMap[tool.name] || { pretty: "", fields: [], required: [], error: "" };
+                            const structuredValues = structuredInputs[tool.name] || {};
+                            return (
+                                <Grid item xs={12} key={tool.name}>
+                                    <Accordion defaultExpanded={false} sx={{ borderRadius: 3, overflow: "hidden" }}>
+                                        <AccordionSummary expandIcon={<ExpandMoreOutlinedIcon />}>
+                                            <Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2} width="100%" pr={1}>
+                                                <Box>
+                                                    <Typography variant="subtitle1">{tool.name}</Typography>
+                                                    <Typography variant="body2" color="text.secondary">
+                                                        {tool.description || "鏆傛棤鎻忚堪"}
+                                                    </Typography>
+                                                </Box>
+                                                <Stack direction="row" spacing={1} alignItems="center" flexWrap="wrap" useFlexGap>
+                                                    <Typography variant="caption" color="text.secondary">
+                                                        {schemaInfo.fields.length} 涓弬鏁�
+                                                    </Typography>
+                                                    <Typography variant="caption" color="text.secondary">
+                                                        {tool.returnDirect ? "returnDirect" : "normal"}
+                                                    </Typography>
                                                 </Stack>
-                                                <TextField
-                                                    label="娴嬭瘯缁撴灉"
-                                                    value={outputs[tool.name] || ""}
-                                                    fullWidth
-                                                    multiline
-                                                    minRows={5}
-                                                    maxRows={16}
-                                                    sx={{ mt: 2 }}
-                                                    InputProps={{ readOnly: true }}
-                                                />
-                                            </CardContent>
-                                        </Card>
-                                    </AccordionDetails>
-                                </Accordion>
-                            </Grid>
-                        ))}
+                                            </Stack>
+                                        </AccordionSummary>
+                                        <AccordionDetails>
+                                            <Card variant="outlined" sx={{ borderRadius: 3 }}>
+                                                <CardContent>
+                                                    {!!schemaInfo.error && (
+                                                        <Alert severity="warning" sx={{ mb: 2 }}>
+                                                            {schemaInfo.error}
+                                                        </Alert>
+                                                    )}
+                                                    <TextField
+                                                        label="鏍煎紡鍖� Input Schema"
+                                                        value={schemaInfo.pretty || tool.inputSchema || ""}
+                                                        fullWidth
+                                                        multiline
+                                                        minRows={6}
+                                                        maxRows={16}
+                                                        InputProps={{ readOnly: true }}
+                                                    />
+                                                    {!!schemaInfo.fields.length && (
+                                                        <Grid container spacing={2} sx={{ mt: 0.5 }}>
+                                                            {schemaInfo.fields.map((field) => (
+                                                                <Grid item xs={12} md={field.type === "boolean" ? 6 : 12} key={`${tool.name}-${field.name}`}>
+                                                                    <TextField
+                                                                        select={field.type === "boolean" || field.enumValues.length > 0}
+                                                                        type={field.type === "integer" || field.type === "number" ? "number" : "text"}
+                                                                        label={`${field.title}${schemaInfo.required.includes(field.name) ? " *" : ""}`}
+                                                                        value={structuredValues[field.name] ?? ""}
+                                                                        onChange={(event) => handleStructuredFieldChange(tool.name, field.name, event.target.value)}
+                                                                        fullWidth
+                                                                        helperText={field.description || field.type}
+                                                                        sx={{ mt: 2 }}
+                                                                    >
+                                                                        {field.type === "boolean" && (
+                                                                            [
+                                                                                <MenuItem key="true" value="true">true</MenuItem>,
+                                                                                <MenuItem key="false" value="false">false</MenuItem>,
+                                                                            ]
+                                                                        )}
+                                                                        {field.type !== "boolean" && field.enumValues.map((value) => (
+                                                                            <MenuItem key={value} value={String(value)}>
+                                                                                {String(value)}
+                                                                            </MenuItem>
+                                                                        ))}
+                                                                    </TextField>
+                                                                </Grid>
+                                                            ))}
+                                                        </Grid>
+                                                    )}
+                                                    <TextField
+                                                        label="娴嬭瘯杈撳叆 JSON"
+                                                        value={inputs[tool.name] || ""}
+                                                        onChange={(event) => handleInputChange(tool.name, event.target.value)}
+                                                        fullWidth
+                                                        multiline
+                                                        minRows={5}
+                                                        maxRows={12}
+                                                        sx={{ mt: 2 }}
+                                                        placeholder='渚嬪锛歿"code":"A01"}'
+                                                    />
+                                                    <Stack direction="row" justifyContent="flex-end" mt={1.5}>
+                                                        <Button
+                                                            variant="contained"
+                                                            startIcon={<PlayCircleOutlineOutlinedIcon />}
+                                                            onClick={() => handleTest(tool.name)}
+                                                            disabled={testingToolName === tool.name}
+                                                        >
+                                                            {testingToolName === tool.name ? "娴嬭瘯涓�..." : "鎵ц娴嬭瘯"}
+                                                        </Button>
+                                                    </Stack>
+                                                    <TextField
+                                                        label="娴嬭瘯缁撴灉"
+                                                        value={outputs[tool.name] || ""}
+                                                        fullWidth
+                                                        multiline
+                                                        minRows={5}
+                                                        maxRows={16}
+                                                        sx={{ mt: 2 }}
+                                                        InputProps={{ readOnly: true }}
+                                                    />
+                                                </CardContent>
+                                            </Card>
+                                        </AccordionDetails>
+                                    </Accordion>
+                                </Grid>
+                            );
+                        })}
                     </Grid>
                 </AccordionDetails>
             </Accordion>
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/config/AiDefaults.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/config/AiDefaults.java
index 7cc00eb..fe09412 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/config/AiDefaults.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/config/AiDefaults.java
@@ -14,6 +14,9 @@
     public static final String MCP_BUILTIN_RSF_WMS_STOCK = "RSF_WMS_STOCK";
     public static final String MCP_BUILTIN_RSF_WMS_TASK = "RSF_WMS_TASK";
     public static final String MCP_BUILTIN_RSF_WMS_BASE = "RSF_WMS_BASE";
+    public static final String MCP_HEALTH_NOT_TESTED = "NOT_TESTED";
+    public static final String MCP_HEALTH_HEALTHY = "HEALTHY";
+    public static final String MCP_HEALTH_UNHEALTHY = "UNHEALTHY";
     public static final long SSE_TIMEOUT_MS = 0L;
     public static final int DEFAULT_TIMEOUT_MS = 60000;
     public static final double DEFAULT_TEMPERATURE = 0.7D;
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiMcpMountController.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiMcpMountController.java
index 3525e6e..90ba38f 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiMcpMountController.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiMcpMountController.java
@@ -71,6 +71,12 @@
     }
 
     @PreAuthorize("hasAuthority('system:aiMcpMount:update')")
+    @PostMapping("/aiMcpMount/{id}/connectivity/test")
+    public R testConnectivity(@PathVariable("id") Long id) {
+        return R.ok().add(aiMcpMountService.testConnectivity(id, getLoginUserId(), getTenantId()));
+    }
+
+    @PreAuthorize("hasAuthority('system:aiMcpMount:update')")
     @PostMapping("/aiMcpMount/{id}/tool/test")
     public R testTool(@PathVariable("id") Long id, @RequestBody AiMcpToolTestRequest request) {
         return R.ok().add(aiMcpMountService.testTool(id, getLoginUserId(), getTenantId(), request));
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiMcpConnectivityTestDto.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiMcpConnectivityTestDto.java
new file mode 100644
index 0000000..2bd80a6
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiMcpConnectivityTestDto.java
@@ -0,0 +1,23 @@
+package com.vincent.rsf.server.ai.dto;
+
+import lombok.Builder;
+import lombok.Data;
+
+@Data
+@Builder
+public class AiMcpConnectivityTestDto {
+
+    private Long mountId;
+
+    private String mountName;
+
+    private String healthStatus;
+
+    private String message;
+
+    private Long initElapsedMs;
+
+    private Integer toolCount;
+
+    private String testedAt;
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/entity/AiMcpMount.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/entity/AiMcpMount.java
index ffaec3c..04a7796 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/entity/AiMcpMount.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/entity/AiMcpMount.java
@@ -54,6 +54,20 @@
     @ApiModelProperty(value = "瓒呮椂鏃堕棿")
     private Integer requestTimeoutMs;
 
+    @ApiModelProperty(value = "鍋ュ悍鐘舵��")
+    private String healthStatus;
+
+    @ApiModelProperty(value = "鏈�杩戞祴璇曟椂闂�")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date lastTestTime;
+
+    @ApiModelProperty(value = "鏈�杩戞祴璇曚俊鎭�")
+    private String lastTestMessage;
+
+    @ApiModelProperty(value = "鏈�杩戝垵濮嬪寲鑰楁椂")
+    private Long lastInitElapsedMs;
+
     @ApiModelProperty(value = "鎺掑簭")
     private Integer sort;
 
@@ -96,6 +110,17 @@
         return this.status == 1;
     }
 
+    public String getHealthStatus$() {
+        return this.healthStatus;
+    }
+
+    public String getLastTestTime$() {
+        if (this.lastTestTime == null) {
+            return "";
+        }
+        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(this.lastTestTime);
+    }
+
     public String getCreateTime$() {
         if (this.createTime == null) {
             return "";
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiMcpMountService.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiMcpMountService.java
index efac400..1f9f5bb 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiMcpMountService.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiMcpMountService.java
@@ -1,6 +1,7 @@
 package com.vincent.rsf.server.ai.service;
 
 import com.baomidou.mybatisplus.extension.service.IService;
+import com.vincent.rsf.server.ai.dto.AiMcpConnectivityTestDto;
 import com.vincent.rsf.server.ai.dto.AiMcpToolPreviewDto;
 import com.vincent.rsf.server.ai.dto.AiMcpToolTestDto;
 import com.vincent.rsf.server.ai.dto.AiMcpToolTestRequest;
@@ -18,5 +19,7 @@
 
     List<AiMcpToolPreviewDto> previewTools(Long mountId, Long userId, Long tenantId);
 
+    AiMcpConnectivityTestDto testConnectivity(Long mountId, Long userId, Long tenantId);
+
     AiMcpToolTestDto testTool(Long mountId, Long userId, Long tenantId, AiMcpToolTestRequest request);
 }
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiMcpMountServiceImpl.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiMcpMountServiceImpl.java
index 7221714..5bde192 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiMcpMountServiceImpl.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiMcpMountServiceImpl.java
@@ -1,10 +1,12 @@
 package com.vincent.rsf.server.ai.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.fasterxml.jackson.databind.ObjectMapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.vincent.rsf.framework.exception.CoolException;
 import com.vincent.rsf.server.ai.config.AiDefaults;
+import com.vincent.rsf.server.ai.dto.AiMcpConnectivityTestDto;
 import com.vincent.rsf.server.ai.dto.AiMcpToolPreviewDto;
 import com.vincent.rsf.server.ai.dto.AiMcpToolTestDto;
 import com.vincent.rsf.server.ai.dto.AiMcpToolTestRequest;
@@ -22,6 +24,7 @@
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Date;
 import java.util.List;
 import java.util.Map;
 
@@ -67,20 +70,50 @@
     @Override
     public List<AiMcpToolPreviewDto> previewTools(Long mountId, Long userId, Long tenantId) {
         AiMcpMount mount = requireMount(mountId, tenantId);
+        long startedAt = System.currentTimeMillis();
         try (McpMountRuntimeFactory.McpMountRuntime runtime = mcpMountRuntimeFactory.create(List.of(mount), userId)) {
-            List<AiMcpToolPreviewDto> tools = new ArrayList<>();
-            for (ToolCallback callback : runtime.getToolCallbacks()) {
-                if (callback == null || callback.getToolDefinition() == null) {
-                    continue;
-                }
-                tools.add(AiMcpToolPreviewDto.builder()
-                        .name(callback.getToolDefinition().name())
-                        .description(callback.getToolDefinition().description())
-                        .inputSchema(callback.getToolDefinition().inputSchema())
-                        .returnDirect(callback.getToolMetadata() == null ? null : callback.getToolMetadata().returnDirect())
-                        .build());
+            List<AiMcpToolPreviewDto> tools = buildToolPreviewDtos(runtime.getToolCallbacks());
+            if (!runtime.getErrors().isEmpty()) {
+                String message = String.join("锛�", runtime.getErrors());
+                updateHealthStatus(mount.getId(), AiDefaults.MCP_HEALTH_UNHEALTHY, message, System.currentTimeMillis() - startedAt);
+                throw new CoolException(message);
             }
+            updateHealthStatus(mount.getId(), AiDefaults.MCP_HEALTH_HEALTHY,
+                    "宸ュ叿瑙f瀽鎴愬姛锛屽叡 " + tools.size() + " 涓伐鍏�", System.currentTimeMillis() - startedAt);
             return tools;
+        } catch (CoolException e) {
+            throw e;
+        } catch (Exception e) {
+            updateHealthStatus(mount.getId(), AiDefaults.MCP_HEALTH_UNHEALTHY,
+                    "宸ュ叿瑙f瀽澶辫触: " + e.getMessage(), System.currentTimeMillis() - startedAt);
+            throw new CoolException("鑾峰彇宸ュ叿鍒楄〃澶辫触: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public AiMcpConnectivityTestDto testConnectivity(Long mountId, Long userId, Long tenantId) {
+        AiMcpMount mount = requireMount(mountId, tenantId);
+        long startedAt = System.currentTimeMillis();
+        try (McpMountRuntimeFactory.McpMountRuntime runtime = mcpMountRuntimeFactory.create(List.of(mount), userId)) {
+            long elapsedMs = System.currentTimeMillis() - startedAt;
+            if (!runtime.getErrors().isEmpty()) {
+                String message = String.join("锛�", runtime.getErrors());
+                updateHealthStatus(mount.getId(), AiDefaults.MCP_HEALTH_UNHEALTHY, message, elapsedMs);
+                AiMcpMount latest = requireMount(mount.getId(), tenantId);
+                return buildConnectivityDto(latest, message, elapsedMs, runtime.getToolCallbacks().length);
+            }
+            String message = "杩為�氭�ф祴璇曟垚鍔燂紝瑙f瀽鍑� " + runtime.getToolCallbacks().length + " 涓伐鍏�";
+            updateHealthStatus(mount.getId(), AiDefaults.MCP_HEALTH_HEALTHY, message, elapsedMs);
+            AiMcpMount latest = requireMount(mount.getId(), tenantId);
+            return buildConnectivityDto(latest, message, elapsedMs, runtime.getToolCallbacks().length);
+        } catch (CoolException e) {
+            throw e;
+        } catch (Exception e) {
+            long elapsedMs = System.currentTimeMillis() - startedAt;
+            String message = "杩為�氭�ф祴璇曞け璐�: " + e.getMessage();
+            updateHealthStatus(mount.getId(), AiDefaults.MCP_HEALTH_UNHEALTHY, message, elapsedMs);
+            AiMcpMount latest = requireMount(mount.getId(), tenantId);
+            return buildConnectivityDto(latest, message, elapsedMs, 0);
         }
     }
 
@@ -107,6 +140,7 @@
             throw new CoolException("宸ュ叿杈撳叆 JSON 鏍煎紡閿欒: " + e.getMessage());
         }
         AiMcpMount mount = requireMount(mountId, tenantId);
+        long startedAt = System.currentTimeMillis();
         try (McpMountRuntimeFactory.McpMountRuntime runtime = mcpMountRuntimeFactory.create(List.of(mount), userId)) {
             ToolCallback callback = Arrays.stream(runtime.getToolCallbacks())
                     .filter(item -> item != null && item.getToolDefinition() != null)
@@ -117,11 +151,21 @@
                     request.getInputJson(),
                     new ToolContext(Map.of("userId", userId, "tenantId", tenantId, "mountId", mountId))
             );
+            updateHealthStatus(mount.getId(), AiDefaults.MCP_HEALTH_HEALTHY,
+                    "宸ュ叿娴嬭瘯鎴愬姛: " + request.getToolName(), System.currentTimeMillis() - startedAt);
             return AiMcpToolTestDto.builder()
                     .toolName(request.getToolName())
                     .inputJson(request.getInputJson())
                     .output(output)
                     .build();
+        } catch (CoolException e) {
+            updateHealthStatus(mount.getId(), AiDefaults.MCP_HEALTH_UNHEALTHY,
+                    "宸ュ叿娴嬭瘯澶辫触: " + e.getMessage(), System.currentTimeMillis() - startedAt);
+            throw e;
+        } catch (Exception e) {
+            updateHealthStatus(mount.getId(), AiDefaults.MCP_HEALTH_UNHEALTHY,
+                    "宸ュ叿娴嬭瘯澶辫触: " + e.getMessage(), System.currentTimeMillis() - startedAt);
+            throw new CoolException("宸ュ叿娴嬭瘯澶辫触: " + e.getMessage());
         }
     }
 
@@ -137,6 +181,9 @@
         }
         if (aiMcpMount.getStatus() == null) {
             aiMcpMount.setStatus(StatusType.ENABLE.val);
+        }
+        if (!StringUtils.hasText(aiMcpMount.getHealthStatus())) {
+            aiMcpMount.setHealthStatus(AiDefaults.MCP_HEALTH_NOT_TESTED);
         }
     }
 
@@ -226,4 +273,41 @@
             throw new CoolException("褰撳墠绉熸埛涓嶅瓨鍦�");
         }
     }
+
+    private List<AiMcpToolPreviewDto> buildToolPreviewDtos(ToolCallback[] callbacks) {
+        List<AiMcpToolPreviewDto> tools = new ArrayList<>();
+        for (ToolCallback callback : callbacks) {
+            if (callback == null || callback.getToolDefinition() == null) {
+                continue;
+            }
+            tools.add(AiMcpToolPreviewDto.builder()
+                    .name(callback.getToolDefinition().name())
+                    .description(callback.getToolDefinition().description())
+                    .inputSchema(callback.getToolDefinition().inputSchema())
+                    .returnDirect(callback.getToolMetadata() == null ? null : callback.getToolMetadata().returnDirect())
+                    .build());
+        }
+        return tools;
+    }
+
+    private void updateHealthStatus(Long mountId, String healthStatus, String message, Long initElapsedMs) {
+        this.update(new LambdaUpdateWrapper<AiMcpMount>()
+                .eq(AiMcpMount::getId, mountId)
+                .set(AiMcpMount::getHealthStatus, healthStatus)
+                .set(AiMcpMount::getLastTestTime, new Date())
+                .set(AiMcpMount::getLastTestMessage, message)
+                .set(AiMcpMount::getLastInitElapsedMs, initElapsedMs));
+    }
+
+    private AiMcpConnectivityTestDto buildConnectivityDto(AiMcpMount mount, String message, Long initElapsedMs, Integer toolCount) {
+        return AiMcpConnectivityTestDto.builder()
+                .mountId(mount.getId())
+                .mountName(mount.getName())
+                .healthStatus(mount.getHealthStatus())
+                .message(message)
+                .initElapsedMs(initElapsedMs)
+                .toolCount(toolCount)
+                .testedAt(mount.getLastTestTime$())
+                .build();
+    }
 }
diff --git a/version/db/ai_feature.sql b/version/db/ai_feature.sql
index 898732a..35b3112 100644
--- a/version/db/ai_feature.sql
+++ b/version/db/ai_feature.sql
@@ -56,6 +56,10 @@
   `env_json` text COMMENT '鐜鍙橀噺 JSON',
   `headers_json` text COMMENT '璇锋眰澶� JSON',
   `request_timeout_ms` int(11) DEFAULT NULL COMMENT '璇锋眰瓒呮椂鏃堕棿',
+  `health_status` varchar(32) DEFAULT 'NOT_TESTED' COMMENT '鍋ュ悍鐘舵��',
+  `last_test_time` datetime DEFAULT NULL COMMENT '鏈�杩戞祴璇曟椂闂�',
+  `last_test_message` varchar(500) DEFAULT NULL COMMENT '鏈�杩戞祴璇曚俊鎭�',
+  `last_init_elapsed_ms` bigint(20) DEFAULT NULL COMMENT '鏈�杩戝垵濮嬪寲鑰楁椂',
   `sort` int(11) DEFAULT '0' COMMENT '鎺掑簭',
   `tenant_id` bigint(20) DEFAULT NULL COMMENT '绉熸埛',
   `status` int(11) DEFAULT '1' COMMENT '鐘舵��',
@@ -121,6 +125,74 @@
 EXECUTE builtin_code_stmt;
 DEALLOCATE PREPARE builtin_code_stmt;
 
+SET @mcp_health_status_exists := (
+  SELECT COUNT(1)
+  FROM `information_schema`.`COLUMNS`
+  WHERE `TABLE_SCHEMA` = DATABASE()
+    AND `TABLE_NAME` = 'sys_ai_mcp_mount'
+    AND `COLUMN_NAME` = 'health_status'
+);
+SET @mcp_health_status_sql := IF(
+  @mcp_health_status_exists = 0,
+  'ALTER TABLE `sys_ai_mcp_mount` ADD COLUMN `health_status` varchar(32) DEFAULT ''NOT_TESTED'' COMMENT ''鍋ュ悍鐘舵��'' AFTER `request_timeout_ms`',
+  'SELECT 1'
+);
+PREPARE mcp_health_status_stmt FROM @mcp_health_status_sql;
+EXECUTE mcp_health_status_stmt;
+DEALLOCATE PREPARE mcp_health_status_stmt;
+
+SET @mcp_last_test_time_exists := (
+  SELECT COUNT(1)
+  FROM `information_schema`.`COLUMNS`
+  WHERE `TABLE_SCHEMA` = DATABASE()
+    AND `TABLE_NAME` = 'sys_ai_mcp_mount'
+    AND `COLUMN_NAME` = 'last_test_time'
+);
+SET @mcp_last_test_time_sql := IF(
+  @mcp_last_test_time_exists = 0,
+  'ALTER TABLE `sys_ai_mcp_mount` ADD COLUMN `last_test_time` datetime DEFAULT NULL COMMENT ''鏈�杩戞祴璇曟椂闂�'' AFTER `health_status`',
+  'SELECT 1'
+);
+PREPARE mcp_last_test_time_stmt FROM @mcp_last_test_time_sql;
+EXECUTE mcp_last_test_time_stmt;
+DEALLOCATE PREPARE mcp_last_test_time_stmt;
+
+SET @mcp_last_test_message_exists := (
+  SELECT COUNT(1)
+  FROM `information_schema`.`COLUMNS`
+  WHERE `TABLE_SCHEMA` = DATABASE()
+    AND `TABLE_NAME` = 'sys_ai_mcp_mount'
+    AND `COLUMN_NAME` = 'last_test_message'
+);
+SET @mcp_last_test_message_sql := IF(
+  @mcp_last_test_message_exists = 0,
+  'ALTER TABLE `sys_ai_mcp_mount` ADD COLUMN `last_test_message` varchar(500) DEFAULT NULL COMMENT ''鏈�杩戞祴璇曚俊鎭�'' AFTER `last_test_time`',
+  'SELECT 1'
+);
+PREPARE mcp_last_test_message_stmt FROM @mcp_last_test_message_sql;
+EXECUTE mcp_last_test_message_stmt;
+DEALLOCATE PREPARE mcp_last_test_message_stmt;
+
+SET @mcp_last_init_elapsed_exists := (
+  SELECT COUNT(1)
+  FROM `information_schema`.`COLUMNS`
+  WHERE `TABLE_SCHEMA` = DATABASE()
+    AND `TABLE_NAME` = 'sys_ai_mcp_mount'
+    AND `COLUMN_NAME` = 'last_init_elapsed_ms'
+);
+SET @mcp_last_init_elapsed_sql := IF(
+  @mcp_last_init_elapsed_exists = 0,
+  'ALTER TABLE `sys_ai_mcp_mount` ADD COLUMN `last_init_elapsed_ms` bigint(20) DEFAULT NULL COMMENT ''鏈�杩戝垵濮嬪寲鑰楁椂'' AFTER `last_test_message`',
+  'SELECT 1'
+);
+PREPARE mcp_last_init_elapsed_stmt FROM @mcp_last_init_elapsed_sql;
+EXECUTE mcp_last_init_elapsed_stmt;
+DEALLOCATE PREPARE mcp_last_init_elapsed_stmt;
+
+UPDATE `sys_ai_mcp_mount`
+SET `health_status` = 'NOT_TESTED'
+WHERE `health_status` IS NULL OR `health_status` = '';
+
 SET @chat_session_pinned_exists := (
   SELECT COUNT(1)
   FROM `information_schema`.`COLUMNS`

--
Gitblit v1.9.1