Skip to content

Commit f1356e9

Browse files
committed
feat: add MCP tool support with new form and dropdown options
1 parent c468952 commit f1356e9

File tree

22 files changed

+769
-5
lines changed

22 files changed

+769
-5
lines changed

apps/common/utils/tool_code.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# coding=utf-8
2-
2+
import ast
33
import os
44
import pickle
55
import subprocess
@@ -83,6 +83,39 @@ def exec_code(self, code_str, keywords):
8383
return result.get('data')
8484
raise Exception(result.get('msg'))
8585

86+
def generate_mcp_server_code(self, _code):
87+
self.validate_banned_keywords(_code)
88+
89+
# 解析代码,提取导入语句和函数定义
90+
try:
91+
tree = ast.parse(_code)
92+
except SyntaxError:
93+
return _code
94+
95+
imports = []
96+
functions = []
97+
other_code = []
98+
99+
for node in tree.body:
100+
if isinstance(node, ast.Import) or isinstance(node, ast.ImportFrom):
101+
imports.append(ast.unparse(node))
102+
elif isinstance(node, ast.FunctionDef):
103+
# 为函数添加 @mcp.tool() 装饰器
104+
func_code = ast.unparse(node)
105+
functions.append(f"@mcp.tool()\n{func_code}\n")
106+
else:
107+
other_code.append(ast.unparse(node))
108+
109+
# 构建完整的 MCP 服务器代码
110+
code_parts = ["from mcp.server.fastmcp import FastMCP"]
111+
code_parts.extend(imports)
112+
code_parts.append(f"\nmcp = FastMCP(\"{uuid.uuid7()}\")\n")
113+
code_parts.extend(other_code)
114+
code_parts.extend(functions)
115+
code_parts.append("\nmcp.run(transport=\"stdio\")\n")
116+
117+
return "\n".join(code_parts)
118+
86119
def _exec_sandbox(self, _code, _id):
87120
exec_python_file = f'{self.sandbox_path}/execute/{_id}.py'
88121
with open(exec_python_file, 'w') as file:
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.2.4 on 2025-08-11 09:45
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('knowledge', '0001_initial'),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name='file',
15+
name='source_type',
16+
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='资源类型'),
17+
),
18+
]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.2.4 on 2025-08-11 09:37
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('tools', '0001_initial'),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name='tool',
15+
name='tool_type',
16+
field=models.CharField(choices=[('INTERNAL', '内置'), ('CUSTOM', '自定义'), ('MCP', 'MCP工具')], db_index=True, default='CUSTOM', max_length=20, verbose_name='工具类型'),
17+
),
18+
]

apps/tools/models/tool.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class ToolScope(models.TextChoices):
3131
class ToolType(models.TextChoices):
3232
INTERNAL = "INTERNAL", '内置'
3333
CUSTOM = "CUSTOM", "自定义"
34+
MCP = "MCP", "MCP工具"
3435

3536

3637
class Tool(AppModelMixin):

apps/tools/serializers/tool.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
# -*- coding: utf-8 -*-
2+
import asyncio
23
import io
34
import json
45
import os
56
import pickle
67
import re
8+
from typing import Dict
79

810
import uuid_utils.compat as uuid
911
from django.core import validators
@@ -12,6 +14,7 @@
1214
from django.http import HttpResponse
1315
from django.utils import timezone
1416
from django.utils.translation import gettext_lazy as _
17+
from langchain_mcp_adapters.client import MultiServerMCPClient
1518
from pylint.lint import Run
1619
from pylint.reporters import JSON2Reporter
1720
from rest_framework import serializers, status
@@ -22,6 +25,7 @@
2225
from common.field.common import UploadedImageField
2326
from common.result import result
2427
from common.utils.common import get_file_content
28+
from common.utils.logger import maxkb_logger
2529
from common.utils.rsa_util import rsa_long_decrypt, rsa_long_encrypt
2630
from common.utils.tool_code import ToolExecutor
2731
from knowledge.models import File, FileSourceType
@@ -103,6 +107,18 @@ def encryption(message: str):
103107
return pre_str + content + end_str
104108

105109

110+
def validate_mcp_config(servers: Dict):
111+
async def validate():
112+
client = MultiServerMCPClient(servers)
113+
await client.get_tools()
114+
115+
try:
116+
asyncio.run(validate())
117+
except Exception as e:
118+
maxkb_logger.error(f"validate mcp config error: {e}, servers: {servers}")
119+
raise serializers.ValidationError(_('MCP configuration is invalid'))
120+
121+
106122
class ToolModelSerializer(serializers.ModelSerializer):
107123
class Meta:
108124
model = Tool
@@ -201,6 +217,131 @@ class PylintInstance(serializers.Serializer):
201217

202218

203219
class ToolSerializer(serializers.Serializer):
220+
class Query(serializers.Serializer):
221+
workspace_id = serializers.CharField(required=True, label=_('workspace id'))
222+
folder_id = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_('folder id'))
223+
name = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_('tool name'))
224+
user_id = serializers.UUIDField(required=False, allow_null=True, label=_('user id'))
225+
scope = serializers.CharField(required=True, label=_('scope'))
226+
tool_type = serializers.CharField(required=False, label=_('tool type'), allow_null=True, allow_blank=True)
227+
create_user = serializers.UUIDField(required=False, label=_('create user'), allow_null=True)
228+
229+
def get_query_set(self, workspace_manage, is_x_pack_ee):
230+
tool_query_set = QuerySet(Tool).filter(workspace_id=self.data.get('workspace_id'))
231+
folder_query_set = QuerySet(ToolFolder)
232+
default_query_set = QuerySet(Tool)
233+
234+
workspace_id = self.data.get('workspace_id')
235+
user_id = self.data.get('user_id')
236+
scope = self.data.get('scope')
237+
tool_type = self.data.get('tool_type')
238+
desc = self.data.get('desc')
239+
name = self.data.get('name')
240+
folder_id = self.data.get('folder_id')
241+
create_user = self.data.get('create_user')
242+
243+
if workspace_id is not None:
244+
folder_query_set = folder_query_set.filter(workspace_id=workspace_id)
245+
default_query_set = default_query_set.filter(workspace_id=workspace_id)
246+
if folder_id is not None:
247+
folder_query_set = folder_query_set.filter(parent=folder_id)
248+
default_query_set = default_query_set.filter(folder_id=folder_id)
249+
if name is not None:
250+
folder_query_set = folder_query_set.filter(name__icontains=name)
251+
default_query_set = default_query_set.filter(name__icontains=name)
252+
if desc is not None:
253+
folder_query_set = folder_query_set.filter(desc__icontains=desc)
254+
default_query_set = default_query_set.filter(desc__icontains=desc)
255+
if create_user is not None:
256+
tool_query_set = tool_query_set.filter(user_id=create_user)
257+
folder_query_set = folder_query_set.filter(user_id=create_user)
258+
259+
default_query_set = default_query_set.order_by("-create_time")
260+
261+
if scope is not None:
262+
tool_query_set = tool_query_set.filter(scope=scope)
263+
if tool_type:
264+
tool_query_set = tool_query_set.filter(tool_type=tool_type)
265+
266+
query_set_dict = {
267+
'folder_query_set': folder_query_set,
268+
'tool_query_set': tool_query_set,
269+
'default_query_set': default_query_set,
270+
}
271+
if not workspace_manage:
272+
query_set_dict['workspace_user_resource_permission_query_set'] = QuerySet(
273+
WorkspaceUserResourcePermission).filter(
274+
auth_target_type="TOOL",
275+
workspace_id=workspace_id,
276+
user_id=user_id
277+
)
278+
return query_set_dict
279+
280+
def get_authorized_query_set(self):
281+
default_query_set = QuerySet(Tool)
282+
tool_type = self.data.get('tool_type')
283+
desc = self.data.get('desc')
284+
name = self.data.get('name')
285+
create_user = self.data.get('create_user')
286+
287+
default_query_set = default_query_set.filter(workspace_id='None')
288+
default_query_set = default_query_set.filter(scope=ToolScope.SHARED)
289+
if name is not None:
290+
default_query_set = default_query_set.filter(name__icontains=name)
291+
if desc is not None:
292+
default_query_set = default_query_set.filter(desc__icontains=desc)
293+
if create_user is not None:
294+
default_query_set = default_query_set.filter(user_id=create_user)
295+
if tool_type:
296+
default_query_set = default_query_set.filter(tool_type=tool_type)
297+
298+
default_query_set = default_query_set.order_by("-create_time")
299+
300+
return default_query_set
301+
302+
@staticmethod
303+
def is_x_pack_ee():
304+
workspace_user_role_mapping_model = DatabaseModelManage.get_model("workspace_user_role_mapping")
305+
role_permission_mapping_model = DatabaseModelManage.get_model("role_permission_mapping_model")
306+
return workspace_user_role_mapping_model is not None and role_permission_mapping_model is not None
307+
308+
def get_tools(self):
309+
self.is_valid(raise_exception=True)
310+
311+
workspace_manage = is_workspace_manage(self.data.get('user_id'), self.data.get('workspace_id'))
312+
is_x_pack_ee = self.is_x_pack_ee()
313+
results = native_search(
314+
self.get_query_set(workspace_manage, is_x_pack_ee),
315+
get_file_content(
316+
os.path.join(
317+
PROJECT_DIR,
318+
"apps", "tools", 'sql',
319+
'list_tool.sql' if workspace_manage else (
320+
'list_tool_user_ee.sql' if is_x_pack_ee else 'list_tool_user.sql'
321+
)
322+
)
323+
),
324+
)
325+
326+
get_authorized_tool = DatabaseModelManage.get_model("get_authorized_tool")
327+
shared_queryset = QuerySet(Tool).none()
328+
if get_authorized_tool is not None:
329+
shared_queryset = self.get_authorized_query_set()
330+
shared_queryset = get_authorized_tool(shared_queryset, self.data.get('workspace_id'))
331+
332+
return {
333+
'shared_tools': [
334+
ToolModelSerializer(data).data for data in shared_queryset
335+
],
336+
'tools': [
337+
{
338+
**tool,
339+
'input_field_list': json.loads(tool.get('input_field_list', '[]')),
340+
'init_field_list': json.loads(tool.get('init_field_list', '[]')),
341+
} for tool in results if tool['resource_type'] == 'tool'
342+
],
343+
}
344+
204345
class Create(serializers.Serializer):
205346
user_id = serializers.UUIDField(required=True, label=_('user id'))
206347
workspace_id = serializers.CharField(required=True, label=_('workspace id'))
@@ -212,6 +353,10 @@ def insert(self, instance, with_valid=True):
212353
ToolCreateRequest(data=instance).is_valid(raise_exception=True)
213354
# 校验代码是否包括禁止的关键字
214355
ToolExecutor().validate_banned_keywords(instance.get('code', ''))
356+
# 校验mcp json
357+
if instance.get('tool_type') == ToolType.MCP.value:
358+
validate_mcp_config(json.loads(instance.get('code')))
359+
215360
tool_id = uuid.uuid7()
216361
Tool(
217362
id=tool_id,
@@ -223,6 +368,7 @@ def insert(self, instance, with_valid=True):
223368
input_field_list=instance.get('input_field_list', []),
224369
init_field_list=instance.get('init_field_list', []),
225370
scope=instance.get('scope', ToolScope.WORKSPACE),
371+
tool_type=instance.get('tool_type', ToolType.CUSTOM),
226372
folder_id=instance.get('folder_id', self.data.get('workspace_id')),
227373
is_active=False
228374
).save()
@@ -326,6 +472,10 @@ def edit(self, instance, with_valid=True):
326472
ToolEditRequest(data=instance).is_valid(raise_exception=True)
327473
# 校验代码是否包括禁止的关键字
328474
ToolExecutor().validate_banned_keywords(instance.get('code', ''))
475+
# 校验mcp json
476+
if instance.get('tool_type') == ToolType.MCP.value:
477+
validate_mcp_config(json.loads(instance.get('code')))
478+
329479
if not QuerySet(Tool).filter(id=self.data.get('id')).exists():
330480
raise serializers.ValidationError(_('Tool not found'))
331481

@@ -574,6 +724,7 @@ class Query(serializers.Serializer):
574724
name = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_('tool name'))
575725
user_id = serializers.UUIDField(required=False, allow_null=True, label=_('user id'))
576726
scope = serializers.CharField(required=True, label=_('scope'))
727+
tool_type = serializers.CharField(required=False, label=_('tool type'), allow_null=True, allow_blank=True)
577728
create_user = serializers.UUIDField(required=False, label=_('create user'), allow_null=True)
578729

579730
def page_tool(self, current_page: int, page_size: int):
@@ -609,6 +760,7 @@ def get_query_set(self, workspace_manage, is_x_pack_ee):
609760
workspace_id = self.data.get('workspace_id')
610761
user_id = self.data.get('user_id')
611762
scope = self.data.get('scope')
763+
tool_type = self.data.get('tool_type')
612764
desc = self.data.get('desc')
613765
name = self.data.get('name')
614766
folder_id = self.data.get('folder_id')
@@ -634,6 +786,8 @@ def get_query_set(self, workspace_manage, is_x_pack_ee):
634786

635787
if scope is not None:
636788
tool_query_set = tool_query_set.filter(scope=scope)
789+
if tool_type:
790+
tool_query_set = tool_query_set.filter(tool_type=tool_type)
637791

638792
query_set_dict = {
639793
'folder_query_set': folder_query_set,

apps/tools/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
path('workspace/<str:workspace_id>/tool/import', views.ToolView.Import.as_view()),
1111
path('workspace/<str:workspace_id>/tool/pylint', views.ToolView.Pylint.as_view()),
1212
path('workspace/<str:workspace_id>/tool/debug', views.ToolView.Debug.as_view()),
13+
path('workspace/<str:workspace_id>/tool/tool_list', views.ToolView.Query.as_view()),
1314
path('workspace/<str:workspace_id>/tool/<str:tool_id>', views.ToolView.Operate.as_view()),
1415
path('workspace/<str:workspace_id>/tool/<str:tool_id>/edit_icon', views.ToolView.EditIcon.as_view()),
1516
path('workspace/<str:workspace_id>/tool/<str:tool_id>/export', views.ToolView.Export.as_view()),

apps/tools/views/tool.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ def get(self, request: Request, workspace_id: str):
7373
'folder_id': request.query_params.get('folder_id'),
7474
'name': request.query_params.get('name'),
7575
'scope': request.query_params.get('scope', ToolScope.WORKSPACE),
76+
'tool_type': request.query_params.get('tool_type'),
7677
'user_id': request.user.id,
7778
'create_user': request.query_params.get('create_user'),
7879
}
@@ -209,11 +210,43 @@ def get(self, request: Request, workspace_id: str, current_page: int, page_size:
209210
'folder_id': request.query_params.get('folder_id'),
210211
'name': request.query_params.get('name'),
211212
'scope': request.query_params.get('scope'),
213+
'tool_type': request.query_params.get('tool_type'),
212214
'user_id': request.user.id,
213215
'create_user': request.query_params.get('create_user'),
214216
}
215217
).page_tool_with_folders(current_page, page_size))
216218

219+
class Query(APIView):
220+
authentication_classes = [TokenAuth]
221+
222+
@extend_schema(
223+
methods=['GET'],
224+
description=_('Get tool list '),
225+
summary=_('Get tool list'),
226+
operation_id=_('Get tool list'), # type: ignore
227+
parameters=ToolReadAPI.get_parameters(),
228+
responses=ToolReadAPI.get_response(),
229+
tags=[_('Tool')] # type: ignore
230+
)
231+
@has_permissions(
232+
PermissionConstants.TOOL_READ.get_workspace_permission(),
233+
PermissionConstants.TOOL_READ.get_workspace_permission_workspace_manage_role(),
234+
RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), RoleConstants.USER.get_workspace_role()
235+
)
236+
@log(menu='Tool', operate='Get tool list')
237+
def get(self, request: Request, workspace_id: str):
238+
return result.success(ToolSerializer.Query(
239+
data={
240+
'workspace_id': workspace_id,
241+
'folder_id': request.query_params.get('folder_id'),
242+
'name': request.query_params.get('name'),
243+
'scope': request.query_params.get('scope'),
244+
'tool_type': request.query_params.get('tool_type'),
245+
'user_id': request.user.id,
246+
'create_user': request.query_params.get('create_user'),
247+
}
248+
).get_tools())
249+
217250
class Import(APIView):
218251
authentication_classes = [TokenAuth]
219252
parser_classes = [MultiPartParser]

0 commit comments

Comments
 (0)