From 79ff9b14b85d23da5b23894d9603861869b2e5e0 Mon Sep 17 00:00:00 2001 From: Wenjie Zhang Date: Wed, 3 Jun 2026 22:12:13 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=A7=BB=E9=99=A4=E9=80=80=E5=BD=B9?= =?UTF-8?q?=E5=86=85=E7=BD=AE=20MCP=20=E9=BB=98=E8=AE=A4=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/package/yuxi/agents/mcp/service.py | 20 +++-- backend/test/unit/routers/test_mcp_router.py | 6 +- .../test/unit/services/test_mcp_service.py | 86 ++++++++++++++++++- docs/agents/mcp-integration.md | 4 +- docs/develop-guides/roadmap.md | 1 + 5 files changed, 104 insertions(+), 13 deletions(-) diff --git a/backend/package/yuxi/agents/mcp/service.py b/backend/package/yuxi/agents/mcp/service.py index 82f2fa52..65fd0c88 100644 --- a/backend/package/yuxi/agents/mcp/service.py +++ b/backend/package/yuxi/agents/mcp/service.py @@ -38,13 +38,6 @@ _UNSET = object() # Default MCP Server configurations (Imported to DB on first run) _DEFAULT_MCP_SERVERS = { - "sequentialthinking": { - "url": "https://remote.mcpservers.org/sequentialthinking/mcp", - "transport": "streamable_http", - "description": "顺序思考工具,帮助 AI 将复杂问题分解为多个步骤", - "icon": "🧠", - "tags": ["内置", "AI"], - }, "mcp-server-chart": { "command": "npx", "args": ["-y", "@antv/mcp-server-chart"], @@ -55,6 +48,8 @@ _DEFAULT_MCP_SERVERS = { }, } +_RETIRED_BUILTIN_MCP_SERVER_SLUGS = ("sequentialthinking",) + _SYNCED_MCP_FIELDS = ( "description", "transport", @@ -81,6 +76,17 @@ async def ensure_builtin_mcp_servers_in_db() -> None: try: async with pg_manager.get_async_session_context() as session: any_changed = False + for slug in _RETIRED_BUILTIN_MCP_SERVER_SLUGS: + result = await session.execute( + select(MCPServer).filter(MCPServer.slug == slug, MCPServer.created_by == "system") + ) + retired = result.scalar_one_or_none() + if retired: + await session.delete(retired) + clear_mcp_server_tools_cache(slug) + any_changed = True + logger.info(f"Removed retired built-in MCP server '{slug}' from database") + for slug, config in _DEFAULT_MCP_SERVERS.items(): result = await session.execute(select(MCPServer).filter(MCPServer.slug == slug)) existing = result.scalar_one_or_none() diff --git a/backend/test/unit/routers/test_mcp_router.py b/backend/test/unit/routers/test_mcp_router.py index b9704035..26078ecb 100644 --- a/backend/test/unit/routers/test_mcp_router.py +++ b/backend/test/unit/routers/test_mcp_router.py @@ -49,7 +49,7 @@ def test_update_mcp_server_status(monkeypatch): self.enabled = enabled def to_dict(self): - return {"name": "sequentialthinking", "enabled": self.enabled} + return {"name": "demo-mcp", "enabled": self.enabled} async def fake_set_server_enabled(db, name, enabled, updated_by=None): captured["name"] = name @@ -60,13 +60,13 @@ def test_update_mcp_server_status(monkeypatch): monkeypatch.setattr("server.routers.mcp_router.set_server_enabled", fake_set_server_enabled) client = TestClient(_build_app()) - resp = client.put("/api/system/mcp-servers/sequentialthinking/status", json={"enabled": False}) + resp = client.put("/api/system/mcp-servers/demo-mcp/status", json={"enabled": False}) assert resp.status_code == 200, resp.text payload = resp.json() assert payload["success"] is True assert payload["enabled"] is False assert payload["data"]["enabled"] is False - assert captured == {"name": "sequentialthinking", "enabled": False, "updated_by": "admin"} + assert captured == {"name": "demo-mcp", "enabled": False, "updated_by": "admin"} def test_update_mcp_server_status_not_found(monkeypatch): diff --git a/backend/test/unit/services/test_mcp_service.py b/backend/test/unit/services/test_mcp_service.py index 6482725f..8d454ee1 100644 --- a/backend/test/unit/services/test_mcp_service.py +++ b/backend/test/unit/services/test_mcp_service.py @@ -2,7 +2,37 @@ from __future__ import annotations from types import SimpleNamespace +import pytest_asyncio +from sqlalchemy import select +from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine + from yuxi.agents.mcp import service as mcp_service +from yuxi.storage.postgres import manager as postgres_manager +from yuxi.storage.postgres.models_business import MCPServer + + +class _AsyncSessionContext: + def __init__(self, db): + self.db = db + + async def __aenter__(self): + return self.db + + async def __aexit__(self, *_args): + return False + + +@pytest_asyncio.fixture +async def mcp_session(): + engine = create_async_engine("sqlite+aiosqlite:///:memory:") + async with engine.begin() as conn: + await conn.run_sync(MCPServer.__table__.create) + + session_factory = async_sessionmaker(engine, expire_on_commit=False) + async with session_factory() as session: + yield session + + await engine.dispose() class _FakeClient: @@ -13,6 +43,61 @@ class _FakeClient: return self._tools +async def test_ensure_builtin_mcp_servers_removes_retired_system_server(monkeypatch, mcp_session): + retired_server = MCPServer( + slug="sequentialthinking", + name="sequentialthinking", + description="old builtin", + transport="streamable_http", + url="https://remote.mcpservers.org/sequentialthinking/mcp", + enabled=1, + created_by="system", + updated_by="system", + ) + mcp_session.add(retired_server) + await mcp_session.commit() + + monkeypatch.setattr( + postgres_manager.pg_manager, + "get_async_session_context", + lambda: _AsyncSessionContext(mcp_session), + ) + + await mcp_service.ensure_builtin_mcp_servers_in_db() + + retired = await mcp_session.scalar(select(MCPServer).where(MCPServer.slug == "sequentialthinking")) + chart = await mcp_session.scalar(select(MCPServer).where(MCPServer.slug == "mcp-server-chart")) + assert retired is None + assert chart is not None + + +async def test_ensure_builtin_mcp_servers_preserves_user_server_with_retired_slug(monkeypatch, mcp_session): + user_server = MCPServer( + slug="sequentialthinking", + name="用户自定义 MCP", + description="user managed", + transport="streamable_http", + url="https://example.com/mcp", + enabled=1, + created_by="admin", + updated_by="admin", + ) + mcp_session.add(user_server) + await mcp_session.commit() + + monkeypatch.setattr( + postgres_manager.pg_manager, + "get_async_session_context", + lambda: _AsyncSessionContext(mcp_session), + ) + + await mcp_service.ensure_builtin_mcp_servers_in_db() + + server = await mcp_session.scalar(select(MCPServer).where(MCPServer.slug == "sequentialthinking")) + assert server is not None + assert server.created_by == "admin" + + async def test_get_enabled_mcp_tools_loads_latest_config_from_db(monkeypatch): captured: list[dict] = [] @@ -135,4 +220,3 @@ async def test_get_mcp_tools_sets_handle_tool_error(monkeypatch): assert tools[0].handle_tool_error is True mcp_service.clear_mcp_cache() - diff --git a/docs/agents/mcp-integration.md b/docs/agents/mcp-integration.md index 6f68cf2d..08b9f529 100644 --- a/docs/agents/mcp-integration.md +++ b/docs/agents/mcp-integration.md @@ -18,9 +18,9 @@ MCP(Model Context Protocol)是扩展智能体能力的重要方式。系统 ```json { - "name": "sequentialthinking", + "name": "custom-remote-mcp", "transport": "streamable_http", - "url": "https://remote.mcpservers.org/sequentialthinking/mcp" + "url": "https://example.com/mcp" } ``` diff --git a/docs/develop-guides/roadmap.md b/docs/develop-guides/roadmap.md index ded6f993..1e8e51bd 100644 --- a/docs/develop-guides/roadmap.md +++ b/docs/develop-guides/roadmap.md @@ -38,6 +38,7 @@ ### 0.7.0 开发记录 +- 调整内置 MCP 默认项:移除 `sequentialthinking` 的系统内置同步,启动同步时清理历史系统内置记录,保留用户手动创建的同名 MCP。 - 图片生成能力迁移为 Skill:Qwen-Image 从内置 Python 生成工具迁移到内置 Skill `image-gen`,模型调用与图片下载在 Agent 沙盒中完成,生成结果保存到 outputs 并通过 `present_artifacts` 展示,为多图片生成模型接入复用同一产物展示链路。 - 降低知识库路由与工具模块复杂度:示例问题生成迁移到知识库 utils,文件上传统一 100 MB 限制,URL 预处理入库路径与旧 `content_type=url` 行为收敛,并修复 uid、导出 MIME 与异常透传等路由问题。 - 重构智能体配置语义:用户可见的 `AgentConfig` 收敛为数据库持久化的一级 `Agent`,内置 Python Agent 改为智能体后端;新增 `/api/agent` 管理与运行接口,聊天、运行任务、恢复审批和文件预览均从线程绑定的 Agent 解析运行时上下文,前端只提交 `agent_id`,并在模型配置页新增“智能体”管理页签。