Skip to content

Commit 717a10e

Browse files
committed
feat: add MCP server tools integration and UI components
1 parent e61142a commit 717a10e

File tree

10 files changed

+133
-32
lines changed

10 files changed

+133
-32
lines changed

apps/application/flow/step_node/__init__.py

+2-1
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):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# coding=utf-8
2+
3+
from .impl import *
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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# coding=utf-8
2+
3+
from .base_mcp_node import BaseMcpNode
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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['question'] = details.get('question')
16+
self.answer_text = details.get('result')
17+
18+
def execute(self, mcp_servers, mcp_server, mcp_tool, tool_params, **kwargs) -> NodeResult:
19+
servers = json.loads(mcp_servers)
20+
tool_params = {'query': '中北大学如何'}
21+
22+
async def call_tool(servers, tool_params):
23+
async with MultiServerMCPClient(servers) as client:
24+
print(tool_params)
25+
s = await client.sessions['composio_search'].call_tool(mcp_tool, tool_params)
26+
27+
return s
28+
29+
res = asyncio.run(call_tool(servers, tool_params))
30+
print(res)
31+
return NodeResult({'result': ''}, {})
32+
33+
def get_reference_content(self, fields: List[str]):
34+
return str(self.workflow_manage.get_reference_field(
35+
fields[0],
36+
fields[1:]))
37+
38+
def get_details(self, index: int, **kwargs):
39+
return {
40+
'name': self.node.properties.get('stepName'),
41+
"index": index,
42+
'run_time': self.context.get('run_time'),
43+
'question': self.context.get('question'),
44+
'result': self.context.get('result'),
45+
}

apps/application/serializers/application_serializers.py

+12-8
Original file line numberDiff line numberDiff line change
@@ -1319,11 +1319,15 @@ async def get_mcp_tools(servers):
13191319
async with MultiServerMCPClient(servers) as client:
13201320
return client.get_tools()
13211321

1322-
tools = asyncio.run(get_mcp_tools(servers))
1323-
return [
1324-
{
1325-
'name': tool.name,
1326-
'description': tool.description,
1327-
'args': tool.args,
1328-
}
1329-
for tool in tools]
1322+
tools = []
1323+
for server in servers:
1324+
tools += [
1325+
{
1326+
'server': server,
1327+
'name': tool.name,
1328+
'description': tool.description,
1329+
'args': tool.args,
1330+
}
1331+
for tool in asyncio.run(get_mcp_tools({server: servers[server]}))]
1332+
return tools
1333+

ui/src/locales/lang/en-US/views/application-workflow.ts

+3
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,9 @@ export default {
224224
label: 'MCP Server',
225225
text: 'Call MCP Tools',
226226
getToolsSuccess: 'Get Tools Successfully',
227+
getTool: 'Get Tools',
228+
tool: 'Tool',
229+
toolParam: 'Tool Params'
227230
},
228231
imageGenerateNode: {
229232
label: 'Image Generation',

ui/src/locales/lang/zh-CN/views/application-workflow.ts

+3
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,9 @@ export default {
224224
label: 'MCP 节点',
225225
text: '调用 MCP 工具',
226226
getToolsSuccess: '获取工具成功',
227+
getTool: '获取工具',
228+
tool: '工具',
229+
toolParam: '工具参数'
227230
},
228231
imageGenerateNode: {
229232
label: '图片生成',

ui/src/locales/lang/zh-Hant/views/application-workflow.ts

+3
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,9 @@ export default {
224224
label: 'MCP 節點',
225225
text: '呼叫 MCP 工具',
226226
getToolsSuccess: '獲取工具成功',
227+
getTool: '獲取工具',
228+
tool: '工具',
229+
toolParam: '工具變數'
227230
},
228231
imageGenerateNode: {
229232
label: '圖片生成',

ui/src/workflow/nodes/mcp-node/index.vue

+24-23
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,26 @@
1515
<MdEditorMagnify
1616
@wheel="wheel"
1717
title="MCP Server Config"
18-
v-model="form_data.mcpServers"
18+
v-model="form_data.mcp_servers"
1919
style="height: 150px"
2020
@submitDialog="submitDialog"
2121
/>
2222
</el-form-item>
2323
<el-form-item>
2424
<template v-slot:label>
2525
<div class="flex-between">
26-
<span>工具</span>
27-
<el-button type="primary" link @click="getTools()">获取工具</el-button>
26+
<span>{{ $t('views.applicationWorkflow.nodes.mcpNode.tool') }}</span>
27+
<el-button type="primary" link @click="getTools()">
28+
{{ $t('views.applicationWorkflow.nodes.mcpNode.getTool') }}
29+
</el-button>
2830
</div>
2931
</template>
3032
<el-select
31-
v-model="form_data.mcpTool"
32-
placeholder="请选择工具"
33+
v-model="form_data.mcp_tool"
3334
@change="changeTool"
3435
>
3536
<el-option
36-
v-for="item in form_data.mcpTools"
37+
v-for="item in form_data.mcp_tools"
3738
:key="item.value"
3839
:label="item.name"
3940
:value="item.name"
@@ -53,10 +54,11 @@
5354
</el-form-item>
5455
</el-form>
5556
</div>
56-
<h5 class="title-decoration-1 mb-8">工具参数</h5>
57+
<h5 class="title-decoration-1 mb-8">
58+
{{ $t('views.applicationWorkflow.nodes.mcpNode.toolParam') }}</h5>
5759
<div class="border-r-4 p-8-12 mb-8 layout-bg lighter">
5860
<DynamicsForm
59-
v-if="form_data.mcpTool"
61+
v-if="form_data.mcp_tool"
6062
v-model="form_data.tool_params"
6163
:model="form_data.tool_params"
6264
label-position="top"
@@ -93,26 +95,31 @@ const wheel = (e: any) => {
9395
}
9496
}
9597
const form = {
96-
mcpTool: '',
97-
mcpServers: '',
98+
mcp_tool: '',
99+
mcp_tools: [],
100+
mcp_servers: '',
101+
mcp_server: '',
98102
tool_params: {},
99103
tool_form_field: []
100104
}
101105
102106
103107
function submitDialog(val: string) {
104-
set(props.nodeModel.properties.node_data, 'mcpServers', val)
108+
set(props.nodeModel.properties.node_data, 'mcp_servers', val)
105109
}
106110
107111
function getTools() {
108-
applicationApi.getMcpTools({ mcp_servers: form_data.value.mcpServers }).then((res: any) => {
109-
form_data.value.mcpTools = res.data
112+
applicationApi.getMcpTools({ mcp_servers: form_data.value.mcp_servers }).then((res: any) => {
113+
form_data.value.mcp_tools = res.data
110114
MsgSuccess(t('views.applicationWorkflow.nodes.mcpNode.getToolsSuccess'))
111115
})
112116
}
113117
114118
function changeTool() {
115-
const params = form_data.value.mcpTools.filter((item: any) => item.name === form_data.value.mcpTool)[0].args.params
119+
// form_data.value.mcp_server = item.server
120+
// console.log(item.server)
121+
122+
const params = form_data.value.mcp_tools.filter((item: any) => item.name === form_data.value.mcp_tool)[0].args.params
116123
form_data.value.tool_form_field = []
117124
for (const item in params.properties) {
118125
form_data.value.tool_form_field.push({
@@ -124,13 +131,13 @@ function changeTool() {
124131
props_info: {}
125132
},
126133
input_type: 'TextInput',
127-
required: params.required.indexOf(item) !== -1,
134+
required: params.required?.indexOf(item) !== -1,
128135
default_value: '',
129136
show_default_value: false,
130137
props_info: {
131138
rules: [
132139
{
133-
required: params.required.indexOf(item) !== -1,
140+
required: params.required?.indexOf(item) !== -1,
134141
message: t('dynamicsForm.tip.requiredMessage'),
135142
trigger: 'blur'
136143
}
@@ -158,18 +165,12 @@ const form_data = computed({
158165
159166
160167
const replyNodeFormRef = ref()
161-
const nodeCascaderRef = ref()
162-
const nodeCascaderRef2 = ref()
163168
164169
const validate = async () => {
165-
// console.log(replyNodeFormRef.value.validate())
166170
let ps = [
167171
replyNodeFormRef.value?.validate(),
168-
...nodeCascaderRef.value.map((item: any) => item.validate())
172+
dynamicsFormRef.value?.validate()
169173
]
170-
if (nodeCascaderRef2.value) {
171-
ps = [...ps, ...nodeCascaderRef.value.map((item: any) => item.validate())]
172-
}
173174
return Promise.all(ps).catch((err: any) => {
174175
return Promise.reject({ node: props.nodeModel, errMessage: err })
175176
})

0 commit comments

Comments
 (0)