This is an automated email from the ASF dual-hosted git repository.
liuhongyu 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 4a58f3e9 Feat mcp streamable http (#536)
4a58f3e9 is described below
commit 4a58f3e9dc0b582c060baf5032d88776c803773c
Author: 有若旭 <[email protected]>
AuthorDate: Mon Jul 21 11:08:31 2025 +0800
Feat mcp streamable http (#536)
* [feat] mcp server plugin, Add streamable http support & Add json edit for
Mcp plugin.
* [feat] mcp server plugin, fix es lint for mcp plugin.
---
src/locales/en-US.json | 103 ++-
src/locales/zh-CN.json | 103 ++-
src/routes/Plugin/McpServer/JsonEditModal.js | 709 ++++++++++++++++++++
src/routes/Plugin/McpServer/McpConfigModal.js | 465 +++++++++++++
src/routes/Plugin/McpServer/ToolsModal.js | 919 ++++++++++++++++++++------
src/routes/Plugin/McpServer/index.js | 144 +++-
6 files changed, 2226 insertions(+), 217 deletions(-)
diff --git a/src/locales/en-US.json b/src/locales/en-US.json
index 538211ed..77895823 100644
--- a/src/locales/en-US.json
+++ b/src/locales/en-US.json
@@ -521,5 +521,106 @@
"SHENYU.NAMESPACE.INPUTDESC":"description",
"SHENYU.NAMESPACE.INPUTNAMESPACEID":"namespaceId",
"SHENYU.NAMESPACE.ALERTNAMESPACEID":"Automatically generated namespaceId",
- "SHENYU.MENU.SYSTEM.MANAGMENT.NAMESPACEPLUGIN":"Plugin"
+ "SHENYU.MENU.SYSTEM.MANAGMENT.NAMESPACEPLUGIN":"Plugin",
+ "SHENYU.DISCOVERY.SELECTOR.UPSTREAM.WEIGHT": "Weight",
+ "SHENYU.DISCOVERY.SELECTOR.UPSTREAM.WARMUP": "Warmup",
+ "SHENYU.DISCOVERY.SELECTOR.UPSTREAM.GRAY": "Gray",
+ "SHENYU.DISCOVERY.SELECTOR.UPSTREAM.STARTUP": "Startup",
+ "SHENYU.DISCOVERY.SELECTOR.UPSTREAM.PROTOCOL": "Protocol",
+ "SHENYU.DISCOVERY.SELECTOR.UPSTREAM.STATUS": "Status",
+ "SHENYU.DISCOVERY.SELECTOR.UPSTREAM.URL": "URL",
+ "SHENYU.DISCOVERY.SELECTOR.UPSTREAM.TIMESTAMP": "Timestamp",
+ "SHENYU.DISCOVERY.SELECTOR.UPSTREAM.TOTAL": "Total",
+ "SHENYU.DISCOVERY.SELECTOR.UPSTREAM.HEALTHY": "Healthy",
+ "SHENYU.DISCOVERY.SELECTOR.UPSTREAM.UNHEALTHY": "Unhealthy",
+ "SHENYU.DISCOVERY.SELECTOR.UPSTREAM.LISTEN": "Listen",
+ "SHENYU.DISCOVERY.SELECTOR.UPSTREAM.HANDLER": "Handler",
+ "SHENYU.DISCOVERY.SELECTOR.UPSTREAM.SELECTED": "Selected",
+ "SHENYU.DISCOVERY.SELECTOR.UPSTREAM.SELECTED.TOTAL": "Selected Total",
+ "SHENYU.DISCOVERY.SELECTOR.UPSTREAM.DETECTED": "Detected",
+ "SHENYU.DISCOVERY.SELECTOR.UPSTREAM.DETECTED.TOTAL": "Detected Total",
+ "SHENYU.DISCOVERY.CONFIG.DISCOVERYTYPE": "Discovery Type",
+ "SHENYU.DISCOVERY.CONFIG.SERVERLIST": "Server List",
+ "SHENYU.DISCOVERY.CONFIG.PROPS": "Properties",
+ "SHENYU.DISCOVERY.CONFIG.LISTENERNODE": "Listener Node",
+ "SHENYU.DISCOVERY.CONFIG.HANDLER": "Handler",
+ "SHENYU.DISCOVERY.CONFIG.PROPS.PLACEHOLDER": "Please enter properties (JSON
format)",
+ "SHENYU.DISCOVERY.CONFIG.SERVERLIST.PLACEHOLDER": "Please enter server list",
+ "SHENYU.DISCOVERY.CONFIG.LISTENERNODE.PLACEHOLDER": "Please enter listener
node",
+ "SHENYU.DISCOVERY.CONFIG.HANDLER.PLACEHOLDER": "Please enter handler (JSON
format)",
+ "SHENYU.DISCOVERY.CONFIG.DISCOVERYTYPE.PLACEHOLDER": "Please select
discovery type",
+ "SHENYU.MCP.JSON.EDIT.TITLE": "JSON Edit Handle Configuration",
+ "SHENYU.MCP.JSON.EDIT.DESCRIPTION": "You can directly edit the JSON
configuration of the Handle field. Support copy and paste the entire JSON
text.",
+ "SHENYU.MCP.JSON.EDIT.FORMAT": "Format",
+ "SHENYU.MCP.JSON.EDIT.COMPRESS": "Compress",
+ "SHENYU.MCP.JSON.EDIT.COPY": "Copy All",
+ "SHENYU.MCP.JSON.EDIT.TAB.TEXT": "Text Edit",
+ "SHENYU.MCP.JSON.EDIT.TAB.PREVIEW": "Visual Preview",
+ "SHENYU.MCP.JSON.EDIT.ERROR.PREFIX": "Format Error: ",
+ "SHENYU.MCP.JSON.EDIT.HANDLE.TITLE": "Handle Field Structure Description:",
+ "SHENYU.MCP.JSON.EDIT.HANDLE.PARAMETERS": "Tool parameter list, including
name, type, description and other fields",
+ "SHENYU.MCP.JSON.EDIT.HANDLE.REQUESTCONFIG": "Request configuration
information, usually in JSON string format",
+ "SHENYU.MCP.JSON.EDIT.HANDLE.DESCRIPTION": "Detailed description of the
tool",
+ "SHENYU.MCP.JSON.EDIT.OPERATION.TITLE": "Operation Instructions:",
+ "SHENYU.MCP.JSON.EDIT.OPERATION.KEYBOARD": "Support Ctrl+A to select all,
Ctrl+C to copy, Ctrl+V to paste Handle configuration",
+ "SHENYU.MCP.JSON.EDIT.OPERATION.SWITCH": "You can switch between text
editing and visual preview",
+ "SHENYU.MCP.JSON.EDIT.OPERATION.VALIDATE": "JSON format will be
automatically validated before saving",
+ "SHENYU.MCP.JSON.EDIT.OPERATION.ONLY.HANDLE": "Only the Handle field will be
updated, other tool information remains unchanged",
+ "SHENYU.MCP.JSON.EDIT.PLACEHOLDER": "Please enter Handle configuration in
JSON format",
+ "SHENYU.MCP.JSON.EDIT.EMPTY.ERROR": "Handle configuration cannot be empty",
+ "SHENYU.MCP.JSON.EDIT.FORMAT.ERROR": "JSON Format Error: ",
+ "SHENYU.MCP.JSON.EDIT.FORMAT.SUCCESS": "JSON formatted successfully",
+ "SHENYU.MCP.JSON.EDIT.FORMAT.FAILED": "JSON format is incorrect and cannot
be formatted",
+ "SHENYU.MCP.JSON.EDIT.COMPRESS.SUCCESS": "JSON compressed successfully",
+ "SHENYU.MCP.JSON.EDIT.COMPRESS.FAILED": "JSON format is incorrect and cannot
be compressed",
+ "SHENYU.MCP.JSON.EDIT.COPY.SUCCESS": "Copied to clipboard",
+ "SHENYU.MCP.JSON.EDIT.COPY.FAILED": "Copy failed",
+ "SHENYU.MCP.JSON.EDIT.UPDATE.SUCCESS": "Tool data updated successfully",
+ "SHENYU.MCP.EDIT.JSON": "EditJSON",
+ "SHENYU.MCP.JSON.EDIT.TOOL.NAME": "Tool Name",
+ "SHENYU.MCP.JSON.EDIT.TOOL.NAME.PLACEHOLDER": "Please enter tool name",
+ "SHENYU.MCP.JSON.EDIT.TOOL.NAME.ERROR": "Tool name cannot be empty",
+ "SHENYU.MCP.JSON.EDIT.HANDLE.FIELD": "Handle Configuration",
+ "SHENYU.MCP.JSON.EDIT.MODE.SEPARATE": "Separate Edit",
+ "SHENYU.MCP.JSON.EDIT.MODE.UNIFIED": "Unified Edit",
+ "SHENYU.MCP.JSON.EDIT.UNIFIED.TITLE": "Unified JSON Edit",
+ "SHENYU.MCP.JSON.EDIT.UNIFIED.DESCRIPTION": "You can directly edit the
complete JSON data including tool name and Handle configuration.",
+ "SHENYU.MCP.JSON.EDIT.UNIFIED.STRUCTURE.TITLE": "Complete Data Structure
Description:",
+ "SHENYU.MCP.JSON.EDIT.UNIFIED.STRUCTURE.NAME": "Tool's name identifier",
+ "SHENYU.MCP.JSON.EDIT.UNIFIED.STRUCTURE.HANDLE": "Tool's complete Handle
configuration, including parameters, requestConfig, etc.",
+ "SHENYU.MCP.JSON.EDIT.UNIFIED.PLACEHOLDER": "Please enter complete JSON
configuration containing name and handle fields",
+ "SHENYU.MCP.JSON.EDIT.OPERATION.UNIFIED.UPDATE": "Update both tool name and
Handle fields simultaneously",
+ "SHENYU.MCP.TOOLS.ADD.MODE.FORM": "Form Edit",
+ "SHENYU.MCP.TOOLS.ADD.MODE.JSON": "JSON Edit",
+ "SHENYU.MCP.TOOLS.ADD.JSON.TITLE": "JSON Add Tool",
+ "SHENYU.MCP.TOOLS.ADD.JSON.DESCRIPTION": "You can directly edit the complete
tool JSON configuration containing all necessary fields.",
+ "SHENYU.MCP.TOOLS.ADD.JSON.STRUCTURE.TITLE": "Complete Tool Data Structure
Description:",
+ "SHENYU.MCP.TOOLS.ADD.JSON.STRUCTURE.NAME": "Tool's name identifier",
+ "SHENYU.MCP.TOOLS.ADD.JSON.STRUCTURE.DESCRIPTION": "Tool's detailed
description",
+ "SHENYU.MCP.TOOLS.ADD.JSON.STRUCTURE.ENABLED": "Whether to enable this tool",
+ "SHENYU.MCP.TOOLS.ADD.JSON.STRUCTURE.HANDLE": "Tool's complete Handle
configuration, including parameters, requestConfig, etc.",
+ "SHENYU.MCP.TOOLS.ADD.JSON.PLACEHOLDER": "Please enter complete tool JSON
configuration containing name, description, enabled, parameters, requestConfig
and other fields",
+ "SHENYU.MCP.TOOLS.ADD.JSON.TEMPLATE": "Generate Template",
+ "SHENYU.MCP.TOOLS.ADD.JSON.TEMPLATE.SUCCESS": "Tool template generated
successfully",
+ "SHENYU.MCP.TOOLS.ADD.JSON.OPERATION.TEMPLATE": "Support one-click template
generation with complete tool structure examples",
+ "SHENYU.MCP.CONFIG.SSE": "SSE Config",
+ "SHENYU.MCP.CONFIG.STREAMABLE": "Streamable Config",
+ "SHENYU.MCP.CONFIG.SSE.TITLE": "SSE Protocol MCP Service Configuration",
+ "SHENYU.MCP.CONFIG.STREAMABLE.TITLE": "Streamable HTTP Protocol MCP Service
Configuration",
+ "SHENYU.MCP.CONFIG.DESCRIPTION": "The following configuration can be used
for MCP client integration with ShenYu gateway",
+ "SHENYU.MCP.CONFIG.SERVICE.NAME": "Service Name",
+ "SHENYU.MCP.CONFIG.SERVICE.DESCRIPTION": "Service Description",
+ "SHENYU.MCP.CONFIG.SERVICE.URL": "Service URL",
+ "SHENYU.MCP.CONFIG.SERVICE.HEADERS": "Request Headers",
+ "SHENYU.MCP.CONFIG.SERVICE.TRANSPORT": "Transport Protocol",
+ "SHENYU.MCP.CONFIG.COPY.SUCCESS": "Configuration copied to clipboard",
+ "SHENYU.MCP.CONFIG.COPY.FAILED": "Failed to copy configuration",
+ "SHENYU.MCP.CONFIG.EXPLANATION.TITLE": "Configuration Description:",
+ "SHENYU.MCP.CONFIG.EXPLANATION.URL": "MCP service access address, generated
based on selector rules",
+ "SHENYU.MCP.CONFIG.EXPLANATION.NAME": "Service display name",
+ "SHENYU.MCP.CONFIG.EXPLANATION.DESCRIPTION": "Service detailed description",
+ "SHENYU.MCP.CONFIG.EXPLANATION.HEADERS": "HTTP headers required for
requests. X-Client-ID and Authorization should be filled according to actual
situation",
+ "SHENYU.MCP.CONFIG.EXPLANATION.TRANSPORT": "Transport protocol type",
+ "SHENYU.MCP.CONFIG.JSON.TITLE": "JSON Configuration (Copy Ready):",
+ "SHENYU.MCP.CONFIG.COPY.JSON": "Copy JSON"
}
diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json
index 997ab6db..fcf4e881 100644
--- a/src/locales/zh-CN.json
+++ b/src/locales/zh-CN.json
@@ -527,5 +527,106 @@
"SHENYU.NAMESPACE.INPUTDESC":"请输入命名空间描述",
"SHENYU.NAMESPACE.INPUTNAMESPACEID":"请输入namespaceId",
"SHENYU.NAMESPACE.ALERTNAMESPACEID":"系统自动生成namespaceId",
- "SHENYU.MENU.SYSTEM.MANAGMENT.NAMESPACEPLUGIN":"插件管理"
+ "SHENYU.MENU.SYSTEM.MANAGMENT.NAMESPACEPLUGIN":"插件管理",
+ "SHENYU.DISCOVERY.SELECTOR.UPSTREAM.WEIGHT": "权重",
+ "SHENYU.DISCOVERY.SELECTOR.UPSTREAM.WARMUP": "热身",
+ "SHENYU.DISCOVERY.SELECTOR.UPSTREAM.GRAY": "灰度",
+ "SHENYU.DISCOVERY.SELECTOR.UPSTREAM.STARTUP": "启动",
+ "SHENYU.DISCOVERY.SELECTOR.UPSTREAM.PROTOCOL": "协议",
+ "SHENYU.DISCOVERY.SELECTOR.UPSTREAM.STATUS": "状态",
+ "SHENYU.DISCOVERY.SELECTOR.UPSTREAM.URL": "URL",
+ "SHENYU.DISCOVERY.SELECTOR.UPSTREAM.TIMESTAMP": "时间戳",
+ "SHENYU.DISCOVERY.SELECTOR.UPSTREAM.TOTAL": "总数",
+ "SHENYU.DISCOVERY.SELECTOR.UPSTREAM.HEALTHY": "健康",
+ "SHENYU.DISCOVERY.SELECTOR.UPSTREAM.UNHEALTHY": "不健康",
+ "SHENYU.DISCOVERY.SELECTOR.UPSTREAM.LISTEN": "监听",
+ "SHENYU.DISCOVERY.SELECTOR.UPSTREAM.HANDLER": "处理器",
+ "SHENYU.DISCOVERY.SELECTOR.UPSTREAM.SELECTED": "被选中",
+ "SHENYU.DISCOVERY.SELECTOR.UPSTREAM.SELECTED.TOTAL": "被选中总数",
+ "SHENYU.DISCOVERY.SELECTOR.UPSTREAM.DETECTED": "检测到",
+ "SHENYU.DISCOVERY.SELECTOR.UPSTREAM.DETECTED.TOTAL": "检测到总数",
+ "SHENYU.DISCOVERY.CONFIG.DISCOVERYTYPE": "发现类型",
+ "SHENYU.DISCOVERY.CONFIG.SERVERLIST": "服务器列表",
+ "SHENYU.DISCOVERY.CONFIG.PROPS": "属性",
+ "SHENYU.DISCOVERY.CONFIG.LISTENERNODE": "监听节点",
+ "SHENYU.DISCOVERY.CONFIG.HANDLER": "处理器",
+ "SHENYU.DISCOVERY.CONFIG.PROPS.PLACEHOLDER": "请输入属性(JSON 格式)",
+ "SHENYU.DISCOVERY.CONFIG.SERVERLIST.PLACEHOLDER": "请输入服务器列表",
+ "SHENYU.DISCOVERY.CONFIG.LISTENERNODE.PLACEHOLDER": "请输入监听节点",
+ "SHENYU.DISCOVERY.CONFIG.HANDLER.PLACEHOLDER": "请输入处理器(JSON 格式)",
+ "SHENYU.DISCOVERY.CONFIG.DISCOVERYTYPE.PLACEHOLDER": "请选择发现类型",
+ "SHENYU.MCP.JSON.EDIT.TITLE": "JSON编辑Handle配置",
+ "SHENYU.MCP.JSON.EDIT.DESCRIPTION": "您可以直接编辑Handle字段的JSON配置。支持复制粘贴整个JSON文本。",
+ "SHENYU.MCP.JSON.EDIT.FORMAT": "格式化",
+ "SHENYU.MCP.JSON.EDIT.COMPRESS": "压缩",
+ "SHENYU.MCP.JSON.EDIT.COPY": "复制全部",
+ "SHENYU.MCP.JSON.EDIT.TAB.TEXT": "文本编辑",
+ "SHENYU.MCP.JSON.EDIT.TAB.PREVIEW": "可视化预览",
+ "SHENYU.MCP.JSON.EDIT.ERROR.PREFIX": "格式错误: ",
+ "SHENYU.MCP.JSON.EDIT.HANDLE.TITLE": "Handle字段结构说明:",
+ "SHENYU.MCP.JSON.EDIT.HANDLE.PARAMETERS":
"工具参数列表,包含name、type、description等字段",
+ "SHENYU.MCP.JSON.EDIT.HANDLE.REQUESTCONFIG": "请求配置信息,通常是JSON字符串格式",
+ "SHENYU.MCP.JSON.EDIT.HANDLE.DESCRIPTION": "工具的详细描述信息",
+ "SHENYU.MCP.JSON.EDIT.OPERATION.TITLE": "操作说明:",
+ "SHENYU.MCP.JSON.EDIT.OPERATION.KEYBOARD":
"支持Ctrl+A全选,Ctrl+C复制,Ctrl+V粘贴Handle配置",
+ "SHENYU.MCP.JSON.EDIT.OPERATION.SWITCH": "可以在文本编辑和可视化预览之间切换查看",
+ "SHENYU.MCP.JSON.EDIT.OPERATION.VALIDATE": "保存前会自动验证JSON格式的正确性",
+ "SHENYU.MCP.JSON.EDIT.OPERATION.ONLY.HANDLE": "只会更新Handle字段,其他工具信息保持不变",
+ "SHENYU.MCP.JSON.EDIT.PLACEHOLDER": "请输入JSON格式的Handle配置",
+ "SHENYU.MCP.JSON.EDIT.EMPTY.ERROR": "Handle配置不能为空",
+ "SHENYU.MCP.JSON.EDIT.FORMAT.ERROR": "JSON格式错误: ",
+ "SHENYU.MCP.JSON.EDIT.FORMAT.SUCCESS": "JSON格式化成功",
+ "SHENYU.MCP.JSON.EDIT.FORMAT.FAILED": "JSON格式不正确,无法格式化",
+ "SHENYU.MCP.JSON.EDIT.COMPRESS.SUCCESS": "JSON压缩成功",
+ "SHENYU.MCP.JSON.EDIT.COMPRESS.FAILED": "JSON格式不正确,无法压缩",
+ "SHENYU.MCP.JSON.EDIT.COPY.SUCCESS": "已复制到剪贴板",
+ "SHENYU.MCP.JSON.EDIT.COPY.FAILED": "复制失败",
+ "SHENYU.MCP.JSON.EDIT.UPDATE.SUCCESS": "工具数据更新成功",
+ "SHENYU.MCP.EDIT.JSON": "EditJSON",
+ "SHENYU.MCP.JSON.EDIT.TOOL.NAME": "工具名称",
+ "SHENYU.MCP.JSON.EDIT.TOOL.NAME.PLACEHOLDER": "请输入工具名称",
+ "SHENYU.MCP.JSON.EDIT.TOOL.NAME.ERROR": "工具名称不能为空",
+ "SHENYU.MCP.JSON.EDIT.HANDLE.FIELD": "Handle配置",
+ "SHENYU.MCP.JSON.EDIT.MODE.SEPARATE": "分别编辑",
+ "SHENYU.MCP.JSON.EDIT.MODE.UNIFIED": "统一编辑",
+ "SHENYU.MCP.JSON.EDIT.UNIFIED.TITLE": "统一JSON编辑",
+ "SHENYU.MCP.JSON.EDIT.UNIFIED.DESCRIPTION":
"您可以直接编辑包含工具名称和Handle配置的完整JSON数据。",
+ "SHENYU.MCP.JSON.EDIT.UNIFIED.STRUCTURE.TITLE": "完整数据结构说明:",
+ "SHENYU.MCP.JSON.EDIT.UNIFIED.STRUCTURE.NAME": "工具的名称标识",
+ "SHENYU.MCP.JSON.EDIT.UNIFIED.STRUCTURE.HANDLE":
"工具的完整Handle配置,包含parameters、requestConfig等",
+ "SHENYU.MCP.JSON.EDIT.UNIFIED.PLACEHOLDER": "请输入包含name和handle字段的完整JSON配置",
+ "SHENYU.MCP.JSON.EDIT.OPERATION.UNIFIED.UPDATE": "同时更新工具名称和Handle字段",
+ "SHENYU.MCP.TOOLS.ADD.MODE.FORM": "表单编辑",
+ "SHENYU.MCP.TOOLS.ADD.MODE.JSON": "JSON编辑",
+ "SHENYU.MCP.TOOLS.ADD.JSON.TITLE": "JSON添加工具",
+ "SHENYU.MCP.TOOLS.ADD.JSON.DESCRIPTION": "您可以直接编辑完整的工具JSON配置,包含所有必要字段。",
+ "SHENYU.MCP.TOOLS.ADD.JSON.STRUCTURE.TITLE": "完整工具数据结构说明:",
+ "SHENYU.MCP.TOOLS.ADD.JSON.STRUCTURE.NAME": "工具的名称标识",
+ "SHENYU.MCP.TOOLS.ADD.JSON.STRUCTURE.DESCRIPTION": "工具的详细描述",
+ "SHENYU.MCP.TOOLS.ADD.JSON.STRUCTURE.ENABLED": "是否启用该工具",
+ "SHENYU.MCP.TOOLS.ADD.JSON.STRUCTURE.HANDLE":
"工具的完整Handle配置,包含parameters、requestConfig等",
+ "SHENYU.MCP.TOOLS.ADD.JSON.PLACEHOLDER":
"请输入包含name、description、enabled、parameters、requestConfig等字段的完整工具JSON配置",
+ "SHENYU.MCP.TOOLS.ADD.JSON.TEMPLATE": "生成模板",
+ "SHENYU.MCP.TOOLS.ADD.JSON.TEMPLATE.SUCCESS": "已生成工具模板",
+ "SHENYU.MCP.TOOLS.ADD.JSON.OPERATION.TEMPLATE": "支持一键生成模板,包含完整的工具结构示例",
+ "SHENYU.MCP.CONFIG.SSE": "SSE配置",
+ "SHENYU.MCP.CONFIG.STREAMABLE": "Streamable配置",
+ "SHENYU.MCP.CONFIG.SSE.TITLE": "SSE协议MCP服务配置",
+ "SHENYU.MCP.CONFIG.STREAMABLE.TITLE": "Streamable HTTP协议MCP服务配置",
+ "SHENYU.MCP.CONFIG.DESCRIPTION": "以下配置可用于MCP客户端与ShenYu网关的集成",
+ "SHENYU.MCP.CONFIG.SERVICE.NAME": "服务名称",
+ "SHENYU.MCP.CONFIG.SERVICE.DESCRIPTION": "服务描述",
+ "SHENYU.MCP.CONFIG.SERVICE.URL": "服务URL",
+ "SHENYU.MCP.CONFIG.SERVICE.HEADERS": "请求头",
+ "SHENYU.MCP.CONFIG.SERVICE.TRANSPORT": "传输协议",
+ "SHENYU.MCP.CONFIG.COPY.SUCCESS": "配置已复制到剪贴板",
+ "SHENYU.MCP.CONFIG.COPY.FAILED": "复制配置失败",
+ "SHENYU.MCP.CONFIG.EXPLANATION.TITLE": "配置说明:",
+ "SHENYU.MCP.CONFIG.EXPLANATION.URL": "MCP服务访问地址,根据选择器规则生成",
+ "SHENYU.MCP.CONFIG.EXPLANATION.NAME": "服务显示名称",
+ "SHENYU.MCP.CONFIG.EXPLANATION.DESCRIPTION": "服务详细描述",
+ "SHENYU.MCP.CONFIG.EXPLANATION.HEADERS":
"请求时需要携带的HTTP头信息。X-Client-ID和Authorization需要根据实际情况填写",
+ "SHENYU.MCP.CONFIG.EXPLANATION.TRANSPORT": "传输协议类型",
+ "SHENYU.MCP.CONFIG.JSON.TITLE": "JSON配置(可直接复制):",
+ "SHENYU.MCP.CONFIG.COPY.JSON": "复制JSON"
}
diff --git a/src/routes/Plugin/McpServer/JsonEditModal.js
b/src/routes/Plugin/McpServer/JsonEditModal.js
new file mode 100644
index 00000000..86ef1673
--- /dev/null
+++ b/src/routes/Plugin/McpServer/JsonEditModal.js
@@ -0,0 +1,709 @@
+/*
+ * 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, Modal, message, Input, Row, Col, Tabs, Radio } from "antd";
+import ReactJson from "react-json-view";
+import { getIntlContent } from "../../../utils/IntlUtils";
+
+const { TextArea } = Input;
+const { TabPane } = Tabs;
+
+class JsonEditModal extends Component {
+ constructor(props) {
+ super(props);
+
+ // 解析handle字段,提升到最外层
+ let flattenedData = {};
+ let flattenedText = "{}";
+ let toolName = "";
+ let unifiedText = "{}";
+
+ if (props.data) {
+ toolName = props.data.name || "";
+
+ // 将handle内容提升到最外层
+ if (props.data.handle) {
+ try {
+ const handleObj = JSON.parse(props.data.handle);
+ flattenedData = {
+ name: toolName,
+ parameters: handleObj.parameters || [],
+ requestConfig: handleObj.requestConfig || "{}",
+ description: handleObj.description || "",
+ };
+ flattenedText = JSON.stringify(flattenedData, null, 2);
+ } catch (e) {
+ // Failed to parse handle JSON
+ flattenedData = {
+ name: toolName,
+ parameters: [],
+ requestConfig: "{}",
+ description: "",
+ };
+ flattenedText = JSON.stringify(flattenedData, null, 2);
+ }
+ } else {
+ flattenedData = {
+ name: toolName,
+ parameters: [],
+ requestConfig: "{}",
+ description: "",
+ };
+ flattenedText = JSON.stringify(flattenedData, null, 2);
+ }
+ }
+
+ // 统一编辑模式使用相同的扁平化数据
+ unifiedText = flattenedText;
+
+ this.state = {
+ originalData: props.data || {},
+ toolName,
+ flattenedText,
+ unifiedText,
+ editMode: "separate", // "separate" 或 "unified"
+ activeTab: "1",
+ parseError: null,
+ unifiedParseError: null,
+ toolNameError: null,
+ };
+ }
+
+ componentDidUpdate(prevProps) {
+ if (prevProps.data !== this.props.data && this.props.data) {
+ let flattenedData = {};
+ let flattenedText = "{}";
+ let toolName = "";
+ let unifiedText = "{}";
+
+ toolName = this.props.data.name || "";
+
+ // 将handle内容提升到最外层
+ if (this.props.data.handle) {
+ try {
+ const handleObj = JSON.parse(this.props.data.handle);
+ flattenedData = {
+ name: toolName,
+ parameters: handleObj.parameters || [],
+ requestConfig: handleObj.requestConfig || "{}",
+ description: handleObj.description || "",
+ };
+ flattenedText = JSON.stringify(flattenedData, null, 2);
+ } catch (e) {
+ flattenedData = {
+ name: toolName,
+ parameters: [],
+ requestConfig: "{}",
+ description: "",
+ };
+ flattenedText = JSON.stringify(flattenedData, null, 2);
+ }
+ } else {
+ flattenedData = {
+ name: toolName,
+ parameters: [],
+ requestConfig: "{}",
+ description: "",
+ };
+ flattenedText = JSON.stringify(flattenedData, null, 2);
+ }
+
+ // 统一编辑模式使用相同的扁平化数据
+ unifiedText = flattenedText;
+
+ this.setState({
+ originalData: { ...this.props.data },
+ toolName,
+ flattenedText,
+ unifiedText,
+ parseError: null,
+ unifiedParseError: null,
+ toolNameError: null,
+ });
+ }
+ }
+
+ handleToolNameChange = (e) => {
+ const toolName = e.target.value;
+ this.setState({
+ toolName,
+ toolNameError: null,
+ });
+ };
+
+ handleTextChange = (e) => {
+ const flattenedText = e.target.value;
+ this.setState({ flattenedText });
+
+ // 实时验证JSON格式
+ try {
+ JSON.parse(flattenedText);
+ this.setState({ parseError: null });
+ } catch (error) {
+ this.setState({ parseError: error.message });
+ }
+ };
+
+ handleUnifiedTextChange = (e) => {
+ const unifiedText = e.target.value;
+ this.setState({ unifiedText });
+
+ // 实时验证JSON格式
+ try {
+ const parsed = JSON.parse(unifiedText);
+ this.setState({ unifiedParseError: null });
+
+ // 同步更新分别编辑模式的数据
+ if (parsed.name !== undefined) {
+ this.setState({ toolName: parsed.name });
+ }
+
+ // 更新扁平化文本
+ this.setState({
+ flattenedText: JSON.stringify(parsed, null, 2),
+ parseError: null,
+ });
+ } catch (error) {
+ this.setState({ unifiedParseError: error.message });
+ }
+ };
+
+ handleEditModeChange = (e) => {
+ const editMode = e.target.value;
+ this.setState({ editMode });
+
+ // 切换到统一编辑模式时,同步数据
+ if (editMode === "unified") {
+ try {
+ const flattenedJson = JSON.parse(this.state.flattenedText);
+ // 确保toolName被包含在JSON中
+ flattenedJson.name = this.state.toolName;
+ const unifiedText = JSON.stringify(flattenedJson, null, 2);
+ this.setState({
+ unifiedText,
+ unifiedParseError: null,
+ });
+ } catch (error) {
+ // 如果解析失败,使用基础模板
+ const basicJson = {
+ name: this.state.toolName,
+ parameters: [],
+ requestConfig: "{}",
+ description: "",
+ };
+ const unifiedText = JSON.stringify(basicJson, null, 2);
+ this.setState({
+ unifiedText,
+ unifiedParseError: null,
+ });
+ }
+ }
+ };
+
+ handleFormatJson = () => {
+ const { editMode } = this.state;
+
+ if (editMode === "separate") {
+ try {
+ const parsed = JSON.parse(this.state.flattenedText);
+ const formatted = JSON.stringify(parsed, null, 2);
+ this.setState({
+ flattenedText: formatted,
+ parseError: null,
+ });
+ message.success(getIntlContent("SHENYU.MCP.JSON.EDIT.FORMAT.SUCCESS"));
+ } catch (error) {
+ message.error(getIntlContent("SHENYU.MCP.JSON.EDIT.FORMAT.FAILED"));
+ }
+ } else {
+ try {
+ const parsed = JSON.parse(this.state.unifiedText);
+ const formatted = JSON.stringify(parsed, null, 2);
+ this.setState({
+ unifiedText: formatted,
+ unifiedParseError: null,
+ });
+ message.success(getIntlContent("SHENYU.MCP.JSON.EDIT.FORMAT.SUCCESS"));
+ } catch (error) {
+ message.error(getIntlContent("SHENYU.MCP.JSON.EDIT.FORMAT.FAILED"));
+ }
+ }
+ };
+
+ handleCompressJson = () => {
+ const { editMode } = this.state;
+
+ if (editMode === "separate") {
+ try {
+ const parsed = JSON.parse(this.state.flattenedText);
+ const compressed = JSON.stringify(parsed);
+ this.setState({
+ flattenedText: compressed,
+ parseError: null,
+ });
+ message.success(
+ getIntlContent("SHENYU.MCP.JSON.EDIT.COMPRESS.SUCCESS"),
+ );
+ } catch (error) {
+ message.error(getIntlContent("SHENYU.MCP.JSON.EDIT.COMPRESS.FAILED"));
+ }
+ } else {
+ try {
+ const parsed = JSON.parse(this.state.unifiedText);
+ const compressed = JSON.stringify(parsed);
+ this.setState({
+ unifiedText: compressed,
+ unifiedParseError: null,
+ });
+ message.success(
+ getIntlContent("SHENYU.MCP.JSON.EDIT.COMPRESS.SUCCESS"),
+ );
+ } catch (error) {
+ message.error(getIntlContent("SHENYU.MCP.JSON.EDIT.COMPRESS.FAILED"));
+ }
+ }
+ };
+
+ handleCopyToClipboard = () => {
+ const { editMode } = this.state;
+ const textToCopy =
+ editMode === "separate"
+ ? this.state.flattenedText
+ : this.state.unifiedText;
+
+ navigator.clipboard
+ .writeText(textToCopy)
+ .then(() => {
+ message.success(getIntlContent("SHENYU.MCP.JSON.EDIT.COPY.SUCCESS"));
+ })
+ .catch(() => {
+ message.error(getIntlContent("SHENYU.MCP.JSON.EDIT.COPY.FAILED"));
+ });
+ };
+
+ handleOk = () => {
+ const { editMode, toolName, flattenedText, unifiedText, originalData } =
+ this.state;
+ const { onOk } = this.props;
+
+ let finalJsonData = {};
+
+ if (editMode === "separate") {
+ // 分别编辑模式验证
+ if (!toolName.trim()) {
+ this.setState({
+ toolNameError:
getIntlContent("SHENYU.MCP.JSON.EDIT.TOOL.NAME.ERROR"),
+ });
+ return;
+ }
+
+ if (!flattenedText.trim()) {
+ message.error(getIntlContent("SHENYU.MCP.JSON.EDIT.EMPTY.ERROR"));
+ return;
+ }
+
+ try {
+ const parsed = JSON.parse(flattenedText);
+ // 确保name字段是从toolName输入框获取的
+ finalJsonData = {
+ ...parsed,
+ name: toolName,
+ };
+ } catch (error) {
+ message.error(
+
`${getIntlContent("SHENYU.MCP.JSON.EDIT.FORMAT.ERROR")}${error.message}`,
+ );
+ return;
+ }
+ } else {
+ // 统一编辑模式验证
+ if (!unifiedText.trim()) {
+ message.error(getIntlContent("SHENYU.MCP.JSON.EDIT.EMPTY.ERROR"));
+ return;
+ }
+
+ try {
+ finalJsonData = JSON.parse(unifiedText);
+
+ if (!finalJsonData.name || !finalJsonData.name.trim()) {
+
message.error(getIntlContent("SHENYU.MCP.JSON.EDIT.TOOL.NAME.ERROR"));
+ return;
+ }
+ } catch (error) {
+ message.error(
+
`${getIntlContent("SHENYU.MCP.JSON.EDIT.FORMAT.ERROR")}${error.message}`,
+ );
+ return;
+ }
+ }
+
+ // 验证必要字段
+ if (!finalJsonData.parameters) {
+ finalJsonData.parameters = [];
+ }
+ if (!finalJsonData.requestConfig) {
+ finalJsonData.requestConfig = "{}";
+ }
+ if (!finalJsonData.description) {
+ finalJsonData.description = "";
+ }
+
+ // 将扁平化数据转换回原始格式
+ const updatedData = {
+ ...originalData,
+ name: finalJsonData.name,
+ handle: JSON.stringify({
+ parameters: finalJsonData.parameters,
+ requestConfig: finalJsonData.requestConfig,
+ description: finalJsonData.description,
+ }),
+ };
+
+ onOk(updatedData);
+ };
+
+ handleCancel = () => {
+ const { onCancel } = this.props;
+ onCancel();
+ };
+
+ render() {
+ const { visible } = this.props;
+ const {
+ toolName,
+ flattenedText,
+ unifiedText,
+ editMode,
+ parseError,
+ unifiedParseError,
+ toolNameError,
+ activeTab,
+ } = this.state;
+
+ // 用于预览的JSON对象
+ let previewJson = {};
+ let unifiedPreviewJson = {};
+
+ try {
+ previewJson = JSON.parse(flattenedText);
+ } catch (e) {
+ previewJson = {
+ error: getIntlContent("SHENYU.MCP.JSON.EDIT.ERROR.PREFIX") + e.message,
+ };
+ }
+
+ try {
+ unifiedPreviewJson = JSON.parse(unifiedText);
+ } catch (e) {
+ unifiedPreviewJson = {
+ error: getIntlContent("SHENYU.MCP.JSON.EDIT.ERROR.PREFIX") + e.message,
+ };
+ }
+
+ const modalTitle =
+ editMode === "separate"
+ ? getIntlContent("SHENYU.MCP.JSON.EDIT.TITLE")
+ : getIntlContent("SHENYU.MCP.JSON.EDIT.UNIFIED.TITLE");
+
+ return (
+ <Modal
+ title={modalTitle}
+ visible={visible}
+ onOk={this.handleOk}
+ onCancel={this.handleCancel}
+ width={900}
+ okText={getIntlContent("SHENYU.COMMON.SURE")}
+ cancelText={getIntlContent("SHENYU.COMMON.CALCEL")}
+ bodyStyle={{ padding: "20px" }}
+ >
+ <div style={{ marginBottom: "16px" }}>
+ <div style={{ marginBottom: "12px" }}>
+ <Radio.Group value={editMode} onChange={this.handleEditModeChange}>
+ <Radio.Button value="separate">
+ {getIntlContent("SHENYU.MCP.JSON.EDIT.MODE.SEPARATE")}
+ </Radio.Button>
+ <Radio.Button value="unified">
+ {getIntlContent("SHENYU.MCP.JSON.EDIT.MODE.UNIFIED")}
+ </Radio.Button>
+ </Radio.Group>
+ </div>
+
+ <p style={{ color: "#666", fontSize: "12px", marginBottom: "8px" }}>
+ {editMode === "separate"
+ ? getIntlContent("SHENYU.MCP.JSON.EDIT.DESCRIPTION")
+ : getIntlContent("SHENYU.MCP.JSON.EDIT.UNIFIED.DESCRIPTION")}
+ </p>
+
+ <Row gutter={8}>
+ <Col>
+ <Button size="small" onClick={this.handleFormatJson}>
+ {getIntlContent("SHENYU.MCP.JSON.EDIT.FORMAT")}
+ </Button>
+ </Col>
+ <Col>
+ <Button size="small" onClick={this.handleCompressJson}>
+ {getIntlContent("SHENYU.MCP.JSON.EDIT.COMPRESS")}
+ </Button>
+ </Col>
+ <Col>
+ <Button size="small" onClick={this.handleCopyToClipboard}>
+ {getIntlContent("SHENYU.MCP.JSON.EDIT.COPY")}
+ </Button>
+ </Col>
+ </Row>
+ </div>
+
+ {editMode === "separate" ? (
+ <Tabs
+ activeKey={activeTab}
+ onChange={(key) => this.setState({ activeTab: key })}
+ >
+ <TabPane
+ tab={getIntlContent("SHENYU.MCP.JSON.EDIT.TAB.TEXT")}
+ key="1"
+ >
+ <div style={{ marginBottom: "16px" }}>
+ <div style={{ marginBottom: "8px", fontWeight: "bold" }}>
+ {getIntlContent("SHENYU.MCP.JSON.EDIT.TOOL.NAME")}
+ </div>
+ <Input
+ value={toolName}
+ onChange={this.handleToolNameChange}
+ placeholder={getIntlContent(
+ "SHENYU.MCP.JSON.EDIT.TOOL.NAME.PLACEHOLDER",
+ )}
+ style={{ marginBottom: "8px" }}
+ />
+ {toolNameError && (
+ <div
+ style={{
+ color: "#ff4d4f",
+ fontSize: "12px",
+ marginBottom: "8px",
+ }}
+ >
+ {toolNameError}
+ </div>
+ )}
+ </div>
+
+ <div style={{ marginBottom: "8px", fontWeight: "bold" }}>
+ {getIntlContent("SHENYU.MCP.JSON.EDIT.HANDLE.FIELD")}
+ </div>
+ <div style={{ position: "relative" }}>
+ <TextArea
+ value={flattenedText}
+ onChange={this.handleTextChange}
+ placeholder={getIntlContent(
+ "SHENYU.MCP.JSON.EDIT.UNIFIED.PLACEHOLDER",
+ )}
+ autoSize={{ minRows: 12, maxRows: 20 }}
+ style={{
+ fontFamily: "Monaco, Menlo, 'Ubuntu Mono', monospace",
+ fontSize: "13px",
+ lineHeight: "1.4",
+ }}
+ />
+ {parseError && (
+ <div
+ style={{
+ position: "absolute",
+ bottom: "8px",
+ right: "8px",
+ background: "#ff4d4f",
+ color: "white",
+ padding: "4px 8px",
+ borderRadius: "4px",
+ fontSize: "12px",
+ maxWidth: "300px",
+ wordBreak: "break-word",
+ }}
+ >
+ {getIntlContent("SHENYU.MCP.JSON.EDIT.ERROR.PREFIX")}
+ {parseError}
+ </div>
+ )}
+ </div>
+ </TabPane>
+ <TabPane
+ tab={getIntlContent("SHENYU.MCP.JSON.EDIT.TAB.PREVIEW")}
+ key="2"
+ >
+ <div style={{ marginBottom: "16px" }}>
+ <div style={{ marginBottom: "8px", fontWeight: "bold" }}>
+ {getIntlContent("SHENYU.MCP.JSON.EDIT.TOOL.NAME")}
+ </div>
+ <div
+ style={{
+ padding: "8px 12px",
+ backgroundColor: "#f5f5f5",
+ borderRadius: "4px",
+ fontFamily: "Monaco, Menlo, 'Ubuntu Mono', monospace",
+ fontSize: "13px",
+ }}
+ >
+ {toolName ||
+ getIntlContent(
+ "SHENYU.MCP.JSON.EDIT.TOOL.NAME.PLACEHOLDER",
+ )}
+ </div>
+ </div>
+
+ <div style={{ marginBottom: "8px", fontWeight: "bold" }}>
+ {getIntlContent("SHENYU.MCP.JSON.EDIT.HANDLE.FIELD")}
+ </div>
+ <div
+ style={{
+ border: "1px solid #d9d9d9",
+ borderRadius: "4px",
+ maxHeight: "400px",
+ overflow: "auto",
+ }}
+ >
+ <ReactJson
+ src={previewJson}
+ theme="monokai"
+ displayDataTypes={false}
+ displayObjectSize={false}
+ name={false}
+ enableClipboard={false}
+ style={{
+ padding: "16px",
+ fontSize: "13px",
+ }}
+ />
+ </div>
+ </TabPane>
+ </Tabs>
+ ) : (
+ <Tabs
+ activeKey={activeTab}
+ onChange={(key) => this.setState({ activeTab: key })}
+ >
+ <TabPane
+ tab={getIntlContent("SHENYU.MCP.JSON.EDIT.TAB.TEXT")}
+ key="1"
+ >
+ <div style={{ position: "relative" }}>
+ <TextArea
+ value={unifiedText}
+ onChange={this.handleUnifiedTextChange}
+ placeholder={getIntlContent(
+ "SHENYU.MCP.JSON.EDIT.UNIFIED.PLACEHOLDER",
+ )}
+ autoSize={{ minRows: 15, maxRows: 25 }}
+ style={{
+ fontFamily: "Monaco, Menlo, 'Ubuntu Mono', monospace",
+ fontSize: "13px",
+ lineHeight: "1.4",
+ }}
+ />
+ {unifiedParseError && (
+ <div
+ style={{
+ position: "absolute",
+ bottom: "8px",
+ right: "8px",
+ background: "#ff4d4f",
+ color: "white",
+ padding: "4px 8px",
+ borderRadius: "4px",
+ fontSize: "12px",
+ maxWidth: "300px",
+ wordBreak: "break-word",
+ }}
+ >
+ {getIntlContent("SHENYU.MCP.JSON.EDIT.ERROR.PREFIX")}
+ {unifiedParseError}
+ </div>
+ )}
+ </div>
+ </TabPane>
+ <TabPane
+ tab={getIntlContent("SHENYU.MCP.JSON.EDIT.TAB.PREVIEW")}
+ key="2"
+ >
+ <div
+ style={{
+ border: "1px solid #d9d9d9",
+ borderRadius: "4px",
+ maxHeight: "400px",
+ overflow: "auto",
+ }}
+ >
+ <ReactJson
+ src={unifiedPreviewJson}
+ theme="monokai"
+ displayDataTypes={false}
+ displayObjectSize={false}
+ name={false}
+ enableClipboard={false}
+ style={{
+ padding: "16px",
+ fontSize: "13px",
+ }}
+ />
+ </div>
+ </TabPane>
+ </Tabs>
+ )}
+
+ <div style={{ marginTop: "16px", color: "#666", fontSize: "12px" }}>
+ <p>
+ <strong>
+ {getIntlContent("SHENYU.MCP.JSON.EDIT.UNIFIED.STRUCTURE.TITLE")}
+ </strong>
+ </p>
+ <ul>
+ <li>
+ <code>name</code>:{" "}
+ {getIntlContent("SHENYU.MCP.JSON.EDIT.UNIFIED.STRUCTURE.NAME")}
+ </li>
+ <li>
+ <code>parameters</code>:{" "}
+ {getIntlContent("SHENYU.MCP.JSON.EDIT.HANDLE.PARAMETERS")}
+ </li>
+ <li>
+ <code>requestConfig</code>:{" "}
+ {getIntlContent("SHENYU.MCP.JSON.EDIT.HANDLE.REQUESTCONFIG")}
+ </li>
+ <li>
+ <code>description</code>:{" "}
+ {getIntlContent("SHENYU.MCP.JSON.EDIT.HANDLE.DESCRIPTION")}
+ </li>
+ </ul>
+ <p>
+ <strong>
+ {getIntlContent("SHENYU.MCP.JSON.EDIT.OPERATION.TITLE")}
+ </strong>
+ </p>
+ <ul>
+
<li>{getIntlContent("SHENYU.MCP.JSON.EDIT.OPERATION.KEYBOARD")}</li>
+ <li>{getIntlContent("SHENYU.MCP.JSON.EDIT.OPERATION.SWITCH")}</li>
+
<li>{getIntlContent("SHENYU.MCP.JSON.EDIT.OPERATION.VALIDATE")}</li>
+ <li>
+ {getIntlContent("SHENYU.MCP.JSON.EDIT.OPERATION.UNIFIED.UPDATE")}
+ </li>
+ </ul>
+ </div>
+ </Modal>
+ );
+ }
+}
+
+export default JsonEditModal;
diff --git a/src/routes/Plugin/McpServer/McpConfigModal.js
b/src/routes/Plugin/McpServer/McpConfigModal.js
new file mode 100644
index 00000000..a0866519
--- /dev/null
+++ b/src/routes/Plugin/McpServer/McpConfigModal.js
@@ -0,0 +1,465 @@
+/*
+ * 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 { Modal, Button, message, Typography, Divider, Input } from "antd";
+import ReactJson from "react-json-view";
+import { getIntlContent } from "../../../utils/IntlUtils";
+
+const { Title, Text } = Typography;
+const { TextArea } = Input;
+
+class McpConfigModal extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ sseConfig: {},
+ streamableConfig: {},
+ customGatewayHost: "", // 用户自定义的网关地址
+ };
+ }
+
+ componentDidMount() {
+ this.generateConfigs();
+ }
+
+ componentDidUpdate(prevProps) {
+ if (prevProps.selectorList !== this.props.selectorList) {
+ this.generateConfigs();
+ }
+ }
+
+ // 生成配置信息
+ generateConfigs = () => {
+ const { selectorList } = this.props;
+
+ if (!selectorList || selectorList.length === 0) {
+ this.setState({
+ sseConfig: this.getDefaultConfig("sse"),
+ streamableConfig: this.getDefaultConfig("streamableHttp"),
+ });
+ return;
+ }
+
+ // 获取网关基础信息
+ const gatewayHost = this.getGatewayHost();
+ const defaultHeaders = this.getDefaultHeaders();
+
+ // 为每种协议类型生成配置
+ const mcpServers = {};
+
+ selectorList.forEach((selector, index) => {
+ const selectorName = selector.name || `selector-${index}`;
+ const selectorDescription = this.getHandleDescription(selector.handle);
+ const selectorUrl = this.getSelectorUrl(selector, gatewayHost);
+
+ // SSE 配置
+ const sseKey = `shenyu-mcp-sse-${selectorName}`;
+ mcpServers[sseKey] = {
+ url: `${selectorUrl}/sse`,
+ name: `${selectorName}服务sse`,
+ description: `${selectorName}服务测试sse - ${selectorDescription}`,
+ headers: defaultHeaders,
+ transport: "sse",
+ };
+
+ // Streamable HTTP 配置
+ const streamableKey = `shenyu-mcp-${selectorName}`;
+ mcpServers[streamableKey] = {
+ url: `${selectorUrl}/streamablehttp`,
+ name: `${selectorName}服务`,
+ description: `${selectorName}服务测试 - ${selectorDescription}`,
+ headers: defaultHeaders,
+ transport: "streamableHttp",
+ };
+ });
+
+ this.setState({
+ sseConfig: { mcpServers: this.filterByTransport(mcpServers, "sse") },
+ streamableConfig: {
+ mcpServers: this.filterByTransport(mcpServers, "streamableHttp"),
+ },
+ });
+ };
+
+ // 获取网关主机地址
+ getGatewayHost = () => {
+ // 优先使用用户自定义的网关地址
+ if (this.state.customGatewayHost && this.state.customGatewayHost.trim()) {
+ let customHost = this.state.customGatewayHost.trim();
+ // 确保包含协议
+ if (
+ !customHost.startsWith("http://") &&
+ !customHost.startsWith("https://")
+ ) {
+ customHost = `http://${customHost}`;
+ }
+ return customHost;
+ }
+
+ // 否则使用当前浏览器地址
+ const protocol = window.location.protocol;
+ const hostname = window.location.hostname;
+ const port = "9195"; // ShenYu默认端口,可以考虑从配置读取
+
+ // 可以在这里添加从配置文件或环境变量读取的逻辑
+ // const configHost = process.env.REACT_APP_SHENYU_HOST;
+ // const configPort = process.env.REACT_APP_SHENYU_PORT;
+
+ return `${protocol}//${hostname}:${port}`;
+ };
+
+ // 获取默认请求头
+ getDefaultHeaders = () => {
+ return {
+ "X-Client-ID": "cursor-client",
+ Authorization:
+ "Bearer
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.E8C6cQxv5N9Qy7JHHZzY3osVbP40TgXnyFEG-Dc2uo0",
+ };
+ };
+
+ // 从selector的handle字段获取描述信息
+ getHandleDescription = (handle) => {
+ if (!handle) return "默认MCP服务";
+
+ try {
+ const handleObj = JSON.parse(handle);
+ return handleObj.description || "默认MCP服务";
+ } catch (e) {
+ return "默认MCP服务";
+ }
+ };
+
+ // 根据传输协议过滤配置
+ filterByTransport = (mcpServers, transport) => {
+ const filtered = {};
+ Object.keys(mcpServers).forEach((key) => {
+ if (mcpServers[key].transport === transport) {
+ filtered[key] = mcpServers[key];
+ }
+ });
+ return filtered;
+ };
+
+ // 获取默认配置
+ getDefaultConfig = (transport) => {
+ const gatewayHost = this.getGatewayHost();
+ const defaultHeaders = this.getDefaultHeaders();
+
+ const key = transport === "sse" ? "shenyu-mcp-sse" : "shenyu-mcp";
+ // 使用更合理的默认路径,与selector路径生成逻辑保持一致
+ const basePath = "/http"; // MCP插件的默认基础路径
+ const urlPath =
+ transport === "sse" ? `${basePath}/sse` : `${basePath}/streamablehttp`;
+ const nameSuffix = transport === "sse" ? "服务sse" : "服务";
+ const descSuffix = transport === "sse" ? "服务测试sse" : "服务测试";
+
+ return {
+ mcpServers: {
+ [key]: {
+ url: `${gatewayHost}${urlPath}`,
+ name: `shenyuMcp${nameSuffix}`,
+ description: `shenyuMcp${descSuffix}`,
+ headers: defaultHeaders,
+ transport,
+ },
+ },
+ };
+ };
+
+ // 复制配置到剪贴板
+ handleCopyConfig = (config) => {
+ const configText = JSON.stringify(config, null, 2);
+ navigator.clipboard
+ .writeText(configText)
+ .then(() => {
+ message.success(getIntlContent("SHENYU.MCP.CONFIG.COPY.SUCCESS"));
+ })
+ .catch(() => {
+ message.error(getIntlContent("SHENYU.MCP.CONFIG.COPY.FAILED"));
+ });
+ };
+
+ // 复制JSON文本
+ copyJsonText = (text) => {
+ navigator.clipboard
+ .writeText(text)
+ .then(() => {
+ message.success(getIntlContent("SHENYU.MCP.CONFIG.COPY.SUCCESS"));
+ })
+ .catch(() => {
+ message.error(getIntlContent("SHENYU.MCP.CONFIG.COPY.FAILED"));
+ });
+ };
+
+ // 根据selector规则生成URL
+ getSelectorUrl = (selector, gatewayHost) => {
+ // 默认路径使用/http,这是MCP插件的通用前缀
+ let basePath = "/http";
+
+ // 尝试从selector的条件中获取更具体的路径信息
+ if (selector.selectorConditions && selector.selectorConditions.length > 0)
{
+ for (let i = 0; i < selector.selectorConditions.length; i += 1) {
+ const condition = selector.selectorConditions[i];
+ // 查找URI相关的条件
+ if (condition.paramType === "uri" && condition.paramValue) {
+ let conditionPath = condition.paramValue.trim();
+
+ // 移除通配符和多余的字符
+ conditionPath = conditionPath
+ .replace(/\/\*\*$/, "") // 移除 /**
+ .replace(/\/\*$/, "") // 移除 /*
+ .replace(/\*$/, ""); // 移除结尾的 *
+
+ // 如果路径不为空且不是根路径,使用它作为基础路径
+ if (
+ conditionPath &&
+ conditionPath !== "/" &&
+ conditionPath !== "/**"
+ ) {
+ // 确保路径以/开头
+ if (!conditionPath.startsWith("/")) {
+ conditionPath = `/${conditionPath}`;
+ }
+ basePath = conditionPath;
+ break; // 找到第一个有效的URI条件就使用
+ }
+ }
+ }
+ }
+
+ return `${gatewayHost}${basePath}`;
+ };
+
+ // 获取从selector提取的路径信息,用于界面显示
+ getSelectorPathInfo = (selector) => {
+ if (
+ !selector ||
+ !selector.selectorConditions ||
+ selector.selectorConditions.length === 0
+ ) {
+ return null;
+ }
+
+ for (let i = 0; i < selector.selectorConditions.length; i += 1) {
+ const condition = selector.selectorConditions[i];
+ if (condition.paramType === "uri" && condition.paramValue) {
+ let conditionPath = condition.paramValue.trim();
+
+ // 移除通配符和多余的字符
+ conditionPath = conditionPath
+ .replace(/\/\*\*$/, "") // 移除 /**
+ .replace(/\/\*$/, "") // 移除 /*
+ .replace(/\*$/, ""); // 移除结尾的 *
+
+ // 如果路径不为空且不是根路径,返回它
+ if (conditionPath && conditionPath !== "/" && conditionPath !== "/**")
{
+ // 确保路径以/开头
+ if (!conditionPath.startsWith("/")) {
+ conditionPath = `/${conditionPath}`;
+ }
+ return conditionPath;
+ }
+ }
+ }
+
+ return "/http (默认)";
+ };
+
+ render() {
+ const { visible, configType, onCancel, selectorList } = this.props;
+ const { sseConfig, streamableConfig } = this.state;
+
+ const isSSE = configType === "sse";
+ const config = isSSE ? sseConfig : streamableConfig;
+ const title = isSSE
+ ? getIntlContent("SHENYU.MCP.CONFIG.SSE.TITLE")
+ : getIntlContent("SHENYU.MCP.CONFIG.STREAMABLE.TITLE");
+
+ // 获取当前selector信息
+ const currentSelector =
+ selectorList && selectorList.length === 1 ? selectorList[0] : null;
+ const selectorInfo = currentSelector
+ ? ` - ${currentSelector.name || "Unnamed Selector"}`
+ : "";
+
+ return (
+ <Modal
+ title={`${title}${selectorInfo}`}
+ visible={visible}
+ onCancel={onCancel}
+ width={800}
+ footer={[
+ <Button
+ key="copy"
+ type="primary"
+ onClick={() => this.handleCopyConfig(config)}
+ >
+ {getIntlContent("SHENYU.MCP.JSON.EDIT.COPY")}
+ </Button>,
+ <Button key="close" onClick={onCancel}>
+ {getIntlContent("SHENYU.COMMON.CALCEL")}
+ </Button>,
+ ]}
+ >
+ <div style={{ marginBottom: "16px" }}>
+ <Text type="secondary">
+ {getIntlContent("SHENYU.MCP.CONFIG.DESCRIPTION")}
+ </Text>
+ </div>
+
+ <div style={{ marginBottom: "16px" }}>
+ <div style={{ marginBottom: "8px" }}>
+ <Text strong>网关地址配置:</Text>
+ </div>
+ <Input
+ placeholder="输入自定义网关地址,如: http://localhost:9195 (留空使用默认)"
+ value={this.state.customGatewayHost}
+ onChange={(e) => {
+ this.setState({ customGatewayHost: e.target.value }, () => {
+ // 当网关地址改变时重新生成配置
+ this.generateConfigs();
+ });
+ }}
+ style={{ marginBottom: "8px" }}
+ />
+ <div style={{ fontSize: "12px", color: "#666" }}>
+ 当前使用: <strong>{this.getGatewayHost()}</strong>
+ </div>
+ </div>
+
+ {currentSelector && (
+ <div style={{ marginBottom: "16px" }}>
+ <Text type="secondary">
+ <>
+ <strong>Selector:</strong> {currentSelector.name}
+ {currentSelector.enabled ? " (启用)" : " (禁用)"}
+ <br />
+ <strong>基础路径:</strong>{" "}
+ {this.getSelectorPathInfo(currentSelector)}
+ <br />
+ <small style={{ color: "#888" }}>
+ 最终URL = 网关地址 + 基础路径 + 协议后缀(/sse 或
+ /streamablehttp)
+ </small>
+ </>
+ </Text>
+ </div>
+ )}
+
+ <Divider />
+
+ <div style={{ marginBottom: "16px" }}>
+ <Title level={5}>
+ {getIntlContent("SHENYU.MCP.CONFIG.SERVICE.TRANSPORT")}:{" "}
+ {isSSE ? "SSE" : "Streamable HTTP"}
+ </Title>
+ </div>
+
+ <div
+ style={{
+ border: "1px solid #d9d9d9",
+ borderRadius: "4px",
+ maxHeight: "500px",
+ overflow: "auto",
+ backgroundColor: "#fafafa",
+ }}
+ >
+ <ReactJson
+ src={config}
+ theme="monokai"
+ displayDataTypes={false}
+ displayObjectSize={false}
+ name={false}
+ enableClipboard={true}
+ collapsed={1}
+ style={{
+ padding: "16px",
+ fontSize: "13px",
+ }}
+ />
+ </div>
+
+ {/* 可复制的JSON文本展示 */}
+ <div style={{ marginTop: "16px" }}>
+ <div
+ style={{
+ display: "flex",
+ justifyContent: "space-between",
+ alignItems: "center",
+ marginBottom: "8px",
+ }}
+ >
+ <Title level={5}>
+ {getIntlContent("SHENYU.MCP.CONFIG.JSON.TITLE")}
+ </Title>
+ <Button
+ size="small"
+ type="primary"
+ onClick={() => this.copyJsonText(JSON.stringify(config, null,
2))}
+ >
+ {getIntlContent("SHENYU.MCP.CONFIG.COPY.JSON")}
+ </Button>
+ </div>
+ <TextArea
+ value={JSON.stringify(config, null, 2)}
+ readOnly
+ autoSize={{ minRows: 6, maxRows: 12 }}
+ style={{
+ fontFamily: "Monaco, Menlo, 'Ubuntu Mono', monospace",
+ fontSize: "12px",
+ backgroundColor: "#f8f8f8",
+ }}
+ />
+ </div>
+
+ {Object.keys(config.mcpServers || {}).length > 0 && (
+ <div style={{ marginTop: "16px" }}>
+ <Title level={5}>
+ {getIntlContent("SHENYU.MCP.CONFIG.EXPLANATION.TITLE")}
+ </Title>
+ <ul style={{ fontSize: "12px", color: "#666" }}>
+ <li>
+ <strong>url</strong>:{" "}
+ {getIntlContent("SHENYU.MCP.CONFIG.EXPLANATION.URL")}
+ </li>
+ <li>
+ <strong>name</strong>:{" "}
+ {getIntlContent("SHENYU.MCP.CONFIG.EXPLANATION.NAME")}
+ </li>
+ <li>
+ <strong>description</strong>:{" "}
+ {getIntlContent("SHENYU.MCP.CONFIG.EXPLANATION.DESCRIPTION")}
+ </li>
+ <li>
+ <strong>headers</strong>:{" "}
+ {getIntlContent("SHENYU.MCP.CONFIG.EXPLANATION.HEADERS")}
+ </li>
+ <li>
+ <strong>transport</strong>:{" "}
+ {getIntlContent("SHENYU.MCP.CONFIG.EXPLANATION.TRANSPORT")} (
+ {isSSE ? "sse" : "streamableHttp"})
+ </li>
+ </ul>
+ </div>
+ )}
+ </Modal>
+ );
+ }
+}
+
+export default McpConfigModal;
diff --git a/src/routes/Plugin/McpServer/ToolsModal.js
b/src/routes/Plugin/McpServer/ToolsModal.js
index 73d87ddf..ee967530 100644
--- a/src/routes/Plugin/McpServer/ToolsModal.js
+++ b/src/routes/Plugin/McpServer/ToolsModal.js
@@ -26,16 +26,18 @@ import {
Row,
Select,
Switch,
+ Radio,
+ Tabs,
} 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;
+const { TabPane } = Tabs;
@connect(({ global }) => ({
currentNamespaceId: global.currentNamespaceId,
@@ -43,45 +45,222 @@ const { Option } = Select;
class AddModal extends Component {
constructor(props) {
super(props);
+
+ // 初始化表单参数
+ const formData = this.initParameters(props);
+
+ // 初始化JSON模式数据
+ const jsonData = this.initJsonMode(props);
+
this.state = {
- visible: false,
- questJson: {},
+ // 表单模式状态
+ parameters: formData.parameters,
+ questJson: formData.questJson,
+ // JSON模式状态
+ jsonText: jsonData.jsonText,
+ jsonError: jsonData.jsonError,
+ // 通用状态
+ editMode: "form", // "form" 或 "json"
+ activeTab: "1",
};
-
- this.initParameters(props);
}
initParameters = (props) => {
- let parameters = [];
+ const { handle } = props;
let questJson = {};
- try {
- const handle = props.handle ? JSON.parse(props.handle) : {};
- parameters = handle.parameters || [
+ let parameters = [];
+
+ if (handle) {
+ try {
+ const handleObj = JSON.parse(handle);
+ parameters = handleObj.parameters || [];
+ questJson = handleObj.requestConfig
+ ? JSON.parse(handleObj.requestConfig)
+ : {};
+ } catch (e) {
+ // Failed to parse handle JSON
+ parameters = [
+ {
+ type: "String",
+ name: "",
+ description: "",
+ },
+ ];
+ }
+ } else {
+ parameters = [
{
type: "String",
name: "",
description: "",
},
];
- questJson = JSON.parse(handle.requestConfig);
- } catch (e) {
- console.error("Failed to parse handle JSON:", e);
- parameters = [
+ }
+ return { parameters, questJson };
+ };
+
+ initJsonMode = (props) => {
+ const { name, description, enabled, handle } = props;
+
+ // 将handle内容提升到最外层,创建扁平化的JSON结构
+ let flattenedJson = {
+ name: name || "",
+ description: description || "",
+ enabled: enabled !== undefined ? enabled : true,
+ parameters: [],
+ requestConfig: "{}",
+ };
+
+ if (handle) {
+ try {
+ const handleObj = JSON.parse(handle);
+ flattenedJson = {
+ name: name || "",
+ description: description || "",
+ enabled: enabled !== undefined ? enabled : true,
+ parameters: handleObj.parameters || [],
+ requestConfig: handleObj.requestConfig || "{}",
+ };
+ } catch (e) {
+ // Failed to parse handle JSON
+ }
+ }
+
+ return {
+ jsonText: JSON.stringify(flattenedJson, null, 2),
+ jsonError: null,
+ };
+ };
+
+ generateTemplate = () => {
+ const template = {
+ name: "getOrderById",
+ description: "Get order details by ID",
+ enabled: true,
+ parameters: [
{
- type: "String",
- name: "",
- description: "",
+ name: "orderId",
+ type: "string",
+ description: "Order ID",
+ required: true,
},
- ];
+ ],
+ requestConfig: JSON.stringify({
+ requestTemplate: {
+ url: "/api/orders/{orderId}",
+ method: "GET",
+ headers: [
+ {
+ name: "Authorization",
+ value: "Bearer {token}",
+ },
+ ],
+ timeout: 30000,
+ },
+ argsPosition: {
+ orderId: "url.path",
+ },
+ }),
+ };
+
+ this.setState({
+ jsonText: JSON.stringify(template, null, 2),
+ jsonError: null,
+ });
+
+ message.success(
+ getIntlContent("SHENYU.MCP.TOOLS.ADD.JSON.TEMPLATE.SUCCESS"),
+ );
+ };
+
+ handleEditModeChange = (e) => {
+ const editMode = e.target.value;
+ this.setState({ editMode });
+
+ // 切换到JSON模式时,同步表单数据到JSON
+ if (editMode === "json") {
+ const { form } = this.props;
+ const { parameters, questJson } = this.state;
+
+ // 获取表单数据
+ form.validateFields((err, values) => {
+ if (!err) {
+ const toolJson = {
+ name: values.name || "",
+ description: values.description || "",
+ enabled: values.enabled !== undefined ? values.enabled : true,
+ handle: {
+ parameters,
+ requestConfig: questJson,
+ description: values.description || "",
+ },
+ };
+
+ this.setState({
+ jsonText: JSON.stringify(toolJson, null, 2),
+ jsonError: null,
+ });
+ }
+ });
+ }
+ };
+
+ handleJsonTextChange = (e) => {
+ const jsonText = e.target.value;
+ this.setState({ jsonText });
+
+ // 实时验证JSON格式
+ try {
+ JSON.parse(jsonText);
+ this.setState({ jsonError: null });
+ } catch (error) {
+ this.setState({ jsonError: error.message });
}
- this.state.parameters = parameters;
- this.state.questJson = questJson;
+ };
+
+ handleFormatJson = () => {
+ try {
+ const parsed = JSON.parse(this.state.jsonText);
+ const formatted = JSON.stringify(parsed, null, 2);
+ this.setState({
+ jsonText: formatted,
+ jsonError: null,
+ });
+ message.success(getIntlContent("SHENYU.MCP.JSON.EDIT.FORMAT.SUCCESS"));
+ } catch (error) {
+ message.error(getIntlContent("SHENYU.MCP.JSON.EDIT.FORMAT.FAILED"));
+ }
+ };
+
+ handleCompressJson = () => {
+ try {
+ const parsed = JSON.parse(this.state.jsonText);
+ const compressed = JSON.stringify(parsed);
+ this.setState({
+ jsonText: compressed,
+ jsonError: null,
+ });
+ message.success(getIntlContent("SHENYU.MCP.JSON.EDIT.COMPRESS.SUCCESS"));
+ } catch (error) {
+ message.error(getIntlContent("SHENYU.MCP.JSON.EDIT.COMPRESS.FAILED"));
+ }
+ };
+
+ handleCopyToClipboard = () => {
+ navigator.clipboard
+ .writeText(this.state.jsonText)
+ .then(() => {
+ message.success(getIntlContent("SHENYU.MCP.JSON.EDIT.COPY.SUCCESS"));
+ })
+ .catch(() => {
+ message.error(getIntlContent("SHENYU.MCP.JSON.EDIT.COPY.FAILED"));
+ });
};
updateJson = (obj) => {
- this.setState({ questJson: obj.updated_src });
- this.props.form.setFieldsValue({
- requestConfig: JSON.stringify(obj.updated_src),
+ this.setState({
+ jsonText: JSON.stringify(obj, null, 2),
+ jsonError: null,
});
};
@@ -109,11 +288,31 @@ class AddModal extends Component {
handleSubmit = (e) => {
e.preventDefault();
+ const { editMode } = this.state;
+
+ if (editMode === "form") {
+ // 表单模式提交
+ this.handleFormSubmit();
+ } else {
+ // JSON模式提交
+ this.handleJsonSubmit();
+ }
+ };
+
+ handleFormSubmit = () => {
const { form, handleOk } = this.props;
const { parameters } = this.state;
form.validateFieldsAndScroll((err, values) => {
- const { name, description, enabled } = values;
+ const {
+ name,
+ description,
+ enabled,
+ sort,
+ loged,
+ matchMode,
+ matchRestful,
+ } = values;
if (!err) {
const submit = this.checkParams();
if (submit) {
@@ -130,10 +329,10 @@ class AddModal extends Component {
description,
handle,
enabled,
- sort: 1,
- loged: true,
- matchMode: "0",
- matchRestful: false,
+ sort: parseInt(sort, 10),
+ loged,
+ matchMode,
+ matchRestful,
ruleConditions: [
{
paramType: "uri",
@@ -148,6 +347,66 @@ class AddModal extends Component {
});
};
+ handleJsonSubmit = () => {
+ const { jsonText } = this.state;
+ const { handleOk } = this.props;
+
+ if (!jsonText.trim()) {
+ message.error(getIntlContent("SHENYU.MCP.JSON.EDIT.EMPTY.ERROR"));
+ return;
+ }
+
+ try {
+ const parsedJson = JSON.parse(jsonText);
+
+ // 验证必要字段
+ if (!parsedJson.name || !parsedJson.name.trim()) {
+ message.error(getIntlContent("SHENYU.MCP.JSON.EDIT.TOOL.NAME.ERROR"));
+ return;
+ }
+
+ // 确保字段存在
+ const finalData = {
+ name: parsedJson.name,
+ description: parsedJson.description || "",
+ enabled: parsedJson.enabled !== undefined ? parsedJson.enabled : true,
+ parameters: parsedJson.parameters || [],
+ requestConfig: parsedJson.requestConfig || "{}",
+ };
+
+ // 将扁平化数据转换回原始格式,并添加必需的规则级别字段
+ const transformedData = {
+ name: finalData.name,
+ description: finalData.description,
+ enabled: finalData.enabled,
+ handle: JSON.stringify({
+ parameters: finalData.parameters,
+ requestConfig: finalData.requestConfig,
+ description: finalData.description,
+ }),
+ // 添加必需的规则级别字段默认值
+ sort: 1,
+ loged: true,
+ matchMode: "0",
+ matchRestful: false,
+ ruleConditions: [
+ {
+ paramType: "uri",
+ operator: "pathPattern",
+ paramName: "/",
+ paramValue: "/**",
+ },
+ ],
+ };
+
+ handleOk(transformedData);
+ } catch (error) {
+ message.error(
+
`${getIntlContent("SHENYU.MCP.JSON.EDIT.FORMAT.ERROR")}${error.message}`,
+ );
+ }
+ };
+
handleAdd = () => {
let { parameters } = this.state;
parameters.push({
@@ -169,24 +428,6 @@ class AddModal extends Component {
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,
@@ -195,15 +436,20 @@ class AddModal extends Component {
description = "",
enabled = true,
handle = "{}",
+ sort = 1,
+ loged = true,
+ matchMode = "0",
+ matchRestful = false,
} = this.props;
- const { parameters, visible, questJson } = this.state;
+ const { parameters, questJson, editMode, jsonText, jsonError, activeTab } =
+ 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);
+ // Failed to parse handle JSON
}
const { description: handleDescription = "" } = parsedHandle;
@@ -219,190 +465,435 @@ class AddModal extends Component {
sm: { span: 20 },
},
};
+
+ // 用于预览的JSON对象
+ let previewJson = {};
+ try {
+ previewJson = JSON.parse(jsonText);
+ } catch (e) {
+ previewJson = {
+ error: getIntlContent("SHENYU.MCP.JSON.EDIT.ERROR.PREFIX") + e.message,
+ };
+ }
+
+ const modalTitle =
+ editMode === "form"
+ ? getIntlContent("SHENYU.TOOL.NAME")
+ : getIntlContent("SHENYU.MCP.TOOLS.ADD.JSON.TITLE");
+
return (
<Modal
width={1000}
centered
- title={getIntlContent("SHENYU.TOOL.NAME")}
+ title={modalTitle}
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}>
+ <div style={{ marginBottom: "16px" }}>
+ <div style={{ marginBottom: "12px" }}>
+ <Radio.Group value={editMode} onChange={this.handleEditModeChange}>
+ <Radio.Button value="form">
+ {getIntlContent("SHENYU.MCP.TOOLS.ADD.MODE.FORM")}
+ </Radio.Button>
+ <Radio.Button value="json">
+ {getIntlContent("SHENYU.MCP.TOOLS.ADD.MODE.JSON")}
+ </Radio.Button>
+ </Radio.Group>
+ </div>
+
+ <p style={{ color: "#666", fontSize: "12px", marginBottom: "8px" }}>
+ {editMode === "form"
+ ? getIntlContent("SHENYU.MCP.JSON.EDIT.DESCRIPTION")
+ : getIntlContent("SHENYU.MCP.TOOLS.ADD.JSON.DESCRIPTION")}
+ </p>
+
+ {editMode === "json" && (
+ <Row gutter={8}>
+ <Col>
+ <Button size="small" onClick={this.generateTemplate}>
+ {getIntlContent("SHENYU.MCP.TOOLS.ADD.JSON.TEMPLATE")}
+ </Button>
+ </Col>
+ <Col>
+ <Button size="small" onClick={this.handleFormatJson}>
+ {getIntlContent("SHENYU.MCP.JSON.EDIT.FORMAT")}
+ </Button>
+ </Col>
+ <Col>
+ <Button size="small" onClick={this.handleCompressJson}>
+ {getIntlContent("SHENYU.MCP.JSON.EDIT.COMPRESS")}
+ </Button>
+ </Col>
+ <Col>
+ <Button size="small" onClick={this.handleCopyToClipboard}>
+ {getIntlContent("SHENYU.MCP.JSON.EDIT.COPY")}
+ </Button>
+ </Col>
+ </Row>
+ )}
+ </div>
+
+ {editMode === "form" ? (
+ // 表单模式
+ <Form onSubmit={this.handleSubmit} className="login-form">
<FormItem
- label={getIntlContent("SHENYU.COMMON.PARAMETER")}
+ label={getIntlContent("SHENYU.PLUGIN.SELECTOR.LIST.COLUMN.NAME")}
{...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>
- );
- })}
+ {getFieldDecorator("name", {
+ rules: [
+ {
+ required: true,
+ message: getIntlContent("SHENYU.COMMON.INPUTNAME"),
+ },
+ ],
+ initialValue: name,
+ })(
+ <Input
+ allowClear
+ placeholder={getIntlContent(
+ "SHENYU.PLUGIN.SELECTOR.LIST.COLUMN.NAME",
+ )}
+ />,
+ )}
</FormItem>
- <FormItem label={" "} colon={false} {...formItemLayout}>
- <Button
- className={styles.addButton}
- onClick={this.handleAdd}
- type="primary"
+ <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>
+
+ <FormItem
+ label={getIntlContent("SHENYU.SELECTOR.EXEORDER")}
+ {...formItemLayout}
+ >
+ {getFieldDecorator("sort", {
+ rules: [
+ {
+ required: true,
+ message: getIntlContent("SHENYU.SELECTOR.INPUTNUMBER"),
+ },
+ ],
+ initialValue: sort,
+ })(
+ <Input
+ allowClear
+ type="number"
+ min={1}
+ max={1000}
+ placeholder={getIntlContent("SHENYU.SELECTOR.INPUTORDER")}
+ />,
+ )}
+ </FormItem>
+
+ <Row gutter={16}>
+ <Col span={12}>
+ <FormItem
+ label={getIntlContent("SHENYU.SELECTOR.PRINTLOG")}
+ {...formItemLayout}
+ labelCol={{ sm: { span: 8 } }}
+ wrapperCol={{ sm: { span: 16 } }}
+ >
+ {getFieldDecorator("loged", {
+ initialValue: loged,
+ valuePropName: "checked",
+ rules: [{ required: true }],
+ })(<Switch />)}
+ </FormItem>
+ </Col>
+ <Col span={12}>
+ <FormItem
+ label={getIntlContent("SHENYU.SELECTOR.MATCHRESTFUL")}
+ {...formItemLayout}
+ labelCol={{ sm: { span: 8 } }}
+ wrapperCol={{ sm: { span: 16 } }}
+ >
+ {getFieldDecorator("matchRestful", {
+ initialValue: matchRestful,
+ valuePropName: "checked",
+ rules: [{ required: true }],
+ })(<Switch />)}
+ </FormItem>
+ </Col>
+ </Row>
+
+ <FormItem
+ label={getIntlContent("SHENYU.COMMON.MATCHTYPE")}
+ {...formItemLayout}
+ >
+ {getFieldDecorator("matchMode", {
+ rules: [
+ {
+ required: true,
+ message: getIntlContent("SHENYU.COMMON.INPUTMATCHTYPE"),
+ },
+ ],
+ initialValue: matchMode,
+ })(
+ <Select
+ placeholder={getIntlContent("SHENYU.COMMON.INPUTMATCHTYPE")}
+ >
+ <Option value="0">and</Option>
+ <Option value="1">or</Option>
+ </Select>,
+ )}
+ </FormItem>
+
+ <div className={styles.condition}>
+ <FormItem
+ label={getIntlContent("SHENYU.COMMON.PARAMETER")}
+ {...formItemLayout}
>
- {getIntlContent("SHENYU.COMMON.ADD")}{" "}
- {getIntlContent("SHENYU.COMMON.PARAMETER")}
- </Button>
+ {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>
- </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")}
+ <FormItem
+ {...formItemLayout}
+ label={getIntlContent("SHENYU.SELECTOR.WHETHEROPEN")}
+ >
+ {getFieldDecorator("enabled", {
+ initialValue: enabled,
+ valuePropName: "checked",
+ rules: [{ required: true }],
+ })(<Switch />)}
+ </FormItem>
+ </Form>
+ ) : (
+ // JSON模式
+ <Tabs
+ activeKey={activeTab}
+ onChange={(key) => this.setState({ activeTab: key })}
>
- {getFieldDecorator("enabled", {
- initialValue: enabled,
- valuePropName: "checked",
- rules: [{ required: true }],
- })(<Switch />)}
- </FormItem>
- </Form>
+ <TabPane
+ tab={getIntlContent("SHENYU.MCP.JSON.EDIT.TAB.TEXT")}
+ key="1"
+ >
+ <div style={{ position: "relative" }}>
+ <TextArea
+ value={jsonText}
+ onChange={this.handleJsonTextChange}
+ placeholder={getIntlContent(
+ "SHENYU.MCP.TOOLS.ADD.JSON.PLACEHOLDER",
+ )}
+ autoSize={{ minRows: 15, maxRows: 25 }}
+ style={{
+ fontFamily: "Monaco, Menlo, 'Ubuntu Mono', monospace",
+ fontSize: "13px",
+ lineHeight: "1.4",
+ }}
+ />
+ {jsonError && (
+ <div
+ style={{
+ position: "absolute",
+ bottom: "8px",
+ right: "8px",
+ background: "#ff4d4f",
+ color: "white",
+ padding: "4px 8px",
+ borderRadius: "4px",
+ fontSize: "12px",
+ maxWidth: "300px",
+ wordBreak: "break-word",
+ }}
+ >
+ {getIntlContent("SHENYU.MCP.JSON.EDIT.ERROR.PREFIX")}
+ {jsonError}
+ </div>
+ )}
+ </div>
+ </TabPane>
+ <TabPane
+ tab={getIntlContent("SHENYU.MCP.JSON.EDIT.TAB.PREVIEW")}
+ key="2"
+ >
+ <div
+ style={{
+ border: "1px solid #d9d9d9",
+ borderRadius: "4px",
+ maxHeight: "400px",
+ overflow: "auto",
+ }}
+ >
+ <ReactJson
+ src={previewJson}
+ theme="monokai"
+ displayDataTypes={false}
+ displayObjectSize={false}
+ name={false}
+ enableClipboard={false}
+ style={{
+ padding: "16px",
+ fontSize: "13px",
+ }}
+ />
+ </div>
+ </TabPane>
+ </Tabs>
+ )}
+
+ {editMode === "json" && (
+ <div style={{ marginTop: "16px", color: "#666", fontSize: "12px" }}>
+ <p>
+ <strong>
+ {getIntlContent("SHENYU.MCP.TOOLS.ADD.JSON.STRUCTURE.TITLE")}
+ </strong>
+ </p>
+ <ul>
+ <li>
+ <code>name</code>:{" "}
+ {getIntlContent("SHENYU.MCP.TOOLS.ADD.JSON.STRUCTURE.NAME")}
+ </li>
+ <li>
+ <code>description</code>:{" "}
+ {getIntlContent(
+ "SHENYU.MCP.TOOLS.ADD.JSON.STRUCTURE.DESCRIPTION",
+ )}
+ </li>
+ <li>
+ <code>enabled</code>:{" "}
+ {getIntlContent("SHENYU.MCP.TOOLS.ADD.JSON.STRUCTURE.ENABLED")}
+ </li>
+ <li>
+ <code>parameters</code>:{" "}
+ {getIntlContent("SHENYU.MCP.JSON.EDIT.HANDLE.PARAMETERS")}
+ </li>
+ <li>
+ <code>requestConfig</code>:{" "}
+ {getIntlContent("SHENYU.MCP.JSON.EDIT.HANDLE.REQUESTCONFIG")}
+ </li>
+ </ul>
+ <p>
+ <strong>
+ {getIntlContent("SHENYU.MCP.JSON.EDIT.OPERATION.TITLE")}
+ </strong>
+ </p>
+ <ul>
+ <li>
+ {getIntlContent("SHENYU.MCP.JSON.EDIT.OPERATION.KEYBOARD")}
+ </li>
+
<li>{getIntlContent("SHENYU.MCP.JSON.EDIT.OPERATION.SWITCH")}</li>
+ <li>
+ {getIntlContent("SHENYU.MCP.JSON.EDIT.OPERATION.VALIDATE")}
+ </li>
+ <li>
+
{getIntlContent("SHENYU.MCP.TOOLS.ADD.JSON.OPERATION.TEMPLATE")}
+ </li>
+ </ul>
+ </div>
+ )}
</Modal>
);
}
diff --git a/src/routes/Plugin/McpServer/index.js
b/src/routes/Plugin/McpServer/index.js
index ea497ed7..496f064c 100755
--- a/src/routes/Plugin/McpServer/index.js
+++ b/src/routes/Plugin/McpServer/index.js
@@ -34,6 +34,8 @@ import ReactJson from "react-json-view";
import styles from "../index.less";
import Selector from "../Common/Selector";
import Tools from "./ToolsModal";
+import JsonEditModal from "./JsonEditModal";
+import McpConfigModal from "./McpConfigModal";
import { getCurrentLocale, getIntlContent } from "../../../utils/IntlUtils";
import AuthButton from "../../../utils/AuthButton";
import {
@@ -65,6 +67,11 @@ export default class McpServer extends Component {
isPluginEnabled: false,
pluginName: "mcpServer",
pluginRole: "Mcp",
+ jsonEditVisible: false,
+ currentJsonData: null,
+ mcpConfigVisible: false,
+ mcpConfigType: "sse", // "sse" or "streamable"
+ currentSelectorForConfig: null,
};
}
@@ -773,7 +780,7 @@ export default class McpServer extends Component {
};
editTool = (record) => {
- console.log("record", record);
+ // record
const { dispatch, currentSelector, plugins, currentNamespaceId } =
this.props;
const { toolPage, toolPageSize, pluginId, pluginName } = this.state;
@@ -907,6 +914,89 @@ export default class McpServer extends Component {
});
};
+ editToolByJson = (record) => {
+ const { dispatch, currentNamespaceId } = this.props;
+ const { id } = record;
+
+ dispatch({
+ type: "common/fetchRuleItem",
+ payload: {
+ id,
+ namespaceId: currentNamespaceId,
+ },
+ callback: (rule) => {
+ this.setState({
+ currentJsonData: rule,
+ jsonEditVisible: true,
+ });
+ },
+ });
+ };
+
+ handleJsonEditOk = (jsonData) => {
+ const { dispatch, currentSelector, currentNamespaceId } = this.props;
+ const { toolPage, toolPageSize } = this.state;
+ const selectorId = currentSelector ? currentSelector.id : "";
+
+ dispatch({
+ type: "common/updateRule",
+ payload: {
+ selectorId,
+ ...jsonData,
+ namespaceId: currentNamespaceId,
+ },
+ fetchValue: {
+ selectorId,
+ currentPage: toolPage,
+ pageSize: toolPageSize,
+ namespaceId: currentNamespaceId,
+ },
+ callback: () => {
+ this.setState({
+ jsonEditVisible: false,
+ currentJsonData: null,
+ });
+ message.success(getIntlContent("SHENYU.MCP.JSON.EDIT.UPDATE.SUCCESS"));
+
+ // 刷新工具列表以确保最新数据显示
+ this.getAllTools(toolPage, toolPageSize);
+ },
+ });
+ };
+
+ handleJsonEditCancel = () => {
+ this.setState({
+ jsonEditVisible: false,
+ currentJsonData: null,
+ });
+ };
+
+ // 显示SSE配置
+ showSSEConfig = (selector = null) => {
+ this.setState({
+ mcpConfigVisible: true,
+ mcpConfigType: "sse",
+ currentSelectorForConfig: selector,
+ });
+ };
+
+ // 显示Streamable配置
+ showStreamableConfig = (selector = null) => {
+ this.setState({
+ mcpConfigVisible: true,
+ mcpConfigType: "streamable",
+ currentSelectorForConfig: selector,
+ });
+ };
+
+ // 关闭MCP配置模态框
+ handleMcpConfigCancel = () => {
+ this.setState({
+ mcpConfigVisible: false,
+ currentSelectorForConfig: null,
+ });
+ };
+
// eslint-disable-next-line react/no-unused-class-component-methods
changeLocales(locale) {
this.setState({
@@ -984,6 +1074,26 @@ export default class McpServer extends Component {
{getIntlContent("SHENYU.COMMON.CHANGE")}
</span>
</AuthButton>
+ <span
+ style={{ marginRight: 8 }}
+ className="edit"
+ onClick={(e) => {
+ e.stopPropagation();
+ this.showSSEConfig(record);
+ }}
+ >
+ {getIntlContent("SHENYU.MCP.CONFIG.SSE")}
+ </span>
+ <span
+ style={{ marginRight: 8 }}
+ className="edit"
+ onClick={(e) => {
+ e.stopPropagation();
+ this.showStreamableConfig(record);
+ }}
+ >
+ {getIntlContent("SHENYU.MCP.CONFIG.STREAMABLE")}
+ </span>
<AuthButton
perms={`plugin:${this.state.pluginName}Selector:delete`}
>
@@ -1131,6 +1241,18 @@ export default class McpServer extends Component {
{getIntlContent("SHENYU.COMMON.CHANGE")}
</span>
</AuthButton>
+ <AuthButton perms={`plugin:${this.state.pluginName}Rule:edit`}>
+ <span
+ className="edit"
+ style={{ marginRight: 8 }}
+ onClick={(e) => {
+ e.stopPropagation();
+ this.editToolByJson(record);
+ }}
+ >
+ {getIntlContent("SHENYU.MCP.EDIT.JSON")}
+ </span>
+ </AuthButton>
<AuthButton perms={`plugin:${this.state.pluginName}Rule:delete`}>
<Popconfirm
title={getIntlContent("SHENYU.COMMON.DELETE")}
@@ -1379,6 +1501,26 @@ export default class McpServer extends Component {
</Col>
</Row>
{popup}
+ {this.state.jsonEditVisible && (
+ <JsonEditModal
+ visible={this.state.jsonEditVisible}
+ data={this.state.currentJsonData}
+ onOk={this.handleJsonEditOk}
+ onCancel={this.handleJsonEditCancel}
+ />
+ )}
+ {this.state.mcpConfigVisible && (
+ <McpConfigModal
+ visible={this.state.mcpConfigVisible}
+ configType={this.state.mcpConfigType}
+ selectorList={
+ this.state.currentSelectorForConfig
+ ? [this.state.currentSelectorForConfig]
+ : selectorList
+ }
+ onCancel={this.handleMcpConfigCancel}
+ />
+ )}
</div>
);
}