feat: add MCP tool support with new form and dropdown options

This commit is contained in:
CaptainB 2025-08-11 13:12:56 +08:00 committed by 刘瑞斌
parent c468952274
commit f1356e9b61
22 changed files with 769 additions and 5 deletions

View File

@ -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:

View File

@ -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='资源类型'),
),
]

View File

@ -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='工具类型'),
),
]

View File

@ -31,6 +31,7 @@ class ToolScope(models.TextChoices):
class ToolType(models.TextChoices):
INTERNAL = "INTERNAL", '内置'
CUSTOM = "CUSTOM", "自定义"
MCP = "MCP", "MCP工具"
class Tool(AppModelMixin):

View File

@ -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,

View File

@ -10,6 +10,7 @@ urlpatterns = [
path('workspace/<str:workspace_id>/tool/import', views.ToolView.Import.as_view()),
path('workspace/<str:workspace_id>/tool/pylint', views.ToolView.Pylint.as_view()),
path('workspace/<str:workspace_id>/tool/debug', views.ToolView.Debug.as_view()),
path('workspace/<str:workspace_id>/tool/tool_list', views.ToolView.Query.as_view()),
path('workspace/<str:workspace_id>/tool/<str:tool_id>', views.ToolView.Operate.as_view()),
path('workspace/<str:workspace_id>/tool/<str:tool_id>/edit_icon', views.ToolView.EditIcon.as_view()),
path('workspace/<str:workspace_id>/tool/<str:tool_id>/export', views.ToolView.Export.as_view()),

View File

@ -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]

View File

@ -21,6 +21,21 @@ const getToolList: (data?: any, loading?: Ref<boolean>) => Promise<Result<Array<
return get(`${prefix}`, data, loading)
}
/**
*
* @params
* param {
"name": "string",
"tool_type": "string",
}
*/
const getAllToolList: (data?: any, loading?: Ref<boolean>) => Promise<Result<Array<any>>> = (
data,
loading,
) => {
return get(`${prefix}/tool_list`, data, loading)
}
/**
*
* @param
@ -110,6 +125,7 @@ const postPylint: (code: string, loading?: Ref<boolean>) => Promise<Result<any>>
export default {
getToolListPage,
getToolList,
getAllToolList,
putTool,
getToolById,
postToolDebug,

View File

@ -17,6 +17,16 @@ const getToolList: (data?: any, loading?: Ref<boolean>) => Promise<Result<Array<
return get(`${prefix}`, data, loading)
}
/**
*
*/
const getAllToolList: (data?: any, loading?: Ref<boolean>) => Promise<Result<Array<any>>> = (
data,
loading,
) => {
return get(`${prefix}/tool_list`, data, loading)
}
/**
*
* @param
@ -135,6 +145,7 @@ const addInternalTool: (
export default {
getToolList,
getAllToolList,
getToolListPage,
putTool,
getToolById,

View File

@ -24,6 +24,16 @@ const getToolList: (
return get(`${prefix.value}`, data, loading)
}
/**
*
*/
const getAllToolList: (
data?: any,
loading?: Ref<boolean>,
) => Promise<Result<{ tools: any[]; folders: any[] }>> = (data, loading) => {
return get(`${prefix.value}/tool_list`, data, loading)
}
/**
*
* @param
@ -140,6 +150,7 @@ const addInternalTool: (
export default {
getToolList,
getAllToolList,
getToolListPage,
putTool,
getToolById,

View File

@ -8,6 +8,7 @@ interface toolData {
init_field_list?: Array<any>
is_active?: boolean
folder_id?: string
tool_type?: string
}
interface AddInternalToolParam {

View File

@ -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',

View File

@ -238,6 +238,7 @@ export default {
mcpServerTip: '请输入JSON格式的MCP服务器配置',
mcpToolTip: '请选择工具',
configLabel: 'MCP Server Config (仅支持SSE/Streamable HTTP调用方式)',
reference: '引用MCP',
},
imageGenerateNode: {
label: '图片生成',

View File

@ -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: '输出',

View File

@ -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: '輸出',

View File

@ -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
},
},
})

View File

@ -72,9 +72,14 @@
<el-table-column prop="tool_type" :label="$t('views.system.resource_management.type')">
<template #default="scope">
<span v-if="scope.row.tool_type === 'MCP'">
MCP
</span>
<span v-else>
{{
$t(ToolType[scope.row.template_id ? 'INTERNAL' : ('CUSTOM' as keyof typeof ToolType)])
$t(ToolType[scope.row.template_id ? 'INTERNAL' : ('CUSTOM' as keyof typeof ToolType)])
}}
</span>
</template>
</el-table-column>
<el-table-column :label="$t('common.status.label')" width="120">

View File

@ -8,6 +8,13 @@
<h5 class="ml-4 color-text-primary">{{ t('views.tool.title') }}</h5>
</el-breadcrumb-item>
</el-breadcrumb>
<div class="mt-16 mb-16">
<el-radio-group v-model="toolType" @change="radioChange" class="app-radio-button-group">
<el-radio-button value="">{{ $t('views.tool.all') }}</el-radio-button>
<el-radio-button value="CUSTOM">{{ $t('views.tool.title') }}</el-radio-button>
<el-radio-button value="MCP">MCP</el-radio-button>
</el-radio-group>
</div>
</template>
</ToolListContainer>
</div>
@ -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(() => {})
</script>

View File

@ -0,0 +1,284 @@
<template>
<el-drawer v-model="visible" size="60%" :before-close="close">
<template #header>
<h4>{{ title }}</h4>
</template>
<div>
<h4 class="title-decoration-1 mb-16">
{{ $t('views.model.modelForm.title.baseInfo') }}
</h4>
<el-form
ref="FormRef"
:model="form"
:rules="rules"
label-position="top"
require-asterisk-position="right"
v-loading="loading"
@submit.prevent
>
<el-form-item :label="$t('views.tool.form.toolName.label')" prop="name">
<div class="flex w-full">
<div
v-if="form.id"
class="edit-avatar mr-12"
@mouseenter="showEditIcon = true"
@mouseleave="showEditIcon = false"
>
<el-Avatar
v-if="isAppIcon(form.icon)"
:id="form.id"
shape="square"
:size="32"
style="background: none"
>
<img :src="String(form.icon)" alt=""/>
</el-Avatar>
<el-avatar v-else class="avatar-green" shape="square" :size="32">
<img src="@/assets/workflow/icon_tool.svg" style="width: 58%" alt=""/>
</el-avatar>
<el-Avatar
v-if="showEditIcon"
:id="form.id"
shape="square"
class="edit-mask"
:size="32"
@click="openEditAvatar"
>
<AppIcon iconName="app-edit"></AppIcon>
</el-Avatar>
</div>
<el-avatar v-else class="avatar-green mr-12" shape="square" :size="32">
<img src="@/assets/workflow/icon_tool.svg" style="width: 58%" alt=""/>
</el-avatar>
<el-input
v-model="form.name"
:placeholder="$t('views.tool.form.toolName.placeholder')"
maxlength="64"
show-word-limit
@blur="form.name = form.name?.trim()"
/>
</div>
</el-form-item>
<el-form-item :label="$t('views.tool.form.toolDescription.label')">
<el-input
v-model="form.desc"
type="textarea"
:placeholder="$t('views.tool.form.toolDescription.placeholder')"
maxlength="128"
show-word-limit
:autosize="{ minRows: 3 }"
@blur="form.desc = form.desc?.trim()"
/>
</el-form-item>
</el-form>
<h4 class="title-decoration-1 mb-16">
{{ $t('views.tool.form.mcp.label') }}
<span style="color: red; margin-left: -10px">*</span>
<el-text type="info" class="color-secondary">
{{ $t('views.tool.form.mcp.tip') }}
</el-text>
</h4>
<div class="mb-8">
<el-input
v-model="form.code"
:placeholder="mcpServerJson"
type="textarea"
:autosize="{ minRows: 5 }"
/>
</div>
</div>
<template #footer>
<div>
<el-button :loading="loading" @click="visible = false">{{ $t('common.cancel') }}</el-button>
<el-button
type="primary"
@click="submit(FormRef)"
:loading="loading"
v-if="isEdit ? permissionPrecise.edit(form?.id as string) : permissionPrecise.create()"
>
{{ isEdit ? $t('common.save') : $t('common.create') }}
</el-button>
</div>
</template>
<EditAvatarDialog ref="EditAvatarDialogRef" @refresh="refreshTool"/>
</el-drawer>
</template>
<script setup lang="ts">
import {computed, reactive, ref, watch} from 'vue'
import EditAvatarDialog from '@/views/tool/component/EditAvatarDialog.vue'
import type {toolData} from '@/api/type/tool'
import type {FormInstance} from 'element-plus'
import {MsgConfirm, MsgSuccess} from '@/utils/message'
import {cloneDeep} from 'lodash'
import {t} from '@/locales'
import {isAppIcon} from '@/utils/common'
import {useRoute} from 'vue-router'
import useStore from '@/stores'
import permissionMap from '@/permission'
import {loadSharedApi} from '@/utils/dynamics-api/shared-api'
const route = useRoute()
const props = defineProps({
title: String,
})
const {folder, user} = useStore()
const apiType = computed(() => {
if (route.path.includes('shared')) {
return 'systemShare'
} else if (route.path.includes('resource-management')) {
return 'systemManage'
} else {
return 'workspace'
}
})
const permissionPrecise = computed(() => {
return permissionMap['tool'][apiType.value]
})
const emit = defineEmits(['refresh'])
const EditAvatarDialogRef = ref()
const mcpServerJson = `{
"math": {
"url": "your_server",
"transport": "sse"
}
}`
const FormRef = ref()
const isEdit = ref(false)
const loading = ref(false)
const visible = ref(false)
const showEditor = ref(false)
const currentIndex = ref<any>(null)
const showEditIcon = ref(false)
const form = ref<toolData>({
name: '',
desc: '',
code: '',
icon: '',
input_field_list: [],
init_field_list: [],
tool_type: 'MCP',
})
watch(visible, (bool) => {
if (!bool) {
isEdit.value = false
showEditor.value = false
currentIndex.value = null
form.value = {
name: '',
desc: '',
code: '',
icon: '',
input_field_list: [],
init_field_list: [],
tool_type: 'MCP',
}
FormRef.value?.clearValidate()
}
})
const rules = reactive({
name: [
{
required: true,
message: t('views.tool.form.toolName.requiredMessage'),
trigger: 'blur',
},
],
})
function close() {
if (isEdit.value || !areAllValuesNonEmpty(form.value)) {
visible.value = false
} else {
MsgConfirm(t('common.tip'), t('views.tool.tip.saveMessage'), {
confirmButtonText: t('common.confirm'),
type: 'warning',
})
.then(() => {
visible.value = false
})
.catch(() => {
})
}
}
function areAllValuesNonEmpty(obj: any) {
return Object.values(obj).some((value) => {
return Array.isArray(value)
? value.length !== 0
: value !== null && value !== undefined && value !== ''
})
}
function refreshTool(data: any) {
form.value.icon = data
}
function openEditAvatar() {
EditAvatarDialogRef.value.open(form.value)
}
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid: any) => {
if (valid) {
if (isEdit.value) {
loadSharedApi({type: 'tool', systemType: apiType.value})
.putTool(form.value?.id as string, form.value, loading)
.then((res: any) => {
MsgSuccess(t('common.editSuccess'))
emit('refresh', res.data)
return user.profile()
})
.then(() => {
visible.value = false
})
} else {
const obj = {
folder_id: folder.currentFolder?.id,
...form.value,
}
loadSharedApi({type: 'tool', systemType: apiType.value})
.postTool(obj, loading)
.then((res: any) => {
MsgSuccess(t('common.createSuccess'))
emit('refresh')
return user.profile()
})
.then(() => {
visible.value = false
})
}
}
})
}
const open = (data: any) => {
if (data) {
isEdit.value = data?.id ? true : false
form.value = cloneDeep(data)
}
visible.value = true
setTimeout(() => {
showEditor.value = true
}, 100)
}
defineExpose({
open,
})
</script>
<style lang="scss" scoped></style>

View File

@ -54,6 +54,16 @@
</div>
</div>
</el-dropdown-item>
<el-dropdown-item @click="openCreateMcpDialog()">
<div class="flex align-center">
<el-avatar class="avatar-green" shape="square" :size="32">
<img src="@/assets/workflow/icon_tool.svg" style="width: 58%" alt="" />
</el-avatar>
<div class="pre-wrap ml-8">
<div class="lighter">创建MCP</div>
</div>
</div>
</el-dropdown-item>
<el-upload
ref="elUploadRef"
:file-list="[]"
@ -284,6 +294,7 @@
</ContentContainer>
<InitParamDrawer ref="InitParamDrawerRef" @refresh="refresh" />
<ToolFormDrawer ref="ToolFormDrawerRef" @refresh="refresh" :title="ToolDrawertitle" />
<McpToolFormDrawer ref="McpToolFormDrawerRef" @refresh="refresh" :title="McpToolDrawertitle" />
<CreateFolderDialog ref="CreateFolderDialogRef" v-if="!isShared" @refresh="refreshFolder" />
<ToolStoreDialog ref="toolStoreDialogRef" :api-type="apiType" @refresh="refresh" />
<AddInternalToolDialog ref="AddInternalToolDialogRef" @refresh="confirmAddInternalTool" />
@ -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]

View File

@ -20,6 +20,13 @@
{{ $t('views.shared.shared_tool') }}
</h2>
<FolderBreadcrumb :folderList="folderList" @click="folderClickHandle" v-else />
<div class="mt-16 mb-16">
<el-radio-group v-model="toolType" @change="radioChange" class="app-radio-button-group">
<el-radio-button value="">{{ $t('views.tool.all') }}</el-radio-button>
<el-radio-button value="CUSTOM">{{ $t('views.tool.title') }}</el-radio-button>
<el-radio-button value="MCP">MCP</el-radio-button>
</el-radio-group>
</div>
</template>
</ToolListContainer>
</LayoutContainer>
@ -49,6 +56,7 @@ const permissionPrecise = computed(() => {
})
const loading = ref(false)
const toolType = ref('')
const folderList = ref<any[]>([])
@ -71,6 +79,10 @@ function folderClickHandle(row: any) {
tool.setToolList([])
}
function radioChange() {
tool.setToolType(toolType.value)
}
function refreshFolder() {
getFolder()
}

View File

@ -12,7 +12,28 @@
hide-required-asterisk
>
<el-form-item label="MCP Server Config">
<template #label>
<div class="flex-between">
<div>
MCP Server Config
<span class="color-danger">*</span>
</div>
<el-select
:teleported="false"
v-model="form_data.mcp_source"
size="small"
style="width: 85px"
>
<el-option
:label="$t('views.applicationWorkflow.nodes.mcpNode.reference')"
value="referencing"
/>
<el-option :label="$t('common.custom')" value="custom" />
</el-select>
</div>
</template>
<MdEditorMagnify
v-if="form_data.mcp_source === 'custom'"
@wheel="wheel"
title="MCP Server Config"
v-model="form_data.mcp_servers"
@ -20,6 +41,19 @@
@submitDialog="submitDialog"
:placeholder="mcpServerJson"
/>
<el-select v-else v-model="form_data.mcp_tool_id" filterable @change="mcpToolSelectChange">
<el-option
v-for="mcpTool in mcpToolSelectOptions"
:key="mcpTool.id"
:label="mcpTool.name"
:value="mcpTool.id"
>
<span>{{ mcpTool.name }}</span>
<el-tag v-if="mcpTool.scope === 'SHARED'" type="info" class="info-tag ml-8 mt-4">
{{ t('views.shared.title') }}
</el-tag>
</el-option>
</el-select>
</el-form-item>
<el-form-item>
<template v-slot:label>
@ -201,7 +235,7 @@
<script setup lang="ts">
import { cloneDeep, set } from 'lodash'
import NodeContainer from '@/workflow/common/NodeContainer.vue'
import { computed, onMounted, ref } from 'vue'
import { computed, onMounted, ref, inject } from 'vue'
import { isLastNode } from '@/workflow/common/data'
import { t } from '@/locales'
import { MsgError, MsgSuccess } from '@/utils/message'
@ -209,12 +243,16 @@ import TooltipLabel from '@/components/dynamics-form/items/label/TooltipLabel.vu
import NodeCascader from '@/workflow/common/NodeCascader.vue'
import { useRoute } from 'vue-router'
import { loadSharedApi } from '@/utils/dynamics-api/shared-api'
import useStore from "@/stores";
const props = defineProps<{ nodeModel: any }>()
const { user } = useStore()
const route = useRoute()
const {
params: { id },
} = route as any
const getApplicationDetail = inject('getApplicationDetail') as any
const applicationDetail = getApplicationDetail()
const apiType = computed(() => {
if (route.path.includes('resource-management')) {
@ -248,17 +286,31 @@ const form = {
mcp_tools: [],
mcp_servers: '',
mcp_server: '',
mcp_source: 'referencing',
mcp_tool_id: '',
tool_params: {},
tool_form_field: [],
params_nested: '',
}
const mcpToolSelectOptions = ref<any[]>([])
function submitDialog(val: string) {
set(props.nodeModel.properties.node_data, 'mcp_servers', val)
}
async function mcpToolSelectChange() {
const tool = await loadSharedApi({ type: 'tool', systemType: apiType.value })
.getToolById(form_data.value.mcp_tool_id, loading)
form_data.value.mcp_servers = tool.data.code
}
function getTools() {
if (!form_data.value.mcp_servers) {
if (form_data.value.mcp_source === 'referencing' && !form_data.value.mcp_tool_id) {
MsgError(t('views.applicationWorkflow.nodes.mcpNode.mcpToolTip'))
return
}
if (form_data.value.mcp_source === 'custom' && !form_data.value.mcp_servers) {
MsgError(t('views.applicationWorkflow.nodes.mcpNode.mcpServerTip'))
return
}
@ -434,13 +486,34 @@ const validate = async () => {
}
}
function getMcpToolSelectOptions() {
const obj =
apiType.value === 'systemManage'
? {
scope: 'WORKSPACE',
tool_type: 'MCP',
workspace_id: applicationDetail.value?.workspace_id,
}
: {
scope: 'WORKSPACE',
tool_type: 'MCP',
}
loadSharedApi({type: 'tool', systemType: apiType.value})
.getAllToolList(obj, loading)
.then((res: any) => {
mcpToolSelectOptions.value = [...res.data.shared_tools, ...res.data.tools]
.filter((item: any) => item.is_active)
})
}
onMounted(() => {
if (typeof props.nodeModel.properties.node_data?.is_result === 'undefined') {
if (isLastNode(props.nodeModel)) {
set(props.nodeModel.properties.node_data, 'is_result', true)
}
}
getMcpToolSelectOptions()
set(props.nodeModel, 'validate', validate)
})
</script>