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 <[email protected]>
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}`, {