Skip to content

Commit 2a98d71

Browse files
committed
feat: add MCP server tools integration and UI components
1 parent c526a27 commit 2a98d71

File tree

18 files changed

+488
-5
lines changed

18 files changed

+488
-5
lines changed

apps/application/flow/step_node/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,14 @@
2525
from .start_node import *
2626
from .text_to_speech_step_node.impl.base_text_to_speech_node import BaseTextToSpeechNode
2727
from .variable_assign_node import BaseVariableAssignNode
28+
from .mcp_node import BaseMcpNode
2829

2930
node_list = [BaseStartStepNode, BaseChatNode, BaseSearchDatasetNode, BaseQuestionNode,
3031
BaseConditionNode, BaseReplyNode,
3132
BaseFunctionNodeNode, BaseFunctionLibNodeNode, BaseRerankerNode, BaseApplicationNode,
3233
BaseDocumentExtractNode,
3334
BaseImageUnderstandNode, BaseFormNode, BaseSpeechToTextNode, BaseTextToSpeechNode,
34-
BaseImageGenerateNode, BaseVariableAssignNode]
35+
BaseImageGenerateNode, BaseVariableAssignNode, BaseMcpNode]
3536

3637

3738
def get_node(node_type):
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# coding=utf-8
2+
3+
from .impl import *
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# coding=utf-8
2+
3+
from typing import Type
4+
5+
from rest_framework import serializers
6+
7+
from application.flow.i_step_node import INode, NodeResult
8+
from common.util.field_message import ErrMessage
9+
from django.utils.translation import gettext_lazy as _
10+
11+
12+
class McpNodeSerializer(serializers.Serializer):
13+
mcp_servers = serializers.JSONField(required=True,
14+
error_messages=ErrMessage.char(_("Mcp servers")))
15+
16+
mcp_server = serializers.CharField(required=True,
17+
error_messages=ErrMessage.char(_("Mcp server")))
18+
19+
mcp_tool = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Mcp tool")))
20+
21+
tool_params = serializers.DictField(required=True,
22+
error_messages=ErrMessage.char(_("Tool parameters")))
23+
24+
25+
class IMcpNode(INode):
26+
type = 'mcp-node'
27+
28+
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
29+
return McpNodeSerializer
30+
31+
def _run(self):
32+
return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data)
33+
34+
def execute(self, mcp_servers, mcp_server, mcp_tool, tool_params, **kwargs) -> NodeResult:
35+
pass
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# coding=utf-8
2+
3+
from .base_mcp_node import BaseMcpNode
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# coding=utf-8
2+
import asyncio
3+
import json
4+
from typing import List
5+
6+
from langchain_mcp_adapters.client import MultiServerMCPClient
7+
8+
from application.flow.i_step_node import NodeResult
9+
from application.flow.step_node.mcp_node.i_mcp_node import IMcpNode
10+
11+
12+
class BaseMcpNode(IMcpNode):
13+
def save_context(self, details, workflow_manage):
14+
self.context['result'] = details.get('result')
15+
self.context['tool_params'] = details.get('tool_params')
16+
self.context['mcp_tool'] = details.get('mcp_tool')
17+
self.answer_text = details.get('result')
18+
19+
def execute(self, mcp_servers, mcp_server, mcp_tool, tool_params, **kwargs) -> NodeResult:
20+
servers = json.loads(mcp_servers)
21+
params = self.handle_variables(tool_params)
22+
23+
async def call_tool(s, session, t, a):
24+
async with MultiServerMCPClient(s) as client:
25+
s = await client.sessions[session].call_tool(t, a)
26+
return s
27+
28+
res = asyncio.run(call_tool(servers, mcp_server, mcp_tool, params))
29+
return NodeResult({'result': [content.text for content in res.content], 'tool_params': params, 'mcp_tool': mcp_tool}, {})
30+
31+
def handle_variables(self, tool_params):
32+
# 处理参数中的变量
33+
for k, v in tool_params.items():
34+
if type(v) == str:
35+
tool_params[k] = self.workflow_manage.generate_prompt(tool_params[k])
36+
if type(v) == dict:
37+
self.handle_variables(v)
38+
return tool_params
39+
40+
def get_reference_content(self, fields: List[str]):
41+
return str(self.workflow_manage.get_reference_field(
42+
fields[0],
43+
fields[1:]))
44+
45+
def get_details(self, index: int, **kwargs):
46+
return {
47+
'name': self.node.properties.get('stepName'),
48+
"index": index,
49+
'run_time': self.context.get('run_time'),
50+
'status': self.status,
51+
'err_message': self.err_message,
52+
'type': self.node.type,
53+
'mcp_tool': self.context.get('mcp_tool'),
54+
'tool_params': self.context.get('tool_params'),
55+
'result': self.context.get('result'),
56+
}

apps/application/serializers/application_serializers.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
@date:2023/11/7 10:02
77
@desc:
88
"""
9+
import asyncio
910
import datetime
1011
import hashlib
1112
import json
@@ -23,6 +24,8 @@
2324
from django.db.models.expressions import RawSQL
2425
from django.http import HttpResponse
2526
from django.template import Template, Context
27+
from langchain_mcp_adapters.client import MultiServerMCPClient
28+
from mcp.client.sse import sse_client
2629
from rest_framework import serializers, status
2730
from rest_framework.utils.formatting import lazy_format
2831

@@ -1305,3 +1308,28 @@ def edit(self, instance, with_valid=True):
13051308
application_api_key.save()
13061309
# 写入缓存
13071310
get_application_api_key(application_api_key.secret_key, False)
1311+
1312+
class McpServers(serializers.Serializer):
1313+
mcp_servers = serializers.JSONField(required=True)
1314+
1315+
def get_mcp_servers(self, with_valid=True):
1316+
if with_valid:
1317+
self.is_valid(raise_exception=True)
1318+
servers = json.loads(self.data.get('mcp_servers'))
1319+
1320+
async def get_mcp_tools(servers):
1321+
async with MultiServerMCPClient(servers) as client:
1322+
return client.get_tools()
1323+
1324+
tools = []
1325+
for server in servers:
1326+
tools += [
1327+
{
1328+
'server': server,
1329+
'name': tool.name,
1330+
'description': tool.description,
1331+
'args_schema': tool.args_schema,
1332+
}
1333+
for tool in asyncio.run(get_mcp_tools({server: servers[server]}))]
1334+
return tools
1335+

apps/application/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
path('application/profile', views.Application.Profile.as_view(), name='application/profile'),
1010
path('application/embed', views.Application.Embed.as_view()),
1111
path('application/authentication', views.Application.Authentication.as_view()),
12+
path('application/mcp_servers', views.Application.McpServers.as_view()),
1213
path('application/<str:application_id>/publish', views.Application.Publish.as_view()),
1314
path('application/<str:application_id>/edit_icon', views.Application.EditIcon.as_view()),
1415
path('application/<str:application_id>/export', views.Application.Export.as_view()),

apps/application/views/application_views.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,3 +700,13 @@ def post(self, request: Request, application_id: str):
700700
data={'application_id': application_id, 'user_id': request.user.id}).play_demo_text(request.data)
701701
return HttpResponse(byte_data, status=200, headers={'Content-Type': 'audio/mp3',
702702
'Content-Disposition': 'attachment; filename="abc.mp3"'})
703+
704+
class McpServers(APIView):
705+
authentication_classes = [TokenAuth]
706+
707+
@action(methods=['GET'], detail=False)
708+
@has_permissions(PermissionConstants.APPLICATION_READ, compare=CompareConstants.AND)
709+
@log(menu='Application', operate="Get the MCP server tools")
710+
def get(self, request: Request):
711+
return result.success(ApplicationSerializer.McpServers(
712+
data={'mcp_servers': request.query_params.get('mcp_servers')}).get_mcp_servers())

ui/src/api/application.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,13 @@ const getFunctionLib: (
350350
return get(`${prefix}/${application_id}/function_lib/${function_lib_id}`, undefined, loading)
351351
}
352352

353+
const getMcpTools: (
354+
data: any,
355+
loading?: Ref<boolean>
356+
) => Promise<Result<any>> = (data, loading) => {
357+
return get(`${prefix}/mcp_servers`, data, loading)
358+
}
359+
353360
const getApplicationById: (
354361
application_id: String,
355362
app_id: String,
@@ -576,5 +583,6 @@ export default {
576583
uploadFile,
577584
exportApplication,
578585
importApplication,
579-
getApplicationById
586+
getApplicationById,
587+
getMcpTools
580588
}

ui/src/components/ai-chat/ExecutionDetailDialog.vue

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,40 @@
639639
</div>
640640
</div>
641641
</template>
642+
643+
<!-- MCP 节点 -->
644+
<template v-if="item.type === WorkflowType.McpNode">
645+
<div class="card-never border-r-4">
646+
<h5 class="p-8-12">
647+
{{ $t('views.applicationWorkflow.nodes.mcpNode.tool') }}
648+
</h5>
649+
<div class="p-8-12 border-t-dashed lighter">
650+
<div class="mb-8">
651+
<span class="color-secondary"> {{ $t('views.applicationWorkflow.nodes.mcpNode.tool') }}: </span> {{ item.mcp_tool }}
652+
</div>
653+
</div>
654+
</div>
655+
<div class="card-never border-r-4">
656+
<h5 class="p-8-12">
657+
{{ $t('views.applicationWorkflow.nodes.mcpNode.toolParam') }}
658+
</h5>
659+
<div class="p-8-12 border-t-dashed lighter">
660+
<div v-for="(value, name) in item.tool_params" :key="name" class="mb-8">
661+
<span class="color-secondary">{{ name }}:</span> {{ value }}
662+
</div>
663+
</div>
664+
</div>
665+
<div class="card-never border-r-4">
666+
<h5 class="p-8-12">
667+
{{ $t('common.param.outputParam') }}
668+
</h5>
669+
<div class="p-8-12 border-t-dashed lighter">
670+
<div v-for="(f, i) in item.result" :key="i" class="mb-8">
671+
<span class="color-secondary">result:</span> {{ f }}
672+
</div>
673+
</div>
674+
</div>
675+
</template>
642676
</template>
643677
<template v-else>
644678
<div class="card-never border-r-4">

0 commit comments

Comments
 (0)