This is an automated email from the ASF dual-hosted git repository.

achao pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu-dashboard.git


The following commit(s) were added to refs/heads/master by this push:
     new f0199666 [Feat] mcp server (#528)
f0199666 is described below

commit f0199666384fd7c92e15c8dbc8726bb32d171438
Author: aias00 <rok...@163.com>
AuthorDate: Tue Jul 15 10:28:27 2025 +0800

    [Feat] mcp server (#528)
---
 .github/workflows/build.yml               |    4 +-
 .gitignore                                |    3 +
 src/common/router.js                      |    7 +
 src/locales/en-US.json                    |   25 +
 src/locales/zh-CN.json                    |   25 +
 src/models/mcpServer.js                   |   85 ++
 src/routes/Plugin/Common/Selector.js      |    2 +-
 src/routes/Plugin/McpServer/ToolsModal.js |  411 +++++++++
 src/routes/Plugin/McpServer/index.js      | 1385 +++++++++++++++++++++++++++++
 src/services/api.js                       |   33 +
 10 files changed, 1977 insertions(+), 3 deletions(-)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 02afdb7d..131a7eae 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -13,7 +13,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        node-version: [12.x, 14.x, 16.x, 18.x]
+        node-version: [12.x, 14.x, 16.x, 18.x, 20.x]
     steps:
       - uses: actions/checkout@v2
       - name: Use Node.js ${{ matrix.node-version }}
@@ -22,5 +22,5 @@ jobs:
           node-version: ${{ matrix.node-version }}
       - run: npm install
       - run: npm run lint
-        if: matrix.node-version == '18.x'
+        if: matrix.node-version == '20.x'
       - run: npm run build --if-present
diff --git a/.gitignore b/.gitignore
index 9de6675e..24ba8043 100755
--- a/.gitignore
+++ b/.gitignore
@@ -24,3 +24,6 @@ package-lock.json
 # visual studio
 .history
 dist/*
+
+# Private individual user cursor rules
+.cursor/rules/_*.mdc
diff --git a/src/common/router.js b/src/common/router.js
index 3847aa11..b56037c2 100644
--- a/src/common/router.js
+++ b/src/common/router.js
@@ -95,6 +95,13 @@ export const getRouterData = (app) => {
         () => import("../layouts/BasicLayout"),
       ),
     },
+    "/plug/Mcp/mcpServer": {
+      component: dynamicWrapper(
+        app,
+        ["mcpServer"],
+        () => import("../routes/Plugin/McpServer"),
+      ),
+    },
     "/home": {
       component: dynamicWrapper(app, [], () => import("../routes/Home")),
     },
diff --git a/src/locales/en-US.json b/src/locales/en-US.json
index 4a436f44..70880969 100644
--- a/src/locales/en-US.json
+++ b/src/locales/en-US.json
@@ -24,11 +24,23 @@
   "SHENYU.COMMON.SURE": "Sure",
   "SHENYU.COMMON.CALCEL": "Cancel",
   "SHENYU.COMMON.RULE.NAME": "RuleName",
+  "SHENYU.COMMON.TOOL.NAME": "ToolName",
+  "SHENYU.COMMON.RULE.HANDLE": "Handle",
+  "SHENYU.COMMON.TOOL.REQUESTMETHOD": "RequestMethod",
+  "SHENYU.COMMON.TOOL.REQUESTURI": "RequestURI",
+  "SHENYU.COMMON.TOOL.REQUESTPARAMS": "RequestParams",
+  "SHENYU.COMMON.TOOL.REQUESTCONFIG": "RequestConfig",
   "SHENYU.COMMON.SYN": "Synchronous",
+  "SHENYU.COMMON.PREVIEW": "Preview",
   "SHENYU.COMMON.ADD.RULE": "Add",
+  "SHENYU.COMMON.ADD.TOOL": "Add",
   "SHENYU.COMMON.ADD": "Add",
   "SHENYU.COMMON.COPY": "Copy",
   "SHENYU.COMMON.INPUTNAME": "InputName",
+  "SHENYU.COMMON.INPUTDESCRIPTION": "InputDescription",
+  "SHENYU.COMMON.INPUTREQUESTCONFIG": "InputRequestConfig",
+  "SHENYU.COMMON.INPUTREQUESTMETHOD": "InputRequestMethod",
+  "SHENYU.COMMON.INPUTREQUESTURI": "InputRequestURI",
   "SHENYU.COMMON.TYPE": "Type",
   "SHENYU.COMMON.INPUTTYPE": "InputType",
   "SHENYU.COMMON.SELECTOR.TYPE.FULL": "full",
@@ -37,6 +49,10 @@
   "SHENYU.COMMON.MATCHTYPE": "MatchType",
   "SHENYU.COMMON.INPUTMATCHTYPE": "MatchMode",
   "SHENYU.COMMON.CONDITION": "Conditions",
+  "SHENYU.COMMON.PARAMETER": "Parameter",
+  "SHENYU.COMMON.PARAMETER.NAME": "ParamName",
+  "SHENYU.COMMON.PARAMETER.TYPE": "ParamType",
+  "SHENYU.COMMON.PARAMETER.DESCRIPTION": "ParamDescription",
   "SHENYU.COMMON.DEAL": "Handler",
   "SHENYU.COMMON.DEAL.COMPONENT": "Component Handler",
   "SHENYU.COMMON.DEAL.CUSTOM": "Custom Handler",
@@ -81,15 +97,23 @@
   "SHENYU.MENU.SYSTEM.MANAGMENT.NAMESPACE": "Namespace",
   "SHENYU.MENU.CONFIG.MANAGMENT": "BasicConfig",
   "SHENYU.PLUGIN.SELECTOR.LIST.TITLE": "SelectorList",
+  "SHENYU.PLUGIN.SERVER.LIST.TITLE": "ServerList",
   "SHENYU.PLUGIN.SELECTOR.LIST.ADD": "Add",
   "SHENYU.PLUGIN.SELECTOR.BATCH.OPENED": "Batch Opened",
   "SHENYU.PLUGIN.SELECTOR.BATCH.CLOSED": "Batch Closed",
   "SHENYU.PLUGIN.SELECTOR.RULE.LIST": "RulesList",
+  "SHENYU.PLUGIN.SELECTOR.TOOL.LIST": "ToolList",
   "SHENYU.PLUGIN.SELECTOR.LIST.COLUMN.NAME": "Name",
+  "SHENYU.PLUGIN.SELECTOR.LIST.COLUMN.DESCRIPTION": "Description",
+  "SHENYU.PLUGIN.SELECTOR.LIST.COLUMN.REQUESTCONFIG": "RequestConfig",
+  "SHENYU.PLUGIN.SELECTOR.LIST.COLUMN.REQUESTMETHOD": "RequestMethod",
+  "SHENYU.PLUGIN.SELECTOR.LIST.COLUMN.REQUESTURI": "RequestURI",
   "SHENYU.PLUGIN.SEARCH.SELECTOR.NAME": "Name",
   "SHENYU.PLUGIN.SEARCH.RULE.NAME": "RuleName",
+  "SHENYU.PLUGIN.SEARCH.TOOL.NAME": "ToolName",
   "SHENYU.PLUGIN.SEARCH.RULE.COPY": "Copy Rule",
   "SHENYU.SELECTOR.NAME": "Selector",
+  "SHENYU.SERVER.NAME": "Server",
   "SHENYU.SELECTOR.COPY": "Copy Selector",
   "SHENYU.SELECTOR.CONTINUE": "Continued",
   "SHENYU.SELECTOR.PRINTLOG": "PrintLogs",
@@ -100,6 +124,7 @@
   "SHENYU.SELECTOR.INPUTORDER": "You can fill in the numeric flag execution 
order between 1 and 1000",
   "SHENYU.SELECTOR.SOURCE.PLACEHOLDER": "Please select the selector you want 
to copy",
   "SHENYU.RULE.NAME": "Rules",
+  "SHENYU.TOOL.NAME": "Tools",
   "SHENYU.RULE.SOURCE.PLACEHOLDER": "Please select the rule you want to copy",
   "SHENYU.COMMON.LOAD": "LoadPlugins",
   "SHENYU.COMMON.LOADSTRATEGY": "LoadStrategy",
diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json
index dc3e33f5..27fe04ac 100644
--- a/src/locales/zh-CN.json
+++ b/src/locales/zh-CN.json
@@ -23,11 +23,23 @@
   "SHENYU.COMMON.SURE": "确认",
   "SHENYU.COMMON.CALCEL": "取消",
   "SHENYU.COMMON.RULE.NAME": "规则名称",
+  "SHENYU.COMMON.TOOL.NAME": "工具名称",
+  "SHENYU.COMMON.RULE.HANDLE": "处理",
+  "SHENYU.COMMON.TOOL.REQUESTMETHOD": "请求方法",
+  "SHENYU.COMMON.TOOL.REQUESTURI": "请求URI",
+  "SHENYU.COMMON.TOOL.REQUESTPARAMS": "请求参数",
+  "SHENYU.COMMON.TOOL.REQUESTCONFIG": "请求配置",
   "SHENYU.COMMON.SYN": "同步自定义",
+  "SHENYU.COMMON.PREVIEW": "预览",
   "SHENYU.COMMON.ADD.RULE": "添加规则",
+  "SHENYU.COMMON.ADD.TOOL": "添加工具",
   "SHENYU.COMMON.ADD": "新增",
   "SHENYU.COMMON.COPY": "复制",
   "SHENYU.COMMON.INPUTNAME": "请输入名称",
+  "SHENYU.COMMON.INPUTDESCRIPTION": "请输入描述",
+  "SHENYU.COMMON.INPUTREQUESTCONFIG": "请输入请求配置",
+  "SHENYU.COMMON.INPUTREQUESTMETHOD": "请输入请求方法",
+  "SHENYU.COMMON.INPUTREQUESTURI": "请输入请求URI",
   "SHENYU.COMMON.TYPE": "类型",
   "SHENYU.COMMON.INPUTTYPE": "请输入类型",
   "SHENYU.COMMON.SELECTOR.TYPE.FULL": "全流量",
@@ -36,6 +48,10 @@
   "SHENYU.COMMON.MATCHTYPE": "匹配方式",
   "SHENYU.COMMON.INPUTMATCHTYPE": "请选择匹配方式",
   "SHENYU.COMMON.CONDITION": "条件",
+  "SHENYU.COMMON.PARAMETER": "参数",
+  "SHENYU.COMMON.PARAMETER.NAME": "参数名称",
+  "SHENYU.COMMON.PARAMETER.TYPE": "参数类型",
+  "SHENYU.COMMON.PARAMETER.DESCRIPTION": "参数描述",
   "SHENYU.COMMON.DEAL": "处理",
   "SHENYU.COMMON.DEAL.COMPONENT": "组件处理",
   "SHENYU.COMMON.DEAL.CUSTOM": "自定义处理",
@@ -82,15 +98,23 @@
   "SHENYU.MENU.CONFIG.MANAGMENT": "基础配置",
   "SHENYU.MENU.SYSTEM.MANAGMENT.NAMESPACE": "命名空间管理",
   "SHENYU.PLUGIN.SELECTOR.LIST.TITLE": "选择器列表",
+  "SHENYU.PLUGIN.SERVER.LIST.TITLE": "服务列表",
   "SHENYU.PLUGIN.SELECTOR.LIST.ADD": "添加选择器",
   "SHENYU.PLUGIN.SELECTOR.BATCH.OPENED": "批量开启",
   "SHENYU.PLUGIN.SELECTOR.BATCH.CLOSED": "批量关闭",
   "SHENYU.PLUGIN.SELECTOR.RULE.LIST": "选择器规则列表",
+  "SHENYU.PLUGIN.SELECTOR.TOOL.LIST": "选择器工具列表",
   "SHENYU.PLUGIN.SELECTOR.LIST.COLUMN.NAME": "名称",
+  "SHENYU.PLUGIN.SELECTOR.LIST.COLUMN.DESCRIPTION": "描述",
+  "SHENYU.PLUGIN.SELECTOR.LIST.COLUMN.REQUESTCONFIG": "请求配置",
+  "SHENYU.PLUGIN.SELECTOR.LIST.COLUMN.REQUESTMETHOD": "请求方法",
+  "SHENYU.PLUGIN.SELECTOR.LIST.COLUMN.REQUESTURI": "请求URI",
   "SHENYU.PLUGIN.SEARCH.SELECTOR.NAME": "选择器名称",
   "SHENYU.PLUGIN.SEARCH.RULE.NAME": "规则名称",
+  "SHENYU.PLUGIN.SEARCH.TOOL.NAME": "工具名称",
   "SHENYU.PLUGIN.SEARCH.RULE.COPY": "复制规则",
   "SHENYU.SELECTOR.NAME": "选择器",
+  "SHENYU.SERVER.NAME": "服务",
   "SHENYU.SELECTOR.COPY": "复制选择器",
   "SHENYU.SELECTOR.CONTINUE": "继续后续选择器",
   "SHENYU.SELECTOR.PRINTLOG": "打印日志",
@@ -101,6 +125,7 @@
   "SHENYU.SELECTOR.INPUTORDER": "可以填写1-1000之间的数字标志执行先后顺序",
   "SHENYU.SELECTOR.SOURCE.PLACEHOLDER": "请选择需要复制的选择器",
   "SHENYU.RULE.NAME": "规则",
+  "SHENYU.TOOL.NAME": "工具",
   "SHENYU.RULE.SOURCE.PLACEHOLDER": "请选择需要复制的规则",
   "SHENYU.COMMON.LOAD": "负载",
   "SHENYU.COMMON.LOADSTRATEGY": "负载策略",
diff --git a/src/models/mcpServer.js b/src/models/mcpServer.js
new file mode 100644
index 00000000..2390bf1a
--- /dev/null
+++ b/src/models/mcpServer.js
@@ -0,0 +1,85 @@
+import { message } from "antd";
+import {
+  fetchMcpServer,
+  addMcpServer,
+  updateMcpServer,
+  deleteMcpServer,
+} from "../services/api";
+import { getIntlContent } from "../utils/IntlUtils";
+
+export default {
+  namespace: "mcpServer",
+
+  state: {
+    list: [],
+    total: 0,
+    currentPage: 1,
+    pageSize: 12,
+  },
+
+  effects: {
+    *fetch({ payload }, { call, put }) {
+      const response = yield call(fetchMcpServer, payload);
+      if (response) {
+        yield put({
+          type: "saveList",
+          payload: {
+            list: response.data,
+            total: response.total,
+            currentPage: payload.currentPage,
+            pageSize: payload.pageSize,
+          },
+        });
+      }
+    },
+    *add({ payload, callback }, { call, put }) {
+      const response = yield call(addMcpServer, payload);
+      if (response) {
+        message.success(getIntlContent("SHENYU.COMMON.RESPONSE.ADD.SUCCESS"));
+        yield put({ type: "reload" });
+      }
+      if (callback) callback();
+    },
+    *update({ payload, callback }, { call, put }) {
+      const response = yield call(updateMcpServer, payload);
+      if (response) {
+        message.success(
+          getIntlContent("SHENYU.COMMON.RESPONSE.UPDATE.SUCCESS"),
+        );
+        yield put({ type: "reload" });
+      }
+      if (callback) callback();
+    },
+    *delete({ payload, callback }, { call, put }) {
+      const response = yield call(deleteMcpServer, payload);
+      if (response) {
+        message.success(
+          getIntlContent("SHENYU.COMMON.RESPONSE.DELETE.SUCCESS"),
+        );
+        yield put({ type: "reload" });
+      }
+      if (callback) callback();
+    },
+    *reload(_, { put, select }) {
+      const { currentPage, pageSize } = yield select(
+        (state) => state.mcpServer,
+      );
+      yield put({
+        type: "fetch",
+        payload: { currentPage, pageSize },
+      });
+    },
+  },
+
+  reducers: {
+    saveList(state, { payload }) {
+      return {
+        ...state,
+        list: payload.list,
+        total: payload.total,
+        currentPage: payload.currentPage,
+        pageSize: payload.pageSize,
+      };
+    },
+  },
+};
diff --git a/src/routes/Plugin/Common/Selector.js 
b/src/routes/Plugin/Common/Selector.js
index f2bf78a9..c9bbd5c9 100644
--- a/src/routes/Plugin/Common/Selector.js
+++ b/src/routes/Plugin/Common/Selector.js
@@ -1725,7 +1725,7 @@ class AddModal extends Component {
       <Modal
         width="1100px"
         centered
-        title={getIntlContent("SHENYU.SELECTOR.NAME")}
+        title={this.props.modalTitle || getIntlContent("SHENYU.SELECTOR.NAME")}
         // visible here defaults to true, because the visibility of modal is 
determined by the popup attribute in index.js
         visible
         okText={getIntlContent("SHENYU.COMMON.SURE")}
diff --git a/src/routes/Plugin/McpServer/ToolsModal.js 
b/src/routes/Plugin/McpServer/ToolsModal.js
new file mode 100644
index 00000000..73d87ddf
--- /dev/null
+++ b/src/routes/Plugin/McpServer/ToolsModal.js
@@ -0,0 +1,411 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { Component } from "react";
+import {
+  Button,
+  Col,
+  Form,
+  Input,
+  message,
+  Modal,
+  Row,
+  Select,
+  Switch,
+} from "antd";
+import { connect } from "dva";
+import TextArea from "antd/lib/input/TextArea";
+import ReactJson from "react-json-view";
+import styles from "../index.less";
+import { getIntlContent } from "../../../utils/IntlUtils";
+import RuleCopy from "../Common/RuleCopy";
+
+const FormItem = Form.Item;
+const { Option } = Select;
+
+@connect(({ global }) => ({
+  currentNamespaceId: global.currentNamespaceId,
+}))
+class AddModal extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      visible: false,
+      questJson: {},
+    };
+
+    this.initParameters(props);
+  }
+
+  initParameters = (props) => {
+    let parameters = [];
+    let questJson = {};
+    try {
+      const handle = props.handle ? JSON.parse(props.handle) : {};
+      parameters = handle.parameters || [
+        {
+          type: "String",
+          name: "",
+          description: "",
+        },
+      ];
+      questJson = JSON.parse(handle.requestConfig);
+    } catch (e) {
+      console.error("Failed to parse handle JSON:", e);
+      parameters = [
+        {
+          type: "String",
+          name: "",
+          description: "",
+        },
+      ];
+    }
+    this.state.parameters = parameters;
+    this.state.questJson = questJson;
+  };
+
+  updateJson = (obj) => {
+    this.setState({ questJson: obj.updated_src });
+    this.props.form.setFieldsValue({
+      requestConfig: JSON.stringify(obj.updated_src),
+    });
+  };
+
+  checkParams = () => {
+    let { parameters } = this.state;
+    let result = true;
+    if (parameters) {
+      parameters.forEach((item, index) => {
+        const { type, name, description } = item;
+        if (!type || !name || !description) {
+          message.destroy();
+          message.error(`Line ${index + 1} param is incomplete`);
+          result = false;
+        }
+        // eslint-disable-next-line no-lonely-if
+        if (!name) {
+          message.destroy();
+          message.error(`Line ${index + 1} param is incomplete`);
+          result = false;
+        }
+      });
+    }
+    return result;
+  };
+
+  handleSubmit = (e) => {
+    e.preventDefault();
+    const { form, handleOk } = this.props;
+    const { parameters } = this.state;
+
+    form.validateFieldsAndScroll((err, values) => {
+      const { name, description, enabled } = values;
+      if (!err) {
+        const submit = this.checkParams();
+        if (submit) {
+          let handle = {
+            parameters,
+            requestConfig: JSON.stringify(this.state.questJson),
+            description,
+          };
+          handle = JSON.stringify({
+            ...handle,
+          });
+          handleOk({
+            name,
+            description,
+            handle,
+            enabled,
+            sort: 1,
+            loged: true,
+            matchMode: "0",
+            matchRestful: false,
+            ruleConditions: [
+              {
+                paramType: "uri",
+                operator: "pathPattern",
+                paramName: "/",
+                paramValue: "/**",
+              },
+            ],
+          });
+        }
+      }
+    });
+  };
+
+  handleAdd = () => {
+    let { parameters } = this.state;
+    parameters.push({
+      type: "String",
+      name: "",
+      description: "",
+    });
+
+    this.setState({ parameters }, () => {
+      let len = parameters.length || 0;
+      let key = `typeValueEn${len - 1}`;
+      this.setState({ [key]: true });
+    });
+  };
+
+  handleDelete = (index) => {
+    let { parameters } = this.state;
+    parameters.splice(index, 1);
+    this.setState({ parameters });
+  };
+
+  handleCopyData = (copyData) => {
+    if (!copyData) {
+      this.setState({ visible: false });
+      return;
+    }
+    const { form } = this.props;
+    const { name, matchMode, loged, enabled, sort } = copyData;
+    const formData = {
+      name,
+      matchMode: matchMode.toString(),
+      loged,
+      enabled,
+      sort,
+    };
+    form.setFieldsValue(formData);
+    this.setState({ visible: false });
+  };
+
+  render() {
+    let {
+      onCancel,
+      form,
+      name = "",
+      description = "",
+      enabled = true,
+      handle = "{}",
+    } = this.props;
+    const { parameters, visible, questJson } = this.state;
+
+    // Parse handle JSON to get requestConfig and description
+    let parsedHandle = {};
+    try {
+      parsedHandle = JSON.parse(handle);
+    } catch (e) {
+      console.error("Failed to parse handle JSON:", e);
+    }
+
+    const { description: handleDescription = "" } = parsedHandle;
+    // Use description from handle if available, otherwise use from props
+    const finalDescription = handleDescription || description;
+
+    const { getFieldDecorator } = form;
+    const formItemLayout = {
+      labelCol: {
+        sm: { span: 4 },
+      },
+      wrapperCol: {
+        sm: { span: 20 },
+      },
+    };
+    return (
+      <Modal
+        width={1000}
+        centered
+        title={getIntlContent("SHENYU.TOOL.NAME")}
+        visible
+        okText={getIntlContent("SHENYU.COMMON.SURE")}
+        cancelText={getIntlContent("SHENYU.COMMON.CALCEL")}
+        onOk={this.handleSubmit}
+        onCancel={onCancel}
+      >
+        <Form onSubmit={this.handleSubmit} className="login-form">
+          <FormItem
+            label={getIntlContent("SHENYU.PLUGIN.SELECTOR.LIST.COLUMN.NAME")}
+            {...formItemLayout}
+          >
+            {getFieldDecorator("name", {
+              rules: [
+                {
+                  required: true,
+                  message: getIntlContent("SHENYU.COMMON.INPUTNAME"),
+                },
+              ],
+              initialValue: name,
+            })(
+              <Input
+                allowClear
+                placeholder={getIntlContent(
+                  "SHENYU.PLUGIN.SELECTOR.LIST.COLUMN.NAME",
+                )}
+                addonAfter={
+                  <Button
+                    size="small"
+                    type="link"
+                    onClick={() => {
+                      this.setState({ visible: true });
+                    }}
+                  >
+                    {getIntlContent("SHENYU.PLUGIN.SEARCH.RULE.COPY")}
+                  </Button>
+                }
+              />,
+            )}
+          </FormItem>
+          <RuleCopy
+            visible={visible}
+            onOk={this.handleCopyData}
+            onCancel={() => {
+              this.setState({ visible: false });
+            }}
+          />
+          <FormItem
+            label={getIntlContent(
+              "SHENYU.PLUGIN.SELECTOR.LIST.COLUMN.DESCRIPTION",
+            )}
+            {...formItemLayout}
+          >
+            {getFieldDecorator("description", {
+              rules: [
+                {
+                  required: true,
+                  message: getIntlContent("SHENYU.COMMON.INPUTDESCRIPTION"),
+                },
+              ],
+              initialValue: finalDescription,
+            })(
+              <TextArea
+                allowClear
+                placeholder={getIntlContent(
+                  "SHENYU.PLUGIN.SELECTOR.LIST.COLUMN.DESCRIPTION",
+                )}
+              />,
+            )}
+          </FormItem>
+          <div className={styles.condition}>
+            <FormItem
+              label={getIntlContent("SHENYU.COMMON.PARAMETER")}
+              {...formItemLayout}
+            >
+              {parameters.map((item, index) => {
+                return (
+                  <Row key={index} gutter={8}>
+                    <Col span={4}>
+                      <Input
+                        allowClear
+                        value={item.name}
+                        placeholder={getIntlContent(
+                          "SHENYU.COMMON.PARAMETER.NAME",
+                        )}
+                        onChange={(e) => {
+                          const newValue = e.target.value;
+                          const newParameters = [...parameters];
+                          newParameters[index].name = newValue;
+                          this.setState({ parameters: newParameters });
+                        }}
+                      />
+                    </Col>
+                    <Col span={5}>
+                      <Select
+                        value={item.type}
+                        placeholder={getIntlContent(
+                          "SHENYU.COMMON.PARAMETER.TYPE",
+                        )}
+                        onChange={(value) => {
+                          const newParameters = [...parameters];
+                          newParameters[index].type = value;
+                          this.setState({ parameters: newParameters });
+                        }}
+                      >
+                        <Option value="String">String</Option>
+                        <Option value="Integer">Integer</Option>
+                        <Option value="Long">Long</Option>
+                        <Option value="Double">Double</Option>
+                        <Option value="Float">Float</Option>
+                        <Option value="Boolean">Boolean</Option>
+                      </Select>
+                    </Col>
+                    <Col span={11}>
+                      <Input
+                        allowClear
+                        value={item.description}
+                        placeholder={getIntlContent(
+                          "SHENYU.COMMON.PARAMETER.DESCRIPTION",
+                        )}
+                        onChange={(e) => {
+                          const newValue = e.target.value;
+                          const newParameters = [...parameters];
+                          newParameters[index].description = newValue;
+                          this.setState({ parameters: newParameters });
+                        }}
+                      />
+                    </Col>
+                    <Col span={4}>
+                      <Button
+                        type="danger"
+                        onClick={() => {
+                          this.handleDelete(index);
+                        }}
+                      >
+                        {getIntlContent("SHENYU.COMMON.DELETE.NAME")}
+                      </Button>
+                    </Col>
+                  </Row>
+                );
+              })}
+            </FormItem>
+            <FormItem label={" "} colon={false} {...formItemLayout}>
+              <Button
+                className={styles.addButton}
+                onClick={this.handleAdd}
+                type="primary"
+              >
+                {getIntlContent("SHENYU.COMMON.ADD")}{" "}
+                {getIntlContent("SHENYU.COMMON.PARAMETER")}
+              </Button>
+            </FormItem>
+          </div>
+          <FormItem
+            label={getIntlContent("SHENYU.COMMON.TOOL.REQUESTCONFIG")}
+            {...formItemLayout}
+            required={true}
+          >
+            <ReactJson
+              src={questJson}
+              theme="monokai"
+              displayDataTypes={false}
+              name={false}
+              onAdd={this.updateJson}
+              onEdit={this.updateJson}
+              onDelete={this.updateJson}
+              style={{ borderRadius: 4, padding: 16 }}
+            />
+          </FormItem>
+          <FormItem
+            {...formItemLayout}
+            label={getIntlContent("SHENYU.SELECTOR.WHETHEROPEN")}
+          >
+            {getFieldDecorator("enabled", {
+              initialValue: enabled,
+              valuePropName: "checked",
+              rules: [{ required: true }],
+            })(<Switch />)}
+          </FormItem>
+        </Form>
+      </Modal>
+    );
+  }
+}
+
+export default Form.create()(AddModal);
diff --git a/src/routes/Plugin/McpServer/index.js 
b/src/routes/Plugin/McpServer/index.js
new file mode 100755
index 00000000..ea497ed7
--- /dev/null
+++ b/src/routes/Plugin/McpServer/index.js
@@ -0,0 +1,1385 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { Component } from "react";
+import {
+  Button,
+  Col,
+  Input,
+  message,
+  Popconfirm,
+  Popover,
+  Row,
+  Switch,
+  Table,
+  Tag,
+  Typography,
+} from "antd";
+import { connect } from "dva";
+import ReactJson from "react-json-view";
+import styles from "../index.less";
+import Selector from "../Common/Selector";
+import Tools from "./ToolsModal";
+import { getCurrentLocale, getIntlContent } from "../../../utils/IntlUtils";
+import AuthButton from "../../../utils/AuthButton";
+import {
+  getUpdateModal,
+  updateNamespacePluginsEnabledByNamespace,
+} from "../../../utils/namespacePlugin";
+
+const { Search } = Input;
+const { Title } = Typography;
+@connect(({ common, global, loading }) => ({
+  ...global,
+  ...common,
+  loading: loading.effects["global/fetchPlatform"],
+}))
+export default class McpServer extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      selectorPage: 1,
+      selectorPageSize: 12,
+      selectorSelectedRowKeys: [],
+      toolPage: 1,
+      toolPageSize: 12,
+      toolSelectedRowKeys: [],
+      popup: "",
+      localeName: "",
+      selectorName: undefined,
+      toolName: undefined,
+      isPluginEnabled: false,
+      pluginName: "mcpServer",
+      pluginRole: "Mcp",
+    };
+  }
+
+  componentDidMount() {
+    const { dispatch, plugins } = this.props;
+    const { selectorPage, selectorPageSize } = this.state;
+    if (plugins && plugins.length > 0) {
+      this.getAllSelectors(selectorPage, selectorPageSize, plugins);
+    } else {
+      dispatch({
+        type: "global/fetchPlugins",
+        payload: {
+          callback: (pluginList) => {
+            this.getAllSelectors(selectorPage, selectorPageSize, pluginList);
+          },
+        },
+      });
+    }
+  }
+
+  /* eslint-disable no-unused-vars */
+  componentDidUpdate(prevProps, prevState, snapshot) {
+    const preId = prevProps.match.params.id;
+    const newId = this.props.match.params.id;
+    const { selectorPage, selectorPageSize } = this.state;
+    const { dispatch, plugins, currentNamespaceId } = this.props;
+    if (newId !== preId) {
+      dispatch({
+        type: "common/resetData",
+      });
+
+      if (prevProps.plugins && prevProps.plugins.length > 0) {
+        this.getAllSelectors(selectorPage, selectorPageSize, 
prevProps.plugins);
+      } else {
+        dispatch({
+          type: "global/fetchPlugins",
+          payload: {
+            callback: (pluginList) => {
+              this.getAllSelectors(selectorPage, selectorPageSize, pluginList);
+            },
+          },
+        });
+      }
+    }
+    if (prevProps.currentNamespaceId !== currentNamespaceId) {
+      if (plugins) {
+        this.getAllSelectors(selectorPage, selectorPageSize, plugins);
+      }
+    }
+  }
+  /* eslint-enable no-unused-vars */
+
+  componentWillUnmount() {
+    const { dispatch } = this.props;
+    dispatch({
+      type: "common/resetData",
+    });
+  }
+
+  getAllSelectors = (page, pageSize, plugins) => {
+    const { dispatch, currentNamespaceId } = this.props;
+    const { selectorName } = this.state;
+    const tempPlugin = this.getPlugin(plugins, this.state.pluginName);
+    const tempPluginId = tempPlugin?.pluginId;
+    const enabled = tempPlugin?.enabled ?? false;
+    this.setState({ pluginId: tempPluginId, isPluginEnabled: enabled });
+    dispatch({
+      type: "common/fetchSelector",
+      payload: {
+        currentPage: page,
+        pageSize,
+        pluginId: tempPluginId,
+        name: selectorName,
+        namespaceId: currentNamespaceId,
+      },
+    });
+    this.setState({ selectorSelectedRowKeys: [] });
+    this.setState({ toolSelectedRowKeys: [] });
+  };
+
+  getAllTools = (page, pageSize) => {
+    const { dispatch, currentSelector, currentNamespaceId } = this.props;
+    const { toolName } = this.state;
+    const selectorId = currentSelector ? currentSelector.id : "";
+    dispatch({
+      type: "common/fetchRule",
+      payload: {
+        selectorId,
+        currentPage: page,
+        pageSize,
+        name: toolName,
+        namespaceId: currentNamespaceId,
+      },
+    });
+    this.setState({ selectorSelectedRowKeys: [] });
+    this.setState({ toolSelectedRowKeys: [] });
+  };
+
+  getPlugin = (plugins, name) => {
+    const plugin = plugins.filter((item) => {
+      return item.name === name;
+    });
+    return plugin && plugin.length > 0 ? plugin[0] : null;
+  };
+
+  getPluginConfigField = (config, fieldName) => {
+    if (config) {
+      let configObj = JSON.parse(config);
+      return configObj[fieldName];
+    } else {
+      return "";
+    }
+  };
+
+  closeModal = () => {
+    this.setState({ popup: "" });
+  };
+
+  searchSelectorOnchange = (e) => {
+    const selectorName = e.target.value;
+    this.setState({ selectorName });
+  };
+
+  searchSelector = () => {
+    const { plugins } = this.props;
+    const { selectorPage, selectorPageSize } = this.state;
+    this.getAllSelectors(selectorPage, selectorPageSize, plugins);
+  };
+
+  isDiscovery = (pluginId) => {
+    // 5:   divide
+    // 15:  grpc
+    // 26:  websocket
+    return ["5", "15", "26"].includes(pluginId);
+  };
+
+  addSelector = () => {
+    const { selectorPage, selectorPageSize, pluginName } = this.state;
+    const { dispatch, plugins, currentNamespaceId } = this.props;
+    const plugin = this.getPlugin(plugins, pluginName);
+    const { pluginId, config } = plugin;
+    const multiSelectorHandle =
+      this.getPluginConfigField(config, "multiSelectorHandle") === "1";
+    const isDiscovery = this.isDiscovery(pluginId);
+    if (isDiscovery) {
+      let discoveryConfig = {
+        discoveryType: "",
+        serverList: "",
+        handler: {},
+        listenerNode: "",
+        props: {},
+      };
+      this.setState({
+        popup: (
+          <Selector
+            modalTitle={getIntlContent("SHENYU.SERVER.NAME")}
+            pluginName
+            pluginId={pluginId}
+            multiSelectorHandle={multiSelectorHandle}
+            isAdd={true}
+            selectorConditions={[
+              {
+                paramType: "uri",
+                operator: "match",
+                paramName: "/**",
+                paramValue: "",
+              },
+            ]}
+            discoveryConfig={discoveryConfig}
+            isDiscovery={true}
+            handleOk={(selector) => {
+              const {
+                name: selectorName,
+                listenerNode,
+                serverList,
+                selectedDiscoveryType,
+                discoveryProps,
+                handler,
+                upstreams,
+                importedDiscoveryId,
+              } = selector;
+              const upstreamsWithProps = this.getUpstreamsWithProps(upstreams);
+              dispatch({
+                type: "common/addSelector",
+                payload: {
+                  pluginId,
+                  ...selector,
+                  upstreams: upstreamsWithProps,
+                  namespaceId: currentNamespaceId,
+                },
+                fetchValue: {
+                  pluginId,
+                  currentPage: selectorPage,
+                  pageSize: selectorPageSize,
+                  namespaceId: currentNamespaceId,
+                },
+                callback: (selectorId) => {
+                  this.addDiscoveryUpstream({
+                    selectorId,
+                    selectorName,
+                    pluginName,
+                    listenerNode,
+                    handler,
+                    typeValue: this.getTypeValueByPluginName(pluginName),
+                    upstreamsWithProps,
+                    importedDiscoveryId,
+                    selectedDiscoveryType,
+                    serverList,
+                    discoveryProps,
+                    namespaceId: currentNamespaceId,
+                  });
+                  this.closeModal();
+                },
+              });
+            }}
+            onCancel={this.closeModal}
+          />
+        ),
+      });
+    } else {
+      this.setState({
+        popup: (
+          <Selector
+            pluginName={pluginName}
+            pluginId={pluginId}
+            multiSelectorHandle={multiSelectorHandle}
+            isDiscovery={false}
+            handleOk={(selector) => {
+              dispatch({
+                type: "common/addSelector",
+                payload: {
+                  pluginId,
+                  ...selector,
+                  namespaceId: currentNamespaceId,
+                },
+                fetchValue: {
+                  pluginId,
+                  currentPage: selectorPage,
+                  pageSize: selectorPageSize,
+                  namespaceId: currentNamespaceId,
+                },
+                callback: () => {
+                  this.closeModal();
+                },
+              });
+            }}
+            onCancel={this.closeModal}
+          />
+        ),
+      });
+    }
+  };
+
+  searchToolOnchange = (e) => {
+    const toolName = e.target.value;
+    this.setState({ toolName });
+  };
+
+  searchTool = () => {
+    this.setState({ toolPage: 1 });
+    const { toolPageSize } = this.state;
+    this.getAllTools(1, toolPageSize);
+  };
+
+  addTool = () => {
+    const { toolPage, toolPageSize, pluginId, pluginName } = this.state;
+    const { dispatch, currentSelector, plugins, currentNamespaceId } =
+      this.props;
+    const plugin = this.getPlugin(plugins, this.state.pluginName);
+    const { config } = plugin;
+    const multiRuleHandle =
+      this.getPluginConfigField(config, "multiRuleHandle") === "1";
+    if (currentSelector && currentSelector.id) {
+      const selectorId = currentSelector.id;
+      this.setState({
+        popup: (
+          <Tools
+            pluginId={pluginId}
+            pluginName={pluginName}
+            multiRuleHandle={multiRuleHandle}
+            handleOk={(rule) => {
+              dispatch({
+                type: "common/addRule",
+                payload: {
+                  selectorId,
+                  ...rule,
+                  namespaceId: currentNamespaceId,
+                },
+                fetchValue: {
+                  selectorId,
+                  currentPage: toolPage,
+                  pageSize: toolPageSize,
+                  namespaceId: currentNamespaceId,
+                },
+                callback: () => {
+                  this.closeModal();
+                },
+              });
+            }}
+            onCancel={this.closeModal}
+          />
+        ),
+      });
+    } else {
+      message.destroy();
+      message.warn(getIntlContent("SHENYU.COMMON.WARN.INPUT_SELECTOR"));
+    }
+  };
+
+  togglePluginStatus = () => {
+    const { dispatch, plugins } = this.props;
+    const pluginName = this.state.pluginName;
+    const plugin = this.getPlugin(plugins, pluginName);
+    const enabled = !this.state.isPluginEnabled;
+    updateNamespacePluginsEnabledByNamespace({
+      list: [plugin.pluginId],
+      namespaceId: this.props.currentNamespaceId,
+      enabled,
+      dispatch,
+      callback: () => {
+        plugin.enabled = enabled;
+        this.setState({ isPluginEnabled: enabled });
+        this.closeModal();
+      },
+    });
+  };
+
+  editClick = () => {
+    const { dispatch, plugins } = this.props;
+    const plugin = this.getPlugin(plugins, this.state.pluginName);
+    getUpdateModal({
+      id: plugin.id,
+      namespaceId: plugin.namespaceId,
+      dispatch,
+      callback: (popup) => {
+        this.setState({ popup });
+      },
+      updatedCallback: ({ enabled }) => {
+        this.setState({ isPluginEnabled: enabled });
+        this.closeModal();
+      },
+      canceledCallback: () => {
+        this.closeModal();
+      },
+    });
+  };
+
+  getTypeValueByPluginName = (name) => {
+    return name === "divide"
+      ? "http"
+      : name === "websocket"
+        ? "ws"
+        : name === "grpc"
+          ? "grpc"
+          : "http";
+  };
+
+  getUpstreamsWithProps = (upstreams) => {
+    const { currentNamespaceId } = this.props;
+    return upstreams.map((item) => ({
+      protocol: item.protocol,
+      url: item.url,
+      status: parseInt(item.status, 10),
+      weight: item.weight,
+      startupTime: item.startupTime,
+      props: JSON.stringify({
+        warmupTime: item.warmupTime,
+        gray: `${item.gray}`,
+      }),
+      namespaceId: currentNamespaceId,
+    }));
+  };
+
+  addDiscoveryUpstream = ({
+    selectorId,
+    selectorName,
+    pluginName,
+    listenerNode,
+    handler,
+    typeValue,
+    upstreamsWithProps,
+    importedDiscoveryId,
+    selectedDiscoveryType,
+    serverList,
+    discoveryProps,
+    namespaceId,
+  }) => {
+    const { dispatch } = this.props;
+    dispatch({
+      type: "discovery/bindSelector",
+      payload: {
+        selectorId,
+        name: selectorName,
+        pluginName,
+        listenerNode,
+        handler,
+        type: typeValue,
+        discoveryUpstreams: upstreamsWithProps,
+        discovery: {
+          id: importedDiscoveryId,
+          discoveryType: selectedDiscoveryType,
+          serverList,
+          props: discoveryProps,
+          name: selectorName,
+        },
+        namespaceId,
+      },
+    });
+  };
+
+  updateDiscoveryUpstream = (discoveryHandlerId, upstreams) => {
+    const { dispatch, currentNamespaceId } = this.props;
+    const upstreamsWithHandlerId = upstreams.map((item) => ({
+      protocol: item.protocol,
+      url: item.url,
+      status: parseInt(item.status, 10),
+      weight: item.weight,
+      props: JSON.stringify({
+        warmupTime: item.warmupTime,
+        gray: `${item.gray}`,
+      }),
+      discoveryHandlerId,
+      namespaceId: currentNamespaceId,
+    }));
+    dispatch({
+      type: "discovery/updateDiscoveryUpstream",
+      payload: {
+        discoveryHandlerId,
+        upstreams: upstreamsWithHandlerId,
+      },
+    });
+  };
+
+  editSelector = (record) => {
+    const { dispatch, plugins, currentNamespaceId } = this.props;
+    const { selectorPage, selectorPageSize, pluginName } = this.state;
+    const plugin = this.getPlugin(plugins, pluginName);
+    const { pluginId, config } = plugin;
+    const multiSelectorHandle =
+      this.getPluginConfigField(config, "multiSelectorHandle") === "1";
+    const isDiscovery = this.isDiscovery(pluginId);
+    const { id } = record;
+    dispatch({
+      type: "common/fetchSeItem",
+      payload: {
+        id,
+        namespaceId: currentNamespaceId,
+      },
+      callback: (selector) => {
+        if (isDiscovery) {
+          let discoveryConfig = {
+            props:
+              selector.discoveryVO && selector.discoveryVO.props
+                ? selector.discoveryVO.props
+                : "{}",
+            discoveryType:
+              selector.discoveryVO && selector.discoveryVO.type
+                ? selector.discoveryVO.type
+                : "local",
+            serverList:
+              selector.discoveryVO && selector.discoveryVO.serverList
+                ? selector.discoveryVO.serverList
+                : "",
+            handler:
+              selector.discoveryHandler && selector.discoveryHandler.handler
+                ? selector.discoveryHandler.handler
+                : "{}",
+            listenerNode:
+              selector.discoveryHandler &&
+              selector.discoveryHandler.listenerNode
+                ? selector.discoveryHandler.listenerNode
+                : "",
+          };
+          let updateArray = [];
+          if (selector.discoveryUpstreams) {
+            updateArray = selector.discoveryUpstreams.map((item) => {
+              let propsObj = JSON.parse(item.props || "{}");
+              if (item.props === null) {
+                propsObj = {
+                  warmupTime: 10,
+                  gray: "false",
+                };
+              }
+              return {
+                ...item,
+                key: item.id,
+                warmupTime: propsObj.warmupTime,
+                gray: propsObj.gray,
+              };
+            });
+          }
+          let discoveryHandlerId = selector.discoveryHandler
+            ? selector.discoveryHandler.id
+            : "";
+          this.setState({
+            popup: (
+              <Selector
+                pluginName={pluginName}
+                {...selector}
+                multiSelectorHandle={multiSelectorHandle}
+                discoveryConfig={discoveryConfig}
+                discoveryUpstreams={updateArray}
+                isAdd={false}
+                isDiscovery={true}
+                handleOk={(values) => {
+                  dispatch({
+                    type: "common/updateSelector",
+                    payload: {
+                      pluginId,
+                      ...values,
+                      id,
+                      namespaceId: currentNamespaceId,
+                    },
+                    fetchValue: {
+                      pluginId,
+                      currentPage: selectorPage,
+                      pageSize: selectorPageSize,
+                      namespaceId: currentNamespaceId,
+                    },
+                    callback: () => {
+                      const {
+                        name: selectorName,
+                        handler,
+                        upstreams,
+                        serverList,
+                        listenerNode,
+                        discoveryProps,
+                        importedDiscoveryId,
+                        selectedDiscoveryType,
+                      } = values;
+
+                      if (!discoveryHandlerId) {
+                        this.addDiscoveryUpstream({
+                          selectorId: id,
+                          selectorName,
+                          pluginName,
+                          listenerNode,
+                          handler,
+                          typeValue: this.getTypeValueByPluginName(pluginName),
+                          upstreamsWithProps:
+                            this.getUpstreamsWithProps(upstreams),
+                          importedDiscoveryId,
+                          selectedDiscoveryType,
+                          serverList,
+                          discoveryProps,
+                          namespaceId: currentNamespaceId,
+                        });
+                      } else {
+                        this.updateDiscoveryUpstream(
+                          discoveryHandlerId,
+                          upstreams,
+                        );
+                      }
+                      this.closeModal();
+                    },
+                  });
+                }}
+                onCancel={this.closeModal}
+              />
+            ),
+          });
+        } else {
+          this.setState({
+            popup: (
+              <Selector
+                pluginName={pluginName}
+                pluginId={pluginId}
+                {...selector}
+                multiSelectorHandle={multiSelectorHandle}
+                isDiscovery={false}
+                handleOk={(values) => {
+                  dispatch({
+                    type: "common/updateSelector",
+                    payload: {
+                      pluginId,
+                      ...values,
+                      id,
+                      namespaceId: currentNamespaceId,
+                    },
+                    fetchValue: {
+                      pluginId,
+                      currentPage: selectorPage,
+                      pageSize: selectorPageSize,
+                      namespaceId: currentNamespaceId,
+                    },
+                    callback: () => {
+                      this.closeModal();
+                    },
+                  });
+                }}
+                onCancel={this.closeModal}
+              />
+            ),
+          });
+        }
+      },
+    });
+  };
+
+  enableSelector = ({ list, enabled }) => {
+    const { dispatch, plugins, currentNamespaceId } = this.props;
+    const { selectorPage, selectorPageSize, pluginName } = this.state;
+    const plugin = this.getPlugin(plugins, pluginName);
+    const { pluginId } = plugin;
+    dispatch({
+      type: "common/enableSelector",
+      payload: {
+        list,
+        enabled,
+        namespaceId: currentNamespaceId,
+      },
+      fetchValue: {
+        pluginId,
+        currentPage: selectorPage,
+        pageSize: selectorPageSize,
+        namespaceId: currentNamespaceId,
+      },
+    });
+  };
+
+  onSelectorSelectChange = (selectorSelectedRowKeys) => {
+    this.setState({ selectorSelectedRowKeys });
+  };
+
+  openSelectorClick = () => {
+    const { selectorSelectedRowKeys } = this.state;
+    const { selectorList } = this.props;
+    if (selectorSelectedRowKeys && selectorSelectedRowKeys.length > 0) {
+      let anyEnabled = selectorList.some(
+        (selector) =>
+          selectorSelectedRowKeys.includes(selector.id) && selector.enabled,
+      );
+      this.enableSelector({
+        list: selectorSelectedRowKeys,
+        enabled: !anyEnabled,
+      });
+    } else {
+      message.destroy();
+      message.warn("Please select data");
+    }
+  };
+
+  deleteSelector = (record) => {
+    const { dispatch, plugins, currentNamespaceId } = this.props;
+    const { selectorPage, selectorPageSize, pluginName } = this.state;
+    const pluginId = this.getPluginId(plugins, pluginName);
+    dispatch({
+      type: "common/deleteSelector",
+      payload: {
+        list: [record.id],
+        namespaceId: currentNamespaceId,
+      },
+      fetchValue: {
+        pluginId,
+        currentPage: selectorPage,
+        pageSize: selectorPageSize,
+        namespaceId: currentNamespaceId,
+      },
+    });
+  };
+
+  pageSelectorChange = (page) => {
+    this.setState({ selectorPage: page });
+    const { plugins } = this.props;
+    const { selectorPageSize } = this.state;
+    this.getAllSelectors(page, selectorPageSize, plugins);
+  };
+
+  pageSelectorChangeSize = (currentPage, pageSize) => {
+    const { plugins } = this.props;
+    this.setState({ selectorPage: 1, selectorPageSize: pageSize });
+    this.getAllSelectors(1, pageSize, plugins);
+  };
+
+  // select
+  rowClick = (record) => {
+    const { id } = record;
+    const { dispatch, currentNamespaceId } = this.props;
+    const { selectorPageSize } = this.state;
+    dispatch({
+      type: "common/saveCurrentSelector",
+      payload: {
+        currentSelector: record,
+      },
+    });
+    dispatch({
+      type: "common/fetchRule",
+      payload: {
+        currentPage: 1,
+        pageSize: selectorPageSize,
+        selectorId: id,
+        namespaceId: currentNamespaceId,
+      },
+    });
+  };
+
+  pageToolChange = (page) => {
+    this.setState({ toolPage: page });
+    const { toolPageSize } = this.state;
+    this.getAllTools(page, toolPageSize);
+  };
+
+  pageToolChangeSize = (currentPage, pageSize) => {
+    this.setState({ toolPage: 1, toolPageSize: pageSize });
+    this.getAllTools(1, pageSize);
+  };
+
+  editTool = (record) => {
+    console.log("record", record);
+    const { dispatch, currentSelector, plugins, currentNamespaceId } =
+      this.props;
+    const { toolPage, toolPageSize, pluginId, pluginName } = this.state;
+    const plugin = this.getPlugin(plugins, this.state.pluginName);
+    const { config } = plugin;
+    const multiRuleHandle =
+      this.getPluginConfigField(config, "multiRuleHandle") === "1";
+    const selectorId = currentSelector ? currentSelector.id : "";
+    const { id } = record;
+    dispatch({
+      type: "common/fetchRuleItem",
+      payload: {
+        id,
+        namespaceId: currentNamespaceId,
+      },
+      callback: (rule) => {
+        this.setState({
+          popup: (
+            <Tools
+              {...rule}
+              pluginId={pluginId}
+              pluginName={pluginName}
+              multiRuleHandle={multiRuleHandle}
+              handleOk={(values) => {
+                dispatch({
+                  type: "common/updateRule",
+                  payload: {
+                    selectorId,
+                    ...values,
+                    id,
+                    namespaceId: currentNamespaceId,
+                  },
+                  fetchValue: {
+                    selectorId,
+                    currentPage: toolPage,
+                    pageSize: toolPageSize,
+                    namespaceId: currentNamespaceId,
+                  },
+                  callback: () => {
+                    this.closeModal();
+                  },
+                });
+              }}
+              onCancel={this.closeModal}
+            />
+          ),
+        });
+      },
+    });
+  };
+
+  enableTool = ({ list, enabled }) => {
+    const { toolPage, toolPageSize } = this.state;
+    const { dispatch, currentSelector, currentNamespaceId } = this.props;
+    const selectorId = currentSelector ? currentSelector.id : "";
+    dispatch({
+      type: "common/enableRule",
+      payload: {
+        list,
+        enabled,
+        namespaceId: currentNamespaceId,
+      },
+      fetchValue: {
+        selectorId,
+        currentPage: toolPage,
+        pageSize: toolPageSize,
+        namespaceId: currentNamespaceId,
+      },
+    });
+  };
+
+  onToolSelectChange = (toolSelectedRowKeys) => {
+    if (toolSelectedRowKeys && toolSelectedRowKeys.length > 0) {
+      this.setState({ toolSelectedRowKeys });
+    } else {
+      this.setState({ toolSelectedRowKeys: [] });
+    }
+  };
+
+  openToolClick = () => {
+    const { toolSelectedRowKeys } = this.state;
+    const { ruleList } = this.props;
+    if (toolSelectedRowKeys && toolSelectedRowKeys.length > 0) {
+      let anyEnabled = ruleList
+        ? ruleList.some(
+            (tool) => toolSelectedRowKeys.includes(tool.id) && tool.enabled,
+          )
+        : false;
+      this.enableTool({
+        list: toolSelectedRowKeys,
+        enabled: !anyEnabled,
+      });
+    } else {
+      message.destroy();
+      message.warn("Please select data");
+    }
+  };
+
+  deleteTool = (record) => {
+    const { dispatch, currentSelector, ruleList, currentNamespaceId } =
+      this.props;
+    const { toolPage, toolPageSize } = this.state;
+    const currentPage =
+      toolPage > 1 && ruleList.length === 1 ? toolPage - 1 : toolPage;
+    dispatch({
+      type: "common/deleteRule",
+      payload: {
+        list: [record.id],
+        namespaceId: currentNamespaceId,
+      },
+      fetchValue: {
+        selectorId: currentSelector.id,
+        currentPage,
+        pageSize: toolPageSize,
+        namespaceId: currentNamespaceId,
+      },
+      callback: () => {
+        this.setState({ toolSelectedRowKeys: [] });
+      },
+    });
+  };
+
+  asyncClick = () => {
+    const { dispatch, plugins } = this.props;
+    const plugin = this.getPlugin(plugins, this.state.pluginName);
+    dispatch({
+      type: "global/asyncPlugin",
+      payload: {
+        id: plugin.id,
+      },
+    });
+  };
+
+  // eslint-disable-next-line react/no-unused-class-component-methods
+  changeLocales(locale) {
+    this.setState({
+      localeName: locale,
+    });
+    getCurrentLocale(this.state.localeName);
+  }
+
+  render() {
+    const {
+      popup,
+      selectorPage,
+      selectorPageSize,
+      selectorSelectedRowKeys,
+      toolPage,
+      toolPageSize,
+      toolSelectedRowKeys,
+    } = this.state;
+    const {
+      ruleList,
+      selectorList,
+      selectorTotal,
+      currentSelector,
+      toolTotal,
+    } = this.props;
+
+    const selectColumns = [
+      {
+        align: "center",
+        title: getIntlContent("SHENYU.SELECTOR.EXEORDER"),
+        dataIndex: "sort",
+        key: "sort",
+      },
+      {
+        align: "center",
+        title: getIntlContent("SHENYU.PLUGIN.SELECTOR.LIST.COLUMN.NAME"),
+        dataIndex: "name",
+        key: "name",
+      },
+      {
+        align: "center",
+        title: getIntlContent("SHENYU.COMMON.OPEN"),
+        dataIndex: "enabled",
+        key: "enabled",
+        render: (text, row) => (
+          <Switch
+            checkedChildren={getIntlContent("SHENYU.COMMON.OPEN")}
+            unCheckedChildren={getIntlContent("SHENYU.COMMON.CLOSE")}
+            checked={text}
+            onChange={(checked) => {
+              this.enableSelector({ list: [row.id], enabled: checked });
+            }}
+          />
+        ),
+      },
+      {
+        align: "center",
+        title: getIntlContent("SHENYU.COMMON.OPERAT"),
+        dataIndex: "operate",
+        key: "operate",
+        render: (text, record) => {
+          return (
+            <div>
+              <AuthButton
+                perms={`plugin:${this.state.pluginName}Selector:edit`}
+              >
+                <span
+                  style={{ marginRight: 8 }}
+                  className="edit"
+                  onClick={(e) => {
+                    e.stopPropagation();
+                    this.editSelector(record);
+                  }}
+                >
+                  {getIntlContent("SHENYU.COMMON.CHANGE")}
+                </span>
+              </AuthButton>
+              <AuthButton
+                perms={`plugin:${this.state.pluginName}Selector:delete`}
+              >
+                <Popconfirm
+                  title={getIntlContent("SHENYU.COMMON.DELETE")}
+                  placement="bottom"
+                  onCancel={(e) => {
+                    e.stopPropagation();
+                  }}
+                  onConfirm={(e) => {
+                    e.stopPropagation();
+                    this.deleteSelector(record);
+                  }}
+                  okText={getIntlContent("SHENYU.COMMON.SURE")}
+                  cancelText={getIntlContent("SHENYU.COMMON.CALCEL")}
+                >
+                  <span
+                    className="edit"
+                    onClick={(e) => {
+                      e.stopPropagation();
+                    }}
+                  >
+                    {getIntlContent("SHENYU.COMMON.DELETE.NAME")}
+                  </span>
+                </Popconfirm>
+              </AuthButton>
+            </div>
+          );
+        },
+      },
+    ];
+    const selectorRowSelection = {
+      selectedRowKeys: selectorSelectedRowKeys,
+      onChange: this.onSelectorSelectChange,
+    };
+
+    const toolRowSelection = {
+      selectedRowKeys: toolSelectedRowKeys,
+      onChange: this.onToolSelectChange,
+    };
+
+    const toolsColumns = [
+      {
+        align: "center",
+        title: getIntlContent("SHENYU.SELECTOR.EXEORDER"),
+        dataIndex: "sort",
+        key: "sort",
+      },
+      {
+        align: "center",
+        title: getIntlContent("SHENYU.COMMON.TOOL.NAME"),
+        dataIndex: "name",
+        key: "name",
+      },
+      {
+        align: "center",
+        title: getIntlContent("SHENYU.COMMON.TOOL.REQUESTPARAMS"),
+        dataIndex: "handle",
+        key: "requestParams",
+        render: (text) => {
+          const handle = JSON.parse(text);
+          const parameters = handle.parameters;
+          return (
+            <Popover
+              content={
+                <ReactJson
+                  name={false}
+                  displayDataTypes={false}
+                  src={parameters}
+                  theme="monokai"
+                />
+              }
+              title={getIntlContent("SHENYU.COMMON.TOOL.REQUESTPARAMS")}
+            >
+              <a>{getIntlContent("SHENYU.COMMON.PREVIEW")}</a>
+            </Popover>
+          );
+        },
+      },
+      {
+        align: "center",
+        title: getIntlContent("SHENYU.COMMON.TOOL.REQUESTCONFIG"),
+        dataIndex: "handle",
+        key: "requestConfig",
+        render: (text) => {
+          const handle = JSON.parse(text);
+          const requestConfig = JSON.parse(handle.requestConfig);
+          return (
+            <Popover
+              content={
+                <ReactJson
+                  name={false}
+                  displayDataTypes={false}
+                  src={requestConfig}
+                  theme="monokai"
+                />
+              }
+              title={getIntlContent("SHENYU.COMMON.TOOL.REQUESTCONFIG")}
+            >
+              <a>{getIntlContent("SHENYU.COMMON.PREVIEW")}</a>
+            </Popover>
+          );
+        },
+      },
+      {
+        align: "center",
+        title: getIntlContent("SHENYU.COMMON.OPEN"),
+        dataIndex: "enabled",
+        key: "enabled",
+        render: (text, row) => (
+          <Switch
+            checkedChildren={getIntlContent("SHENYU.COMMON.OPEN")}
+            unCheckedChildren={getIntlContent("SHENYU.COMMON.CLOSE")}
+            checked={text}
+            onChange={(checked) => {
+              this.enableTool({ list: [row.id], enabled: checked });
+            }}
+          />
+        ),
+      },
+      {
+        align: "center",
+        title: getIntlContent("SHENYU.SYSTEM.UPDATETIME"),
+        dataIndex: "dateCreated",
+        key: "dateCreated",
+        sorter: (a, b) => (a.dateCreated > b.dateCreated ? 1 : -1),
+      },
+      {
+        align: "center",
+        title: getIntlContent("SHENYU.COMMON.OPERAT"),
+        dataIndex: "operate",
+        key: "operate",
+        render: (text, record) => {
+          return (
+            <div>
+              <AuthButton perms={`plugin:${this.state.pluginName}Rule:edit`}>
+                <span
+                  className="edit"
+                  style={{ marginRight: 8 }}
+                  onClick={(e) => {
+                    e.stopPropagation();
+                    this.editTool(record);
+                  }}
+                >
+                  {getIntlContent("SHENYU.COMMON.CHANGE")}
+                </span>
+              </AuthButton>
+              <AuthButton perms={`plugin:${this.state.pluginName}Rule:delete`}>
+                <Popconfirm
+                  title={getIntlContent("SHENYU.COMMON.DELETE")}
+                  placement="bottom"
+                  onCancel={(e) => {
+                    e.stopPropagation();
+                  }}
+                  onConfirm={(e) => {
+                    e.stopPropagation();
+                    this.deleteTool(record);
+                  }}
+                  okText={getIntlContent("SHENYU.COMMON.SURE")}
+                  cancelText={getIntlContent("SHENYU.COMMON.CALCEL")}
+                >
+                  <span
+                    className="edit"
+                    onClick={(e) => {
+                      e.stopPropagation();
+                    }}
+                  >
+                    {getIntlContent("SHENYU.COMMON.DELETE.NAME")}
+                  </span>
+                </Popconfirm>
+              </AuthButton>
+            </div>
+          );
+        },
+      },
+    ];
+
+    const tag = {
+      text: this.state.isPluginEnabled
+        ? getIntlContent("SHENYU.COMMON.OPEN")
+        : getIntlContent("SHENYU.COMMON.CLOSE"),
+      color: this.state.isPluginEnabled ? "green" : "red",
+    };
+
+    const expandedRowRender = (record) => (
+      <p
+        style={{
+          maxWidth: document.documentElement.clientWidth * 0.5 - 50,
+        }}
+      >
+        {record.handle}
+      </p>
+    );
+
+    return (
+      <div className="plug-content-wrap">
+        <Row
+          style={{
+            marginBottom: "5px",
+            display: "flex",
+            justifyContent: "space-between",
+            alignItems: "center",
+          }}
+        >
+          <div
+            style={{ display: "flex", alignItems: "end", flex: 1, margin: 0 }}
+          >
+            <Title
+              level={2}
+              style={{ textTransform: "capitalize", margin: "0 20px 0 0" }}
+            >
+              {this.state.pluginName}
+            </Title>
+            <Title level={3} type="secondary" style={{ margin: "0 20px 0 0" }}>
+              {this.state.pluginRole}
+            </Title>
+            <Tag color={tag.color}>{tag.text}</Tag>
+          </div>
+          <div
+            style={{
+              display: "flex",
+              alignItems: "end",
+              gap: 10,
+              minHeight: 32,
+            }}
+          >
+            <Switch
+              checked={this.state.isPluginEnabled ?? false}
+              onChange={this.togglePluginStatus}
+            />
+            <AuthButton perms="system:plugin:edit">
+              <div className="edit" onClick={this.editClick}>
+                {getIntlContent("SHENYU.SYSTEM.EDITOR")}
+              </div>
+            </AuthButton>
+          </div>
+        </Row>
+        <Row gutter={20}>
+          <Col span={10}>
+            <div className="table-header">
+              <h3>{getIntlContent("SHENYU.PLUGIN.SERVER.LIST.TITLE")}</h3>
+              <div className={styles.headerSearch}>
+                <AuthButton
+                  perms={`plugin:${this.state.pluginName}Selector:query`}
+                >
+                  <Search
+                    className={styles.search}
+                    style={{ minWidth: "130px" }}
+                    placeholder={getIntlContent(
+                      "SHENYU.PLUGIN.SEARCH.SELECTOR.NAME",
+                    )}
+                    enterButton={getIntlContent("SHENYU.SYSTEM.SEARCH")}
+                    size="default"
+                    onChange={this.searchSelectorOnchange}
+                    onSearch={this.searchSelector}
+                  />
+                </AuthButton>
+                <AuthButton
+                  perms={`plugin:${this.state.pluginName}Selector:add`}
+                >
+                  <Button type="primary" onClick={this.addSelector}>
+                    {getIntlContent("SHENYU.PLUGIN.SELECTOR.LIST.ADD")}
+                  </Button>
+                </AuthButton>
+                <AuthButton
+                  perms={`plugin:${this.state.pluginName}Selector:edit`}
+                >
+                  <Button
+                    type="primary"
+                    onClick={this.openSelectorClick}
+                    style={{ marginLeft: 10 }}
+                  >
+                    {getIntlContent(
+                      selectorList.some(
+                        (selector) =>
+                          selectorSelectedRowKeys.includes(selector.id) &&
+                          selector.enabled,
+                      )
+                        ? "SHENYU.PLUGIN.SELECTOR.BATCH.CLOSED"
+                        : "SHENYU.PLUGIN.SELECTOR.BATCH.OPENED",
+                    )}
+                  </Button>
+                </AuthButton>
+              </div>
+            </div>
+            <Table
+              size="small"
+              onRow={(record) => {
+                return {
+                  onClick: () => {
+                    this.rowClick(record);
+                  },
+                };
+              }}
+              style={{ marginTop: 30 }}
+              bordered
+              columns={selectColumns}
+              dataSource={selectorList}
+              rowSelection={selectorRowSelection}
+              pagination={{
+                total: selectorTotal,
+                showTotal: (showTotal) => `${showTotal}`,
+                showSizeChanger: true,
+                pageSizeOptions: ["12", "20", "50", "100"],
+                current: selectorPage,
+                pageSize: selectorPageSize,
+                onChange: this.pageSelectorChange,
+                onShowSizeChange: this.pageSelectorChangeSize,
+              }}
+              rowClassName={(item) => {
+                if (currentSelector && currentSelector.id === item.id) {
+                  return "table-selected";
+                } else {
+                  return "";
+                }
+              }}
+            />
+          </Col>
+          <Col span={14}>
+            <div className="table-header">
+              <div style={{ display: "flex", alignItems: "center" }}>
+                <h3>{getIntlContent("SHENYU.PLUGIN.SELECTOR.TOOL.LIST")}</h3>
+                <AuthButton perms={`plugin:${this.state.pluginName}:modify`}>
+                  <Button
+                    icon="reload"
+                    onClick={this.asyncClick}
+                    type="primary"
+                  >
+                    {getIntlContent("SHENYU.COMMON.SYN")}{" "}
+                    {this.state.pluginName}
+                  </Button>
+                </AuthButton>
+              </div>
+
+              <div className={styles.headerSearch}>
+                <AuthButton 
perms={`plugin:${this.state.pluginName}Rule:query`}>
+                  <Search
+                    className={styles.search}
+                    placeholder={getIntlContent(
+                      "SHENYU.PLUGIN.SEARCH.TOOL.NAME",
+                    )}
+                    enterButton={getIntlContent("SHENYU.SYSTEM.SEARCH")}
+                    size="default"
+                    onChange={this.searchToolOnchange}
+                    onSearch={this.searchTool}
+                  />
+                </AuthButton>
+                <AuthButton perms={`plugin:${this.state.pluginName}Rule:add`}>
+                  <Button type="primary" onClick={this.addTool}>
+                    {getIntlContent("SHENYU.COMMON.ADD.TOOL")}
+                  </Button>
+                </AuthButton>
+                <AuthButton perms={`plugin:${this.state.pluginName}Rule:edit`}>
+                  <Button
+                    type="primary"
+                    onClick={this.openToolClick}
+                    style={{ marginLeft: 10 }}
+                  >
+                    {getIntlContent(
+                      ruleList
+                        ? ruleList.some(
+                            (tool) =>
+                              toolSelectedRowKeys.includes(tool.id) &&
+                              tool.enabled,
+                          )
+                          ? "SHENYU.PLUGIN.SELECTOR.BATCH.CLOSED"
+                          : "SHENYU.PLUGIN.SELECTOR.BATCH.OPENED"
+                        : "",
+                    )}
+                  </Button>
+                </AuthButton>
+              </div>
+            </div>
+            <Table
+              size="small"
+              style={{ marginTop: 30 }}
+              bordered
+              columns={toolsColumns}
+              expandedRowRender={expandedRowRender}
+              dataSource={ruleList}
+              rowSelection={toolRowSelection}
+              pagination={{
+                total: toolTotal,
+                showTotal: (showTotal) => `${showTotal}`,
+                showSizeChanger: true,
+                pageSizeOptions: ["12", "20", "50", "100"],
+                current: toolPage,
+                pageSize: toolPageSize,
+                onChange: this.pageToolChange,
+                onShowSizeChange: this.pageToolChangeSize,
+              }}
+            />
+          </Col>
+        </Row>
+        {popup}
+      </div>
+    );
+  }
+}
diff --git a/src/services/api.js b/src/services/api.js
index 2b6ad181..e3b34dd4 100644
--- a/src/services/api.js
+++ b/src/services/api.js
@@ -1356,6 +1356,12 @@ export async function asyncNamespacePlugin(params) {
   });
 }
 
+/* get mcpServer list */
+export async function fetchMcpServer(params) {
+  return request(`${baseUrl}/mcpServer/list?${stringify(params)}`, {
+    method: `GET`,
+  });
+}
 /* getInstancesByNamespace */
 export async function getInstancesByNamespace(params) {
   return request(`${baseUrl}/instance?${stringify(params)}`, {
@@ -1363,6 +1369,33 @@ export async function getInstancesByNamespace(params) {
   });
 }
 
+/* add mcpServer */
+export async function addMcpServer(params) {
+  return request(`${baseUrl}/mcpServer/add`, {
+    method: `POST`,
+    body: {
+      ...params,
+    },
+  });
+}
+
+/* update mcpServer */
+export async function updateMcpServer(params) {
+  return request(`${baseUrl}/mcpServer/update`, {
+    method: `PUT`,
+    body: {
+      ...params,
+    },
+  });
+}
+
+/* delete mcpServer */
+export async function deleteMcpServer(params) {
+  return request(`${baseUrl}/mcpServer/delete`, {
+    method: `DELETE`,
+    body: [...params.list],
+  });
+}
 /* findInstance */
 export async function findInstance(params) {
   return request(`${baseUrl}/instance/${params.id}`, {

Reply via email to