Skip to content

Commit 9ac8b1a

Browse files
RockChinQwangchamThetail001fdc310Alfonsxh
authored
feat: ui for mcp (#1600)
* feat: code by huntun * chore: revert group.py * refactor: api * feat: adjust ui * chore: stash * feat: add dialog * feat: add mcp from sse on frontend * feat: add mcp db * feat: semi frontend * feat: change sse frontend * fix: page out of control * fix: mcp card * fix: mcp refactor * fix: delete description * feat: add mcp servers * fix: status icon * feat: mcp-ui * perf: remove title from mcp mgm page * fix: delete mcp market * feat: add i18n * fix: run lint * feat: add i18n * fix: delete print function * fix: mcp test error * fix: i18n and mcp test * refactor(mcp): bridge controller and db operation with service layer * fix: try & catch & error * fix: error message in mcp card * feat: no longer register tool loader as component for type hints * perf: make startup async * feat: completely remove the fucking mcp market components and refs * refactor: mcp server datastructure * perf: tidy dir * feat: perf mcp server api datastruct * perf: ui * perf: mcp server status checking logic * perf: mcp server testing and refreshing * perf: no mcp server tips * perf: update sidebar title * chore: update * chore: bump langbot-plugin to 0.1.3 * chore: bump version v4.3.4 * chore: release v4.3.5 * Fix: Correct data type mismatch in AtBotRule (#1705) Fix can't '@' in QQ group. * chore: bump version 4.3.6 * feat: update for new events fields * Fix/qqo (#1709) * fix: qq official * fix: appid * chore: add `codecov.yml` * chore: bump langbot-plugin to 0.1.4b2 * chore: bump version 4.3.7b1 * fix: return empty data when plugin system disabled (#1710) * chore: bump version 4.3.7 * fix: bad Plain component init in wechatpad (#1712) * perf: allow not set llm model (#1703) * perf: output pipeline error in en * fix: datetime serialization error in emit_event (#1713) * chore: bump version 4.3.8 * perf: add component list in plugin detail dialog * perf: store pipeline sort method * Feat/coze runner (#1714) * feat:add coze api client and coze runner and coze config * del print * fix:Change the default setting of the plugin system to true * fix:del multimodal-support config, default multimodal-support,and in cozeapi.py Obtain timeout and auto-save-history config * chore: add comment for coze.com --------- Co-authored-by: Junyan Qin <rockchinq@gmail.com> * chore: bump version 4.3.9 * feat: 实现企业微信智能机器人流式响应 - 重构 WecomBotClient,支持流式会话管理和队列机制 - 新增 StreamSession 和 StreamSessionManager 类管理流式上下文 - 实现 reply_message_chunk 接口支持流式输出 - 优化消息处理流程,支持异步流式响应 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor: split WeCom callback handlers * fix: langchain error * fix: add langchain test splitter module * perf: config reset logic (#1742) * fix: inherit settings from existing settings * feat: add optional data cleanup checkbox to plugin uninstall dialog (#1743) * Initial plan * Add checkbox for plugin config/storage deletion - Add delete_data parameter to backend API endpoint - Update delete_plugin flow to clean up settings and binary storage - Add checkbox in uninstall dialog using shadcn/ui - Add translations for checkbox label in all languages Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * perf: param list --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> Co-authored-by: Junyan Qin <rockchinq@gmail.com> * chore: fix linter errors --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> --------- Co-authored-by: WangCham <651122857@qq.com> Co-authored-by: wangcham <wangcham233@gmail.com> Co-authored-by: Thetail001 <56257172+Thetail001@users.noreply.github.com> Co-authored-by: fdc310 <82008029+fdc310@users.noreply.github.com> Co-authored-by: Alfons <alfonsxh@gmail.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
2 parents da9afcd + f476c47 commit 9ac8b1a

File tree

35 files changed

+2270
-153
lines changed

35 files changed

+2270
-153
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,6 @@ test.py
4444
.venv/
4545
uv.lock
4646
/test
47+
plugins.bak
4748
coverage.xml
48-
.coverage
49+
.coverage

pkg/api/http/controller/groups/resources/__init__.py

Whitespace-only changes.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from __future__ import annotations
2+
3+
import quart
4+
import traceback
5+
6+
7+
from ... import group
8+
9+
10+
@group.group_class('mcp', '/api/v1/mcp')
11+
class MCPRouterGroup(group.RouterGroup):
12+
async def initialize(self) -> None:
13+
@self.route('/servers', methods=['GET', 'POST'], auth_type=group.AuthType.USER_TOKEN)
14+
async def _() -> str:
15+
"""获取MCP服务器列表"""
16+
if quart.request.method == 'GET':
17+
servers = await self.ap.mcp_service.get_mcp_servers(contain_runtime_info=True)
18+
19+
return self.success(data={'servers': servers})
20+
21+
elif quart.request.method == 'POST':
22+
data = await quart.request.json
23+
24+
try:
25+
uuid = await self.ap.mcp_service.create_mcp_server(data)
26+
return self.success(data={'uuid': uuid})
27+
except Exception as e:
28+
traceback.print_exc()
29+
return self.http_status(500, -1, f'Failed to create MCP server: {str(e)}')
30+
31+
@self.route('/servers/<server_name>', methods=['GET', 'PUT', 'DELETE'], auth_type=group.AuthType.USER_TOKEN)
32+
async def _(server_name: str) -> str:
33+
"""获取、更新或删除MCP服务器配置"""
34+
35+
server_data = await self.ap.mcp_service.get_mcp_server_by_name(server_name)
36+
if server_data is None:
37+
return self.http_status(404, -1, 'Server not found')
38+
39+
if quart.request.method == 'GET':
40+
return self.success(data={'server': server_data})
41+
42+
elif quart.request.method == 'PUT':
43+
data = await quart.request.json
44+
try:
45+
await self.ap.mcp_service.update_mcp_server(server_data['uuid'], data)
46+
return self.success()
47+
except Exception as e:
48+
return self.http_status(500, -1, f'Failed to update MCP server: {str(e)}')
49+
50+
elif quart.request.method == 'DELETE':
51+
try:
52+
await self.ap.mcp_service.delete_mcp_server(server_data['uuid'])
53+
return self.success()
54+
except Exception as e:
55+
return self.http_status(500, -1, f'Failed to delete MCP server: {str(e)}')
56+
57+
@self.route('/servers/<server_name>/test', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
58+
async def _(server_name: str) -> str:
59+
"""测试MCP服务器连接"""
60+
server_data = await quart.request.json
61+
task_id = await self.ap.mcp_service.test_mcp_server(server_name=server_name, server_data=server_data)
62+
return self.success(data={'task_id': task_id})

pkg/api/http/controller/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@
1515
from .groups import platform as groups_platform
1616
from .groups import pipelines as groups_pipelines
1717
from .groups import knowledge as groups_knowledge
18+
from .groups import resources as groups_resources
1819

1920
importutil.import_modules_in_pkg(groups)
2021
importutil.import_modules_in_pkg(groups_provider)
2122
importutil.import_modules_in_pkg(groups_platform)
2223
importutil.import_modules_in_pkg(groups_pipelines)
2324
importutil.import_modules_in_pkg(groups_knowledge)
25+
importutil.import_modules_in_pkg(groups_resources)
2426

2527

2628
class HTTPController:

pkg/api/http/service/mcp.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
from __future__ import annotations
2+
3+
import sqlalchemy
4+
import uuid
5+
import asyncio
6+
7+
from ....core import app
8+
from ....entity.persistence import mcp as persistence_mcp
9+
from ....core import taskmgr
10+
from ....provider.tools.loaders.mcp import RuntimeMCPSession, MCPSessionStatus
11+
12+
13+
class MCPService:
14+
ap: app.Application
15+
16+
def __init__(self, ap: app.Application) -> None:
17+
self.ap = ap
18+
19+
async def get_runtime_info(self, server_name: str) -> dict | None:
20+
session = self.ap.tool_mgr.mcp_tool_loader.get_session(server_name)
21+
if session:
22+
return session.get_runtime_info_dict()
23+
return None
24+
25+
async def get_mcp_servers(self, contain_runtime_info: bool = False) -> list[dict]:
26+
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_mcp.MCPServer))
27+
28+
servers = result.all()
29+
serialized_servers = [
30+
self.ap.persistence_mgr.serialize_model(persistence_mcp.MCPServer, server) for server in servers
31+
]
32+
if contain_runtime_info:
33+
for server in serialized_servers:
34+
runtime_info = await self.get_runtime_info(server['name'])
35+
36+
server['runtime_info'] = runtime_info if runtime_info else None
37+
38+
return serialized_servers
39+
40+
async def create_mcp_server(self, server_data: dict) -> str:
41+
server_data['uuid'] = str(uuid.uuid4())
42+
await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_mcp.MCPServer).values(server_data))
43+
44+
result = await self.ap.persistence_mgr.execute_async(
45+
sqlalchemy.select(persistence_mcp.MCPServer).where(persistence_mcp.MCPServer.uuid == server_data['uuid'])
46+
)
47+
server_entity = result.first()
48+
if server_entity:
49+
server_config = self.ap.persistence_mgr.serialize_model(persistence_mcp.MCPServer, server_entity)
50+
if self.ap.tool_mgr.mcp_tool_loader:
51+
task = asyncio.create_task(self.ap.tool_mgr.mcp_tool_loader.host_mcp_server(server_config))
52+
self.ap.tool_mgr.mcp_tool_loader._hosted_mcp_tasks.append(task)
53+
54+
return server_data['uuid']
55+
56+
async def get_mcp_server_by_name(self, server_name: str) -> dict | None:
57+
result = await self.ap.persistence_mgr.execute_async(
58+
sqlalchemy.select(persistence_mcp.MCPServer).where(persistence_mcp.MCPServer.name == server_name)
59+
)
60+
server = result.first()
61+
if server is None:
62+
return None
63+
64+
runtime_info = await self.get_runtime_info(server.name)
65+
server_data = self.ap.persistence_mgr.serialize_model(persistence_mcp.MCPServer, server)
66+
server_data['runtime_info'] = runtime_info if runtime_info else None
67+
return server_data
68+
69+
async def update_mcp_server(self, server_uuid: str, server_data: dict) -> None:
70+
result = await self.ap.persistence_mgr.execute_async(
71+
sqlalchemy.select(persistence_mcp.MCPServer).where(persistence_mcp.MCPServer.uuid == server_uuid)
72+
)
73+
old_server = result.first()
74+
old_server_name = old_server.name if old_server else None
75+
76+
await self.ap.persistence_mgr.execute_async(
77+
sqlalchemy.update(persistence_mcp.MCPServer)
78+
.where(persistence_mcp.MCPServer.uuid == server_uuid)
79+
.values(server_data)
80+
)
81+
82+
if self.ap.tool_mgr.mcp_tool_loader:
83+
if old_server_name and old_server_name in self.ap.tool_mgr.mcp_tool_loader.sessions:
84+
await self.ap.tool_mgr.mcp_tool_loader.remove_mcp_server(old_server_name)
85+
86+
result = await self.ap.persistence_mgr.execute_async(
87+
sqlalchemy.select(persistence_mcp.MCPServer).where(persistence_mcp.MCPServer.uuid == server_uuid)
88+
)
89+
updated_server = result.first()
90+
if updated_server:
91+
# convert entity to config dict
92+
server_config = self.ap.persistence_mgr.serialize_model(persistence_mcp.MCPServer, updated_server)
93+
task = asyncio.create_task(self.ap.tool_mgr.mcp_tool_loader.host_mcp_server(server_config))
94+
self.ap.tool_mgr.mcp_tool_loader._hosted_mcp_tasks.append(task)
95+
96+
async def delete_mcp_server(self, server_uuid: str) -> None:
97+
result = await self.ap.persistence_mgr.execute_async(
98+
sqlalchemy.select(persistence_mcp.MCPServer).where(persistence_mcp.MCPServer.uuid == server_uuid)
99+
)
100+
server = result.first()
101+
server_name = server.name if server else None
102+
103+
await self.ap.persistence_mgr.execute_async(
104+
sqlalchemy.delete(persistence_mcp.MCPServer).where(persistence_mcp.MCPServer.uuid == server_uuid)
105+
)
106+
107+
if server_name and self.ap.tool_mgr.mcp_tool_loader:
108+
if server_name in self.ap.tool_mgr.mcp_tool_loader.sessions:
109+
await self.ap.tool_mgr.mcp_tool_loader.remove_mcp_server(server_name)
110+
111+
async def test_mcp_server(self, server_name: str, server_data: dict) -> int:
112+
"""测试 MCP 服务器连接并返回任务 ID"""
113+
114+
runtime_mcp_session: RuntimeMCPSession | None = None
115+
116+
if server_name != '_':
117+
runtime_mcp_session = self.ap.tool_mgr.mcp_tool_loader.get_session(server_name)
118+
if runtime_mcp_session is None:
119+
raise ValueError(f'Server not found: {server_name}')
120+
121+
if runtime_mcp_session.status == MCPSessionStatus.ERROR:
122+
coroutine = runtime_mcp_session.start()
123+
else:
124+
coroutine = runtime_mcp_session.refresh()
125+
else:
126+
runtime_mcp_session = await self.ap.tool_mgr.mcp_tool_loader.load_mcp_server(server_config=server_data)
127+
coroutine = runtime_mcp_session.start()
128+
129+
ctx = taskmgr.TaskContext.new()
130+
wrapper = self.ap.task_mgr.create_user_task(
131+
coroutine,
132+
kind='mcp-operation',
133+
name=f'mcp-test-{server_name}',
134+
label=f'Testing MCP server {server_name}',
135+
context=ctx,
136+
)
137+
return wrapper.id

pkg/core/app.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from ..api.http.service import pipeline as pipeline_service
2323
from ..api.http.service import bot as bot_service
2424
from ..api.http.service import knowledge as knowledge_service
25+
from ..api.http.service import mcp as mcp_service
2526
from ..discover import engine as discover_engine
2627
from ..storage import mgr as storagemgr
2728
from ..utils import logcache
@@ -119,6 +120,8 @@ class Application:
119120

120121
knowledge_service: knowledge_service.KnowledgeService = None
121122

123+
mcp_service: mcp_service.MCPService = None
124+
122125
def __init__(self):
123126
pass
124127

pkg/core/stages/build_app.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from ...api.http.service import pipeline as pipeline_service
2020
from ...api.http.service import bot as bot_service
2121
from ...api.http.service import knowledge as knowledge_service
22+
from ...api.http.service import mcp as mcp_service
2223
from ...discover import engine as discover_engine
2324
from ...storage import mgr as storagemgr
2425
from ...utils import logcache
@@ -126,5 +127,8 @@ async def runtime_disconnect_callback(connector: plugin_connector.PluginRuntimeC
126127
knowledge_service_inst = knowledge_service.KnowledgeService(ap)
127128
ap.knowledge_service = knowledge_service_inst
128129

130+
mcp_service_inst = mcp_service.MCPService(ap)
131+
ap.mcp_service = mcp_service_inst
132+
129133
ctrl = controller.Controller(ap)
130134
ap.ctrl = ctrl

pkg/core/taskmgr.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ def to_dict(self) -> dict:
156156
'state': self.task._state,
157157
'exception': self.assume_exception().__str__() if self.assume_exception() is not None else None,
158158
'exception_traceback': exception_traceback,
159-
'result': self.assume_result().__str__() if self.assume_result() is not None else None,
159+
'result': self.assume_result() if self.assume_result() is not None else None,
160160
},
161161
}
162162

pkg/entity/persistence/mcp.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import sqlalchemy
2+
3+
from .base import Base
4+
5+
6+
class MCPServer(Base):
7+
__tablename__ = 'mcp_servers'
8+
9+
uuid = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True, unique=True)
10+
name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
11+
enable = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False, default=False)
12+
mode = sqlalchemy.Column(sqlalchemy.String(255), nullable=False) # stdio, sse
13+
extra_args = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default={})
14+
created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now())
15+
updated_at = sqlalchemy.Column(
16+
sqlalchemy.DateTime,
17+
nullable=False,
18+
server_default=sqlalchemy.func.now(),
19+
onupdate=sqlalchemy.func.now(),
20+
)

pkg/platform/sources/discord.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from ..logger import EventLogger
2323

2424

25+
2526
# 语音功能相关异常定义
2627
class VoiceConnectionError(Exception):
2728
"""语音连接基础异常"""

0 commit comments

Comments
 (0)