Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat(mcp): 重构 MCP 服务器及智能助手模块
- 新增代码生成模块路由支持
- 重构 MCP 服务器API,支持增删改查接口和分页查询
- 实现 MCP 智能对话的流式响应和 WebSocket 通信
- 优化 MCP 数据模型,支持环境变量等配置项
- 改进 MCP CRUD 层,集成权限控制和统一操作方法
- 优化异常处理,提升接口错误信息准确性
- 修正依赖导入和数据校验,增强代码健壮性
  • Loading branch information
zhangtao
zhangtao committed Sep 21, 2025
commit 9feb08a962cb5ee11e08f74a648e3d103768fa80
6 changes: 5 additions & 1 deletion backend/app/api/v1/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
# AI模块
from .module_ai import AIRouter

# 代码生成模块
from .module_generator import GeneratorRouter


# 创建主路由
router = APIRouter()
Expand All @@ -34,4 +37,5 @@
router.include_router(ResourceRouter)
router.include_router(ExampleRouter)
router.include_router(ApplicationRouter)
router.include_router(AIRouter)
router.include_router(AIRouter)
router.include_router(GeneratorRouter)
141 changes: 70 additions & 71 deletions backend/app/api/v1/module_ai/mcp/controller.py
Original file line number Diff line number Diff line change
@@ -1,57 +1,101 @@
# -*- coding: utf-8 -*-

from typing import Annotated

from fastapi import APIRouter, Depends, Path, Query
from starlette.responses import StreamingResponse

from backend.common.pagination import DependsPagination, PageData, paging_data
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.database.db import CurrentSession
from .schema import CreateMcpParam, GetMcpDetail, McpChatParam, UpdateMcpParam
from .service import mcp_service

from fastapi import APIRouter, Depends, WebSocket
from fastapi import APIRouter, Depends, Path, Query, Body, WebSocket
from fastapi.responses import JSONResponse, StreamingResponse

from app.common.response import StreamResponse, SuccessResponse
from app.common.request import PaginationService
from app.core.base_params import PaginationQueryParam
from app.core.dependencies import AuthPermission
from app.core.router_class import OperationLogRoute
from app.core.logger import logger
from app.api.v1.module_system.auth.schema import AuthSchema
from .service import MCPService
from .schema import ChatQuerySchema
from .param import McpQueryParam
from .service import McpService
from .schema import McpCreateSchema, McpUpdateSchema, ChatQuerySchema


MCPRouter = APIRouter(route_class=OperationLogRoute, prefix="", tags=["MCP智能助手"])
MCPRouter = APIRouter(route_class=OperationLogRoute, prefix="/mcp", tags=["MCP智能助手"])


@MCPRouter.post("/mcp/chat", summary="智能对话", description="与MCP智能助手进行对话")
@MCPRouter.post("/chat", summary="智能对话", description="与MCP智能助手进行对话")
async def chat_controller(
query: ChatQuerySchema,
auth: AuthSchema = Depends(AuthPermission(permissions=["ai:mcp:chat"]))
) -> StreamingResponse:
"""智能对话接口"""
logger.info(f"用户 {auth.user.name} 发起智能对话: {query.message[:50]}...")
user_name = auth.user.name if auth.user else "未知用户"
logger.info(f"用户 {user_name} 发起智能对话: {query.message[:50]}...")

async def generate_response():
try:
async for chunk in MCPService.chat_query(query.message):
async for chunk in McpService.chat_query(query=query):
# 确保返回的是字节串
if chunk:
yield chunk.encode('utf-8') if isinstance(chunk, str) else chunk
except Exception as e:
logger.error(f"流式响应出错: {str(e)}")
yield f"抱歉,处理您的请求时出现了错误: {str(e)}".encode('utf-8')

return StreamingResponse(generate_response(), media_type="text/plain; charset=utf-8")


@MCPRouter.websocket("/ws/mcp/chat", name="WebSocket聊天")
return StreamResponse(generate_response(), media_type="text/plain; charset=utf-8")


@MCPRouter.get("/detail/{id}", summary="获取 MCP 服务器详情", description="获取 MCP 服务器详情")
async def get_mcp_detail_controller(
id: int = Path(..., description="MCP ID"),
auth: AuthSchema = Depends(AuthPermission(permissions=["ai:mcp:query"]))
) -> JSONResponse:
result_dict = await McpService.get_mcp_detail_service(auth=auth, id=id)
logger.info(f"获取 MCP 服务器详情成功 {id}")
return SuccessResponse(data=result_dict, msg="获取 MCP 服务器详情成功")


@MCPRouter.get("/list", summary="查询 MCP 服务器列表", description="查询 MCP 服务器列表")
async def get_mcp_list_controller(
page: PaginationQueryParam = Depends(),
search: McpQueryParam = Depends(),
auth: AuthSchema = Depends(AuthPermission(permissions=["ai:mcp:query"]))
) -> JSONResponse:
result_dict_list = await McpService.get_mcp_list_service(auth=auth, search=search, order_by=page.order_by)
result_dict = await PaginationService.paginate(data_list=result_dict_list, page_no=page.page_no, page_size=page.page_size)
logger.info(f"查询 MCP 服务器列表成功")
return SuccessResponse(data=result_dict, msg="查询 MCP 服务器列表成功")


@MCPRouter.post("/create", summary="创建 MCP 服务器", description="创建 MCP 服务器")
async def create_mcp_controller(
data: McpCreateSchema,
auth: AuthSchema = Depends(AuthPermission(permissions=["ai:mcp:create"]))
) -> JSONResponse:
result_dict = await McpService.create_mcp_service(auth=auth, data=data)
logger.info(f"创建 MCP 服务器成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="创建 MCP 服务器成功")


@MCPRouter.put("/update/{id}", summary="修改 MCP 服务器", description="修改 MCP 服务器")
async def update_mcp_controller(
data: McpUpdateSchema,
id: int = Path(..., description="MCP ID"),
auth: AuthSchema = Depends(AuthPermission(permissions=["ai:mcp:update"]))
) -> JSONResponse:
result_dict = await McpService.update_mcp_service(auth=auth, id=id, data=data)
logger.info(f"修改 MCP 服务器成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="修改 MCP 服务器成功")


@MCPRouter.delete("/delete", summary="删除 MCP 服务器", description="删除 MCP 服务器")
async def delete_mcp_controller(
ids: list[int] = Body(..., description="ID列表"),
auth: AuthSchema = Depends(AuthPermission(permissions=["ai:mcp:delete"]))
) -> JSONResponse:
await McpService.delete_mcp_service(auth=auth, ids=ids)
logger.info(f"删除 MCP 服务器成功: {ids}")
return SuccessResponse(msg="删除 MCP 服务器成功")


@MCPRouter.websocket("/ws/chat", name="WebSocket聊天")
async def websocket_chat_controller(
query: ChatQuerySchema,
websocket: WebSocket,
):
"""WebSocket聊天接口
Expand All @@ -64,7 +108,7 @@ async def websocket_chat_controller(
data = await websocket.receive_text()
# 流式发送响应
try:
async for chunk in MCPService.chat_query(data):
async for chunk in McpService.chat_query(query=query):
if chunk:
await websocket.send_text(chunk)
except Exception as e:
Expand All @@ -74,48 +118,3 @@ async def websocket_chat_controller(
logger.error(f"WebSocket聊天出错: {str(e)}")
finally:
await websocket.close()


@MCPRouter.get('/{pk}', summary='获取 MCP 服务器详情', dependencies=[DependsJwtAuth])
async def get_mcp(pk: Annotated[int, Path(description='MCP ID')]) -> ResponseSchemaModel[GetMcpDetail]:
mcp = await mcp_service.get(pk=pk)
return response_base.success(data=mcp)


@MCPRouter.get('', summary='分页获取所有 MCP 服务器', dependencies=[DependsJwtAuth, DependsPagination])
async def get_pagination_mcps(
db: CurrentSession,
name: Annotated[str | None, Query(description='MCP 名称')] = None,
type: Annotated[int | None, Query(description='MCP 类型')] = None,
) -> ResponseSchemaModel[PageData[GetMcpDetail]]:
mcp_select = await mcp_service.get_select(name=name, type=type)
page_data = await paging_data(db, mcp_select)
return response_base.success(data=page_data)


@MCPRouter.post('', summary='创建 MCP 服务器', dependencies=[Depends(RequestPermission('sys:mcp:add')), DependsRBAC])
async def create_mcp(obj: CreateMcpParam) -> ResponseModel:
await mcp_service.create(obj=obj)
return response_base.success()


@MCPRouter.put('/{pk}', summary='更新 MCP 服务器', dependencies=[ Depends(RequestPermission('sys:mcp:edit')), DependsRBAC])
async def update_mcp(pk: Annotated[int, Path(description='MCP ID')], obj: UpdateMcpParam) -> ResponseModel:
count = await mcp_service.update(pk=pk, obj=obj)
if count > 0:
return response_base.success()
return response_base.fail()


@MCPRouter.delete('/{pk}',summary='删除 MCP 服务器',dependencies=[Depends(RequestPermission('sys:mcp:del')), DependsRBAC])
async def delete_mcp(pk: Annotated[int, Path(description='MCP ID')]) -> ResponseModel:
count = await mcp_service.delete(pk=pk)
if count > 0:
return response_base.success()
return response_base.fail()


@MCPRouter.post('/chat',summary='MCP ChatGPT', dependencies=[Depends(RequestPermission('sys:mcp:chat')), DependsRBAC])
async def mcp_chat(obj: McpChatParam) -> StreamingResponse:
data = await mcp_service.chat(obj=obj)
return StreamingResponse(data, media_type='text/event-stream')
121 changes: 42 additions & 79 deletions backend/app/api/v1/module_ai/mcp/crud.py
Original file line number Diff line number Diff line change
@@ -1,81 +1,44 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sqlalchemy import Select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus

from .model import Mcp
from .schema import CreateMcpParam, UpdateMcpParam


class CRUDMcp(CRUDPlus[Mcp]):
"""MCP 服务器数据库操作类"""

async def get(self, db: AsyncSession, pk: int) -> Mcp | None:
"""
获取 MCP 服务器

:param db: 数据库会话
:param pk: MCP ID
:return:
"""
return await self.select_model(db, pk)

async def get_by_name(self, db: AsyncSession, name: str) -> Mcp | None:
"""
通过名称获取 MCP 服务器

:param db: 数据库会话
:param name: MCP 名称
:return:
"""
return await self.select_model_by_column(db, name=name)

async def get_list(self, name: str | None, type: int | None) -> Select[Mcp]:
"""
获取 MCP 服务器列表

:param name: MCP 名称
:param type: MCP 类型
:return:
"""
filters = {}
if name is not None:
filters.update(name__like=f'%{name}%')
if type is not None:
filters.update(type=type)
return await self.select_order('created_time', 'desc', **filters)

async def create(self, db: AsyncSession, obj: CreateMcpParam) -> None:
"""
创建 MCP 服务器

:param db: 数据库会话
:param obj: 创建 MCP 服务器参数
:return:
"""
await self.create_model(db, obj)

async def update(self, db: AsyncSession, pk: int, obj: UpdateMcpParam) -> int:
"""
更新 MCP 服务器

:param db: 数据库会话
:param pk: MCP ID
:param obj: 更新 MCP 服务器参数
:return:
"""
return await self.update_model(db, pk, obj)

async def delete(self, db: AsyncSession, pk: int) -> int:
"""
删除 MCP 服务器

:param db: 数据库会话
:param pk: MCP ID
:return:
"""
return await self.delete_model(db, pk)


mcp_dao: CRUDMcp = CRUDMcp(Mcp)
from typing import Dict, List, Optional, Sequence

from app.core.base_crud import CRUDBase
from app.api.v1.module_system.auth.schema import AuthSchema
from .model import McpModel
from .schema import McpCreateSchema, McpUpdateSchema


class McpCRUD(CRUDBase[McpModel, McpCreateSchema, McpUpdateSchema]):
"""MCP 服务器数据层"""

def __init__(self, auth: AuthSchema) -> None:
"""初始化CRUD"""
self.auth = auth
super().__init__(model=McpModel(), auth=auth)

async def get_by_id_crud(self, id: int) -> Optional[McpModel]:
"""详情"""
return await self.get(id=id)

async def get_by_name_crud(self, name: str) -> Optional[McpModel]:
"""通过名称获取MCP服务器"""
return await self.get(name=name)

async def get_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Sequence[McpModel]:
"""列表查询"""
return await self.list(search=search or {}, order_by=order_by or [{'id': 'asc'}])

async def create_crud(self, data: McpCreateSchema) -> Optional[McpModel]:
"""创建"""
return await self.create(data=data)

async def update_crud(self, id: int, data: McpUpdateSchema) -> Optional[McpModel]:
"""更新"""
return await self.update(id=id, data=data)

async def delete_crud(self, ids: List[int]) -> None:
"""批量删除"""
return await self.delete(ids=ids)


mcp_crud: McpCRUD = McpCRUD(auth=AuthSchema())
20 changes: 12 additions & 8 deletions backend/app/api/v1/module_ai/mcp/model.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
# -*- coding: utf-8 -*-

from sqlalchemy import JSON, String
from typing import Optional, Dict, Any
from sqlalchemy import JSON, String, Integer
from sqlalchemy.orm import Mapped, mapped_column

from app.core.base_model import CreatorMixin

class Mcp(CreatorMixin):
"""MCP 服务器表"""

class McpModel(CreatorMixin):
"""
MCP 服务器表
"""

__tablename__ = 'ai_mcp'
__table_args__ = ({'comment': 'MCP 服务器表'})

name: Mapped[str] = mapped_column(String(50), unique=True, comment='MCP 名称')
type: Mapped[int] = mapped_column(default=0, comment='MCP 类型(0stdio 1sse)')
url: Mapped[str | None] = mapped_column(String(255), default=None, comment='远程 SSE 地址')
command: Mapped[str | None] = mapped_column(String(255), default=None, comment='MCP 命令')
args: Mapped[str | None] = mapped_column(String(255), default=None, comment='MCP 命令参数')
env: Mapped[str | None] = mapped_column(JSON(), default=None, comment='MCP 环境变量')
type: Mapped[int] = mapped_column(Integer, default=0, comment='MCP 类型(0:stdio 1:sse)')
url: Mapped[Optional[str]] = mapped_column(String(255), default=None, comment='远程 SSE 地址')
command: Mapped[Optional[str]] = mapped_column(String(255), default=None, comment='MCP 命令')
args: Mapped[Optional[str]] = mapped_column(String(255), default=None, comment='MCP 命令参数')
env: Mapped[Optional[Dict[str, Any]]] = mapped_column(JSON(), default=None, comment='MCP 环境变量')
Loading