From f1356e9b617d7c4ee9a9e281a23d4209ad86c423 Mon Sep 17 00:00:00 2001 From: CaptainB Date: Mon, 11 Aug 2025 13:12:56 +0800 Subject: [PATCH] feat: add MCP tool support with new form and dropdown options --- apps/common/utils/tool_code.py | 35 ++- .../migrations/0002_alter_file_source_type.py | 18 ++ .../migrations/0002_alter_tool_tool_type.py | 18 ++ apps/tools/models/tool.py | 1 + apps/tools/serializers/tool.py | 154 ++++++++++ apps/tools/urls.py | 1 + apps/tools/views/tool.py | 33 ++ ui/src/api/system-resource-management/tool.ts | 16 + ui/src/api/system-shared/tool.ts | 11 + ui/src/api/tool/tool.ts | 11 + ui/src/api/type/tool.ts | 1 + ui/src/locales/lang/en-US/views/tool.ts | 6 + .../lang/zh-CN/views/application-workflow.ts | 1 + ui/src/locales/lang/zh-CN/views/tool.ts | 8 + ui/src/locales/lang/zh-Hant/views/tool.ts | 6 + ui/src/stores/modules/tool.ts | 4 + .../ToolResourceIndex.vue | 7 +- .../views/system-shared/ToolSharedIndex.vue | 17 ++ ui/src/views/tool/McpToolFormDrawer.vue | 284 ++++++++++++++++++ .../tool/component/ToolListContainer.vue | 51 ++++ ui/src/views/tool/index.vue | 12 + ui/src/workflow/nodes/mcp-node/index.vue | 79 ++++- 22 files changed, 769 insertions(+), 5 deletions(-) create mode 100644 apps/knowledge/migrations/0002_alter_file_source_type.py create mode 100644 apps/tools/migrations/0002_alter_tool_tool_type.py create mode 100644 ui/src/views/tool/McpToolFormDrawer.vue diff --git a/apps/common/utils/tool_code.py b/apps/common/utils/tool_code.py index 13e894c3a..f4b8e2ada 100644 --- a/apps/common/utils/tool_code.py +++ b/apps/common/utils/tool_code.py @@ -1,5 +1,5 @@ # coding=utf-8 - +import ast import os import pickle import subprocess @@ -83,6 +83,39 @@ except Exception as e: return result.get('data') raise Exception(result.get('msg')) + def generate_mcp_server_code(self, _code): + self.validate_banned_keywords(_code) + + # 解析代码,提取导入语句和函数定义 + try: + tree = ast.parse(_code) + except SyntaxError: + return _code + + imports = [] + functions = [] + other_code = [] + + for node in tree.body: + if isinstance(node, ast.Import) or isinstance(node, ast.ImportFrom): + imports.append(ast.unparse(node)) + elif isinstance(node, ast.FunctionDef): + # 为函数添加 @mcp.tool() 装饰器 + func_code = ast.unparse(node) + functions.append(f"@mcp.tool()\n{func_code}\n") + else: + other_code.append(ast.unparse(node)) + + # 构建完整的 MCP 服务器代码 + code_parts = ["from mcp.server.fastmcp import FastMCP"] + code_parts.extend(imports) + code_parts.append(f"\nmcp = FastMCP(\"{uuid.uuid7()}\")\n") + code_parts.extend(other_code) + code_parts.extend(functions) + code_parts.append("\nmcp.run(transport=\"stdio\")\n") + + return "\n".join(code_parts) + def _exec_sandbox(self, _code, _id): exec_python_file = f'{self.sandbox_path}/execute/{_id}.py' with open(exec_python_file, 'w') as file: diff --git a/apps/knowledge/migrations/0002_alter_file_source_type.py b/apps/knowledge/migrations/0002_alter_file_source_type.py new file mode 100644 index 000000000..37278ae90 --- /dev/null +++ b/apps/knowledge/migrations/0002_alter_file_source_type.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.4 on 2025-08-11 09:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('knowledge', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='file', + name='source_type', + field=models.CharField(choices=[('KNOWLEDGE', 'Knowledge'), ('APPLICATION', 'Application'), ('TOOL', 'Tool'), ('DOCUMENT', 'Document'), ('CHAT', 'Chat'), ('SYSTEM', 'System'), ('TEMPORARY_30_MINUTE', 'Temporary 30 Minute'), ('TEMPORARY_120_MINUTE', 'Temporary 120 Minute'), ('TEMPORARY_1_DAY', 'Temporary 1 Day')], db_index=True, default='TEMPORARY_120_MINUTE', verbose_name='资源类型'), + ), + ] diff --git a/apps/tools/migrations/0002_alter_tool_tool_type.py b/apps/tools/migrations/0002_alter_tool_tool_type.py new file mode 100644 index 000000000..ffc2b74f6 --- /dev/null +++ b/apps/tools/migrations/0002_alter_tool_tool_type.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.4 on 2025-08-11 09:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tools', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='tool', + name='tool_type', + field=models.CharField(choices=[('INTERNAL', '内置'), ('CUSTOM', '自定义'), ('MCP', 'MCP工具')], db_index=True, default='CUSTOM', max_length=20, verbose_name='工具类型'), + ), + ] diff --git a/apps/tools/models/tool.py b/apps/tools/models/tool.py index 73e9ee1f5..41918b2aa 100644 --- a/apps/tools/models/tool.py +++ b/apps/tools/models/tool.py @@ -31,6 +31,7 @@ class ToolScope(models.TextChoices): class ToolType(models.TextChoices): INTERNAL = "INTERNAL", '内置' CUSTOM = "CUSTOM", "自定义" + MCP = "MCP", "MCP工具" class Tool(AppModelMixin): diff --git a/apps/tools/serializers/tool.py b/apps/tools/serializers/tool.py index 00b0ada13..4ed23788a 100644 --- a/apps/tools/serializers/tool.py +++ b/apps/tools/serializers/tool.py @@ -1,9 +1,11 @@ # -*- coding: utf-8 -*- +import asyncio import io import json import os import pickle import re +from typing import Dict import uuid_utils.compat as uuid from django.core import validators @@ -12,6 +14,7 @@ from django.db.models import QuerySet, Q from django.http import HttpResponse from django.utils import timezone from django.utils.translation import gettext_lazy as _ +from langchain_mcp_adapters.client import MultiServerMCPClient from pylint.lint import Run from pylint.reporters import JSON2Reporter from rest_framework import serializers, status @@ -22,6 +25,7 @@ from common.exception.app_exception import AppApiException from common.field.common import UploadedImageField from common.result import result from common.utils.common import get_file_content +from common.utils.logger import maxkb_logger from common.utils.rsa_util import rsa_long_decrypt, rsa_long_encrypt from common.utils.tool_code import ToolExecutor from knowledge.models import File, FileSourceType @@ -103,6 +107,18 @@ def encryption(message: str): return pre_str + content + end_str +def validate_mcp_config(servers: Dict): + async def validate(): + client = MultiServerMCPClient(servers) + await client.get_tools() + + try: + asyncio.run(validate()) + except Exception as e: + maxkb_logger.error(f"validate mcp config error: {e}, servers: {servers}") + raise serializers.ValidationError(_('MCP configuration is invalid')) + + class ToolModelSerializer(serializers.ModelSerializer): class Meta: model = Tool @@ -201,6 +217,131 @@ class PylintInstance(serializers.Serializer): class ToolSerializer(serializers.Serializer): + class Query(serializers.Serializer): + workspace_id = serializers.CharField(required=True, label=_('workspace id')) + folder_id = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_('folder id')) + name = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_('tool name')) + user_id = serializers.UUIDField(required=False, allow_null=True, label=_('user id')) + scope = serializers.CharField(required=True, label=_('scope')) + tool_type = serializers.CharField(required=False, label=_('tool type'), allow_null=True, allow_blank=True) + create_user = serializers.UUIDField(required=False, label=_('create user'), allow_null=True) + + def get_query_set(self, workspace_manage, is_x_pack_ee): + tool_query_set = QuerySet(Tool).filter(workspace_id=self.data.get('workspace_id')) + folder_query_set = QuerySet(ToolFolder) + default_query_set = QuerySet(Tool) + + workspace_id = self.data.get('workspace_id') + user_id = self.data.get('user_id') + scope = self.data.get('scope') + tool_type = self.data.get('tool_type') + desc = self.data.get('desc') + name = self.data.get('name') + folder_id = self.data.get('folder_id') + create_user = self.data.get('create_user') + + if workspace_id is not None: + folder_query_set = folder_query_set.filter(workspace_id=workspace_id) + default_query_set = default_query_set.filter(workspace_id=workspace_id) + if folder_id is not None: + folder_query_set = folder_query_set.filter(parent=folder_id) + default_query_set = default_query_set.filter(folder_id=folder_id) + if name is not None: + folder_query_set = folder_query_set.filter(name__icontains=name) + default_query_set = default_query_set.filter(name__icontains=name) + if desc is not None: + folder_query_set = folder_query_set.filter(desc__icontains=desc) + default_query_set = default_query_set.filter(desc__icontains=desc) + if create_user is not None: + tool_query_set = tool_query_set.filter(user_id=create_user) + folder_query_set = folder_query_set.filter(user_id=create_user) + + default_query_set = default_query_set.order_by("-create_time") + + if scope is not None: + tool_query_set = tool_query_set.filter(scope=scope) + if tool_type: + tool_query_set = tool_query_set.filter(tool_type=tool_type) + + query_set_dict = { + 'folder_query_set': folder_query_set, + 'tool_query_set': tool_query_set, + 'default_query_set': default_query_set, + } + if not workspace_manage: + query_set_dict['workspace_user_resource_permission_query_set'] = QuerySet( + WorkspaceUserResourcePermission).filter( + auth_target_type="TOOL", + workspace_id=workspace_id, + user_id=user_id + ) + return query_set_dict + + def get_authorized_query_set(self): + default_query_set = QuerySet(Tool) + tool_type = self.data.get('tool_type') + desc = self.data.get('desc') + name = self.data.get('name') + create_user = self.data.get('create_user') + + default_query_set = default_query_set.filter(workspace_id='None') + default_query_set = default_query_set.filter(scope=ToolScope.SHARED) + if name is not None: + default_query_set = default_query_set.filter(name__icontains=name) + if desc is not None: + default_query_set = default_query_set.filter(desc__icontains=desc) + if create_user is not None: + default_query_set = default_query_set.filter(user_id=create_user) + if tool_type: + default_query_set = default_query_set.filter(tool_type=tool_type) + + default_query_set = default_query_set.order_by("-create_time") + + return default_query_set + + @staticmethod + def is_x_pack_ee(): + workspace_user_role_mapping_model = DatabaseModelManage.get_model("workspace_user_role_mapping") + role_permission_mapping_model = DatabaseModelManage.get_model("role_permission_mapping_model") + return workspace_user_role_mapping_model is not None and role_permission_mapping_model is not None + + def get_tools(self): + self.is_valid(raise_exception=True) + + workspace_manage = is_workspace_manage(self.data.get('user_id'), self.data.get('workspace_id')) + is_x_pack_ee = self.is_x_pack_ee() + results = native_search( + self.get_query_set(workspace_manage, is_x_pack_ee), + get_file_content( + os.path.join( + PROJECT_DIR, + "apps", "tools", 'sql', + 'list_tool.sql' if workspace_manage else ( + 'list_tool_user_ee.sql' if is_x_pack_ee else 'list_tool_user.sql' + ) + ) + ), + ) + + get_authorized_tool = DatabaseModelManage.get_model("get_authorized_tool") + shared_queryset = QuerySet(Tool).none() + if get_authorized_tool is not None: + shared_queryset = self.get_authorized_query_set() + shared_queryset = get_authorized_tool(shared_queryset, self.data.get('workspace_id')) + + return { + 'shared_tools': [ + ToolModelSerializer(data).data for data in shared_queryset + ], + 'tools': [ + { + **tool, + 'input_field_list': json.loads(tool.get('input_field_list', '[]')), + 'init_field_list': json.loads(tool.get('init_field_list', '[]')), + } for tool in results if tool['resource_type'] == 'tool' + ], + } + class Create(serializers.Serializer): user_id = serializers.UUIDField(required=True, label=_('user id')) workspace_id = serializers.CharField(required=True, label=_('workspace id')) @@ -212,6 +353,10 @@ class ToolSerializer(serializers.Serializer): ToolCreateRequest(data=instance).is_valid(raise_exception=True) # 校验代码是否包括禁止的关键字 ToolExecutor().validate_banned_keywords(instance.get('code', '')) + # 校验mcp json + if instance.get('tool_type') == ToolType.MCP.value: + validate_mcp_config(json.loads(instance.get('code'))) + tool_id = uuid.uuid7() Tool( id=tool_id, @@ -223,6 +368,7 @@ class ToolSerializer(serializers.Serializer): input_field_list=instance.get('input_field_list', []), init_field_list=instance.get('init_field_list', []), scope=instance.get('scope', ToolScope.WORKSPACE), + tool_type=instance.get('tool_type', ToolType.CUSTOM), folder_id=instance.get('folder_id', self.data.get('workspace_id')), is_active=False ).save() @@ -326,6 +472,10 @@ class ToolSerializer(serializers.Serializer): ToolEditRequest(data=instance).is_valid(raise_exception=True) # 校验代码是否包括禁止的关键字 ToolExecutor().validate_banned_keywords(instance.get('code', '')) + # 校验mcp json + if instance.get('tool_type') == ToolType.MCP.value: + validate_mcp_config(json.loads(instance.get('code'))) + if not QuerySet(Tool).filter(id=self.data.get('id')).exists(): raise serializers.ValidationError(_('Tool not found')) @@ -574,6 +724,7 @@ class ToolTreeSerializer(serializers.Serializer): name = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_('tool name')) user_id = serializers.UUIDField(required=False, allow_null=True, label=_('user id')) scope = serializers.CharField(required=True, label=_('scope')) + tool_type = serializers.CharField(required=False, label=_('tool type'), allow_null=True, allow_blank=True) create_user = serializers.UUIDField(required=False, label=_('create user'), allow_null=True) def page_tool(self, current_page: int, page_size: int): @@ -609,6 +760,7 @@ class ToolTreeSerializer(serializers.Serializer): workspace_id = self.data.get('workspace_id') user_id = self.data.get('user_id') scope = self.data.get('scope') + tool_type = self.data.get('tool_type') desc = self.data.get('desc') name = self.data.get('name') folder_id = self.data.get('folder_id') @@ -634,6 +786,8 @@ class ToolTreeSerializer(serializers.Serializer): if scope is not None: tool_query_set = tool_query_set.filter(scope=scope) + if tool_type: + tool_query_set = tool_query_set.filter(tool_type=tool_type) query_set_dict = { 'folder_query_set': folder_query_set, diff --git a/apps/tools/urls.py b/apps/tools/urls.py index 63c1cb8d4..b62d0d675 100644 --- a/apps/tools/urls.py +++ b/apps/tools/urls.py @@ -10,6 +10,7 @@ urlpatterns = [ path('workspace//tool/import', views.ToolView.Import.as_view()), path('workspace//tool/pylint', views.ToolView.Pylint.as_view()), path('workspace//tool/debug', views.ToolView.Debug.as_view()), + path('workspace//tool/tool_list', views.ToolView.Query.as_view()), path('workspace//tool/', views.ToolView.Operate.as_view()), path('workspace//tool//edit_icon', views.ToolView.EditIcon.as_view()), path('workspace//tool//export', views.ToolView.Export.as_view()), diff --git a/apps/tools/views/tool.py b/apps/tools/views/tool.py index 9cf2fa382..6d8f794f1 100644 --- a/apps/tools/views/tool.py +++ b/apps/tools/views/tool.py @@ -73,6 +73,7 @@ class ToolView(APIView): 'folder_id': request.query_params.get('folder_id'), 'name': request.query_params.get('name'), 'scope': request.query_params.get('scope', ToolScope.WORKSPACE), + 'tool_type': request.query_params.get('tool_type'), 'user_id': request.user.id, 'create_user': request.query_params.get('create_user'), } @@ -209,11 +210,43 @@ class ToolView(APIView): 'folder_id': request.query_params.get('folder_id'), 'name': request.query_params.get('name'), 'scope': request.query_params.get('scope'), + 'tool_type': request.query_params.get('tool_type'), 'user_id': request.user.id, 'create_user': request.query_params.get('create_user'), } ).page_tool_with_folders(current_page, page_size)) + class Query(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=['GET'], + description=_('Get tool list '), + summary=_('Get tool list'), + operation_id=_('Get tool list'), # type: ignore + parameters=ToolReadAPI.get_parameters(), + responses=ToolReadAPI.get_response(), + tags=[_('Tool')] # type: ignore + ) + @has_permissions( + PermissionConstants.TOOL_READ.get_workspace_permission(), + PermissionConstants.TOOL_READ.get_workspace_permission_workspace_manage_role(), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), RoleConstants.USER.get_workspace_role() + ) + @log(menu='Tool', operate='Get tool list') + def get(self, request: Request, workspace_id: str): + return result.success(ToolSerializer.Query( + data={ + 'workspace_id': workspace_id, + 'folder_id': request.query_params.get('folder_id'), + 'name': request.query_params.get('name'), + 'scope': request.query_params.get('scope'), + 'tool_type': request.query_params.get('tool_type'), + 'user_id': request.user.id, + 'create_user': request.query_params.get('create_user'), + } + ).get_tools()) + class Import(APIView): authentication_classes = [TokenAuth] parser_classes = [MultiPartParser] diff --git a/ui/src/api/system-resource-management/tool.ts b/ui/src/api/system-resource-management/tool.ts index 00ae4f853..11b13b1d6 100644 --- a/ui/src/api/system-resource-management/tool.ts +++ b/ui/src/api/system-resource-management/tool.ts @@ -21,6 +21,21 @@ const getToolList: (data?: any, loading?: Ref) => Promise) => Promise>> = ( + data, + loading, +) => { + return get(`${prefix}/tool_list`, data, loading) +} + /** * 工具列表带分页 * @param 参数 @@ -110,6 +125,7 @@ const postPylint: (code: string, loading?: Ref) => Promise> export default { getToolListPage, getToolList, + getAllToolList, putTool, getToolById, postToolDebug, diff --git a/ui/src/api/system-shared/tool.ts b/ui/src/api/system-shared/tool.ts index befdb6d07..17fcb053a 100644 --- a/ui/src/api/system-shared/tool.ts +++ b/ui/src/api/system-shared/tool.ts @@ -17,6 +17,16 @@ const getToolList: (data?: any, loading?: Ref) => Promise) => Promise>> = ( + data, + loading, +) => { + return get(`${prefix}/tool_list`, data, loading) +} + /** * 工具列表带分页 * @param 参数 @@ -135,6 +145,7 @@ const addInternalTool: ( export default { getToolList, + getAllToolList, getToolListPage, putTool, getToolById, diff --git a/ui/src/api/tool/tool.ts b/ui/src/api/tool/tool.ts index fb6b9da45..f03982345 100644 --- a/ui/src/api/tool/tool.ts +++ b/ui/src/api/tool/tool.ts @@ -24,6 +24,16 @@ const getToolList: ( return get(`${prefix.value}`, data, loading) } +/** + * 工具列表带分页(无分页) + */ +const getAllToolList: ( + data?: any, + loading?: Ref, +) => Promise> = (data, loading) => { + return get(`${prefix.value}/tool_list`, data, loading) +} + /** * 工具列表带分页 * @param 参数 @@ -140,6 +150,7 @@ const addInternalTool: ( export default { getToolList, + getAllToolList, getToolListPage, putTool, getToolById, diff --git a/ui/src/api/type/tool.ts b/ui/src/api/type/tool.ts index 867100451..0444f21cb 100644 --- a/ui/src/api/type/tool.ts +++ b/ui/src/api/type/tool.ts @@ -8,6 +8,7 @@ interface toolData { init_field_list?: Array is_active?: boolean folder_id?: string + tool_type?: string } interface AddInternalToolParam { diff --git a/ui/src/locales/lang/en-US/views/tool.ts b/ui/src/locales/lang/en-US/views/tool.ts index 410150488..c73779646 100644 --- a/ui/src/locales/lang/en-US/views/tool.ts +++ b/ui/src/locales/lang/en-US/views/tool.ts @@ -1,5 +1,6 @@ export default { title: 'Tool', + all: 'All', createTool: 'Create Tool', editTool: 'Edit Tool', copyTool: 'Copy Tool', @@ -66,6 +67,11 @@ export default { selectPlaceholder: 'Please select parameter', inputPlaceholder: 'Please enter parameter values', }, + mcp: { + label: 'MCP Server Config', + placeholder: 'Please enter MCP Server config', + tip: 'Only supports SSE and Streamable HTTP calling methods', + }, debug: { run: 'Run', output: 'Output', diff --git a/ui/src/locales/lang/zh-CN/views/application-workflow.ts b/ui/src/locales/lang/zh-CN/views/application-workflow.ts index 8c4575711..a0bd58225 100644 --- a/ui/src/locales/lang/zh-CN/views/application-workflow.ts +++ b/ui/src/locales/lang/zh-CN/views/application-workflow.ts @@ -238,6 +238,7 @@ export default { mcpServerTip: '请输入JSON格式的MCP服务器配置', mcpToolTip: '请选择工具', configLabel: 'MCP Server Config (仅支持SSE/Streamable HTTP调用方式)', + reference: '引用MCP', }, imageGenerateNode: { label: '图片生成', diff --git a/ui/src/locales/lang/zh-CN/views/tool.ts b/ui/src/locales/lang/zh-CN/views/tool.ts index e0260994f..d1c1dd061 100644 --- a/ui/src/locales/lang/zh-CN/views/tool.ts +++ b/ui/src/locales/lang/zh-CN/views/tool.ts @@ -1,7 +1,10 @@ export default { title: '工具', + all: '全部', createTool: '创建工具', editTool: '编辑工具', + createMcpTool: '创建MCP', + editMcpTool: '编辑MCP', copyTool: '复制工具', importTool: '导入工具', toolStore: { @@ -60,6 +63,11 @@ export default { selectPlaceholder: '请选择参数', inputPlaceholder: '请输入参数值', }, + mcp: { + label: 'MCP Server Config', + placeholder: '请输入MCP Server配置', + tip: '仅支持SSE、Streamable HTTP调用方式', + }, debug: { run: '运行', output: '输出', diff --git a/ui/src/locales/lang/zh-Hant/views/tool.ts b/ui/src/locales/lang/zh-Hant/views/tool.ts index 3c7a30ba0..a3d06efc9 100644 --- a/ui/src/locales/lang/zh-Hant/views/tool.ts +++ b/ui/src/locales/lang/zh-Hant/views/tool.ts @@ -1,5 +1,6 @@ export default { title: '工具', + all: '全部', createTool: '建立工具', editTool: '編輯工具', copyTool: '複製工具', @@ -63,6 +64,11 @@ export default { selectPlaceholder: '請选择參數', inputPlaceholder: '請輸入參數值', }, + mcp: { + label: 'MCP Server Config', + placeholder: '請輸入MCP Server配置', + tip: '僅支援SSE、Streamable HTTP呼叫方式', + }, debug: { run: '運行', output: '輸出', diff --git a/ui/src/stores/modules/tool.ts b/ui/src/stores/modules/tool.ts index 34a6242e2..d31e7dd0b 100644 --- a/ui/src/stores/modules/tool.ts +++ b/ui/src/stores/modules/tool.ts @@ -8,11 +8,15 @@ import useFolderStore from './folder' const useToolStore = defineStore('tool', { state: () => ({ toolList: [] as any[], + tool_type: '' as string, }), actions: { setToolList(list: any[]) { this.toolList = list }, + setToolType(type: string) { + this.tool_type = type + }, }, }) diff --git a/ui/src/views/system-resource-management/ToolResourceIndex.vue b/ui/src/views/system-resource-management/ToolResourceIndex.vue index bf8dc9691..cc4ff8f0b 100644 --- a/ui/src/views/system-resource-management/ToolResourceIndex.vue +++ b/ui/src/views/system-resource-management/ToolResourceIndex.vue @@ -72,9 +72,14 @@ diff --git a/ui/src/views/system-shared/ToolSharedIndex.vue b/ui/src/views/system-shared/ToolSharedIndex.vue index db9d9c927..d3ffe2e8d 100644 --- a/ui/src/views/system-shared/ToolSharedIndex.vue +++ b/ui/src/views/system-shared/ToolSharedIndex.vue @@ -8,6 +8,13 @@
{{ t('views.tool.title') }}
+
+ + {{ $t('views.tool.all') }} + {{ $t('views.tool.title') }} + MCP + +
@@ -19,6 +26,16 @@ import { onMounted, ref, reactive, computed } from 'vue' import ToolListContainer from '@/views/tool/component/ToolListContainer.vue' import { t } from '@/locales' +import useStore from "@/stores"; + +const { tool } = useStore() + + +const toolType = ref('') + +function radioChange() { + tool.setToolType(toolType.value) +} onMounted(() => {}) diff --git a/ui/src/views/tool/McpToolFormDrawer.vue b/ui/src/views/tool/McpToolFormDrawer.vue new file mode 100644 index 000000000..c94da16e6 --- /dev/null +++ b/ui/src/views/tool/McpToolFormDrawer.vue @@ -0,0 +1,284 @@ + + + + diff --git a/ui/src/views/tool/component/ToolListContainer.vue b/ui/src/views/tool/component/ToolListContainer.vue index 3199a51d3..9ecbdc691 100644 --- a/ui/src/views/tool/component/ToolListContainer.vue +++ b/ui/src/views/tool/component/ToolListContainer.vue @@ -54,6 +54,16 @@ + +
+ + + +
+
创建MCP
+
+
+
+ @@ -305,6 +316,7 @@ import { cloneDeep } from 'lodash' import { useRoute, onBeforeRouteLeave } from 'vue-router' import InitParamDrawer from '@/views/tool/component/InitParamDrawer.vue' import ToolFormDrawer from '@/views/tool/ToolFormDrawer.vue' +import McpToolFormDrawer from '@/views/tool/McpToolFormDrawer.vue' import CreateFolderDialog from '@/components/folder-tree/CreateFolderDialog.vue' import AuthorizedWorkspace from '@/views/system-shared/AuthorizedWorkspaceDialog.vue' import ToolStoreDialog from '@/views/tool/toolStore/ToolStoreDialog.vue' @@ -374,7 +386,9 @@ const search_type_change = () => { search_form.value = { name: '', create_user: '' } } const ToolFormDrawerRef = ref() +const McpToolFormDrawerRef = ref() const ToolDrawertitle = ref('') +const McpToolDrawertitle = ref('') const MoveToDialogRef = ref() function openMoveToDialog(data: any) { @@ -400,6 +414,11 @@ function openAuthorizedWorkspaceDialog(row: any) { } function openCreateDialog(data?: any) { + // mcp工具 + if (data?.tool_type === 'MCP') { + openCreateMcpDialog(data) + return + } // 有template_id的不允许编辑,是模板转换来的 if (data?.template_id) { return @@ -420,6 +439,27 @@ function openCreateDialog(data?: any) { } } +function openCreateMcpDialog(data?: any) { + // 有template_id的不允许编辑,是模板转换来的 + if (data?.template_id) { + return + } + // 共享过来的工具不让编辑 + if (isShared.value) { + return + } + McpToolDrawertitle.value = data ? t('views.tool.editMcpTool') : t('views.tool.createMcpTool') + if (data) { + loadSharedApi({ type: 'tool', systemType: apiType.value }) + .getToolById(data?.id, loading) + .then((res: any) => { + McpToolFormDrawerRef.value.open(res.data) + }) + } else { + McpToolFormDrawerRef.value.open(data) + } +} + async function changeState(row: any) { if (row.is_active) { MsgConfirm( @@ -616,10 +656,21 @@ watch( }, { deep: true, immediate: true }, ) + +watch( + () => tool.tool_type, + () => { + paginationConfig.current_page = 1 + tool.setToolList([]) + getList() + }, +) + function getList() { const params: any = { folder_id: folder.currentFolder?.id || user.getWorkspaceId(), scope: apiType.value === 'systemShare' ? 'SHARED' : 'WORKSPACE', + tool_type: tool.tool_type || '', } if (search_form.value[search_type.value]) { params[search_type.value] = search_form.value[search_type.value] diff --git a/ui/src/views/tool/index.vue b/ui/src/views/tool/index.vue index fd5ceac1b..def98866c 100644 --- a/ui/src/views/tool/index.vue +++ b/ui/src/views/tool/index.vue @@ -20,6 +20,13 @@ {{ $t('views.shared.shared_tool') }} +
+ + {{ $t('views.tool.all') }} + {{ $t('views.tool.title') }} + MCP + +
@@ -49,6 +56,7 @@ const permissionPrecise = computed(() => { }) const loading = ref(false) +const toolType = ref('') const folderList = ref([]) @@ -71,6 +79,10 @@ function folderClickHandle(row: any) { tool.setToolList([]) } +function radioChange() { + tool.setToolType(toolType.value) +} + function refreshFolder() { getFolder() } diff --git a/ui/src/workflow/nodes/mcp-node/index.vue b/ui/src/workflow/nodes/mcp-node/index.vue index 3c3ec6814..2879cb60d 100644 --- a/ui/src/workflow/nodes/mcp-node/index.vue +++ b/ui/src/workflow/nodes/mcp-node/index.vue @@ -12,7 +12,28 @@ hide-required-asterisk > + + + + {{ mcpTool.name }} + + {{ t('views.shared.title') }} + + +