From 2a98d717e06c71e526c0c5675b92c3522bfe3a4a Mon Sep 17 00:00:00 2001 From: CaptainB Date: Fri, 21 Mar 2025 17:06:01 +0800 Subject: [PATCH 1/2] feat: add MCP server tools integration and UI components --- apps/application/flow/step_node/__init__.py | 3 +- .../flow/step_node/mcp_node/__init__.py | 3 + .../flow/step_node/mcp_node/i_mcp_node.py | 35 +++ .../flow/step_node/mcp_node/impl/__init__.py | 3 + .../step_node/mcp_node/impl/base_mcp_node.py | 56 ++++ .../serializers/application_serializers.py | 28 ++ apps/application/urls.py | 1 + apps/application/views/application_views.py | 10 + ui/src/api/application.ts | 10 +- .../ai-chat/ExecutionDetailDialog.vue | 34 +++ ui/src/enums/workflow.ts | 3 +- .../lang/en-US/views/application-workflow.ts | 8 + .../lang/zh-CN/views/application-workflow.ts | 8 + .../zh-Hant/views/application-workflow.ts | 8 + ui/src/workflow/common/data.ts | 24 +- ui/src/workflow/icons/mcp-node-icon.vue | 6 + ui/src/workflow/nodes/mcp-node/index.ts | 14 + ui/src/workflow/nodes/mcp-node/index.vue | 239 ++++++++++++++++++ 18 files changed, 488 insertions(+), 5 deletions(-) create mode 100644 apps/application/flow/step_node/mcp_node/__init__.py create mode 100644 apps/application/flow/step_node/mcp_node/i_mcp_node.py create mode 100644 apps/application/flow/step_node/mcp_node/impl/__init__.py create mode 100644 apps/application/flow/step_node/mcp_node/impl/base_mcp_node.py create mode 100644 ui/src/workflow/icons/mcp-node-icon.vue create mode 100644 ui/src/workflow/nodes/mcp-node/index.ts create mode 100644 ui/src/workflow/nodes/mcp-node/index.vue diff --git a/apps/application/flow/step_node/__init__.py b/apps/application/flow/step_node/__init__.py index f3602901aa5..0ce1d5fedd1 100644 --- a/apps/application/flow/step_node/__init__.py +++ b/apps/application/flow/step_node/__init__.py @@ -25,13 +25,14 @@ from .start_node import * from .text_to_speech_step_node.impl.base_text_to_speech_node import BaseTextToSpeechNode from .variable_assign_node import BaseVariableAssignNode +from .mcp_node import BaseMcpNode node_list = [BaseStartStepNode, BaseChatNode, BaseSearchDatasetNode, BaseQuestionNode, BaseConditionNode, BaseReplyNode, BaseFunctionNodeNode, BaseFunctionLibNodeNode, BaseRerankerNode, BaseApplicationNode, BaseDocumentExtractNode, BaseImageUnderstandNode, BaseFormNode, BaseSpeechToTextNode, BaseTextToSpeechNode, - BaseImageGenerateNode, BaseVariableAssignNode] + BaseImageGenerateNode, BaseVariableAssignNode, BaseMcpNode] def get_node(node_type): diff --git a/apps/application/flow/step_node/mcp_node/__init__.py b/apps/application/flow/step_node/mcp_node/__init__.py new file mode 100644 index 00000000000..f3feecc9ce2 --- /dev/null +++ b/apps/application/flow/step_node/mcp_node/__init__.py @@ -0,0 +1,3 @@ +# coding=utf-8 + +from .impl import * diff --git a/apps/application/flow/step_node/mcp_node/i_mcp_node.py b/apps/application/flow/step_node/mcp_node/i_mcp_node.py new file mode 100644 index 00000000000..94cb4da7729 --- /dev/null +++ b/apps/application/flow/step_node/mcp_node/i_mcp_node.py @@ -0,0 +1,35 @@ +# coding=utf-8 + +from typing import Type + +from rest_framework import serializers + +from application.flow.i_step_node import INode, NodeResult +from common.util.field_message import ErrMessage +from django.utils.translation import gettext_lazy as _ + + +class McpNodeSerializer(serializers.Serializer): + mcp_servers = serializers.JSONField(required=True, + error_messages=ErrMessage.char(_("Mcp servers"))) + + mcp_server = serializers.CharField(required=True, + error_messages=ErrMessage.char(_("Mcp server"))) + + mcp_tool = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Mcp tool"))) + + tool_params = serializers.DictField(required=True, + error_messages=ErrMessage.char(_("Tool parameters"))) + + +class IMcpNode(INode): + type = 'mcp-node' + + def get_node_params_serializer_class(self) -> Type[serializers.Serializer]: + return McpNodeSerializer + + def _run(self): + return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data) + + def execute(self, mcp_servers, mcp_server, mcp_tool, tool_params, **kwargs) -> NodeResult: + pass diff --git a/apps/application/flow/step_node/mcp_node/impl/__init__.py b/apps/application/flow/step_node/mcp_node/impl/__init__.py new file mode 100644 index 00000000000..8c9a5ee197c --- /dev/null +++ b/apps/application/flow/step_node/mcp_node/impl/__init__.py @@ -0,0 +1,3 @@ +# coding=utf-8 + +from .base_mcp_node import BaseMcpNode diff --git a/apps/application/flow/step_node/mcp_node/impl/base_mcp_node.py b/apps/application/flow/step_node/mcp_node/impl/base_mcp_node.py new file mode 100644 index 00000000000..b3c1f2d9bed --- /dev/null +++ b/apps/application/flow/step_node/mcp_node/impl/base_mcp_node.py @@ -0,0 +1,56 @@ +# coding=utf-8 +import asyncio +import json +from typing import List + +from langchain_mcp_adapters.client import MultiServerMCPClient + +from application.flow.i_step_node import NodeResult +from application.flow.step_node.mcp_node.i_mcp_node import IMcpNode + + +class BaseMcpNode(IMcpNode): + def save_context(self, details, workflow_manage): + self.context['result'] = details.get('result') + self.context['tool_params'] = details.get('tool_params') + self.context['mcp_tool'] = details.get('mcp_tool') + self.answer_text = details.get('result') + + def execute(self, mcp_servers, mcp_server, mcp_tool, tool_params, **kwargs) -> NodeResult: + servers = json.loads(mcp_servers) + params = self.handle_variables(tool_params) + + async def call_tool(s, session, t, a): + async with MultiServerMCPClient(s) as client: + s = await client.sessions[session].call_tool(t, a) + return s + + res = asyncio.run(call_tool(servers, mcp_server, mcp_tool, params)) + return NodeResult({'result': [content.text for content in res.content], 'tool_params': params, 'mcp_tool': mcp_tool}, {}) + + def handle_variables(self, tool_params): + # 处理参数中的变量 + for k, v in tool_params.items(): + if type(v) == str: + tool_params[k] = self.workflow_manage.generate_prompt(tool_params[k]) + if type(v) == dict: + self.handle_variables(v) + return tool_params + + def get_reference_content(self, fields: List[str]): + return str(self.workflow_manage.get_reference_field( + fields[0], + fields[1:])) + + def get_details(self, index: int, **kwargs): + return { + 'name': self.node.properties.get('stepName'), + "index": index, + 'run_time': self.context.get('run_time'), + 'status': self.status, + 'err_message': self.err_message, + 'type': self.node.type, + 'mcp_tool': self.context.get('mcp_tool'), + 'tool_params': self.context.get('tool_params'), + 'result': self.context.get('result'), + } diff --git a/apps/application/serializers/application_serializers.py b/apps/application/serializers/application_serializers.py index 69d518cfd1c..514a9e5f00a 100644 --- a/apps/application/serializers/application_serializers.py +++ b/apps/application/serializers/application_serializers.py @@ -6,6 +6,7 @@ @date:2023/11/7 10:02 @desc: """ +import asyncio import datetime import hashlib import json @@ -23,6 +24,8 @@ from django.db.models.expressions import RawSQL from django.http import HttpResponse from django.template import Template, Context +from langchain_mcp_adapters.client import MultiServerMCPClient +from mcp.client.sse import sse_client from rest_framework import serializers, status from rest_framework.utils.formatting import lazy_format @@ -1305,3 +1308,28 @@ def edit(self, instance, with_valid=True): application_api_key.save() # 写入缓存 get_application_api_key(application_api_key.secret_key, False) + + class McpServers(serializers.Serializer): + mcp_servers = serializers.JSONField(required=True) + + def get_mcp_servers(self, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + servers = json.loads(self.data.get('mcp_servers')) + + async def get_mcp_tools(servers): + async with MultiServerMCPClient(servers) as client: + return client.get_tools() + + tools = [] + for server in servers: + tools += [ + { + 'server': server, + 'name': tool.name, + 'description': tool.description, + 'args_schema': tool.args_schema, + } + for tool in asyncio.run(get_mcp_tools({server: servers[server]}))] + return tools + diff --git a/apps/application/urls.py b/apps/application/urls.py index 6dc2ae5af63..b294289541e 100644 --- a/apps/application/urls.py +++ b/apps/application/urls.py @@ -9,6 +9,7 @@ path('application/profile', views.Application.Profile.as_view(), name='application/profile'), path('application/embed', views.Application.Embed.as_view()), path('application/authentication', views.Application.Authentication.as_view()), + path('application/mcp_servers', views.Application.McpServers.as_view()), path('application//publish', views.Application.Publish.as_view()), path('application//edit_icon', views.Application.EditIcon.as_view()), path('application//export', views.Application.Export.as_view()), diff --git a/apps/application/views/application_views.py b/apps/application/views/application_views.py index ab97de6262e..991a6f4d5dc 100644 --- a/apps/application/views/application_views.py +++ b/apps/application/views/application_views.py @@ -700,3 +700,13 @@ def post(self, request: Request, application_id: str): data={'application_id': application_id, 'user_id': request.user.id}).play_demo_text(request.data) return HttpResponse(byte_data, status=200, headers={'Content-Type': 'audio/mp3', 'Content-Disposition': 'attachment; filename="abc.mp3"'}) + + class McpServers(APIView): + authentication_classes = [TokenAuth] + + @action(methods=['GET'], detail=False) + @has_permissions(PermissionConstants.APPLICATION_READ, compare=CompareConstants.AND) + @log(menu='Application', operate="Get the MCP server tools") + def get(self, request: Request): + return result.success(ApplicationSerializer.McpServers( + data={'mcp_servers': request.query_params.get('mcp_servers')}).get_mcp_servers()) diff --git a/ui/src/api/application.ts b/ui/src/api/application.ts index a85b031c00f..efd4a4985a8 100644 --- a/ui/src/api/application.ts +++ b/ui/src/api/application.ts @@ -350,6 +350,13 @@ const getFunctionLib: ( return get(`${prefix}/${application_id}/function_lib/${function_lib_id}`, undefined, loading) } +const getMcpTools: ( + data: any, + loading?: Ref +) => Promise> = (data, loading) => { + return get(`${prefix}/mcp_servers`, data, loading) +} + const getApplicationById: ( application_id: String, app_id: String, @@ -576,5 +583,6 @@ export default { uploadFile, exportApplication, importApplication, - getApplicationById + getApplicationById, + getMcpTools } diff --git a/ui/src/components/ai-chat/ExecutionDetailDialog.vue b/ui/src/components/ai-chat/ExecutionDetailDialog.vue index cfb6f54d0ae..c36e36e52c6 100644 --- a/ui/src/components/ai-chat/ExecutionDetailDialog.vue +++ b/ui/src/components/ai-chat/ExecutionDetailDialog.vue @@ -639,6 +639,40 @@ + + +